@foresthubai/workflow-builder 0.3.0 → 0.4.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/LICENSE +661 -661
- package/NOTICE +16 -16
- package/README.md +110 -93
- package/dist/components/ui/command.d.ts +2 -2
- package/dist/components/ui/input.d.ts +1 -1
- package/dist/components/ui/resizable.d.ts +1 -1
- package/dist/components/ui/textarea.d.ts +1 -1
- package/dist/graph/BaseNode.js +10 -10
- package/dist/graph/reactFlowRegistry.d.ts.map +1 -1
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +6 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/toolbars/CanvasTabsToolbar.d.ts +11 -0
- package/dist/toolbars/CanvasTabsToolbar.d.ts.map +1 -0
- package/dist/toolbars/CanvasTabsToolbar.js +101 -0
- package/dist/toolbars/CanvasTabsToolbar.js.map +1 -0
- package/package.json +2 -2
- package/src/BuilderLayout.tsx +345 -345
- package/src/Canvas.tsx +261 -261
- package/src/CanvasEditor.tsx +142 -142
- package/src/CanvasTabsToolbar.tsx +176 -176
- package/src/RightConfigPanel.tsx +266 -266
- package/src/WorkflowBuilder.tsx +412 -412
- package/src/cn.ts +6 -6
- package/src/components/ui/add-button.tsx +39 -39
- package/src/components/ui/alert-dialog.tsx +141 -141
- package/src/components/ui/alert.tsx +59 -59
- package/src/components/ui/badge.tsx +36 -36
- package/src/components/ui/button.tsx +85 -85
- package/src/components/ui/card.tsx +79 -79
- package/src/components/ui/checkbox.tsx +28 -28
- package/src/components/ui/collapsible.tsx +9 -9
- package/src/components/ui/command.tsx +153 -153
- package/src/components/ui/delete-button.tsx +23 -23
- package/src/components/ui/dialog.tsx +125 -125
- package/src/components/ui/dropdown-menu.tsx +198 -198
- package/src/components/ui/input.tsx +55 -55
- package/src/components/ui/label.tsx +24 -24
- package/src/components/ui/readonly-banner.tsx +15 -15
- package/src/components/ui/resizable.tsx +43 -43
- package/src/components/ui/scroll-area.tsx +102 -102
- package/src/components/ui/select.tsx +160 -160
- package/src/components/ui/separator.tsx +29 -29
- package/src/components/ui/switch.tsx +27 -27
- package/src/components/ui/textarea.tsx +51 -51
- package/src/components/ui/toast.tsx +127 -127
- package/src/components/ui/toaster.tsx +33 -33
- package/src/components/ui/toggle-group.tsx +59 -59
- package/src/components/ui/toggle.tsx +43 -43
- package/src/components/ui/tooltip.tsx +32 -32
- package/src/dialogs/NodePickerDialog.tsx +84 -84
- package/src/dialogs/ValidationDialog.tsx +184 -184
- package/src/graph/BaseNode.tsx +557 -557
- package/src/graph/CustomEdge.tsx +185 -185
- package/src/graph/CustomNode.tsx +16 -16
- package/src/graph/FunctionCallNode.tsx +30 -30
- package/src/graph/PortHandle.tsx +189 -189
- package/src/graph/reactFlowRegistry.ts +26 -26
- package/src/hooks/use-toast.ts +125 -125
- package/src/hooks/useAvailableVariables.ts +20 -20
- package/src/hooks/useCanvasHistory.ts +22 -22
- package/src/hooks/useCanvasTabs.ts +168 -168
- package/src/hooks/useFunctionDiagnosticsSync.ts +40 -40
- package/src/hooks/useFunctionRegistry.ts +26 -26
- package/src/hooks/useFunctions.ts +44 -44
- package/src/hooks/useGraph.ts +161 -161
- package/src/hooks/useNodeDefinitions.ts +82 -82
- package/src/hooks/useParamErrors.ts +26 -26
- package/src/hooks/useResolvedTheme.ts +30 -30
- package/src/hooks/useResourceDiagnosticsSync.ts +58 -58
- package/src/hooks/useSuppressThemeTransition.ts +79 -79
- package/src/hooks/useWorkflowSerialization.ts +127 -127
- package/src/i18n/index.ts +53 -53
- package/src/i18n/locales/de.json +501 -501
- package/src/i18n/locales/en.json +557 -557
- package/src/index.ts +27 -27
- package/src/inputs/ExpressionInput.tsx +297 -297
- package/src/inputs/ParameterEditor.tsx +515 -515
- package/src/inputs/PortSection.tsx +144 -144
- package/src/panels/BuilderSidebar.tsx +301 -301
- package/src/panels/ChannelConfigPanel.tsx +49 -49
- package/src/panels/ChannelsPanel.tsx +28 -28
- package/src/panels/DebugConsolePanel.tsx +73 -73
- package/src/panels/DebugContextPanel.tsx +77 -77
- package/src/panels/DebugExternalIOPanel.tsx +180 -180
- package/src/panels/DiagnosticsPanel.tsx +170 -170
- package/src/panels/EdgeConfigPanel.tsx +104 -104
- package/src/panels/FunctionConfigPanel.tsx +179 -179
- package/src/panels/FunctionListPanel.tsx +45 -45
- package/src/panels/MemoryConfigPanel.tsx +55 -55
- package/src/panels/MemoryPanel.tsx +40 -40
- package/src/panels/ModelConfigPanel.tsx +41 -41
- package/src/panels/ModelsPanel.tsx +36 -36
- package/src/panels/NodeConfigPanel.tsx +630 -630
- package/src/panels/NodeLibrary.tsx +288 -288
- package/src/panels/ResourceConfigPanel.tsx +132 -132
- package/src/panels/ResourceListPanel.tsx +113 -113
- package/src/panels/VariableConfigPanel.tsx +161 -161
- package/src/panels/VariablesPanel.tsx +145 -145
- package/src/stores/canvasStore.test.ts +44 -44
- package/src/stores/canvasStore.ts +245 -245
- package/src/stores/debugStore.ts +74 -74
- package/src/stores/diagnosticsStore.ts +130 -130
- package/src/stores/editorStore.ts +202 -202
- package/src/styles/index.css +526 -526
- package/src/utils/categoryConstants.ts +26 -26
- package/src/utils/channelOperations.ts +86 -86
- package/src/utils/connectionRules.ts +137 -137
- package/src/utils/functionOperations.ts +179 -179
- package/src/utils/graphOperations.ts +550 -550
- package/src/utils/history.ts +207 -207
- package/src/utils/memoryOperations.ts +57 -57
- package/src/utils/migrateFunctionNodes.ts +107 -107
- package/src/utils/modelOperations.ts +55 -55
- package/src/utils/paramDisplay.ts +71 -71
- package/src/utils/resourceHelpers.ts +32 -32
- package/src/utils/translation.ts +28 -28
- package/src/utils/variableOperations.ts +75 -75
- 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
|
+
};
|
package/src/hooks/useGraph.ts
CHANGED
|
@@ -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
|
+
};
|