Migrate Forms Runtime interface patterns
This commit is contained in:
@@ -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.
|
||||||
@@ -317,6 +317,66 @@ manifest = ModuleManifest(
|
|||||||
kind="repository",
|
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(
|
architecture=declared_module_architecture(
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -2,15 +2,20 @@ import { ArrowLeft, ExternalLink, RefreshCw, Save, Send } from "lucide-react";
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
import {
|
import {
|
||||||
|
ActionBlockerHint,
|
||||||
Button,
|
Button,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
|
DocumentationHelpLink,
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
LoadingIndicator,
|
LoadingIndicator,
|
||||||
PageScrollViewport,
|
PageScrollViewport,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
ToggleSwitch,
|
ToggleSwitch,
|
||||||
hasScope,
|
hasScope,
|
||||||
|
i18nMessage,
|
||||||
useGuardedNavigate,
|
useGuardedNavigate,
|
||||||
|
usePlatformLanguage,
|
||||||
|
useUnsavedDraftGuard,
|
||||||
type PlatformRouteContext
|
type PlatformRouteContext
|
||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
import {
|
import {
|
||||||
@@ -31,11 +36,17 @@ import {
|
|||||||
type FormHandoff,
|
type FormHandoff,
|
||||||
type ValidationResult
|
type ValidationResult
|
||||||
} from "../../api/formsRuntime";
|
} from "../../api/formsRuntime";
|
||||||
|
import {
|
||||||
|
FORMS_RUNTIME_DOCUMENTATION,
|
||||||
|
FORMS_RUNTIME_FIELD_DOCUMENTATION,
|
||||||
|
FORMS_RUNTIME_I18N
|
||||||
|
} from "./interfacePatterns";
|
||||||
|
|
||||||
|
|
||||||
export default function FormInstancePage({ settings, auth }: PlatformRouteContext) {
|
export default function FormInstancePage({ settings, auth }: PlatformRouteContext) {
|
||||||
const { instanceId = "" } = useParams();
|
const { instanceId = "" } = useParams();
|
||||||
const navigate = useGuardedNavigate();
|
const navigate = useGuardedNavigate();
|
||||||
|
const { language } = usePlatformLanguage();
|
||||||
const [instance, setInstance] = useState<FormInstance | null>(null);
|
const [instance, setInstance] = useState<FormInstance | null>(null);
|
||||||
const [definition, setDefinition] = useState<FormDefinition | null>(null);
|
const [definition, setDefinition] = useState<FormDefinition | null>(null);
|
||||||
const [history, setHistory] = useState<FormInstance[]>([]);
|
const [history, setHistory] = useState<FormInstance[]>([]);
|
||||||
@@ -50,6 +61,8 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
|
|||||||
const [handoffKind, setHandoffKind] = useState<"case" | "workflow">("case");
|
const [handoffKind, setHandoffKind] = useState<"case" | "workflow">("case");
|
||||||
const [handoffBinding, setHandoffBinding] = useState("");
|
const [handoffBinding, setHandoffBinding] = useState("");
|
||||||
const [compensating, setCompensating] = useState<FormHandoff | null>(null);
|
const [compensating, setCompensating] = useState<FormHandoff | null>(null);
|
||||||
|
const [confirmingSubmit, setConfirmingSubmit] = useState(false);
|
||||||
|
const [confirmingHandoff, setConfirmingHandoff] = useState(false);
|
||||||
|
|
||||||
const load = useCallback(async (signal?: AbortSignal) => {
|
const load = useCallback(async (signal?: AbortSignal) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -91,8 +104,13 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
|
|||||||
}
|
}
|
||||||
}, [definition, handoffKind]);
|
}, [definition, handoffKind]);
|
||||||
|
|
||||||
const editable = instance?.status === "started" || instance?.status === "draft";
|
const editableLifecycle = instance?.status === "started" || instance?.status === "draft";
|
||||||
const canSave = instance?.status === "draft" && definition?.allow_drafts;
|
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(
|
const changed = useMemo(
|
||||||
() => Boolean(instance && JSON.stringify(values) !== JSON.stringify(instance.values)),
|
() => Boolean(instance && JSON.stringify(values) !== JSON.stringify(instance.values)),
|
||||||
[instance, values]
|
[instance, values]
|
||||||
@@ -106,30 +124,43 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
|
|||||||
return grouped;
|
return grouped;
|
||||||
}, [instance]);
|
}, [instance]);
|
||||||
const localized = useMemo(
|
const localized = useMemo(
|
||||||
() => localizeDefinition(definition),
|
() => localizeDefinition(definition, language),
|
||||||
[definition]
|
[definition, language]
|
||||||
);
|
);
|
||||||
const groups = useMemo(
|
const groups = useMemo(
|
||||||
() => definition ? visibleGroups(definition, values) : [],
|
() => definition ? visibleGroups(definition, values) : [],
|
||||||
[definition, values]
|
[definition, values]
|
||||||
);
|
);
|
||||||
const mayHandoff = Boolean(instance && ["submitted", "validated", "needs_review", "accepted"].includes(instance.status) && instance.service_ref);
|
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() {
|
async function save(): Promise<boolean> {
|
||||||
if (!instance || !canSave || !changed || !changeReason.trim()) return;
|
if (!instance || !canSave || !changed || !changeReason.trim()) return false;
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
await saveFormDraft(settings, instance, values, changeReason.trim());
|
await saveFormDraft(settings, instance, values, changeReason.trim());
|
||||||
await load();
|
await load();
|
||||||
|
return true;
|
||||||
} catch (reason) {
|
} catch (reason) {
|
||||||
setError(reason instanceof Error ? reason.message : "The draft could not be saved.");
|
setError(reason instanceof Error ? reason.message : "The draft could not be saved.");
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
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() {
|
async function submit() {
|
||||||
if (!instance || !editable) return;
|
if (!instance || !editable) return;
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
@@ -195,7 +226,8 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
|
|||||||
Forms
|
Forms
|
||||||
</Button>
|
</Button>
|
||||||
{definition && <strong>{localized.title}</strong>}
|
{definition && <strong>{localized.title}</strong>}
|
||||||
{instance && <StatusBadge status={editable ? "active" : "inactive"} label={humanize(instance.status)} />}
|
{instance && <StatusBadge status={editableLifecycle ? "active" : "inactive"} label={stateLabel(instance.status)} />}
|
||||||
|
<DocumentationHelpLink reference={FORMS_RUNTIME_DOCUMENTATION} />
|
||||||
</div>
|
</div>
|
||||||
<PageScrollViewport className="form-instance-viewport">
|
<PageScrollViewport className="form-instance-viewport">
|
||||||
{error &&
|
{error &&
|
||||||
@@ -204,6 +236,23 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
|
|||||||
</DismissibleAlert>
|
</DismissibleAlert>
|
||||||
}
|
}
|
||||||
{loading && <LoadingIndicator label="Loading Form" />}
|
{loading && <LoadingIndicator label="Loading Form" />}
|
||||||
|
{!loading && instance && !editable &&
|
||||||
|
<ActionBlockerHint
|
||||||
|
tone="info"
|
||||||
|
reason={canEditPermission ? {
|
||||||
|
summary: "Read-only Form",
|
||||||
|
details: FORMS_RUNTIME_I18N.lifecycleReason
|
||||||
|
} : {
|
||||||
|
summary: "Read-only Form",
|
||||||
|
details: FORMS_RUNTIME_I18N.editReason,
|
||||||
|
requiredAction: FORMS_RUNTIME_I18N.permissionAction,
|
||||||
|
actor: FORMS_RUNTIME_I18N.permissionActor,
|
||||||
|
target: FORMS_RUNTIME_I18N.permissionDestination
|
||||||
|
}}
|
||||||
|
labels={{ requiredAction: FORMS_RUNTIME_I18N.requiredAction, actor: FORMS_RUNTIME_I18N.actor, target: FORMS_RUNTIME_I18N.destination }}
|
||||||
|
documentation={FORMS_RUNTIME_DOCUMENTATION}
|
||||||
|
/>
|
||||||
|
}
|
||||||
{!loading && instance && definition &&
|
{!loading && instance && definition &&
|
||||||
<div className="form-instance-content">
|
<div className="form-instance-content">
|
||||||
<section className="form-instance-main">
|
<section className="form-instance-main">
|
||||||
@@ -226,7 +275,7 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
|
|||||||
}}
|
}}
|
||||||
optionLabels={localized.optionLabels[field.key] ?? {}}
|
optionLabels={localized.optionLabels[field.key] ?? {}}
|
||||||
value={values[field.key]}
|
value={values[field.key]}
|
||||||
disabled={!editable || saving}
|
disabled={!editable || saving}
|
||||||
diagnostics={diagnostics.get(field.key) ?? []}
|
diagnostics={diagnostics.get(field.key) ?? []}
|
||||||
onChange={(value) => setValues((current) => {
|
onChange={(value) => setValues((current) => {
|
||||||
const next = { ...current };
|
const next = { ...current };
|
||||||
@@ -247,6 +296,7 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
|
|||||||
}
|
}
|
||||||
{editable &&
|
{editable &&
|
||||||
<div className="form-instance-actions">
|
<div className="form-instance-actions">
|
||||||
|
<DocumentationHelpLink reference={FORMS_RUNTIME_FIELD_DOCUMENTATION} />
|
||||||
{canSave &&
|
{canSave &&
|
||||||
<label className="form-change-reason">
|
<label className="form-change-reason">
|
||||||
<span>Change reason</span>
|
<span>Change reason</span>
|
||||||
@@ -260,12 +310,13 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
|
|||||||
{canSave &&
|
{canSave &&
|
||||||
<Button
|
<Button
|
||||||
onClick={() => void save()}
|
onClick={() => void save()}
|
||||||
disabled={!changed || !changeReason.trim() || saving}>
|
disabled={!changed || !changeReason.trim() || saving}
|
||||||
|
disabledReason={saving ? FORMS_RUNTIME_I18N.saving : !changed ? FORMS_RUNTIME_I18N.unchanged : !changeReason.trim() ? FORMS_RUNTIME_I18N.changeReason : undefined}>
|
||||||
<Save size={16} aria-hidden="true" />
|
<Save size={16} aria-hidden="true" />
|
||||||
Save draft
|
Save draft
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
<Button variant="primary" onClick={() => void submit()} disabled={saving}>
|
<Button variant="primary" onClick={() => setConfirmingSubmit(true)} disabled={saving} disabledReason={saving ? FORMS_RUNTIME_I18N.saving : undefined}>
|
||||||
<Send size={16} aria-hidden="true" />
|
<Send size={16} aria-hidden="true" />
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
@@ -288,7 +339,7 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
|
|||||||
{definition.handoff_kinds.includes("workflow") && <option value="workflow">Start Workflow</option>}
|
{definition.handoff_kinds.includes("workflow") && <option value="workflow">Start Workflow</option>}
|
||||||
</select>
|
</select>
|
||||||
<input value={handoffBinding} disabled={handoffBusy} onChange={(event) => setHandoffBinding(event.target.value)} placeholder="Target binding (optional)" aria-label="Exact target binding" />
|
<input value={handoffBinding} disabled={handoffBusy} onChange={(event) => setHandoffBinding(event.target.value)} placeholder="Target binding (optional)" aria-label="Exact target binding" />
|
||||||
<Button variant="primary" disabled={handoffBusy} onClick={() => void startHandoff()}><Send size={15} aria-hidden="true" />Start</Button>
|
<Button variant="primary" disabled={handoffBusy || !canHandoff} disabledReason={handoffBusy ? FORMS_RUNTIME_I18N.saving : !canHandoff ? FORMS_RUNTIME_I18N.handoffReason : undefined} onClick={() => setConfirmingHandoff(true)}><Send size={15} aria-hidden="true" />Start</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -296,13 +347,13 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
|
|||||||
<div className="form-handoff-list">
|
<div className="form-handoff-list">
|
||||||
{handoffs.map((item) =>
|
{handoffs.map((item) =>
|
||||||
<div className="form-handoff-row" key={item.effect_id}>
|
<div className="form-handoff-row" key={item.effect_id}>
|
||||||
<span><strong>{humanize(item.binding_kind)}</strong><small>{item.binding_reference}</small></span>
|
<span><strong>{domainLabel(item.binding_kind)}</strong><small>{item.binding_reference}</small></span>
|
||||||
<StatusBadge status={item.state === "accepted" || item.state === "reconciled" ? "active" : "inactive"} label={humanize(item.state)} />
|
<StatusBadge status={item.state === "accepted" || item.state === "reconciled" ? "active" : "inactive"} label={stateLabel(item.state)} />
|
||||||
{item.last_error && <span className="form-handoff-error">{item.last_error}</span>}
|
{item.last_error && <span className="form-handoff-error">{item.last_error}</span>}
|
||||||
<span className="form-handoff-actions">
|
<span className="form-handoff-actions">
|
||||||
{item.href && <Button onClick={() => navigate(item.href!)}><ExternalLink size={15} aria-hidden="true" />Open</Button>}
|
{item.href && <Button onClick={() => navigate(item.href!)}><ExternalLink size={15} aria-hidden="true" />Open</Button>}
|
||||||
{item.state === "rejected" && <Button disabled={handoffBusy} onClick={() => void handoffAction(item, "retry")}><RefreshCw size={15} aria-hidden="true" />Retry</Button>}
|
{item.state === "rejected" && <Button disabled={handoffBusy || !canHandoff} disabledReason={handoffBusy ? FORMS_RUNTIME_I18N.saving : !canHandoff ? FORMS_RUNTIME_I18N.handoffReason : undefined} onClick={() => void handoffAction(item, "retry")}><RefreshCw size={15} aria-hidden="true" />Retry</Button>}
|
||||||
{item.state === "outcome_unknown" && <Button disabled={handoffBusy} onClick={() => void handoffAction(item, "reconcile")}><RefreshCw size={15} aria-hidden="true" />Reconcile</Button>}
|
{item.state === "outcome_unknown" && <Button disabled={handoffBusy || !canHandoff} disabledReason={handoffBusy ? FORMS_RUNTIME_I18N.saving : !canHandoff ? FORMS_RUNTIME_I18N.handoffReason : undefined} onClick={() => void handoffAction(item, "reconcile")}><RefreshCw size={15} aria-hidden="true" />Reconcile</Button>}
|
||||||
{canAdmin && (item.state === "rejected" || item.state === "outcome_unknown") && <Button variant="danger" disabled={handoffBusy} onClick={() => setCompensating(item)}>Compensate</Button>}
|
{canAdmin && (item.state === "rejected" || item.state === "outcome_unknown") && <Button variant="danger" disabled={handoffBusy} onClick={() => setCompensating(item)}>Compensate</Button>}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -317,9 +368,9 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
|
|||||||
<ol>
|
<ol>
|
||||||
{events.map((event) =>
|
{events.map((event) =>
|
||||||
<li key={event.event_id}>
|
<li key={event.event_id}>
|
||||||
<strong>{humanize(event.status)}</strong>
|
<strong>{stateLabel(event.status)}</strong>
|
||||||
<span>{humanize(event.event_type)}</span>
|
<span>{humanize(event.event_type)}</span>
|
||||||
<time>{formatDateTime(event.occurred_at)}</time>
|
<time>{formatDateTime(event.occurred_at, language)}</time>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</ol>
|
</ol>
|
||||||
@@ -331,7 +382,7 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
|
|||||||
<li key={item.revision}>
|
<li key={item.revision}>
|
||||||
<strong>Revision {item.revision}</strong>
|
<strong>Revision {item.revision}</strong>
|
||||||
<span>{item.change_reason}</span>
|
<span>{item.change_reason}</span>
|
||||||
<time>{formatDateTime(item.recorded_at)}</time>
|
<time>{formatDateTime(item.recorded_at, language)}</time>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</ol>
|
</ol>
|
||||||
@@ -341,6 +392,30 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
|
|||||||
}
|
}
|
||||||
</PageScrollViewport>
|
</PageScrollViewport>
|
||||||
</div>
|
</div>
|
||||||
|
<ConfirmDialog
|
||||||
|
open={confirmingSubmit}
|
||||||
|
title="i18n:govoplan-forms-runtime.submit_title"
|
||||||
|
message="i18n:govoplan-forms-runtime.submit_message"
|
||||||
|
confirmLabel="Submit"
|
||||||
|
busy={saving}
|
||||||
|
onCancel={() => setConfirmingSubmit(false)}
|
||||||
|
onConfirm={() => {
|
||||||
|
setConfirmingSubmit(false);
|
||||||
|
void submit();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ConfirmDialog
|
||||||
|
open={confirmingHandoff}
|
||||||
|
title="i18n:govoplan-forms-runtime.handoff_title"
|
||||||
|
message={i18nMessage("i18n:govoplan-forms-runtime.handoff_message", { kind: domainLabel(handoffKind) })}
|
||||||
|
confirmLabel="Start"
|
||||||
|
busy={handoffBusy}
|
||||||
|
onCancel={() => setConfirmingHandoff(false)}
|
||||||
|
onConfirm={() => {
|
||||||
|
setConfirmingHandoff(false);
|
||||||
|
void startHandoff();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={Boolean(compensating)}
|
open={Boolean(compensating)}
|
||||||
title="Compensate handoff"
|
title="Compensate handoff"
|
||||||
@@ -565,7 +640,7 @@ function comparable(left: unknown, right: unknown, compare: (left: number | stri
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function localizeDefinition(definition: FormDefinition | null) {
|
function localizeDefinition(definition: FormDefinition | null, language: string) {
|
||||||
const canonical = {
|
const canonical = {
|
||||||
title: definition?.title ?? "",
|
title: definition?.title ?? "",
|
||||||
description: definition?.description ?? null,
|
description: definition?.description ?? null,
|
||||||
@@ -575,8 +650,8 @@ function localizeDefinition(definition: FormDefinition | null) {
|
|||||||
sectionTitles: {} as Record<string, string>
|
sectionTitles: {} as Record<string, string>
|
||||||
};
|
};
|
||||||
if (!definition?.localizations?.length) return canonical;
|
if (!definition?.localizations?.length) return canonical;
|
||||||
const browserLocales = navigator.languages.map((value) => value.toLowerCase());
|
const requestedLocale = language.toLowerCase();
|
||||||
const localization = definition.localizations.find((item) => browserLocales.some((locale) => locale === item.locale.toLowerCase() || locale.startsWith(`${item.locale.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());
|
?? definition.localizations.find((item) => item.locale.toLowerCase() === definition.fallback_locale?.toLowerCase());
|
||||||
if (!localization) return canonical;
|
if (!localization) return canonical;
|
||||||
return {
|
return {
|
||||||
@@ -589,10 +664,18 @@ function localizeDefinition(definition: FormDefinition | null) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDateTime(value: string): string {
|
function formatDateTime(value: string, locale?: string): string {
|
||||||
return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(new Date(value));
|
return new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short" }).format(new Date(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
function humanize(value: string): string {
|
function humanize(value: string): string {
|
||||||
return value.replace(/[_:.-]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
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}`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,20 +2,25 @@ import { RefreshCw } from "lucide-react";
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
DocumentationHelpLink,
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
LoadingIndicator,
|
LoadingIndicator,
|
||||||
PageScrollViewport,
|
PageScrollViewport,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
|
i18nMessage,
|
||||||
useGuardedNavigate,
|
useGuardedNavigate,
|
||||||
|
usePlatformLanguage,
|
||||||
type PlatformRouteContext
|
type PlatformRouteContext
|
||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
import { listFormInstances, type FormInstance } from "../../api/formsRuntime";
|
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"];
|
const OPEN_STATUSES = ["started", "draft", "submitted", "validated", "needs_review"];
|
||||||
|
|
||||||
export default function FormsRuntimePage({ settings }: PlatformRouteContext) {
|
export default function FormsRuntimePage({ settings }: PlatformRouteContext) {
|
||||||
const navigate = useGuardedNavigate();
|
const navigate = useGuardedNavigate();
|
||||||
|
const { language } = usePlatformLanguage();
|
||||||
const [items, setItems] = useState<FormInstance[]>([]);
|
const [items, setItems] = useState<FormInstance[]>([]);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [status, setStatus] = useState("open");
|
const [status, setStatus] = useState("open");
|
||||||
@@ -50,7 +55,7 @@ export default function FormsRuntimePage({ settings }: PlatformRouteContext) {
|
|||||||
<main className="forms-runtime-page">
|
<main className="forms-runtime-page">
|
||||||
<div className="forms-runtime-shell">
|
<div className="forms-runtime-shell">
|
||||||
<div className="forms-runtime-toolbar">
|
<div className="forms-runtime-toolbar">
|
||||||
<Button onClick={() => void load()} disabled={loading}>
|
<Button onClick={() => void load()} disabled={loading} disabledReason={loading ? FORMS_RUNTIME_I18N.loading : undefined}>
|
||||||
<RefreshCw size={16} aria-hidden="true" />
|
<RefreshCw size={16} aria-hidden="true" />
|
||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
@@ -68,7 +73,8 @@ export default function FormsRuntimePage({ settings }: PlatformRouteContext) {
|
|||||||
<option value="archived">Archived</option>
|
<option value="archived">Archived</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<span className="forms-runtime-count">{total} forms</span>
|
<span className="forms-runtime-count">{i18nMessage("i18n:govoplan-forms-runtime.form_count", { total })}</span>
|
||||||
|
<DocumentationHelpLink reference={FORMS_RUNTIME_DOCUMENTATION} />
|
||||||
</div>
|
</div>
|
||||||
<PageScrollViewport className="forms-runtime-list-viewport">
|
<PageScrollViewport className="forms-runtime-list-viewport">
|
||||||
{error &&
|
{error &&
|
||||||
@@ -93,8 +99,8 @@ export default function FormsRuntimePage({ settings }: PlatformRouteContext) {
|
|||||||
<strong>{item.definition_ref.label ?? humanize(item.definition_ref.object_id)}</strong>
|
<strong>{item.definition_ref.label ?? humanize(item.definition_ref.object_id)}</strong>
|
||||||
<span>Revision {item.definition_ref.version ?? "-"}</span>
|
<span>Revision {item.definition_ref.version ?? "-"}</span>
|
||||||
</span>
|
</span>
|
||||||
<span>{formatDateTime(item.recorded_at)}</span>
|
<span>{formatDateTime(item.recorded_at, language)}</span>
|
||||||
<StatusBadge status={isOpen(item.status) ? "active" : "inactive"} label={humanize(item.status)} />
|
<StatusBadge status={isOpen(item.status) ? "active" : "inactive"} label={stateLabel(item.status)} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -109,10 +115,14 @@ function isOpen(status: string): boolean {
|
|||||||
return OPEN_STATUSES.includes(status);
|
return OPEN_STATUSES.includes(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDateTime(value: string): string {
|
function formatDateTime(value: string, locale?: string): string {
|
||||||
return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(new Date(value));
|
return new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short" }).format(new Date(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
function humanize(value: string): string {
|
function humanize(value: string): string {
|
||||||
return value.replace(/[_:.-]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
return value.replace(/[_:.-]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stateLabel(value: string): string {
|
||||||
|
return `i18n:govoplan-forms-runtime.state_${value}`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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<keyof typeof en, string> = {
|
||||||
|
"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 };
|
||||||
+4
-2
@@ -1,5 +1,6 @@
|
|||||||
import { createElement, lazy } from "react";
|
import { createElement, lazy } from "react";
|
||||||
import type { PlatformWebModule } from "@govoplan/core-webui";
|
import type { PlatformWebModule } from "@govoplan/core-webui";
|
||||||
|
import { generatedTranslations } from "./i18n/generatedTranslations";
|
||||||
import "./styles/forms-runtime.css";
|
import "./styles/forms-runtime.css";
|
||||||
|
|
||||||
|
|
||||||
@@ -12,10 +13,11 @@ const routeScopes = [
|
|||||||
|
|
||||||
export const formsRuntimeModule: PlatformWebModule = {
|
export const formsRuntimeModule: PlatformWebModule = {
|
||||||
id: "forms_runtime",
|
id: "forms_runtime",
|
||||||
label: "Forms",
|
label: "i18n:govoplan-forms-runtime.forms",
|
||||||
version: "0.1.14",
|
version: "0.1.14",
|
||||||
dependencies: ["access", "forms"],
|
dependencies: ["access", "forms"],
|
||||||
optionalDependencies: ["files", "approvals", "workflow_engine", "portal", "cases", "policy", "audit"],
|
optionalDependencies: ["files", "approvals", "workflow_engine", "portal", "cases", "policy", "audit"],
|
||||||
|
translations: generatedTranslations,
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: "/forms-runtime",
|
path: "/forms-runtime",
|
||||||
@@ -35,7 +37,7 @@ export const formsRuntimeModule: PlatformWebModule = {
|
|||||||
navItems: [
|
navItems: [
|
||||||
{
|
{
|
||||||
to: "/forms-runtime",
|
to: "/forms-runtime",
|
||||||
label: "Forms",
|
label: "i18n:govoplan-forms-runtime.forms",
|
||||||
iconName: "form",
|
iconName: "form",
|
||||||
anyOf: routeScopes,
|
anyOf: routeScopes,
|
||||||
order: 37,
|
order: 37,
|
||||||
|
|||||||
@@ -48,6 +48,11 @@
|
|||||||
padding: 16px 18px 24px;
|
padding: 16px 18px 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-instance-viewport > .action-blocker-hint {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.forms-runtime-list {
|
.forms-runtime-list {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
|
|||||||
Reference in New Issue
Block a user