Migrate Forms interface patterns

This commit is contained in:
2026-08-03 13:08:43 +02:00
parent e2033640a1
commit e505536e6f
9 changed files with 429 additions and 31 deletions
@@ -2,11 +2,17 @@ import { ArrowDown, ArrowUp, Eye, Languages, Plus, Trash2 } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import {
Button,
ConfirmDialog,
Dialog,
DocumentationHelpLink,
DismissibleAlert,
FormField as Field,
IconButton,
ToggleSwitch,
i18nMessage,
usePlatformLanguage,
useUnsavedChanges,
useUnsavedDraftGuard,
type ApiSettings
} from "@govoplan/core-webui";
import {
@@ -18,6 +24,7 @@ import {
type FormPageDefinition,
type FormValueType
} from "../../api/forms";
import { FORMS_FIELD_DOCUMENTATION, FORMS_I18N } from "./interfacePatterns";
const VALUE_TYPES: Array<{ value: FormValueType; label: string }> = [
@@ -52,17 +59,24 @@ export default function FormDefinitionDialog({
onClose: () => 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 [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const [confirmLifecycle, setConfirmLifecycle] = useState(false);
const { requestDiscard } = useUnsavedChanges();
useEffect(() => {
if (!open) return;
setDraft(initialDraft(tenantId, definition));
const next = initialDraft(tenantId, definition);
setDraft(next);
setBaseline(next);
setChangeReason("");
setBusy(false);
setError("");
setConfirmLifecycle(false);
}, [definition, open, tenantId]);
const valid = useMemo(() => Boolean(
@@ -72,9 +86,13 @@ export default function FormDefinitionDialog({
&& draft.fields.every((field) => field.key.trim() && field.label.trim())
&& changeReason.trim()
), [changeReason, draft]);
const dirty = useMemo(
() => Boolean(changeReason || JSON.stringify(draft) !== JSON.stringify(baseline)),
[baseline, changeReason, draft]
);
async function save() {
if (!valid) return;
async function save(): Promise<boolean> {
if (!valid) return false;
setBusy(true);
setError("");
const revision = crypto.randomUUID();
@@ -106,13 +124,41 @@ export default function FormDefinitionDialog({
definition?.reference.version
);
onSaved(saved);
return true;
} catch (reason) {
setError(reason instanceof Error ? reason.message : "The Form definition could not be saved.");
return false;
} finally {
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>) {
setDraft((current) => ({
...current,
@@ -137,19 +183,20 @@ export default function FormDefinitionDialog({
<Dialog
open={open}
title={definition ? `Revise ${definition.title}` : "New Form definition"}
onClose={onClose}
onClose={requestClose}
closeDisabled={busy}
portal
className="form-definition-dialog"
footer={
<>
<Button onClick={onClose} disabled={busy}>Cancel</Button>
<Button variant="primary" onClick={() => void save()} disabled={busy || !valid}>
<Button onClick={requestClose} disabled={busy} disabledReason={busy ? FORMS_I18N.busy : undefined}>Cancel</Button>
<Button variant="primary" onClick={requestSave} disabled={busy || !valid} disabledReason={busy ? FORMS_I18N.busy : !valid ? FORMS_I18N.incomplete : undefined}>
{busy ? "Saving" : "Save revision"}
</Button>
</>
}>
<div className="form-definition-editor">
<div className="form-definition-help"><DocumentationHelpLink reference={FORMS_FIELD_DOCUMENTATION} /></div>
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
<div className="form-definition-grid">
<Field label="Title">
@@ -161,14 +208,14 @@ export default function FormDefinitionDialog({
<Field label="Description" className="form-definition-wide">
<textarea rows={3} value={draft.description ?? ""} disabled={busy} onChange={(event) => setDraft({ ...draft, description: event.target.value })} />
</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"] })}>
{definition?.publication_state !== "published" && <option value="draft">Draft</option>}
{canPublish && <option value="published">Published</option>}
{canPublish && <option value="retired">Retired</option>}
</select>
</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"] })}>
<option value="none">Not used</option>
<option value="optional">Optional</option>
@@ -181,7 +228,7 @@ export default function FormDefinitionDialog({
<div className="form-definition-toggle">
<ToggleSwitch label="Draft saving" checked={draft.allow_drafts} disabled={busy} onChange={(allow_drafts) => setDraft({ ...draft, allow_drafts })} />
</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) })} />
</Field>
<div className="form-definition-handoffs form-definition-wide">
@@ -201,7 +248,7 @@ export default function FormDefinitionDialog({
/>
)}
</div>
<Field label="Accessibility instructions" className="form-definition-wide">
<Field label="Accessibility instructions" className="form-definition-wide" documentation={FORMS_FIELD_DOCUMENTATION}>
<textarea
rows={2}
value={String(draft.accessibility?.instructions ?? "")}
@@ -253,6 +300,7 @@ export default function FormDefinitionDialog({
icon={<Trash2 size={16} />}
variant="danger"
disabled={busy || draft.fields.length === 1}
disabledReason={busy ? FORMS_I18N.busy : draft.fields.length === 1 ? FORMS_I18N.oneField : undefined}
onClick={() => setDraft(removeField(draft, index))}
/>
</div>
@@ -261,10 +309,23 @@ export default function FormDefinitionDialog({
<PageEditor draft={draft} disabled={busy} onChange={setDraft} />
<LocalizationEditor draft={draft} disabled={busy} onChange={setDraft} />
<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?" />
</Field>
</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>
);
}
+37 -10
View File
@@ -1,8 +1,10 @@
import { Download, Pencil, Plus, RefreshCw, Search, Upload } from "lucide-react";
import { useCallback, useEffect, useRef, useState, type FormEvent } from "react";
import {
ActionBlockerHint,
Button,
Dialog,
DocumentationHelpLink,
DismissibleAlert,
IconButton,
FormField,
@@ -10,6 +12,8 @@ import {
PageScrollViewport,
StatusBadge,
hasScope,
useUnsavedChanges,
useUnsavedDraftGuard,
type PlatformRouteContext
} from "@govoplan/core-webui";
import {
@@ -21,6 +25,7 @@ import {
type FormPackageFragment
} from "../../api/forms";
import FormDefinitionDialog from "./FormDefinitionDialog";
import { FORMS_DOCUMENTATION, FORMS_FIELD_DOCUMENTATION, FORMS_I18N } from "./interfacePatterns";
export default function FormsPage({ settings, auth }: PlatformRouteContext) {
@@ -36,6 +41,7 @@ export default function FormsPage({ settings, auth }: PlatformRouteContext) {
const [importReason, setImportReason] = useState("");
const [importAssessment, setImportAssessment] = useState("");
const [importBusy, setImportBusy] = useState(false);
const { requestDiscard } = useUnsavedChanges();
const importInput = useRef<HTMLInputElement>(null);
const canWrite = hasScope(auth, "forms:definition:write");
const canAdmin = hasScope(auth, "forms:definition:admin");
@@ -87,21 +93,40 @@ export default function FormsPage({ settings, auth }: PlatformRouteContext) {
}
}
async function importPackage() {
if (!importing || !importReason.trim()) return;
async function importPackage(): Promise<boolean> {
if (!importing || !importReason.trim()) return false;
setImportBusy(true);
setError("");
try {
await importFormDefinitionPackage(settings, importing, { changeReason: importReason.trim() });
setImporting(null);
await load();
return true;
} catch (reason) {
setError(reason instanceof Error ? reason.message : "The Form package could not be imported.");
return false;
} finally {
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) {
setError("");
try {
@@ -135,14 +160,16 @@ export default function FormsPage({ settings, auth }: PlatformRouteContext) {
<option value="retired">Retired</option>
</select>
</label>
<IconButton label="Refresh definitions" icon={<RefreshCw size={16} />} onClick={() => void load()} disabled={loading} />
{canWrite && <IconButton label="Import Form package" icon={<Upload size={16} />} onClick={() => importInput.current?.click()} disabled={loading} />}
<IconButton label="Refresh definitions" icon={<RefreshCw size={16} />} onClick={() => void load()} disabled={loading} disabledReason={loading ? FORMS_I18N.loading : undefined} />
<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])} />
{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>
<DocumentationHelpLink reference={FORMS_DOCUMENTATION} />
</div>
<PageScrollViewport className="forms-list-viewport">
{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 && !error && items.length === 0 && <div className="forms-empty">No matching definitions.</div>}
{!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)} />
<span className="forms-row-actions">
<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>
</div>
);
@@ -183,16 +210,16 @@ export default function FormsPage({ settings, auth }: PlatformRouteContext) {
<Dialog
open={Boolean(importing)}
title="Import Form package"
onClose={() => setImporting(null)}
onClose={closeImport}
closeDisabled={importBusy}
portal
footer={<>
<Button onClick={() => setImporting(null)} disabled={importBusy}>Cancel</Button>
<Button variant="primary" onClick={() => void importPackage()} disabled={importBusy || !importReason.trim()}>{importBusy ? "Importing" : "Import as draft"}</Button>
<Button onClick={closeImport} disabled={importBusy} disabledReason={importBusy ? FORMS_I18N.busy : undefined}>Cancel</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">
<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>
</Dialog>
</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;
+149
View File
@@ -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
View File
@@ -1,5 +1,6 @@
import { createElement, lazy } from "react";
import type { PlatformWebModule } from "@govoplan/core-webui";
import { generatedTranslations } from "./i18n/generatedTranslations";
import "./styles/forms.css";
@@ -7,10 +8,11 @@ const FormsPage = lazy(() => import("./features/forms/FormsPage"));
export const formsModule: PlatformWebModule = {
id: "forms",
label: "Form definitions",
label: "i18n:govoplan-forms.form_definitions",
version: "0.1.14",
dependencies: ["access"],
optionalDependencies: ["forms_runtime", "portal", "workflow_engine", "cases", "policy"],
translations: generatedTranslations,
routes: [
{
path: "/forms",
@@ -23,7 +25,7 @@ export const formsModule: PlatformWebModule = {
navItems: [
{
to: "/forms",
label: "Form definitions",
label: "i18n:govoplan-forms.form_definitions",
iconName: "list-tree",
anyOf: ["forms:definition:read"],
order: 36,
+9
View File
@@ -52,6 +52,10 @@
padding: 16px 18px 24px;
}
.forms-list-viewport > .action-blocker-hint {
margin-bottom: 16px;
}
.forms-list {
overflow: hidden;
border: 1px solid var(--border);
@@ -136,6 +140,11 @@
gap: 18px;
}
.form-definition-help {
display: flex;
justify-content: flex-end;
}
.form-definition-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));