From c125f332baf229d8a9646235261e1ec59d9b2d8e Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Mon, 3 Aug 2026 11:57:44 +0200 Subject: [PATCH] Migrate Views interface patterns --- README.md | 4 + docs/INTERFACE_PATTERN_MIGRATION.md | 48 ++ src/govoplan_views/backend/manifest.py | 77 +++ .../test_interface_documentation_contract.py | 42 ++ webui/package.json | 3 + .../test-interface-pattern-language.mjs | 29 + webui/src/components/ViewSelector.tsx | 26 +- .../src/features/views/PersonalViewsPanel.tsx | 41 +- webui/src/features/views/ViewsAdminPanel.tsx | 593 +++++++++++++----- webui/src/features/views/interfacePatterns.ts | 63 ++ webui/src/i18n/generatedTranslations.ts | 264 ++++++++ webui/src/module.ts | 21 +- 12 files changed, 1013 insertions(+), 198 deletions(-) create mode 100644 docs/INTERFACE_PATTERN_MIGRATION.md create mode 100644 tests/test_interface_documentation_contract.py create mode 100644 webui/scripts/test-interface-pattern-language.mjs create mode 100644 webui/src/features/views/interfacePatterns.ts create mode 100644 webui/src/i18n/generatedTranslations.ts diff --git a/README.md b/README.md index 2eb6c77..79a50bf 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,10 @@ without a reachable navigation entry and route, Core shows the normal authorized interface and reports the stale projection instead of trapping the user. +The surface inventory, consequence classes, availability explanations, and +accessibility evidence for the shared interface language are recorded in +[`docs/INTERFACE_PATTERN_MIGRATION.md`](docs/INTERFACE_PATTERN_MIGRATION.md). + ## Development Install the module as an editable package alongside Core and include `views` in diff --git a/docs/INTERFACE_PATTERN_MIGRATION.md b/docs/INTERFACE_PATTERN_MIGRATION.md new file mode 100644 index 0000000..ee0aa02 --- /dev/null +++ b/docs/INTERFACE_PATTERN_MIGRATION.md @@ -0,0 +1,48 @@ +# Views Interface Pattern Migration + +This document records the bounded migration of Views-owned WebUI surfaces to +the GovOPlaN interface pattern language. Core owns shared controls, surface +projection, and the host shells. Views owns definitions, revisions, +assignments, effective selection, and the safeguards described here. + +## Surface Inventory + +| Surface | Archetype | Consequence class | Contract | +| --- | --- | --- | --- | +| `views.selector` | Compact global selector | Change active presentation projection | Localized accessible selector, required-state explanation, unsaved-change guard | +| `views.admin.system` | Administration list-detail editor | Create, publish, archive, assign | Shared admin layout, explicit disabled reasons, required-View lockout validation, contextual help | +| `views.admin.tenant` | Administration list-detail editor | Create, publish, archive, assign | Inheritance provenance, searchable group/user targets, explicit optional-module blocker | +| `views.settings.personal` | Settings list-detail editor | Create and publish owned Views | Shared owner selector, permission blocker, contextual field help | + +## Consequence And Availability Rules + +- Views filter presentation only. Authorization remains owned by Access and + each protected backend route. +- Published revisions are immutable. Unpinned assignments follow later + publications; pinned assignments retain one published revision. +- Required Views keep the selector and the applicable administration escape + surface. The backend validates this before accepting the assignment. +- User and group assignments take precedence over tenant and system + assignments. Inherited definitions and assignments are changed at their + owning scope, not copied implicitly. +- The optional Access directory supplies searchable group and user targets. + If it is absent, the UI explains the missing capability and its remedy while + system and tenant targets remain usable. +- Definition, creation, and assignment drafts use Core's shared unsaved-change + guard. Archive and assignment removal use shared consequential-action + confirmation. + +## State And Accessibility Evidence + +The surfaces use Core loading, error, success, empty, disabled-action, blocker, +dialog, status, selector, and tree controls. Consequential actions stay visible +and explain missing permission, inherited ownership, lifecycle state, missing +catalogue data, or lockout validation. Labels and accessible properties are +provided through the module translation catalogue. Shared dialogs own focus +containment and restoration, while fixed-height panes retain responsive +scrolling. + +Stable help references are contributed through the manifest for the selector, +administration sections, settings panel, fields, assignment precedence, and +consequential actions. Backend and WebUI contract tests prevent those +references and safeguards from silently regressing. diff --git a/src/govoplan_views/backend/manifest.py b/src/govoplan_views/backend/manifest.py index a72e6a2..a7deaf4 100644 --- a/src/govoplan_views/backend/manifest.py +++ b/src/govoplan_views/backend/manifest.py @@ -11,6 +11,7 @@ from govoplan_core.core.module_guards import ( persistent_table_uninstall_guard, ) from govoplan_core.core.modules import ( + DocumentationLink, DocumentationTopic, FrontendModule, MigrationSpec, @@ -309,8 +310,84 @@ manifest = ModuleManifest( documentation_types=("admin", "user"), audience=("administrator", "power_user", "workflow_designer"), related_modules=("access", "admin", "policy", "workflow_engine"), + links=( + DocumentationLink( + label="Views administration", + href="/admin?section=system-views", + kind="runtime", + ), + DocumentationLink( + label="Views API", + href="/api/v1/views/definitions", + kind="api", + ), + ), + metadata={ + "kind": "guide", + "help_contexts": [ + "views.selector", + "views.admin.system", + "views.admin.tenant", + "views.settings.personal", + "views.admin.blocked", + ], + }, order=18, ), + DocumentationTopic( + id="views.reference.fields-and-consequences", + title="View fields, assignments, and consequences", + summary=( + "Understand immutable revisions, assignment precedence, required " + "View safeguards, and the difference between visibility and access." + ), + body=( + "A View definition owns immutable revisions of visible surface IDs. " + "Publishing makes the latest revision assignable. Available assignments " + "let users opt in, defaults apply until changed, and required assignments " + "cannot be left. User and group assignments take precedence over tenant " + "and system assignments. Pinning preserves one published revision; an " + "unpinned assignment follows later publications. Required Views must keep " + "the selector and administration escape surfaces. Hiding a surface never " + "grants or revokes authorization, and inherited definitions or assignments " + "must be changed in their owning scope." + ), + documentation_types=("admin",), + audience=("administrator", "power_user", "workflow_designer"), + related_modules=("access", "admin", "policy", "workflow_engine"), + links=( + DocumentationLink( + label="Views administration", + href="/admin?section=system-views", + kind="runtime", + ), + DocumentationLink( + label="View assignments API", + href="/api/v1/views/assignments", + kind="api", + ), + ), + metadata={ + "kind": "reference", + "help_contexts": [ + "views.field.name", + "views.field.description", + "views.field.surfaces", + "views.field.assignment-target", + "views.field.assignment-mode", + "views.field.assignment-priority", + "views.action.publish", + "views.action.archive", + ], + "consequence_classes": { + "publish": "Creates the assignable immutable revision used by unpinned assignments.", + "required": "Constrains affected users while retaining selector and administration escape surfaces.", + "archive": "Deactivates optional assignments; required assignments must be removed first.", + "remove_assignment": "Stops the target from inheriting this assignment without deleting the View.", + }, + }, + order=19, + ), ), architecture=declared_module_architecture( layer="governance_accountability", diff --git a/tests/test_interface_documentation_contract.py b/tests/test_interface_documentation_contract.py new file mode 100644 index 0000000..58de975 --- /dev/null +++ b/tests/test_interface_documentation_contract.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import unittest + +from govoplan_views.backend.manifest import manifest + + +class ViewsInterfaceDocumentationContractTests(unittest.TestCase): + def test_views_surfaces_remain_declared(self) -> None: + frontend = manifest.frontend + self.assertIsNotNone(frontend) + surfaces = {surface.id for surface in frontend.view_surfaces} # type: ignore[union-attr] + self.assertEqual( + { + "views.selector", + "views.admin.system", + "views.admin.tenant", + "views.settings.personal", + }, + surfaces, + ) + + def test_views_topics_publish_stable_help_contexts(self) -> None: + topics = {topic.id: topic for topic in manifest.documentation} + self.assertIn("views.interface-projections", topics) + self.assertIn("views.reference.fields-and-consequences", topics) + + projection_contexts = set( + topics["views.interface-projections"].metadata["help_contexts"] + ) + self.assertIn("views.selector", projection_contexts) + self.assertIn("views.admin.system", projection_contexts) + self.assertIn("views.admin.tenant", projection_contexts) + self.assertIn("views.settings.personal", projection_contexts) + + reference = topics["views.reference.fields-and-consequences"] + self.assertIn("views.action.publish", reference.metadata["help_contexts"]) + self.assertIn("required", reference.metadata["consequence_classes"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/webui/package.json b/webui/package.json index df74075..9a321ef 100644 --- a/webui/package.json +++ b/webui/package.json @@ -6,6 +6,9 @@ "main": "src/index.ts", "module": "src/index.ts", "types": "src/index.ts", + "scripts": { + "test:interface-patterns": "node scripts/test-interface-pattern-language.mjs" + }, "exports": { ".": { "types": "./src/index.ts", diff --git a/webui/scripts/test-interface-pattern-language.mjs b/webui/scripts/test-interface-pattern-language.mjs new file mode 100644 index 0000000..0d9197a --- /dev/null +++ b/webui/scripts/test-interface-pattern-language.mjs @@ -0,0 +1,29 @@ +import { readFileSync } from "node:fs"; + +function source(path) { + return readFileSync(new URL(path, import.meta.url), "utf8"); +} + +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +const admin = source("../src/features/views/ViewsAdminPanel.tsx"); +const personal = source("../src/features/views/PersonalViewsPanel.tsx"); +const selector = source("../src/components/ViewSelector.tsx"); +const patterns = source("../src/features/views/interfacePatterns.ts"); +const moduleSource = source("../src/module.ts"); +const translations = source("../src/i18n/generatedTranslations.ts"); + +assert(admin.includes("DocumentationHelpLink") && personal.includes("VIEWS_FIELD_DOCUMENTATION"), "Views administration and personal settings expose contextual documentation"); +assert(admin.includes("ActionBlockerHint") && personal.includes("ActionBlockerHint"), "Read-only and optional-capability states identify the required remedy"); +assert(admin.includes("disabledReason"), "Unavailable Views actions explain their state"); +assert(admin.includes("createDirty ? requestDiscard(closeCreate)") && admin.includes("assignmentDirty ? requestDiscard(closeAssignment)"), "Definition and assignment dialogs use the shared unsaved-change guard"); +assert(admin.includes("ConfirmDialog") && admin.includes("archiveTarget") && admin.includes("deleteAssignmentTarget"), "Archive and assignment removal remain explicitly confirmed"); +assert(admin.includes("LOCKOUT_SURFACES") && admin.includes("requiredMissing"), "Required Views retain the selector and administration escape paths"); +assert(patterns.includes('topicId: "views.interface-projections"') && patterns.includes('topicId: "views.reference.fields-and-consequences"'), "Views uses stable manifest-backed help references"); +assert(moduleSource.includes("generatedTranslations") && moduleSource.includes('label: "i18n:govoplan-views.views"'), "Module and surface labels are localized"); +assert(selector.includes("usePlatformLanguage") && selector.includes("required_by_administrator"), "The global selector localizes and explains its locked state"); +assert(translations.includes('"i18n:govoplan-views.views_administration_is_read_only"'), "Availability explanations are present in the translation catalogue"); + +console.log("Views surfaces satisfy the recorded interface pattern-language contract."); diff --git a/webui/src/components/ViewSelector.tsx b/webui/src/components/ViewSelector.tsx index b87fbd7..1416eee 100644 --- a/webui/src/components/ViewSelector.tsx +++ b/webui/src/components/ViewSelector.tsx @@ -2,6 +2,7 @@ import { Eye, LockKeyhole } from "lucide-react"; import { useState } from "react"; import { dispatchPlatformViewChanged, + usePlatformLanguage, useUnsavedChanges, type ViewSelectorProps } from "@govoplan/core-webui"; @@ -14,6 +15,7 @@ export default function ViewSelector({ }: ViewSelectorProps) { const [busy, setBusy] = useState(false); const [error, setError] = useState(""); + const { translateText } = usePlatformLanguage(); const { requestNavigation } = useUnsavedChanges(); const options = projection?.availableViews ?? []; @@ -26,7 +28,11 @@ export default function ViewSelector({ await selectEffectiveView(settings, viewId || null); dispatchPlatformViewChanged(); } catch (caught) { - setError(caught instanceof Error ? caught.message : "View selection failed"); + setError( + caught instanceof Error + ? caught.message + : "i18n:govoplan-views.view_selection_failed" + ); } finally { setBusy(false); } @@ -41,26 +47,30 @@ export default function ViewSelector({ const locked = Boolean(projection?.locked); const diagnostic = projection?.diagnostics.find((item) => item.severity === "error") ?? projection?.diagnostics[0]; - const title = error || diagnostic?.message || ( + const title = translateText(error || diagnostic?.message || ( locked - ? "This View is required by an administrator." - : "Choose which parts of GovOPlaN are shown." - ); + ? "i18n:govoplan-views.required_by_administrator" + : "i18n:govoplan-views.selector_help" + )); return (