@flowdrop/flowdrop 1.15.0 → 2.0.0-beta.2
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/CHANGELOG.md +508 -0
- package/MIGRATION-2.0.md +629 -0
- package/README.md +23 -23
- package/dist/adapters/WorkflowAdapter.d.ts +1 -1
- package/dist/adapters/WorkflowAdapter.js +14 -8
- package/dist/adapters/agentspec/AgentSpecAdapter.js +7 -7
- package/dist/api/enhanced-client.js +6 -11
- package/dist/chat/batchFeedback.d.ts +39 -0
- package/dist/chat/batchFeedback.js +51 -0
- package/dist/commands/executor.js +15 -1
- package/dist/commands/storeIntegration.svelte.d.ts +4 -1
- package/dist/commands/storeIntegration.svelte.js +26 -21
- package/dist/commands/types.d.ts +2 -0
- package/dist/components/App.svelte +163 -192
- package/dist/components/App.svelte.d.ts +47 -8
- package/dist/components/ConfigForm.svelte +77 -49
- package/dist/components/ConfigModal.svelte +7 -2
- package/dist/components/ConnectionLine.svelte +4 -2
- package/dist/components/Navbar.svelte +61 -1
- package/dist/components/NodeSidebar.svelte +27 -45
- package/dist/components/NodeStatusOverlay.svelte +94 -6
- package/dist/components/NodeSwapPicker.svelte +10 -8
- package/dist/components/PipelineStatus.svelte +22 -68
- package/dist/components/PipelineStatus.svelte.d.ts +3 -0
- package/dist/components/PortCoordinateTracker.svelte +5 -6
- package/dist/components/SchemaForm.stories.svelte +1 -3
- package/dist/components/SchemaForm.svelte +22 -27
- package/dist/components/SchemaForm.svelte.d.ts +0 -8
- package/dist/components/SettingsModal.svelte +8 -3
- package/dist/components/SettingsPanel.svelte +20 -4
- package/dist/components/SwapMappingEditor.svelte +67 -49
- package/dist/components/SwapMappingEditor.svelte.d.ts +0 -2
- package/dist/components/UniversalNode.svelte +9 -7
- package/dist/components/WorkflowEditor.svelte +121 -111
- package/dist/components/WorkflowEditor.svelte.d.ts +21 -10
- package/dist/components/chat/AIChatPanel.svelte +98 -89
- package/dist/components/chat/AIChatPanel.svelte.d.ts +0 -4
- package/dist/components/chat/CommandPreview.svelte +2 -1
- package/dist/components/console/CommandConsole.svelte +7 -5
- package/dist/components/console/ConsoleAutocomplete.svelte +10 -11
- package/dist/components/console/ConsoleAutocomplete.svelte.d.ts +6 -0
- package/dist/components/console/ConsoleInput.svelte +15 -6
- package/dist/components/console/ConsoleOutput.svelte +2 -1
- package/dist/components/form/FormArray.svelte +5 -9
- package/dist/components/form/FormArray.svelte.d.ts +2 -1
- package/dist/components/form/FormAutocomplete.svelte +16 -15
- package/dist/components/form/FormField.svelte +4 -2
- package/dist/components/form/FormFieldLight.svelte +34 -3
- package/dist/components/form/FormFieldLight.svelte.d.ts +12 -0
- package/dist/components/form/FormMarkdownEditor.svelte +9 -4
- package/dist/components/form/FormRangeField.svelte +1 -0
- package/dist/components/form/FormTemplateEditor.svelte +11 -3
- package/dist/components/form/FormToggle.svelte +5 -12
- package/dist/components/form/FormToggle.svelte.d.ts +4 -2
- package/dist/components/form/FormUISchemaRenderer.svelte +3 -1
- package/dist/components/form/templateAutocomplete.js +1 -5
- package/dist/components/form/types.d.ts +1 -14
- package/dist/components/interrupt/FormPrompt.svelte +3 -2
- package/dist/components/interrupt/InterruptBubble.svelte +25 -17
- package/dist/components/interrupt/ReviewPrompt.svelte +10 -3
- package/dist/components/interrupt/TextInputPrompt.svelte +2 -1
- package/dist/components/layouts/MainLayout.svelte +20 -13
- package/dist/components/layouts/MainLayout.svelte.d.ts +4 -0
- package/dist/components/nodes/AtomNode.svelte +17 -5
- package/dist/components/nodes/GatewayNode.svelte +19 -10
- package/dist/components/nodes/IdeaNode.svelte +7 -0
- package/dist/components/nodes/SimpleNode.svelte +11 -6
- package/dist/components/nodes/SquareNode.svelte +15 -8
- package/dist/components/nodes/TerminalNode.svelte +9 -4
- package/dist/components/nodes/ToolNode.svelte +7 -1
- package/dist/components/nodes/WorkflowNode.svelte +16 -7
- package/dist/components/playground/ChatInput.svelte +11 -14
- package/dist/components/playground/ChatPanel.svelte +6 -49
- package/dist/components/playground/ChatPanel.svelte.d.ts +0 -14
- package/dist/components/playground/ControlPanel.svelte +134 -123
- package/dist/components/playground/ControlPanel.svelte.d.ts +3 -0
- package/dist/components/playground/ExecutionLogs.svelte +11 -9
- package/dist/components/playground/InputCollector.svelte +11 -9
- package/dist/components/playground/MessageStream.svelte +17 -23
- package/dist/components/playground/PipelineKanbanView.svelte +69 -8
- package/dist/components/playground/PipelineKanbanView.svelte.d.ts +2 -0
- package/dist/components/playground/PipelinePanel.svelte +31 -8
- package/dist/components/playground/PipelinePanel.svelte.d.ts +2 -0
- package/dist/components/playground/PipelineTableView.svelte +188 -44
- package/dist/components/playground/PipelineTableView.svelte.d.ts +2 -0
- package/dist/components/playground/Playground.svelte +154 -105
- package/dist/components/playground/Playground.svelte.d.ts +5 -0
- package/dist/components/playground/PlaygroundApp.svelte +11 -1
- package/dist/components/playground/PlaygroundApp.svelte.d.ts +6 -0
- package/dist/components/playground/PlaygroundModal.svelte +18 -3
- package/dist/components/playground/PlaygroundModal.svelte.d.ts +6 -0
- package/dist/components/playground/PlaygroundStudio.svelte +40 -32
- package/dist/components/playground/PlaygroundStudio.svelte.d.ts +6 -0
- package/dist/components/playground/SessionManager.svelte +9 -12
- package/dist/components/playground/pipelineViewUtils.svelte.d.ts +30 -1
- package/dist/components/playground/pipelineViewUtils.svelte.js +40 -3
- package/dist/config/endpoints.d.ts +23 -7
- package/dist/config/endpoints.js +30 -10
- package/dist/core/index.d.ts +5 -6
- package/dist/core/index.js +8 -12
- package/dist/display/index.d.ts +6 -3
- package/dist/display/index.js +7 -5
- package/dist/editor/index.d.ts +20 -21
- package/dist/editor/index.js +26 -36
- package/dist/form/code.d.ts +25 -15
- package/dist/form/code.js +44 -41
- package/dist/form/fieldRegistry.d.ts +17 -13
- package/dist/form/fieldRegistry.js +32 -12
- package/dist/form/full.d.ts +19 -14
- package/dist/form/full.js +26 -28
- package/dist/form/index.d.ts +3 -4
- package/dist/form/index.js +6 -5
- package/dist/form/markdown.d.ts +13 -8
- package/dist/form/markdown.js +22 -23
- package/dist/helpers/proximityConnect.d.ts +3 -2
- package/dist/helpers/proximityConnect.js +2 -5
- package/dist/helpers/workflowEditorHelper.d.ts +14 -5
- package/dist/helpers/workflowEditorHelper.js +28 -25
- package/dist/index.d.ts +28 -24
- package/dist/index.js +27 -50
- package/dist/messages/defaults.d.ts +2 -5
- package/dist/messages/defaults.js +3 -6
- package/dist/messages/index.d.ts +0 -1
- package/dist/messages/index.js +0 -1
- package/dist/mocks/app-forms.d.ts +6 -2
- package/dist/mocks/app-forms.js +11 -4
- package/dist/openapi/v1/openapi.yaml +3 -3
- package/dist/playground/index.d.ts +4 -5
- package/dist/playground/index.js +4 -32
- package/dist/playground/mount.d.ts +25 -0
- package/dist/playground/mount.js +50 -20
- package/dist/registry/{BaseRegistry.d.ts → BaseRegistry.svelte.d.ts} +22 -1
- package/dist/registry/{BaseRegistry.js → BaseRegistry.svelte.js} +37 -1
- package/dist/registry/builtinFormats.d.ts +9 -18
- package/dist/registry/builtinFormats.js +9 -39
- package/dist/registry/builtinNodeTypes.d.ts +53 -0
- package/dist/registry/builtinNodeTypes.js +67 -0
- package/dist/registry/builtinNodes.d.ts +2 -64
- package/dist/registry/builtinNodes.js +7 -103
- package/dist/registry/index.d.ts +3 -4
- package/dist/registry/index.js +4 -6
- package/dist/registry/nodeComponentRegistry.d.ts +182 -15
- package/dist/registry/nodeComponentRegistry.js +235 -17
- package/dist/registry/workflowFormatRegistry.d.ts +14 -9
- package/dist/registry/workflowFormatRegistry.js +24 -8
- package/dist/{schema → schemas}/index.d.ts +2 -2
- package/dist/{schema → schemas}/index.js +2 -2
- package/dist/schemas/v1/workflow.schema.json +3 -3
- package/dist/services/agentSpecExecutionService.d.ts +0 -2
- package/dist/services/agentSpecExecutionService.js +0 -3
- package/dist/services/apiVariableService.d.ts +2 -1
- package/dist/services/apiVariableService.js +16 -47
- package/dist/services/autoSaveService.d.ts +7 -0
- package/dist/services/autoSaveService.js +6 -4
- package/dist/services/categoriesApi.js +3 -6
- package/dist/services/chatService.d.ts +9 -4
- package/dist/services/chatService.js +23 -28
- package/dist/services/draftStorage.d.ts +129 -13
- package/dist/services/draftStorage.js +185 -37
- package/dist/services/dynamicSchemaService.d.ts +2 -1
- package/dist/services/dynamicSchemaService.js +5 -22
- package/dist/services/globalSave.d.ts +13 -12
- package/dist/services/globalSave.js +29 -51
- package/dist/services/historyService.d.ts +9 -3
- package/dist/services/historyService.js +9 -3
- package/dist/services/interruptService.d.ts +15 -9
- package/dist/services/interruptService.js +35 -37
- package/dist/services/nodeExecutionService.d.ts +18 -3
- package/dist/services/nodeExecutionService.js +71 -45
- package/dist/services/playgroundService.d.ts +16 -10
- package/dist/services/playgroundService.js +42 -43
- package/dist/services/portConfigApi.js +3 -6
- package/dist/services/settingsService.d.ts +9 -4
- package/dist/services/settingsService.js +23 -12
- package/dist/services/variableService.d.ts +2 -1
- package/dist/services/variableService.js +2 -2
- package/dist/services/workflowStorage.js +6 -6
- package/dist/stores/apiContext.d.ts +56 -0
- package/dist/stores/apiContext.js +80 -0
- package/dist/stores/categoriesStore.svelte.d.ts +28 -23
- package/dist/stores/categoriesStore.svelte.js +69 -64
- package/dist/stores/getInstance.svelte.d.ts +39 -0
- package/dist/stores/getInstance.svelte.js +65 -0
- package/dist/stores/historyStore.svelte.d.ts +77 -93
- package/dist/stores/historyStore.svelte.js +134 -160
- package/dist/stores/instanceContainer.svelte.d.ts +111 -0
- package/dist/stores/instanceContainer.svelte.js +114 -0
- package/dist/stores/interruptStore.svelte.d.ts +112 -82
- package/dist/stores/interruptStore.svelte.js +253 -226
- package/dist/stores/pipelinePanelStore.svelte.d.ts +27 -3
- package/dist/stores/pipelinePanelStore.svelte.js +61 -14
- package/dist/stores/playgroundStore.svelte.d.ts +169 -222
- package/dist/stores/playgroundStore.svelte.js +513 -580
- package/dist/stores/portCoordinateStore.svelte.d.ts +57 -51
- package/dist/stores/portCoordinateStore.svelte.js +109 -98
- package/dist/stores/settingsStore.svelte.d.ts +4 -1
- package/dist/stores/settingsStore.svelte.js +47 -12
- package/dist/stores/workflowStore.svelte.d.ts +178 -213
- package/dist/stores/workflowStore.svelte.js +449 -501
- package/dist/stories/EdgeDecorator.svelte +5 -2
- package/dist/stories/NodeDecorator.svelte +5 -3
- package/dist/svelte-app.d.ts +60 -10
- package/dist/svelte-app.js +159 -54
- package/dist/types/auth.d.ts +9 -51
- package/dist/types/auth.js +4 -54
- package/dist/types/events.d.ts +6 -3
- package/dist/types/index.d.ts +37 -5
- package/dist/types/index.js +0 -1
- package/dist/types/navbar.d.ts +7 -0
- package/dist/types/playground.d.ts +18 -3
- package/dist/types/settings.d.ts +13 -0
- package/dist/types/settings.js +1 -0
- package/dist/utils/colors.d.ts +47 -21
- package/dist/utils/colors.js +69 -68
- package/dist/utils/connections.d.ts +9 -15
- package/dist/utils/connections.js +13 -32
- package/dist/utils/duration.d.ts +13 -0
- package/dist/utils/duration.js +45 -0
- package/dist/utils/edgeStyling.js +9 -5
- package/dist/utils/fetchWithAuth.d.ts +36 -15
- package/dist/utils/fetchWithAuth.js +53 -23
- package/dist/utils/icons.d.ts +5 -2
- package/dist/utils/icons.js +6 -5
- package/dist/utils/nodeSwap.d.ts +6 -2
- package/dist/utils/nodeSwap.js +62 -126
- package/dist/utils/nodeTypes.d.ts +17 -8
- package/dist/utils/nodeTypes.js +27 -20
- package/dist/utils/performanceUtils.js +7 -0
- package/package.json +7 -5
- package/dist/messages/deprecation.d.ts +0 -20
- package/dist/messages/deprecation.js +0 -33
- package/dist/registry/plugin.d.ts +0 -215
- package/dist/registry/plugin.js +0 -249
- package/dist/services/api.d.ts +0 -129
- package/dist/services/api.js +0 -217
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Workflow Store for FlowDrop (Svelte 5 Runes)
|
|
3
3
|
*
|
|
4
|
-
* Provides
|
|
5
|
-
* and undo/redo history integration.
|
|
4
|
+
* Provides per-instance state management for workflows with dirty state
|
|
5
|
+
* tracking and undo/redo history integration.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* The reactive state lives in the {@link WorkflowStore} class — one per
|
|
8
|
+
* FlowDrop instance, created by `createFlowDropInstance()` and resolved in
|
|
9
|
+
* components via `getInstance().workflow`.
|
|
10
10
|
*
|
|
11
11
|
* @module stores/workflowStore
|
|
12
12
|
*/
|
|
13
13
|
import { DEFAULT_WORKFLOW_FORMAT } from '../types/index.js';
|
|
14
|
-
import {
|
|
14
|
+
import { WORKFLOW_SCHEMA_VERSION } from '../schemas/index.js';
|
|
15
15
|
/**
|
|
16
16
|
* Safely build updated workflow metadata, providing defaults for required fields.
|
|
17
|
+
*
|
|
18
|
+
* Accepts loosely-typed legacy metadata so it can absorb 1.x documents (where
|
|
19
|
+
* the schema-version field was named `version`) during load-time healing.
|
|
17
20
|
*/
|
|
18
21
|
function buildMetadata(existing, updates) {
|
|
19
22
|
return {
|
|
20
|
-
|
|
23
|
+
schemaVersion: existing?.schemaVersion ?? existing?.version ?? WORKFLOW_SCHEMA_VERSION,
|
|
21
24
|
createdAt: existing?.createdAt ?? new Date().toISOString(),
|
|
22
25
|
updatedAt: new Date().toISOString(),
|
|
23
26
|
author: existing?.author,
|
|
@@ -28,336 +31,30 @@ function buildMetadata(existing, updates) {
|
|
|
28
31
|
...updates
|
|
29
32
|
};
|
|
30
33
|
}
|
|
31
|
-
// =========================================================================
|
|
32
|
-
// Core Workflow State (Svelte 5 Runes)
|
|
33
|
-
// =========================================================================
|
|
34
|
-
/** Global workflow state */
|
|
35
|
-
let workflowState = $state(null);
|
|
36
|
-
// =========================================================================
|
|
37
|
-
// Dirty State Tracking (Version Counter)
|
|
38
|
-
// =========================================================================
|
|
39
|
-
/**
|
|
40
|
-
* Monotonic edit version — bumps on every mutation action.
|
|
41
|
-
*
|
|
42
|
-
* Used for two purposes:
|
|
43
|
-
* 1. O(1) dirty detection: isDirty = _editVersion !== _savedVersion
|
|
44
|
-
* 2. Save verification protocol: include the version in the save request,
|
|
45
|
-
* backend echoes it back. If the echoed version doesn't match, the save
|
|
46
|
-
* didn't persist the version the client submitted.
|
|
47
|
-
*/
|
|
48
|
-
let _editVersion = $state(0);
|
|
49
|
-
/**
|
|
50
|
-
* Edit version captured at the last successful save.
|
|
51
|
-
* When _editVersion === _savedVersion, the workflow is clean.
|
|
52
|
-
*/
|
|
53
|
-
let _savedVersion = $state(0);
|
|
54
|
-
/**
|
|
55
|
-
* Callback for dirty state changes
|
|
56
|
-
*
|
|
57
|
-
* Set by the App component to notify parent application.
|
|
58
|
-
*/
|
|
59
|
-
let onDirtyStateChangeCallback = null;
|
|
60
|
-
/**
|
|
61
|
-
* Callback for workflow changes
|
|
62
|
-
*
|
|
63
|
-
* Set by the App component to notify parent application.
|
|
64
|
-
*/
|
|
65
|
-
let onWorkflowChangeCallback = null;
|
|
66
|
-
/**
|
|
67
|
-
* Flag to track if we're currently restoring from history (undo/redo)
|
|
68
|
-
*
|
|
69
|
-
* When true, prevents pushing to history to avoid recursive loops.
|
|
70
|
-
*/
|
|
71
|
-
let isRestoringFromHistory = false;
|
|
72
|
-
/**
|
|
73
|
-
* Flag to track if history recording is enabled
|
|
74
|
-
*
|
|
75
|
-
* Can be disabled for bulk operations or when history is not needed.
|
|
76
|
-
*/
|
|
77
|
-
let historyEnabled = true;
|
|
78
|
-
// =========================================================================
|
|
79
|
-
// Getter Functions (Reactive State Access)
|
|
80
|
-
// =========================================================================
|
|
81
|
-
/**
|
|
82
|
-
* Get the current workflow store value reactively
|
|
83
|
-
*
|
|
84
|
-
* @returns The current workflow or null
|
|
85
|
-
*/
|
|
86
|
-
export function getWorkflowStore() {
|
|
87
|
-
return workflowState;
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Get the current dirty state reactively
|
|
91
|
-
*
|
|
92
|
-
* Reads both _editVersion and _savedVersion, so Svelte tracks them
|
|
93
|
-
* as reactive dependencies — any component using this will re-render
|
|
94
|
-
* when dirty state changes.
|
|
95
|
-
*
|
|
96
|
-
* @returns true if there are unsaved changes
|
|
97
|
-
*/
|
|
98
|
-
export function getIsDirty() {
|
|
99
|
-
return _editVersion !== _savedVersion;
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Get the workflow ID reactively
|
|
103
|
-
*
|
|
104
|
-
* @returns The workflow ID or null
|
|
105
|
-
*/
|
|
106
|
-
export function getWorkflowId() {
|
|
107
|
-
return workflowState?.id ?? null;
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Get the workflow name reactively
|
|
111
|
-
*
|
|
112
|
-
* @returns The workflow name or 'Untitled Workflow'
|
|
113
|
-
*/
|
|
114
|
-
export function getWorkflowName() {
|
|
115
|
-
return workflowState?.name ?? 'Untitled Workflow';
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Get the workflow nodes reactively
|
|
119
|
-
*
|
|
120
|
-
* @returns Array of workflow nodes
|
|
121
|
-
*/
|
|
122
|
-
export function getWorkflowNodes() {
|
|
123
|
-
return workflowState?.nodes ?? [];
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Get the workflow edges reactively
|
|
127
|
-
*
|
|
128
|
-
* @returns Array of workflow edges
|
|
129
|
-
*/
|
|
130
|
-
export function getWorkflowEdges() {
|
|
131
|
-
return workflowState?.edges ?? [];
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Get the workflow metadata reactively
|
|
135
|
-
*
|
|
136
|
-
* @returns The workflow metadata with defaults
|
|
137
|
-
*/
|
|
138
|
-
export function getWorkflowMetadata() {
|
|
139
|
-
return (workflowState?.metadata ?? {
|
|
140
|
-
version: '1.0.0',
|
|
141
|
-
createdAt: new Date().toISOString(),
|
|
142
|
-
updatedAt: new Date().toISOString(),
|
|
143
|
-
versionId: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
144
|
-
updateNumber: 0
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Get the current workflow format reactively
|
|
149
|
-
*
|
|
150
|
-
* @returns The workflow format string
|
|
151
|
-
*/
|
|
152
|
-
export function getWorkflowFormat() {
|
|
153
|
-
return workflowState?.metadata?.format ?? DEFAULT_WORKFLOW_FORMAT;
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Get workflow change summary reactively (useful for triggering saves)
|
|
157
|
-
*
|
|
158
|
-
* @returns Object with nodes, edges, and name
|
|
159
|
-
*/
|
|
160
|
-
export function getWorkflowChanged() {
|
|
161
|
-
return {
|
|
162
|
-
nodes: getWorkflowNodes(),
|
|
163
|
-
edges: getWorkflowEdges(),
|
|
164
|
-
name: getWorkflowName()
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Get workflow validation state reactively
|
|
169
|
-
*
|
|
170
|
-
* @returns Validation info object
|
|
171
|
-
*/
|
|
172
|
-
export function getWorkflowValidation() {
|
|
173
|
-
const nodes = getWorkflowNodes();
|
|
174
|
-
const edges = getWorkflowEdges();
|
|
175
|
-
return {
|
|
176
|
-
hasNodes: nodes.length > 0,
|
|
177
|
-
hasEdges: edges.length > 0,
|
|
178
|
-
nodeCount: nodes.length,
|
|
179
|
-
edgeCount: edges.length,
|
|
180
|
-
isValid: nodes.length > 0 && edges.length >= 0
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Get workflow metadata change summary reactively
|
|
185
|
-
*
|
|
186
|
-
* @returns Metadata change info
|
|
187
|
-
*/
|
|
188
|
-
export function getWorkflowMetadataChanged() {
|
|
189
|
-
const metadata = getWorkflowMetadata();
|
|
190
|
-
return {
|
|
191
|
-
createdAt: metadata.createdAt,
|
|
192
|
-
updatedAt: metadata.updatedAt,
|
|
193
|
-
version: metadata.version ?? '1.0.0'
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* Get connected handles reactively
|
|
198
|
-
*
|
|
199
|
-
* Provides a Set of all handle IDs that are currently connected to edges.
|
|
200
|
-
* Used by node components to implement hideUnconnectedHandles functionality.
|
|
201
|
-
*
|
|
202
|
-
* @example
|
|
203
|
-
* ```typescript
|
|
204
|
-
* import { getConnectedHandles } from './workflowStore.svelte.js';
|
|
205
|
-
*
|
|
206
|
-
* // Check if a specific handle is connected
|
|
207
|
-
* const isConnected = getConnectedHandles().has('node-1-input-data');
|
|
208
|
-
* ```
|
|
209
|
-
*/
|
|
210
|
-
export function getConnectedHandles() {
|
|
211
|
-
const edges = getWorkflowEdges();
|
|
212
|
-
const handles = new Set();
|
|
213
|
-
edges.forEach((edge) => {
|
|
214
|
-
// Add source handle (output port)
|
|
215
|
-
if (edge.sourceHandle) {
|
|
216
|
-
handles.add(edge.sourceHandle);
|
|
217
|
-
}
|
|
218
|
-
// Add target handle (input port)
|
|
219
|
-
if (edge.targetHandle) {
|
|
220
|
-
handles.add(edge.targetHandle);
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
return handles;
|
|
224
|
-
}
|
|
225
|
-
// =========================================================================
|
|
226
|
-
// Callback Setters
|
|
227
|
-
// =========================================================================
|
|
228
|
-
/**
|
|
229
|
-
* Set the dirty state change callback
|
|
230
|
-
*
|
|
231
|
-
* @param callback - Function to call when dirty state changes
|
|
232
|
-
*/
|
|
233
|
-
export function setOnDirtyStateChange(callback) {
|
|
234
|
-
onDirtyStateChangeCallback = callback;
|
|
235
|
-
}
|
|
236
|
-
/**
|
|
237
|
-
* Set the workflow change callback
|
|
238
|
-
*
|
|
239
|
-
* @param callback - Function to call when workflow changes
|
|
240
|
-
*/
|
|
241
|
-
export function setOnWorkflowChange(callback) {
|
|
242
|
-
onWorkflowChangeCallback = callback;
|
|
243
|
-
}
|
|
244
|
-
// =========================================================================
|
|
245
|
-
// Internal Helpers
|
|
246
|
-
// =========================================================================
|
|
247
|
-
/**
|
|
248
|
-
* Bump the edit version and notify dirty state change if needed.
|
|
249
|
-
* Called by every mutation action.
|
|
250
|
-
*/
|
|
251
|
-
function bumpVersion() {
|
|
252
|
-
_editVersion++;
|
|
253
|
-
// Dirty state just flipped from clean → dirty
|
|
254
|
-
if (_editVersion - 1 === _savedVersion) {
|
|
255
|
-
if (onDirtyStateChangeCallback) {
|
|
256
|
-
onDirtyStateChangeCallback(true);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* Notify external listeners of a workflow change.
|
|
262
|
-
* Does NOT bump the version — callers that mutate must call bumpVersion() explicitly.
|
|
263
|
-
*
|
|
264
|
-
* @param changeType - The type of change that occurred
|
|
265
|
-
*/
|
|
266
|
-
function notifyWorkflowChange(changeType) {
|
|
267
|
-
if (workflowState && onWorkflowChangeCallback) {
|
|
268
|
-
onWorkflowChangeCallback(workflowState, changeType);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
/**
|
|
272
|
-
* Mark the current workflow state as saved.
|
|
273
|
-
*
|
|
274
|
-
* Captures the current edit version so isDirty becomes false.
|
|
275
|
-
* Call this after a successful backend save.
|
|
276
|
-
*/
|
|
277
|
-
export function markAsSaved() {
|
|
278
|
-
const wasDirty = _editVersion !== _savedVersion;
|
|
279
|
-
_savedVersion = _editVersion;
|
|
280
|
-
if (wasDirty && onDirtyStateChangeCallback) {
|
|
281
|
-
onDirtyStateChangeCallback(false);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
/**
|
|
285
|
-
* Check if there are unsaved changes (non-reactive version for plain TS)
|
|
286
|
-
*
|
|
287
|
-
* @returns true if there are unsaved changes
|
|
288
|
-
*/
|
|
289
|
-
export function isDirty() {
|
|
290
|
-
return _editVersion !== _savedVersion;
|
|
291
|
-
}
|
|
292
|
-
/**
|
|
293
|
-
* Get the current edit version.
|
|
294
|
-
*
|
|
295
|
-
* Use this for the save verification protocol:
|
|
296
|
-
* 1. Before save: `const v = getEditVersion()`
|
|
297
|
-
* 2. Include `v` in the save request payload
|
|
298
|
-
* 3. Backend echoes `v` in the response
|
|
299
|
-
* 4. If echoed version matches: `markAsSaved()`
|
|
300
|
-
* 5. If not: the save didn't persist the version you submitted — reset from backend response
|
|
301
|
-
*
|
|
302
|
-
* @returns The current monotonic edit version
|
|
303
|
-
*/
|
|
304
|
-
export function getEditVersion() {
|
|
305
|
-
return _editVersion;
|
|
306
|
-
}
|
|
307
34
|
/**
|
|
308
|
-
*
|
|
35
|
+
* Normalize workflow metadata at load time (the only back-compat path we keep,
|
|
36
|
+
* mirroring the storage-key migration in commit 8fab9157).
|
|
309
37
|
*
|
|
310
|
-
*
|
|
38
|
+
* Rules:
|
|
39
|
+
* - missing `metadata` → `buildMetadata(undefined)` (fresh required defaults)
|
|
40
|
+
* - legacy `version` key present → copied to `schemaVersion` when absent, then
|
|
41
|
+
* the legacy key is dropped
|
|
311
42
|
*
|
|
312
|
-
*
|
|
43
|
+
* Idempotent: re-running on an already-healed workflow returns equivalent
|
|
44
|
+
* metadata (round-trip stable).
|
|
313
45
|
*/
|
|
314
|
-
export function
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
* Check if history recording is enabled
|
|
319
|
-
*
|
|
320
|
-
* @returns true if history is being recorded
|
|
321
|
-
*/
|
|
322
|
-
export function isHistoryEnabled() {
|
|
323
|
-
return historyEnabled;
|
|
324
|
-
}
|
|
325
|
-
/**
|
|
326
|
-
* Set the restoring from history flag
|
|
327
|
-
*
|
|
328
|
-
* Used internally by the history store when performing undo/redo.
|
|
329
|
-
*
|
|
330
|
-
* @param restoring - Whether we're currently restoring from history
|
|
331
|
-
*/
|
|
332
|
-
export function setRestoringFromHistory(restoring) {
|
|
333
|
-
isRestoringFromHistory = restoring;
|
|
334
|
-
}
|
|
335
|
-
/**
|
|
336
|
-
* Push current state to history before making changes
|
|
337
|
-
*
|
|
338
|
-
* @param description - Description of the change about to be made
|
|
339
|
-
* @param workflow - Optional workflow to push (uses store if not provided)
|
|
340
|
-
*/
|
|
341
|
-
function pushToHistory(description, workflow) {
|
|
342
|
-
if (!historyEnabled || isRestoringFromHistory) {
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
const workflowToPush = workflow ?? workflowState;
|
|
346
|
-
if (workflowToPush) {
|
|
347
|
-
historyService.push(workflowToPush, { description });
|
|
46
|
+
export function normalizeWorkflowMetadata(workflow) {
|
|
47
|
+
const raw = workflow.metadata;
|
|
48
|
+
if (!raw) {
|
|
49
|
+
return { ...workflow, metadata: buildMetadata(undefined) };
|
|
348
50
|
}
|
|
51
|
+
const { version: legacyVersion, ...rest } = raw;
|
|
52
|
+
const healed = {
|
|
53
|
+
...rest,
|
|
54
|
+
schemaVersion: rest.schemaVersion ?? legacyVersion ?? WORKFLOW_SCHEMA_VERSION
|
|
55
|
+
};
|
|
56
|
+
return { ...workflow, metadata: healed };
|
|
349
57
|
}
|
|
350
|
-
/**
|
|
351
|
-
* Get the current workflow (non-reactive version for plain TS)
|
|
352
|
-
*
|
|
353
|
-
* @returns The current workflow or null
|
|
354
|
-
*/
|
|
355
|
-
export function getWorkflow() {
|
|
356
|
-
return workflowState;
|
|
357
|
-
}
|
|
358
|
-
// =========================================================================
|
|
359
|
-
// Helper Functions
|
|
360
|
-
// =========================================================================
|
|
361
58
|
/**
|
|
362
59
|
* Check if workflow data has actually changed
|
|
363
60
|
*
|
|
@@ -397,235 +94,486 @@ function hasWorkflowDataChanged(currentWorkflow, newNodes, newEdges) {
|
|
|
397
94
|
}
|
|
398
95
|
return false;
|
|
399
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Heal nodes that are missing `data.nodeId`.
|
|
99
|
+
*
|
|
100
|
+
* Node components derive their handle IDs from `data.nodeId` — a node without
|
|
101
|
+
* it renders with zero handles and SvelteFlow silently drops every edge
|
|
102
|
+
* anchored to it, making an intact graph look corrupted on the canvas.
|
|
103
|
+
* `data.nodeId` always equals the node's own `id`, so it is safe to restore.
|
|
104
|
+
*/
|
|
105
|
+
function healMissingNodeIds(workflow) {
|
|
106
|
+
if (!workflow.nodes?.some((node) => !node.data.nodeId)) {
|
|
107
|
+
return workflow;
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
...workflow,
|
|
111
|
+
nodes: workflow.nodes.map((node) => node.data.nodeId ? node : { ...node, data: { ...node.data, nodeId: node.id } })
|
|
112
|
+
};
|
|
113
|
+
}
|
|
400
114
|
// =========================================================================
|
|
401
|
-
//
|
|
115
|
+
// WorkflowStore (per-instance reactive state)
|
|
402
116
|
// =========================================================================
|
|
403
117
|
/**
|
|
404
|
-
*
|
|
118
|
+
* Per-instance workflow state with dirty tracking and history integration.
|
|
405
119
|
*
|
|
406
|
-
* All
|
|
407
|
-
*
|
|
120
|
+
* All reads go through getters backed by `$state`, so reading them inside a
|
|
121
|
+
* component template or `$derived` tracks reactively, exactly like the
|
|
122
|
+
* legacy module-level functions did.
|
|
408
123
|
*/
|
|
409
|
-
export
|
|
124
|
+
export class WorkflowStore {
|
|
125
|
+
/** Current workflow */
|
|
126
|
+
#workflow = $state(null);
|
|
127
|
+
/**
|
|
128
|
+
* Monotonic edit version — bumps on every mutation action.
|
|
129
|
+
*
|
|
130
|
+
* Used for two purposes:
|
|
131
|
+
* 1. O(1) dirty detection: isDirty = #editVersion !== #savedVersion
|
|
132
|
+
* 2. Save verification protocol: include the version in the save request,
|
|
133
|
+
* backend echoes it back. If the echoed version doesn't match, the save
|
|
134
|
+
* didn't persist the version the client submitted.
|
|
135
|
+
*/
|
|
136
|
+
#editVersion = $state(0);
|
|
137
|
+
/**
|
|
138
|
+
* Edit version captured at the last successful save.
|
|
139
|
+
* When #editVersion === #savedVersion, the workflow is clean.
|
|
140
|
+
*/
|
|
141
|
+
#savedVersion = $state(0);
|
|
142
|
+
/** Callback for dirty state changes — set by the App component. */
|
|
143
|
+
#onDirtyStateChange = null;
|
|
144
|
+
/** Callback for workflow changes — set by the App component. */
|
|
145
|
+
#onWorkflowChange = null;
|
|
146
|
+
/**
|
|
147
|
+
* Whether we're currently restoring from history (undo/redo).
|
|
148
|
+
* When true, prevents pushing to history to avoid recursive loops.
|
|
149
|
+
*/
|
|
150
|
+
#restoringFromHistory = false;
|
|
151
|
+
/** Whether history recording is enabled (disable for bulk operations). */
|
|
152
|
+
#historyEnabled = true;
|
|
153
|
+
/** Undo/redo engine for this instance (constructor-injected). */
|
|
154
|
+
#history;
|
|
155
|
+
/** Bound mutation facade — see {@link WorkflowStoreActions}. */
|
|
156
|
+
actions;
|
|
157
|
+
constructor(history) {
|
|
158
|
+
this.#history = history;
|
|
159
|
+
this.actions = Object.freeze({
|
|
160
|
+
initialize: this.initialize.bind(this),
|
|
161
|
+
updateWorkflow: this.updateWorkflow.bind(this),
|
|
162
|
+
restoreFromHistory: this.restoreFromHistory.bind(this),
|
|
163
|
+
updateNodes: this.updateNodes.bind(this),
|
|
164
|
+
updateEdges: this.updateEdges.bind(this),
|
|
165
|
+
updateName: this.updateName.bind(this),
|
|
166
|
+
addNode: this.addNode.bind(this),
|
|
167
|
+
removeNode: this.removeNode.bind(this),
|
|
168
|
+
addEdge: this.addEdge.bind(this),
|
|
169
|
+
removeEdge: this.removeEdge.bind(this),
|
|
170
|
+
updateNode: this.updateNode.bind(this),
|
|
171
|
+
clear: this.clear.bind(this),
|
|
172
|
+
updateMetadata: this.updateMetadata.bind(this),
|
|
173
|
+
batchUpdate: this.batchUpdate.bind(this),
|
|
174
|
+
swapNode: this.swapNode.bind(this),
|
|
175
|
+
pushHistory: this.pushHistory.bind(this)
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
// -----------------------------------------------------------------------
|
|
179
|
+
// Reactive getters
|
|
180
|
+
// -----------------------------------------------------------------------
|
|
181
|
+
/** The current workflow, or null when none is loaded. */
|
|
182
|
+
get current() {
|
|
183
|
+
return this.#workflow;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Whether there are unsaved changes.
|
|
187
|
+
*
|
|
188
|
+
* Reads both #editVersion and #savedVersion, so Svelte tracks them as
|
|
189
|
+
* reactive dependencies — any component using this re-renders when dirty
|
|
190
|
+
* state changes.
|
|
191
|
+
*/
|
|
192
|
+
get isDirty() {
|
|
193
|
+
return this.#editVersion !== this.#savedVersion;
|
|
194
|
+
}
|
|
195
|
+
/** The workflow ID, or null. */
|
|
196
|
+
get id() {
|
|
197
|
+
return this.#workflow?.id ?? null;
|
|
198
|
+
}
|
|
199
|
+
/** The workflow name, or 'Untitled Workflow'. */
|
|
200
|
+
get name() {
|
|
201
|
+
return this.#workflow?.name ?? 'Untitled Workflow';
|
|
202
|
+
}
|
|
203
|
+
/** The workflow nodes. */
|
|
204
|
+
get nodes() {
|
|
205
|
+
return this.#workflow?.nodes ?? [];
|
|
206
|
+
}
|
|
207
|
+
/** The workflow edges. */
|
|
208
|
+
get edges() {
|
|
209
|
+
return this.#workflow?.edges ?? [];
|
|
210
|
+
}
|
|
211
|
+
/** The workflow metadata, with defaults. */
|
|
212
|
+
get metadata() {
|
|
213
|
+
return (this.#workflow?.metadata ?? {
|
|
214
|
+
schemaVersion: WORKFLOW_SCHEMA_VERSION,
|
|
215
|
+
createdAt: new Date().toISOString(),
|
|
216
|
+
updatedAt: new Date().toISOString(),
|
|
217
|
+
versionId: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
218
|
+
updateNumber: 0
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
/** The workflow format string. */
|
|
222
|
+
get format() {
|
|
223
|
+
return this.#workflow?.metadata?.format ?? DEFAULT_WORKFLOW_FORMAT;
|
|
224
|
+
}
|
|
225
|
+
/** Workflow change summary (useful for triggering saves). */
|
|
226
|
+
get changeSummary() {
|
|
227
|
+
return {
|
|
228
|
+
nodes: this.nodes,
|
|
229
|
+
edges: this.edges,
|
|
230
|
+
name: this.name
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/** Workflow validation state. */
|
|
234
|
+
get validation() {
|
|
235
|
+
const nodes = this.nodes;
|
|
236
|
+
const edges = this.edges;
|
|
237
|
+
return {
|
|
238
|
+
hasNodes: nodes.length > 0,
|
|
239
|
+
hasEdges: edges.length > 0,
|
|
240
|
+
nodeCount: nodes.length,
|
|
241
|
+
edgeCount: edges.length,
|
|
242
|
+
isValid: nodes.length > 0 && edges.length >= 0
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/** Workflow metadata change summary. */
|
|
246
|
+
get metadataChangeSummary() {
|
|
247
|
+
const metadata = this.metadata;
|
|
248
|
+
return {
|
|
249
|
+
createdAt: metadata.createdAt,
|
|
250
|
+
updatedAt: metadata.updatedAt,
|
|
251
|
+
schemaVersion: metadata.schemaVersion ?? WORKFLOW_SCHEMA_VERSION
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Set of all handle IDs currently connected to edges.
|
|
256
|
+
*
|
|
257
|
+
* Used by node components to implement hideUnconnectedHandles.
|
|
258
|
+
*/
|
|
259
|
+
get connectedHandles() {
|
|
260
|
+
const handles = new Set();
|
|
261
|
+
this.edges.forEach((edge) => {
|
|
262
|
+
// Add source handle (output port)
|
|
263
|
+
if (edge.sourceHandle) {
|
|
264
|
+
handles.add(edge.sourceHandle);
|
|
265
|
+
}
|
|
266
|
+
// Add target handle (input port)
|
|
267
|
+
if (edge.targetHandle) {
|
|
268
|
+
handles.add(edge.targetHandle);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
return handles;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* The current monotonic edit version.
|
|
275
|
+
*
|
|
276
|
+
* Use this for the save verification protocol:
|
|
277
|
+
* 1. Before save: `const v = store.editVersion`
|
|
278
|
+
* 2. Include `v` in the save request payload
|
|
279
|
+
* 3. Backend echoes `v` in the response
|
|
280
|
+
* 4. If echoed version matches: `store.markAsSaved()`
|
|
281
|
+
* 5. If not: the save didn't persist the version you submitted — reset from backend response
|
|
282
|
+
*/
|
|
283
|
+
get editVersion() {
|
|
284
|
+
return this.#editVersion;
|
|
285
|
+
}
|
|
286
|
+
/** Whether history recording is enabled. */
|
|
287
|
+
get historyEnabled() {
|
|
288
|
+
return this.#historyEnabled;
|
|
289
|
+
}
|
|
290
|
+
/** Enable or disable history recording (e.g. for bulk operations). */
|
|
291
|
+
set historyEnabled(enabled) {
|
|
292
|
+
this.#historyEnabled = enabled;
|
|
293
|
+
}
|
|
294
|
+
// -----------------------------------------------------------------------
|
|
295
|
+
// Callback setters & save protocol
|
|
296
|
+
// -----------------------------------------------------------------------
|
|
297
|
+
/** Set the dirty state change callback. */
|
|
298
|
+
setOnDirtyStateChange(callback) {
|
|
299
|
+
this.#onDirtyStateChange = callback;
|
|
300
|
+
}
|
|
301
|
+
/** Set the workflow change callback. */
|
|
302
|
+
setOnWorkflowChange(callback) {
|
|
303
|
+
this.#onWorkflowChange = callback;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Mark the current workflow state as saved.
|
|
307
|
+
*
|
|
308
|
+
* Captures the current edit version so isDirty becomes false.
|
|
309
|
+
* Call this after a successful backend save.
|
|
310
|
+
*/
|
|
311
|
+
markAsSaved() {
|
|
312
|
+
const wasDirty = this.#editVersion !== this.#savedVersion;
|
|
313
|
+
this.#savedVersion = this.#editVersion;
|
|
314
|
+
if (wasDirty && this.#onDirtyStateChange) {
|
|
315
|
+
this.#onDirtyStateChange(false);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Set the restoring from history flag.
|
|
320
|
+
*
|
|
321
|
+
* Used internally by the history store when performing undo/redo.
|
|
322
|
+
*/
|
|
323
|
+
setRestoringFromHistory(restoring) {
|
|
324
|
+
this.#restoringFromHistory = restoring;
|
|
325
|
+
}
|
|
326
|
+
// -----------------------------------------------------------------------
|
|
327
|
+
// Internal helpers
|
|
328
|
+
// -----------------------------------------------------------------------
|
|
329
|
+
/**
|
|
330
|
+
* Bump the edit version and notify dirty state change if needed.
|
|
331
|
+
* Called by every mutation action.
|
|
332
|
+
*/
|
|
333
|
+
#bumpVersion() {
|
|
334
|
+
this.#editVersion++;
|
|
335
|
+
// Dirty state just flipped from clean → dirty
|
|
336
|
+
if (this.#editVersion - 1 === this.#savedVersion) {
|
|
337
|
+
if (this.#onDirtyStateChange) {
|
|
338
|
+
this.#onDirtyStateChange(true);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Notify external listeners of a workflow change.
|
|
344
|
+
* Does NOT bump the version — callers that mutate must call #bumpVersion() explicitly.
|
|
345
|
+
*/
|
|
346
|
+
#notifyWorkflowChange(changeType) {
|
|
347
|
+
if (this.#workflow && this.#onWorkflowChange) {
|
|
348
|
+
this.#onWorkflowChange(this.#workflow, changeType);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Push current state to history before making changes.
|
|
353
|
+
*
|
|
354
|
+
* @param description - Description of the change about to be made
|
|
355
|
+
* @param workflow - Optional workflow to push (uses store state if not provided)
|
|
356
|
+
*/
|
|
357
|
+
#pushToHistory(description, workflow) {
|
|
358
|
+
if (!this.#historyEnabled || this.#restoringFromHistory) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const workflowToPush = workflow ?? this.#workflow;
|
|
362
|
+
if (workflowToPush) {
|
|
363
|
+
this.#history.push(workflowToPush, { description });
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// -----------------------------------------------------------------------
|
|
367
|
+
// Mutation actions
|
|
368
|
+
// -----------------------------------------------------------------------
|
|
410
369
|
/**
|
|
411
|
-
* Initialize workflow (from load or new)
|
|
370
|
+
* Initialize workflow (from load or new).
|
|
412
371
|
*
|
|
413
372
|
* This sets the initial saved snapshot, clears dirty state, and initializes history.
|
|
414
373
|
*/
|
|
415
|
-
initialize
|
|
416
|
-
|
|
374
|
+
initialize(workflow) {
|
|
375
|
+
workflow = normalizeWorkflowMetadata(workflow);
|
|
376
|
+
workflow = healMissingNodeIds(workflow);
|
|
377
|
+
this.#workflow = workflow;
|
|
417
378
|
// Reset version counters — workflow is "clean" after initialization
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
if (
|
|
421
|
-
|
|
379
|
+
this.#editVersion = 0;
|
|
380
|
+
this.#savedVersion = 0;
|
|
381
|
+
if (this.#onDirtyStateChange) {
|
|
382
|
+
this.#onDirtyStateChange(false);
|
|
422
383
|
}
|
|
423
384
|
// Initialize history with the loaded workflow
|
|
424
|
-
|
|
425
|
-
}
|
|
385
|
+
this.#history.initialize(workflow);
|
|
386
|
+
}
|
|
426
387
|
/**
|
|
427
|
-
* Update the entire workflow
|
|
388
|
+
* Update the entire workflow.
|
|
428
389
|
*
|
|
429
390
|
* Note: This is typically called from SvelteFlow sync and should not push to history
|
|
430
391
|
* for every small change. History is pushed by specific actions or drag handlers.
|
|
431
392
|
*/
|
|
432
|
-
updateWorkflow
|
|
433
|
-
|
|
434
|
-
bumpVersion();
|
|
435
|
-
notifyWorkflowChange('metadata');
|
|
436
|
-
}
|
|
393
|
+
updateWorkflow(workflow) {
|
|
394
|
+
this.#workflow = workflow;
|
|
395
|
+
this.#bumpVersion();
|
|
396
|
+
this.#notifyWorkflowChange('metadata');
|
|
397
|
+
}
|
|
437
398
|
/**
|
|
438
|
-
* Restore workflow from history (undo/redo)
|
|
399
|
+
* Restore workflow from history (undo/redo).
|
|
439
400
|
*
|
|
440
401
|
* This bypasses history recording to prevent recursive loops.
|
|
441
402
|
*/
|
|
442
|
-
restoreFromHistory
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
if (!workflowState)
|
|
403
|
+
restoreFromHistory(workflow) {
|
|
404
|
+
this.#restoringFromHistory = true;
|
|
405
|
+
workflow = normalizeWorkflowMetadata(workflow);
|
|
406
|
+
this.#workflow = workflow;
|
|
407
|
+
this.#bumpVersion();
|
|
408
|
+
this.#notifyWorkflowChange('metadata');
|
|
409
|
+
this.#restoringFromHistory = false;
|
|
410
|
+
}
|
|
411
|
+
/** Update nodes. */
|
|
412
|
+
updateNodes(nodes) {
|
|
413
|
+
if (!this.#workflow)
|
|
454
414
|
return;
|
|
455
415
|
// Check if nodes have actually changed to prevent infinite loops
|
|
456
|
-
if (!hasWorkflowDataChanged(
|
|
416
|
+
if (!hasWorkflowDataChanged(this.#workflow, nodes, this.#workflow.edges)) {
|
|
457
417
|
return;
|
|
458
418
|
}
|
|
459
419
|
// Generate unique version identifier
|
|
460
420
|
const versionId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
461
|
-
|
|
462
|
-
...
|
|
421
|
+
this.#workflow = {
|
|
422
|
+
...this.#workflow,
|
|
463
423
|
nodes,
|
|
464
|
-
metadata: buildMetadata(
|
|
424
|
+
metadata: buildMetadata(this.#workflow.metadata, {
|
|
465
425
|
versionId,
|
|
466
|
-
updateNumber: (
|
|
426
|
+
updateNumber: (this.#workflow.metadata?.updateNumber ?? 0) + 1
|
|
467
427
|
})
|
|
468
428
|
};
|
|
469
|
-
bumpVersion();
|
|
470
|
-
notifyWorkflowChange('node_move');
|
|
471
|
-
}
|
|
472
|
-
/**
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
updateEdges: (edges) => {
|
|
476
|
-
if (!workflowState)
|
|
429
|
+
this.#bumpVersion();
|
|
430
|
+
this.#notifyWorkflowChange('node_move');
|
|
431
|
+
}
|
|
432
|
+
/** Update edges. */
|
|
433
|
+
updateEdges(edges) {
|
|
434
|
+
if (!this.#workflow)
|
|
477
435
|
return;
|
|
478
436
|
// Check if edges have actually changed to prevent infinite loops
|
|
479
|
-
if (!hasWorkflowDataChanged(
|
|
437
|
+
if (!hasWorkflowDataChanged(this.#workflow, this.#workflow.nodes, edges)) {
|
|
480
438
|
return;
|
|
481
439
|
}
|
|
482
440
|
// Generate unique version identifier
|
|
483
441
|
const versionId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
484
|
-
|
|
485
|
-
...
|
|
442
|
+
this.#workflow = {
|
|
443
|
+
...this.#workflow,
|
|
486
444
|
edges,
|
|
487
|
-
metadata: buildMetadata(
|
|
445
|
+
metadata: buildMetadata(this.#workflow.metadata, {
|
|
488
446
|
versionId,
|
|
489
|
-
updateNumber: (
|
|
447
|
+
updateNumber: (this.#workflow.metadata?.updateNumber ?? 0) + 1
|
|
490
448
|
})
|
|
491
449
|
};
|
|
492
|
-
bumpVersion();
|
|
493
|
-
notifyWorkflowChange('edge_add');
|
|
494
|
-
}
|
|
495
|
-
/**
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
updateName: (name) => {
|
|
499
|
-
if (!workflowState)
|
|
450
|
+
this.#bumpVersion();
|
|
451
|
+
this.#notifyWorkflowChange('edge_add');
|
|
452
|
+
}
|
|
453
|
+
/** Update workflow name. */
|
|
454
|
+
updateName(name) {
|
|
455
|
+
if (!this.#workflow)
|
|
500
456
|
return;
|
|
501
|
-
|
|
502
|
-
...
|
|
457
|
+
this.#workflow = {
|
|
458
|
+
...this.#workflow,
|
|
503
459
|
name,
|
|
504
|
-
metadata: buildMetadata(
|
|
460
|
+
metadata: buildMetadata(this.#workflow.metadata)
|
|
505
461
|
};
|
|
506
|
-
bumpVersion();
|
|
507
|
-
notifyWorkflowChange('name');
|
|
508
|
-
}
|
|
509
|
-
/**
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
pushToHistory('Add node');
|
|
514
|
-
if (!workflowState)
|
|
462
|
+
this.#bumpVersion();
|
|
463
|
+
this.#notifyWorkflowChange('name');
|
|
464
|
+
}
|
|
465
|
+
/** Add a node. */
|
|
466
|
+
addNode(node) {
|
|
467
|
+
this.#pushToHistory('Add node');
|
|
468
|
+
if (!this.#workflow)
|
|
515
469
|
return;
|
|
516
|
-
|
|
517
|
-
...
|
|
518
|
-
nodes: [...
|
|
519
|
-
metadata: buildMetadata(
|
|
470
|
+
this.#workflow = {
|
|
471
|
+
...this.#workflow,
|
|
472
|
+
nodes: [...this.#workflow.nodes, node],
|
|
473
|
+
metadata: buildMetadata(this.#workflow.metadata)
|
|
520
474
|
};
|
|
521
|
-
bumpVersion();
|
|
522
|
-
notifyWorkflowChange('node_add');
|
|
523
|
-
}
|
|
475
|
+
this.#bumpVersion();
|
|
476
|
+
this.#notifyWorkflowChange('node_add');
|
|
477
|
+
}
|
|
524
478
|
/**
|
|
525
|
-
* Remove a node
|
|
479
|
+
* Remove a node.
|
|
526
480
|
*
|
|
527
481
|
* This is an atomic operation that also removes connected edges.
|
|
528
482
|
* A single undo will restore both the node and its edges.
|
|
529
483
|
*/
|
|
530
|
-
removeNode
|
|
531
|
-
pushToHistory('Delete node');
|
|
532
|
-
if (!
|
|
484
|
+
removeNode(nodeId) {
|
|
485
|
+
this.#pushToHistory('Delete node');
|
|
486
|
+
if (!this.#workflow)
|
|
533
487
|
return;
|
|
534
|
-
|
|
535
|
-
...
|
|
536
|
-
nodes:
|
|
537
|
-
edges:
|
|
538
|
-
metadata: buildMetadata(
|
|
488
|
+
this.#workflow = {
|
|
489
|
+
...this.#workflow,
|
|
490
|
+
nodes: this.#workflow.nodes.filter((node) => node.id !== nodeId),
|
|
491
|
+
edges: this.#workflow.edges.filter((edge) => edge.source !== nodeId && edge.target !== nodeId),
|
|
492
|
+
metadata: buildMetadata(this.#workflow.metadata)
|
|
539
493
|
};
|
|
540
|
-
bumpVersion();
|
|
541
|
-
notifyWorkflowChange('node_remove');
|
|
542
|
-
}
|
|
543
|
-
/**
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
pushToHistory('Add connection');
|
|
548
|
-
if (!workflowState)
|
|
494
|
+
this.#bumpVersion();
|
|
495
|
+
this.#notifyWorkflowChange('node_remove');
|
|
496
|
+
}
|
|
497
|
+
/** Add an edge. */
|
|
498
|
+
addEdge(edge) {
|
|
499
|
+
this.#pushToHistory('Add connection');
|
|
500
|
+
if (!this.#workflow)
|
|
549
501
|
return;
|
|
550
|
-
|
|
551
|
-
...
|
|
552
|
-
edges: [...
|
|
553
|
-
metadata: buildMetadata(
|
|
502
|
+
this.#workflow = {
|
|
503
|
+
...this.#workflow,
|
|
504
|
+
edges: [...this.#workflow.edges, edge],
|
|
505
|
+
metadata: buildMetadata(this.#workflow.metadata)
|
|
554
506
|
};
|
|
555
|
-
bumpVersion();
|
|
556
|
-
notifyWorkflowChange('edge_add');
|
|
557
|
-
}
|
|
558
|
-
/**
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
pushToHistory('Delete connection');
|
|
563
|
-
if (!workflowState)
|
|
507
|
+
this.#bumpVersion();
|
|
508
|
+
this.#notifyWorkflowChange('edge_add');
|
|
509
|
+
}
|
|
510
|
+
/** Remove an edge. */
|
|
511
|
+
removeEdge(edgeId) {
|
|
512
|
+
this.#pushToHistory('Delete connection');
|
|
513
|
+
if (!this.#workflow)
|
|
564
514
|
return;
|
|
565
|
-
|
|
566
|
-
...
|
|
567
|
-
edges:
|
|
568
|
-
metadata: buildMetadata(
|
|
515
|
+
this.#workflow = {
|
|
516
|
+
...this.#workflow,
|
|
517
|
+
edges: this.#workflow.edges.filter((edge) => edge.id !== edgeId),
|
|
518
|
+
metadata: buildMetadata(this.#workflow.metadata)
|
|
569
519
|
};
|
|
570
|
-
bumpVersion();
|
|
571
|
-
notifyWorkflowChange('edge_remove');
|
|
572
|
-
}
|
|
520
|
+
this.#bumpVersion();
|
|
521
|
+
this.#notifyWorkflowChange('edge_remove');
|
|
522
|
+
}
|
|
573
523
|
/**
|
|
574
|
-
* Update a specific node
|
|
524
|
+
* Update a specific node.
|
|
575
525
|
*
|
|
576
526
|
* Used for config changes. Pushes to history for undo support.
|
|
577
527
|
*/
|
|
578
|
-
updateNode
|
|
579
|
-
pushToHistory('Update node config');
|
|
580
|
-
if (!
|
|
528
|
+
updateNode(nodeId, updates) {
|
|
529
|
+
this.#pushToHistory('Update node config');
|
|
530
|
+
if (!this.#workflow)
|
|
581
531
|
return;
|
|
582
|
-
|
|
583
|
-
...
|
|
584
|
-
nodes:
|
|
585
|
-
metadata: buildMetadata(
|
|
532
|
+
this.#workflow = {
|
|
533
|
+
...this.#workflow,
|
|
534
|
+
nodes: this.#workflow.nodes.map((node) => node.id === nodeId ? { ...node, ...updates } : node),
|
|
535
|
+
metadata: buildMetadata(this.#workflow.metadata)
|
|
586
536
|
};
|
|
587
|
-
bumpVersion();
|
|
588
|
-
notifyWorkflowChange('node_config');
|
|
589
|
-
}
|
|
537
|
+
this.#bumpVersion();
|
|
538
|
+
this.#notifyWorkflowChange('node_config');
|
|
539
|
+
}
|
|
590
540
|
/**
|
|
591
|
-
* Clear the workflow
|
|
541
|
+
* Clear the workflow.
|
|
592
542
|
*
|
|
593
543
|
* Resets the workflow and clears history.
|
|
594
544
|
*/
|
|
595
|
-
clear
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
if (
|
|
601
|
-
|
|
545
|
+
clear() {
|
|
546
|
+
this.#workflow = null;
|
|
547
|
+
this.#editVersion = 0;
|
|
548
|
+
this.#savedVersion = 0;
|
|
549
|
+
this.#history.clear();
|
|
550
|
+
if (this.#onDirtyStateChange) {
|
|
551
|
+
this.#onDirtyStateChange(false);
|
|
602
552
|
}
|
|
603
|
-
}
|
|
604
|
-
/**
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
updateMetadata: (metadata) => {
|
|
608
|
-
if (!workflowState)
|
|
553
|
+
}
|
|
554
|
+
/** Update workflow metadata. */
|
|
555
|
+
updateMetadata(metadata) {
|
|
556
|
+
if (!this.#workflow)
|
|
609
557
|
return;
|
|
610
|
-
|
|
611
|
-
...
|
|
612
|
-
metadata: buildMetadata(
|
|
558
|
+
this.#workflow = {
|
|
559
|
+
...this.#workflow,
|
|
560
|
+
metadata: buildMetadata(this.#workflow.metadata, metadata)
|
|
613
561
|
};
|
|
614
|
-
bumpVersion();
|
|
615
|
-
notifyWorkflowChange('metadata');
|
|
616
|
-
}
|
|
562
|
+
this.#bumpVersion();
|
|
563
|
+
this.#notifyWorkflowChange('metadata');
|
|
564
|
+
}
|
|
617
565
|
/**
|
|
618
|
-
* Batch update nodes and edges
|
|
566
|
+
* Batch update nodes and edges.
|
|
619
567
|
*
|
|
620
568
|
* Useful for complex operations that update multiple things at once.
|
|
621
569
|
* Creates a single history entry for the entire batch.
|
|
622
570
|
*/
|
|
623
|
-
batchUpdate
|
|
624
|
-
pushToHistory('Batch update');
|
|
625
|
-
if (!
|
|
571
|
+
batchUpdate(updates) {
|
|
572
|
+
this.#pushToHistory('Batch update');
|
|
573
|
+
if (!this.#workflow)
|
|
626
574
|
return;
|
|
627
|
-
|
|
628
|
-
...
|
|
575
|
+
this.#workflow = {
|
|
576
|
+
...this.#workflow,
|
|
629
577
|
...(updates.nodes && { nodes: updates.nodes }),
|
|
630
578
|
...(updates.edges && { edges: updates.edges }),
|
|
631
579
|
...(updates.name && { name: updates.name }),
|
|
@@ -633,32 +581,32 @@ export const workflowActions = {
|
|
|
633
581
|
description: updates.description
|
|
634
582
|
}),
|
|
635
583
|
...(updates.config !== undefined && { config: updates.config }),
|
|
636
|
-
metadata: buildMetadata(
|
|
584
|
+
metadata: buildMetadata(this.#workflow.metadata, updates.metadata ?? undefined)
|
|
637
585
|
};
|
|
638
|
-
bumpVersion();
|
|
639
|
-
notifyWorkflowChange('metadata');
|
|
640
|
-
}
|
|
586
|
+
this.#bumpVersion();
|
|
587
|
+
this.#notifyWorkflowChange('metadata');
|
|
588
|
+
}
|
|
641
589
|
/**
|
|
642
590
|
* Swap a node — atomically replaces nodes and edges with a descriptive history entry.
|
|
643
591
|
*
|
|
644
592
|
* Unlike batchUpdate, this uses `"node_swap"` as the change type and
|
|
645
593
|
* records a meaningful description for the undo history.
|
|
646
594
|
*/
|
|
647
|
-
swapNode
|
|
648
|
-
pushToHistory(updates.description ?? 'Swap node');
|
|
649
|
-
if (!
|
|
595
|
+
swapNode(updates) {
|
|
596
|
+
this.#pushToHistory(updates.description ?? 'Swap node');
|
|
597
|
+
if (!this.#workflow)
|
|
650
598
|
return;
|
|
651
|
-
|
|
652
|
-
...
|
|
599
|
+
this.#workflow = {
|
|
600
|
+
...this.#workflow,
|
|
653
601
|
nodes: updates.nodes,
|
|
654
602
|
edges: updates.edges,
|
|
655
|
-
metadata: buildMetadata(
|
|
603
|
+
metadata: buildMetadata(this.#workflow.metadata)
|
|
656
604
|
};
|
|
657
|
-
bumpVersion();
|
|
658
|
-
notifyWorkflowChange('node_swap');
|
|
659
|
-
}
|
|
605
|
+
this.#bumpVersion();
|
|
606
|
+
this.#notifyWorkflowChange('node_swap');
|
|
607
|
+
}
|
|
660
608
|
/**
|
|
661
|
-
* Push current state to history manually
|
|
609
|
+
* Push current state to history manually.
|
|
662
610
|
*
|
|
663
611
|
* Use this before operations that modify the workflow through other means
|
|
664
612
|
* (e.g., drag operations handled by SvelteFlow directly).
|
|
@@ -666,7 +614,7 @@ export const workflowActions = {
|
|
|
666
614
|
* @param description - Description of the upcoming change
|
|
667
615
|
* @param workflow - Optional workflow to push (uses store state if not provided)
|
|
668
616
|
*/
|
|
669
|
-
pushHistory
|
|
670
|
-
pushToHistory(description, workflow);
|
|
617
|
+
pushHistory(description, workflow) {
|
|
618
|
+
this.#pushToHistory(description, workflow);
|
|
671
619
|
}
|
|
672
|
-
}
|
|
620
|
+
}
|