From 4e7e9724151343be1fe3bc4cdc3b89ec8f24d9b4 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Tue, 4 Aug 2026 08:21:50 +0200 Subject: [PATCH] Adopt the shared interface pattern language --- README.md | 2 + docs/INTERFACE_PATTERN_MIGRATION.md | 27 +++++++++++++ src/govoplan_projects/backend/manifest.py | 5 +++ webui/package.json | 3 ++ webui/scripts/test-interface-pattern.mjs | 18 +++++++++ webui/src/features/projects/ProjectsPage.tsx | 42 ++++++++++++++------ 6 files changed, 84 insertions(+), 13 deletions(-) create mode 100644 docs/INTERFACE_PATTERN_MIGRATION.md create mode 100644 webui/scripts/test-interface-pattern.mjs diff --git a/README.md b/README.md index c84f8fa..36fbb23 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ Connectors; Projects stores only the native planning object and canonical external reference. See [docs/PROJECTS_DOMAIN_BOUNDARY.md](docs/PROJECTS_DOMAIN_BOUNDARY.md). +The list-detail, editor, state, consequence, and accessibility mapping is in +[docs/INTERFACE_PATTERN_MIGRATION.md](docs/INTERFACE_PATTERN_MIGRATION.md). ## Development diff --git a/docs/INTERFACE_PATTERN_MIGRATION.md b/docs/INTERFACE_PATTERN_MIGRATION.md new file mode 100644 index 0000000..c4ebaec --- /dev/null +++ b/docs/INTERFACE_PATTERN_MIGRATION.md @@ -0,0 +1,27 @@ +# Projects Interface Pattern Migration + +Projects uses a full-height list-detail workspace. It owns native portfolio, +project, and milestone context; Tasks and Tickets own actionable work, +Connectors owns OpenProject transport, and optional integrations remain +capability-based. + +| Surface | Task and archetype | Consequence and state contract | +| --- | --- | --- | +| `/projects` catalogue | Search/filter and select a planning object | Loading, empty, failed, filtered, and selected states retain the list context. Restricted records are removed by backend authorization rather than cosmetically hidden. | +| Object detail | Inspect status, dates, ownership/membership evidence, outcomes, benefits, dependencies, and links | The selected identity, key, state, and revision remain visible while detail changes. | +| Create/edit dialog | Adaptive create/edit | Core Dialog supplies focus containment and return. Stable key, state, visibility, parent, dates, and change reason have labelled controls; save errors remain attached to the dialog. | +| Revisioned save | Consequential corrective action | Every successful save creates immutable revision and lifecycle evidence under optimistic concurrency. The required change reason explains the new record. | + +Creating and editing require the Projects write permission. Restricted +visibility changes who may read an object, so its consequence is explained at +the field and enforced by the backend ACL. The workspace reflows to list then +detail at narrow widths, preserves semantic button/list behavior, and uses Core +buttons, icon buttons, dialog, alerts, loading, status, field labels, scrolling, +and documentation help. + +Verification: + +- `npm run test:interface-pattern` +- Projects service, migration, and manifest tests +- the Core TypeScript graph, structural localization audit, theme check, module + permutations, and full-product bundle budget diff --git a/src/govoplan_projects/backend/manifest.py b/src/govoplan_projects/backend/manifest.py index b4e2829..881d3cc 100644 --- a/src/govoplan_projects/backend/manifest.py +++ b/src/govoplan_projects/backend/manifest.py @@ -222,6 +222,11 @@ DOCUMENTATION = ( href="govoplan-projects/docs/PROJECTS_DOMAIN_BOUNDARY.md", kind="repository", ), + DocumentationLink( + label="Projects interface pattern audit", + href="govoplan-projects/docs/INTERFACE_PATTERN_MIGRATION.md", + kind="repository", + ), ), metadata={ "seed": True, diff --git a/webui/package.json b/webui/package.json index 9b96c13..b154173 100644 --- a/webui/package.json +++ b/webui/package.json @@ -13,6 +13,9 @@ }, "./styles/projects.css": "./src/styles/projects.css" }, + "scripts": { + "test:interface-pattern": "node scripts/test-interface-pattern.mjs" + }, "peerDependencies": { "@govoplan/core-webui": "^0.1.14", "lucide-react": "^1.23.0", diff --git a/webui/scripts/test-interface-pattern.mjs b/webui/scripts/test-interface-pattern.mjs new file mode 100644 index 0000000..dd059bd --- /dev/null +++ b/webui/scripts/test-interface-pattern.mjs @@ -0,0 +1,18 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; + +const page = fs.readFileSync("src/features/projects/ProjectsPage.tsx", "utf8"); +const styles = fs.readFileSync("src/styles/projects.css", "utf8"); + +assert.ok(page.includes("DocumentationHelpLink"), "Projects exposes configured-system help"); +assert.ok(page.includes("PageScrollViewport"), "Projects owns bounded list and detail scrolling"); +assert.ok(page.includes("]*\bonClick\s*=/.test(page), "Projects uses semantic interactive elements"); +assert.ok(styles.includes("@media (max-width: 560px)"), "Projects retains a narrow-viewport editor layout"); +assert.ok(styles.includes(":focus-visible"), "Projects retains visible keyboard focus"); + +console.log("Projects interface pattern contract passed."); diff --git a/webui/src/features/projects/ProjectsPage.tsx b/webui/src/features/projects/ProjectsPage.tsx index c26e851..8f93280 100644 --- a/webui/src/features/projects/ProjectsPage.tsx +++ b/webui/src/features/projects/ProjectsPage.tsx @@ -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(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) { {total} objects + {canWrite && }> + {error && {error}}