Migrate Templates interface patterns
This commit is contained in:
@@ -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.
|
||||||
@@ -85,7 +85,15 @@ DOCUMENTATION = (
|
|||||||
layer="available",
|
layer="available",
|
||||||
documentation_types=("admin", "user"),
|
documentation_types=("admin", "user"),
|
||||||
audience=("operator", "module_admin", "product_owner"),
|
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(
|
DocumentationTopic(
|
||||||
id="templates.printable-output",
|
id="templates.printable-output",
|
||||||
@@ -101,7 +109,51 @@ DOCUMENTATION = (
|
|||||||
documentation_types=("admin", "user"),
|
documentation_types=("admin", "user"),
|
||||||
audience=("operator", "module_admin", "product_owner"),
|
audience=("operator", "module_admin", "product_owner"),
|
||||||
related_modules=("files", "dist_lists", "campaigns", "audit"),
|
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()
|
||||||
@@ -12,9 +12,11 @@ import {
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
ApiError,
|
ApiError,
|
||||||
|
ActionBlockerHint,
|
||||||
Button,
|
Button,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
Dialog,
|
Dialog,
|
||||||
|
DocumentationHelpLink,
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
FormField,
|
FormField,
|
||||||
IconButton,
|
IconButton,
|
||||||
@@ -24,6 +26,7 @@ import {
|
|||||||
ToggleSwitch,
|
ToggleSwitch,
|
||||||
formatDateTime,
|
formatDateTime,
|
||||||
hasScope,
|
hasScope,
|
||||||
|
useUnsavedChanges,
|
||||||
useUnsavedDraftGuard,
|
useUnsavedDraftGuard,
|
||||||
type ApiSettings,
|
type ApiSettings,
|
||||||
type AuthInfo
|
type AuthInfo
|
||||||
@@ -49,6 +52,12 @@ import {
|
|||||||
type TemplateRevision,
|
type TemplateRevision,
|
||||||
type TemplateType
|
type TemplateType
|
||||||
} from "../../api/templates";
|
} from "../../api/templates";
|
||||||
|
import {
|
||||||
|
TEMPLATE_FIELDS_DOCUMENTATION,
|
||||||
|
TEMPLATE_OUTPUT_DOCUMENTATION,
|
||||||
|
TEMPLATES_DOCUMENTATION,
|
||||||
|
TEMPLATES_I18N
|
||||||
|
} from "./interfacePatterns";
|
||||||
|
|
||||||
type Props = { settings: ApiSettings; auth: AuthInfo };
|
type Props = { settings: ApiSettings; auth: AuthInfo };
|
||||||
type WorkspaceView = "definition" | "preview";
|
type WorkspaceView = "definition" | "preview";
|
||||||
@@ -83,6 +92,8 @@ export default function TemplatesPage({ settings, auth }: Props) {
|
|||||||
const [createName, setCreateName] = useState("");
|
const [createName, setCreateName] = useState("");
|
||||||
const [createType, setCreateType] = useState<TemplateType>("form_letter");
|
const [createType, setCreateType] = useState<TemplateType>("form_letter");
|
||||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
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 [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 [usage, setUsage] = useState("campaign.postal");
|
||||||
const [outputFormat, setOutputFormat] = useState<"html" | "text">("html");
|
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 [render, setRender] = useState<TemplateRender | null>(null);
|
||||||
const [revisions, setRevisions] = useState<TemplateRevision[]>([]);
|
const [revisions, setRevisions] = useState<TemplateRevision[]>([]);
|
||||||
const [renders, setRenders] = useState<TemplateRender[]>([]);
|
const [renders, setRenders] = useState<TemplateRender[]>([]);
|
||||||
|
const { requestDiscard } = useUnsavedChanges();
|
||||||
|
|
||||||
const selected = items.find((item) => item.id === selectedId) ?? null;
|
const selected = items.find((item) => item.id === selectedId) ?? null;
|
||||||
const canWrite = hasScope(auth, "templates:template:write") || hasScope(auth, "templates:template:admin");
|
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 () => {
|
const create = async (): Promise<boolean> => {
|
||||||
if (!createName.trim()) return;
|
if (!createName.trim()) return false;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
@@ -188,18 +206,39 @@ export default function TemplatesPage({ settings, auth }: Props) {
|
|||||||
setCreateName("");
|
setCreateName("");
|
||||||
setSuccess(`Created ${created.name}.`);
|
setSuccess(`Created ${created.name}.`);
|
||||||
await reload(created.id);
|
await reload(created.id);
|
||||||
|
return true;
|
||||||
} catch (caught) {
|
} catch (caught) {
|
||||||
setError(errorMessage(caught));
|
setError(errorMessage(caught));
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
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 () => {
|
const publish = async () => {
|
||||||
if (!selected || dirty) return;
|
if (!selected || dirty) return;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
const updated = await publishTemplate(settings, selected);
|
const updated = await publishTemplate(settings, selected);
|
||||||
|
setPublishOpen(false);
|
||||||
setSuccess(`Published revision ${updated.current_revision}.`);
|
setSuccess(`Published revision ${updated.current_revision}.`);
|
||||||
await reload(updated.id);
|
await reload(updated.id);
|
||||||
} catch (caught) {
|
} catch (caught) {
|
||||||
@@ -242,6 +281,7 @@ export default function TemplatesPage({ settings, auth }: Props) {
|
|||||||
persistToFiles
|
persistToFiles
|
||||||
});
|
});
|
||||||
setRender(nextRender);
|
setRender(nextRender);
|
||||||
|
if (final) setFinalRenderOpen(false);
|
||||||
setRenders(await listTemplateRenders(settings, selected.id));
|
setRenders(await listTemplateRenders(settings, selected.id));
|
||||||
setSuccess(`${final ? "Final" : "Preview"} output rendered with ${nextRender.item_count} item(s).`);
|
setSuccess(`${final ? "Final" : "Preview"} output rendered with ${nextRender.item_count} item(s).`);
|
||||||
} catch (caught) {
|
} catch (caught) {
|
||||||
@@ -257,7 +297,7 @@ export default function TemplatesPage({ settings, auth }: Props) {
|
|||||||
<aside className="templates-sidebar">
|
<aside className="templates-sidebar">
|
||||||
<div className="templates-sidebar-toolbar">
|
<div className="templates-sidebar-toolbar">
|
||||||
<strong>Template library</strong>
|
<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>
|
||||||
<div className="templates-search"><input value={search} onChange={(event) => setSearch(event.target.value)} placeholder="Search templates" /></div>
|
<div className="templates-search"><input value={search} onChange={(event) => setSearch(event.target.value)} placeholder="Search templates" /></div>
|
||||||
<div className="templates-list">
|
<div className="templates-list">
|
||||||
@@ -268,8 +308,10 @@ export default function TemplatesPage({ settings, auth }: Props) {
|
|||||||
className={item.id === selectedId ? "is-selected" : ""}
|
className={item.id === selectedId ? "is-selected" : ""}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (item.id === selectedId) return;
|
if (item.id === selectedId) return;
|
||||||
setSelectedId(item.id);
|
requestDiscard(() => {
|
||||||
applyItem(item);
|
setSelectedId(item.id);
|
||||||
|
applyItem(item);
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span><strong>{item.name}</strong><small>{typeLabel(item.template_type)} · revision {item.current_revision}</small></span>
|
<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>
|
<small>{selected ? `${typeLabel(selected.template_type)} · ${selected.revision.locale}` : ""}</small>
|
||||||
</span>
|
</span>
|
||||||
<div className="templates-toolbar-actions">
|
<div className="templates-toolbar-actions">
|
||||||
<IconButton label="Discard and reload" icon={<RefreshCw size={17} />} onClick={() => void reload(selectedId)} />
|
<DocumentationHelpLink reference={TEMPLATES_DOCUMENTATION} />
|
||||||
<Button variant="primary" disabled={!selected || readOnly || !dirty || busy} onClick={() => void save()}><Save size={16} /> Save revision</Button>
|
<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 disabled={!selected || !canPublish || dirty || busy} onClick={() => void publish()}><FileCheck2 size={16} /> Publish</Button>
|
<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>
|
||||||
<IconButton label="Delete template" icon={<Trash2 size={17} />} variant="danger" disabled={!selected || readOnly} onClick={() => setDeleteOpen(true)} />
|
<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
|
<SegmentedControl
|
||||||
value={view}
|
value={view}
|
||||||
onChange={setView}
|
onChange={setView}
|
||||||
@@ -303,6 +346,18 @@ export default function TemplatesPage({ settings, auth }: Props) {
|
|||||||
<div className="templates-alerts">
|
<div className="templates-alerts">
|
||||||
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
|
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
|
||||||
{success && <DismissibleAlert tone="success" resetKey={success}>{success}</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>
|
</div>
|
||||||
|
|
||||||
<LoadingFrame loading={loading} label="Loading templates">
|
<LoadingFrame loading={loading} label="Loading templates">
|
||||||
@@ -320,11 +375,12 @@ export default function TemplatesPage({ settings, auth }: Props) {
|
|||||||
compatibility={compatibility}
|
compatibility={compatibility}
|
||||||
render={render}
|
render={render}
|
||||||
disabled={busy || dirty || !canRender}
|
disabled={busy || dirty || !canRender}
|
||||||
|
disabledReason={busy ? TEMPLATES_I18N.busy : dirty ? TEMPLATES_I18N.saveBeforeAction : !canRender ? TEMPLATES_I18N.renderReason : undefined}
|
||||||
onSampleText={setSampleText}
|
onSampleText={setSampleText}
|
||||||
onUsage={setUsage}
|
onUsage={setUsage}
|
||||||
onOutputFormat={setOutputFormat}
|
onOutputFormat={setOutputFormat}
|
||||||
onPersistToFiles={setPersistToFiles}
|
onPersistToFiles={setPersistToFiles}
|
||||||
onRender={runRender}
|
onRender={(final) => final ? setFinalRenderOpen(true) : void runRender(false)}
|
||||||
onDownload={() => render && void downloadTemplateRender(settings, render).catch((caught) => setError(errorMessage(caught)))}
|
onDownload={() => render && void downloadTemplateRender(settings, render).catch((caught) => setError(errorMessage(caught)))}
|
||||||
/>
|
/>
|
||||||
<RenderHistory renders={renders} settings={settings} onError={setError} />
|
<RenderHistory renders={renders} settings={settings} onError={setError} />
|
||||||
@@ -334,12 +390,14 @@ export default function TemplatesPage({ settings, auth }: Props) {
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</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">
|
<div className="templates-dialog-form">
|
||||||
<FormField label="Name"><input autoFocus value={createName} onChange={(event) => setCreateName(event.target.value)} /></FormField>
|
<FormField label="Name" documentation={TEMPLATE_FIELDS_DOCUMENTATION}><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="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>
|
</div>
|
||||||
</Dialog>
|
</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()} />
|
<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>
|
</main>
|
||||||
);
|
);
|
||||||
@@ -386,11 +444,11 @@ function DefinitionEditor({ draft, disabled, auth, onChange }: { draft: Template
|
|||||||
return (
|
return (
|
||||||
<div className="templates-definition">
|
<div className="templates-definition">
|
||||||
<div className="templates-definition-fields">
|
<div className="templates-definition-fields">
|
||||||
<FormField label="Name"><input disabled={disabled} value={draft.name} onChange={(event) => update("name", 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"><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="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"><input disabled={disabled} value={draft.locale} onChange={(event) => update("locale", event.target.value)} /></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"><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="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."><input disabled={disabled} value={draft.usages.join(", ")} onChange={(event) => update("usages", splitValues(event.target.value))} /></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>
|
<FormField label="Description"><input disabled={disabled} value={draft.description ?? ""} onChange={(event) => update("description", event.target.value || null)} /></FormField>
|
||||||
</div>
|
</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;
|
item: TemplateDefinition;
|
||||||
sampleText: string;
|
sampleText: string;
|
||||||
usage: string;
|
usage: string;
|
||||||
@@ -440,6 +498,7 @@ function PreviewPanel({ item, sampleText, usage, outputFormat, persistToFiles, c
|
|||||||
compatibility: TemplateCompatibility | null;
|
compatibility: TemplateCompatibility | null;
|
||||||
render: TemplateRender | null;
|
render: TemplateRender | null;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
disabledReason?: string;
|
||||||
onSampleText: (value: string) => void;
|
onSampleText: (value: string) => void;
|
||||||
onUsage: (value: string) => void;
|
onUsage: (value: string) => void;
|
||||||
onOutputFormat: (value: "html" | "text") => void;
|
onOutputFormat: (value: "html" | "text") => void;
|
||||||
@@ -452,14 +511,14 @@ function PreviewPanel({ item, sampleText, usage, outputFormat, persistToFiles, c
|
|||||||
<section className="templates-section">
|
<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-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">
|
<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="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"><SegmentedControl value={outputFormat} onChange={onOutputFormat} options={[{ id: "html", label: "Printable HTML" }, { id: "text", label: "Plain text" }]} ariaLabel="Output format" /></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} />
|
<ToggleSwitch checked={persistToFiles} label="Store in Files when available" onChange={onPersistToFiles} />
|
||||||
</div>
|
</div>
|
||||||
<textarea className="templates-sample" value={sampleText} onChange={(event) => onSampleText(event.target.value)} spellCheck={false} aria-label="Sample item JSON" />
|
<textarea className="templates-sample" value={sampleText} onChange={(event) => onSampleText(event.target.value)} spellCheck={false} aria-label="Sample item JSON" />
|
||||||
<div className="templates-preview-actions">
|
<div className="templates-preview-actions">
|
||||||
<Button disabled={disabled} onClick={() => onRender(false)}><Eye size={16} /> Validate and preview</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={!item.revision.published_at ? "Publish this revision before producing final output." : undefined} onClick={() => onRender(true)}><Send size={16} /> Render final output</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>
|
</div>
|
||||||
</section>
|
</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;
|
||||||
@@ -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
@@ -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/templates.css";
|
import "./styles/templates.css";
|
||||||
|
|
||||||
const TemplatesPage = lazy(() => import("./features/templates/TemplatesPage"));
|
const TemplatesPage = lazy(() => import("./features/templates/TemplatesPage"));
|
||||||
@@ -14,12 +15,19 @@ const readScopes = [
|
|||||||
|
|
||||||
export const templatesModule: PlatformWebModule = {
|
export const templatesModule: PlatformWebModule = {
|
||||||
id: "templates",
|
id: "templates",
|
||||||
label: "Templates",
|
label: "i18n:govoplan-templates.templates",
|
||||||
version: "0.1.14",
|
version: "0.1.14",
|
||||||
optionalDependencies: ["files", "dist_lists", "campaigns", "audit"],
|
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: [{
|
navItems: [{
|
||||||
to: "/templates",
|
to: "/templates",
|
||||||
label: "Templates",
|
label: "i18n:govoplan-templates.templates",
|
||||||
iconName: "layout-template",
|
iconName: "layout-template",
|
||||||
anyOf: readScopes,
|
anyOf: readScopes,
|
||||||
order: 75
|
order: 75
|
||||||
@@ -28,6 +36,7 @@ export const templatesModule: PlatformWebModule = {
|
|||||||
path: "/templates",
|
path: "/templates",
|
||||||
anyOf: readScopes,
|
anyOf: readScopes,
|
||||||
order: 75,
|
order: 75,
|
||||||
|
surfaceId: "templates.page",
|
||||||
render: ({ settings, auth }) => createElement(TemplatesPage, { settings, auth })
|
render: ({ settings, auth }) => createElement(TemplatesPage, { settings, auth })
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user