From 07dd35bcc0b373f4abbf66be3cf9768d3eadc47c Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Mon, 3 Aug 2026 13:02:19 +0200 Subject: [PATCH] Migrate Forms Runtime interface patterns --- docs/INTERFACE_PATTERN_MIGRATION.md | 33 ++++ .../backend/manifest.py | 60 +++++++ .../test_interface_documentation_contract.py | 33 ++++ webui/src/features/forms/FormInstancePage.tsx | 131 ++++++++++++--- webui/src/features/forms/FormsRuntimePage.tsx | 22 ++- webui/src/features/forms/interfacePatterns.ts | 27 +++ webui/src/i18n/generatedTranslations.ts | 155 ++++++++++++++++++ webui/src/module.ts | 6 +- webui/src/styles/forms-runtime.css | 5 + 9 files changed, 440 insertions(+), 32 deletions(-) create mode 100644 docs/INTERFACE_PATTERN_MIGRATION.md create mode 100644 tests/test_interface_documentation_contract.py create mode 100644 webui/src/features/forms/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..c224281 --- /dev/null +++ b/docs/INTERFACE_PATTERN_MIGRATION.md @@ -0,0 +1,33 @@ +# Forms Runtime Interface Pattern Migration + +This migration applies the GovOPlaN interface pattern language to the Forms +Runtime list, instance, submission, and native handoff surfaces. Core owns the +shared controls and interaction states. Forms Runtime keeps ownership of +instance revisions, validation, receipts, and handoff evidence. + +## Surface Inventory + +| Surface | Archetype | Consequence class | Contract | +| --- | --- | --- | --- | +| `/forms-runtime` | Filtered work queue | Select assigned or authorized instance | Shared loading/error/empty/help states and locale-aware dates | +| `/forms-runtime/:instanceId` | Guided runtime form | Change draft values or inspect immutable submission | Definition-provided fields/help, permission/lifecycle blocker, guarded draft | +| Save and submit actions | Consequential editor | Save revision or issue immutable receipt | Explained disabled reasons; submission confirmation; server validation remains authoritative | +| Case/Workflow handoffs | External-effect recovery queue | Record intent, execute, retry, reconcile, compensate | Permission reason, start confirmation, explicit unknown-outcome and compensation semantics | +| History and revisions | Evidence/provenance | Reconstruct status and value revisions | Immutable chronological evidence with platform-locale timestamps | + +## Consequence And Availability Rules + +- Runtime localization follows the platform-selected language, not the browser + language independently of the active account preference. +- A draft save requires changed values and a reason. Submission is separately + confirmed and creates an immutable receipt after server validation. +- Read-only fields explain whether permission or lifecycle caused the state. +- Handoff intent is persisted before provider execution. Unknown outcomes are + reconciled before retry; compensation records verified absence only. +- Optional Files, Policy, Case, Workflow, Approval, Portal, and Audit behavior + remains behind declared capabilities and interfaces. + +Native controls preserve keyboard order, shared dialogs manage focus, changed +values use the global unsaved-draft guard, and bounded list/detail regions keep +their existing responsive scrolling. English and German catalogues cover +module-owned copy; definition content uses its own published localization. diff --git a/src/govoplan_forms_runtime/backend/manifest.py b/src/govoplan_forms_runtime/backend/manifest.py index 99f4a53..b04e134 100644 --- a/src/govoplan_forms_runtime/backend/manifest.py +++ b/src/govoplan_forms_runtime/backend/manifest.py @@ -317,6 +317,66 @@ manifest = ModuleManifest( kind="repository", ), ), + metadata={ + "seed": True, + "help_contexts": [ + "forms_runtime.navigation", + "forms_runtime.workspace", + "forms_runtime.instance", + "forms_runtime.state.read-only", + "forms_runtime.state.permission-blocked", + ], + "privacy_notes": [ + "Form values are returned only through tenant-bound instance permissions and ownership rules.", + "Validation messages expose field-level diagnostics without disclosing unrelated submissions.", + "Handoff rows retain provider references and outcomes but do not bypass target-module authorization.", + ], + }, + ), + DocumentationTopic( + id="forms_runtime.reference.fields-and-consequences", + title="Form values, submission, and handoff consequences", + summary="Runtime field behavior, immutable receipts, draft revisions, optional evidence, and recoverable external effects.", + body=( + "The active instance resolves one exact published Form definition revision. Visibility conditions alter presentation, " + "not server validation or authorization. Saving a permitted draft creates a new revision with its change reason. " + "Submitting validates values, attachments, signatures, and policy requirements and records an immutable receipt; it is " + "not an editable draft save. A Case or Workflow handoff records intent before calling its optional provider and uses a " + "stable idempotency key. Rejected effects may be retried. Unknown outcomes must be reconciled before retry to avoid a " + "duplicate target. Administrative compensation records verified absence and never deletes a remote target." + ), + layer="configured", + documentation_types=("admin", "user"), + audience=("user", "operator", "module_admin", "auditor"), + related_modules=OPTIONAL_DEPENDENCIES, + links=( + DocumentationLink( + label="Forms Runtime security and recovery", + href="govoplan-forms-runtime/docs/FORMS_RUNTIME_DOMAIN_BOUNDARY.md", + kind="repository", + ), + ), + metadata={ + "seed": True, + "help_contexts": [ + "forms_runtime.field.dynamic-value", + "forms_runtime.field.change-reason", + "forms_runtime.field.handoff-kind", + "forms_runtime.field.target-binding", + "forms_runtime.action.save-draft", + "forms_runtime.action.submit", + "forms_runtime.action.start-handoff", + "forms_runtime.action.reconcile-handoff", + "forms_runtime.action.compensate-handoff", + ], + "consequence_classes": { + "save_draft": "Creates an immutable draft revision with a change reason.", + "submit": "Validates the exact definition and creates an immutable submission receipt.", + "start_handoff": "Persists intent before invoking an optional Case or Workflow provider.", + "reconcile": "Resolves an outcome-unknown effect without unsafe duplicate execution.", + "compensate": "Records an administrative proof that no target effect exists.", + }, + }, ), ), architecture=declared_module_architecture( diff --git a/tests/test_interface_documentation_contract.py b/tests/test_interface_documentation_contract.py new file mode 100644 index 0000000..e453161 --- /dev/null +++ b/tests/test_interface_documentation_contract.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import unittest + +from govoplan_forms_runtime.backend.manifest import manifest + + +class FormsRuntimeInterfaceDocumentationContractTests(unittest.TestCase): + def test_routes_and_surfaces_remain_declared(self) -> None: + frontend = manifest.frontend + self.assertIsNotNone(frontend) + self.assertEqual( + {"/forms-runtime", "/forms-runtime/:instanceId"}, + {item.path for item in frontend.routes}, # type: ignore[union-attr] + ) + self.assertEqual( + {"forms_runtime.navigation", "forms_runtime.workspace", "forms_runtime.instance"}, + {item.id for item in frontend.view_surfaces}, # type: ignore[union-attr] + ) + + def test_help_privacy_and_consequence_metadata_remain_published(self) -> None: + topics = {topic.id: topic for topic in manifest.documentation} + guide = topics["forms_runtime.submissions"] + reference = topics["forms_runtime.reference.fields-and-consequences"] + self.assertIn("forms_runtime.instance", guide.metadata["help_contexts"]) + self.assertGreaterEqual(len(guide.metadata["privacy_notes"]), 3) + self.assertIn("forms_runtime.action.submit", reference.metadata["help_contexts"]) + self.assertIn("submit", reference.metadata["consequence_classes"]) + self.assertIn("compensate", reference.metadata["consequence_classes"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/webui/src/features/forms/FormInstancePage.tsx b/webui/src/features/forms/FormInstancePage.tsx index 196f7b3..42272e3 100644 --- a/webui/src/features/forms/FormInstancePage.tsx +++ b/webui/src/features/forms/FormInstancePage.tsx @@ -2,15 +2,20 @@ import { ArrowLeft, ExternalLink, RefreshCw, Save, Send } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useParams } from "react-router"; import { + ActionBlockerHint, Button, ConfirmDialog, + DocumentationHelpLink, DismissibleAlert, LoadingIndicator, PageScrollViewport, StatusBadge, ToggleSwitch, hasScope, + i18nMessage, useGuardedNavigate, + usePlatformLanguage, + useUnsavedDraftGuard, type PlatformRouteContext } from "@govoplan/core-webui"; import { @@ -31,11 +36,17 @@ import { type FormHandoff, type ValidationResult } from "../../api/formsRuntime"; +import { + FORMS_RUNTIME_DOCUMENTATION, + FORMS_RUNTIME_FIELD_DOCUMENTATION, + FORMS_RUNTIME_I18N +} from "./interfacePatterns"; export default function FormInstancePage({ settings, auth }: PlatformRouteContext) { const { instanceId = "" } = useParams(); const navigate = useGuardedNavigate(); + const { language } = usePlatformLanguage(); const [instance, setInstance] = useState(null); const [definition, setDefinition] = useState(null); const [history, setHistory] = useState([]); @@ -50,6 +61,8 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex const [handoffKind, setHandoffKind] = useState<"case" | "workflow">("case"); const [handoffBinding, setHandoffBinding] = useState(""); const [compensating, setCompensating] = useState(null); + const [confirmingSubmit, setConfirmingSubmit] = useState(false); + const [confirmingHandoff, setConfirmingHandoff] = useState(false); const load = useCallback(async (signal?: AbortSignal) => { setLoading(true); @@ -91,8 +104,13 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex } }, [definition, handoffKind]); - const editable = instance?.status === "started" || instance?.status === "draft"; - const canSave = instance?.status === "draft" && definition?.allow_drafts; + const editableLifecycle = instance?.status === "started" || instance?.status === "draft"; + const canParticipate = hasScope(auth, "forms_runtime:submission:participate"); + const canWrite = hasScope(auth, "forms_runtime:workspace:write"); + const canAdmin = hasScope(auth, "forms_runtime:workspace:admin"); + const canEditPermission = canParticipate || canWrite; + const editable = Boolean(editableLifecycle && canEditPermission); + const canSave = Boolean(instance?.status === "draft" && definition?.allow_drafts && canEditPermission); const changed = useMemo( () => Boolean(instance && JSON.stringify(values) !== JSON.stringify(instance.values)), [instance, values] @@ -106,30 +124,43 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex return grouped; }, [instance]); const localized = useMemo( - () => localizeDefinition(definition), - [definition] + () => localizeDefinition(definition, language), + [definition, language] ); const groups = useMemo( () => definition ? visibleGroups(definition, values) : [], [definition, values] ); const mayHandoff = Boolean(instance && ["submitted", "validated", "needs_review", "accepted"].includes(instance.status) && instance.service_ref); - const canAdmin = hasScope(auth, "forms_runtime:workspace:admin"); + const canHandoff = canWrite || canAdmin; - async function save() { - if (!instance || !canSave || !changed || !changeReason.trim()) return; + async function save(): Promise { + if (!instance || !canSave || !changed || !changeReason.trim()) return false; setSaving(true); setError(""); try { await saveFormDraft(settings, instance, values, changeReason.trim()); await load(); + return true; } catch (reason) { setError(reason instanceof Error ? reason.message : "The draft could not be saved."); + return false; } finally { setSaving(false); } } + useUnsavedDraftGuard({ + dirty: Boolean(instance && (changed || changeReason)), + onSave: save, + onDiscard: () => { + setValues(instance?.values ?? {}); + setChangeReason(""); + }, + title: "i18n:govoplan-forms-runtime.unsaved_title", + message: "i18n:govoplan-forms-runtime.unsaved_message" + }); + async function submit() { if (!instance || !editable) return; setSaving(true); @@ -195,7 +226,8 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex Forms {definition && {localized.title}} - {instance && } + {instance && } + {error && @@ -204,6 +236,23 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex } {loading && } + {!loading && instance && !editable && + + } {!loading && instance && definition &&
@@ -226,7 +275,7 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex }} optionLabels={localized.optionLabels[field.key] ?? {}} value={values[field.key]} - disabled={!editable || saving} + disabled={!editable || saving} diagnostics={diagnostics.get(field.key) ?? []} onChange={(value) => setValues((current) => { const next = { ...current }; @@ -247,6 +296,7 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex } {editable &&
+ {canSave &&
}
@@ -296,13 +347,13 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
{handoffs.map((item) =>
- {humanize(item.binding_kind)}{item.binding_reference} - + {domainLabel(item.binding_kind)}{item.binding_reference} + {item.last_error && {item.last_error}} {item.href && } - {item.state === "rejected" && } - {item.state === "outcome_unknown" && } + {item.state === "rejected" && } + {item.state === "outcome_unknown" && } {canAdmin && (item.state === "rejected" || item.state === "outcome_unknown") && }
@@ -317,9 +368,9 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
    {events.map((event) =>
  1. - {humanize(event.status)} + {stateLabel(event.status)} {humanize(event.event_type)} - +
  2. )}
@@ -331,7 +382,7 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
  • Revision {item.revision} {item.change_reason} - +
  • )} @@ -341,6 +392,30 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex }
    + setConfirmingSubmit(false)} + onConfirm={() => { + setConfirmingSubmit(false); + void submit(); + }} + /> + setConfirmingHandoff(false)} + onConfirm={() => { + setConfirmingHandoff(false); + void startHandoff(); + }} + /> }; if (!definition?.localizations?.length) return canonical; - const browserLocales = navigator.languages.map((value) => value.toLowerCase()); - const localization = definition.localizations.find((item) => browserLocales.some((locale) => locale === item.locale.toLowerCase() || locale.startsWith(`${item.locale.toLowerCase()}-`))) + const requestedLocale = language.toLowerCase(); + const localization = definition.localizations.find((item) => requestedLocale === item.locale.toLowerCase() || requestedLocale.startsWith(`${item.locale.toLowerCase()}-`)) ?? definition.localizations.find((item) => item.locale.toLowerCase() === definition.fallback_locale?.toLowerCase()); if (!localization) return canonical; return { @@ -589,10 +664,18 @@ function localizeDefinition(definition: FormDefinition | null) { }; } -function formatDateTime(value: string): string { - return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(new Date(value)); +function formatDateTime(value: string, locale?: string): string { + return new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short" }).format(new Date(value)); } function humanize(value: string): string { return value.replace(/[_:.-]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()); } + +function stateLabel(value: string): string { + return `i18n:govoplan-forms-runtime.state_${value}`; +} + +function domainLabel(value: string): string { + return `i18n:govoplan-forms-runtime.domain_${value}`; +} diff --git a/webui/src/features/forms/FormsRuntimePage.tsx b/webui/src/features/forms/FormsRuntimePage.tsx index 7917fbb..1eba6ae 100644 --- a/webui/src/features/forms/FormsRuntimePage.tsx +++ b/webui/src/features/forms/FormsRuntimePage.tsx @@ -2,20 +2,25 @@ import { RefreshCw } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; import { Button, + DocumentationHelpLink, DismissibleAlert, LoadingIndicator, PageScrollViewport, StatusBadge, + i18nMessage, useGuardedNavigate, + usePlatformLanguage, type PlatformRouteContext } from "@govoplan/core-webui"; import { listFormInstances, type FormInstance } from "../../api/formsRuntime"; +import { FORMS_RUNTIME_DOCUMENTATION, FORMS_RUNTIME_I18N } from "./interfacePatterns"; const OPEN_STATUSES = ["started", "draft", "submitted", "validated", "needs_review"]; export default function FormsRuntimePage({ settings }: PlatformRouteContext) { const navigate = useGuardedNavigate(); + const { language } = usePlatformLanguage(); const [items, setItems] = useState([]); const [total, setTotal] = useState(0); const [status, setStatus] = useState("open"); @@ -50,7 +55,7 @@ export default function FormsRuntimePage({ settings }: PlatformRouteContext) {
    - @@ -68,7 +73,8 @@ export default function FormsRuntimePage({ settings }: PlatformRouteContext) { - {total} forms + {i18nMessage("i18n:govoplan-forms-runtime.form_count", { total })} +
    {error && @@ -93,8 +99,8 @@ export default function FormsRuntimePage({ settings }: PlatformRouteContext) { {item.definition_ref.label ?? humanize(item.definition_ref.object_id)} Revision {item.definition_ref.version ?? "-"} - {formatDateTime(item.recorded_at)} - + {formatDateTime(item.recorded_at, language)} + )}
    @@ -109,10 +115,14 @@ function isOpen(status: string): boolean { return OPEN_STATUSES.includes(status); } -function formatDateTime(value: string): string { - return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(new Date(value)); +function formatDateTime(value: string, locale?: string): string { + return new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short" }).format(new Date(value)); } function humanize(value: string): string { return value.replace(/[_:.-]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()); } + +function stateLabel(value: string): string { + return `i18n:govoplan-forms-runtime.state_${value}`; +} diff --git a/webui/src/features/forms/interfacePatterns.ts b/webui/src/features/forms/interfacePatterns.ts new file mode 100644 index 0000000..0d477a3 --- /dev/null +++ b/webui/src/features/forms/interfacePatterns.ts @@ -0,0 +1,27 @@ +import type { DocumentationHelpReference } from "@govoplan/core-webui"; + +export const FORMS_RUNTIME_DOCUMENTATION = { + topicId: "forms_runtime.submissions", + documentationType: "user" +} satisfies DocumentationHelpReference; + +export const FORMS_RUNTIME_FIELD_DOCUMENTATION = { + topicId: "forms_runtime.reference.fields-and-consequences", + documentationType: "admin" +} satisfies DocumentationHelpReference; + +export const FORMS_RUNTIME_I18N = { + loading: "i18n:govoplan-forms-runtime.loading_reason", + saving: "i18n:govoplan-forms-runtime.saving_reason", + editReason: "i18n:govoplan-forms-runtime.edit_permission_reason", + handoffReason: "i18n:govoplan-forms-runtime.handoff_permission_reason", + lifecycleReason: "i18n:govoplan-forms-runtime.lifecycle_reason", + unchanged: "i18n:govoplan-forms-runtime.unchanged_reason", + changeReason: "i18n:govoplan-forms-runtime.change_reason_required", + requiredAction: "i18n:govoplan-forms-runtime.required_action", + actor: "i18n:govoplan-forms-runtime.responsible_actor", + destination: "i18n:govoplan-forms-runtime.destination", + permissionAction: "i18n:govoplan-forms-runtime.permission_action", + permissionActor: "i18n:govoplan-forms-runtime.permission_actor", + permissionDestination: "i18n:govoplan-forms-runtime.permission_destination" +} as const; diff --git a/webui/src/i18n/generatedTranslations.ts b/webui/src/i18n/generatedTranslations.ts new file mode 100644 index 0000000..b0fd8dc --- /dev/null +++ b/webui/src/i18n/generatedTranslations.ts @@ -0,0 +1,155 @@ +import type { PlatformTranslations } from "@govoplan/core-webui"; + +const en = { + "i18n:govoplan-forms-runtime.forms": "Forms", + "i18n:govoplan-forms-runtime.loading_reason": "Form data is still loading.", + "i18n:govoplan-forms-runtime.saving_reason": "A Form operation is still running.", + "i18n:govoplan-forms-runtime.edit_permission_reason": "Your account may read this Form but may not change or submit it.", + "i18n:govoplan-forms-runtime.handoff_permission_reason": "Your account may not create or recover Case and Workflow handoffs.", + "i18n:govoplan-forms-runtime.lifecycle_reason": "This Form is immutable in its current lifecycle state.", + "i18n:govoplan-forms-runtime.unchanged_reason": "There are no changed values to save.", + "i18n:govoplan-forms-runtime.change_reason_required": "Enter a change reason before saving the draft.", + "i18n:govoplan-forms-runtime.required_action": "Required action", + "i18n:govoplan-forms-runtime.responsible_actor": "Responsible actor", + "i18n:govoplan-forms-runtime.destination": "Destination", + "i18n:govoplan-forms-runtime.permission_action": "Ask for the appropriate Forms Runtime permission or assignment.", + "i18n:govoplan-forms-runtime.permission_actor": "An Access administrator or the responsible process owner", + "i18n:govoplan-forms-runtime.permission_destination": "Access role assignments or the assigning workflow", + "i18n:govoplan-forms-runtime.unsaved_title": "Unsaved Form", + "i18n:govoplan-forms-runtime.unsaved_message": "Save or discard the changed Form values before leaving this surface.", + "i18n:govoplan-forms-runtime.submit_title": "Submit Form", + "i18n:govoplan-forms-runtime.submit_message": "Submit this Form? The server validates the exact published definition and records an immutable receipt.", + "i18n:govoplan-forms-runtime.handoff_title": "Start governed handoff", + "i18n:govoplan-forms-runtime.handoff_message": "Start the {kind} handoff? Intent is recorded before execution and an unknown outcome requires reconciliation.", + "i18n:govoplan-forms-runtime.form_count": "{total} forms", + "i18n:govoplan-forms-runtime.state_started": "Started", + "i18n:govoplan-forms-runtime.state_draft": "Draft", + "i18n:govoplan-forms-runtime.state_submitted": "Submitted", + "i18n:govoplan-forms-runtime.state_validated": "Validated", + "i18n:govoplan-forms-runtime.state_needs_review": "Needs review", + "i18n:govoplan-forms-runtime.state_accepted": "Accepted", + "i18n:govoplan-forms-runtime.state_rejected": "Rejected", + "i18n:govoplan-forms-runtime.state_handed_off": "Handed off", + "i18n:govoplan-forms-runtime.state_archived": "Archived", + "i18n:govoplan-forms-runtime.state_reconciled": "Reconciled", + "i18n:govoplan-forms-runtime.state_outcome_unknown": "Outcome unknown", + "i18n:govoplan-forms-runtime.state_requested": "Requested", + "i18n:govoplan-forms-runtime.state_compensated": "Compensated", + "i18n:govoplan-forms-runtime.domain_case": "Case", + "i18n:govoplan-forms-runtime.domain_workflow": "Workflow", + "Forms": "Forms", + "Refresh": "Refresh", + "Status": "Status", + "Open": "Open", + "All": "All", + "Draft": "Draft", + "Submitted": "Submitted", + "Needs review": "Needs review", + "Accepted": "Accepted", + "Rejected": "Rejected", + "Handed off": "Handed off", + "Archived": "Archived", + "Loading forms": "Loading forms", + "No matching Forms.": "No matching Forms.", + "Revision": "Revision", + "Loading Form": "Loading Form", + "Change reason": "Change reason", + "Save draft": "Save draft", + "Submit": "Submit", + "Submission receipt": "Submission receipt", + "Case and workflow handoffs": "Case and workflow handoffs", + "Create Case": "Create Case", + "Start Workflow": "Start Workflow", + "Target binding (optional)": "Target binding (optional)", + "Exact target binding": "Exact target binding", + "Start": "Start", + "No handoff has been requested.": "No handoff has been requested.", + "Open target": "Open target", + "Retry": "Retry", + "Reconcile": "Reconcile", + "Compensate": "Compensate", + "Status history": "Status history", + "Revisions": "Revisions", + "Select": "Select", + "Compensate handoff": "Compensate handoff", + "Confirm absent and compensate": "Confirm absent and compensate", + "Read-only Form": "Read-only Form" +} as const; + +const de: Record = { + "i18n:govoplan-forms-runtime.forms": "Formulare", + "i18n:govoplan-forms-runtime.loading_reason": "Formulardaten werden noch geladen.", + "i18n:govoplan-forms-runtime.saving_reason": "Eine Formularaktion läuft noch.", + "i18n:govoplan-forms-runtime.edit_permission_reason": "Ihr Konto darf dieses Formular lesen, aber nicht ändern oder absenden.", + "i18n:govoplan-forms-runtime.handoff_permission_reason": "Ihr Konto darf keine Fall- oder Workflow-Übergaben erstellen oder wiederherstellen.", + "i18n:govoplan-forms-runtime.lifecycle_reason": "Dieses Formular ist in seinem aktuellen Lebenszyklus unveränderlich.", + "i18n:govoplan-forms-runtime.unchanged_reason": "Es gibt keine geänderten Werte zu speichern.", + "i18n:govoplan-forms-runtime.change_reason_required": "Geben Sie vor dem Speichern des Entwurfs einen Änderungsgrund ein.", + "i18n:govoplan-forms-runtime.required_action": "Erforderliche Aktion", + "i18n:govoplan-forms-runtime.responsible_actor": "Verantwortliche Stelle", + "i18n:govoplan-forms-runtime.destination": "Ziel", + "i18n:govoplan-forms-runtime.permission_action": "Fordern Sie die passende Formularberechtigung oder Zuweisung an.", + "i18n:govoplan-forms-runtime.permission_actor": "Eine Zugriffsadministration oder die verantwortliche Prozessstelle", + "i18n:govoplan-forms-runtime.permission_destination": "Zugriff und Rollenzuweisungen oder der zuweisende Workflow", + "i18n:govoplan-forms-runtime.unsaved_title": "Ungespeichertes Formular", + "i18n:govoplan-forms-runtime.unsaved_message": "Speichern oder verwerfen Sie die geänderten Formularwerte, bevor Sie diese Oberfläche verlassen.", + "i18n:govoplan-forms-runtime.submit_title": "Formular absenden", + "i18n:govoplan-forms-runtime.submit_message": "Dieses Formular absenden? Der Server prüft die exakte veröffentlichte Definition und erfasst einen unveränderlichen Beleg.", + "i18n:govoplan-forms-runtime.handoff_title": "Geregelte Übergabe starten", + "i18n:govoplan-forms-runtime.handoff_message": "Die Übergabe an {kind} starten? Die Absicht wird vor der Ausführung erfasst; ein unbekanntes Ergebnis muss abgeglichen werden.", + "i18n:govoplan-forms-runtime.form_count": "{total} Formulare", + "i18n:govoplan-forms-runtime.state_started": "Gestartet", + "i18n:govoplan-forms-runtime.state_draft": "Entwurf", + "i18n:govoplan-forms-runtime.state_submitted": "Abgesendet", + "i18n:govoplan-forms-runtime.state_validated": "Validiert", + "i18n:govoplan-forms-runtime.state_needs_review": "Prüfung erforderlich", + "i18n:govoplan-forms-runtime.state_accepted": "Angenommen", + "i18n:govoplan-forms-runtime.state_rejected": "Abgelehnt", + "i18n:govoplan-forms-runtime.state_handed_off": "Übergeben", + "i18n:govoplan-forms-runtime.state_archived": "Archiviert", + "i18n:govoplan-forms-runtime.state_reconciled": "Abgeglichen", + "i18n:govoplan-forms-runtime.state_outcome_unknown": "Ergebnis unbekannt", + "i18n:govoplan-forms-runtime.state_requested": "Angefordert", + "i18n:govoplan-forms-runtime.state_compensated": "Kompensiert", + "i18n:govoplan-forms-runtime.domain_case": "Fall", + "i18n:govoplan-forms-runtime.domain_workflow": "Workflow", + "Forms": "Formulare", + "Refresh": "Aktualisieren", + "Status": "Status", + "Open": "Offen", + "All": "Alle", + "Draft": "Entwurf", + "Submitted": "Abgesendet", + "Needs review": "Prüfung erforderlich", + "Accepted": "Angenommen", + "Rejected": "Abgelehnt", + "Handed off": "Übergeben", + "Archived": "Archiviert", + "Loading forms": "Formulare werden geladen", + "No matching Forms.": "Keine passenden Formulare.", + "Revision": "Revision", + "Loading Form": "Formular wird geladen", + "Change reason": "Änderungsgrund", + "Save draft": "Entwurf speichern", + "Submit": "Absenden", + "Submission receipt": "Übermittlungsbeleg", + "Case and workflow handoffs": "Fall- und Workflow-Übergaben", + "Create Case": "Fall erstellen", + "Start Workflow": "Workflow starten", + "Target binding (optional)": "Zielbindung (optional)", + "Exact target binding": "Exakte Zielbindung", + "Start": "Starten", + "No handoff has been requested.": "Es wurde keine Übergabe angefordert.", + "Open target": "Ziel öffnen", + "Retry": "Erneut versuchen", + "Reconcile": "Abgleichen", + "Compensate": "Kompensieren", + "Status history": "Statusverlauf", + "Revisions": "Revisionen", + "Select": "Auswählen", + "Compensate handoff": "Übergabe kompensieren", + "Confirm absent and compensate": "Fehlen bestätigen und kompensieren", + "Read-only Form": "Schreibgeschütztes Formular" +}; + +export const generatedTranslations: PlatformTranslations = { en, de }; diff --git a/webui/src/module.ts b/webui/src/module.ts index 661eeb3..4f23b37 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/forms-runtime.css"; @@ -12,10 +13,11 @@ const routeScopes = [ export const formsRuntimeModule: PlatformWebModule = { id: "forms_runtime", - label: "Forms", + label: "i18n:govoplan-forms-runtime.forms", version: "0.1.14", dependencies: ["access", "forms"], optionalDependencies: ["files", "approvals", "workflow_engine", "portal", "cases", "policy", "audit"], + translations: generatedTranslations, routes: [ { path: "/forms-runtime", @@ -35,7 +37,7 @@ export const formsRuntimeModule: PlatformWebModule = { navItems: [ { to: "/forms-runtime", - label: "Forms", + label: "i18n:govoplan-forms-runtime.forms", iconName: "form", anyOf: routeScopes, order: 37, diff --git a/webui/src/styles/forms-runtime.css b/webui/src/styles/forms-runtime.css index 3d9f9cf..1fbd835 100644 --- a/webui/src/styles/forms-runtime.css +++ b/webui/src/styles/forms-runtime.css @@ -48,6 +48,11 @@ padding: 16px 18px 24px; } +.form-instance-viewport > .action-blocker-hint { + max-width: 1280px; + margin: 0 auto 16px; +} + .forms-runtime-list { overflow: hidden; border: 1px solid var(--border);