Add workflow standard comparison controls

This commit is contained in:
2026-07-31 19:40:08 +02:00
parent 8fd8753012
commit 9d987c7339
3 changed files with 168 additions and 90 deletions
+78 -61
View File
@@ -45,11 +45,11 @@ import {
import {
activateWorkflowDefinition,
archiveWorkflowDefinition,
compareWorkflowDefinitionToStandard,
compileWorkflowBpmn,
createWorkflowDefinition,
deleteWorkflowDefinition,
deriveWorkflowDefinition,
getWorkflowRevision,
listWorkflowDefinitions,
listWorkflowNodeTypes,
listWorkflowRevisions,
@@ -63,7 +63,8 @@ import {
type WorkflowDiagnostic,
type WorkflowGraphEdge,
type WorkflowNodeType,
type WorkflowRevision
type WorkflowRevision,
type WorkflowStandardDiff
} from "../../api/workflow";
import WorkflowCanvas, {
updateWorkflowGraphNode
@@ -151,10 +152,6 @@ export default function WorkflowPage({
() => definitions.find((item) => item.id === draft?.id) ?? null,
[definitions, draft?.id]
);
const baselineDefinition = useMemo(() => {
const baselineId = selectedDefinition?.standard?.baseline_definition_id;
return definitions.find((item) => item.id === baselineId) ?? null;
}, [definitions, selectedDefinition]);
const dirty = Boolean(draft)
&& workflowFingerprint(draft) !== workflowFingerprint(savedDraft);
const displayedGraph = historicalRevision?.graph ?? draft?.graph ?? null;
@@ -987,7 +984,6 @@ export default function WorkflowPage({
open={compareOpen}
settings={settings}
override={selectedDefinition}
baseline={baselineDefinition}
onClose={() => setCompareOpen(false)}
/>
<WorkflowDefinitionSettingsDialog
@@ -1041,45 +1037,31 @@ function WorkflowStandardComparisonDialog({
open,
settings,
override,
baseline,
onClose
}: {
open: boolean;
settings: ApiSettings;
override: WorkflowDefinition | null;
baseline: WorkflowDefinition | null;
onClose: () => void;
}) {
const [pinned, setPinned] = useState<WorkflowRevision | null>(null);
const [latest, setLatest] = useState<WorkflowRevision | null>(null);
const [comparison, setComparison] = useState<WorkflowStandardDiff | null>(null);
const [error, setError] = useState("");
const standard = override?.standard;
useEffect(() => {
if (!open || !baseline || !standard) return;
if (!open || !override) return;
let cancelled = false;
setError("");
const pinnedRevision = standard.pinned_baseline_revision
?? standard.active_baseline_revision
?? 1;
void Promise.all([
getWorkflowRevision(settings, baseline.id, pinnedRevision),
getWorkflowRevision(
settings,
baseline.id,
standard.latest_baseline_revision
)
]).then(([pinnedItem, latestItem]) => {
setComparison(null);
void compareWorkflowDefinitionToStandard(settings, override.id).then((result) => {
if (cancelled) return;
setPinned(pinnedItem);
setLatest(latestItem);
setComparison(result);
}).catch((loadError) => {
if (!cancelled) setError(apiErrorMessage(loadError));
});
return () => {
cancelled = true;
};
}, [baseline, open, settings, standard]);
}, [open, override, settings]);
return (
<Dialog
@@ -1094,44 +1076,79 @@ function WorkflowStandardComparisonDialog({
{error}
</DismissibleAlert>
) : null}
<div className="workflow-standard-comparison">
<StandardRevisionColumn
label="Pinned standard"
revision={pinned}
/>
<StandardRevisionColumn
label="Local override"
revision={override?.revision ?? null}
/>
<StandardRevisionColumn
label="Latest standard"
revision={latest}
/>
</div>
{!error && !comparison ? <LoadingFrame label="Comparing workflow revisions..." /> : null}
{comparison ? (
<div className="workflow-standard-comparison">
<div className="workflow-standard-comparison-summary">
<span>
Pinned revision <strong>{comparison.pinned_baseline_revision}</strong>
</span>
<span>
Local revision <strong>{comparison.local_revision}</strong>
</span>
<span>
Latest standard <strong>{comparison.latest_baseline_revision}</strong>
</span>
<StatusBadge
status={comparison.auto_mergeable ? "ready" : "warning"}
label={comparison.auto_mergeable
? "No semantic conflicts"
: `${comparison.conflict_count} conflicts`}
/>
</div>
<div className="workflow-standard-diff-list">
{comparison.items.map((item) => (
<section
className={`workflow-standard-diff-item state-${item.state}`}
key={`${item.resource_type}:${item.resource_id}`}
>
<header>
<span>
<strong>{item.resource_id}</strong>
<small>{item.resource_type} · {item.changed_fields.join(", ")}</small>
</span>
<StatusBadge
status={item.state === "conflict"
? "warning"
: item.state === "upstream_only"
? "queued"
: "active"}
label={standardDiffLabel(item.state)}
/>
</header>
<p>{standardDiffAction(item.recommended_action)}</p>
</section>
))}
{!comparison.items.length ? (
<p className="workflow-standard-diff-empty">
The local override and latest standard contain no semantic changes.
</p>
) : null}
</div>
</div>
) : null}
</Dialog>
);
}
function StandardRevisionColumn({
label,
revision
}: {
label: string;
revision: WorkflowRevision | null;
}) {
return (
<section className="workflow-standard-revision">
<header>
<strong>{label}</strong>
<small>
{revision
? `Revision ${revision.revision} · ${revision.content_hash.slice(0, 12)}`
: "Loading..."}
</small>
</header>
<pre>{revision ? JSON.stringify(revision.graph, null, 2) : ""}</pre>
</section>
);
function standardDiffLabel(state: string): string {
return ({
local_only: "Local change",
upstream_only: "Standard update",
same_change: "Same change",
conflict: "Conflict",
unchanged: "Unchanged"
} as Record<string, string>)[state] ?? state;
}
function standardDiffAction(action: string): string {
return ({
keep_local: "The local change can be retained automatically.",
adopt_upstream: "The standard update can be adopted automatically.",
either: "Both revisions made the same semantic change.",
manual_resolution: "Local and upstream changes overlap and need a decision.",
none: "No action is required."
} as Record<string, string>)[action] ?? action;
}
function WorkflowDefinitionSettingsDialog({