feat: consolidate shared UI and harden browser authority for release

This commit is contained in:
2026-09-08 01:35:05 +02:00
parent ac40774785
commit b75ca34295
143 changed files with 8664 additions and 752 deletions
@@ -0,0 +1,66 @@
import { useCallback, useState } from "react";
import { ConcurrencyConflictProvider } from "../src/components/ConcurrencyConflictDialog";
import { PlatformLanguageProvider } from "../src/i18n/LanguageContext";
import { useCampaignDraftEditor } from "../../../govoplan-campaign/webui/src/features/campaigns/hooks/useCampaignDraftEditor";
import { getCampaignVersion, type CampaignVersionDetail } from "../../../govoplan-campaign/webui/src/api/campaigns";
import {
HeaderAddressEditorDialog, RecipientAddressEditorDialog, entryWithAddressValues, getAddressColumn,
type HeaderAddressValues
} from "../../../govoplan-campaign/webui/src/features/campaigns/recipients/RecipientAddressEditor";
import { generatedTranslations } from "../../../govoplan-campaign/webui/src/i18n/generatedTranslations";
import "../../../govoplan-campaign/webui/src/styles/campaign-workspace.css";
const settings = { apiBaseUrl: "", accessToken: "", apiKey: "" };
const initialAddresses = [
{ name: "Alpha", email: "alpha@example.test" },
{ name: "Beta", email: "beta@example.test" },
{ name: "Zulu", email: "zulu@example.test" }
];
const initialVersion: CampaignVersionDetail = {
id: "order-version", campaign_id: "order-campaign", version_number: 1, edit_revision: 1, strong_etag: '"order-version:1"',
current_flow: "manual", current_step: "recipients", workflow_state: "editing", is_complete: false,
raw_json: {
campaign: { name: "Recipient ordering" }, server: {},
recipients: { allow_individual_to: true, to: initialAddresses },
entries: { defaults: {}, inline: [{ id: "entry-1", name: "Alpha", email: "alpha@example.test", to: initialAddresses }] }
}, editor_state: {}, updated_at: "2026-09-07T10:00:00Z"
};
export default function CampaignRecipientOrderScenario() {
return <PlatformLanguageProvider preferredLanguageCode={new URLSearchParams(window.location.search).get("language") ?? "en"} moduleTranslations={[generatedTranslations]}>
<ConcurrencyConflictProvider><RecipientOrderEditor /></ConcurrencyConflictProvider>
</PlatformLanguageProvider>;
}
function RecipientOrderEditor() {
const [version, setVersion] = useState(initialVersion);
const [error, setError] = useState("");
const [dialog, setDialog] = useState<"entry" | "header" | null>(null);
const reload = useCallback(async () => setVersion(await getCampaignVersion(settings, "order-campaign", "order-version")), []);
const editor = useCampaignDraftEditor({ settings, campaignId: "order-campaign", version, locked: false,
reload, setError, currentStep: "recipients", unsavedTitle: "Unsaved recipient order", unsavedMessage: "Save or discard the campaign draft?" });
const entries = editor.displayDraft.entries as { defaults: Record<string, unknown>; inline: Record<string, unknown>[] };
const recipients = editor.displayDraft.recipients as Record<string, unknown>;
const entry = entries?.inline?.[0] ?? {};
return <main>
<button onClick={() => setDialog("entry")}>Edit individual addresses</button>
<button onClick={() => setDialog("header")}>Edit global addresses</button>
<button disabled={!editor.dirty || editor.saving} onClick={() => void editor.saveDraft()}>Save campaign</button>
<output data-testid="recipient-order">{JSON.stringify(entry.to)}</output>
<output data-testid="global-order">{JSON.stringify(recipients?.to)}</output>
<output data-testid="primary-address">{String(entry.email ?? "")}</output>
<output data-testid="recipient-dirty">{String(editor.dirty)}</output>
<output data-testid="recipient-error">{editor.localError || error}</output>
{dialog === "entry" && <RecipientAddressEditorDialog
entry={entry} index={0} locked={false} recipientsSection={recipients} entryDefaults={entries.defaults ?? {}}
onSave={(values, merges) => {
// Same owning helper as RecipientDataPage; no test copy of ordering or
// legacy primary-address/merge-field normalization.
editor.patch(["entries", "inline"], [entryWithAddressValues(entry, values, merges)]);
setDialog(null);
}} onClose={() => setDialog(null)} />}
{dialog === "header" && <HeaderAddressEditorDialog title="Global To addresses"
columns={[getAddressColumn("to")]} values={{ to: recipients.to } as HeaderAddressValues} locked={false}
onSave={(values) => { editor.patch(["recipients", "to"], values.to); setDialog(null); }} onClose={() => setDialog(null)} />}
</main>;
}