feat: add governed postbox delivery and report hardening

This commit is contained in:
2026-07-29 14:16:28 +02:00
parent f11c56e890
commit 5240749ae1
47 changed files with 5538 additions and 288 deletions

View File

@@ -1,5 +1,9 @@
import { useState } from "react";
import { useEffect, useMemo, useState } from "react";
import type { ApiSettings, AuthInfo } from "../../types";
import {
getCampaignPostboxCatalog,
type CampaignPostboxCatalog
} from "../../api/campaigns";
import { Button } from "@govoplan/core-webui";
import { Card } from "@govoplan/core-webui";
import { FormField } from "@govoplan/core-webui";
@@ -14,10 +18,15 @@ import VersionLine from "./components/VersionLine";
import { ToggleSwitch } from "@govoplan/core-webui";
import { hasScope } from "@govoplan/core-webui";
import { RetentionPolicyEditor } from "@govoplan/core-webui";
import { usePlatformModuleInstalled } from "@govoplan/core-webui";
import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
import { useCampaignDraftEditor } from "./hooks/useCampaignDraftEditor";
import { asRecord, isAuditLockedVersion } from "./utils/campaignView";
import { cloneJson, getBool, getNumber, getText, updateNested } from "./utils/draftEditor";
import { getDraftFields } from "./utils/fieldDefinitions";
import PostboxTargetsDialog, {
normalizePostboxTargets
} from "./components/PostboxTargetsDialog";
const behaviorOptions = ["block", "ask", "drop", "continue", "warn"];
@@ -34,6 +43,14 @@ type GlobalSettingsPageProps = {
export default function GlobalSettingsPage({ settings, auth, campaignId, view = "settings" }: GlobalSettingsPageProps) {
const { data, loading, error, reload, setError } = useCampaignWorkspaceData(settings, campaignId);
const [editorState, setEditorState] = useState<EditorState>({});
const [postboxCatalog, setPostboxCatalog] = useState<CampaignPostboxCatalog>({
available: false,
postboxes: [],
templates: [],
organization_units: []
});
const [postboxTargetsOpen, setPostboxTargetsOpen] = useState(false);
const postboxModuleInstalled = usePlatformModuleInstalled("postbox");
const isPolicyView = view === "policy";
const version = data.currentVersion;
@@ -59,12 +76,53 @@ export default function GlobalSettingsPage({ settings, auth, campaignId, view =
const delivery = asRecord(displayDraft.delivery);
const rateLimit = asRecord(delivery.rate_limit);
const retry = asRecord(delivery.retry);
const postboxDelivery = asRecord(delivery.postbox);
const fieldDefinitions = useMemo(
() => getDraftFields(displayDraft),
[displayDraft]
);
const statusTracking = asRecord(displayDraft.status_tracking);
const optIns = asRecord(editorState.opt_ins);
const canReadRetentionPolicy = hasScope(auth, "admin:policies:read");
const canWriteRetentionPolicy = hasScope(auth, "admin:policies:write");
const pageTitle = isPolicyView ? "i18n:govoplan-campaign.campaign_policies.0b5de1f5" : "i18n:govoplan-campaign.campaign_settings.efffec26";
useEffect(() => {
if (!postboxModuleInstalled) {
setPostboxCatalog({
available: false,
postboxes: [],
templates: [],
organization_units: []
});
return;
}
let cancelled = false;
void getCampaignPostboxCatalog(settings, campaignId)
.then((catalog) => {
if (!cancelled) setPostboxCatalog(catalog);
})
.catch(() => {
if (!cancelled) {
setPostboxCatalog({
available: false,
postboxes: [],
templates: [],
organization_units: []
});
}
});
return () => {
cancelled = true;
};
}, [
campaignId,
postboxModuleInstalled,
settings.accessToken,
settings.apiBaseUrl,
settings.apiKey
]);
function patchEditor(path: string[], value: unknown) {
if (locked) return;
setEditorState((current) => updateNested(current, path, value));
@@ -232,6 +290,43 @@ export default function GlobalSettingsPage({ settings, auth, campaignId, view =
<FormField label="i18n:govoplan-campaign.max_attempts.f684fef4"><input type="number" min={1} value={getNumber(retry, "max_attempts", 3)} disabled={locked} onChange={(event) => patch(["delivery", "retry", "max_attempts"], Number(event.target.value || 1))} /></FormField>
<ToggleSwitch label="i18n:govoplan-campaign.status_tracking.15f61f88" checked={getBool(statusTracking, "enabled", true)} disabled={locked} onChange={(checked) => patch(["status_tracking", "enabled"], checked)} />
</div>
{postboxModuleInstalled &&
<div className="campaign-postbox-delivery-settings">
<FormField label="Delivery channels">
<select
value={getText(delivery, "channel_policy", "mail")}
disabled={locked}
onChange={(event) => patch(["delivery", "channel_policy"], event.target.value)}
>
<option value="mail">Mail</option>
<option value="postbox">Postbox</option>
<option value="mail_and_postbox">Mail and Postbox</option>
<option value="mail_then_postbox">Mail, then Postbox on pre-acceptance rejection</option>
<option value="postbox_then_mail">Postbox, then Mail on pre-acceptance rejection</option>
</select>
</FormField>
<FormField label="Postbox classification">
<input
value={getText(postboxDelivery, "classification", "internal")}
disabled={locked}
onChange={(event) => patch(["delivery", "postbox", "classification"], event.target.value)}
/>
</FormField>
<FormField label="Default Postbox targets">
<Button
disabled={locked || !postboxCatalog.available}
onClick={() => setPostboxTargetsOpen(true)}
>
Configure ({normalizePostboxTargets(postboxDelivery.targets).length})
</Button>
</FormField>
{!postboxCatalog.available &&
<DismissibleAlert tone="warning" dismissible={false}>
Postbox delivery is installed but its delivery catalog is unavailable.
</DismissibleAlert>
}
</div>
}
</Card>
<Card title="i18n:govoplan-campaign.opt_ins_and_local_assistance.d0d23635" collapsible>
@@ -246,6 +341,21 @@ export default function GlobalSettingsPage({ settings, auth, campaignId, view =
</>
}
</LoadingFrame>
{postboxTargetsOpen &&
<PostboxTargetsDialog
open
title="Default Postbox targets"
catalog={postboxCatalog}
fields={fieldDefinitions}
targets={normalizePostboxTargets(postboxDelivery.targets)}
locked={locked}
onSave={(targets) => {
patch(["delivery", "postbox", "targets"], targets);
setPostboxTargetsOpen(false);
}}
onClose={() => setPostboxTargetsOpen(false)}
/>
}
</div>);
}