feat: expose workflow semantic documentation subjects

This commit is contained in:
2026-08-21 15:53:44 +02:00
parent f49ad8eff4
commit 0fd744a2db
10 changed files with 967 additions and 17 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@govoplan/workflow-webui",
"version": "0.1.18",
"version": "0.1.19",
"private": true,
"type": "module",
"main": "src/index.ts",
@@ -1,8 +1,9 @@
import { useEffect, useMemo, useState } from "react";
import { Trash2 } from "lucide-react";
import { BookOpen, Trash2 } from "lucide-react";
import {
ActionToolbar,
Button,
DocumentationHelpLink,
DismissibleAlert,
FormField,
ReferenceMultiSelect,
@@ -21,6 +22,8 @@ export default function WorkflowInspector({
edge,
nodeLibrary,
readOnly,
semanticDefinitionId,
semanticStepAvailable,
onChange,
onDelete,
onEdgeChange,
@@ -30,6 +33,8 @@ export default function WorkflowInspector({
edge: WorkflowGraphEdge | null;
nodeLibrary: WorkflowNodeType[];
readOnly: boolean;
semanticDefinitionId: string | null;
semanticStepAvailable: boolean;
onChange: (node: WorkflowGraphNode) => void;
onDelete: (nodeId: string) => void;
onEdgeChange: (edge: WorkflowGraphEdge) => void;
@@ -207,16 +212,35 @@ export default function WorkflowInspector({
<strong>Inspector</strong>
<small>{definition?.label ?? node.type}</small>
</span>
<Button
variant="ghost"
className="workflow-inspector-delete"
onClick={() => onDelete(node.id)}
disabled={readOnly}
aria-label="Delete node"
title="Delete node"
>
<Trash2 size={16} />
</Button>
<span className="workflow-inspector-actions">
{semanticDefinitionId && semanticStepAvailable ? <>
<DocumentationHelpLink
reference={{
contextId: semanticHelpContext(semanticDefinitionId, node.id),
documentationType: "user"
}}
label="Open step meaning"
/>
<a
className="btn btn-secondary"
href={semanticAuthoringHref(semanticDefinitionId, node.id)}
target="_blank"
rel="noreferrer"
>
<BookOpen size={15} aria-hidden="true" /> Document meaning
</a>
</> : null}
<Button
variant="ghost"
className="workflow-inspector-delete"
onClick={() => onDelete(node.id)}
disabled={readOnly}
aria-label="Delete node"
title="Delete node"
>
<Trash2 size={16} />
</Button>
</span>
</ActionToolbar>
<div className="workflow-inspector-fields">
{error ? (
@@ -332,3 +356,23 @@ function stringList(value: unknown): string[] {
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function semanticHelpContext(definitionId: string, nodeId: string): string {
return [
"semantic",
"workflow",
"workflow_definition",
definitionId,
`workflow-node-${nodeId}`
].join(".");
}
function semanticAuthoringHref(definitionId: string, nodeId: string): string {
const params = new URLSearchParams({
module: "workflow",
subjectKind: "workflow_definition",
subjectId: definitionId,
routeAnchor: `workflow-node-${nodeId}`
});
return `/docs/semantic?${params}`;
}
@@ -104,6 +104,7 @@ const iconByName: Record<string, LucideIcon> = {
};
export default function WorkflowNode({
id,
data,
selected,
isConnectable
@@ -114,6 +115,7 @@ export default function WorkflowNode({
const outputPorts = data.definition.output_ports;
return (
<div
id={`workflow-node-${id}`}
className={[
"workflow-node",
`workflow-node-${data.definition.category}`,
@@ -8,6 +8,7 @@ import {
} from "react";
import {
Archive,
BookOpen,
CheckCircle2,
CopyPlus,
Download,
@@ -708,6 +709,23 @@ export default function WorkflowPage({
aria-label="Workflow description"
/>
</span>}
helpAction={draft.id ? <>
<DocumentationHelpLink
reference={{
contextId: semanticHelpContext(draft.id),
documentationType: "user"
}}
label="Open workflow meaning"
/>
<a
className="btn btn-secondary"
href={semanticAuthoringHref(draft.id)}
target="_blank"
rel="noreferrer"
>
<BookOpen size={16} aria-hidden="true" /> Document meaning
</a>
</> : undefined}
primaryActions={<span className="workflow-command-bar">
{draft.id ? (
<select
@@ -927,6 +945,15 @@ export default function WorkflowPage({
edge={selectedEdge}
nodeLibrary={nodeLibrary}
readOnly={graphReadOnly}
semanticDefinitionId={draft.id || null}
semanticStepAvailable={Boolean(
draft.id
&& !historicalRevision
&& selectedNode
&& savedDraft?.graph.nodes.some(
(node) => node.id === selectedNode.id
)
)}
onChange={(node) => {
if (!draft || graphReadOnly) return;
updateGraph(updateWorkflowGraphNode(draft.graph, node));
@@ -1053,6 +1080,32 @@ export default function WorkflowPage({
);
}
function semanticHelpContext(
definitionId: string,
routeAnchor?: string
): string {
return [
"semantic",
"workflow",
"workflow_definition",
definitionId,
routeAnchor
].filter(Boolean).join(".");
}
function semanticAuthoringHref(
definitionId: string,
routeAnchor?: string
): string {
const params = new URLSearchParams({
module: "workflow",
subjectKind: "workflow_definition",
subjectId: definitionId
});
if (routeAnchor) params.set("routeAnchor", routeAnchor);
return `/docs/semantic?${params}`;
}
function WorkflowStandardComparisonDialog({
open,
settings,
+12
View File
@@ -451,6 +451,18 @@
color: var(--danger-text);
}
.workflow-inspector-actions {
display: inline-flex;
min-width: 0;
align-items: center;
justify-content: flex-end;
gap: 5px;
}
.workflow-inspector-actions .btn {
white-space: nowrap;
}
.workflow-diagnostics {
display: flex;
max-height: 38%;