Migrate Templates interface patterns

This commit is contained in:
2026-08-03 13:34:37 +02:00
parent 3551c48e14
commit 72fafa23c7
7 changed files with 399 additions and 28 deletions
+32
View File
@@ -0,0 +1,32 @@
# Templates Interface Pattern Migration
This migration applies the GovOPlaN interface pattern language to the Template
library, immutable-revision editor, preview/final-output workspace, and render
evidence history.
## Surface Inventory
| Surface | Archetype | Consequence class | Contract |
| --- | --- | --- | --- |
| `/templates` library | Governed directory | Select, create, or retire Template | Shared loading, empty, permission, read-only, disabled-reason, and help states |
| Definition editor | Consequential definition editor | Save immutable revision | Guarded draft with scope, usage, required-data, layout, and content semantics |
| Publish action | Governed lifecycle transition | Make one revision consumable | Permission/lifecycle explanation and explicit confirmation |
| Preview/final output | Evidence-producing preview | Validate sample or render final output | Compatibility diagnostics, published-revision gate, confirmation, and retained hashes |
| Revision/render history | Evidence register | Inspect immutable history | Stable status, timestamps, digests, artifact availability, and bounded download |
## Consequence And Availability Rules
- Saving creates a new immutable revision. Publishing never rewrites an older
revision or its render evidence.
- Inherited or policy-constrained Templates remain visible but identify why
they are read-only, who can change that, and where to continue.
- Final output requires a published revision and explicit confirmation. It
records template, input, output, and renderer evidence and may use Files only
through the optional artifact capability.
- Deleting prevents future selection while retained revisions and renders stay
governed by retention policy.
Backend and WebUI manifests publish the same surface identifiers. English and
German catalogues cover module-owned vocabulary, contextual help resolves from
manifest documentation, and create/editor drafts are guarded across selection,
reload, and navigation.
+54 -2
View File
@@ -85,7 +85,15 @@ DOCUMENTATION = (
layer="available",
documentation_types=("admin", "user"),
audience=("operator", "module_admin", "product_owner"),
metadata={"seed": True},
metadata={
"seed": True,
"help_contexts": [
"templates.page",
"templates.library",
"templates.editor",
"templates.state.read-only",
],
},
),
DocumentationTopic(
id="templates.printable-output",
@@ -101,7 +109,51 @@ DOCUMENTATION = (
documentation_types=("admin", "user"),
audience=("operator", "module_admin", "product_owner"),
related_modules=("files", "dist_lists", "campaigns", "audit"),
metadata={"seed": True},
metadata={
"seed": True,
"help_contexts": [
"templates.preview",
"templates.action.validate-preview",
"templates.action.render-final",
"templates.evidence.render",
],
},
),
DocumentationTopic(
id="templates.reference.fields-and-consequences",
title="Template fields and lifecycle consequences",
summary="Scope, usage, data contract, publication, rendering, and deletion semantics for reusable Templates.",
body=(
"Visibility determines which tenant, group, or user scope may discover the Template; inherited Templates may be read-only. "
"Usages are capability contexts that constrain where a Template may be selected. Required fields form the compatibility "
"contract checked against supplied data before rendering. Saving creates a new immutable revision. Publishing marks one "
"revision as available for final output without rewriting older revisions or evidence. Preview validates and renders bounded "
"sample output; final rendering requires the published revision and records template, input, and output hashes plus renderer "
"evidence. Files may retain the artifact when its optional capability is available. Deletion removes the Template from future "
"selection but does not rewrite retained render evidence."
),
layer="available",
documentation_types=("admin", "user"),
audience=("operator", "module_admin", "product_owner"),
related_modules=("files", "dist_lists", "campaigns", "audit", "policy"),
metadata={
"seed": True,
"help_contexts": [
"templates.field.type",
"templates.field.locale",
"templates.field.visibility",
"templates.field.usages",
"templates.field.required-data",
"templates.action.publish",
"templates.action.delete",
],
"consequence_classes": {
"save_revision": "Creates a new immutable Template revision.",
"publish_revision": "Makes the selected immutable revision eligible for final consumer output.",
"render_final": "Creates retained render evidence and may persist an artifact through Files.",
"delete_template": "Prevents future selection without rewriting retained revisions or render evidence.",
},
},
),
)
@@ -0,0 +1,37 @@
from __future__ import annotations
import unittest
from govoplan_templates.backend.manifest import manifest
class TemplatesInterfaceDocumentationContractTests(unittest.TestCase):
def test_route_and_surfaces_remain_declared(self) -> None:
frontend = manifest.frontend
self.assertIsNotNone(frontend)
self.assertEqual({"/templates"}, {item.path for item in frontend.routes}) # type: ignore[union-attr]
self.assertEqual(
{
"templates.page",
"templates.library",
"templates.editor",
"templates.preview",
},
{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}
library = topics["templates.library"]
output = topics["templates.printable-output"]
reference = topics["templates.reference.fields-and-consequences"]
self.assertIn("templates.state.read-only", library.metadata["help_contexts"])
self.assertIn("templates.action.render-final", output.metadata["help_contexts"])
self.assertIn("templates.field.usages", reference.metadata["help_contexts"])
self.assertIn("publish_revision", reference.metadata["consequence_classes"])
self.assertIn("delete_template", reference.metadata["consequence_classes"])
if __name__ == "__main__":
unittest.main()
+83 -24
View File
@@ -12,9 +12,11 @@ import {
import { useCallback, useEffect, useMemo, useState } from "react";
import {
ApiError,
ActionBlockerHint,
Button,
ConfirmDialog,
Dialog,
DocumentationHelpLink,
DismissibleAlert,
FormField,
IconButton,
@@ -24,6 +26,7 @@ import {
ToggleSwitch,
formatDateTime,
hasScope,
useUnsavedChanges,
useUnsavedDraftGuard,
type ApiSettings,
type AuthInfo
@@ -49,6 +52,12 @@ import {
type TemplateRevision,
type TemplateType
} from "../../api/templates";
import {
TEMPLATE_FIELDS_DOCUMENTATION,
TEMPLATE_OUTPUT_DOCUMENTATION,
TEMPLATES_DOCUMENTATION,
TEMPLATES_I18N
} from "./interfacePatterns";
type Props = { settings: ApiSettings; auth: AuthInfo };
type WorkspaceView = "definition" | "preview";
@@ -83,6 +92,8 @@ export default function TemplatesPage({ settings, auth }: Props) {
const [createName, setCreateName] = useState("");
const [createType, setCreateType] = useState<TemplateType>("form_letter");
const [deleteOpen, setDeleteOpen] = useState(false);
const [publishOpen, setPublishOpen] = useState(false);
const [finalRenderOpen, setFinalRenderOpen] = useState(false);
const [sampleText, setSampleText] = useState('{\n "name": "Ada Example",\n "address": "Main Street 1",\n "postal_code": "10115",\n "city": "Berlin"\n}');
const [usage, setUsage] = useState("campaign.postal");
const [outputFormat, setOutputFormat] = useState<"html" | "text">("html");
@@ -91,6 +102,7 @@ export default function TemplatesPage({ settings, auth }: Props) {
const [render, setRender] = useState<TemplateRender | null>(null);
const [revisions, setRevisions] = useState<TemplateRevision[]>([]);
const [renders, setRenders] = useState<TemplateRender[]>([]);
const { requestDiscard } = useUnsavedChanges();
const selected = items.find((item) => item.id === selectedId) ?? null;
const canWrite = hasScope(auth, "templates:template:write") || hasScope(auth, "templates:template:admin");
@@ -173,10 +185,16 @@ export default function TemplatesPage({ settings, auth }: Props) {
}
};
useUnsavedDraftGuard({ dirty, onSave: save, onDiscard: () => applyItem(selected) });
useUnsavedDraftGuard({
dirty,
onSave: save,
onDiscard: () => applyItem(selected),
title: "i18n:govoplan-templates.unsaved_title",
message: "i18n:govoplan-templates.unsaved_message"
});
const create = async () => {
if (!createName.trim()) return;
const create = async (): Promise<boolean> => {
if (!createName.trim()) return false;
setBusy(true);
setError("");
try {
@@ -188,18 +206,39 @@ export default function TemplatesPage({ settings, auth }: Props) {
setCreateName("");
setSuccess(`Created ${created.name}.`);
await reload(created.id);
return true;
} catch (caught) {
setError(errorMessage(caught));
return false;
} finally {
setBusy(false);
}
};
useUnsavedDraftGuard({
dirty: Boolean(createOpen && createName.trim()),
onSave: create,
onDiscard: () => {
setCreateOpen(false);
setCreateName("");
setCreateType("form_letter");
},
title: "i18n:govoplan-templates.create_unsaved_title",
message: "i18n:govoplan-templates.create_unsaved_message"
});
const closeCreate = () => {
if (busy) return;
if (createName.trim()) requestDiscard(() => setCreateOpen(false));
else setCreateOpen(false);
};
const publish = async () => {
if (!selected || dirty) return;
setBusy(true);
try {
const updated = await publishTemplate(settings, selected);
setPublishOpen(false);
setSuccess(`Published revision ${updated.current_revision}.`);
await reload(updated.id);
} catch (caught) {
@@ -242,6 +281,7 @@ export default function TemplatesPage({ settings, auth }: Props) {
persistToFiles
});
setRender(nextRender);
if (final) setFinalRenderOpen(false);
setRenders(await listTemplateRenders(settings, selected.id));
setSuccess(`${final ? "Final" : "Preview"} output rendered with ${nextRender.item_count} item(s).`);
} catch (caught) {
@@ -257,7 +297,7 @@ export default function TemplatesPage({ settings, auth }: Props) {
<aside className="templates-sidebar">
<div className="templates-sidebar-toolbar">
<strong>Template library</strong>
<IconButton label="Add template" icon={<Plus size={17} />} variant="primary" disabled={!canWrite} onClick={() => setCreateOpen(true)} />
<IconButton label="Add template" icon={<Plus size={17} />} variant="primary" disabled={!canWrite} disabledReason={!canWrite ? TEMPLATES_I18N.writeReason : undefined} onClick={() => requestDiscard(() => setCreateOpen(true))} />
</div>
<div className="templates-search"><input value={search} onChange={(event) => setSearch(event.target.value)} placeholder="Search templates" /></div>
<div className="templates-list">
@@ -268,8 +308,10 @@ export default function TemplatesPage({ settings, auth }: Props) {
className={item.id === selectedId ? "is-selected" : ""}
onClick={() => {
if (item.id === selectedId) return;
setSelectedId(item.id);
applyItem(item);
requestDiscard(() => {
setSelectedId(item.id);
applyItem(item);
});
}}
>
<span><strong>{item.name}</strong><small>{typeLabel(item.template_type)} · revision {item.current_revision}</small></span>
@@ -287,10 +329,11 @@ export default function TemplatesPage({ settings, auth }: Props) {
<small>{selected ? `${typeLabel(selected.template_type)} · ${selected.revision.locale}` : ""}</small>
</span>
<div className="templates-toolbar-actions">
<IconButton label="Discard and reload" icon={<RefreshCw size={17} />} onClick={() => void reload(selectedId)} />
<Button variant="primary" disabled={!selected || readOnly || !dirty || busy} onClick={() => void save()}><Save size={16} /> Save revision</Button>
<Button disabled={!selected || !canPublish || dirty || busy} onClick={() => void publish()}><FileCheck2 size={16} /> Publish</Button>
<IconButton label="Delete template" icon={<Trash2 size={17} />} variant="danger" disabled={!selected || readOnly} onClick={() => setDeleteOpen(true)} />
<DocumentationHelpLink reference={TEMPLATES_DOCUMENTATION} />
<IconButton label="Discard and reload" icon={<RefreshCw size={17} />} disabled={loading || busy} disabledReason={loading ? TEMPLATES_I18N.loading : busy ? TEMPLATES_I18N.busy : undefined} onClick={() => requestDiscard(() => void reload(selectedId))} />
<Button variant="primary" disabled={!selected || readOnly || !dirty || busy} disabledReason={busy ? TEMPLATES_I18N.busy : !selected ? TEMPLATES_I18N.noSelection : readOnly ? (canWrite ? TEMPLATES_I18N.readOnlyReason : TEMPLATES_I18N.writeReason) : !dirty ? TEMPLATES_I18N.noChanges : undefined} onClick={() => void save()}><Save size={16} /> Save revision</Button>
<Button disabled={!selected || !canPublish || dirty || busy} disabledReason={busy ? TEMPLATES_I18N.busy : !selected ? TEMPLATES_I18N.noSelection : !canPublish ? TEMPLATES_I18N.publishReason : dirty ? TEMPLATES_I18N.saveBeforeAction : undefined} onClick={() => setPublishOpen(true)}><FileCheck2 size={16} /> Publish</Button>
<IconButton label="Delete template" icon={<Trash2 size={17} />} variant="danger" disabled={!selected || readOnly} disabledReason={!selected ? TEMPLATES_I18N.noSelection : readOnly ? (canWrite ? TEMPLATES_I18N.readOnlyReason : TEMPLATES_I18N.writeReason) : undefined} onClick={() => setDeleteOpen(true)} />
<SegmentedControl
value={view}
onChange={setView}
@@ -303,6 +346,18 @@ export default function TemplatesPage({ settings, auth }: Props) {
<div className="templates-alerts">
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
{success && <DismissibleAlert tone="success" resetKey={success}>{success}</DismissibleAlert>}
{selected && readOnly && <ActionBlockerHint
tone="info"
reason={{
summary: "Template is read-only",
details: canWrite ? TEMPLATES_I18N.readOnlyReason : TEMPLATES_I18N.writeReason,
requiredAction: TEMPLATES_I18N.permissionAction,
actor: TEMPLATES_I18N.permissionActor,
target: TEMPLATES_I18N.permissionDestination
}}
labels={{ requiredAction: TEMPLATES_I18N.requiredAction, actor: TEMPLATES_I18N.actor, target: TEMPLATES_I18N.destination }}
documentation={TEMPLATES_DOCUMENTATION}
/>}
</div>
<LoadingFrame loading={loading} label="Loading templates">
@@ -320,11 +375,12 @@ export default function TemplatesPage({ settings, auth }: Props) {
compatibility={compatibility}
render={render}
disabled={busy || dirty || !canRender}
disabledReason={busy ? TEMPLATES_I18N.busy : dirty ? TEMPLATES_I18N.saveBeforeAction : !canRender ? TEMPLATES_I18N.renderReason : undefined}
onSampleText={setSampleText}
onUsage={setUsage}
onOutputFormat={setOutputFormat}
onPersistToFiles={setPersistToFiles}
onRender={runRender}
onRender={(final) => final ? setFinalRenderOpen(true) : void runRender(false)}
onDownload={() => render && void downloadTemplateRender(settings, render).catch((caught) => setError(errorMessage(caught)))}
/>
<RenderHistory renders={renders} settings={settings} onError={setError} />
@@ -334,12 +390,14 @@ export default function TemplatesPage({ settings, auth }: Props) {
</section>
</div>
<Dialog open={createOpen} title="Add template" onClose={() => setCreateOpen(false)} footer={<><Button onClick={() => setCreateOpen(false)}>Cancel</Button><Button variant="primary" disabled={!createName.trim() || busy} onClick={() => void create()}>Create</Button></>}>
<Dialog open={createOpen} title="Add template" onClose={closeCreate} closeDisabled={busy} footer={<><Button onClick={closeCreate} disabled={busy} disabledReason={busy ? TEMPLATES_I18N.busy : undefined}>Cancel</Button><Button variant="primary" disabled={!createName.trim() || busy} disabledReason={busy ? TEMPLATES_I18N.busy : !createName.trim() ? TEMPLATES_I18N.incomplete : undefined} onClick={() => void create()}>Create</Button></>}>
<div className="templates-dialog-form">
<FormField label="Name"><input autoFocus value={createName} onChange={(event) => setCreateName(event.target.value)} /></FormField>
<FormField label="Type"><select value={createType} onChange={(event) => setCreateType(event.target.value as TemplateType)}>{TEMPLATE_TYPES.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}</select></FormField>
<FormField label="Name" documentation={TEMPLATE_FIELDS_DOCUMENTATION}><input autoFocus value={createName} onChange={(event) => setCreateName(event.target.value)} /></FormField>
<FormField label="Type" documentation={TEMPLATE_FIELDS_DOCUMENTATION}><select value={createType} onChange={(event) => setCreateType(event.target.value as TemplateType)}>{TEMPLATE_TYPES.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}</select></FormField>
</div>
</Dialog>
<ConfirmDialog open={publishOpen} title="i18n:govoplan-templates.publish_title" message="i18n:govoplan-templates.publish_message" confirmLabel="Publish" busy={busy} onCancel={() => setPublishOpen(false)} onConfirm={() => void publish()} />
<ConfirmDialog open={finalRenderOpen} title="i18n:govoplan-templates.render_title" message="i18n:govoplan-templates.render_message" confirmLabel="Render final output" busy={busy} onCancel={() => setFinalRenderOpen(false)} onConfirm={() => void runRender(true)} />
<ConfirmDialog open={deleteOpen} title="Delete template?" message="Existing render evidence remains until module retention removes it. Consumers can no longer select this template." confirmLabel="Delete" tone="danger" busy={busy} onCancel={() => setDeleteOpen(false)} onConfirm={() => void remove()} />
</main>
);
@@ -386,11 +444,11 @@ function DefinitionEditor({ draft, disabled, auth, onChange }: { draft: Template
return (
<div className="templates-definition">
<div className="templates-definition-fields">
<FormField label="Name"><input disabled={disabled} value={draft.name} onChange={(event) => update("name", event.target.value)} /></FormField>
<FormField label="Type"><select disabled={disabled} value={draft.template_type} onChange={(event) => update("template_type", event.target.value as TemplateType)}>{TEMPLATE_TYPES.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}</select></FormField>
<FormField label="Locale"><input disabled={disabled} value={draft.locale} onChange={(event) => update("locale", event.target.value)} /></FormField>
<FormField label="Visibility"><select disabled={disabled} value={scopeValue} onChange={(event) => { const [scopeType, scopeId] = event.target.value.split(":", 2); onChange({ ...draft, scope_type: scopeType as TemplatePayload["scope_type"], scope_id: scopeId || null }); }}>{scopeOptions.map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}</select></FormField>
<FormField label="Usages" help="Comma-separated capability contexts, for example campaign.postal or addresses.labels."><input disabled={disabled} value={draft.usages.join(", ")} onChange={(event) => update("usages", splitValues(event.target.value))} /></FormField>
<FormField label="Name" documentation={TEMPLATE_FIELDS_DOCUMENTATION}><input disabled={disabled} value={draft.name} onChange={(event) => update("name", event.target.value)} /></FormField>
<FormField label="Type" documentation={TEMPLATE_FIELDS_DOCUMENTATION}><select disabled={disabled} value={draft.template_type} onChange={(event) => update("template_type", event.target.value as TemplateType)}>{TEMPLATE_TYPES.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}</select></FormField>
<FormField label="Locale" documentation={TEMPLATE_FIELDS_DOCUMENTATION}><input disabled={disabled} value={draft.locale} onChange={(event) => update("locale", event.target.value)} /></FormField>
<FormField label="Visibility" documentation={TEMPLATE_FIELDS_DOCUMENTATION}><select disabled={disabled} value={scopeValue} onChange={(event) => { const [scopeType, scopeId] = event.target.value.split(":", 2); onChange({ ...draft, scope_type: scopeType as TemplatePayload["scope_type"], scope_id: scopeId || null }); }}>{scopeOptions.map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}</select></FormField>
<FormField label="Usages" help="Comma-separated capability contexts, for example campaign.postal or addresses.labels." documentation={TEMPLATE_FIELDS_DOCUMENTATION}><input disabled={disabled} value={draft.usages.join(", ")} onChange={(event) => update("usages", splitValues(event.target.value))} /></FormField>
<FormField label="Description"><input disabled={disabled} value={draft.description ?? ""} onChange={(event) => update("description", event.target.value || null)} /></FormField>
</div>
@@ -431,7 +489,7 @@ function DefinitionEditor({ draft, disabled, auth, onChange }: { draft: Template
);
}
function PreviewPanel({ item, sampleText, usage, outputFormat, persistToFiles, compatibility, render, disabled, onSampleText, onUsage, onOutputFormat, onPersistToFiles, onRender, onDownload }: {
function PreviewPanel({ item, sampleText, usage, outputFormat, persistToFiles, compatibility, render, disabled, disabledReason, onSampleText, onUsage, onOutputFormat, onPersistToFiles, onRender, onDownload }: {
item: TemplateDefinition;
sampleText: string;
usage: string;
@@ -440,6 +498,7 @@ function PreviewPanel({ item, sampleText, usage, outputFormat, persistToFiles, c
compatibility: TemplateCompatibility | null;
render: TemplateRender | null;
disabled: boolean;
disabledReason?: string;
onSampleText: (value: string) => void;
onUsage: (value: string) => void;
onOutputFormat: (value: "html" | "text") => void;
@@ -452,14 +511,14 @@ function PreviewPanel({ item, sampleText, usage, outputFormat, persistToFiles, c
<section className="templates-section">
<div className="templates-section-heading"><strong>Validated sample input</strong><small>Preview and final output use the same pinned revision and canonical input.</small></div>
<div className="templates-preview-controls">
<FormField label="Usage"><select value={usage} onChange={(event) => onUsage(event.target.value)}>{item.revision.usages.map((value) => <option key={value} value={value}>{value}</option>)}</select></FormField>
<FormField label="Output"><SegmentedControl value={outputFormat} onChange={onOutputFormat} options={[{ id: "html", label: "Printable HTML" }, { id: "text", label: "Plain text" }]} ariaLabel="Output format" /></FormField>
<FormField label="Usage" documentation={TEMPLATE_OUTPUT_DOCUMENTATION}><select value={usage} onChange={(event) => onUsage(event.target.value)}>{item.revision.usages.map((value) => <option key={value} value={value}>{value}</option>)}</select></FormField>
<FormField label="Output" documentation={TEMPLATE_OUTPUT_DOCUMENTATION}><SegmentedControl value={outputFormat} onChange={onOutputFormat} options={[{ id: "html", label: "Printable HTML" }, { id: "text", label: "Plain text" }]} ariaLabel="Output format" /></FormField>
<ToggleSwitch checked={persistToFiles} label="Store in Files when available" onChange={onPersistToFiles} />
</div>
<textarea className="templates-sample" value={sampleText} onChange={(event) => onSampleText(event.target.value)} spellCheck={false} aria-label="Sample item JSON" />
<div className="templates-preview-actions">
<Button disabled={disabled} onClick={() => onRender(false)}><Eye size={16} /> Validate and preview</Button>
<Button variant="primary" disabled={disabled || !item.revision.published_at} disabledReason={!item.revision.published_at ? "Publish this revision before producing final output." : undefined} onClick={() => onRender(true)}><Send size={16} /> Render final output</Button>
<Button disabled={disabled} disabledReason={disabled ? disabledReason : undefined} onClick={() => onRender(false)}><Eye size={16} /> Validate and preview</Button>
<Button variant="primary" disabled={disabled || !item.revision.published_at} disabledReason={disabled ? disabledReason : !item.revision.published_at ? "Publish this revision before producing final output." : undefined} onClick={() => onRender(true)}><Send size={16} /> Render final output</Button>
</div>
</section>
@@ -0,0 +1,35 @@
import type { DocumentationHelpReference } from "@govoplan/core-webui";
export const TEMPLATES_DOCUMENTATION = {
topicId: "templates.library",
documentationType: "user"
} satisfies DocumentationHelpReference;
export const TEMPLATE_FIELDS_DOCUMENTATION = {
topicId: "templates.reference.fields-and-consequences",
documentationType: "admin"
} satisfies DocumentationHelpReference;
export const TEMPLATE_OUTPUT_DOCUMENTATION = {
topicId: "templates.printable-output",
documentationType: "user"
} satisfies DocumentationHelpReference;
export const TEMPLATES_I18N = {
loading: "i18n:govoplan-templates.loading_reason",
busy: "i18n:govoplan-templates.busy_reason",
writeReason: "i18n:govoplan-templates.write_permission_reason",
publishReason: "i18n:govoplan-templates.publish_permission_reason",
renderReason: "i18n:govoplan-templates.render_permission_reason",
readOnlyReason: "i18n:govoplan-templates.read_only_reason",
noSelection: "i18n:govoplan-templates.no_selection_reason",
noChanges: "i18n:govoplan-templates.no_changes_reason",
incomplete: "i18n:govoplan-templates.incomplete_reason",
saveBeforeAction: "i18n:govoplan-templates.save_before_action_reason",
requiredAction: "i18n:govoplan-templates.required_action",
actor: "i18n:govoplan-templates.actor",
destination: "i18n:govoplan-templates.destination",
permissionAction: "i18n:govoplan-templates.permission_action",
permissionActor: "i18n:govoplan-templates.permission_actor",
permissionDestination: "i18n:govoplan-templates.permission_destination"
} as const;
+147
View File
@@ -0,0 +1,147 @@
import type { PlatformTranslations } from "@govoplan/core-webui";
const en = {
"i18n:govoplan-templates.templates": "Templates",
"i18n:govoplan-templates.library": "Template library",
"i18n:govoplan-templates.editor": "Template editor",
"i18n:govoplan-templates.preview": "Template preview and output",
"i18n:govoplan-templates.loading_reason": "Templates are still loading.",
"i18n:govoplan-templates.busy_reason": "Another Template action is still running.",
"i18n:govoplan-templates.write_permission_reason": "Your account may not create or revise Templates.",
"i18n:govoplan-templates.publish_permission_reason": "Your account may not publish Template revisions.",
"i18n:govoplan-templates.render_permission_reason": "Your account may not render Template output.",
"i18n:govoplan-templates.read_only_reason": "This Template is inherited or otherwise read-only in the current scope.",
"i18n:govoplan-templates.no_selection_reason": "Select or create a Template first.",
"i18n:govoplan-templates.no_changes_reason": "There are no definition changes to save.",
"i18n:govoplan-templates.incomplete_reason": "Complete the required name and usage fields first.",
"i18n:govoplan-templates.save_before_action_reason": "Save the current revision before publishing or rendering it.",
"i18n:govoplan-templates.required_action": "Required action",
"i18n:govoplan-templates.actor": "Responsible actor",
"i18n:govoplan-templates.destination": "Where to continue",
"i18n:govoplan-templates.permission_action": "Ask for Template management permission or select a writable Template.",
"i18n:govoplan-templates.permission_actor": "A tenant administrator or the owner of the governing scope",
"i18n:govoplan-templates.permission_destination": "Access and Template scope administration",
"i18n:govoplan-templates.unsaved_title": "Unsaved Template revision",
"i18n:govoplan-templates.unsaved_message": "Save or discard this Template revision before leaving the editor.",
"i18n:govoplan-templates.create_unsaved_title": "Uncreated Template",
"i18n:govoplan-templates.create_unsaved_message": "Create the Template or discard its name before leaving this dialog.",
"i18n:govoplan-templates.publish_title": "Publish Template revision",
"i18n:govoplan-templates.publish_message": "Publish this immutable revision? Consumers may use it for final output until another revision is published.",
"i18n:govoplan-templates.render_title": "Render final output",
"i18n:govoplan-templates.render_message": "Render final output from this published revision and the current sample input? The render hashes and output evidence will be retained.",
"Template is read-only": "Template is read-only",
"Template library": "Template library",
"Search templates": "Search templates",
"No matching templates.": "No matching templates.",
"Select a template": "Select a template",
"Discard and reload": "Discard and reload",
"Save revision": "Save revision",
"Publish": "Publish",
"Delete template": "Delete template",
"Definition": "Definition",
"Preview": "Preview",
"Loading templates": "Loading templates",
"Create or select a reusable template.": "Create or select a reusable template.",
"Add template": "Add template",
"Name": "Name",
"Type": "Type",
"Locale": "Locale",
"Visibility": "Visibility",
"Usages": "Usages",
"Description": "Description",
"Required data contract": "Required data contract",
"Add field": "Add field",
"No required fields. Tokens still resolve from supplied parameters and items.": "No required fields. Tokens still resolve from supplied parameters and items.",
"Page and media": "Page and media",
"Page size": "Page size",
"Margin (mm)": "Margin (mm)",
"Columns": "Columns",
"Rows": "Rows",
"Gap (mm)": "Gap (mm)",
"Template body": "Template body",
"Revision history": "Revision history",
"Output history": "Output history",
"Validated sample input": "Validated sample input",
"Usage": "Usage",
"Output": "Output",
"Store in Files when available": "Store in Files when available",
"Validate and preview": "Validate and preview",
"Render final output": "Render final output",
"Render evidence": "Render evidence",
"Download": "Download",
"Delete template?": "Delete template?"
} as const;
const de: Record<keyof typeof en, string> = {
"i18n:govoplan-templates.templates": "Vorlagen",
"i18n:govoplan-templates.library": "Vorlagenbibliothek",
"i18n:govoplan-templates.editor": "Vorlageneditor",
"i18n:govoplan-templates.preview": "Vorlagenvorschau und Ausgabe",
"i18n:govoplan-templates.loading_reason": "Vorlagen werden noch geladen.",
"i18n:govoplan-templates.busy_reason": "Eine andere Vorlagenaktion läuft noch.",
"i18n:govoplan-templates.write_permission_reason": "Ihr Konto darf Vorlagen nicht erstellen oder überarbeiten.",
"i18n:govoplan-templates.publish_permission_reason": "Ihr Konto darf Vorlagenrevisionen nicht veröffentlichen.",
"i18n:govoplan-templates.render_permission_reason": "Ihr Konto darf keine Vorlagenausgabe erzeugen.",
"i18n:govoplan-templates.read_only_reason": "Diese Vorlage ist im aktuellen Bereich geerbt oder anderweitig schreibgeschützt.",
"i18n:govoplan-templates.no_selection_reason": "Wählen oder erstellen Sie zuerst eine Vorlage.",
"i18n:govoplan-templates.no_changes_reason": "Es gibt keine Definitionsänderungen zu speichern.",
"i18n:govoplan-templates.incomplete_reason": "Füllen Sie zuerst Name und Verwendungszweck aus.",
"i18n:govoplan-templates.save_before_action_reason": "Speichern Sie die aktuelle Revision, bevor Sie sie veröffentlichen oder ausgeben.",
"i18n:govoplan-templates.required_action": "Erforderliche Aktion",
"i18n:govoplan-templates.actor": "Verantwortliche Stelle",
"i18n:govoplan-templates.destination": "Fortsetzung",
"i18n:govoplan-templates.permission_action": "Fordern Sie die Vorlagenberechtigung an oder wählen Sie eine beschreibbare Vorlage.",
"i18n:govoplan-templates.permission_actor": "Mandantenadministration oder Eigentümer des maßgeblichen Bereichs",
"i18n:govoplan-templates.permission_destination": "Zugriffs- und Vorlagenbereichsverwaltung",
"i18n:govoplan-templates.unsaved_title": "Ungespeicherte Vorlagenrevision",
"i18n:govoplan-templates.unsaved_message": "Speichern oder verwerfen Sie diese Vorlagenrevision, bevor Sie den Editor verlassen.",
"i18n:govoplan-templates.create_unsaved_title": "Nicht erstellte Vorlage",
"i18n:govoplan-templates.create_unsaved_message": "Erstellen Sie die Vorlage oder verwerfen Sie ihren Namen, bevor Sie diesen Dialog verlassen.",
"i18n:govoplan-templates.publish_title": "Vorlagenrevision veröffentlichen",
"i18n:govoplan-templates.publish_message": "Diese unveränderliche Revision veröffentlichen? Verbraucher dürfen sie für endgültige Ausgaben nutzen, bis eine andere Revision veröffentlicht wird.",
"i18n:govoplan-templates.render_title": "Endgültige Ausgabe erzeugen",
"i18n:govoplan-templates.render_message": "Endgültige Ausgabe aus dieser veröffentlichten Revision und den aktuellen Beispieldaten erzeugen? Ausgabe-Hashes und Nachweise werden aufbewahrt.",
"Template is read-only": "Vorlage ist schreibgeschützt",
"Template library": "Vorlagenbibliothek",
"Search templates": "Vorlagen suchen",
"No matching templates.": "Keine passenden Vorlagen.",
"Select a template": "Vorlage auswählen",
"Discard and reload": "Verwerfen und neu laden",
"Save revision": "Revision speichern",
"Publish": "Veröffentlichen",
"Delete template": "Vorlage löschen",
"Definition": "Definition",
"Preview": "Vorschau",
"Loading templates": "Vorlagen werden geladen",
"Create or select a reusable template.": "Erstellen oder wählen Sie eine wiederverwendbare Vorlage.",
"Add template": "Vorlage hinzufügen",
"Name": "Name",
"Type": "Typ",
"Locale": "Gebietsschema",
"Visibility": "Sichtbarkeit",
"Usages": "Verwendungen",
"Description": "Beschreibung",
"Required data contract": "Erforderlicher Datenvertrag",
"Add field": "Feld hinzufügen",
"No required fields. Tokens still resolve from supplied parameters and items.": "Keine Pflichtfelder. Platzhalter werden weiterhin aus Parametern und Einträgen aufgelöst.",
"Page and media": "Seite und Medium",
"Page size": "Seitengröße",
"Margin (mm)": "Rand (mm)",
"Columns": "Spalten",
"Rows": "Zeilen",
"Gap (mm)": "Abstand (mm)",
"Template body": "Vorlageninhalt",
"Revision history": "Revisionsverlauf",
"Output history": "Ausgabeverlauf",
"Validated sample input": "Validierte Beispieldaten",
"Usage": "Verwendung",
"Output": "Ausgabe",
"Store in Files when available": "Wenn verfügbar in Dateien speichern",
"Validate and preview": "Validieren und Vorschau erzeugen",
"Render final output": "Endgültige Ausgabe erzeugen",
"Render evidence": "Ausgabenachweis",
"Download": "Herunterladen",
"Delete template?": "Vorlage löschen?"
};
export const generatedTranslations: PlatformTranslations = { en, de };
+11 -2
View File
@@ -1,5 +1,6 @@
import { createElement, lazy } from "react";
import type { PlatformWebModule } from "@govoplan/core-webui";
import { generatedTranslations } from "./i18n/generatedTranslations";
import "./styles/templates.css";
const TemplatesPage = lazy(() => import("./features/templates/TemplatesPage"));
@@ -14,12 +15,19 @@ const readScopes = [
export const templatesModule: PlatformWebModule = {
id: "templates",
label: "Templates",
label: "i18n:govoplan-templates.templates",
version: "0.1.14",
optionalDependencies: ["files", "dist_lists", "campaigns", "audit"],
translations: generatedTranslations,
viewSurfaces: [
{ id: "templates.page", moduleId: "templates", kind: "route", label: "i18n:govoplan-templates.templates", order: 75 },
{ id: "templates.library", moduleId: "templates", kind: "section", label: "i18n:govoplan-templates.library", parentId: "templates.page", order: 10 },
{ id: "templates.editor", moduleId: "templates", kind: "section", label: "i18n:govoplan-templates.editor", parentId: "templates.page", order: 20 },
{ id: "templates.preview", moduleId: "templates", kind: "section", label: "i18n:govoplan-templates.preview", parentId: "templates.page", order: 30 }
],
navItems: [{
to: "/templates",
label: "Templates",
label: "i18n:govoplan-templates.templates",
iconName: "layout-template",
anyOf: readScopes,
order: 75
@@ -28,6 +36,7 @@ export const templatesModule: PlatformWebModule = {
path: "/templates",
anyOf: readScopes,
order: 75,
surfaceId: "templates.page",
render: ({ settings, auth }) => createElement(TemplatesPage, { settings, auth })
}]
};