@foresthubai/workflow-builder 0.3.0 → 0.4.1

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.
Files changed (124) hide show
  1. package/LICENSE +661 -661
  2. package/NOTICE +16 -16
  3. package/README.md +110 -93
  4. package/dist/components/ui/command.d.ts +2 -2
  5. package/dist/components/ui/input.d.ts +1 -1
  6. package/dist/components/ui/resizable.d.ts +1 -1
  7. package/dist/components/ui/textarea.d.ts +1 -1
  8. package/dist/graph/BaseNode.js +10 -10
  9. package/dist/graph/reactFlowRegistry.d.ts.map +1 -1
  10. package/dist/lib/utils.d.ts +3 -0
  11. package/dist/lib/utils.d.ts.map +1 -0
  12. package/dist/lib/utils.js +6 -0
  13. package/dist/lib/utils.js.map +1 -0
  14. package/dist/stores/editorStore.d.ts +0 -1
  15. package/dist/stores/editorStore.d.ts.map +1 -1
  16. package/dist/stores/editorStore.js +1 -11
  17. package/dist/stores/editorStore.js.map +1 -1
  18. package/dist/toolbars/CanvasTabsToolbar.d.ts +11 -0
  19. package/dist/toolbars/CanvasTabsToolbar.d.ts.map +1 -0
  20. package/dist/toolbars/CanvasTabsToolbar.js +101 -0
  21. package/dist/toolbars/CanvasTabsToolbar.js.map +1 -0
  22. package/package.json +2 -2
  23. package/src/BuilderLayout.tsx +345 -345
  24. package/src/Canvas.tsx +261 -261
  25. package/src/CanvasEditor.tsx +142 -142
  26. package/src/CanvasTabsToolbar.tsx +176 -176
  27. package/src/RightConfigPanel.tsx +266 -266
  28. package/src/WorkflowBuilder.tsx +412 -412
  29. package/src/cn.ts +6 -6
  30. package/src/components/ui/add-button.tsx +39 -39
  31. package/src/components/ui/alert-dialog.tsx +141 -141
  32. package/src/components/ui/alert.tsx +59 -59
  33. package/src/components/ui/badge.tsx +36 -36
  34. package/src/components/ui/button.tsx +85 -85
  35. package/src/components/ui/card.tsx +79 -79
  36. package/src/components/ui/checkbox.tsx +28 -28
  37. package/src/components/ui/collapsible.tsx +9 -9
  38. package/src/components/ui/command.tsx +153 -153
  39. package/src/components/ui/delete-button.tsx +23 -23
  40. package/src/components/ui/dialog.tsx +125 -125
  41. package/src/components/ui/dropdown-menu.tsx +198 -198
  42. package/src/components/ui/input.tsx +55 -55
  43. package/src/components/ui/label.tsx +24 -24
  44. package/src/components/ui/readonly-banner.tsx +15 -15
  45. package/src/components/ui/resizable.tsx +43 -43
  46. package/src/components/ui/scroll-area.tsx +102 -102
  47. package/src/components/ui/select.tsx +160 -160
  48. package/src/components/ui/separator.tsx +29 -29
  49. package/src/components/ui/switch.tsx +27 -27
  50. package/src/components/ui/textarea.tsx +51 -51
  51. package/src/components/ui/toast.tsx +127 -127
  52. package/src/components/ui/toaster.tsx +33 -33
  53. package/src/components/ui/toggle-group.tsx +59 -59
  54. package/src/components/ui/toggle.tsx +43 -43
  55. package/src/components/ui/tooltip.tsx +32 -32
  56. package/src/dialogs/NodePickerDialog.tsx +84 -84
  57. package/src/dialogs/ValidationDialog.tsx +184 -184
  58. package/src/graph/BaseNode.tsx +557 -557
  59. package/src/graph/CustomEdge.tsx +185 -185
  60. package/src/graph/CustomNode.tsx +16 -16
  61. package/src/graph/FunctionCallNode.tsx +30 -30
  62. package/src/graph/PortHandle.tsx +189 -189
  63. package/src/graph/reactFlowRegistry.ts +26 -26
  64. package/src/hooks/use-toast.ts +125 -125
  65. package/src/hooks/useAvailableVariables.ts +20 -20
  66. package/src/hooks/useCanvasHistory.ts +22 -22
  67. package/src/hooks/useCanvasTabs.ts +168 -168
  68. package/src/hooks/useFunctionDiagnosticsSync.ts +40 -40
  69. package/src/hooks/useFunctionRegistry.ts +26 -26
  70. package/src/hooks/useFunctions.ts +44 -44
  71. package/src/hooks/useGraph.ts +161 -161
  72. package/src/hooks/useNodeDefinitions.ts +82 -82
  73. package/src/hooks/useParamErrors.ts +26 -26
  74. package/src/hooks/useResolvedTheme.ts +30 -30
  75. package/src/hooks/useResourceDiagnosticsSync.ts +58 -58
  76. package/src/hooks/useSuppressThemeTransition.ts +79 -79
  77. package/src/hooks/useWorkflowSerialization.ts +127 -127
  78. package/src/i18n/index.ts +53 -53
  79. package/src/i18n/locales/de.json +501 -501
  80. package/src/i18n/locales/en.json +557 -557
  81. package/src/index.ts +27 -27
  82. package/src/inputs/ExpressionInput.tsx +297 -297
  83. package/src/inputs/ParameterEditor.tsx +515 -515
  84. package/src/inputs/PortSection.tsx +144 -144
  85. package/src/panels/BuilderSidebar.tsx +301 -301
  86. package/src/panels/ChannelConfigPanel.tsx +49 -49
  87. package/src/panels/ChannelsPanel.tsx +28 -28
  88. package/src/panels/DebugConsolePanel.tsx +73 -73
  89. package/src/panels/DebugContextPanel.tsx +77 -77
  90. package/src/panels/DebugExternalIOPanel.tsx +180 -180
  91. package/src/panels/DiagnosticsPanel.tsx +170 -170
  92. package/src/panels/EdgeConfigPanel.tsx +104 -104
  93. package/src/panels/FunctionConfigPanel.tsx +179 -179
  94. package/src/panels/FunctionListPanel.tsx +45 -45
  95. package/src/panels/MemoryConfigPanel.tsx +55 -55
  96. package/src/panels/MemoryPanel.tsx +40 -40
  97. package/src/panels/ModelConfigPanel.tsx +41 -41
  98. package/src/panels/ModelsPanel.tsx +36 -36
  99. package/src/panels/NodeConfigPanel.tsx +630 -630
  100. package/src/panels/NodeLibrary.tsx +288 -288
  101. package/src/panels/ResourceConfigPanel.tsx +132 -132
  102. package/src/panels/ResourceListPanel.tsx +113 -113
  103. package/src/panels/VariableConfigPanel.tsx +161 -161
  104. package/src/panels/VariablesPanel.tsx +145 -145
  105. package/src/stores/canvasStore.test.ts +44 -44
  106. package/src/stores/canvasStore.ts +245 -245
  107. package/src/stores/debugStore.ts +74 -74
  108. package/src/stores/diagnosticsStore.ts +130 -130
  109. package/src/stores/editorStore.ts +190 -202
  110. package/src/styles/index.css +526 -526
  111. package/src/utils/categoryConstants.ts +26 -26
  112. package/src/utils/channelOperations.ts +86 -86
  113. package/src/utils/connectionRules.ts +137 -137
  114. package/src/utils/functionOperations.ts +179 -179
  115. package/src/utils/graphOperations.ts +550 -550
  116. package/src/utils/history.ts +207 -207
  117. package/src/utils/memoryOperations.ts +57 -57
  118. package/src/utils/migrateFunctionNodes.ts +107 -107
  119. package/src/utils/modelOperations.ts +55 -55
  120. package/src/utils/paramDisplay.ts +71 -71
  121. package/src/utils/resourceHelpers.ts +32 -32
  122. package/src/utils/translation.ts +28 -28
  123. package/src/utils/variableOperations.ts +75 -75
  124. package/tailwind-preset.ts +166 -166
