@d34dman/flowdrop 0.0.61 → 0.0.63
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 +6 -0
- package/dist/adapters/WorkflowAdapter.d.ts +1 -1
- package/dist/adapters/agentspec/AgentSpecAdapter.js +3 -1
- package/dist/api/client.d.ts +4 -0
- package/dist/api/client.js +6 -1
- package/dist/api/enhanced-client.js +7 -6
- package/dist/components/App.svelte +143 -219
- package/dist/components/CanvasBanner.stories.svelte +25 -0
- package/dist/components/CanvasBanner.stories.svelte.d.ts +27 -0
- package/dist/components/CanvasBanner.svelte +2 -2
- package/dist/components/ConfigForm.svelte +37 -36
- package/dist/components/ConfigPanel.stories.svelte +38 -0
- package/dist/components/ConfigPanel.stories.svelte.d.ts +27 -0
- package/dist/components/ConfigPanel.svelte +2 -2
- package/dist/components/ConnectionLine.svelte +2 -2
- package/dist/components/FlowDropZone.svelte +18 -2
- package/dist/components/FlowDropZone.svelte.d.ts +2 -0
- package/dist/components/LoadingSpinner.stories.svelte +30 -0
- package/dist/components/LoadingSpinner.stories.svelte.d.ts +27 -0
- package/dist/components/Logo.stories.svelte +22 -0
- package/dist/components/Logo.stories.svelte.d.ts +27 -0
- package/dist/components/Logo.svelte +33 -13
- package/dist/components/Logo.svelte.d.ts +1 -1
- package/dist/components/MarkdownDisplay.stories.svelte +21 -0
- package/dist/components/MarkdownDisplay.stories.svelte.d.ts +27 -0
- package/dist/components/MarkdownDisplay.svelte +4 -3
- package/dist/components/Navbar.stories.svelte +41 -0
- package/dist/components/Navbar.stories.svelte.d.ts +27 -0
- package/dist/components/Navbar.svelte +4 -4
- package/dist/components/NodeSidebar.svelte +12 -12
- package/dist/components/NodeStatusOverlay.stories.svelte +74 -0
- package/dist/components/NodeStatusOverlay.stories.svelte.d.ts +27 -0
- package/dist/components/PipelineStatus.svelte +11 -4
- package/dist/components/PortCoordinateTracker.svelte +1 -1
- package/dist/components/SchemaForm.stories.svelte +101 -0
- package/dist/components/SchemaForm.stories.svelte.d.ts +27 -0
- package/dist/components/SchemaForm.svelte +17 -12
- package/dist/components/SettingsModal.svelte +3 -3
- package/dist/components/SettingsPanel.svelte +23 -22
- package/dist/components/StatusIcon.stories.svelte +60 -0
- package/dist/components/StatusIcon.stories.svelte.d.ts +27 -0
- package/dist/components/StatusIcon.svelte +7 -0
- package/dist/components/StatusLabel.stories.svelte +17 -0
- package/dist/components/StatusLabel.stories.svelte.d.ts +27 -0
- package/dist/components/ThemeToggle.stories.svelte +25 -0
- package/dist/components/ThemeToggle.stories.svelte.d.ts +27 -0
- package/dist/components/ThemeToggle.svelte +8 -8
- package/dist/components/UniversalNode.svelte +1 -1
- package/dist/components/WorkflowEditor.svelte +298 -298
- package/dist/components/form/FormAutocomplete.svelte +20 -19
- package/dist/components/form/FormCheckboxGroup.stories.svelte +28 -0
- package/dist/components/form/FormCheckboxGroup.stories.svelte.d.ts +27 -0
- package/dist/components/form/FormField.svelte +3 -3
- package/dist/components/form/FormFieldLight.svelte +2 -2
- package/dist/components/form/FormFieldWrapper.stories.svelte +31 -0
- package/dist/components/form/FormFieldWrapper.stories.svelte.d.ts +27 -0
- package/dist/components/form/FormFieldset.svelte +7 -7
- package/dist/components/form/FormNumberField.stories.svelte +33 -0
- package/dist/components/form/FormNumberField.stories.svelte.d.ts +27 -0
- package/dist/components/form/FormRangeField.stories.svelte +31 -0
- package/dist/components/form/FormRangeField.stories.svelte.d.ts +27 -0
- package/dist/components/form/FormSelect.stories.svelte +50 -0
- package/dist/components/form/FormSelect.stories.svelte.d.ts +27 -0
- package/dist/components/form/FormTemplateEditor.svelte +2 -1
- package/dist/components/form/FormTextField.stories.svelte +30 -0
- package/dist/components/form/FormTextField.stories.svelte.d.ts +27 -0
- package/dist/components/form/FormTextarea.stories.svelte +31 -0
- package/dist/components/form/FormTextarea.stories.svelte.d.ts +27 -0
- package/dist/components/form/FormToggle.stories.svelte +30 -0
- package/dist/components/form/FormToggle.stories.svelte.d.ts +27 -0
- package/dist/components/form/FormUISchemaRenderer.svelte +1 -1
- package/dist/components/form/types.d.ts +15 -47
- package/dist/components/interrupt/ChoicePrompt.stories.svelte +43 -0
- package/dist/components/interrupt/ChoicePrompt.stories.svelte.d.ts +27 -0
- package/dist/components/interrupt/ChoicePrompt.svelte +24 -24
- package/dist/components/interrupt/ConfirmationPrompt.stories.svelte +49 -0
- package/dist/components/interrupt/ConfirmationPrompt.stories.svelte.d.ts +27 -0
- package/dist/components/interrupt/ConfirmationPrompt.svelte +19 -19
- package/dist/components/interrupt/FormPrompt.svelte +15 -15
- package/dist/components/interrupt/InterruptBubble.svelte +202 -236
- package/dist/components/interrupt/InterruptBubble.svelte.d.ts +1 -1
- package/dist/components/interrupt/ReviewPrompt.stories.svelte +46 -0
- package/dist/components/interrupt/ReviewPrompt.stories.svelte.d.ts +27 -0
- package/dist/components/interrupt/ReviewPrompt.svelte +842 -0
- package/dist/components/interrupt/ReviewPrompt.svelte.d.ts +23 -0
- package/dist/components/interrupt/TextInputPrompt.stories.svelte +34 -0
- package/dist/components/interrupt/TextInputPrompt.stories.svelte.d.ts +27 -0
- package/dist/components/interrupt/TextInputPrompt.svelte +21 -21
- package/dist/components/nodes/GatewayNode.stories.svelte +76 -0
- package/dist/components/nodes/GatewayNode.stories.svelte.d.ts +26 -0
- package/dist/components/nodes/GatewayNode.svelte +19 -17
- package/dist/components/nodes/IdeaNode.stories.svelte +48 -0
- package/dist/components/nodes/IdeaNode.stories.svelte.d.ts +26 -0
- package/dist/components/nodes/IdeaNode.svelte +10 -26
- package/dist/components/nodes/NotesNode.stories.svelte +69 -0
- package/dist/components/nodes/NotesNode.stories.svelte.d.ts +26 -0
- package/dist/components/nodes/NotesNode.svelte +8 -8
- package/dist/components/nodes/SimpleNode.stories.svelte +101 -0
- package/dist/components/nodes/SimpleNode.stories.svelte.d.ts +26 -0
- package/dist/components/nodes/SimpleNode.svelte +16 -24
- package/dist/components/nodes/SquareNode.stories.svelte +56 -0
- package/dist/components/nodes/SquareNode.stories.svelte.d.ts +26 -0
- package/dist/components/nodes/SquareNode.svelte +13 -21
- package/dist/components/nodes/TerminalNode.stories.svelte +25 -0
- package/dist/components/nodes/TerminalNode.stories.svelte.d.ts +26 -0
- package/dist/components/nodes/TerminalNode.svelte +7 -7
- package/dist/components/nodes/ToolNode.stories.svelte +71 -0
- package/dist/components/nodes/ToolNode.stories.svelte.d.ts +26 -0
- package/dist/components/nodes/ToolNode.svelte +7 -15
- package/dist/components/nodes/WorkflowNode.stories.svelte +50 -0
- package/dist/components/nodes/WorkflowNode.stories.svelte.d.ts +26 -0
- package/dist/components/nodes/WorkflowNode.svelte +13 -13
- package/dist/components/playground/ChatPanel.svelte +48 -48
- package/dist/components/playground/ExecutionLogs.svelte +23 -23
- package/dist/components/playground/InputCollector.svelte +24 -24
- package/dist/components/playground/MessageBubble.stories.svelte +49 -0
- package/dist/components/playground/MessageBubble.stories.svelte.d.ts +27 -0
- package/dist/components/playground/MessageBubble.svelte +49 -46
- package/dist/components/playground/Playground.svelte +194 -129
- package/dist/components/playground/PlaygroundModal.svelte +5 -5
- package/dist/components/playground/SessionManager.svelte +26 -26
- package/dist/config/constants.d.ts +22 -0
- package/dist/config/constants.js +22 -0
- package/dist/config/endpoints.d.ts +19 -0
- package/dist/config/runtimeConfig.js +2 -1
- package/dist/core/index.d.ts +5 -2
- package/dist/core/index.js +9 -1
- package/dist/editor/index.d.ts +13 -9
- package/dist/editor/index.js +15 -11
- package/dist/form/code.d.ts +2 -1
- package/dist/form/code.js +1 -3
- package/dist/form/markdown.d.ts +2 -1
- package/dist/form/markdown.js +1 -3
- package/dist/helpers/workflowEditorHelper.js +18 -33
- package/dist/mocks/app-forms.js +1 -0
- package/dist/mocks/app-navigation.js +3 -1
- package/dist/mocks/app-stores.d.ts +4 -4
- package/dist/playground/index.d.ts +4 -3
- package/dist/playground/index.js +12 -10
- package/dist/playground/mount.js +6 -13
- package/dist/services/agentSpecExecutionService.js +2 -1
- package/dist/services/api.js +10 -18
- package/dist/services/apiVariableService.js +2 -1
- package/dist/services/autoSaveService.d.ts +3 -3
- package/dist/services/autoSaveService.js +21 -17
- package/dist/services/categoriesApi.js +13 -5
- package/dist/services/draftStorage.js +5 -4
- package/dist/services/dynamicSchemaService.js +4 -4
- package/dist/services/globalSave.d.ts +60 -11
- package/dist/services/globalSave.js +160 -83
- package/dist/services/historyService.d.ts +2 -1
- package/dist/services/historyService.js +7 -3
- package/dist/services/interruptService.js +9 -8
- package/dist/services/nodeExecutionService.js +14 -6
- package/dist/services/playgroundService.js +2 -1
- package/dist/services/portConfigApi.js +11 -7
- package/dist/services/toastService.d.ts +1 -1
- package/dist/services/toastService.js +6 -5
- package/dist/services/variableService.js +3 -2
- package/dist/settings/index.d.ts +1 -1
- package/dist/settings/index.js +1 -1
- package/dist/stores/{categoriesStore.d.ts → categoriesStore.svelte.d.ts} +3 -3
- package/dist/stores/{categoriesStore.js → categoriesStore.svelte.js} +15 -18
- package/dist/stores/editorStateMachine.svelte.d.ts +42 -0
- package/dist/stores/editorStateMachine.svelte.js +132 -0
- package/dist/stores/{historyStore.d.ts → historyStore.svelte.d.ts} +18 -15
- package/dist/stores/{historyStore.js → historyStore.svelte.js} +40 -21
- package/dist/stores/{interruptStore.d.ts → interruptStore.svelte.d.ts} +16 -15
- package/dist/stores/{interruptStore.js → interruptStore.svelte.js} +85 -94
- package/dist/stores/{playgroundStore.d.ts → playgroundStore.svelte.d.ts} +41 -33
- package/dist/stores/{playgroundStore.js → playgroundStore.svelte.js} +164 -84
- package/dist/stores/{portCoordinateStore.d.ts → portCoordinateStore.svelte.d.ts} +10 -4
- package/dist/stores/{portCoordinateStore.js → portCoordinateStore.svelte.js} +38 -35
- package/dist/stores/{settingsStore.d.ts → settingsStore.svelte.d.ts} +45 -28
- package/dist/stores/{settingsStore.js → settingsStore.svelte.js} +169 -128
- package/dist/stores/{workflowStore.d.ts → workflowStore.svelte.d.ts} +101 -65
- package/dist/stores/{workflowStore.js → workflowStore.svelte.js} +285 -239
- package/dist/stories/CanvasDecorator.svelte +50 -0
- package/dist/stories/CanvasDecorator.svelte.d.ts +8 -0
- package/dist/stories/NodeDecorator.svelte +74 -0
- package/dist/stories/NodeDecorator.svelte.d.ts +8 -0
- package/dist/stories/utils.d.ts +93 -0
- package/dist/stories/utils.js +122 -0
- package/dist/styles/base.css +114 -61
- package/dist/styles/toast.css +2 -2
- package/dist/styles/tokens.css +250 -185
- package/dist/svelte-app.d.ts +0 -6
- package/dist/svelte-app.js +13 -31
- package/dist/types/index.d.ts +2 -0
- package/dist/types/interrupt.d.ts +89 -5
- package/dist/types/interrupt.js +13 -1
- package/dist/types/playground.d.ts +5 -0
- package/dist/types/settings.js +1 -1
- package/dist/utils/colors.js +4 -4
- package/dist/utils/connections.js +33 -8
- package/dist/utils/icons.js +1 -1
- package/dist/utils/logger.d.ts +47 -0
- package/dist/utils/logger.js +72 -0
- package/dist/utils/nodeWrapper.js +1 -1
- package/dist/utils/sanitize.d.ts +19 -0
- package/dist/utils/sanitize.js +31 -0
- package/dist/utils/validation.d.ts +29 -0
- package/dist/utils/validation.js +39 -0
- package/package.json +243 -232
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
type ColorMode
|
|
17
17
|
} from '@xyflow/svelte';
|
|
18
18
|
import '@xyflow/svelte/dist/style.css';
|
|
19
|
-
import {
|
|
19
|
+
import { getResolvedTheme, getEditorSettings, getBehaviorSettings } from '../stores/settingsStore.svelte.js';
|
|
20
20
|
import type {
|
|
21
21
|
WorkflowNode as WorkflowNodeType,
|
|
22
22
|
NodeMetadata,
|
|
@@ -26,11 +26,11 @@
|
|
|
26
26
|
import CanvasBanner from './CanvasBanner.svelte';
|
|
27
27
|
import FlowDropZone from './FlowDropZone.svelte';
|
|
28
28
|
import EdgeRefresher from './EdgeRefresher.svelte';
|
|
29
|
-
import { tick } from 'svelte';
|
|
29
|
+
import { tick, untrack } from 'svelte';
|
|
30
30
|
import type { EndpointConfig } from '../config/endpoints.js';
|
|
31
31
|
import ConnectionLine from './ConnectionLine.svelte';
|
|
32
|
-
import {
|
|
33
|
-
import { historyActions, setOnRestoreCallback } from '../stores/historyStore.js';
|
|
32
|
+
import { getWorkflowStore, workflowActions } from '../stores/workflowStore.svelte.js';
|
|
33
|
+
import { historyActions, setOnRestoreCallback } from '../stores/historyStore.svelte.js';
|
|
34
34
|
import UniversalNode from './UniversalNode.svelte';
|
|
35
35
|
import {
|
|
36
36
|
EdgeStylingHelper,
|
|
@@ -39,15 +39,17 @@
|
|
|
39
39
|
ConfigurationHelper
|
|
40
40
|
} from '../helpers/workflowEditorHelper.js';
|
|
41
41
|
import type { NodeExecutionInfo } from '../types/index.js';
|
|
42
|
-
import { areNodeArraysEqual, areEdgeArraysEqual, throttle } from '../utils/performanceUtils.js';
|
|
43
42
|
import { Toaster } from 'svelte-5-french-toast';
|
|
44
|
-
import { flowdropToastOptions, FLOWDROP_TOASTER_CLASS } from '../services/toastService.js';
|
|
43
|
+
import { flowdropToastOptions, FLOWDROP_TOASTER_CLASS, apiToasts } from '../services/toastService.js';
|
|
45
44
|
import {
|
|
46
45
|
ProximityConnectHelper,
|
|
47
46
|
type ProximityEdgeCandidate
|
|
48
47
|
} from '../helpers/proximityConnect.js';
|
|
49
48
|
import PortCoordinateTracker from './PortCoordinateTracker.svelte';
|
|
50
|
-
import { getPortCoordinateSnapshot } from '../stores/portCoordinateStore.js';
|
|
49
|
+
import { getPortCoordinateSnapshot } from '../stores/portCoordinateStore.svelte.js';
|
|
50
|
+
import { logger } from '../utils/logger.js';
|
|
51
|
+
import { validateWorkflowData } from '../utils/validation.js';
|
|
52
|
+
import { createEditorStateMachine } from '../stores/editorStateMachine.svelte.js';
|
|
51
53
|
|
|
52
54
|
interface Props {
|
|
53
55
|
nodes?: NodeMetadata[];
|
|
@@ -68,11 +70,19 @@
|
|
|
68
70
|
|
|
69
71
|
let props: Props = $props();
|
|
70
72
|
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
//
|
|
75
|
-
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Editor State Machine
|
|
75
|
+
// Centralizes reactive guards — replaces scattered boolean flags
|
|
76
|
+
// (isDraggingNode, lastEditorStoreValue identity checks, etc.)
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
const machine = createEditorStateMachine();
|
|
79
|
+
|
|
80
|
+
// Dev-mode transition logging
|
|
81
|
+
if (import.meta.env?.DEV) {
|
|
82
|
+
machine.onTransition((from, event, to) => {
|
|
83
|
+
logger.debug(`[EditorFSM] ${from} --${event}--> ${to}`);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
76
86
|
|
|
77
87
|
// Proximity connect state
|
|
78
88
|
let currentProximityCandidates = $state<ProximityEdgeCandidate[]>([]);
|
|
@@ -81,259 +91,228 @@
|
|
|
81
91
|
let portCoordNodeToUpdate = $state<WorkflowNodeType | null>(null);
|
|
82
92
|
let portCoordRebuildTrigger = $state(0);
|
|
83
93
|
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
let
|
|
90
|
-
|
|
91
|
-
// Initialize currentWorkflow from global store
|
|
92
|
-
// Sync on workflow ID change (new workflow loaded) or external programmatic changes
|
|
93
|
-
$effect(() => {
|
|
94
|
-
if ($workflowStore) {
|
|
95
|
-
const storeWorkflowId = $workflowStore.id;
|
|
96
|
-
|
|
97
|
-
if (currentWorkflowId !== storeWorkflowId) {
|
|
98
|
-
// New workflow loaded
|
|
99
|
-
currentWorkflow = $workflowStore;
|
|
100
|
-
currentWorkflowId = storeWorkflowId;
|
|
101
|
-
lastEditorStoreValue = null;
|
|
102
|
-
} else if ($workflowStore !== lastEditorStoreValue) {
|
|
103
|
-
// External programmatic change (e.g. addEdge, updateNode, updateEdges)
|
|
104
|
-
// The store value differs from what this editor last wrote, so sync it
|
|
105
|
-
currentWorkflow = $workflowStore;
|
|
106
|
-
}
|
|
107
|
-
} else if (currentWorkflow !== null) {
|
|
108
|
-
// Store was cleared
|
|
109
|
-
currentWorkflow = null;
|
|
110
|
-
currentWorkflowId = null;
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
// Set up the history restore callback to update workflow when undo/redo is triggered
|
|
115
|
-
$effect(() => {
|
|
116
|
-
setOnRestoreCallback((restoredWorkflow: Workflow) => {
|
|
117
|
-
// Directly update local state (bypass store sync effect)
|
|
118
|
-
currentWorkflow = restoredWorkflow;
|
|
119
|
-
// Mark as our own write so sync effect doesn't re-process it
|
|
120
|
-
lastEditorStoreValue = restoredWorkflow;
|
|
121
|
-
// Also update the store without triggering history
|
|
122
|
-
workflowActions.restoreFromHistory(restoredWorkflow);
|
|
123
|
-
});
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Flow state — bound to SvelteFlow via bind:nodes / bind:edges
|
|
96
|
+
// These are $state.raw to prevent deep proxy leaking (SvelteFlow mutates
|
|
97
|
+
// node internals during drag which would cause infinite loops with $state).
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
let flowNodes = $state.raw<WorkflowNodeType[]>([]);
|
|
100
|
+
let flowEdges = $state.raw<WorkflowEdge[]>([]);
|
|
124
101
|
|
|
125
|
-
|
|
126
|
-
return () => {
|
|
127
|
-
setOnRestoreCallback(null);
|
|
128
|
-
};
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// Create local reactive variables that sync with currentWorkflow
|
|
132
|
-
let flowNodes = $state<WorkflowNodeType[]>([]);
|
|
133
|
-
let flowEdges = $state<WorkflowEdge[]>([]);
|
|
134
|
-
|
|
135
|
-
// Sync local state with currentWorkflow
|
|
102
|
+
// Execution info loading state
|
|
136
103
|
let loadExecutionInfoTimeout: number | null = null;
|
|
137
104
|
let executionInfoAbortController: AbortController | null = null;
|
|
138
|
-
// Track previous workflow ID to detect when we need to reload execution info
|
|
139
|
-
let previousWorkflowId: string | null = null;
|
|
140
|
-
let previousPipelineId: string | undefined = undefined;
|
|
141
105
|
|
|
142
106
|
/**
|
|
143
|
-
* Key for SvelteFlow component
|
|
144
|
-
*
|
|
107
|
+
* Key for SvelteFlow component — changes when workflow ID changes.
|
|
108
|
+
* Forces SvelteFlow to remount with fresh state, allowing fitView to work correctly.
|
|
145
109
|
*/
|
|
146
|
-
let svelteFlowKey = $derived(
|
|
110
|
+
let svelteFlowKey = $derived(getWorkflowStore()?.id ?? 'default');
|
|
147
111
|
|
|
148
112
|
/**
|
|
149
113
|
* Derive snap grid configuration from editor settings
|
|
150
|
-
* Returns [gridSize, gridSize] tuple when snapToGrid is enabled, undefined otherwise
|
|
151
114
|
*/
|
|
152
115
|
let snapGrid = $derived(
|
|
153
|
-
|
|
154
|
-
? ([
|
|
116
|
+
getEditorSettings().snapToGrid
|
|
117
|
+
? ([getEditorSettings().gridSize, getEditorSettings().gridSize] as [number, number])
|
|
155
118
|
: undefined
|
|
156
119
|
);
|
|
157
120
|
|
|
158
121
|
/**
|
|
159
122
|
* Derive initial viewport configuration from editor settings
|
|
160
|
-
* Sets initial zoom level based on user preferences
|
|
161
123
|
*/
|
|
162
124
|
let initialViewport = $derived({
|
|
163
|
-
zoom:
|
|
125
|
+
zoom: getEditorSettings().defaultZoom,
|
|
164
126
|
x: 0,
|
|
165
127
|
y: 0
|
|
166
128
|
});
|
|
167
129
|
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Helper: derive flowNodes/flowEdges from a Workflow object
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
function buildFlowNodesFromStore(workflow: Workflow): {
|
|
134
|
+
nodes: WorkflowNodeType[];
|
|
135
|
+
edges: WorkflowEdge[];
|
|
136
|
+
} {
|
|
137
|
+
const nodesWithCallbacks = workflow.nodes.map((node) => ({
|
|
138
|
+
...node,
|
|
139
|
+
data: {
|
|
140
|
+
...node.data,
|
|
141
|
+
onConfigOpen: props.openConfigSidebar
|
|
142
|
+
}
|
|
143
|
+
}));
|
|
144
|
+
const styledEdges = EdgeStylingHelper.updateEdgeStyles(workflow.edges, nodesWithCallbacks);
|
|
145
|
+
return { nodes: nodesWithCallbacks, edges: styledEdges };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Helper: sync current flowNodes/flowEdges back to the global store
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
function syncFlowToStore(): void {
|
|
152
|
+
const storeValue = untrack(() => getWorkflowStore());
|
|
153
|
+
if (!storeValue) return;
|
|
154
|
+
const updatedWorkflow = WorkflowOperationsHelper.updateWorkflow(
|
|
155
|
+
storeValue,
|
|
156
|
+
flowNodes,
|
|
157
|
+
flowEdges
|
|
158
|
+
);
|
|
159
|
+
workflowActions.updateWorkflow(updatedWorkflow);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Single sync effect: workflowStore → flowNodes / flowEdges
|
|
164
|
+
// Replaces the old Effect A (store→currentWorkflow) + Effect B (currentWorkflow→flow).
|
|
165
|
+
// Suppressed during operations via state machine; handlers update flowNodes directly.
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
let previousSyncedWorkflowId: string | null = null;
|
|
168
|
+
|
|
168
169
|
$effect(() => {
|
|
169
|
-
|
|
170
|
-
const nodesWithCallbacks = currentWorkflow.nodes.map((node) => ({
|
|
171
|
-
...node,
|
|
172
|
-
data: {
|
|
173
|
-
...node.data,
|
|
174
|
-
onConfigOpen: props.openConfigSidebar
|
|
175
|
-
}
|
|
176
|
-
}));
|
|
177
|
-
flowNodes = nodesWithCallbacks;
|
|
170
|
+
const storeValue = getWorkflowStore();
|
|
178
171
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
// Note: Using Date.now() instead of ++ to avoid reading the old value,
|
|
189
|
-
// which would make this effect depend on portCoordRebuildTrigger and loop.
|
|
190
|
-
if ($editorSettings.proximityConnect) {
|
|
191
|
-
portCoordRebuildTrigger = Date.now();
|
|
172
|
+
// Suppressed during operations — handlers write to flowNodes directly
|
|
173
|
+
if (untrack(() => machine.permissions.suppressEffect)) return;
|
|
174
|
+
|
|
175
|
+
if (!storeValue) {
|
|
176
|
+
if (flowNodes.length > 0 || flowEdges.length > 0) {
|
|
177
|
+
flowNodes = [];
|
|
178
|
+
flowEdges = [];
|
|
179
|
+
previousSyncedWorkflowId = null;
|
|
180
|
+
untrack(() => machine.send('WORKFLOW_CLEARED'));
|
|
192
181
|
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
193
184
|
|
|
194
|
-
|
|
195
|
-
// and if the workflow or pipeline has changed
|
|
196
|
-
const workflowChanged = currentWorkflow.id !== previousWorkflowId;
|
|
197
|
-
const pipelineChanged = props.pipelineId !== previousPipelineId;
|
|
185
|
+
const isNewWorkflow = storeValue.id !== previousSyncedWorkflowId;
|
|
198
186
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
187
|
+
if (isNewWorkflow) {
|
|
188
|
+
untrack(() =>
|
|
189
|
+
machine.send(previousSyncedWorkflowId ? 'WORKFLOW_SWITCHED' : 'WORKFLOW_LOADED')
|
|
190
|
+
);
|
|
191
|
+
}
|
|
205
192
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
193
|
+
// Derive flowNodes/flowEdges from store
|
|
194
|
+
const derived = buildFlowNodesFromStore(storeValue);
|
|
195
|
+
flowNodes = derived.nodes;
|
|
196
|
+
flowEdges = derived.edges;
|
|
197
|
+
previousSyncedWorkflowId = storeValue.id;
|
|
211
198
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
() => {
|
|
220
|
-
loadNodeExecutionInfo();
|
|
221
|
-
},
|
|
222
|
-
{ timeout: 500 }
|
|
223
|
-
) as unknown as number;
|
|
224
|
-
} else {
|
|
225
|
-
// Fallback to setTimeout with longer delay for better performance
|
|
226
|
-
loadExecutionInfoTimeout = setTimeout(() => {
|
|
227
|
-
loadNodeExecutionInfo();
|
|
228
|
-
}, 300) as unknown as number;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
199
|
+
// Trigger port coordinate rebuild after workflow load
|
|
200
|
+
if (getEditorSettings().proximityConnect) {
|
|
201
|
+
portCoordRebuildTrigger = Date.now();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (isNewWorkflow) {
|
|
205
|
+
untrack(() => machine.send('LOAD_COMPLETE'));
|
|
231
206
|
}
|
|
232
207
|
});
|
|
233
208
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// Execution info effect (separate — async, depends on workflow + pipeline ID)
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
let previousExecWorkflowId: string | null = null;
|
|
213
|
+
let previousExecPipelineId: string | undefined = undefined;
|
|
214
|
+
|
|
215
|
+
$effect(() => {
|
|
216
|
+
const storeValue = getWorkflowStore();
|
|
217
|
+
const pipelineId = props.pipelineId;
|
|
218
|
+
|
|
219
|
+
if (!storeValue || !pipelineId) return;
|
|
220
|
+
|
|
221
|
+
const workflowChanged = storeValue.id !== previousExecWorkflowId;
|
|
222
|
+
const pipelineChanged = pipelineId !== previousExecPipelineId;
|
|
223
|
+
|
|
224
|
+
if (!workflowChanged && !pipelineChanged) return;
|
|
225
|
+
|
|
226
|
+
previousExecWorkflowId = storeValue.id;
|
|
227
|
+
previousExecPipelineId = pipelineId;
|
|
228
|
+
|
|
229
|
+
// Cancel any pending timeout / in-flight request
|
|
230
|
+
if (loadExecutionInfoTimeout) {
|
|
231
|
+
clearTimeout(loadExecutionInfoTimeout);
|
|
232
|
+
loadExecutionInfoTimeout = null;
|
|
243
233
|
}
|
|
244
|
-
|
|
234
|
+
if (executionInfoAbortController) {
|
|
235
|
+
executionInfoAbortController.abort();
|
|
236
|
+
executionInfoAbortController = null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Schedule loading with requestIdleCallback (falls back to setTimeout)
|
|
240
|
+
if (typeof requestIdleCallback !== 'undefined') {
|
|
241
|
+
loadExecutionInfoTimeout = requestIdleCallback(
|
|
242
|
+
() => {
|
|
243
|
+
loadNodeExecutionInfo();
|
|
244
|
+
},
|
|
245
|
+
{ timeout: 500 }
|
|
246
|
+
) as unknown as number;
|
|
247
|
+
} else {
|
|
248
|
+
loadExecutionInfoTimeout = setTimeout(() => {
|
|
249
|
+
loadNodeExecutionInfo();
|
|
250
|
+
}, 300) as unknown as number;
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// History restore callback
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
$effect(() => {
|
|
258
|
+
setOnRestoreCallback((restoredWorkflow: Workflow) => {
|
|
259
|
+
machine.send('START_RESTORE');
|
|
260
|
+
// Update the store (effect is suppressed during 'restoring')
|
|
261
|
+
workflowActions.restoreFromHistory(restoredWorkflow);
|
|
262
|
+
// Derive flowNodes/flowEdges directly for immediate visual update
|
|
263
|
+
const derived = buildFlowNodesFromStore(restoredWorkflow);
|
|
264
|
+
flowNodes = derived.nodes;
|
|
265
|
+
flowEdges = derived.edges;
|
|
266
|
+
machine.send('RESTORE_COMPLETE');
|
|
267
|
+
// After RESTORE_COMPLETE → idle, the sync effect runs but produces
|
|
268
|
+
// the same data (no-op re-derive).
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
return () => {
|
|
272
|
+
setOnRestoreCallback(null);
|
|
273
|
+
};
|
|
274
|
+
});
|
|
245
275
|
|
|
246
276
|
/**
|
|
247
277
|
* Load node execution information for all nodes in the workflow
|
|
248
|
-
* Optimized to reduce processing time and prevent blocking the main thread
|
|
249
278
|
*/
|
|
250
279
|
async function loadNodeExecutionInfo(): Promise<void> {
|
|
251
|
-
|
|
280
|
+
const workflow = untrack(() => getWorkflowStore());
|
|
281
|
+
if (!workflow?.nodes || !props.pipelineId) return;
|
|
252
282
|
|
|
253
283
|
try {
|
|
254
|
-
// Create abort controller for this request
|
|
255
284
|
executionInfoAbortController = new AbortController();
|
|
256
285
|
|
|
257
|
-
// Fetch execution info with abort signal
|
|
258
286
|
const executionInfo = await NodeOperationsHelper.loadNodeExecutionInfo(
|
|
259
|
-
|
|
287
|
+
workflow,
|
|
260
288
|
props.pipelineId
|
|
261
289
|
);
|
|
262
290
|
|
|
263
|
-
|
|
264
|
-
if (executionInfoAbortController?.signal.aborted) {
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
291
|
+
if (executionInfoAbortController?.signal.aborted) return;
|
|
267
292
|
|
|
268
|
-
// Default execution info for nodes without data
|
|
269
293
|
const defaultExecutionInfo: NodeExecutionInfo = {
|
|
270
294
|
status: 'idle' as const,
|
|
271
295
|
executionCount: 0,
|
|
272
296
|
isExecuting: false
|
|
273
297
|
};
|
|
274
298
|
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
const updatedNodes = currentWorkflow.nodes.map((node) => ({
|
|
299
|
+
// Update flowNodes with execution info (visual-only, no store sync needed)
|
|
300
|
+
flowNodes = flowNodes.map((node) => ({
|
|
278
301
|
...node,
|
|
279
302
|
data: {
|
|
280
303
|
...node.data,
|
|
281
|
-
executionInfo: executionInfo[node.id] || defaultExecutionInfo
|
|
282
|
-
onConfigOpen: props.openConfigSidebar
|
|
304
|
+
executionInfo: executionInfo[node.id] || defaultExecutionInfo
|
|
283
305
|
}
|
|
284
306
|
}));
|
|
285
307
|
|
|
286
|
-
// Update state in a single operation
|
|
287
|
-
flowNodes = updatedNodes;
|
|
288
|
-
currentWorkflow.nodes = updatedNodes;
|
|
289
|
-
|
|
290
|
-
// Clear abort controller
|
|
291
308
|
executionInfoAbortController = null;
|
|
292
309
|
} catch (error) {
|
|
293
|
-
// Only log if it's not an abort error
|
|
294
310
|
if (error instanceof Error && error.name !== 'AbortError') {
|
|
295
|
-
|
|
311
|
+
logger.error('Failed to load node execution info:', error);
|
|
296
312
|
}
|
|
297
313
|
}
|
|
298
314
|
}
|
|
299
315
|
|
|
300
|
-
// Function to update currentWorkflow when SvelteFlow changes nodes/edges
|
|
301
|
-
function updateCurrentWorkflowFromSvelteFlow(): void {
|
|
302
|
-
if (currentWorkflow) {
|
|
303
|
-
currentWorkflow = WorkflowOperationsHelper.updateWorkflow(
|
|
304
|
-
currentWorkflow,
|
|
305
|
-
flowNodes,
|
|
306
|
-
flowEdges
|
|
307
|
-
);
|
|
308
|
-
|
|
309
|
-
// Update the global store
|
|
310
|
-
updateGlobalStore();
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Track previous values to detect changes from SvelteFlow
|
|
315
|
-
let previousNodes = $state<WorkflowNodeType[]>([]);
|
|
316
|
-
let previousEdges = $state<WorkflowEdge[]>([]);
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Watch for changes from SvelteFlow and update currentWorkflow
|
|
320
|
-
* Uses efficient comparison instead of expensive JSON.stringify
|
|
321
|
-
* This reduces event handler time from 290-310ms to <50ms
|
|
322
|
-
*/
|
|
323
|
-
$effect(() => {
|
|
324
|
-
// Check if nodes have changed from SvelteFlow using fast comparison
|
|
325
|
-
const nodesChanged = !areNodeArraysEqual(flowNodes, previousNodes);
|
|
326
|
-
const edgesChanged = !areEdgeArraysEqual(flowEdges, previousEdges);
|
|
327
|
-
|
|
328
|
-
if ((nodesChanged || edgesChanged) && currentWorkflow) {
|
|
329
|
-
updateCurrentWorkflowFromSvelteFlow();
|
|
330
|
-
|
|
331
|
-
// Update previous values with shallow copies
|
|
332
|
-
previousNodes = [...flowNodes];
|
|
333
|
-
previousEdges = [...flowEdges];
|
|
334
|
-
}
|
|
335
|
-
});
|
|
336
|
-
|
|
337
316
|
// The global store should be initialized by the parent App component
|
|
338
317
|
|
|
339
318
|
// Sidebar is now always visible - removed toggle functionality
|
|
@@ -350,10 +329,12 @@
|
|
|
350
329
|
/**
|
|
351
330
|
* Handle node drag start
|
|
352
331
|
*
|
|
353
|
-
*
|
|
332
|
+
* Transitions the state machine to 'dragging', which suppresses
|
|
333
|
+
* the sync effect to prevent reactive loops during high-frequency
|
|
334
|
+
* position updates. SvelteFlow mutates flowNodes directly via bind:nodes.
|
|
354
335
|
*/
|
|
355
336
|
function handleNodeDragStart(): void {
|
|
356
|
-
|
|
337
|
+
machine.send('START_DRAG');
|
|
357
338
|
// Clear any leftover proximity previews
|
|
358
339
|
currentProximityCandidates = [];
|
|
359
340
|
}
|
|
@@ -370,7 +351,7 @@
|
|
|
370
351
|
nodes: WorkflowNodeType[];
|
|
371
352
|
event: MouseEvent | TouchEvent;
|
|
372
353
|
}): void {
|
|
373
|
-
if (
|
|
354
|
+
if (!getEditorSettings().proximityConnect || !targetNode || props.readOnly || props.lockWorkflow) {
|
|
374
355
|
if (currentProximityCandidates.length > 0) {
|
|
375
356
|
flowEdges = ProximityConnectHelper.removePreviewEdges(flowEdges);
|
|
376
357
|
currentProximityCandidates = [];
|
|
@@ -393,13 +374,13 @@
|
|
|
393
374
|
targetNode.id,
|
|
394
375
|
portCoordinates,
|
|
395
376
|
baseEdges,
|
|
396
|
-
|
|
377
|
+
getEditorSettings().proximityConnectDistance
|
|
397
378
|
)
|
|
398
379
|
: ProximityConnectHelper.findCompatibleEdges(
|
|
399
380
|
targetNode,
|
|
400
381
|
flowNodes,
|
|
401
382
|
baseEdges,
|
|
402
|
-
|
|
383
|
+
getEditorSettings().proximityConnectDistance
|
|
403
384
|
);
|
|
404
385
|
|
|
405
386
|
// Create preview edges
|
|
@@ -413,24 +394,19 @@
|
|
|
413
394
|
/**
|
|
414
395
|
* Handle node drag stop
|
|
415
396
|
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
397
|
+
* Still in 'dragging' state — sync effect suppressed.
|
|
398
|
+
* Syncs final positions to store, pushes history, then transitions to idle.
|
|
418
399
|
*/
|
|
419
400
|
function handleNodeDragStop(): void {
|
|
420
|
-
isDraggingNode = false;
|
|
421
401
|
portCoordNodeToUpdate = null;
|
|
422
402
|
|
|
423
403
|
// Finalize proximity connect if there are candidates
|
|
424
|
-
if (
|
|
425
|
-
// Remove all preview edges
|
|
404
|
+
if (getEditorSettings().proximityConnect && currentProximityCandidates.length > 0) {
|
|
426
405
|
const baseEdges = ProximityConnectHelper.removePreviewEdges(flowEdges);
|
|
427
|
-
|
|
428
|
-
// Create permanent edges from candidates
|
|
429
406
|
const permanentEdges = ProximityConnectHelper.createPermanentEdges(
|
|
430
407
|
currentProximityCandidates
|
|
431
408
|
);
|
|
432
409
|
|
|
433
|
-
// Apply proper styling to each new permanent edge
|
|
434
410
|
for (const edge of permanentEdges) {
|
|
435
411
|
const sourceNode = flowNodes.find((n) => n.id === edge.source);
|
|
436
412
|
const targetNode = flowNodes.find((n) => n.id === edge.target);
|
|
@@ -439,27 +415,25 @@
|
|
|
439
415
|
}
|
|
440
416
|
}
|
|
441
417
|
|
|
442
|
-
// Set final edges
|
|
443
418
|
flowEdges = [...baseEdges, ...permanentEdges];
|
|
444
|
-
|
|
445
|
-
// Clear proximity state
|
|
446
419
|
currentProximityCandidates = [];
|
|
447
|
-
|
|
448
|
-
// Update workflow
|
|
449
|
-
if (currentWorkflow) {
|
|
450
|
-
updateCurrentWorkflowFromSvelteFlow();
|
|
451
|
-
}
|
|
452
420
|
}
|
|
453
421
|
|
|
454
|
-
//
|
|
455
|
-
|
|
456
|
-
|
|
422
|
+
// Sync flowNodes/flowEdges → store
|
|
423
|
+
syncFlowToStore();
|
|
424
|
+
|
|
425
|
+
// Push history AFTER the drag completed
|
|
426
|
+
const storeValue = getWorkflowStore();
|
|
427
|
+
if (storeValue) {
|
|
428
|
+
workflowActions.pushHistory('Move node', storeValue);
|
|
457
429
|
}
|
|
430
|
+
|
|
431
|
+
// Transition to idle — sync effect is now unblocked
|
|
432
|
+
machine.send('STOP_DRAG');
|
|
458
433
|
}
|
|
459
434
|
|
|
460
435
|
/**
|
|
461
436
|
* Handle new connections between nodes
|
|
462
|
-
* Let SvelteFlow handle edge creation, styling will be applied via reactive effects
|
|
463
437
|
*/
|
|
464
438
|
async function handleConnect(connection: {
|
|
465
439
|
source: string;
|
|
@@ -467,23 +441,23 @@
|
|
|
467
441
|
sourceHandle?: string;
|
|
468
442
|
targetHandle?: string;
|
|
469
443
|
}): Promise<void> {
|
|
470
|
-
|
|
471
|
-
|
|
444
|
+
machine.send('START_CONNECT');
|
|
445
|
+
|
|
446
|
+
// SvelteFlow auto-creates the edge via bind:edges — wait for DOM update
|
|
472
447
|
await tick();
|
|
473
448
|
|
|
474
|
-
// Apply styling to
|
|
475
|
-
|
|
449
|
+
// Apply styling to all edges (including the new one)
|
|
450
|
+
flowEdges = EdgeStylingHelper.updateEdgeStyles(flowEdges, flowNodes);
|
|
476
451
|
|
|
477
|
-
//
|
|
478
|
-
|
|
479
|
-
updateCurrentWorkflowFromSvelteFlow();
|
|
480
|
-
}
|
|
452
|
+
// Sync to store
|
|
453
|
+
syncFlowToStore();
|
|
481
454
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
workflowActions.pushHistory('Add connection', currentWorkflow);
|
|
455
|
+
const storeValue = getWorkflowStore();
|
|
456
|
+
if (storeValue) {
|
|
457
|
+
workflowActions.pushHistory('Add connection', storeValue);
|
|
486
458
|
}
|
|
459
|
+
|
|
460
|
+
machine.send('CONNECTION_MADE');
|
|
487
461
|
}
|
|
488
462
|
|
|
489
463
|
/**
|
|
@@ -500,7 +474,7 @@
|
|
|
500
474
|
edges: WorkflowEdge[];
|
|
501
475
|
}): Promise<boolean> {
|
|
502
476
|
// If confirmDelete setting is enabled, show confirmation dialog
|
|
503
|
-
if (
|
|
477
|
+
if (getBehaviorSettings().confirmDelete) {
|
|
504
478
|
const nodeCount = params.nodes.length;
|
|
505
479
|
const edgeCount = params.edges.length;
|
|
506
480
|
|
|
@@ -533,6 +507,8 @@
|
|
|
533
507
|
* Handle node deletion - automatically remove connected edges and push to history
|
|
534
508
|
*/
|
|
535
509
|
function handleNodesDelete(params: { nodes: WorkflowNodeType[]; edges: WorkflowEdge[] }): void {
|
|
510
|
+
machine.send('START_DELETE');
|
|
511
|
+
|
|
536
512
|
const deletedNodeIds = new Set(params.nodes.map((node) => node.id));
|
|
537
513
|
|
|
538
514
|
// Filter out edges connected to deleted nodes
|
|
@@ -540,10 +516,8 @@
|
|
|
540
516
|
(edge) => !deletedNodeIds.has(edge.source) && !deletedNodeIds.has(edge.target)
|
|
541
517
|
);
|
|
542
518
|
|
|
543
|
-
//
|
|
544
|
-
|
|
545
|
-
updateCurrentWorkflowFromSvelteFlow();
|
|
546
|
-
}
|
|
519
|
+
// Sync to store
|
|
520
|
+
syncFlowToStore();
|
|
547
521
|
|
|
548
522
|
// Push to history AFTER the deletion so undo restores the previous state
|
|
549
523
|
const nodeCount = params.nodes.length;
|
|
@@ -556,32 +530,12 @@
|
|
|
556
530
|
} else if (edgeCount > 0) {
|
|
557
531
|
description = `Delete ${edgeCount} connection${edgeCount > 1 ? 's' : ''}`;
|
|
558
532
|
}
|
|
559
|
-
|
|
560
|
-
|
|
533
|
+
const storeValue = getWorkflowStore();
|
|
534
|
+
if (storeValue) {
|
|
535
|
+
workflowActions.pushHistory(description, storeValue);
|
|
561
536
|
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
/**
|
|
565
|
-
* Update existing edges with our custom styling rules
|
|
566
|
-
* This ensures all edges (including existing ones) follow our rules
|
|
567
|
-
*/
|
|
568
|
-
async function updateExistingEdgeStyles(): Promise<void> {
|
|
569
|
-
// Wait for any pending DOM updates
|
|
570
|
-
await tick();
|
|
571
537
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
// Update currentWorkflow with the styled edges
|
|
575
|
-
if (currentWorkflow) {
|
|
576
|
-
currentWorkflow = WorkflowOperationsHelper.updateWorkflow(
|
|
577
|
-
currentWorkflow,
|
|
578
|
-
flowNodes,
|
|
579
|
-
updatedEdges
|
|
580
|
-
);
|
|
581
|
-
|
|
582
|
-
// Update the global store
|
|
583
|
-
updateGlobalStore();
|
|
584
|
-
}
|
|
538
|
+
machine.send('DELETE_COMPLETE');
|
|
585
539
|
}
|
|
586
540
|
|
|
587
541
|
// Edge styling will be handled when edges are first created or manually updated
|
|
@@ -602,31 +556,73 @@
|
|
|
602
556
|
|
|
603
557
|
/**
|
|
604
558
|
* Handle drop event and add new node to canvas
|
|
605
|
-
* This will be called from the inner DropZone component
|
|
606
559
|
*/
|
|
607
560
|
async function handleNodeDrop(
|
|
608
561
|
nodeTypeData: string,
|
|
609
562
|
position: { x: number; y: number }
|
|
610
563
|
): Promise<void> {
|
|
611
|
-
|
|
564
|
+
machine.send('START_DROP');
|
|
565
|
+
|
|
612
566
|
const newNode = NodeOperationsHelper.createNodeFromDrop(nodeTypeData, position, flowNodes);
|
|
613
567
|
|
|
614
|
-
if (newNode
|
|
615
|
-
// Add
|
|
616
|
-
|
|
568
|
+
if (newNode) {
|
|
569
|
+
// Add onConfigOpen callback and append to flowNodes for immediate visual feedback
|
|
570
|
+
const nodeWithCallback = {
|
|
571
|
+
...newNode,
|
|
572
|
+
data: { ...newNode.data, onConfigOpen: props.openConfigSidebar }
|
|
573
|
+
};
|
|
574
|
+
flowNodes = [...flowNodes, nodeWithCallback];
|
|
617
575
|
|
|
618
|
-
//
|
|
619
|
-
|
|
576
|
+
// Sync to store
|
|
577
|
+
syncFlowToStore();
|
|
620
578
|
|
|
621
|
-
// Wait for DOM update to ensure SvelteFlow updates
|
|
622
579
|
await tick();
|
|
623
580
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
581
|
+
const storeValue = getWorkflowStore();
|
|
582
|
+
if (storeValue) {
|
|
583
|
+
workflowActions.pushHistory('Add node', storeValue);
|
|
584
|
+
}
|
|
585
|
+
} else {
|
|
586
|
+
logger.warn('Failed to create node from drop data');
|
|
629
587
|
}
|
|
588
|
+
|
|
589
|
+
machine.send('DROP_COMPLETE');
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Handle a workflow JSON file dropped directly onto the canvas.
|
|
594
|
+
*
|
|
595
|
+
* Validates the JSON against the minimum required Workflow fields and, if valid,
|
|
596
|
+
* loads it into the workflow store. Shows a toast on validation failure or read error.
|
|
597
|
+
*/
|
|
598
|
+
function handleWorkflowFileDrop(file: File): void {
|
|
599
|
+
const reader = new FileReader();
|
|
600
|
+
reader.onload = (event) => {
|
|
601
|
+
try {
|
|
602
|
+
const text = event.target?.result;
|
|
603
|
+
if (typeof text !== 'string') {
|
|
604
|
+
throw new Error('Could not read file contents.');
|
|
605
|
+
}
|
|
606
|
+
const data = JSON.parse(text);
|
|
607
|
+
const validation = validateWorkflowData(data);
|
|
608
|
+
if (!validation.valid) {
|
|
609
|
+
apiToasts.error('Import workflow', validation.error ?? 'Invalid workflow JSON');
|
|
610
|
+
logger.warn('Workflow file drop validation failed:', validation.error);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
workflowActions.initialize(data as Workflow);
|
|
614
|
+
} catch (error) {
|
|
615
|
+
const errorObj = error instanceof Error ? error : new Error('Unknown error occurred');
|
|
616
|
+
logger.error('Workflow file drop import failed:', errorObj);
|
|
617
|
+
apiToasts.error('Import workflow', errorObj.message);
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
reader.onerror = () => {
|
|
621
|
+
const message = 'Failed to read the dropped file.';
|
|
622
|
+
logger.error(message);
|
|
623
|
+
apiToasts.error('Import workflow', message);
|
|
624
|
+
};
|
|
625
|
+
reader.readAsText(file);
|
|
630
626
|
}
|
|
631
627
|
|
|
632
628
|
/**
|
|
@@ -636,8 +632,9 @@
|
|
|
636
632
|
|
|
637
633
|
/**
|
|
638
634
|
* Update a node's data in the local editor state.
|
|
639
|
-
*
|
|
640
|
-
*
|
|
635
|
+
* Called by App.svelte AFTER it has already updated the global store via
|
|
636
|
+
* workflowActions.updateNode(). We only need to update flowNodes for
|
|
637
|
+
* immediate visual feedback — no store sync needed.
|
|
641
638
|
*
|
|
642
639
|
* @param nodeId - The ID of the node to update
|
|
643
640
|
* @param dataUpdates - Partial data updates to merge into the node's data
|
|
@@ -646,6 +643,8 @@
|
|
|
646
643
|
nodeId: string,
|
|
647
644
|
dataUpdates: Partial<WorkflowNodeType['data']>
|
|
648
645
|
): void {
|
|
646
|
+
machine.send('START_NODE_UPDATE');
|
|
647
|
+
|
|
649
648
|
flowNodes = flowNodes.map((node) => {
|
|
650
649
|
if (node.id === nodeId) {
|
|
651
650
|
return {
|
|
@@ -658,6 +657,8 @@
|
|
|
658
657
|
}
|
|
659
658
|
return node;
|
|
660
659
|
});
|
|
660
|
+
|
|
661
|
+
machine.send('UPDATE_COMPLETE');
|
|
661
662
|
}
|
|
662
663
|
|
|
663
664
|
/**
|
|
@@ -739,14 +740,14 @@
|
|
|
739
740
|
<div class="flowdrop-workflow-editor__main">
|
|
740
741
|
<!-- Flow Canvas -->
|
|
741
742
|
<div class="flowdrop-canvas">
|
|
742
|
-
<FlowDropZone ondrop={handleNodeDrop}>
|
|
743
|
+
<FlowDropZone ondrop={handleNodeDrop} onfiledrop={handleWorkflowFileDrop}>
|
|
743
744
|
{#key svelteFlowKey}
|
|
744
745
|
<SvelteFlow
|
|
745
746
|
bind:nodes={flowNodes}
|
|
746
747
|
bind:edges={flowEdges}
|
|
747
748
|
{nodeTypes}
|
|
748
749
|
{defaultEdgeOptions}
|
|
749
|
-
onconnect={handleConnect}
|
|
750
|
+
onconnect={(connection) => void handleConnect({ source: connection.source, target: connection.target, sourceHandle: connection.sourceHandle ?? undefined, targetHandle: connection.targetHandle ?? undefined })}
|
|
750
751
|
onbeforedelete={handleBeforeDelete}
|
|
751
752
|
ondelete={handleNodesDelete}
|
|
752
753
|
onnodedragstart={handleNodeDragStart}
|
|
@@ -760,18 +761,18 @@
|
|
|
760
761
|
connectionLineComponent={ConnectionLine}
|
|
761
762
|
{snapGrid}
|
|
762
763
|
{initialViewport}
|
|
763
|
-
colorMode={
|
|
764
|
-
fitView={
|
|
764
|
+
colorMode={getResolvedTheme() as ColorMode}
|
|
765
|
+
fitView={getEditorSettings().fitViewOnLoad}
|
|
765
766
|
>
|
|
766
767
|
<Controls />
|
|
767
768
|
<!-- Always render Background for consistent bg color in dark/light mode -->
|
|
768
769
|
<Background
|
|
769
|
-
gap={
|
|
770
|
+
gap={getEditorSettings().gridSize}
|
|
770
771
|
bgColor="var(--fd-background)"
|
|
771
772
|
variant={BackgroundVariant.Dots}
|
|
772
|
-
patternColor={
|
|
773
|
+
patternColor={getEditorSettings().showGrid ? undefined : 'transparent'}
|
|
773
774
|
/>
|
|
774
|
-
{#if
|
|
775
|
+
{#if getEditorSettings().showMinimap}
|
|
775
776
|
<MiniMap />
|
|
776
777
|
{/if}
|
|
777
778
|
</SvelteFlow>
|
|
@@ -787,8 +788,8 @@
|
|
|
787
788
|
</FlowDropZone>
|
|
788
789
|
</div>
|
|
789
790
|
|
|
790
|
-
<!-- Status Bar -->
|
|
791
|
-
<div class="flowdrop-status-bar">
|
|
791
|
+
<!-- Status Bar: aria-live announces dynamic changes (node/edge counts, cycle warnings) -->
|
|
792
|
+
<div class="flowdrop-status-bar" aria-live="polite" aria-atomic="true">
|
|
792
793
|
<div class="flowdrop-status-bar__content">
|
|
793
794
|
<div class="flowdrop-flex flowdrop-gap--4">
|
|
794
795
|
<span class="flowdrop-text--xs flowdrop-text--gray">{flowNodes.length} nodes</span>
|
|
@@ -810,11 +811,14 @@
|
|
|
810
811
|
</SvelteFlowProvider>
|
|
811
812
|
|
|
812
813
|
<!-- Toast notifications container -->
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
814
|
+
<!-- aria-live="polite" ensures screen readers announce toast messages without interrupting -->
|
|
815
|
+
<div aria-live="polite" aria-atomic="true">
|
|
816
|
+
<Toaster
|
|
817
|
+
position="bottom-center"
|
|
818
|
+
containerClassName={FLOWDROP_TOASTER_CLASS}
|
|
819
|
+
toastOptions={flowdropToastOptions}
|
|
820
|
+
/>
|
|
821
|
+
</div>
|
|
818
822
|
|
|
819
823
|
<style>
|
|
820
824
|
.flowdrop-workflow-editor {
|
|
@@ -901,10 +905,6 @@
|
|
|
901
905
|
cursor: pointer;
|
|
902
906
|
}
|
|
903
907
|
|
|
904
|
-
/* Enhanced arrow markers for input ports */
|
|
905
|
-
:global(.flowdrop-workflow-editor .svelte-flow__edge-marker) {
|
|
906
|
-
fill: currentColor;
|
|
907
|
-
}
|
|
908
908
|
|
|
909
909
|
/* Handle size/position only; colors come from inline --fd-handle-fill and base.css ::before */
|
|
910
910
|
:global(.flowdrop-workflow-editor .svelte-flow__handle) {
|