Migrate Forms interface patterns
This commit is contained in:
@@ -0,0 +1,33 @@
|
|||||||
|
# Forms Interface Pattern Migration
|
||||||
|
|
||||||
|
This migration applies the GovOPlaN interface pattern language to the
|
||||||
|
Forms-owned definition catalogue, package import, and revision editor. Forms
|
||||||
|
owns immutable reusable schemas; Forms Runtime continues to own values,
|
||||||
|
submissions, receipts, and handoff execution.
|
||||||
|
|
||||||
|
## Surface Inventory
|
||||||
|
|
||||||
|
| Surface | Archetype | Consequence class | Contract |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `/forms` catalogue | Searchable definition library | Filter, export, import, create, or revise | Shared loading/error/empty/permission/help states and stable row actions |
|
||||||
|
| Package import dialog | Consequential package review | Create local draft from external provenance | Assessment, required reason, guarded draft, never implicit publish |
|
||||||
|
| Definition dialog | Versioned definition editor | Create immutable revision | Guarded editor, contextual schema help, explained validation and permission states |
|
||||||
|
| Publication state | Governed lifecycle selector | Publish or retire exact revision | Admin-only availability and explicit lifecycle confirmation |
|
||||||
|
| Field/page/localization editors | Structured schema composition | Change future runtime schema | At least one field, stable ordering, preview, localization and accessibility metadata |
|
||||||
|
|
||||||
|
## Consequence And Availability Rules
|
||||||
|
|
||||||
|
- Every save creates an immutable revision with a reason. Runtime instances
|
||||||
|
retain the exact revision with which they were created.
|
||||||
|
- Publishing permits future authorized use. Retirement prevents future use but
|
||||||
|
does not erase definitions, submissions, receipts, or evidence.
|
||||||
|
- Imported packages become local drafts and retain source provenance.
|
||||||
|
- Missing write or administration authority remains visible with an Access
|
||||||
|
destination; retired definitions show a lifecycle explanation.
|
||||||
|
- Optional Runtime, Portal, Workflow, Case, and Policy behavior remains behind
|
||||||
|
declared interfaces and capabilities.
|
||||||
|
|
||||||
|
The module uses shared controls, dialogs, blockers, field help, statuses,
|
||||||
|
loading, empty/error states, disabled reasons, confirmations, and unsaved-draft
|
||||||
|
guards. Existing responsive list/editor layouts remain bounded. English and
|
||||||
|
German catalogues cover the owned route and editor vocabulary.
|
||||||
@@ -38,6 +38,13 @@ MODULE_VERSION = "0.1.14"
|
|||||||
READ_SCOPE = "forms:definition:read"
|
READ_SCOPE = "forms:definition:read"
|
||||||
WRITE_SCOPE = "forms:definition:write"
|
WRITE_SCOPE = "forms:definition:write"
|
||||||
ADMIN_SCOPE = "forms:definition:admin"
|
ADMIN_SCOPE = "forms:definition:admin"
|
||||||
|
OPTIONAL_DEPENDENCIES = (
|
||||||
|
"forms_runtime",
|
||||||
|
"portal",
|
||||||
|
"workflow_engine",
|
||||||
|
"cases",
|
||||||
|
"policy",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _permission(scope: str, label: str, description: str) -> PermissionDefinition:
|
def _permission(scope: str, label: str, description: str) -> PermissionDefinition:
|
||||||
@@ -69,13 +76,7 @@ manifest = ModuleManifest(
|
|||||||
name=MODULE_NAME,
|
name=MODULE_NAME,
|
||||||
version=MODULE_VERSION,
|
version=MODULE_VERSION,
|
||||||
dependencies=("access",),
|
dependencies=("access",),
|
||||||
optional_dependencies=(
|
optional_dependencies=OPTIONAL_DEPENDENCIES,
|
||||||
"forms_runtime",
|
|
||||||
"portal",
|
|
||||||
"workflow_engine",
|
|
||||||
"cases",
|
|
||||||
"policy",
|
|
||||||
),
|
|
||||||
required_capabilities=(
|
required_capabilities=(
|
||||||
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
|
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
|
||||||
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
|
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
|
||||||
@@ -205,6 +206,65 @@ manifest = ModuleManifest(
|
|||||||
kind="repository",
|
kind="repository",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
metadata={
|
||||||
|
"seed": True,
|
||||||
|
"help_contexts": [
|
||||||
|
"forms.navigation",
|
||||||
|
"forms.catalogue",
|
||||||
|
"forms.state.permission-blocked",
|
||||||
|
"forms.state.empty",
|
||||||
|
],
|
||||||
|
"privacy_notes": [
|
||||||
|
"Definition catalogues contain schemas and policy references, not submitted Form values.",
|
||||||
|
"Package assessment does not grant access to referenced runtime submissions or external providers.",
|
||||||
|
"Published accessibility and localization content is visible wherever the exact definition is authorized.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
DocumentationTopic(
|
||||||
|
id="forms.reference.fields-and-consequences",
|
||||||
|
title="Form definition fields and lifecycle consequences",
|
||||||
|
summary="Schema, publication, localization, policy, evidence, package, and handoff semantics for immutable Form revisions.",
|
||||||
|
body=(
|
||||||
|
"The stable key identifies the definition while each save creates a new immutable revision. Field keys, types, "
|
||||||
|
"constraints, visibility conditions, pages, sections, options, help, localization, and accessibility instructions "
|
||||||
|
"become the exact runtime schema. Attachment, signature, policy, draft, and permitted-handoff settings are enforced "
|
||||||
|
"by Forms Runtime when that module is present. Publishing makes a revision available for new instances; existing "
|
||||||
|
"instances retain their prior exact revision. Retirement prevents future use without deleting definitions or submissions. "
|
||||||
|
"Package import always creates a local draft and retains source provenance; it never silently publishes an imported revision."
|
||||||
|
),
|
||||||
|
layer="configured",
|
||||||
|
documentation_types=("admin", "user"),
|
||||||
|
audience=("user", "operator", "module_admin", "auditor"),
|
||||||
|
related_modules=OPTIONAL_DEPENDENCIES,
|
||||||
|
links=(
|
||||||
|
DocumentationLink(
|
||||||
|
label="Forms boundary and recovery",
|
||||||
|
href="govoplan-forms/docs/FORMS_BOUNDARY.md",
|
||||||
|
kind="repository",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
metadata={
|
||||||
|
"seed": True,
|
||||||
|
"help_contexts": [
|
||||||
|
"forms.field.publication-state",
|
||||||
|
"forms.field.signature-requirement",
|
||||||
|
"forms.field.policy-references",
|
||||||
|
"forms.field.handoff-kinds",
|
||||||
|
"forms.field.accessibility",
|
||||||
|
"forms.field.change-reason",
|
||||||
|
"forms.action.save-revision",
|
||||||
|
"forms.action.publish",
|
||||||
|
"forms.action.retire",
|
||||||
|
"forms.action.import-package",
|
||||||
|
],
|
||||||
|
"consequence_classes": {
|
||||||
|
"save_revision": "Creates an immutable definition revision with a change reason.",
|
||||||
|
"publish": "Makes the exact revision available for future authorized instances.",
|
||||||
|
"retire": "Stops future use while retaining definitions and exact runtime references.",
|
||||||
|
"import_package": "Creates a local draft and retains package provenance without automatic publication.",
|
||||||
|
},
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
architecture=declared_module_architecture(
|
architecture=declared_module_architecture(
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from govoplan_forms.backend.manifest import manifest
|
||||||
|
|
||||||
|
|
||||||
|
class FormsInterfaceDocumentationContractTests(unittest.TestCase):
|
||||||
|
def test_route_and_surfaces_remain_declared(self) -> None:
|
||||||
|
frontend = manifest.frontend
|
||||||
|
self.assertIsNotNone(frontend)
|
||||||
|
self.assertEqual({"/forms"}, {item.path for item in frontend.routes}) # type: ignore[union-attr]
|
||||||
|
self.assertEqual(
|
||||||
|
{"forms.navigation", "forms.catalogue"},
|
||||||
|
{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.definitions"]
|
||||||
|
reference = topics["forms.reference.fields-and-consequences"]
|
||||||
|
self.assertIn("forms.catalogue", guide.metadata["help_contexts"])
|
||||||
|
self.assertGreaterEqual(len(guide.metadata["privacy_notes"]), 3)
|
||||||
|
self.assertIn("forms.field.publication-state", reference.metadata["help_contexts"])
|
||||||
|
self.assertIn("publish", reference.metadata["consequence_classes"])
|
||||||
|
self.assertIn("import_package", reference.metadata["consequence_classes"])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -2,11 +2,17 @@ import { ArrowDown, ArrowUp, Eye, Languages, Plus, Trash2 } from "lucide-react";
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
ConfirmDialog,
|
||||||
Dialog,
|
Dialog,
|
||||||
|
DocumentationHelpLink,
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
FormField as Field,
|
FormField as Field,
|
||||||
IconButton,
|
IconButton,
|
||||||
ToggleSwitch,
|
ToggleSwitch,
|
||||||
|
i18nMessage,
|
||||||
|
usePlatformLanguage,
|
||||||
|
useUnsavedChanges,
|
||||||
|
useUnsavedDraftGuard,
|
||||||
type ApiSettings
|
type ApiSettings
|
||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +24,7 @@ import {
|
|||||||
type FormPageDefinition,
|
type FormPageDefinition,
|
||||||
type FormValueType
|
type FormValueType
|
||||||
} from "../../api/forms";
|
} from "../../api/forms";
|
||||||
|
import { FORMS_FIELD_DOCUMENTATION, FORMS_I18N } from "./interfacePatterns";
|
||||||
|
|
||||||
|
|
||||||
const VALUE_TYPES: Array<{ value: FormValueType; label: string }> = [
|
const VALUE_TYPES: Array<{ value: FormValueType; label: string }> = [
|
||||||
@@ -52,17 +59,24 @@ export default function FormDefinitionDialog({
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSaved: (definition: FormDefinition) => void;
|
onSaved: (definition: FormDefinition) => void;
|
||||||
}) {
|
}) {
|
||||||
const [draft, setDraft] = useState<FormDefinition>(() => initialDraft(tenantId, definition));
|
const { translateText } = usePlatformLanguage();
|
||||||
|
const [baseline, setBaseline] = useState<FormDefinition>(() => initialDraft(tenantId, definition));
|
||||||
|
const [draft, setDraft] = useState<FormDefinition>(baseline);
|
||||||
const [changeReason, setChangeReason] = useState("");
|
const [changeReason, setChangeReason] = useState("");
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
const [confirmLifecycle, setConfirmLifecycle] = useState(false);
|
||||||
|
const { requestDiscard } = useUnsavedChanges();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
setDraft(initialDraft(tenantId, definition));
|
const next = initialDraft(tenantId, definition);
|
||||||
|
setDraft(next);
|
||||||
|
setBaseline(next);
|
||||||
setChangeReason("");
|
setChangeReason("");
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
setError("");
|
setError("");
|
||||||
|
setConfirmLifecycle(false);
|
||||||
}, [definition, open, tenantId]);
|
}, [definition, open, tenantId]);
|
||||||
|
|
||||||
const valid = useMemo(() => Boolean(
|
const valid = useMemo(() => Boolean(
|
||||||
@@ -72,9 +86,13 @@ export default function FormDefinitionDialog({
|
|||||||
&& draft.fields.every((field) => field.key.trim() && field.label.trim())
|
&& draft.fields.every((field) => field.key.trim() && field.label.trim())
|
||||||
&& changeReason.trim()
|
&& changeReason.trim()
|
||||||
), [changeReason, draft]);
|
), [changeReason, draft]);
|
||||||
|
const dirty = useMemo(
|
||||||
|
() => Boolean(changeReason || JSON.stringify(draft) !== JSON.stringify(baseline)),
|
||||||
|
[baseline, changeReason, draft]
|
||||||
|
);
|
||||||
|
|
||||||
async function save() {
|
async function save(): Promise<boolean> {
|
||||||
if (!valid) return;
|
if (!valid) return false;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError("");
|
setError("");
|
||||||
const revision = crypto.randomUUID();
|
const revision = crypto.randomUUID();
|
||||||
@@ -106,13 +124,41 @@ export default function FormDefinitionDialog({
|
|||||||
definition?.reference.version
|
definition?.reference.version
|
||||||
);
|
);
|
||||||
onSaved(saved);
|
onSaved(saved);
|
||||||
|
return true;
|
||||||
} catch (reason) {
|
} catch (reason) {
|
||||||
setError(reason instanceof Error ? reason.message : "The Form definition could not be saved.");
|
setError(reason instanceof Error ? reason.message : "The Form definition could not be saved.");
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useUnsavedDraftGuard({
|
||||||
|
dirty: open && dirty,
|
||||||
|
onSave: save,
|
||||||
|
onDiscard: () => {
|
||||||
|
setDraft(baseline);
|
||||||
|
setChangeReason("");
|
||||||
|
},
|
||||||
|
title: "i18n:govoplan-forms.unsaved_title",
|
||||||
|
message: "i18n:govoplan-forms.unsaved_message"
|
||||||
|
});
|
||||||
|
|
||||||
|
function requestClose() {
|
||||||
|
if (busy) return;
|
||||||
|
if (dirty) requestDiscard(onClose);
|
||||||
|
else onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestSave() {
|
||||||
|
const previousState = definition?.publication_state ?? "draft";
|
||||||
|
if (draft.publication_state !== "draft" && draft.publication_state !== previousState) {
|
||||||
|
setConfirmLifecycle(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void save();
|
||||||
|
}
|
||||||
|
|
||||||
function patchField(index: number, patch: Partial<FormFieldDefinition>) {
|
function patchField(index: number, patch: Partial<FormFieldDefinition>) {
|
||||||
setDraft((current) => ({
|
setDraft((current) => ({
|
||||||
...current,
|
...current,
|
||||||
@@ -137,19 +183,20 @@ export default function FormDefinitionDialog({
|
|||||||
<Dialog
|
<Dialog
|
||||||
open={open}
|
open={open}
|
||||||
title={definition ? `Revise ${definition.title}` : "New Form definition"}
|
title={definition ? `Revise ${definition.title}` : "New Form definition"}
|
||||||
onClose={onClose}
|
onClose={requestClose}
|
||||||
closeDisabled={busy}
|
closeDisabled={busy}
|
||||||
portal
|
portal
|
||||||
className="form-definition-dialog"
|
className="form-definition-dialog"
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<Button onClick={onClose} disabled={busy}>Cancel</Button>
|
<Button onClick={requestClose} disabled={busy} disabledReason={busy ? FORMS_I18N.busy : undefined}>Cancel</Button>
|
||||||
<Button variant="primary" onClick={() => void save()} disabled={busy || !valid}>
|
<Button variant="primary" onClick={requestSave} disabled={busy || !valid} disabledReason={busy ? FORMS_I18N.busy : !valid ? FORMS_I18N.incomplete : undefined}>
|
||||||
{busy ? "Saving" : "Save revision"}
|
{busy ? "Saving" : "Save revision"}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
}>
|
}>
|
||||||
<div className="form-definition-editor">
|
<div className="form-definition-editor">
|
||||||
|
<div className="form-definition-help"><DocumentationHelpLink reference={FORMS_FIELD_DOCUMENTATION} /></div>
|
||||||
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
|
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
|
||||||
<div className="form-definition-grid">
|
<div className="form-definition-grid">
|
||||||
<Field label="Title">
|
<Field label="Title">
|
||||||
@@ -161,14 +208,14 @@ export default function FormDefinitionDialog({
|
|||||||
<Field label="Description" className="form-definition-wide">
|
<Field label="Description" className="form-definition-wide">
|
||||||
<textarea rows={3} value={draft.description ?? ""} disabled={busy} onChange={(event) => setDraft({ ...draft, description: event.target.value })} />
|
<textarea rows={3} value={draft.description ?? ""} disabled={busy} onChange={(event) => setDraft({ ...draft, description: event.target.value })} />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Publication state">
|
<Field label="Publication state" help={!canPublish ? FORMS_I18N.adminReason : undefined} documentation={FORMS_FIELD_DOCUMENTATION}>
|
||||||
<select value={draft.publication_state} disabled={busy} onChange={(event) => setDraft({ ...draft, publication_state: event.target.value as FormDefinition["publication_state"] })}>
|
<select value={draft.publication_state} disabled={busy} onChange={(event) => setDraft({ ...draft, publication_state: event.target.value as FormDefinition["publication_state"] })}>
|
||||||
{definition?.publication_state !== "published" && <option value="draft">Draft</option>}
|
{definition?.publication_state !== "published" && <option value="draft">Draft</option>}
|
||||||
{canPublish && <option value="published">Published</option>}
|
{canPublish && <option value="published">Published</option>}
|
||||||
{canPublish && <option value="retired">Retired</option>}
|
{canPublish && <option value="retired">Retired</option>}
|
||||||
</select>
|
</select>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Signature">
|
<Field label="Signature" documentation={FORMS_FIELD_DOCUMENTATION}>
|
||||||
<select value={draft.signature_requirement} disabled={busy} onChange={(event) => setDraft({ ...draft, signature_requirement: event.target.value as FormDefinition["signature_requirement"] })}>
|
<select value={draft.signature_requirement} disabled={busy} onChange={(event) => setDraft({ ...draft, signature_requirement: event.target.value as FormDefinition["signature_requirement"] })}>
|
||||||
<option value="none">Not used</option>
|
<option value="none">Not used</option>
|
||||||
<option value="optional">Optional</option>
|
<option value="optional">Optional</option>
|
||||||
@@ -181,7 +228,7 @@ export default function FormDefinitionDialog({
|
|||||||
<div className="form-definition-toggle">
|
<div className="form-definition-toggle">
|
||||||
<ToggleSwitch label="Draft saving" checked={draft.allow_drafts} disabled={busy} onChange={(allow_drafts) => setDraft({ ...draft, allow_drafts })} />
|
<ToggleSwitch label="Draft saving" checked={draft.allow_drafts} disabled={busy} onChange={(allow_drafts) => setDraft({ ...draft, allow_drafts })} />
|
||||||
</div>
|
</div>
|
||||||
<Field label="Policy references" className="form-definition-wide">
|
<Field label="Policy references" className="form-definition-wide" documentation={FORMS_FIELD_DOCUMENTATION}>
|
||||||
<input value={draft.policy_refs.join(", ")} disabled={busy} placeholder="policy:permit-intake" onChange={(event) => setDraft({ ...draft, policy_refs: splitValues(event.target.value) })} />
|
<input value={draft.policy_refs.join(", ")} disabled={busy} placeholder="policy:permit-intake" onChange={(event) => setDraft({ ...draft, policy_refs: splitValues(event.target.value) })} />
|
||||||
</Field>
|
</Field>
|
||||||
<div className="form-definition-handoffs form-definition-wide">
|
<div className="form-definition-handoffs form-definition-wide">
|
||||||
@@ -201,7 +248,7 @@ export default function FormDefinitionDialog({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Field label="Accessibility instructions" className="form-definition-wide">
|
<Field label="Accessibility instructions" className="form-definition-wide" documentation={FORMS_FIELD_DOCUMENTATION}>
|
||||||
<textarea
|
<textarea
|
||||||
rows={2}
|
rows={2}
|
||||||
value={String(draft.accessibility?.instructions ?? "")}
|
value={String(draft.accessibility?.instructions ?? "")}
|
||||||
@@ -253,6 +300,7 @@ export default function FormDefinitionDialog({
|
|||||||
icon={<Trash2 size={16} />}
|
icon={<Trash2 size={16} />}
|
||||||
variant="danger"
|
variant="danger"
|
||||||
disabled={busy || draft.fields.length === 1}
|
disabled={busy || draft.fields.length === 1}
|
||||||
|
disabledReason={busy ? FORMS_I18N.busy : draft.fields.length === 1 ? FORMS_I18N.oneField : undefined}
|
||||||
onClick={() => setDraft(removeField(draft, index))}
|
onClick={() => setDraft(removeField(draft, index))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -261,10 +309,23 @@ export default function FormDefinitionDialog({
|
|||||||
<PageEditor draft={draft} disabled={busy} onChange={setDraft} />
|
<PageEditor draft={draft} disabled={busy} onChange={setDraft} />
|
||||||
<LocalizationEditor draft={draft} disabled={busy} onChange={setDraft} />
|
<LocalizationEditor draft={draft} disabled={busy} onChange={setDraft} />
|
||||||
<DefinitionPreview definition={draft} previous={definition} />
|
<DefinitionPreview definition={draft} previous={definition} />
|
||||||
<Field label="Change reason">
|
<Field label="Change reason" documentation={FORMS_FIELD_DOCUMENTATION}>
|
||||||
<input value={changeReason} disabled={busy} maxLength={1000} onChange={(event) => setChangeReason(event.target.value)} placeholder="Why is this revision needed?" />
|
<input value={changeReason} disabled={busy} maxLength={1000} onChange={(event) => setChangeReason(event.target.value)} placeholder="Why is this revision needed?" />
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
|
<ConfirmDialog
|
||||||
|
open={confirmLifecycle}
|
||||||
|
title="i18n:govoplan-forms.lifecycle_title"
|
||||||
|
message={i18nMessage("i18n:govoplan-forms.lifecycle_message", { state: translateText(humanize(draft.publication_state)) })}
|
||||||
|
confirmLabel="Save revision"
|
||||||
|
tone={draft.publication_state === "retired" ? "danger" : "default"}
|
||||||
|
busy={busy}
|
||||||
|
onCancel={() => setConfirmLifecycle(false)}
|
||||||
|
onConfirm={() => {
|
||||||
|
setConfirmLifecycle(false);
|
||||||
|
void save();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Download, Pencil, Plus, RefreshCw, Search, Upload } from "lucide-react";
|
import { Download, Pencil, Plus, RefreshCw, Search, Upload } from "lucide-react";
|
||||||
import { useCallback, useEffect, useRef, useState, type FormEvent } from "react";
|
import { useCallback, useEffect, useRef, useState, type FormEvent } from "react";
|
||||||
import {
|
import {
|
||||||
|
ActionBlockerHint,
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
|
DocumentationHelpLink,
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
IconButton,
|
IconButton,
|
||||||
FormField,
|
FormField,
|
||||||
@@ -10,6 +12,8 @@ import {
|
|||||||
PageScrollViewport,
|
PageScrollViewport,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
hasScope,
|
hasScope,
|
||||||
|
useUnsavedChanges,
|
||||||
|
useUnsavedDraftGuard,
|
||||||
type PlatformRouteContext
|
type PlatformRouteContext
|
||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
import {
|
import {
|
||||||
@@ -21,6 +25,7 @@ import {
|
|||||||
type FormPackageFragment
|
type FormPackageFragment
|
||||||
} from "../../api/forms";
|
} from "../../api/forms";
|
||||||
import FormDefinitionDialog from "./FormDefinitionDialog";
|
import FormDefinitionDialog from "./FormDefinitionDialog";
|
||||||
|
import { FORMS_DOCUMENTATION, FORMS_FIELD_DOCUMENTATION, FORMS_I18N } from "./interfacePatterns";
|
||||||
|
|
||||||
|
|
||||||
export default function FormsPage({ settings, auth }: PlatformRouteContext) {
|
export default function FormsPage({ settings, auth }: PlatformRouteContext) {
|
||||||
@@ -36,6 +41,7 @@ export default function FormsPage({ settings, auth }: PlatformRouteContext) {
|
|||||||
const [importReason, setImportReason] = useState("");
|
const [importReason, setImportReason] = useState("");
|
||||||
const [importAssessment, setImportAssessment] = useState("");
|
const [importAssessment, setImportAssessment] = useState("");
|
||||||
const [importBusy, setImportBusy] = useState(false);
|
const [importBusy, setImportBusy] = useState(false);
|
||||||
|
const { requestDiscard } = useUnsavedChanges();
|
||||||
const importInput = useRef<HTMLInputElement>(null);
|
const importInput = useRef<HTMLInputElement>(null);
|
||||||
const canWrite = hasScope(auth, "forms:definition:write");
|
const canWrite = hasScope(auth, "forms:definition:write");
|
||||||
const canAdmin = hasScope(auth, "forms:definition:admin");
|
const canAdmin = hasScope(auth, "forms:definition:admin");
|
||||||
@@ -87,21 +93,40 @@ export default function FormsPage({ settings, auth }: PlatformRouteContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function importPackage() {
|
async function importPackage(): Promise<boolean> {
|
||||||
if (!importing || !importReason.trim()) return;
|
if (!importing || !importReason.trim()) return false;
|
||||||
setImportBusy(true);
|
setImportBusy(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
await importFormDefinitionPackage(settings, importing, { changeReason: importReason.trim() });
|
await importFormDefinitionPackage(settings, importing, { changeReason: importReason.trim() });
|
||||||
setImporting(null);
|
setImporting(null);
|
||||||
await load();
|
await load();
|
||||||
|
return true;
|
||||||
} catch (reason) {
|
} catch (reason) {
|
||||||
setError(reason instanceof Error ? reason.message : "The Form package could not be imported.");
|
setError(reason instanceof Error ? reason.message : "The Form package could not be imported.");
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
setImportBusy(false);
|
setImportBusy(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useUnsavedDraftGuard({
|
||||||
|
dirty: Boolean(importing && importReason),
|
||||||
|
onSave: importPackage,
|
||||||
|
onDiscard: () => {
|
||||||
|
setImporting(null);
|
||||||
|
setImportReason("");
|
||||||
|
},
|
||||||
|
title: "i18n:govoplan-forms.unsaved_title",
|
||||||
|
message: "i18n:govoplan-forms.unsaved_message"
|
||||||
|
});
|
||||||
|
|
||||||
|
function closeImport() {
|
||||||
|
if (importBusy) return;
|
||||||
|
if (importing && importReason) requestDiscard(() => setImporting(null));
|
||||||
|
else setImporting(null);
|
||||||
|
}
|
||||||
|
|
||||||
async function downloadPackage(item: FormDefinition) {
|
async function downloadPackage(item: FormDefinition) {
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
@@ -135,14 +160,16 @@ export default function FormsPage({ settings, auth }: PlatformRouteContext) {
|
|||||||
<option value="retired">Retired</option>
|
<option value="retired">Retired</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<IconButton label="Refresh definitions" icon={<RefreshCw size={16} />} onClick={() => void load()} disabled={loading} />
|
<IconButton label="Refresh definitions" icon={<RefreshCw size={16} />} onClick={() => void load()} disabled={loading} disabledReason={loading ? FORMS_I18N.loading : undefined} />
|
||||||
{canWrite && <IconButton label="Import Form package" icon={<Upload size={16} />} onClick={() => importInput.current?.click()} disabled={loading} />}
|
<IconButton label="Import Form package" icon={<Upload size={16} />} onClick={() => importInput.current?.click()} disabled={loading || !canWrite} disabledReason={loading ? FORMS_I18N.loading : !canWrite ? FORMS_I18N.writeReason : undefined} />
|
||||||
<input ref={importInput} className="forms-hidden-input" type="file" accept="application/json,.json" onChange={(event) => void choosePackage(event.target.files?.[0])} />
|
<input ref={importInput} className="forms-hidden-input" type="file" accept="application/json,.json" onChange={(event) => void choosePackage(event.target.files?.[0])} />
|
||||||
{canWrite && <Button variant="primary" onClick={() => setEditing("new")}><Plus size={16} aria-hidden="true" />New definition</Button>}
|
<Button variant="primary" disabled={!canWrite} disabledReason={!canWrite ? FORMS_I18N.writeReason : undefined} onClick={() => setEditing("new")}><Plus size={16} aria-hidden="true" />New definition</Button>
|
||||||
<span className="forms-count">{total}</span>
|
<span className="forms-count">{total}</span>
|
||||||
|
<DocumentationHelpLink reference={FORMS_DOCUMENTATION} />
|
||||||
</div>
|
</div>
|
||||||
<PageScrollViewport className="forms-list-viewport">
|
<PageScrollViewport className="forms-list-viewport">
|
||||||
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
|
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
|
||||||
|
{!canWrite && <ActionBlockerHint tone="info" reason={{ summary: "No Form management permission", details: FORMS_I18N.writeReason, requiredAction: FORMS_I18N.permissionAction, actor: FORMS_I18N.permissionActor, target: FORMS_I18N.permissionDestination }} labels={{ requiredAction: FORMS_I18N.requiredAction, actor: FORMS_I18N.actor, target: FORMS_I18N.destination }} documentation={FORMS_DOCUMENTATION} />}
|
||||||
{loading && <LoadingIndicator label="Loading Form definitions" />}
|
{loading && <LoadingIndicator label="Loading Form definitions" />}
|
||||||
{!loading && !error && items.length === 0 && <div className="forms-empty">No matching definitions.</div>}
|
{!loading && !error && items.length === 0 && <div className="forms-empty">No matching definitions.</div>}
|
||||||
{!loading && items.length > 0 &&
|
{!loading && items.length > 0 &&
|
||||||
@@ -157,7 +184,7 @@ export default function FormsPage({ settings, auth }: PlatformRouteContext) {
|
|||||||
<StatusBadge status={item.publication_state === "published" ? "active" : "inactive"} label={humanize(item.publication_state)} />
|
<StatusBadge status={item.publication_state === "published" ? "active" : "inactive"} label={humanize(item.publication_state)} />
|
||||||
<span className="forms-row-actions">
|
<span className="forms-row-actions">
|
||||||
<IconButton label={`Export ${item.title}`} icon={<Download size={16} />} onClick={() => void downloadPackage(item)} />
|
<IconButton label={`Export ${item.title}`} icon={<Download size={16} />} onClick={() => void downloadPackage(item)} />
|
||||||
{mayRevise && <IconButton label={`Revise ${item.title}`} icon={<Pencil size={16} />} onClick={() => setEditing(item)} />}
|
<IconButton label={`Revise ${item.title}`} icon={<Pencil size={16} />} disabled={!mayRevise} disabledReason={!canWrite ? FORMS_I18N.writeReason : item.publication_state === "retired" || (item.publication_state === "published" && !canAdmin) ? FORMS_I18N.lifecycleReason : undefined} onClick={() => setEditing(item)} />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -183,16 +210,16 @@ export default function FormsPage({ settings, auth }: PlatformRouteContext) {
|
|||||||
<Dialog
|
<Dialog
|
||||||
open={Boolean(importing)}
|
open={Boolean(importing)}
|
||||||
title="Import Form package"
|
title="Import Form package"
|
||||||
onClose={() => setImporting(null)}
|
onClose={closeImport}
|
||||||
closeDisabled={importBusy}
|
closeDisabled={importBusy}
|
||||||
portal
|
portal
|
||||||
footer={<>
|
footer={<>
|
||||||
<Button onClick={() => setImporting(null)} disabled={importBusy}>Cancel</Button>
|
<Button onClick={closeImport} disabled={importBusy} disabledReason={importBusy ? FORMS_I18N.busy : undefined}>Cancel</Button>
|
||||||
<Button variant="primary" onClick={() => void importPackage()} disabled={importBusy || !importReason.trim()}>{importBusy ? "Importing" : "Import as draft"}</Button>
|
<Button variant="primary" onClick={() => void importPackage()} disabled={importBusy || !importReason.trim()} disabledReason={importBusy ? FORMS_I18N.busy : !importReason.trim() ? FORMS_I18N.incomplete : undefined}>{importBusy ? "Importing" : "Import as draft"}</Button>
|
||||||
</>}>
|
</>}>
|
||||||
<div className="forms-package-import">
|
<div className="forms-package-import">
|
||||||
<p>Assessment: <strong>{humanize(importAssessment)}</strong>. The source revision is retained as provenance and imported as a new local draft.</p>
|
<p>Assessment: <strong>{humanize(importAssessment)}</strong>. The source revision is retained as provenance and imported as a new local draft.</p>
|
||||||
<FormField label="Change reason"><input value={importReason} maxLength={1000} disabled={importBusy} onChange={(event) => setImportReason(event.target.value)} /></FormField>
|
<FormField label="Change reason" documentation={FORMS_FIELD_DOCUMENTATION}><input value={importReason} maxLength={1000} disabled={importBusy} onChange={(event) => setImportReason(event.target.value)} /></FormField>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type { DocumentationHelpReference } from "@govoplan/core-webui";
|
||||||
|
|
||||||
|
export const FORMS_DOCUMENTATION = {
|
||||||
|
topicId: "forms.definitions",
|
||||||
|
documentationType: "user"
|
||||||
|
} satisfies DocumentationHelpReference;
|
||||||
|
|
||||||
|
export const FORMS_FIELD_DOCUMENTATION = {
|
||||||
|
topicId: "forms.reference.fields-and-consequences",
|
||||||
|
documentationType: "admin"
|
||||||
|
} satisfies DocumentationHelpReference;
|
||||||
|
|
||||||
|
export const FORMS_I18N = {
|
||||||
|
loading: "i18n:govoplan-forms.loading_reason",
|
||||||
|
busy: "i18n:govoplan-forms.busy_reason",
|
||||||
|
writeReason: "i18n:govoplan-forms.write_permission_reason",
|
||||||
|
adminReason: "i18n:govoplan-forms.admin_permission_reason",
|
||||||
|
lifecycleReason: "i18n:govoplan-forms.lifecycle_reason",
|
||||||
|
incomplete: "i18n:govoplan-forms.incomplete_reason",
|
||||||
|
oneField: "i18n:govoplan-forms.one_field_reason",
|
||||||
|
requiredAction: "i18n:govoplan-forms.required_action",
|
||||||
|
actor: "i18n:govoplan-forms.responsible_actor",
|
||||||
|
destination: "i18n:govoplan-forms.destination",
|
||||||
|
permissionAction: "i18n:govoplan-forms.permission_action",
|
||||||
|
permissionActor: "i18n:govoplan-forms.permission_actor",
|
||||||
|
permissionDestination: "i18n:govoplan-forms.permission_destination"
|
||||||
|
} as const;
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import type { PlatformTranslations } from "@govoplan/core-webui";
|
||||||
|
|
||||||
|
const en = {
|
||||||
|
"i18n:govoplan-forms.form_definitions": "Form definitions",
|
||||||
|
"i18n:govoplan-forms.loading_reason": "Form definitions are still loading.",
|
||||||
|
"i18n:govoplan-forms.busy_reason": "Another Form definition action is still running.",
|
||||||
|
"i18n:govoplan-forms.write_permission_reason": "Your account may not create, import, or revise Form definitions.",
|
||||||
|
"i18n:govoplan-forms.admin_permission_reason": "Publishing and retirement require Form administration permission.",
|
||||||
|
"i18n:govoplan-forms.lifecycle_reason": "Retired definitions cannot be revised; published definitions require administrative authority.",
|
||||||
|
"i18n:govoplan-forms.incomplete_reason": "Complete the title, key, fields, and change reason first.",
|
||||||
|
"i18n:govoplan-forms.one_field_reason": "A Form definition must retain at least one field.",
|
||||||
|
"i18n:govoplan-forms.required_action": "Required action",
|
||||||
|
"i18n:govoplan-forms.responsible_actor": "Responsible actor",
|
||||||
|
"i18n:govoplan-forms.destination": "Destination",
|
||||||
|
"i18n:govoplan-forms.permission_action": "Ask for the corresponding Form definition permission.",
|
||||||
|
"i18n:govoplan-forms.permission_actor": "An Access or tenant administrator",
|
||||||
|
"i18n:govoplan-forms.permission_destination": "Access role assignments",
|
||||||
|
"i18n:govoplan-forms.unsaved_title": "Unsaved Form definition",
|
||||||
|
"i18n:govoplan-forms.unsaved_message": "Save or discard the Form definition draft before leaving this surface.",
|
||||||
|
"i18n:govoplan-forms.lifecycle_title": "Confirm definition lifecycle",
|
||||||
|
"i18n:govoplan-forms.lifecycle_message": "Save this revision as {state}? Existing submissions keep their exact definition revision.",
|
||||||
|
"Form definitions": "Form definitions",
|
||||||
|
"Search definitions": "Search definitions",
|
||||||
|
"Search Form definitions": "Search Form definitions",
|
||||||
|
"Search": "Search",
|
||||||
|
"State": "State",
|
||||||
|
"All": "All",
|
||||||
|
"Draft": "Draft",
|
||||||
|
"Published": "Published",
|
||||||
|
"Retired": "Retired",
|
||||||
|
"Refresh definitions": "Refresh definitions",
|
||||||
|
"Import Form package": "Import Form package",
|
||||||
|
"New definition": "New definition",
|
||||||
|
"Loading Form definitions": "Loading Form definitions",
|
||||||
|
"No matching definitions.": "No matching definitions.",
|
||||||
|
"Assessment": "Assessment",
|
||||||
|
"Cancel": "Cancel",
|
||||||
|
"Importing": "Importing",
|
||||||
|
"Import as draft": "Import as draft",
|
||||||
|
"Change reason": "Change reason",
|
||||||
|
"New Form definition": "New Form definition",
|
||||||
|
"Saving": "Saving",
|
||||||
|
"Save revision": "Save revision",
|
||||||
|
"Title": "Title",
|
||||||
|
"Key": "Key",
|
||||||
|
"Description": "Description",
|
||||||
|
"Publication state": "Publication state",
|
||||||
|
"Signature": "Signature",
|
||||||
|
"Not used": "Not used",
|
||||||
|
"Optional": "Optional",
|
||||||
|
"Required": "Required",
|
||||||
|
"Maximum attachments": "Maximum attachments",
|
||||||
|
"Draft saving": "Draft saving",
|
||||||
|
"Policy references": "Policy references",
|
||||||
|
"Permitted handoffs": "Permitted handoffs",
|
||||||
|
"Case": "Case",
|
||||||
|
"Workflow": "Workflow",
|
||||||
|
"Record": "Record",
|
||||||
|
"Accessibility instructions": "Accessibility instructions",
|
||||||
|
"Fields": "Fields",
|
||||||
|
"Add field": "Add field",
|
||||||
|
"Label": "Label",
|
||||||
|
"Type": "Type",
|
||||||
|
"Help text": "Help text",
|
||||||
|
"Options": "Options",
|
||||||
|
"Pages and sections": "Pages and sections",
|
||||||
|
"Enable pages": "Enable pages",
|
||||||
|
"Add page": "Add page",
|
||||||
|
"Add section": "Add section",
|
||||||
|
"Localizations": "Localizations",
|
||||||
|
"Add locale": "Add locale",
|
||||||
|
"Preview and revision changes": "Preview and revision changes",
|
||||||
|
"No Form management permission": "No Form management permission"
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const de: Record<keyof typeof en, string> = {
|
||||||
|
"i18n:govoplan-forms.form_definitions": "Formulardefinitionen",
|
||||||
|
"i18n:govoplan-forms.loading_reason": "Formulardefinitionen werden noch geladen.",
|
||||||
|
"i18n:govoplan-forms.busy_reason": "Eine andere Aktion für Formulardefinitionen läuft noch.",
|
||||||
|
"i18n:govoplan-forms.write_permission_reason": "Ihr Konto darf Formulardefinitionen nicht erstellen, importieren oder überarbeiten.",
|
||||||
|
"i18n:govoplan-forms.admin_permission_reason": "Veröffentlichung und Stilllegung erfordern die Formularadministration.",
|
||||||
|
"i18n:govoplan-forms.lifecycle_reason": "Stillgelegte Definitionen können nicht überarbeitet werden; veröffentlichte Definitionen erfordern administrative Berechtigung.",
|
||||||
|
"i18n:govoplan-forms.incomplete_reason": "Füllen Sie zuerst Titel, Schlüssel, Felder und Änderungsgrund aus.",
|
||||||
|
"i18n:govoplan-forms.one_field_reason": "Eine Formulardefinition muss mindestens ein Feld behalten.",
|
||||||
|
"i18n:govoplan-forms.required_action": "Erforderliche Aktion",
|
||||||
|
"i18n:govoplan-forms.responsible_actor": "Verantwortliche Stelle",
|
||||||
|
"i18n:govoplan-forms.destination": "Ziel",
|
||||||
|
"i18n:govoplan-forms.permission_action": "Fordern Sie die entsprechende Berechtigung für Formulardefinitionen an.",
|
||||||
|
"i18n:govoplan-forms.permission_actor": "Eine Zugriffs- oder Mandantenadministration",
|
||||||
|
"i18n:govoplan-forms.permission_destination": "Zugriff und Rollenzuweisungen",
|
||||||
|
"i18n:govoplan-forms.unsaved_title": "Ungespeicherte Formulardefinition",
|
||||||
|
"i18n:govoplan-forms.unsaved_message": "Speichern oder verwerfen Sie den Entwurf der Formulardefinition, bevor Sie diese Oberfläche verlassen.",
|
||||||
|
"i18n:govoplan-forms.lifecycle_title": "Definitionslebenszyklus bestätigen",
|
||||||
|
"i18n:govoplan-forms.lifecycle_message": "Diese Revision als {state} speichern? Bestehende Einreichungen behalten ihre exakte Definitionsrevision.",
|
||||||
|
"Form definitions": "Formulardefinitionen",
|
||||||
|
"Search definitions": "Definitionen suchen",
|
||||||
|
"Search Form definitions": "Formulardefinitionen durchsuchen",
|
||||||
|
"Search": "Suchen",
|
||||||
|
"State": "Status",
|
||||||
|
"All": "Alle",
|
||||||
|
"Draft": "Entwurf",
|
||||||
|
"Published": "Veröffentlicht",
|
||||||
|
"Retired": "Stillgelegt",
|
||||||
|
"Refresh definitions": "Definitionen aktualisieren",
|
||||||
|
"Import Form package": "Formularpaket importieren",
|
||||||
|
"New definition": "Neue Definition",
|
||||||
|
"Loading Form definitions": "Formulardefinitionen werden geladen",
|
||||||
|
"No matching definitions.": "Keine passenden Definitionen.",
|
||||||
|
"Assessment": "Bewertung",
|
||||||
|
"Cancel": "Abbrechen",
|
||||||
|
"Importing": "Importieren",
|
||||||
|
"Import as draft": "Als Entwurf importieren",
|
||||||
|
"Change reason": "Änderungsgrund",
|
||||||
|
"New Form definition": "Neue Formulardefinition",
|
||||||
|
"Saving": "Speichern",
|
||||||
|
"Save revision": "Revision speichern",
|
||||||
|
"Title": "Titel",
|
||||||
|
"Key": "Schlüssel",
|
||||||
|
"Description": "Beschreibung",
|
||||||
|
"Publication state": "Veröffentlichungsstatus",
|
||||||
|
"Signature": "Signatur",
|
||||||
|
"Not used": "Nicht verwendet",
|
||||||
|
"Optional": "Optional",
|
||||||
|
"Required": "Erforderlich",
|
||||||
|
"Maximum attachments": "Maximale Anhänge",
|
||||||
|
"Draft saving": "Entwurfsspeicherung",
|
||||||
|
"Policy references": "Richtlinienreferenzen",
|
||||||
|
"Permitted handoffs": "Zulässige Übergaben",
|
||||||
|
"Case": "Fall",
|
||||||
|
"Workflow": "Workflow",
|
||||||
|
"Record": "Datensatz",
|
||||||
|
"Accessibility instructions": "Hinweise zur Barrierefreiheit",
|
||||||
|
"Fields": "Felder",
|
||||||
|
"Add field": "Feld hinzufügen",
|
||||||
|
"Label": "Bezeichnung",
|
||||||
|
"Type": "Typ",
|
||||||
|
"Help text": "Hilfetext",
|
||||||
|
"Options": "Optionen",
|
||||||
|
"Pages and sections": "Seiten und Abschnitte",
|
||||||
|
"Enable pages": "Seiten aktivieren",
|
||||||
|
"Add page": "Seite hinzufügen",
|
||||||
|
"Add section": "Abschnitt hinzufügen",
|
||||||
|
"Localizations": "Lokalisierungen",
|
||||||
|
"Add locale": "Sprache hinzufügen",
|
||||||
|
"Preview and revision changes": "Vorschau und Revisionsänderungen",
|
||||||
|
"No Form management permission": "Keine Berechtigung zur Formularverwaltung"
|
||||||
|
};
|
||||||
|
|
||||||
|
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.css";
|
import "./styles/forms.css";
|
||||||
|
|
||||||
|
|
||||||
@@ -7,10 +8,11 @@ const FormsPage = lazy(() => import("./features/forms/FormsPage"));
|
|||||||
|
|
||||||
export const formsModule: PlatformWebModule = {
|
export const formsModule: PlatformWebModule = {
|
||||||
id: "forms",
|
id: "forms",
|
||||||
label: "Form definitions",
|
label: "i18n:govoplan-forms.form_definitions",
|
||||||
version: "0.1.14",
|
version: "0.1.14",
|
||||||
dependencies: ["access"],
|
dependencies: ["access"],
|
||||||
optionalDependencies: ["forms_runtime", "portal", "workflow_engine", "cases", "policy"],
|
optionalDependencies: ["forms_runtime", "portal", "workflow_engine", "cases", "policy"],
|
||||||
|
translations: generatedTranslations,
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: "/forms",
|
path: "/forms",
|
||||||
@@ -23,7 +25,7 @@ export const formsModule: PlatformWebModule = {
|
|||||||
navItems: [
|
navItems: [
|
||||||
{
|
{
|
||||||
to: "/forms",
|
to: "/forms",
|
||||||
label: "Form definitions",
|
label: "i18n:govoplan-forms.form_definitions",
|
||||||
iconName: "list-tree",
|
iconName: "list-tree",
|
||||||
anyOf: ["forms:definition:read"],
|
anyOf: ["forms:definition:read"],
|
||||||
order: 36,
|
order: 36,
|
||||||
|
|||||||
@@ -52,6 +52,10 @@
|
|||||||
padding: 16px 18px 24px;
|
padding: 16px 18px 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.forms-list-viewport > .action-blocker-hint {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.forms-list {
|
.forms-list {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
@@ -136,6 +140,11 @@
|
|||||||
gap: 18px;
|
gap: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-definition-help {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.form-definition-grid {
|
.form-definition-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
|||||||
Reference in New Issue
Block a user