Adopt the shared interface pattern language

This commit is contained in:
2026-08-04 08:21:50 +02:00
parent 2f53dc44dc
commit 4e7e972415
6 changed files with 84 additions and 13 deletions
+29 -13
View File
@@ -16,7 +16,9 @@ import {
import {
Button,
Dialog,
DocumentationHelpLink,
DismissibleAlert,
FieldLabel,
IconButton,
LoadingIndicator,
PageScrollViewport,
@@ -69,6 +71,7 @@ export default function ProjectsPage({ settings, auth }: PlatformRouteContext) {
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [editorError, setEditorError] = useState("");
const [editorOpen, setEditorOpen] = useState(false);
const [editing, setEditing] = useState<ProjectRecord | null>(null);
const [saving, setSaving] = useState(false);
@@ -123,12 +126,14 @@ export default function ProjectsPage({ settings, auth }: PlatformRouteContext) {
function openCreate() {
setEditing(null);
setEditorError("");
setEditorOpen(true);
}
function openEdit() {
if (!selected) return;
setEditing(selected);
setEditorError("");
setEditorOpen(true);
}
@@ -188,10 +193,11 @@ export default function ProjectsPage({ settings, auth }: PlatformRouteContext) {
}
setEditorOpen(false);
setEditing(null);
setEditorError("");
await reload();
setSelectedKey(objectKey(saved));
} catch (reason) {
setError(reason instanceof Error ? reason.message : "The project object could not be saved.");
setEditorError(reason instanceof Error ? reason.message : "The project object could not be saved.");
} finally {
setSaving(false);
}
@@ -221,6 +227,10 @@ export default function ProjectsPage({ settings, auth }: PlatformRouteContext) {
</select>
</label>
<span className="projects-count">{total} objects</span>
<DocumentationHelpLink
reference={{ topicId: "projects.module-boundary", documentationType: "user" }}
label="Open Projects documentation"
/>
{canWrite &&
<Button type="button" variant="primary" onClick={openCreate}>
<Plus size={16} aria-hidden="true" /> New
@@ -270,7 +280,11 @@ export default function ProjectsPage({ settings, auth }: PlatformRouteContext) {
record={editing}
objects={parentOptions}
saving={saving}
onClose={() => setEditorOpen(false)}
error={editorError}
onClose={() => {
setEditorOpen(false);
setEditorError("");
}}
onSave={save}
/>
</main>
@@ -337,11 +351,12 @@ function PlanningStat({ label, value }: { label: string; value: number }) {
return <div><span>{label}</span><strong>{value}</strong></div>;
}
function ProjectEditorDialog({ open, record, objects, saving, onClose, onSave }: {
function ProjectEditorDialog({ open, record, objects, saving, error, onClose, onSave }: {
open: boolean;
record: ProjectRecord | null;
objects: ProjectRecord[];
saving: boolean;
error: string;
onClose: () => void;
onSave: (values: EditorValues) => Promise<void>;
}) {
@@ -381,9 +396,10 @@ function ProjectEditorDialog({ open, record, objects, saving, onClose, onSave }:
</Button>
</>
}>
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
<form id="project-editor-form" className="project-editor-form" onSubmit={submit}>
<label>
<span>Type</span>
<FieldLabel>Type</FieldLabel>
<select
value={values.kind}
disabled={Boolean(record)}
@@ -398,7 +414,7 @@ function ProjectEditorDialog({ open, record, objects, saving, onClose, onSave }:
</label>
{values.kind !== "portfolio" &&
<label className="project-editor-wide">
<span>{values.kind === "milestone" ? "Parent project" : "Portfolio"}</span>
<FieldLabel>{values.kind === "milestone" ? "Parent project" : "Portfolio"}</FieldLabel>
<select
value={values.parentRef}
required={values.kind === "milestone"}
@@ -418,40 +434,40 @@ function ProjectEditorDialog({ open, record, objects, saving, onClose, onSave }:
</label>
}
<label>
<span>Key</span>
<FieldLabel help="Stable identifier used in links and external mappings.">Key</FieldLabel>
<input value={values.key} disabled={Boolean(record)} required maxLength={120} onChange={(event) => set("key", event.target.value)} />
</label>
<label className="project-editor-wide">
<span>Title</span>
<FieldLabel>Title</FieldLabel>
<input value={values.title} required maxLength={500} onChange={(event) => set("title", event.target.value)} />
</label>
<label>
<span>State</span>
<FieldLabel>State</FieldLabel>
<select value={values.state} onChange={(event) => set("state", event.target.value)}>
{STATES[values.kind].map((state) => <option key={state} value={state}>{humanize(state)}</option>)}
</select>
</label>
<label>
<span>Visibility</span>
<FieldLabel help="Restricted objects require an owner or explicit membership grant.">Visibility</FieldLabel>
<select value={values.visibility} onChange={(event) => set("visibility", event.target.value as EditorValues["visibility"])}>
<option value="tenant">Tenant</option>
<option value="restricted">Restricted</option>
</select>
</label>
<label>
<span>Starts</span>
<FieldLabel>Starts</FieldLabel>
<input type="date" value={values.startsAt} onChange={(event) => set("startsAt", event.target.value)} />
</label>
<label>
<span>Due</span>
<FieldLabel>Due</FieldLabel>
<input type="date" value={values.dueAt} onChange={(event) => set("dueAt", event.target.value)} />
</label>
<label className="project-editor-wide">
<span>Description</span>
<FieldLabel>Description</FieldLabel>
<textarea rows={5} value={values.description} onChange={(event) => set("description", event.target.value)} />
</label>
<label className="project-editor-wide">
<span>Change reason</span>
<FieldLabel help="Recorded with the immutable project revision and lifecycle evidence.">Change reason</FieldLabel>
<input value={values.changeReason} required maxLength={1000} onChange={(event) => set("changeReason", event.target.value)} />
</label>
</form>