From 6cdd8040391b7307be041f07267c20e4275bd21c Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Mon, 3 Aug 2026 13:28:05 +0200 Subject: [PATCH] Migrate Distribution Lists interface patterns --- docs/INTERFACE_PATTERN_MIGRATION.md | 32 +++ src/govoplan_dist_lists/backend/manifest.py | 47 +++- .../test_interface_documentation_contract.py | 47 ++++ .../src/components/DistributionListPicker.tsx | 3 +- .../DistributionListsPage.tsx | 203 ++++++++++++++---- .../distributionLists/interfacePatterns.ts | 21 ++ webui/src/i18n/generatedTranslations.ts | 141 ++++++++++++ webui/src/module.ts | 13 +- 8 files changed, 462 insertions(+), 45 deletions(-) create mode 100644 docs/INTERFACE_PATTERN_MIGRATION.md create mode 100644 tests/test_interface_documentation_contract.py create mode 100644 webui/src/features/distributionLists/interfacePatterns.ts create mode 100644 webui/src/i18n/generatedTranslations.ts diff --git a/docs/INTERFACE_PATTERN_MIGRATION.md b/docs/INTERFACE_PATTERN_MIGRATION.md new file mode 100644 index 0000000..411cf16 --- /dev/null +++ b/docs/INTERFACE_PATTERN_MIGRATION.md @@ -0,0 +1,32 @@ +# Distribution Lists Interface Pattern Migration + +This migration applies the GovOPlaN interface pattern language to the +distribution-list catalogue, revision editor, expansion preview, snapshot +evidence, and reusable picker. + +## Surface Inventory + +| Surface | Archetype | Consequence class | Contract | +| --- | --- | --- | --- | +| `/distribution-lists` catalogue | Governed work queue | Select, create, or delete a list | Shared loading, empty, permission, disabled-reason, and help states | +| Definition editor | Consequential definition editor | Save immutable revision | Guarded list and nested-entry drafts with contextual field semantics | +| Expansion preview | Evidence preview | Resolve current saved revision | Explicit exclusions, provider availability, provenance, and decision explanations | +| Frozen snapshots | Evidence register | Retain exact expansion | Immutable recipients, exclusions, source revisions, parameters, provenance, and hash | +| Distribution-list picker | Shared object selector | Reference a list | Searchable selector with contextual definition help | + +## Consequence And Availability Rules + +- Saving creates an immutable list revision; previews and snapshots use only a + saved revision. +- Include, exclude, and manual-override entries have distinct semantics. Manual + override requires an auditable reason. +- Freezing is confirmed because it creates retained evidence consumed by other + modules. Deleting the reusable definition does not rewrite retained snapshots. +- Provider absence is explained and never turns an optional integration into a + hard dependency. +- Missing write permission and incomplete or unchanged drafts expose a reason + and required next action instead of a silent disabled control. + +Backend and WebUI manifests publish the same surface identifiers. English and +German catalogues cover the module-owned interaction vocabulary, and contextual +help resolves through manifest documentation rather than detached UI guidance. diff --git a/src/govoplan_dist_lists/backend/manifest.py b/src/govoplan_dist_lists/backend/manifest.py index c7c6c65..47cc4aa 100644 --- a/src/govoplan_dist_lists/backend/manifest.py +++ b/src/govoplan_dist_lists/backend/manifest.py @@ -100,7 +100,15 @@ DOCUMENTATION = ( documentation_types=("admin", "user"), audience=("operator", "module_admin", "product_owner"), related_modules=("addresses", "campaigns", "mail", "postbox", "notifications", "scheduling", "poll", "workflow_engine", "tasks"), - metadata={"seed": True}, + metadata={ + "seed": True, + "help_contexts": [ + "dist_lists.page", + "dist_lists.editor", + "dist_lists.preview", + "dist_lists.picker", + ], + }, ), DocumentationTopic( id=f"{MODULE_ID}.address-contact-resolution", @@ -138,6 +146,43 @@ DOCUMENTATION = ( related_modules=("identity", "idm"), metadata={"seed": True}, ), + DocumentationTopic( + id=f"{MODULE_ID}.reference.fields-and-consequences", + title="Distribution-list fields and consequences", + summary="Definition types, audience modes, effective dates, channel requests, expansion, and frozen-snapshot consequences.", + body=( + "Static definitions contain explicit entries; parameterized and dynamic definitions accept typed values or provider results; " + "templates are reusable definitions that cannot run on their own. Include entries add audiences, exclusions remove them, and " + "manual overrides require an auditable reason. Effective dates constrain when an entry participates. Requested channels narrow " + "candidate delivery methods but do not bypass Policy, consent, suppression, or provider decisions. Saving creates an immutable " + "revision. Previewing explains current recipients and exclusions without retaining evidence. Freezing records the exact revision, " + "parameters, provider revisions, recipients, exclusions, provenance, and expansion hash as immutable consumer evidence. Deleting " + "a definition does not rewrite frozen snapshots retained under their governing policy." + ), + layer="available", + documentation_types=("admin", "user"), + audience=("operator", "module_admin", "product_owner"), + related_modules=("addresses", "campaigns", "dataflow", "idm", "policy", "reporting", "workflow_engine"), + metadata={ + "seed": True, + "help_contexts": [ + "dist_lists.field.definition-kind", + "dist_lists.field.source", + "dist_lists.field.entry-mode", + "dist_lists.field.effective-period", + "dist_lists.field.requested-channels", + "dist_lists.action.expand", + "dist_lists.action.freeze", + "dist_lists.action.delete", + ], + "consequence_classes": { + "save_revision": "Creates a new immutable revision while retaining prior revisions for existing evidence.", + "expand_preview": "Resolves the saved revision without creating retained delivery evidence.", + "freeze_snapshot": "Persists exact expansion inputs, decisions, recipients, exclusions, provenance, and hash.", + "delete_definition": "Retires the reusable definition without rewriting retained frozen snapshots.", + }, + }, + ), ) diff --git a/tests/test_interface_documentation_contract.py b/tests/test_interface_documentation_contract.py new file mode 100644 index 0000000..2c2ad26 --- /dev/null +++ b/tests/test_interface_documentation_contract.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +import unittest + +from govoplan_dist_lists.backend.manifest import manifest + + +class DistributionListsInterfaceDocumentationContractTests(unittest.TestCase): + def test_route_and_surfaces_remain_declared(self) -> None: + frontend = manifest.frontend + self.assertIsNotNone(frontend) + self.assertEqual( + {"/distribution-lists"}, + {item.path for item in frontend.routes}, # type: ignore[union-attr] + ) + self.assertEqual( + { + "dist_lists.page", + "dist_lists.editor", + "dist_lists.preview", + "dist_lists.picker", + }, + {item.id for item in frontend.view_surfaces}, # type: ignore[union-attr] + ) + + def test_help_and_consequence_metadata_remain_published(self) -> None: + topics = {topic.id: topic for topic in manifest.documentation} + boundary = topics["dist_lists.boundary"] + reference = topics["dist_lists.reference.fields-and-consequences"] + + self.assertIn("dist_lists.editor", boundary.metadata["help_contexts"]) + self.assertIn( + "dist_lists.field.entry-mode", + reference.metadata["help_contexts"], + ) + self.assertIn( + "freeze_snapshot", + reference.metadata["consequence_classes"], + ) + self.assertIn( + "delete_definition", + reference.metadata["consequence_classes"], + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/webui/src/components/DistributionListPicker.tsx b/webui/src/components/DistributionListPicker.tsx index 857c038..ccac5ca 100644 --- a/webui/src/components/DistributionListPicker.tsx +++ b/webui/src/components/DistributionListPicker.tsx @@ -9,6 +9,7 @@ import { listDistributionLists, type DistributionList } from "../api/distLists"; +import { DIST_LISTS_FIELD_DOCUMENTATION } from "../features/distributionLists/interfacePatterns"; export type DistributionListPickerProps = { settings: ApiSettings; @@ -47,7 +48,7 @@ export default function DistributionListPicker({ }, [settings]); return ( - + ("static"); const [deleteOpen, setDeleteOpen] = useState(false); + const [freezeOpen, setFreezeOpen] = useState(false); const [entryEditor, setEntryEditor] = useState(null); + const [entryEditorBaselineKey, setEntryEditorBaselineKey] = useState(""); + const [discardEntryOpen, setDiscardEntryOpen] = useState(false); const [providerUnavailable, setProviderUnavailable] = useState([]); const providerCache = useRef(new Map()); const [preview, setPreview] = useState(null); @@ -125,6 +134,9 @@ export default function DistributionListsPage({ settings, auth }: Props) { const canWrite = hasScope(auth, "dist_lists:list:write") || hasScope(auth, "dist_lists:list:admin"); const dirty = Boolean(selected && draftKey(draft) !== savedDraftKey); + const entryEditorDirty = Boolean( + entryEditor && entryEditorKey(entryEditor) !== entryEditorBaselineKey + ); const applyItem = useCallback((item: DistributionList | null) => { const next = item ? draftFromItem(item) : emptyDraft(); @@ -179,8 +191,8 @@ export default function DistributionListsPage({ settings, auth }: Props) { }); }; - const createItem = async () => { - if (!createName.trim()) return; + const createItem = async (): Promise => { + if (!createName.trim()) return false; setBusy(true); setError(""); try { @@ -194,20 +206,23 @@ export default function DistributionListsPage({ settings, auth }: Props) { setCreateKind("static"); setSuccess(`Created ${created.name}.`); await reload(created.id); + return true; } catch (caught) { setError(errorMessage(caught)); + return false; } finally { setBusy(false); } }; - const saveItem = async () => { - if (!selected || !draft.name.trim()) return false; + const persistDraft = async (nextDraft: ListDraft) => { + if (!selected || !nextDraft.name.trim()) return false; setBusy(true); setError(""); try { - const updated = await updateDistributionList(settings, selected, payloadFromDraft(draft)); + const updated = await updateDistributionList(settings, selected, payloadFromDraft(nextDraft)); setSuccess(`Saved revision ${updated.current_revision}.`); + setEntryEditor(null); await reload(updated.id); return true; } catch (caught) { @@ -218,12 +233,67 @@ export default function DistributionListsPage({ settings, auth }: Props) { } }; + const saveItem = async () => persistDraft(draft); + + const saveItemWithEntryEditor = async () => { + if (!entryEditor) return saveItem(); + const nextDraft = draftWithEditorEntry(draft, entryEditor); + if (!nextDraft) return false; + return persistDraft(nextDraft); + }; + useUnsavedDraftGuard({ dirty, onSave: saveItem, - onDiscard: () => applyItem(selected) + onDiscard: () => applyItem(selected), + title: "i18n:govoplan-dist-lists.unsaved_title", + message: "i18n:govoplan-dist-lists.unsaved_message" }); + useUnsavedDraftGuard({ + dirty: Boolean(createOpen && createName), + onSave: createItem, + onDiscard: () => { + setCreateOpen(false); + setCreateName(""); + setCreateKind("static"); + }, + title: "i18n:govoplan-dist-lists.unsaved_title", + message: "i18n:govoplan-dist-lists.unsaved_message" + }); + + useUnsavedDraftGuard({ + dirty: entryEditorDirty, + onSave: saveItemWithEntryEditor, + onDiscard: () => { + applyItem(selected); + setEntryEditor(null); + }, + title: "i18n:govoplan-dist-lists.entry_unsaved_title", + message: "i18n:govoplan-dist-lists.entry_unsaved_message" + }); + + const closeCreate = () => { + if (busy) return; + if (createName) requestDiscard(() => setCreateOpen(false)); + else setCreateOpen(false); + }; + + const openEntryEditor = (next: EntryEditorState) => { + setEntryEditor(next); + setEntryEditorBaselineKey(entryEditorKey(next)); + setDiscardEntryOpen(false); + }; + + const closeEntryEditor = () => { + if (busy) return; + if (entryEditorDirty) { + setDiscardEntryOpen(true); + return; + } + setEntryEditor(null); + }; + const removeItem = async () => { if (!selected) return; setBusy(true); @@ -340,11 +410,12 @@ export default function DistributionListsPage({ settings, auth }: Props) { icon={} variant="ghost" disabled={!canWrite} - onClick={() => setEntryEditor(editorFromEntry(entry, index))} + disabledReason={!canWrite ? DIST_LISTS_I18N.writeReason : undefined} + onClick={() => openEntryEditor(editorFromEntry(entry, index))} /> setEntryEditor(emptyEntryEditor(index + 1))} + onAddBelow={() => openEntryEditor(emptyEntryEditor(index + 1))} onRemove={() => removeEntry(index)} onMoveUp={index > 0 ? () => moveEntry(index, index - 1) : undefined} onMoveDown={index < draft.entries.length - 1 ? () => moveEntry(index, index + 1) : undefined} @@ -450,19 +521,9 @@ export default function DistributionListsPage({ settings, auth }: Props) { function saveEntryEditor() { if (!entryEditor) return; - const entry = entryFromEditor(entryEditor); - if (!entry) return; - setDraft((current) => { - const entries = [...current.entries]; - if (entryEditor.index !== null && entryEditor.index < entries.length) { - entries[entryEditor.index] = entry; - } else if (entryEditor.insertAt !== null) { - entries.splice(Math.min(entryEditor.insertAt, entries.length), 0, entry); - } else { - entries.push(entry); - } - return { ...current, entries }; - }); + const nextDraft = draftWithEditorEntry(draft, entryEditor); + if (!nextDraft) return; + setDraft(nextDraft); setEntryEditor(null); } @@ -478,6 +539,7 @@ export default function DistributionListsPage({ settings, auth }: Props) { icon={} variant="ghost" disabled={loading || busy} + disabledReason={loading ? DIST_LISTS_I18N.loading : busy ? DIST_LISTS_I18N.busy : undefined} onClick={() => void reload(selectedId)} /> } variant="primary" disabled={!canWrite} - onClick={() => setCreateOpen(true)} + disabledReason={!canWrite ? DIST_LISTS_I18N.writeReason : undefined} + onClick={() => requestDiscard(() => setCreateOpen(true))} /> @@ -526,6 +589,7 @@ export default function DistributionListsPage({ settings, auth }: Props) { {selected ? Revision {selected.current_revision} · {selected.scope_type} scope : null} + ariaLabel="Distribution-list workspace" value={view} @@ -539,6 +603,7 @@ export default function DistributionListsPage({ settings, auth }: Props) { @@ -671,18 +737,18 @@ export default function DistributionListsPage({ settings, auth }: Props) { !busy && setCreateOpen(false)} + onClose={closeCreate} closeDisabled={busy} footer={( <> - - + + )} >
- setCreateName(event.target.value)} /> - + setCreateName(event.target.value)} /> + onChange({ ...draft, name: event.target.value })} /> - + - + onChange({ ...state, mode: event.target.value as EntryMode })}> @@ -920,14 +1012,14 @@ function EntryEditorDialog({ onChange({ ...state, label: event.target.value })} /> onChange({ ...state, purpose: event.target.value })} placeholder="All purposes" /> {state.mode === "override" ? ( - + onChange({ ...state, overrideReason: event.target.value })} required /> ) : null} - + onChange({ ...state, effectiveFrom: event.target.value })} /> - + onChange({ ...state, effectiveUntil: event.target.value })} /> {isAddressProviderEntry(state) ? ( @@ -1198,6 +1290,35 @@ function entryFromEditor(editor: EntryEditorState): EntryDraft | null { }; } +function draftWithEditorEntry(draft: ListDraft, editor: EntryEditorState): ListDraft | null { + const entry = entryFromEditor(editor); + if (!entry) return null; + const entries = [...draft.entries]; + if (editor.index !== null && editor.index < entries.length) { + entries[editor.index] = { + ...entry, + clientId: entries[editor.index]?.clientId ?? entry.clientId + }; + } else if (editor.insertAt !== null) { + entries.splice(Math.min(editor.insertAt, entries.length), 0, entry); + } else { + entries.push(entry); + } + return { ...draft, entries }; +} + +function entryEditorKey(editor: EntryEditorState): string { + return JSON.stringify(editor, (_key, value) => { + if (value && typeof value === "object" && !Array.isArray(value)) { + return Object.fromEntries( + Object.entries(value as Record) + .sort(([left], [right]) => left.localeCompare(right)) + ); + } + return value; + }); +} + function directKind(mode: DirectSourceMode): EntryKind { if (mode === "email") return "raw_email"; if (mode === "postal") return "raw_postal_address"; diff --git a/webui/src/features/distributionLists/interfacePatterns.ts b/webui/src/features/distributionLists/interfacePatterns.ts new file mode 100644 index 0000000..77f7a7a --- /dev/null +++ b/webui/src/features/distributionLists/interfacePatterns.ts @@ -0,0 +1,21 @@ +import type { DocumentationHelpReference } from "@govoplan/core-webui"; + +export const DIST_LISTS_DOCUMENTATION = { + topicId: "dist_lists.boundary", + documentationType: "user" +} satisfies DocumentationHelpReference; + +export const DIST_LISTS_FIELD_DOCUMENTATION = { + topicId: "dist_lists.reference.fields-and-consequences", + documentationType: "admin" +} satisfies DocumentationHelpReference; + +export const DIST_LISTS_I18N = { + loading: "i18n:govoplan-dist-lists.loading_reason", + busy: "i18n:govoplan-dist-lists.busy_reason", + writeReason: "i18n:govoplan-dist-lists.write_permission_reason", + noSelection: "i18n:govoplan-dist-lists.no_selection_reason", + noChanges: "i18n:govoplan-dist-lists.no_changes_reason", + incomplete: "i18n:govoplan-dist-lists.incomplete_reason", + saveBeforeExpansion: "i18n:govoplan-dist-lists.save_before_expansion" +} as const; diff --git a/webui/src/i18n/generatedTranslations.ts b/webui/src/i18n/generatedTranslations.ts new file mode 100644 index 0000000..b335d25 --- /dev/null +++ b/webui/src/i18n/generatedTranslations.ts @@ -0,0 +1,141 @@ +import type { PlatformTranslations } from "@govoplan/core-webui"; + +const en = { + "i18n:govoplan-dist-lists.distribution_lists": "Distribution Lists", + "i18n:govoplan-dist-lists.editor": "Distribution-list editor", + "i18n:govoplan-dist-lists.preview": "Distribution-list expansion preview", + "i18n:govoplan-dist-lists.picker": "Distribution-list picker", + "i18n:govoplan-dist-lists.loading_reason": "Distribution Lists are still loading.", + "i18n:govoplan-dist-lists.busy_reason": "Another Distribution List action is still running.", + "i18n:govoplan-dist-lists.write_permission_reason": "Your account may not create or change Distribution Lists.", + "i18n:govoplan-dist-lists.no_selection_reason": "Select or create a Distribution List first.", + "i18n:govoplan-dist-lists.no_changes_reason": "There are no definition changes to save.", + "i18n:govoplan-dist-lists.incomplete_reason": "Complete the required name, source, and override-reason fields first.", + "i18n:govoplan-dist-lists.save_before_expansion": "Save the current revision before expanding or freezing it.", + "i18n:govoplan-dist-lists.unsaved_title": "Unsaved Distribution List", + "i18n:govoplan-dist-lists.unsaved_message": "Save or discard the Distribution List draft before leaving this surface.", + "i18n:govoplan-dist-lists.entry_unsaved_title": "Unsaved audience entry", + "i18n:govoplan-dist-lists.entry_unsaved_message": "Save the complete Distribution List revision or discard all changes before leaving this surface.", + "i18n:govoplan-dist-lists.entry_discard_message": "Discard the changes made in this audience entry? Other changes to the Distribution List draft are kept.", + "i18n:govoplan-dist-lists.snapshot_title": "Freeze recipient snapshot", + "i18n:govoplan-dist-lists.snapshot_message": "Freeze this expansion? The exact recipients, exclusions, source revisions, decisions, provenance, and hash become immutable evidence for consumers.", + "Distribution Lists": "Distribution Lists", + "Refresh": "Refresh", + "New distribution list": "New distribution list", + "Search lists": "Search lists", + "Search distribution lists": "Search distribution lists", + "No distribution lists": "No distribution lists", + "No distribution list selected": "No distribution list selected", + "Definition": "Definition", + "Preview": "Preview", + "Snapshots": "Snapshots", + "Save revision": "Save revision", + "Delete distribution list": "Delete distribution list", + "Purpose": "Purpose", + "Expand preview": "Expand preview", + "Freeze snapshot": "Freeze snapshot", + "Included": "Included", + "Excluded": "Excluded", + "Providers": "Providers", + "Source state": "Source state", + "Frozen snapshots": "Frozen snapshots", + "Refresh snapshots": "Refresh snapshots", + "No frozen snapshots.": "No frozen snapshots.", + "Cancel": "Cancel", + "Create": "Create", + "Name": "Name", + "Definition type": "Definition type", + "Static": "Static", + "Parameterized": "Parameterized", + "Dynamic": "Dynamic", + "Template (not runnable)": "Template (not runnable)", + "Add audience entry": "Add audience entry", + "Edit audience entry": "Edit audience entry", + "Apply": "Apply", + "Source type": "Source type", + "Provider": "Provider", + "Email": "Email", + "Postal": "Postal", + "Internal": "Internal", + "Portal": "Portal", + "Provider object": "Provider object", + "Mode": "Mode", + "Display label": "Display label", + "Override reason": "Override reason", + "Effective from": "Effective from", + "Effective until": "Effective until", + "Requested channels": "Requested channels", + "Delete": "Delete", + "Discard entry changes": "Discard entry changes" +} as const; + +const de: Record = { + "i18n:govoplan-dist-lists.distribution_lists": "Verteilerlisten", + "i18n:govoplan-dist-lists.editor": "Verteilerlisten-Editor", + "i18n:govoplan-dist-lists.preview": "Vorschau der Verteilerauflösung", + "i18n:govoplan-dist-lists.picker": "Auswahl einer Verteilerliste", + "i18n:govoplan-dist-lists.loading_reason": "Verteilerlisten werden noch geladen.", + "i18n:govoplan-dist-lists.busy_reason": "Eine andere Verteilerlistenaktion läuft noch.", + "i18n:govoplan-dist-lists.write_permission_reason": "Ihr Konto darf Verteilerlisten nicht erstellen oder ändern.", + "i18n:govoplan-dist-lists.no_selection_reason": "Wählen oder erstellen Sie zuerst eine Verteilerliste.", + "i18n:govoplan-dist-lists.no_changes_reason": "Es gibt keine Definitionsänderungen zu speichern.", + "i18n:govoplan-dist-lists.incomplete_reason": "Füllen Sie zuerst Name, Quelle und gegebenenfalls Überschreibungsgrund aus.", + "i18n:govoplan-dist-lists.save_before_expansion": "Speichern Sie die aktuelle Revision, bevor Sie sie auflösen oder einfrieren.", + "i18n:govoplan-dist-lists.unsaved_title": "Ungespeicherte Verteilerliste", + "i18n:govoplan-dist-lists.unsaved_message": "Speichern oder verwerfen Sie den Verteilerlistenentwurf, bevor Sie diese Oberfläche verlassen.", + "i18n:govoplan-dist-lists.entry_unsaved_title": "Ungespeicherter Zielgruppeneintrag", + "i18n:govoplan-dist-lists.entry_unsaved_message": "Speichern Sie die vollständige Verteilerlistenrevision oder verwerfen Sie alle Änderungen, bevor Sie diese Oberfläche verlassen.", + "i18n:govoplan-dist-lists.entry_discard_message": "Änderungen an diesem Zielgruppeneintrag verwerfen? Andere Änderungen am Verteilerlistenentwurf bleiben erhalten.", + "i18n:govoplan-dist-lists.snapshot_title": "Empfänger-Snapshot einfrieren", + "i18n:govoplan-dist-lists.snapshot_message": "Diese Auflösung einfrieren? Exakte Empfänger, Ausschlüsse, Quellrevisionen, Entscheidungen, Provenienz und Hash werden unveränderlicher Nachweis für Verbraucher.", + "Distribution Lists": "Verteilerlisten", + "Refresh": "Aktualisieren", + "New distribution list": "Neue Verteilerliste", + "Search lists": "Listen suchen", + "Search distribution lists": "Verteilerlisten durchsuchen", + "No distribution lists": "Keine Verteilerlisten", + "No distribution list selected": "Keine Verteilerliste ausgewählt", + "Definition": "Definition", + "Preview": "Vorschau", + "Snapshots": "Snapshots", + "Save revision": "Revision speichern", + "Delete distribution list": "Verteilerliste löschen", + "Purpose": "Zweck", + "Expand preview": "Vorschau auflösen", + "Freeze snapshot": "Snapshot einfrieren", + "Included": "Enthalten", + "Excluded": "Ausgeschlossen", + "Providers": "Anbieter", + "Source state": "Quellstatus", + "Frozen snapshots": "Eingefrorene Snapshots", + "Refresh snapshots": "Snapshots aktualisieren", + "No frozen snapshots.": "Keine eingefrorenen Snapshots.", + "Cancel": "Abbrechen", + "Create": "Erstellen", + "Name": "Name", + "Definition type": "Definitionsart", + "Static": "Statisch", + "Parameterized": "Parametrisiert", + "Dynamic": "Dynamisch", + "Template (not runnable)": "Vorlage (nicht ausführbar)", + "Add audience entry": "Zielgruppeneintrag hinzufügen", + "Edit audience entry": "Zielgruppeneintrag bearbeiten", + "Apply": "Übernehmen", + "Source type": "Quellenart", + "Provider": "Anbieter", + "Email": "E-Mail", + "Postal": "Post", + "Internal": "Intern", + "Portal": "Portal", + "Provider object": "Anbieterobjekt", + "Mode": "Modus", + "Display label": "Anzeigename", + "Override reason": "Überschreibungsgrund", + "Effective from": "Gültig ab", + "Effective until": "Gültig bis", + "Requested channels": "Angeforderte Kanäle", + "Delete": "Löschen", + "Discard entry changes": "Eintragsänderungen verwerfen" +}; + +export const generatedTranslations: PlatformTranslations = { en, de }; diff --git a/webui/src/module.ts b/webui/src/module.ts index 2b7fd00..478db9b 100644 --- a/webui/src/module.ts +++ b/webui/src/module.ts @@ -1,5 +1,6 @@ import { createElement, lazy } from "react"; import type { PlatformWebModule } from "@govoplan/core-webui"; +import { generatedTranslations } from "./i18n/generatedTranslations"; import "./styles/dist-lists.css"; const DistributionListsPage = lazy( @@ -14,7 +15,7 @@ const readScopes = [ export const distributionListsModule: PlatformWebModule = { id: "dist_lists", - label: "Distribution Lists", + label: "i18n:govoplan-dist-lists.distribution_lists", version: "0.1.14", optionalDependencies: [ "addresses", @@ -24,10 +25,17 @@ export const distributionListsModule: PlatformWebModule = { "dataflow", "policy" ], + translations: generatedTranslations, + viewSurfaces: [ + { id: "dist_lists.page", moduleId: "dist_lists", kind: "route", label: "i18n:govoplan-dist-lists.distribution_lists", order: 74 }, + { id: "dist_lists.editor", moduleId: "dist_lists", kind: "section", label: "i18n:govoplan-dist-lists.editor", parentId: "dist_lists.page", order: 10 }, + { id: "dist_lists.preview", moduleId: "dist_lists", kind: "section", label: "i18n:govoplan-dist-lists.preview", parentId: "dist_lists.page", order: 20 }, + { id: "dist_lists.picker", moduleId: "dist_lists", kind: "action", label: "i18n:govoplan-dist-lists.picker", order: 30 } + ], navItems: [ { to: "/distribution-lists", - label: "Distribution Lists", + label: "i18n:govoplan-dist-lists.distribution_lists", iconName: "list-tree", anyOf: readScopes, order: 74 @@ -38,6 +46,7 @@ export const distributionListsModule: PlatformWebModule = { path: "/distribution-lists", anyOf: readScopes, order: 74, + surfaceId: "dist_lists.page", render: ({ settings, auth }) => createElement(DistributionListsPage, { settings, auth }) }