Adopt the shared interface pattern language

This commit is contained in:
2026-08-04 08:21:50 +02:00
parent 06f93f4f7a
commit 629f76440b
7 changed files with 102 additions and 1 deletions
+3
View File
@@ -23,10 +23,13 @@ ownership boundary.
See [the module concept](docs/CONCEPT.md) and See [the module concept](docs/CONCEPT.md) and
[BPMN interoperability contract](docs/BPMN_INTEROPERABILITY.md) for the shared [BPMN interoperability contract](docs/BPMN_INTEROPERABILITY.md) for the shared
model retained by Workflow Engine. model retained by Workflow Engine.
The editor route, graph, decision, state, and accessibility mapping is recorded
in [the interface pattern audit](docs/INTERFACE_PATTERN_MIGRATION.md).
## Checks ## Checks
```bash ```bash
/mnt/DATA/git/govoplan/.venv/bin/python -m unittest discover -s tests /mnt/DATA/git/govoplan/.venv/bin/python -m unittest discover -s tests
cd webui && npm run typecheck cd webui && npm run typecheck
cd webui && npm run test:interface-pattern
``` ```
+30
View File
@@ -0,0 +1,30 @@
# Workflow Interface Pattern Migration
Workflow is the optional visual authoring and inspection surface for the
headless Workflow Engine. It composes native BPMN graph editing, immutable
revision inspection, governed activation, and instance evidence without owning
runtime persistence or importing module-private action implementations.
| Surface | Task and archetype | Consequence and state contract |
| --- | --- | --- |
| `/workflow` definition catalogue | List-detail workspace | Search, loading, empty, selected, current/historical revision, baseline, override, and update-available states retain definition context. |
| Native BPMN graph | Specialized create/edit workspace | Palette nodes can be dragged or added with keyboard activation. Nodes and edges can be selected, edited, reconnected, or removed through explicit controls; drag is not the only path. |
| Definition settings and derivation | Governed create/edit dialogs | Scope, kind, execution mode, inherited visibility, reuse, automation, and immutable View revision expose field help and policy provenance. |
| Validate/save/activate/archive/delete/reset | Review and consequential actions | Validation diagnostics identify graph elements. Save creates a revision; activation changes the runnable revision; archive/delete/reset use explicit state gates and Core confirmation dialogs. |
| Runs dialog and open-work widget | Monitoring, progress, and human decision | Instance/step status, handoff instructions, evidence references, transitions, retries, cancellation, reconciliation, failures, and partial outcomes remain durable Workflow Engine evidence. |
Core owns buttons, icon buttons, dialog/focus behavior, alerts, status, form
help, toggles, selectors, unsaved-navigation protection, and documentation help.
The graph itself is the authorized domain-specific editor. Module standards and
optional Views/Policy integrations are consumed through public capabilities.
Responsive layouts move catalogue, palette, graph, and inspector into task
order; reduced-motion preferences disable editor animation as a source of
meaning.
Verification:
- `npm run typecheck`
- `npm run test:interface-pattern`
- Workflow editor and Workflow Engine backend suites
- manifest shape, optional-module permutations, structural localization, theme,
and full-product bundle-budget checks
@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
from govoplan_core.core.modules import ( from govoplan_core.core.modules import (
DocumentationLink,
DocumentationTopic, DocumentationTopic,
FrontendModule, FrontendModule,
FrontendRoute, FrontendRoute,
@@ -109,6 +110,13 @@ manifest = ModuleManifest(
documentation_types=("admin", "user"), documentation_types=("admin", "user"),
audience=("operator", "module_admin", "power_user", "product_owner"), audience=("operator", "module_admin", "power_user", "product_owner"),
related_modules=("workflow_engine", "views", "policy", "audit"), related_modules=("workflow_engine", "views", "policy", "audit"),
links=(
DocumentationLink(
label="Workflow interface pattern audit",
href="govoplan-workflow/docs/INTERFACE_PATTERN_MIGRATION.md",
kind="repository",
),
),
order=76, order=76,
), ),
), ),
+2 -1
View File
@@ -13,7 +13,8 @@
} }
}, },
"scripts": { "scripts": {
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit",
"test:interface-pattern": "node scripts/test-interface-pattern.mjs"
}, },
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.14", "@govoplan/core-webui": "^0.1.14",
+21
View File
@@ -0,0 +1,21 @@
import assert from "node:assert/strict";
import fs from "node:fs";
const page = fs.readFileSync("src/features/workflow/WorkflowPage.tsx", "utf8");
const runs = fs.readFileSync("src/features/workflow/WorkflowRunsDialog.tsx", "utf8");
const inspector = fs.readFileSync("src/features/workflow/WorkflowInspector.tsx", "utf8");
const styles = fs.readFileSync("src/styles/workflow.css", "utf8");
assert.ok(page.includes("DocumentationHelpLink"), "Workflow exposes configured-system help");
assert.ok(page.includes("useUnsavedDraftGuard"), "Workflow protects dirty graph revisions during navigation");
assert.ok(page.includes("<ConfirmDialog"), "Destructive and corrective definition actions use shared confirmation");
assert.ok(page.includes("<Dialog"), "Definition settings and derivation use shared focus-contained dialogs");
assert.ok(page.includes("onClick={() => addNodeFromPalette(nodeType.type)}"), "Palette nodes have a keyboard/pointer alternative to drag and drop");
assert.ok(inspector.includes("onEdgeChange"), "Edges can be edited without reconnect dragging");
assert.ok(runs.includes("StatusBadge"), "Run and handoff states are not conveyed by color alone");
assert.ok(runs.includes("DismissibleAlert"), "Run failures use the shared alert contract");
assert.ok(!page.includes("window.alert("), "Workflow must not use browser alerts");
assert.ok(styles.includes("@media (max-width: 680px)"), "Workflow retains a narrow-viewport layout");
assert.ok(styles.includes("@media (prefers-reduced-motion: reduce)"), "Workflow honors reduced motion");
console.log("Workflow interface pattern contract passed.");
@@ -27,6 +27,7 @@ import {
Button, Button,
ConfirmDialog, ConfirmDialog,
Dialog, Dialog,
DocumentationHelpLink,
DismissibleAlert, DismissibleAlert,
FormField, FormField,
IconButton, IconButton,
@@ -74,6 +75,7 @@ import WorkflowRunsDialog from "./WorkflowRunsDialog";
import { import {
FALLBACK_WORKFLOW_LIBRARY, FALLBACK_WORKFLOW_LIBRARY,
draftFromDefinition, draftFromDefinition,
newWorkflowNode,
sampleWorkflowDraft, sampleWorkflowDraft,
workflowFingerprint, workflowFingerprint,
workflowPayload, workflowPayload,
@@ -564,6 +566,21 @@ export default function WorkflowPage({
setSelectedEdgeId(null); setSelectedEdgeId(null);
}; };
const addNodeFromPalette = (nodeType: string) => {
if (!draft || graphReadOnly) return;
const index = draft.graph.nodes.length;
const node = newWorkflowNode(
nodeType,
{
x: 80 + (index % 4) * 220,
y: 80 + Math.floor(index / 4) * 150
},
nodeLibrary
);
updateGraph({ ...draft.graph, nodes: [...draft.graph.nodes, node] });
setSelectedNodeId(node.id);
};
return ( return (
<main className="workflow-page"> <main className="workflow-page">
<div className="workflow-shell"> <div className="workflow-shell">
@@ -571,6 +588,10 @@ export default function WorkflowPage({
<div className="workflow-panel-toolbar"> <div className="workflow-panel-toolbar">
<strong>Workflows</strong> <strong>Workflows</strong>
<span className="workflow-toolbar-actions"> <span className="workflow-toolbar-actions">
<DocumentationHelpLink
reference={{ topicId: "workflow.editor", documentationType: "user" }}
label="Open Workflow documentation"
/>
<IconButton <IconButton
label="Refresh" label="Refresh"
icon={<RefreshCw size={16} />} icon={<RefreshCw size={16} />}
@@ -593,6 +614,7 @@ export default function WorkflowPage({
variant="primary" variant="primary"
onClick={createNew} onClick={createNew}
disabled={!canWrite} disabled={!canWrite}
disabledReason={!canWrite ? "Workflow definition write permission is required." : undefined}
/> />
</span> </span>
</div> </div>
@@ -870,6 +892,7 @@ export default function WorkflowPage({
onDragStart={(event) => onDragStart={(event) =>
startPaletteDrag(event, nodeType.type) startPaletteDrag(event, nodeType.type)
} }
onClick={() => addNodeFromPalette(nodeType.type)}
title={nodeType.description} title={nodeType.description}
> >
<GitFork size={16} /> <GitFork size={16} />
@@ -1195,6 +1218,7 @@ function WorkflowDefinitionSettingsDialog({
<FormField <FormField
label="Scope" label="Scope"
help="The scope determines ownership, visibility, and the Policy inheritance path." help="The scope determines ownership, visibility, and the Policy inheritance path."
documentation={{ topicId: "workflow.editor", documentationType: "admin" }}
> >
<select <select
value={draft.scopeType} value={draft.scopeType}
@@ -1241,6 +1265,7 @@ function WorkflowDefinitionSettingsDialog({
<FormField <FormField
label="Definition kind" label="Definition kind"
help="Templates can be reused or derived, but cannot be activated or started." help="Templates can be reused or derived, but cannot be activated or started."
documentation={{ topicId: "workflow.editor", documentationType: "user" }}
> >
<select <select
value={draft.definitionKind} value={draft.definitionKind}
@@ -1257,6 +1282,7 @@ function WorkflowDefinitionSettingsDialog({
<FormField <FormField
label="Execution mode" label="Execution mode"
help="Guided flows require a user; automated flows cannot contain human handoffs; hybrid flows can combine both." help="Guided flows require a user; automated flows cannot contain human handoffs; hybrid flows can combine both."
documentation={{ topicId: "workflow.editor", documentationType: "user" }}
> >
<select <select
value={draft.executionMode} value={draft.executionMode}
@@ -1278,6 +1304,7 @@ function WorkflowDefinitionSettingsDialog({
<FormField <FormField
label="Workflow View" label="Workflow View"
help="The selected immutable View revision is applied for active runs. Individual steps may narrow it further." help="The selected immutable View revision is applied for active runs. Individual steps may narrow it further."
documentation={{ topicId: "workflow.editor", documentationType: "user" }}
> >
<select <select
value={draft.viewId} value={draft.viewId}
+11
View File
@@ -1226,3 +1226,14 @@
grid-column: 1 / -1; grid-column: 1 / -1;
} }
} }
@media (prefers-reduced-motion: reduce) {
.workflow-page *,
.workflow-page *::before,
.workflow-page *::after {
scroll-behavior: auto !important;
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
}
}