Adopt shared WebUI structural primitives
This commit is contained in:
@@ -29,8 +29,10 @@ 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. The catalogue action row now uses the shared responsive
|
guards. The catalogue now composes `WorkspaceFrame`, `ActionToolbar`,
|
||||||
`ActionToolbar`; equal-column definition/editor groups use `FormGrid` and its
|
`FilterBar`, and `StatePanel`; equal-column definition/editor groups use
|
||||||
named narrow-layout collapse policy. These layout contracts do not change
|
`FormGrid`, while field, page, localization, and preview headings use the
|
||||||
|
shared section-header toolbar surface. Core therefore owns viewport, search,
|
||||||
|
state, heading, and narrow-layout geometry. These contracts do not change
|
||||||
filter scope, permissions, revision semantics, or publication consequences.
|
filter scope, permissions, revision semantics, or publication consequences.
|
||||||
English and German catalogues cover the owned route and editor vocabulary.
|
English and German catalogues cover the owned route and editor vocabulary.
|
||||||
|
|||||||
@@ -194,8 +194,9 @@ 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. The catalogue action row and "
|
"Publishing is explicit; existing submissions continue to retain their exact revision. The catalogue, filter, empty states, "
|
||||||
"definition field groups reflow at narrow widths without changing filter scope, permissions, or lifecycle effects."
|
"section headings, and definition field groups use the shared responsive layout language and 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,7 @@
|
|||||||
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 { FormGrid,
|
import { FormGrid,
|
||||||
|
ActionToolbar,
|
||||||
Button,
|
Button,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -262,12 +263,12 @@ export default function FormDefinitionDialog({
|
|||||||
</Field>
|
</Field>
|
||||||
</FormGrid>
|
</FormGrid>
|
||||||
|
|
||||||
<div className="form-field-editor-heading">
|
<ActionToolbar surface="section-header" className="form-field-editor-heading">
|
||||||
<h3>Fields</h3>
|
<h3>Fields</h3>
|
||||||
<Button onClick={() => setDraft(addField(draft))} disabled={busy}>
|
<Button onClick={() => setDraft(addField(draft))} disabled={busy}>
|
||||||
<Plus size={16} aria-hidden="true" />Add field
|
<Plus size={16} aria-hidden="true" />Add field
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</ActionToolbar>
|
||||||
<div className="form-field-editor-list">
|
<div className="form-field-editor-list">
|
||||||
{draft.fields.map((field, index) =>
|
{draft.fields.map((field, index) =>
|
||||||
<div className="form-field-editor-row" key={`${index}:${field.key}`}>
|
<div className="form-field-editor-row" key={`${index}:${field.key}`}>
|
||||||
@@ -417,12 +418,12 @@ function PageEditor({ draft, disabled, onChange }: { draft: FormDefinition; disa
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<section className="form-composition-section">
|
<section className="form-composition-section">
|
||||||
<div className="form-field-editor-heading">
|
<ActionToolbar surface="section-header" className="form-field-editor-heading">
|
||||||
<h3>Pages and sections</h3>
|
<h3>Pages and sections</h3>
|
||||||
{pages.length === 0
|
{pages.length === 0
|
||||||
? <Button disabled={disabled} onClick={() => updatePages([defaultPage(draft.fields)])}><Plus size={16} aria-hidden="true" />Enable pages</Button>
|
? <Button disabled={disabled} onClick={() => updatePages([defaultPage(draft.fields)])}><Plus size={16} aria-hidden="true" />Enable pages</Button>
|
||||||
: <Button disabled={disabled} onClick={() => updatePages([...pages, emptyPage(pages.length + 1)])}><Plus size={16} aria-hidden="true" />Add page</Button>}
|
: <Button disabled={disabled} onClick={() => updatePages([...pages, emptyPage(pages.length + 1)])}><Plus size={16} aria-hidden="true" />Add page</Button>}
|
||||||
</div>
|
</ActionToolbar>
|
||||||
{pages.length === 0 && <p className="form-section-note">Fields render in their declared order on one page.</p>}
|
{pages.length === 0 && <p className="form-section-note">Fields render in their declared order on one page.</p>}
|
||||||
{pages.map((page, pageIndex) =>
|
{pages.map((page, pageIndex) =>
|
||||||
<div className="form-page-editor" key={`${pageIndex}:${page.key}`}>
|
<div className="form-page-editor" key={`${pageIndex}:${page.key}`}>
|
||||||
@@ -470,10 +471,10 @@ function LocalizationEditor({ draft, disabled, onChange }: { draft: FormDefiniti
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<section className="form-composition-section">
|
<section className="form-composition-section">
|
||||||
<div className="form-field-editor-heading">
|
<ActionToolbar surface="section-header" className="form-field-editor-heading">
|
||||||
<h3><Languages size={17} aria-hidden="true" />Localizations</h3>
|
<h3><Languages size={17} aria-hidden="true" />Localizations</h3>
|
||||||
<Button disabled={disabled} onClick={() => update([...localizations, emptyLocalization()])}><Plus size={16} aria-hidden="true" />Add locale</Button>
|
<Button disabled={disabled} onClick={() => update([...localizations, emptyLocalization()])}><Plus size={16} aria-hidden="true" />Add locale</Button>
|
||||||
</div>
|
</ActionToolbar>
|
||||||
{localizations.length === 0 && <p className="form-section-note">The canonical labels are used for every locale.</p>}
|
{localizations.length === 0 && <p className="form-section-note">The canonical labels are used for every locale.</p>}
|
||||||
{localizations.map((localization, index) =>
|
{localizations.map((localization, index) =>
|
||||||
<div className="form-localization-editor" key={`${index}:${localization.locale}`}>
|
<div className="form-localization-editor" key={`${index}:${localization.locale}`}>
|
||||||
@@ -505,7 +506,7 @@ function DefinitionPreview({ definition, previous }: { definition: FormDefinitio
|
|||||||
const changed = previous ? definitionChanges(previous, definition) : ["New definition"];
|
const changed = previous ? definitionChanges(previous, definition) : ["New definition"];
|
||||||
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>
|
<ActionToolbar surface="section-header" className="form-field-editor-heading"><h3><Eye size={17} aria-hidden="true" />Preview and revision changes</h3></ActionToolbar>
|
||||||
<FormGrid columns={2} collapseAt="narrow" 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>
|
||||||
|
|||||||
@@ -8,12 +8,15 @@ import { ActionToolbar,
|
|||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
IconButton,
|
IconButton,
|
||||||
FormField,
|
FormField,
|
||||||
|
FilterBar,
|
||||||
LoadingIndicator,
|
LoadingIndicator,
|
||||||
PageScrollViewport,
|
PageScrollViewport,
|
||||||
|
StatePanel,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
hasScope,
|
hasScope,
|
||||||
useUnsavedChanges,
|
useUnsavedChanges,
|
||||||
useUnsavedDraftGuard,
|
useUnsavedDraftGuard,
|
||||||
|
WorkspaceFrame,
|
||||||
type PlatformRouteContext
|
type PlatformRouteContext
|
||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
import {
|
import {
|
||||||
@@ -144,13 +147,13 @@ export default function FormsPage({ settings, auth }: PlatformRouteContext) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="forms-page">
|
<main className="forms-page">
|
||||||
<div className="forms-shell">
|
<WorkspaceFrame className="forms-shell" label="Form definitions" interfaceId="forms.catalogue" helpContextId="forms.page.catalogue" helpModuleId="forms">
|
||||||
<ActionToolbar className="forms-toolbar">
|
<ActionToolbar surface="panel-header" className="forms-toolbar">
|
||||||
<form onSubmit={search} className="forms-search">
|
<FilterBar as="form" surface="control" wrap="never" width="default" 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" />
|
||||||
<Button type="submit">Search</Button>
|
<Button type="submit">Search</Button>
|
||||||
</form>
|
</FilterBar>
|
||||||
<label>
|
<label>
|
||||||
<span>State</span>
|
<span>State</span>
|
||||||
<select value={state} onChange={(event) => setState(event.target.value)}>
|
<select value={state} onChange={(event) => setState(event.target.value)}>
|
||||||
@@ -171,7 +174,7 @@ export default function FormsPage({ settings, auth }: PlatformRouteContext) {
|
|||||||
{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} />}
|
||||||
{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 && <StatePanel size="compact" description="No matching definitions." />}
|
||||||
{!loading && items.length > 0 &&
|
{!loading && items.length > 0 &&
|
||||||
<div className="forms-list" role="list">
|
<div className="forms-list" role="list">
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
@@ -192,7 +195,7 @@ export default function FormsPage({ settings, auth }: PlatformRouteContext) {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</PageScrollViewport>
|
</PageScrollViewport>
|
||||||
</div>
|
</WorkspaceFrame>
|
||||||
{editing &&
|
{editing &&
|
||||||
<FormDefinitionDialog
|
<FormDefinitionDialog
|
||||||
open
|
open
|
||||||
|
|||||||
@@ -1,39 +1,14 @@
|
|||||||
.forms-page,
|
.forms-page {
|
||||||
.forms-shell {
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.forms-shell {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background: var(--surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.forms-toolbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
min-height: 58px;
|
|
||||||
padding: 10px 18px;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
background: var(--surface-raised);
|
|
||||||
}
|
|
||||||
|
|
||||||
.forms-search {
|
.forms-search {
|
||||||
display: flex;
|
flex: 1 1 520px;
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
width: min(520px, 100%);
|
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.forms-search input {
|
|
||||||
min-width: 120px;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.forms-toolbar > label {
|
.forms-toolbar > label {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -122,12 +97,6 @@
|
|||||||
color: var(--text-soft);
|
color: var(--text-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.forms-empty {
|
|
||||||
padding: 36px 0;
|
|
||||||
color: var(--text-soft);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-definition-dialog {
|
.form-definition-dialog {
|
||||||
width: min(1120px, calc(100vw - 32px));
|
width: min(1120px, calc(100vw - 32px));
|
||||||
height: min(860px, calc(100vh - 32px));
|
height: min(860px, calc(100vh - 32px));
|
||||||
@@ -171,19 +140,6 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field-editor-heading {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-field-editor-heading h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.98rem;
|
|
||||||
letter-spacing: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-field-editor-list {
|
.form-field-editor-list {
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
@@ -313,15 +269,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.forms-toolbar {
|
|
||||||
align-items: stretch;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.forms-search {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.forms-row {
|
.forms-row {
|
||||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user