Adopt shared WebUI layout primitives
This commit is contained in:
@@ -29,5 +29,8 @@ submissions, receipts, and handoff execution.
|
|||||||
|
|
||||||
The module uses shared controls, dialogs, blockers, field help, statuses,
|
The module uses shared controls, dialogs, blockers, field help, statuses,
|
||||||
loading, empty/error states, disabled reasons, confirmations, and unsaved-draft
|
loading, empty/error states, disabled reasons, confirmations, and unsaved-draft
|
||||||
guards. Existing responsive list/editor layouts remain bounded. English and
|
guards. The catalogue action row now uses the shared responsive
|
||||||
German catalogues cover the owned route and editor vocabulary.
|
`ActionToolbar`; equal-column definition/editor groups use `FormGrid` and its
|
||||||
|
named narrow-layout collapse policy. These layout contracts do not change
|
||||||
|
filter scope, permissions, revision semantics, or publication consequences.
|
||||||
|
English and German catalogues cover the owned route and editor vocabulary.
|
||||||
|
|||||||
@@ -194,7 +194,8 @@ manifest = ModuleManifest(
|
|||||||
summary="Create immutable, versioned schemas consumed by Forms Runtime and institutional services.",
|
summary="Create immutable, versioned schemas consumed by Forms Runtime and institutional services.",
|
||||||
body=(
|
body=(
|
||||||
"Each revision fixes field types, options, constraints, draft, attachment, signature, policy, and handoff requirements. "
|
"Each revision fixes field types, options, constraints, draft, attachment, signature, policy, and handoff requirements. "
|
||||||
"Publishing is explicit; existing submissions continue to retain their exact revision."
|
"Publishing is explicit; existing submissions continue to retain their exact revision. The catalogue action row and "
|
||||||
|
"definition field groups reflow at narrow widths without changing filter scope, permissions, or lifecycle effects."
|
||||||
),
|
),
|
||||||
layer="configured",
|
layer="configured",
|
||||||
documentation_types=("admin", "user"),
|
documentation_types=("admin", "user"),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ArrowDown, ArrowUp, Eye, Languages, Plus, Trash2 } from "lucide-react";
|
import { ArrowDown, ArrowUp, Eye, Languages, Plus, Trash2 } from "lucide-react";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import { FormGrid,
|
||||||
Button,
|
Button,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -198,7 +198,7 @@ export default function FormDefinitionDialog({
|
|||||||
<div className="form-definition-editor">
|
<div className="form-definition-editor">
|
||||||
<div className="form-definition-help"><DocumentationHelpLink reference={FORMS_FIELD_DOCUMENTATION} /></div>
|
<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">
|
<FormGrid columns={2} gap="small" collapseAt="narrow">
|
||||||
<Field label="Title">
|
<Field label="Title">
|
||||||
<input value={draft.title} disabled={busy} onChange={(event) => setDraft({ ...draft, title: event.target.value })} />
|
<input value={draft.title} disabled={busy} onChange={(event) => setDraft({ ...draft, title: event.target.value })} />
|
||||||
</Field>
|
</Field>
|
||||||
@@ -260,7 +260,7 @@ export default function FormDefinitionDialog({
|
|||||||
placeholder="Optional instructions announced before the Form"
|
placeholder="Optional instructions announced before the Form"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</FormGrid>
|
||||||
|
|
||||||
<div className="form-field-editor-heading">
|
<div className="form-field-editor-heading">
|
||||||
<h3>Fields</h3>
|
<h3>Fields</h3>
|
||||||
@@ -333,19 +333,19 @@ export default function FormDefinitionDialog({
|
|||||||
function ConstraintFields({ field, disabled, onChange }: { field: FormFieldDefinition; disabled: boolean; onChange: (value: Record<string, unknown>) => void }) {
|
function ConstraintFields({ field, disabled, onChange }: { field: FormFieldDefinition; disabled: boolean; onChange: (value: Record<string, unknown>) => void }) {
|
||||||
if (["text", "multiline_text", "email"].includes(field.value_type)) {
|
if (["text", "multiline_text", "email"].includes(field.value_type)) {
|
||||||
return (
|
return (
|
||||||
<div className="form-field-constraints">
|
<FormGrid columns={3} gap="compact" collapseAt="narrow" className="form-field-constraints">
|
||||||
<Field label="Minimum length"><input type="number" min={0} value={constraintValue(field.constraints.min_length)} disabled={disabled} onChange={(event) => onChange(patchConstraint(field.constraints, "min_length", event.target.value))} /></Field>
|
<Field label="Minimum length"><input type="number" min={0} value={constraintValue(field.constraints.min_length)} disabled={disabled} onChange={(event) => onChange(patchConstraint(field.constraints, "min_length", event.target.value))} /></Field>
|
||||||
<Field label="Maximum length"><input type="number" min={0} value={constraintValue(field.constraints.max_length)} disabled={disabled} onChange={(event) => onChange(patchConstraint(field.constraints, "max_length", event.target.value))} /></Field>
|
<Field label="Maximum length"><input type="number" min={0} value={constraintValue(field.constraints.max_length)} disabled={disabled} onChange={(event) => onChange(patchConstraint(field.constraints, "max_length", event.target.value))} /></Field>
|
||||||
<Field label="Pattern"><input value={String(field.constraints.pattern ?? "")} disabled={disabled} onChange={(event) => onChange(patchTextConstraint(field.constraints, "pattern", event.target.value))} /></Field>
|
<Field label="Pattern"><input value={String(field.constraints.pattern ?? "")} disabled={disabled} onChange={(event) => onChange(patchTextConstraint(field.constraints, "pattern", event.target.value))} /></Field>
|
||||||
</div>
|
</FormGrid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (["integer", "number"].includes(field.value_type)) {
|
if (["integer", "number"].includes(field.value_type)) {
|
||||||
return (
|
return (
|
||||||
<div className="form-field-constraints">
|
<FormGrid columns={3} gap="compact" collapseAt="narrow" className="form-field-constraints">
|
||||||
<Field label="Minimum"><input type="number" value={constraintValue(field.constraints.minimum)} disabled={disabled} onChange={(event) => onChange(patchConstraint(field.constraints, "minimum", event.target.value))} /></Field>
|
<Field label="Minimum"><input type="number" value={constraintValue(field.constraints.minimum)} disabled={disabled} onChange={(event) => onChange(patchConstraint(field.constraints, "minimum", event.target.value))} /></Field>
|
||||||
<Field label="Maximum"><input type="number" value={constraintValue(field.constraints.maximum)} disabled={disabled} onChange={(event) => onChange(patchConstraint(field.constraints, "maximum", event.target.value))} /></Field>
|
<Field label="Maximum"><input type="number" value={constraintValue(field.constraints.maximum)} disabled={disabled} onChange={(event) => onChange(patchConstraint(field.constraints, "maximum", event.target.value))} /></Field>
|
||||||
</div>
|
</FormGrid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -367,7 +367,7 @@ function ConditionFields({
|
|||||||
const predicate = condition?.kind === "predicate" ? condition : null;
|
const predicate = condition?.kind === "predicate" ? condition : null;
|
||||||
const candidates = fields.filter((item) => item.key !== currentKey && item.key.trim());
|
const candidates = fields.filter((item) => item.key !== currentKey && item.key.trim());
|
||||||
return (
|
return (
|
||||||
<div className="form-field-condition">
|
<FormGrid columns={3} gap="compact" collapseAt="narrow" className="form-field-condition">
|
||||||
<Field label="Visible when">
|
<Field label="Visible when">
|
||||||
<select
|
<select
|
||||||
value={predicate?.field_key ?? ""}
|
value={predicate?.field_key ?? ""}
|
||||||
@@ -406,7 +406,7 @@ function ConditionFields({
|
|||||||
</Field>
|
</Field>
|
||||||
}
|
}
|
||||||
</>}
|
</>}
|
||||||
</div>
|
</FormGrid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -506,7 +506,7 @@ function DefinitionPreview({ definition, previous }: { definition: FormDefinitio
|
|||||||
return (
|
return (
|
||||||
<section className="form-composition-section form-definition-preview">
|
<section className="form-composition-section form-definition-preview">
|
||||||
<div className="form-field-editor-heading"><h3><Eye size={17} aria-hidden="true" />Preview and revision changes</h3></div>
|
<div className="form-field-editor-heading"><h3><Eye size={17} aria-hidden="true" />Preview and revision changes</h3></div>
|
||||||
<div className="form-preview-grid">
|
<FormGrid columns={2} collapseAt="narrow" className="form-preview-grid">
|
||||||
<div>
|
<div>
|
||||||
<strong>{definition.title || "Untitled Form"}</strong>
|
<strong>{definition.title || "Untitled Form"}</strong>
|
||||||
{(definition.pages?.length ? definition.pages : [defaultPage(definition.fields)]).map((page) =>
|
{(definition.pages?.length ? definition.pages : [defaultPage(definition.fields)]).map((page) =>
|
||||||
@@ -517,7 +517,7 @@ function DefinitionPreview({ definition, previous }: { definition: FormDefinitio
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div><strong>Changes</strong><ul>{changed.map((item) => <li key={item}>{item}</li>)}</ul></div>
|
<div><strong>Changes</strong><ul>{changed.map((item) => <li key={item}>{item}</li>)}</ul></div>
|
||||||
</div>
|
</FormGrid>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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 { ActionToolbar,
|
||||||
ActionBlockerHint,
|
ActionBlockerHint,
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -145,7 +145,7 @@ export default function FormsPage({ settings, auth }: PlatformRouteContext) {
|
|||||||
return (
|
return (
|
||||||
<main className="forms-page">
|
<main className="forms-page">
|
||||||
<div className="forms-shell">
|
<div className="forms-shell">
|
||||||
<div className="forms-toolbar">
|
<ActionToolbar className="forms-toolbar">
|
||||||
<form onSubmit={search} className="forms-search">
|
<form onSubmit={search} className="forms-search">
|
||||||
<Search size={17} aria-hidden="true" />
|
<Search size={17} aria-hidden="true" />
|
||||||
<input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Search definitions" aria-label="Search Form definitions" />
|
<input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Search definitions" aria-label="Search Form definitions" />
|
||||||
@@ -166,7 +166,7 @@ export default function FormsPage({ settings, auth }: PlatformRouteContext) {
|
|||||||
<Button variant="primary" disabled={!canWrite} disabledReason={!canWrite ? FORMS_I18N.writeReason : undefined} 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} />
|
<DocumentationHelpLink reference={FORMS_DOCUMENTATION} />
|
||||||
</div>
|
</ActionToolbar>
|
||||||
<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} />}
|
{!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} />}
|
||||||
|
|||||||
@@ -145,12 +145,6 @@
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-definition-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 12px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-definition-wide {
|
.form-definition-wide {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
@@ -229,13 +223,6 @@
|
|||||||
grid-column: 2 / -1;
|
grid-column: 2 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field-constraints,
|
|
||||||
.form-field-condition {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
gap: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-composition-section {
|
.form-composition-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -302,9 +289,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form-preview-grid {
|
.form-preview-grid {
|
||||||
display: grid;
|
min-width: 0;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 18px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-preview-grid > div {
|
.form-preview-grid > div {
|
||||||
@@ -374,14 +359,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 620px) {
|
@media (max-width: 620px) {
|
||||||
.form-definition-grid,
|
|
||||||
.form-field-constraints,
|
|
||||||
.form-field-condition,
|
|
||||||
.form-page-editor-heading,
|
.form-page-editor-heading,
|
||||||
.form-section-editor,
|
.form-section-editor,
|
||||||
.form-localization-editor,
|
.form-localization-editor,
|
||||||
.form-localization-fields,
|
.form-localization-fields {
|
||||||
.form-preview-grid {
|
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user