feat: initialize governed postbox module

This commit is contained in:
2026-07-29 14:16:29 +02:00
parent 19216e5043
commit d848a9a503
31 changed files with 9776 additions and 1 deletions
@@ -0,0 +1,90 @@
import { useCallback } from "react";
import { Inbox, Paperclip } from "lucide-react";
import { Link } from "react-router-dom";
import {
DashboardWidgetList,
DismissibleAlert,
LoadingFrame,
useDashboardWidgetData,
type ApiSettings,
type DashboardWidgetConfiguration
} from "@govoplan/core-webui";
import {
listPostboxMessages,
listPostboxes
} from "../../api/postbox";
export default function PostboxInboxWidget({
settings,
refreshKey,
configuration
}: {
settings: ApiSettings;
refreshKey: number;
configuration: DashboardWidgetConfiguration;
}) {
const maxItems = numberSetting(configuration.maxItems, 5, 1, 12);
const load = useCallback(async () => {
const postboxes = await listPostboxes(settings);
if (!postboxes.length) {
return { messages: [], total: 0 };
}
return listPostboxMessages(
settings,
postboxes.map((postbox) => postbox.id),
maxItems,
0,
"",
"unread"
);
}, [maxItems, settings]);
const { data, loading, error } = useDashboardWidgetData(load, refreshKey);
return (
<LoadingFrame loading={loading} label="Loading unread Postbox messages">
{error && (
<DismissibleAlert tone="warning" resetKey={error}>
{error}
</DismissibleAlert>
)}
<DashboardWidgetList
emptyText="No unread Postbox messages."
items={(data?.messages ?? []).map((message) => ({
id: message.id,
title: message.subject,
detail: message.sender_label || message.producer_module || "Postbox",
meta: new Intl.DateTimeFormat(undefined, {
day: "2-digit",
month: "short",
hour: "2-digit",
minute: "2-digit"
}).format(new Date(message.delivered_at)),
leading: <Inbox size={17} aria-hidden="true" />,
trailing: message.attachments.length ? (
<span title={`${message.attachments.length} attachments`}>
<Paperclip size={15} aria-hidden="true" />
</span>
) : undefined,
to: `/postbox?message=${encodeURIComponent(message.id)}`
}))}
/>
<div className="dashboard-contribution-footer">
<Link className="btn btn-secondary" to="/postbox">
{data?.total ? `Open Postbox (${data.total} unread)` : "Open Postbox"}
</Link>
</div>
</LoadingFrame>
);
}
function numberSetting(
value: unknown,
fallback: number,
minimum: number,
maximum: number
): number {
const numeric = typeof value === "number" ? value : Number(value);
return Number.isFinite(numeric)
? Math.max(minimum, Math.min(maximum, Math.floor(numeric)))
: fallback;
}