@@ -1,40 +1,40 @@
1
- import { useEffect } from "react";
2
- import { validateFunction } from "@foresthubai/workflow-core/diagnostics";
3
- import { computeAvailableVariables } from "@foresthubai/workflow-core/variable";
4
- import { useEditorStore } from "../stores/editorStore";
5
- import { useDiagnosticsStore } from "../stores/diagnosticsStore";
6
- import { getCanvasStore } from "../stores/canvasStore";
7
-
8
- /**
9
- * Keeps the `byFunctionId` diagnostics slot in sync with the function declarations —
10
- * the single source for the sidebar tab badge, the function list ring, AND the config
11
- * panel's per-output rings (they all read this slot). Mounted once at the
12
- * workflow-builder root so it survives tab open/close.
13
- *
14
- * Scope-aware: each function is validated against its own body canvas's variables, so
15
- * invalid/typed return expressions surface here too — not just missing ones. (Reacts
16
- * to declaration/expression edits via `functions`; a body-only edit that changes the
17
- * available variables without touching a declaration refreshes on the next
18
- * declaration change or a full `validate`.)
19
- */
20
- export function useFunctionDiagnosticsSync(): void {
21
- const functions = useEditorStore((s) => s.functions);
22
-
23
- useEffect(() => {
24
- const ds = useDiagnosticsStore.getState();
25
-
26
- const seen = new Set<string>();
27
- for (const [id, def] of Object.entries(functions)) {
28
- seen.add(id);
29
- const store = getCanvasStore(id);
30
- const lookup = store
31
- ? computeAvailableVariables(store.getState().variables, store.getState().edges).lookup
32
- : undefined;
33
- ds.setFunctionDiagnostics(id, validateFunction(def, lookup));
34
- }
35
-
36
- for (const id of Object.keys(ds.byFunctionId)) {
37
- if (!seen.has(id)) ds.clearFunctionDiagnostics(id);
38
- }
39
- }, [functions]);
40
- }
1
+ import { useEffect } from "react";
2
+ import { validateFunction } from "@foresthubai/workflow-core/diagnostics";
3
+ import { computeAvailableVariables } from "@foresthubai/workflow-core/variable";
4
+ import { useEditorStore } from "../stores/editorStore";
5
+ import { useDiagnosticsStore } from "../stores/diagnosticsStore";
6
+ import { getCanvasStore } from "../stores/canvasStore";
7
+
8
+ /**
9
+ * Keeps the `byFunctionId` diagnostics slot in sync with the function declarations —
10
+ * the single source for the sidebar tab badge, the function list ring, AND the config
11
+ * panel's per-output rings (they all read this slot). Mounted once at the
12
+ * workflow-builder root so it survives tab open/close.
13
+ *
14
+ * Scope-aware: each function is validated against its own body canvas's variables, so
15
+ * invalid/typed return expressions surface here too — not just missing ones. (Reacts
16
+ * to declaration/expression edits via `functions`; a body-only edit that changes the
17
+ * available variables without touching a declaration refreshes on the next
18
+ * declaration change or a full `validate`.)
19
+ */
20
+ export function useFunctionDiagnosticsSync(): void {
21
+ const functions = useEditorStore((s) => s.functions);
22
+
23
+ useEffect(() => {
24
+ const ds = useDiagnosticsStore.getState();
25
+
26
+ const seen = new Set<string>();
27
+ for (const [id, def] of Object.entries(functions)) {
28
+ seen.add(id);
29
+ const store = getCanvasStore(id);
30
+ const lookup = store
31
+ ? computeAvailableVariables(store.getState().variables, store.getState().edges).lookup
32
+ : undefined;
33
+ ds.setFunctionDiagnostics(id, validateFunction(def, lookup));
34
+ }
35
+
36
+ for (const id of Object.keys(ds.byFunctionId)) {
37
+ if (!seen.has(id)) ds.clearFunctionDiagnostics(id);
38
+ }
39
+ }, [functions]);
40
+ }
@@ -1,26 +1,26 @@
1
- import { useCallback, useMemo } from "react";
2
- import { useEditorStore } from "../stores/editorStore";
3
- import type { FunctionDeclaration } from "@foresthubai/workflow-core/function";
4
-
5
- // Functions are a project-scoped resource: the registry IS editorStore.functions —
6
- // the domain FunctionDeclaration, no conversion. (Crossing to the flat api FunctionInfo
7
- // is done only when stamping a call-site snapshot; see useNodeDefinitions/migration.)
8
-
9
- /**
10
- * Access to all function declarations by id. Reactive over editorStore.functions.
11
- * - functions: Record of FunctionDeclaration by id
12
- * - functionsList: array of FunctionDeclaration
13
- * - getFunction(id): one declaration by id
14
- */
15
- export function useFunctionRegistry() {
16
- const functions = useEditorStore((s) => s.functions);
17
- const functionsList = useMemo(() => Object.values(functions), [functions]);
18
- const getFunction = useCallback((id: string): FunctionDeclaration | undefined => functions[id], [functions]);
19
-
20
- return { functions, functionsList, getFunction };
21
- }
22
-
23
- /** All function declarations without React subscription (for non-component code). */
24
- export function getAllFunctions(): Record<string, FunctionDeclaration> {
25
- return useEditorStore.getState().functions;
26
- }
1
+ import { useCallback, useMemo } from "react";
2
+ import { useEditorStore } from "../stores/editorStore";
3
+ import type { FunctionDeclaration } from "@foresthubai/workflow-core/function";
4
+
5
+ // Functions are a project-scoped resource: the registry IS editorStore.functions —
6
+ // the domain FunctionDeclaration, no conversion. (Crossing to the flat api FunctionInfo
7
+ // is done only when stamping a call-site snapshot; see useNodeDefinitions/migration.)
8
+
9
+ /**
10
+ * Access to all function declarations by id. Reactive over editorStore.functions.
11
+ * - functions: Record of FunctionDeclaration by id
12
+ * - functionsList: array of FunctionDeclaration
13
+ * - getFunction(id): one declaration by id
14
+ */
15
+ export function useFunctionRegistry() {
16
+ const functions = useEditorStore((s) => s.functions);
17
+ const functionsList = useMemo(() => Object.values(functions), [functions]);
18
+ const getFunction = useCallback((id: string): FunctionDeclaration | undefined => functions[id], [functions]);
19
+
20
+ return { functions, functionsList, getFunction };
21
+ }
22
+
23
+ /** All function declarations without React subscription (for non-component code). */
24
+ export function getAllFunctions(): Record<string, FunctionDeclaration> {
25
+ return useEditorStore.getState().functions;
26
+ }
@@ -1,44 +1,44 @@
1
- import { useCallback } from "react";
2
- import { useEditorStore } from "../stores/editorStore";
3
- import { getOrCreateCanvasStore } from "../stores/canvasStore";
4
- import { useFunctionRegistry } from "./useFunctionRegistry";
5
- import { addFunction } from "../utils/functionOperations";
6
-
7
- export interface UseFunctionsOptions {
8
- /** Open (or focus) a tab for a function canvas. */
9
- onOpenTab: (id: string, label: string) => void;
10
- }
11
-
12
- /**
13
- * Coordinates the canvas-tab UI with function declarations (editorStore). The
14
- * declarations themselves are CRUD'd in utils/functionOperations; this hook only
15
- * handles the open/create flows that must also touch the tab strip and selection.
16
- */
17
- export const useFunctions = ({ onOpenTab }: UseFunctionsOptions) => {
18
- const { functionsList: functions, getFunction } = useFunctionRegistry();
19
- const selectFunction = useEditorStore((s) => s.selectFunction);
20
-
21
- // Open an existing function: ensure its body canvas exists, open its tab, and
22
- // select it so the right panel shows its definition. onOpenTab switches the active
23
- // canvas first; selectFunction sets the selection last so it isn't cleared.
24
- const openFunction = useCallback(
25
- (functionId: string) => {
26
- const fn = getFunction(functionId);
27
- if (!fn) return;
28
- getOrCreateCanvasStore(functionId);
29
- onOpenTab(functionId, fn.name);
30
- selectFunction(functionId);
31
- },
32
- [getFunction, onOpenTab, selectFunction],
33
- );
34
-
35
- // Create a new function and open it (the canvas body is created by addFunction).
36
- const createFunction = useCallback(() => {
37
- const fn = addFunction();
38
- onOpenTab(fn.id, fn.name);
39
- selectFunction(fn.id);
40
- return fn.id;
41
- }, [onOpenTab, selectFunction]);
42
-
43
- return { functions, openFunction, createFunction };
44
- };
1
+ import { useCallback } from "react";
2
+ import { useEditorStore } from "../stores/editorStore";
3
+ import { getOrCreateCanvasStore } from "../stores/canvasStore";
4
+ import { useFunctionRegistry } from "./useFunctionRegistry";
5
+ import { addFunction } from "../utils/functionOperations";
6
+
7
+ export interface UseFunctionsOptions {
8
+ /** Open (or focus) a tab for a function canvas. */
9
+ onOpenTab: (id: string, label: string) => void;
10
+ }
11
+
12
+ /**
13
+ * Coordinates the canvas-tab UI with function declarations (editorStore). The
14
+ * declarations themselves are CRUD'd in utils/functionOperations; this hook only
15
+ * handles the open/create flows that must also touch the tab strip and selection.
16
+ */
17
+ export const useFunctions = ({ onOpenTab }: UseFunctionsOptions) => {
18
+ const { functionsList: functions, getFunction } = useFunctionRegistry();
19
+ const selectFunction = useEditorStore((s) => s.selectFunction);
20
+
21
+ // Open an existing function: ensure its body canvas exists, open its tab, and
22
+ // select it so the right panel shows its definition. onOpenTab switches the active
23
+ // canvas first; selectFunction sets the selection last so it isn't cleared.
24
+ const openFunction = useCallback(
25
+ (functionId: string) => {
26
+ const fn = getFunction(functionId);
27
+ if (!fn) return;
28
+ getOrCreateCanvasStore(functionId);
29
+ onOpenTab(functionId, fn.name);
30
+ selectFunction(functionId);
31
+ },
32
+ [getFunction, onOpenTab, selectFunction],
33
+ );
34
+
35
+ // Create a new function and open it (the canvas body is created by addFunction).
36
+ const createFunction = useCallback(() => {
37
+ const fn = addFunction();
38
+ onOpenTab(fn.id, fn.name);
39
+ selectFunction(fn.id);
40
+ return fn.id;
41
+ }, [onOpenTab, selectFunction]);
42
+
43
+ return { functions, openFunction, createFunction };
44
+ };
@@ -1,161 +1,161 @@
1
- import { NodeDefinition, NodeData } from "@foresthubai/workflow-core/node";
2
- import { Connection, Edge, Node } from "@xyflow/react";
3
- import type { EdgeData } from "@foresthubai/workflow-core/edge";
4
- import { useCallback } from "react";
5
- import { getOrCreateCanvasStore, MAIN_CANVAS_ID } from "../stores/canvasStore";
6
- import {
7
- addNodeToStore,
8
- type Clipboard,
9
- connectNodesInStore,
10
- deleteEdgesFromStore,
11
- deleteNodeFromStore,
12
- pasteToStore,
13
- updateEdgeInStore,
14
- updateNodeInStore,
15
- } from "../utils/graphOperations";
16
- import type { EdgeType } from "@foresthubai/workflow-core/edge";
17
- import { useNodeDefinitions } from "./useNodeDefinitions";
18
-
19
- // Shared across all useGraph instances (one per canvas) so copy/paste works
20
- // across canvas switches within a single builder session.
21
- const clipboardRef: { current: Clipboard | null } = { current: null };
22
-
23
- /**
24
- * Hook that provides graph management actions for a specific canvas.
25
- *
26
- * All mutations are:
27
- * - Gated by readOnly (no-op when true)
28
- * - Automatically wrapped in a history checkpoint (undo/redo)
29
- *
30
- * Callers don't need to worry about either concern.
31
- */
32
- export const useGraph = (canvasId: string = MAIN_CANVAS_ID, readOnly: boolean = false) => {
33
- const canvasStore = getOrCreateCanvasStore(canvasId);
34
- const nodes = canvasStore((s) => s.nodes);
35
- const edges = canvasStore((s) => s.edges);
36
- const { withCheckpoint } = canvasStore;
37
-
38
- const { getNodeDefinition } = useNodeDefinitions();
39
-
40
- // Guarded checkpoint: skips when readOnly, otherwise wraps in undo history entry
41
- const guarded = useCallback(
42
- <R>(operation: () => R): R | undefined => {
43
- if (readOnly) return undefined;
44
- return withCheckpoint(operation);
45
- },
46
- [readOnly, withCheckpoint],
47
- );
48
-
49
- const addNode = useCallback(
50
- (nodeDef: NodeDefinition, position?: { x: number; y: number }) => {
51
- return guarded(() => addNodeToStore(canvasStore, nodeDef, position)) ?? null;
52
- },
53
- [canvasStore, guarded],
54
- );
55
-
56
- const updateNode = useCallback(
57
- (nodeId: string, updates: { arguments?: Record<string, unknown>; label?: string }) => {
58
- guarded(() => updateNodeInStore(canvasStore, nodeId, updates));
59
- },
60
- [canvasStore, guarded],
61
- );
62
-
63
- const deleteNode = useCallback(
64
- (nodeId: string) => {
65
- guarded(() => deleteNodeFromStore(canvasStore, nodeId, getNodeDefinition));
66
- },
67
- [canvasStore, getNodeDefinition, guarded],
68
- );
69
-
70
- const onConnect = useCallback(
71
- (connection: Connection): EdgeType | false => {
72
- return guarded(() => connectNodesInStore(canvasStore, connection)) ?? false;
73
- },
74
- [canvasStore, guarded],
75
- );
76
-
77
- const updateEdge = useCallback(
78
- (edgeId: string, updates: Record<string, unknown>) => {
79
- guarded(() => updateEdgeInStore(canvasStore, edgeId, updates));
80
- },
81
- [canvasStore, guarded],
82
- );
83
-
84
- const deleteEdges = useCallback(
85
- (edgeIds: string[]) => {
86
- guarded(() => deleteEdgesFromStore(canvasStore, edgeIds));
87
- },
88
- [canvasStore, guarded],
89
- );
90
-
91
- // Batch delete nodes and edges as a single undo entry
92
- const deleteSelected = useCallback(
93
- (nodeIds: string[], edgeIds: string[]) => {
94
- if (nodeIds.length === 0 && edgeIds.length === 0) return;
95
- guarded(() => {
96
- for (const nodeId of nodeIds) deleteNodeFromStore(canvasStore, nodeId, getNodeDefinition);
97
- if (edgeIds.length > 0) deleteEdgesFromStore(canvasStore, edgeIds);
98
- });
99
- },
100
- [canvasStore, getNodeDefinition, guarded],
101
- );
102
-
103
- // Copy is read-only — no checkpoint, no readOnly gate
104
- const copySelection = useCallback(
105
- (nodeIds: string[]) => {
106
- if (nodeIds.length === 0) return;
107
-
108
- const nodeIdSet = new Set(nodeIds);
109
-
110
- const copiedNodes = nodes
111
- .filter((node) => nodeIdSet.has(node.id))
112
- .map((node) => JSON.parse(JSON.stringify(node)) as Node<NodeData>);
113
-
114
- const copiedEdges = edges
115
- .filter((edge) => nodeIdSet.has(edge.source) && nodeIdSet.has(edge.target))
116
- .map((edge) => JSON.parse(JSON.stringify(edge)) as Edge<EdgeData>);
117
-
118
- clipboardRef.current = { nodes: copiedNodes, edges: copiedEdges };
119
- },
120
- [nodes, edges],
121
- );
122
-
123
- const pasteSelection = useCallback(
124
- (offset?: { x: number; y: number }) => {
125
- const clipboard = clipboardRef.current;
126
- if (!clipboard) return undefined;
127
- return guarded(() => pasteToStore(canvasStore, clipboard, offset, getNodeDefinition));
128
- },
129
- [canvasStore, getNodeDefinition, guarded],
130
- );
131
-
132
- // Batch add node + connect as a single undo entry (used by contextual node picker)
133
- const addNodeAndConnect = useCallback(
134
- (nodeDef: NodeDefinition, position: { x: number; y: number }, connection: Connection): string | null => {
135
- return (
136
- guarded(() => {
137
- const nodeId = addNodeToStore(canvasStore, nodeDef, position);
138
- if (nodeId == null) return null;
139
- connectNodesInStore(canvasStore, { ...connection, target: nodeId });
140
- return nodeId;
141
- }) ?? null
142
- );
143
- },
144
- [canvasStore, guarded],
145
- );
146
-
147
- return {
148
- nodes,
149
- edges,
150
- addNode,
151
- updateNode,
152
- updateEdge,
153
- deleteNode,
154
- deleteEdges,
155
- deleteSelected,
156
- onConnect,
157
- addNodeAndConnect,
158
- copySelection,
159
- pasteSelection,
160
- };
161
- };
1
+ import { NodeDefinition, NodeData } from "@foresthubai/workflow-core/node";
2
+ import { Connection, Edge, Node } from "@xyflow/react";
3
+ import type { EdgeData } from "@foresthubai/workflow-core/edge";
4
+ import { useCallback } from "react";
5
+ import { getOrCreateCanvasStore, MAIN_CANVAS_ID } from "../stores/canvasStore";
6
+ import {
7
+ addNodeToStore,
8
+ type Clipboard,
9
+ connectNodesInStore,
10
+ deleteEdgesFromStore,
11
+ deleteNodeFromStore,
12
+ pasteToStore,
13
+ updateEdgeInStore,
14
+ updateNodeInStore,
15
+ } from "../utils/graphOperations";
16
+ import type { EdgeType } from "@foresthubai/workflow-core/edge";
17
+ import { useNodeDefinitions } from "./useNodeDefinitions";
18
+
19
+ // Shared across all useGraph instances (one per canvas) so copy/paste works
20
+ // across canvas switches within a single builder session.
21
+ const clipboardRef: { current: Clipboard | null } = { current: null };
22
+
23
+ /**
24
+ * Hook that provides graph management actions for a specific canvas.
25
+ *
26
+ * All mutations are:
27
+ * - Gated by readOnly (no-op when true)
28
+ * - Automatically wrapped in a history checkpoint (undo/redo)
29
+ *
30
+ * Callers don't need to worry about either concern.
31
+ */
32
+ export const useGraph = (canvasId: string = MAIN_CANVAS_ID, readOnly: boolean = false) => {
33
+ const canvasStore = getOrCreateCanvasStore(canvasId);
34
+ const nodes = canvasStore((s) => s.nodes);
35
+ const edges = canvasStore((s) => s.edges);
36
+ const { withCheckpoint } = canvasStore;
37
+
38
+ const { getNodeDefinition } = useNodeDefinitions();
39
+
40
+ // Guarded checkpoint: skips when readOnly, otherwise wraps in undo history entry
41
+ const guarded = useCallback(
42
+ <R>(operation: () => R): R | undefined => {
43
+ if (readOnly) return undefined;
44
+ return withCheckpoint(operation);
45
+ },
46
+ [readOnly, withCheckpoint],
47
+ );
48
+
49
+ const addNode = useCallback(
50
+ (nodeDef: NodeDefinition, position?: { x: number; y: number }) => {
51
+ return guarded(() => addNodeToStore(canvasStore, nodeDef, position)) ?? null;
52
+ },
53
+ [canvasStore, guarded],
54
+ );
55
+
56
+ const updateNode = useCallback(
57
+ (nodeId: string, updates: { arguments?: Record<string, unknown>; label?: string }) => {
58
+ guarded(() => updateNodeInStore(canvasStore, nodeId, updates));
59
+ },
60
+ [canvasStore, guarded],
61
+ );
62
+
63
+ const deleteNode = useCallback(
64
+ (nodeId: string) => {
65
+ guarded(() => deleteNodeFromStore(canvasStore, nodeId, getNodeDefinition));
66
+ },
67
+ [canvasStore, getNodeDefinition, guarded],
68
+ );
69
+
70
+ const onConnect = useCallback(
71
+ (connection: Connection): EdgeType | false => {
72
+ return guarded(() => connectNodesInStore(canvasStore, connection)) ?? false;
73
+ },
74
+ [canvasStore, guarded],
75
+ );
76
+
77
+ const updateEdge = useCallback(
78
+ (edgeId: string, updates: Record<string, unknown>) => {
79
+ guarded(() => updateEdgeInStore(canvasStore, edgeId, updates));
80
+ },
81
+ [canvasStore, guarded],
82
+ );
83
+
84
+ const deleteEdges = useCallback(
85
+ (edgeIds: string[]) => {
86
+ guarded(() => deleteEdgesFromStore(canvasStore, edgeIds));
87
+ },
88
+ [canvasStore, guarded],
89
+ );
90
+
91
+ // Batch delete nodes and edges as a single undo entry
92
+ const deleteSelected = useCallback(
93
+ (nodeIds: string[], edgeIds: string[]) => {
94
+ if (nodeIds.length === 0 && edgeIds.length === 0) return;
95
+ guarded(() => {
96
+ for (const nodeId of nodeIds) deleteNodeFromStore(canvasStore, nodeId, getNodeDefinition);
97
+ if (edgeIds.length > 0) deleteEdgesFromStore(canvasStore, edgeIds);
98
+ });
99
+ },
100
+ [canvasStore, getNodeDefinition, guarded],
101
+ );
102
+
103
+ // Copy is read-only — no checkpoint, no readOnly gate
104
+ const copySelection = useCallback(
105
+ (nodeIds: string[]) => {
106
+ if (nodeIds.length === 0) return;
107
+
108
+ const nodeIdSet = new Set(nodeIds);
109
+
110
+ const copiedNodes = nodes
111
+ .filter((node) => nodeIdSet.has(node.id))
112
+ .map((node) => JSON.parse(JSON.stringify(node)) as Node<NodeData>);
113
+
114
+ const copiedEdges = edges
115
+ .filter((edge) => nodeIdSet.has(edge.source) && nodeIdSet.has(edge.target))
116
+ .map((edge) => JSON.parse(JSON.stringify(edge)) as Edge<EdgeData>);
117
+
118
+ clipboardRef.current = { nodes: copiedNodes, edges: copiedEdges };
119
+ },
120
+ [nodes, edges],
121
+ );
122
+
123
+ const pasteSelection = useCallback(
124
+ (offset?: { x: number; y: number }) => {
125
+ const clipboard = clipboardRef.current;
126
+ if (!clipboard) return undefined;
127
+ return guarded(() => pasteToStore(canvasStore, clipboard, offset, getNodeDefinition));
128
+ },
129
+ [canvasStore, getNodeDefinition, guarded],
130
+ );
131
+
132
+ // Batch add node + connect as a single undo entry (used by contextual node picker)
133
+ const addNodeAndConnect = useCallback(
134
+ (nodeDef: NodeDefinition, position: { x: number; y: number }, connection: Connection): string | null => {
135
+ return (
136
+ guarded(() => {
137
+ const nodeId = addNodeToStore(canvasStore, nodeDef, position);
138
+ if (nodeId == null) return null;
139
+ connectNodesInStore(canvasStore, { ...connection, target: nodeId });
140
+ return nodeId;
141
+ }) ?? null
142
+ );
143
+ },
144
+ [canvasStore, guarded],
145
+ );
146
+
147
+ return {
148
+ nodes,
149
+ edges,
150
+ addNode,
151
+ updateNode,
152
+ updateEdge,
153
+ deleteNode,
154
+ deleteEdges,
155
+ deleteSelected,
156
+ onConnect,
157
+ addNodeAndConnect,
158
+ copySelection,
159
+ pasteSelection,
160
+ };
161
+ };