@cyoda/workflow-viewer 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -9
- package/dist/index.cjs +146 -63
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -6
- package/dist/index.d.ts +27 -6
- package/dist/index.js +147 -64
- package/dist/index.js.map +1 -1
- package/dist/theme/index.cjs +3 -10
- package/dist/theme/index.cjs.map +1 -1
- package/dist/theme/index.d.cts +1 -2
- package/dist/theme/index.d.ts +1 -2
- package/dist/theme/index.js +3 -10
- package/dist/theme/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/components/WorkflowViewer.tsx
|
|
2
|
-
import { useMemo, useState as useState2 } from "react";
|
|
2
|
+
import { useEffect, useMemo, useState as useState2 } from "react";
|
|
3
|
+
import { computeHighlightSet, inspectGraphFocus, projectToGraph } from "@cyoda/workflow-graph";
|
|
3
4
|
|
|
4
5
|
// src/theme/tokens.ts
|
|
5
6
|
var workflowPalette = {
|
|
@@ -125,6 +126,29 @@ function simpleLayout(graph) {
|
|
|
125
126
|
}
|
|
126
127
|
return { positions, width: maxWidth + 24, height: yCursor };
|
|
127
128
|
}
|
|
129
|
+
function nudgeLabels(items) {
|
|
130
|
+
const sorted = [...items].sort((a, b) => a.midX - b.midX || a.midY - b.midY);
|
|
131
|
+
const placed = [];
|
|
132
|
+
const result = /* @__PURE__ */ new Map();
|
|
133
|
+
for (const item of sorted) {
|
|
134
|
+
const { midX } = item;
|
|
135
|
+
let { midY } = item;
|
|
136
|
+
const halfW = item.pillW / 2;
|
|
137
|
+
const halfH = item.pillH / 2;
|
|
138
|
+
let attempts = 0;
|
|
139
|
+
while (attempts < 20) {
|
|
140
|
+
const overlaps = placed.some(
|
|
141
|
+
(p) => midX + halfW > p.x - p.w / 2 && midX - halfW < p.x + p.w / 2 && midY + halfH > p.y - p.h / 2 && midY - halfH < p.y + p.h / 2
|
|
142
|
+
);
|
|
143
|
+
if (!overlaps) break;
|
|
144
|
+
midY += item.pillH + 4;
|
|
145
|
+
attempts++;
|
|
146
|
+
}
|
|
147
|
+
placed.push({ x: midX, y: midY, w: item.pillW, h: item.pillH });
|
|
148
|
+
result.set(item.id, { midX, midY });
|
|
149
|
+
}
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
128
152
|
function groupByWorkflow(nodes) {
|
|
129
153
|
const out = /* @__PURE__ */ new Map();
|
|
130
154
|
for (const n of nodes) {
|
|
@@ -245,8 +269,7 @@ function laneColor(edge, opts) {
|
|
|
245
269
|
return e.automated;
|
|
246
270
|
}
|
|
247
271
|
function laneDashArray(edge) {
|
|
248
|
-
if (edge.
|
|
249
|
-
if (edge.isLoopback) return "6 4";
|
|
272
|
+
if (edge.manual) return "2 4";
|
|
250
273
|
return void 0;
|
|
251
274
|
}
|
|
252
275
|
|
|
@@ -261,9 +284,9 @@ function polylineToPath(points) {
|
|
|
261
284
|
}
|
|
262
285
|
function computeEdgeGeometry(edge, source, target) {
|
|
263
286
|
const sx = source.x + source.width / 2;
|
|
264
|
-
const sy = source.y + source.height
|
|
287
|
+
const sy = source.y + source.height;
|
|
265
288
|
const tx = target.x + target.width / 2;
|
|
266
|
-
const ty = target.y
|
|
289
|
+
const ty = target.y;
|
|
267
290
|
if (edge.isSelf) {
|
|
268
291
|
const rightX = source.x + source.width;
|
|
269
292
|
const topY = source.y + source.height / 3;
|
|
@@ -299,7 +322,6 @@ function EdgePath({
|
|
|
299
322
|
const d = route && route.points.length >= 2 ? polylineToPath(route.points) : computeEdgeGeometry(edge, source, target).d;
|
|
300
323
|
const strokeWidth = selected || highlighted ? geometry.edge.strokeWidth + 0.8 : edge.isLoopback ? geometry.edge.loopStrokeWidth : geometry.edge.strokeWidth;
|
|
301
324
|
const opacity = dimmed ? 0.25 : 1;
|
|
302
|
-
const isManualSolid = edge.manual && !edge.disabled && !edge.isLoopback;
|
|
303
325
|
return /* @__PURE__ */ jsxs(
|
|
304
326
|
"g",
|
|
305
327
|
{
|
|
@@ -324,16 +346,6 @@ function EdgePath({
|
|
|
324
346
|
strokeDasharray: dash,
|
|
325
347
|
markerEnd: `url(#wf-arrow-${colorKey(color)})`
|
|
326
348
|
}
|
|
327
|
-
),
|
|
328
|
-
isManualSolid && /* @__PURE__ */ jsx(
|
|
329
|
-
"path",
|
|
330
|
-
{
|
|
331
|
-
d,
|
|
332
|
-
fill: "none",
|
|
333
|
-
stroke: workflowPalette.neutrals.white,
|
|
334
|
-
strokeWidth: 0.6,
|
|
335
|
-
pointerEvents: "none"
|
|
336
|
-
}
|
|
337
349
|
)
|
|
338
350
|
]
|
|
339
351
|
}
|
|
@@ -422,6 +434,7 @@ function StartMarker({ position }) {
|
|
|
422
434
|
return /* @__PURE__ */ jsx3("g", { "aria-hidden": "true", children: /* @__PURE__ */ jsx3(
|
|
423
435
|
"circle",
|
|
424
436
|
{
|
|
437
|
+
"data-testid": "start-marker",
|
|
425
438
|
cx,
|
|
426
439
|
cy,
|
|
427
440
|
r,
|
|
@@ -437,7 +450,7 @@ function roleCategoryLabel(node) {
|
|
|
437
450
|
if (node.role === "initial" || node.role === "initial-terminal") return "INITIAL";
|
|
438
451
|
if (node.role === "terminal") return "TERMINAL";
|
|
439
452
|
if (node.category === "MANUAL_REVIEW") return "MANUAL REVIEW";
|
|
440
|
-
if (node.category === "PROCESSING_STATE") return "PROCESSING
|
|
453
|
+
if (node.category === "PROCESSING_STATE") return "PROCESSING";
|
|
441
454
|
return "STATE";
|
|
442
455
|
}
|
|
443
456
|
|
|
@@ -454,7 +467,6 @@ function paletteFor(node) {
|
|
|
454
467
|
// src/theme/badges.ts
|
|
455
468
|
function badgesFor(summary, flags) {
|
|
456
469
|
const out = [];
|
|
457
|
-
if (flags.manual) out.push({ key: "manual", label: "Manual" });
|
|
458
470
|
if (summary.processor) {
|
|
459
471
|
if (summary.processor.kind === "single") {
|
|
460
472
|
out.push({ key: "processor", label: summary.processor.name });
|
|
@@ -470,11 +482,6 @@ function badgesFor(summary, flags) {
|
|
|
470
482
|
out.push({ key: "criterion", label: "Criterion" });
|
|
471
483
|
}
|
|
472
484
|
}
|
|
473
|
-
if (summary.execution?.kind === "sync") {
|
|
474
|
-
out.push({ key: "execution", label: "SYNC" });
|
|
475
|
-
} else if (summary.execution?.kind === "asyncSameTx") {
|
|
476
|
-
out.push({ key: "execution", label: "ASYNC_SAME_TX" });
|
|
477
|
-
}
|
|
478
485
|
if (flags.disabled) out.push({ key: "disabled", label: "Disabled" });
|
|
479
486
|
return out;
|
|
480
487
|
}
|
|
@@ -718,25 +725,47 @@ function pickBadgePalette(key) {
|
|
|
718
725
|
// src/components/WorkflowViewer.tsx
|
|
719
726
|
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
720
727
|
function WorkflowViewer({
|
|
721
|
-
graph,
|
|
728
|
+
graph: graphInput,
|
|
729
|
+
document,
|
|
722
730
|
layout,
|
|
723
731
|
width = "100%",
|
|
724
732
|
height = "100%",
|
|
725
733
|
selectedId,
|
|
726
734
|
onSelectionChange,
|
|
735
|
+
surface = "website",
|
|
736
|
+
layoutMode,
|
|
737
|
+
viewerLayout,
|
|
738
|
+
interaction = "hover-highlight",
|
|
739
|
+
onInspect,
|
|
740
|
+
showStartMarker = false,
|
|
727
741
|
className
|
|
728
742
|
}) {
|
|
743
|
+
const graph = useMemo(() => {
|
|
744
|
+
if (graphInput) return graphInput;
|
|
745
|
+
if (document) return projectToGraph(document);
|
|
746
|
+
throw new Error("WorkflowViewer requires either graph or document.");
|
|
747
|
+
}, [graphInput, document]);
|
|
748
|
+
const visibleGraph = useMemo(() => {
|
|
749
|
+
if (showStartMarker) return graph;
|
|
750
|
+
return {
|
|
751
|
+
...graph,
|
|
752
|
+
nodes: graph.nodes.filter((node) => node.kind !== "startMarker"),
|
|
753
|
+
edges: graph.edges.filter((edge) => edge.kind !== "startMarker")
|
|
754
|
+
};
|
|
755
|
+
}, [graph, showStartMarker]);
|
|
756
|
+
const graphLayout = typeof layout === "string" ? void 0 : layout;
|
|
757
|
+
const productLayout = viewerLayout ?? layoutMode ?? (typeof layout === "string" ? layout : void 0) ?? "embedded";
|
|
729
758
|
const effectiveLayout = useMemo(
|
|
730
|
-
() =>
|
|
731
|
-
[
|
|
759
|
+
() => normalizeLayoutForVisibleGraph(graphLayout, visibleGraph) ?? simpleLayout(visibleGraph),
|
|
760
|
+
[graphLayout, visibleGraph]
|
|
732
761
|
);
|
|
733
762
|
const pan = usePanZoom();
|
|
734
763
|
const [internalSelection, setInternalSelection] = useState2(null);
|
|
735
764
|
const [hovered, setHovered] = useState2(null);
|
|
736
765
|
const selection = selectedId ?? internalSelection;
|
|
737
766
|
const stateNodes = useMemo(
|
|
738
|
-
() =>
|
|
739
|
-
[
|
|
767
|
+
() => visibleGraph.nodes.filter((n) => n.kind === "state"),
|
|
768
|
+
[visibleGraph.nodes]
|
|
740
769
|
);
|
|
741
770
|
const stateById = useMemo(() => {
|
|
742
771
|
const m = /* @__PURE__ */ new Map();
|
|
@@ -744,22 +773,71 @@ function WorkflowViewer({
|
|
|
744
773
|
return m;
|
|
745
774
|
}, [stateNodes]);
|
|
746
775
|
const transitionEdges = useMemo(
|
|
747
|
-
() =>
|
|
748
|
-
[
|
|
749
|
-
);
|
|
750
|
-
const highlightSet = useMemo(
|
|
751
|
-
() => computeHighlightSet(hovered ?? selection, graph.nodes, graph.edges),
|
|
752
|
-
[hovered, selection, graph.nodes, graph.edges]
|
|
776
|
+
() => visibleGraph.edges.filter((e) => e.kind === "transition"),
|
|
777
|
+
[visibleGraph.edges]
|
|
753
778
|
);
|
|
779
|
+
useEffect(() => {
|
|
780
|
+
if (process.env.NODE_ENV === "production" || graphLayout) return;
|
|
781
|
+
const sourceCounts = /* @__PURE__ */ new Map();
|
|
782
|
+
for (const e of graph.edges) {
|
|
783
|
+
if (e.kind !== "transition") continue;
|
|
784
|
+
sourceCounts.set(e.sourceId, (sourceCounts.get(e.sourceId) ?? 0) + 1);
|
|
785
|
+
}
|
|
786
|
+
if ([...sourceCounts.values()].some((n) => n > 1)) {
|
|
787
|
+
console.warn(
|
|
788
|
+
"[WorkflowViewer] Rendering without an ELK layout \u2014 branching graphs may not look polished. Pass a layout from `layoutGraph()` (@cyoda/workflow-layout) for best results."
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
}, [graphLayout, visibleGraph.edges]);
|
|
792
|
+
const fallbackLabelPositions = useMemo(() => {
|
|
793
|
+
if (effectiveLayout.edges) return null;
|
|
794
|
+
const CHAR_W = 6.5;
|
|
795
|
+
const PILL_H = 24;
|
|
796
|
+
const items = transitionEdges.flatMap((edge) => {
|
|
797
|
+
const source = effectiveLayout.positions.get(edge.sourceId);
|
|
798
|
+
const target = effectiveLayout.positions.get(edge.targetId);
|
|
799
|
+
if (!source || !target) return [];
|
|
800
|
+
const { midX, midY } = computeEdgeGeometry(edge, source, target);
|
|
801
|
+
const pillW = Math.max(40, edge.summary.display.length * CHAR_W + 12);
|
|
802
|
+
return [{ id: edge.id, midX, midY, pillW, pillH: PILL_H }];
|
|
803
|
+
});
|
|
804
|
+
return nudgeLabels(items);
|
|
805
|
+
}, [effectiveLayout, transitionEdges]);
|
|
806
|
+
const focusId = hovered ?? selection;
|
|
807
|
+
const highlightSet = useMemo(() => {
|
|
808
|
+
if (interaction === "none") return null;
|
|
809
|
+
if (interaction === "select") {
|
|
810
|
+
return computeHighlightSet(selection, visibleGraph.nodes, visibleGraph.edges);
|
|
811
|
+
}
|
|
812
|
+
return computeHighlightSet(focusId, visibleGraph.nodes, visibleGraph.edges);
|
|
813
|
+
}, [interaction, focusId, selection, visibleGraph.nodes, visibleGraph.edges]);
|
|
754
814
|
const anythingFocused = highlightSet !== null;
|
|
755
815
|
const handleSelect = (id) => {
|
|
816
|
+
if (interaction === "none") return;
|
|
756
817
|
setInternalSelection(id);
|
|
757
818
|
onSelectionChange?.(id);
|
|
758
819
|
};
|
|
759
820
|
const handleBackgroundClick = () => {
|
|
821
|
+
if (interaction === "none") return;
|
|
760
822
|
setInternalSelection(null);
|
|
761
823
|
onSelectionChange?.(null);
|
|
762
824
|
};
|
|
825
|
+
const handleHoverEnter = (id) => {
|
|
826
|
+
if (interaction === "hover-highlight" || interaction === "hover-path") {
|
|
827
|
+
setHovered(id);
|
|
828
|
+
}
|
|
829
|
+
if (interaction === "hover-path") {
|
|
830
|
+
onInspect?.(inspectGraphFocus(visibleGraph, id));
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
const handleHoverLeave = () => {
|
|
834
|
+
if (interaction === "hover-highlight" || interaction === "hover-path") {
|
|
835
|
+
setHovered(null);
|
|
836
|
+
}
|
|
837
|
+
if (interaction === "hover-path") {
|
|
838
|
+
onInspect?.(null);
|
|
839
|
+
}
|
|
840
|
+
};
|
|
763
841
|
return /* @__PURE__ */ jsxs5(
|
|
764
842
|
"svg",
|
|
765
843
|
{
|
|
@@ -777,8 +855,12 @@ function WorkflowViewer({
|
|
|
777
855
|
style: {
|
|
778
856
|
background: workflowPalette.neutrals.white,
|
|
779
857
|
fontFamily: "inherit",
|
|
780
|
-
userSelect: "none"
|
|
858
|
+
userSelect: "none",
|
|
859
|
+
...productLayout === "fullWidth" ? { display: "block", width: "100%", height: "100%" } : null
|
|
781
860
|
},
|
|
861
|
+
"data-surface": surface,
|
|
862
|
+
"data-layout": productLayout,
|
|
863
|
+
"data-interaction": interaction,
|
|
782
864
|
"data-testid": "workflow-viewer",
|
|
783
865
|
children: [
|
|
784
866
|
/* @__PURE__ */ jsx6(Defs, {}),
|
|
@@ -808,8 +890,8 @@ function WorkflowViewer({
|
|
|
808
890
|
dimmed: isDimmed,
|
|
809
891
|
selected: isEdgeSelected,
|
|
810
892
|
onSelect: handleSelect,
|
|
811
|
-
onHoverEnter:
|
|
812
|
-
onHoverLeave:
|
|
893
|
+
onHoverEnter: handleHoverEnter,
|
|
894
|
+
onHoverLeave: handleHoverLeave
|
|
813
895
|
},
|
|
814
896
|
edge.id
|
|
815
897
|
);
|
|
@@ -819,7 +901,7 @@ function WorkflowViewer({
|
|
|
819
901
|
const target = effectiveLayout.positions.get(edge.targetId);
|
|
820
902
|
if (!source || !target) return null;
|
|
821
903
|
const route = effectiveLayout.edges?.get(edge.id);
|
|
822
|
-
const labelPos = route ? { midX: route.labelX, midY: route.labelY } : computeEdgeGeometry(edge, source, target);
|
|
904
|
+
const labelPos = route ? { midX: route.labelX, midY: route.labelY } : fallbackLabelPositions?.get(edge.id) ?? computeEdgeGeometry(edge, source, target);
|
|
823
905
|
const isHighlighted = highlightSet?.has(edge.id) ?? false;
|
|
824
906
|
const isDimmed = anythingFocused && !isHighlighted;
|
|
825
907
|
return /* @__PURE__ */ jsx6(
|
|
@@ -835,13 +917,13 @@ function WorkflowViewer({
|
|
|
835
917
|
`label-${edge.id}`
|
|
836
918
|
);
|
|
837
919
|
}),
|
|
838
|
-
|
|
920
|
+
visibleGraph.nodes.map((node) => renderNode(node, effectiveLayout, {
|
|
839
921
|
selection,
|
|
840
922
|
highlightSet,
|
|
841
923
|
anythingFocused,
|
|
842
924
|
onSelect: handleSelect,
|
|
843
|
-
onHoverEnter:
|
|
844
|
-
onHoverLeave:
|
|
925
|
+
onHoverEnter: handleHoverEnter,
|
|
926
|
+
onHoverLeave: handleHoverLeave
|
|
845
927
|
}))
|
|
846
928
|
]
|
|
847
929
|
}
|
|
@@ -850,6 +932,30 @@ function WorkflowViewer({
|
|
|
850
932
|
}
|
|
851
933
|
);
|
|
852
934
|
}
|
|
935
|
+
function normalizeLayoutForVisibleGraph(layout, graph) {
|
|
936
|
+
if (!layout) return void 0;
|
|
937
|
+
const visibleNodeIds = new Set(graph.nodes.map((node) => node.id));
|
|
938
|
+
const positions = new Map(
|
|
939
|
+
Array.from(layout.positions.entries()).filter(([nodeId]) => visibleNodeIds.has(nodeId))
|
|
940
|
+
);
|
|
941
|
+
const visibleEdgeIds = new Set(graph.edges.map((edge) => edge.id));
|
|
942
|
+
const edges = layout.edges ? new Map(Array.from(layout.edges.entries()).filter(([edgeId]) => visibleEdgeIds.has(edgeId))) : void 0;
|
|
943
|
+
return {
|
|
944
|
+
...layout,
|
|
945
|
+
positions,
|
|
946
|
+
edges,
|
|
947
|
+
width: computeLayoutBound(positions, "x"),
|
|
948
|
+
height: computeLayoutBound(positions, "y")
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
function computeLayoutBound(positions, axis) {
|
|
952
|
+
let max = 0;
|
|
953
|
+
for (const position of positions.values()) {
|
|
954
|
+
const bound = axis === "x" ? position.x + position.width : position.y + position.height;
|
|
955
|
+
if (bound > max) max = bound;
|
|
956
|
+
}
|
|
957
|
+
return Math.max(max + 24, 72);
|
|
958
|
+
}
|
|
853
959
|
function renderNode(node, layout, ctx) {
|
|
854
960
|
const pos = layout.positions.get(node.id);
|
|
855
961
|
if (!pos) return null;
|
|
@@ -883,29 +989,6 @@ function smallPositionForMarker(pos) {
|
|
|
883
989
|
height: size
|
|
884
990
|
};
|
|
885
991
|
}
|
|
886
|
-
function computeHighlightSet(focusedId, nodes, edges) {
|
|
887
|
-
if (!focusedId) return null;
|
|
888
|
-
const set = /* @__PURE__ */ new Set();
|
|
889
|
-
set.add(focusedId);
|
|
890
|
-
const node = nodes.find((n) => n.id === focusedId);
|
|
891
|
-
if (node) {
|
|
892
|
-
for (const e of edges) {
|
|
893
|
-
if (e.kind !== "transition") continue;
|
|
894
|
-
if (e.sourceId === focusedId || e.targetId === focusedId) {
|
|
895
|
-
set.add(e.id);
|
|
896
|
-
set.add(e.sourceId);
|
|
897
|
-
set.add(e.targetId);
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
return set;
|
|
901
|
-
}
|
|
902
|
-
const edge = edges.find((e) => e.id === focusedId);
|
|
903
|
-
if (edge && edge.kind === "transition") {
|
|
904
|
-
set.add(edge.sourceId);
|
|
905
|
-
set.add(edge.targetId);
|
|
906
|
-
}
|
|
907
|
-
return set;
|
|
908
|
-
}
|
|
909
992
|
export {
|
|
910
993
|
WorkflowViewer,
|
|
911
994
|
simpleLayout
|