@d34dman/flowdrop 0.0.1 → 0.0.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/README.md +307 -215
- package/dist/adapters/WorkflowAdapter.d.ts +1 -1
- package/dist/adapters/WorkflowAdapter.js +30 -30
- package/dist/api/client.d.ts +24 -1
- package/dist/api/client.js +55 -38
- package/dist/api/enhanced-client.d.ts +46 -0
- package/dist/api/enhanced-client.js +211 -0
- package/dist/clients/ApiClient.d.ts +19 -23
- package/dist/clients/ApiClient.js +36 -34
- package/dist/components/App.svelte +1299 -230
- package/dist/components/App.svelte.d.ts +21 -1
- package/dist/components/CanvasBanner.svelte +50 -44
- package/dist/components/CanvasBanner.svelte.d.ts +5 -19
- package/dist/components/ConfigForm.svelte +555 -0
- package/dist/components/ConfigForm.svelte.d.ts +32 -0
- package/dist/components/ConfigModal.svelte +261 -0
- package/dist/components/ConfigModal.svelte.d.ts +31 -0
- package/dist/components/ConfigSidebar.svelte +934 -0
- package/dist/components/ConfigSidebar.svelte.d.ts +51 -0
- package/dist/components/ConnectionLine.svelte +32 -0
- package/dist/components/ConnectionLine.svelte.d.ts +3 -0
- package/dist/components/GatewayNode.svelte +471 -0
- package/dist/components/GatewayNode.svelte.d.ts +15 -0
- package/dist/components/LoadingSpinner.svelte +23 -23
- package/dist/components/LoadingSpinner.svelte.d.ts +1 -1
- package/dist/components/Logo.svelte +82 -0
- package/dist/components/Logo.svelte.d.ts +26 -0
- package/dist/components/LogsSidebar.svelte +565 -0
- package/dist/components/LogsSidebar.svelte.d.ts +34 -0
- package/dist/components/MarkdownDisplay.svelte +28 -0
- package/dist/components/MarkdownDisplay.svelte.d.ts +7 -0
- package/dist/components/Navbar.svelte +663 -0
- package/dist/components/Navbar.svelte.d.ts +21 -0
- package/dist/components/NodeSidebar.svelte +629 -488
- package/dist/components/NodeSidebar.svelte.d.ts +1 -2
- package/dist/components/NodeStatusOverlay.svelte +327 -0
- package/dist/components/NodeStatusOverlay.svelte.d.ts +11 -0
- package/dist/components/NotesNode.svelte +566 -0
- package/dist/components/NotesNode.svelte.d.ts +43 -0
- package/dist/components/PipelineStatus.svelte +331 -0
- package/dist/components/PipelineStatus.svelte.d.ts +18 -0
- package/dist/components/SimpleNode.svelte +447 -0
- package/dist/components/SimpleNode.svelte.d.ts +24 -0
- package/dist/components/SquareNode.svelte +346 -0
- package/dist/components/SquareNode.svelte.d.ts +24 -0
- package/dist/components/StatusIcon.svelte +112 -0
- package/dist/components/StatusIcon.svelte.d.ts +10 -0
- package/dist/components/StatusLabel.svelte +33 -0
- package/dist/components/StatusLabel.svelte.d.ts +7 -0
- package/dist/components/ToolNode.svelte +385 -0
- package/dist/components/ToolNode.svelte.d.ts +36 -0
- package/dist/components/UniversalNode.svelte +126 -0
- package/dist/components/UniversalNode.svelte.d.ts +15 -0
- package/dist/components/WorkflowEditor.svelte +871 -528
- package/dist/components/WorkflowEditor.svelte.d.ts +15 -5
- package/dist/components/WorkflowNode.svelte +428 -542
- package/dist/components/WorkflowNode.svelte.d.ts +7 -3
- package/dist/config/apiConfig.d.ts +33 -0
- package/dist/config/apiConfig.js +39 -0
- package/dist/config/defaultPortConfig.d.ts +6 -0
- package/dist/config/defaultPortConfig.js +192 -0
- package/dist/config/demo.d.ts +58 -0
- package/dist/config/demo.js +142 -0
- package/dist/config/endpoints.d.ts +106 -0
- package/dist/config/endpoints.js +128 -0
- package/dist/data/samples.d.ts +38 -4
- package/dist/data/samples.js +2789 -737
- package/dist/examples/adapter-usage.d.ts +4 -4
- package/dist/examples/adapter-usage.js +21 -26
- package/dist/examples/api-client-usage.d.ts +6 -6
- package/dist/examples/api-client-usage.js +55 -54
- package/dist/index.d.ts +23 -15
- package/dist/index.js +23 -15
- package/dist/mocks/app-environment.d.ts +8 -0
- package/dist/mocks/app-environment.js +16 -0
- package/dist/mocks/app-forms.d.ts +2 -0
- package/dist/mocks/app-forms.js +21 -0
- package/dist/mocks/app-navigation.d.ts +5 -0
- package/dist/mocks/app-navigation.js +34 -0
- package/dist/mocks/app-stores.d.ts +14 -0
- package/dist/mocks/app-stores.js +26 -0
- package/dist/services/api.d.ts +13 -3
- package/dist/services/api.js +91 -36
- package/dist/services/globalSave.d.ts +20 -0
- package/dist/services/globalSave.js +165 -0
- package/dist/services/nodeExecutionService.d.ts +63 -0
- package/dist/services/nodeExecutionService.js +261 -0
- package/dist/services/portConfigApi.d.ts +14 -0
- package/dist/services/portConfigApi.js +69 -0
- package/dist/services/toastService.d.ts +147 -0
- package/dist/services/toastService.js +235 -0
- package/dist/services/workflowStorage.d.ts +2 -2
- package/dist/services/workflowStorage.js +10 -10
- package/dist/stores/workflowStore.d.ts +53 -0
- package/dist/stores/workflowStore.js +264 -0
- package/dist/styles/base.css +896 -363
- package/dist/svelte-app.d.ts +52 -5
- package/dist/svelte-app.js +128 -6
- package/dist/types/config.d.ts +291 -0
- package/dist/types/config.js +4 -0
- package/dist/types/index.d.ts +231 -19
- package/dist/types/index.js +1 -1
- package/dist/utils/colors.d.ts +67 -33
- package/dist/utils/colors.js +183 -118
- package/dist/utils/config.d.ts +41 -0
- package/dist/utils/config.js +248 -0
- package/dist/utils/connections.d.ts +40 -3
- package/dist/utils/connections.js +115 -44
- package/dist/utils/icons.d.ts +1 -1
- package/dist/utils/icons.js +71 -70
- package/dist/utils/nodeStatus.d.ts +53 -0
- package/dist/utils/nodeStatus.js +183 -0
- package/dist/utils/nodeTypes.d.ts +57 -0
- package/dist/utils/nodeTypes.js +109 -0
- package/dist/utils/nodeWrapper.d.ts +39 -0
- package/dist/utils/nodeWrapper.js +62 -0
- package/package.json +129 -97
- package/dist/components/Node.svelte +0 -38
- package/dist/components/Node.svelte.d.ts +0 -4
|
@@ -5,538 +5,881 @@
|
|
|
5
5
|
-->
|
|
6
6
|
|
|
7
7
|
<script lang="ts">
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
$effect(() => {
|
|
43
|
-
console.log('WorkflowEditor: props received:', {
|
|
44
|
-
nodes: props.nodes?.length || 0,
|
|
45
|
-
workflow: props.workflow ? 'present' : 'none',
|
|
46
|
-
apiBaseUrl: props.apiBaseUrl
|
|
47
|
-
});
|
|
48
|
-
console.log('WorkflowEditor: props.nodes content:', props.nodes);
|
|
49
|
-
|
|
50
|
-
if (!isInitialized) {
|
|
51
|
-
if (props.workflow) {
|
|
52
|
-
flowNodes = props.workflow.nodes || [];
|
|
53
|
-
flowEdges = props.workflow.edges || [];
|
|
54
|
-
} else {
|
|
55
|
-
flowNodes = [];
|
|
56
|
-
flowEdges = [];
|
|
57
|
-
}
|
|
58
|
-
isInitialized = true;
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
let workflowName = $state(props.workflow?.name || "Untitled Workflow");
|
|
63
|
-
let isEditingTitle = $state(false);
|
|
64
|
-
|
|
65
|
-
// Node types for Svelte Flow
|
|
66
|
-
const nodeTypes = {
|
|
67
|
-
workflowNode: WorkflowNode
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
$effect(() => {
|
|
71
|
-
if (props.apiBaseUrl) {
|
|
72
|
-
setApiBaseUrl(props.apiBaseUrl);
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Clear workflow
|
|
78
|
-
*/
|
|
79
|
-
function clearWorkflow(): void {
|
|
80
|
-
flowNodes = [];
|
|
81
|
-
flowEdges = [];
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Save workflow
|
|
86
|
-
*/
|
|
87
|
-
async function saveWorkflow(): Promise<void> {
|
|
88
|
-
try {
|
|
89
|
-
const workflow: Workflow = {
|
|
90
|
-
id: props.workflow?.id || uuidv4(),
|
|
91
|
-
name: workflowName,
|
|
92
|
-
nodes: flowNodes,
|
|
93
|
-
edges: flowEdges,
|
|
94
|
-
metadata: {
|
|
95
|
-
version: "1.0.0",
|
|
96
|
-
createdAt: props.workflow?.metadata?.createdAt || new Date().toISOString(),
|
|
97
|
-
updatedAt: new Date().toISOString()
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const savedWorkflow = await workflowApi.saveWorkflow(workflow);
|
|
102
|
-
console.log("Workflow saved successfully:", savedWorkflow);
|
|
103
|
-
|
|
104
|
-
// Update the workflow ID if it was a new workflow
|
|
105
|
-
if (!props.workflow?.id) {
|
|
106
|
-
// Note: In a real app, you'd want to update the parent component's workflow prop
|
|
107
|
-
console.log("New workflow created with ID:", savedWorkflow.id);
|
|
108
|
-
}
|
|
109
|
-
} catch (error) {
|
|
110
|
-
console.error("Failed to save workflow:", error);
|
|
111
|
-
// Here you would typically show a user-friendly error message
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Export workflow
|
|
117
|
-
*/
|
|
118
|
-
function exportWorkflow(): void {
|
|
119
|
-
const workflow: Workflow = {
|
|
120
|
-
id: props.workflow?.id || uuidv4(),
|
|
121
|
-
name: workflowName,
|
|
122
|
-
nodes: flowNodes,
|
|
123
|
-
edges: flowEdges,
|
|
124
|
-
metadata: {
|
|
125
|
-
version: "1.0.0",
|
|
126
|
-
createdAt: props.workflow?.metadata?.createdAt || new Date().toISOString(),
|
|
127
|
-
updatedAt: new Date().toISOString()
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
const dataStr = JSON.stringify(workflow, null, 2);
|
|
132
|
-
const dataBlob = new Blob([dataStr], { type: "application/json" });
|
|
133
|
-
const url = URL.createObjectURL(dataBlob);
|
|
134
|
-
const link = document.createElement("a");
|
|
135
|
-
link.href = url;
|
|
136
|
-
link.download = `${workflow.name}.json`;
|
|
137
|
-
link.click();
|
|
138
|
-
URL.revokeObjectURL(url);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Check if workflow has cycles
|
|
144
|
-
*/
|
|
145
|
-
function checkWorkflowCycles(): boolean {
|
|
146
|
-
return hasCycles(flowNodes, flowEdges);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Handle title editing
|
|
151
|
-
*/
|
|
152
|
-
function startTitleEdit(): void {
|
|
153
|
-
isEditingTitle = true;
|
|
154
|
-
// Focus the input on next tick
|
|
155
|
-
setTimeout(() => {
|
|
156
|
-
const input = document.querySelector('#workflow-title') as HTMLInputElement;
|
|
157
|
-
if (input) input.focus();
|
|
158
|
-
}, 0);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Save title changes
|
|
163
|
-
*/
|
|
164
|
-
function saveTitle(): void {
|
|
165
|
-
isEditingTitle = false;
|
|
166
|
-
// Update the workflow name in the save/export functions
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Cancel title editing
|
|
171
|
-
*/
|
|
172
|
-
function cancelTitleEdit(): void {
|
|
173
|
-
isEditingTitle = false;
|
|
174
|
-
workflowName = props.workflow?.name || "Untitled Workflow";
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Handle title input keydown
|
|
179
|
-
*/
|
|
180
|
-
function handleTitleKeydown(event: KeyboardEvent): void {
|
|
181
|
-
if (event.key === "Enter") {
|
|
182
|
-
saveTitle();
|
|
183
|
-
} else if (event.key === "Escape") {
|
|
184
|
-
cancelTitleEdit();
|
|
185
|
-
}
|
|
186
|
-
}
|
|
8
|
+
import {
|
|
9
|
+
SvelteFlow,
|
|
10
|
+
ConnectionLineType,
|
|
11
|
+
MarkerType,
|
|
12
|
+
Controls,
|
|
13
|
+
Background,
|
|
14
|
+
BackgroundVariant,
|
|
15
|
+
MiniMap,
|
|
16
|
+
SvelteFlowProvider
|
|
17
|
+
} from '@xyflow/svelte';
|
|
18
|
+
import '@xyflow/svelte/dist/style.css';
|
|
19
|
+
import WorkflowNode from './WorkflowNode.svelte';
|
|
20
|
+
import NotesNode from './NotesNode.svelte';
|
|
21
|
+
import SimpleNode from './SimpleNode.svelte';
|
|
22
|
+
import SquareNode from './SquareNode.svelte';
|
|
23
|
+
import ToolNode from './ToolNode.svelte';
|
|
24
|
+
import type {
|
|
25
|
+
WorkflowNode as WorkflowNodeType,
|
|
26
|
+
NodeMetadata,
|
|
27
|
+
Workflow,
|
|
28
|
+
WorkflowEdge
|
|
29
|
+
} from '../types/index.js';
|
|
30
|
+
import { hasCycles } from '../utils/connections.js';
|
|
31
|
+
import CanvasBanner from './CanvasBanner.svelte';
|
|
32
|
+
import { workflowApi, nodeApi, setApiBaseUrl, setEndpointConfig } from '../services/api.js';
|
|
33
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
34
|
+
import { tick } from 'svelte';
|
|
35
|
+
import type { EndpointConfig } from '../config/endpoints.js';
|
|
36
|
+
import ConnectionLine from './ConnectionLine.svelte';
|
|
37
|
+
import { resolveComponentName } from '../utils/nodeTypes.js';
|
|
38
|
+
import { workflowStore, workflowActions } from '../stores/workflowStore.js';
|
|
39
|
+
import { nodeExecutionService } from '../services/nodeExecutionService.js';
|
|
40
|
+
import type { NodeExecutionInfo } from '../types/index.js';
|
|
41
|
+
import UniversalNode from './UniversalNode.svelte';
|
|
187
42
|
|
|
43
|
+
interface Props {
|
|
44
|
+
nodes?: NodeMetadata[];
|
|
45
|
+
// workflow?: Workflow; // Removed - use global store directly
|
|
46
|
+
endpointConfig?: EndpointConfig;
|
|
47
|
+
height?: string | number;
|
|
48
|
+
width?: string | number;
|
|
49
|
+
isConfigSidebarOpen?: boolean;
|
|
50
|
+
selectedNodeForConfig?: WorkflowNodeType | null;
|
|
51
|
+
openConfigSidebar?: (node: WorkflowNodeType) => void;
|
|
52
|
+
closeConfigSidebar?: () => void;
|
|
53
|
+
// New configuration options for pipeline status mode
|
|
54
|
+
lockWorkflow?: boolean;
|
|
55
|
+
readOnly?: boolean;
|
|
56
|
+
nodeStatuses?: Record<string, 'pending' | 'running' | 'completed' | 'error'>;
|
|
57
|
+
// Pipeline ID for fetching node execution info from jobs
|
|
58
|
+
pipelineId?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let props: Props = $props();
|
|
62
|
+
|
|
63
|
+
// Debug logging for props
|
|
64
|
+
$effect(() => {
|
|
65
|
+
console.log('🔧 WorkflowEditor: Props received:', {
|
|
66
|
+
hasOpenConfigSidebar: !!props.openConfigSidebar,
|
|
67
|
+
hasCloseConfigSidebar: !!props.closeConfigSidebar,
|
|
68
|
+
selectedNodeForConfig: props.selectedNodeForConfig?.id,
|
|
69
|
+
isConfigSidebarOpen: props.isConfigSidebarOpen
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Initialize from props only once, not on every re-render
|
|
74
|
+
let availableNodes = $state<NodeMetadata[]>([]);
|
|
75
|
+
|
|
76
|
+
// Create a local currentWorkflow variable that we can control directly
|
|
77
|
+
let currentWorkflow = $state<Workflow | null>(null);
|
|
78
|
+
|
|
79
|
+
// Initialize currentWorkflow from global store
|
|
80
|
+
$effect(() => {
|
|
81
|
+
if ($workflowStore) {
|
|
82
|
+
currentWorkflow = $workflowStore;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Create local reactive variables that sync with currentWorkflow
|
|
87
|
+
let flowNodes = $state<WorkflowNodeType[]>([]);
|
|
88
|
+
let flowEdges = $state<WorkflowEdge[]>([]);
|
|
89
|
+
|
|
90
|
+
// Sync local state with currentWorkflow
|
|
91
|
+
let loadExecutionInfoTimeout: NodeJS.Timeout | null = null;
|
|
92
|
+
|
|
93
|
+
$effect(() => {
|
|
94
|
+
if (currentWorkflow) {
|
|
95
|
+
flowNodes = currentWorkflow.nodes.map((node) => ({
|
|
96
|
+
...node,
|
|
97
|
+
data: {
|
|
98
|
+
...node.data,
|
|
99
|
+
onConfigOpen: props.openConfigSidebar
|
|
100
|
+
}
|
|
101
|
+
}));
|
|
102
|
+
flowEdges = currentWorkflow.edges;
|
|
103
|
+
|
|
104
|
+
// Debounce node execution info loading to prevent rapid calls
|
|
105
|
+
if (loadExecutionInfoTimeout) {
|
|
106
|
+
clearTimeout(loadExecutionInfoTimeout);
|
|
107
|
+
}
|
|
108
|
+
loadExecutionInfoTimeout = setTimeout(() => {
|
|
109
|
+
loadNodeExecutionInfo();
|
|
110
|
+
}, 100);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Function to update the global store when currentWorkflow changes
|
|
115
|
+
function updateGlobalStore(): void {
|
|
116
|
+
if (currentWorkflow) {
|
|
117
|
+
console.log('🔍 WorkflowEditor: Updating global store from currentWorkflow:', {
|
|
118
|
+
nodeCount: currentWorkflow.nodes.length,
|
|
119
|
+
edgeCount: currentWorkflow.edges.length,
|
|
120
|
+
nodePositions: currentWorkflow.nodes.map((node) => ({
|
|
121
|
+
id: node.id,
|
|
122
|
+
position: node.position
|
|
123
|
+
})),
|
|
124
|
+
workflowName: currentWorkflow.name,
|
|
125
|
+
versionId: currentWorkflow.metadata?.versionId,
|
|
126
|
+
updateNumber: currentWorkflow.metadata?.updateNumber
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
workflowActions.updateWorkflow(currentWorkflow);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Load node execution information for all nodes in the workflow
|
|
135
|
+
*/
|
|
136
|
+
async function loadNodeExecutionInfo(): Promise<void> {
|
|
137
|
+
if (!currentWorkflow?.nodes) return;
|
|
138
|
+
|
|
139
|
+
// Only load execution info if we have a pipelineId (for pipeline status mode)
|
|
140
|
+
if (!props.pipelineId) return;
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const nodeIds = currentWorkflow.nodes.map((node) => node.id);
|
|
144
|
+
const executionInfo = await nodeExecutionService.getMultipleNodeExecutionInfo(
|
|
145
|
+
nodeIds,
|
|
146
|
+
props.pipelineId
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Update nodes with execution information without triggering reactive updates
|
|
150
|
+
const updatedNodes = currentWorkflow.nodes.map((node) => ({
|
|
151
|
+
...node,
|
|
152
|
+
data: {
|
|
153
|
+
...node.data,
|
|
154
|
+
executionInfo: executionInfo[node.id] || {
|
|
155
|
+
status: 'idle',
|
|
156
|
+
executionCount: 0,
|
|
157
|
+
isExecuting: false
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
// Update the flow nodes to reflect the changes
|
|
163
|
+
flowNodes = updatedNodes.map((node) => ({
|
|
164
|
+
...node,
|
|
165
|
+
data: {
|
|
166
|
+
...node.data,
|
|
167
|
+
onConfigOpen: props.openConfigSidebar
|
|
168
|
+
}
|
|
169
|
+
}));
|
|
170
|
+
|
|
171
|
+
// Update currentWorkflow without triggering reactive effects
|
|
172
|
+
currentWorkflow.nodes = updatedNodes;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.error('Failed to load node execution info:', error);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Function to update currentWorkflow when SvelteFlow changes nodes/edges
|
|
179
|
+
function updateCurrentWorkflowFromSvelteFlow(): void {
|
|
180
|
+
if (currentWorkflow) {
|
|
181
|
+
currentWorkflow = {
|
|
182
|
+
...currentWorkflow,
|
|
183
|
+
nodes: flowNodes,
|
|
184
|
+
edges: flowEdges,
|
|
185
|
+
metadata: {
|
|
186
|
+
...currentWorkflow.metadata,
|
|
187
|
+
updatedAt: new Date().toISOString(),
|
|
188
|
+
versionId: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
189
|
+
updateNumber: (currentWorkflow.metadata?.updateNumber || 0) + 1
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Update the global store
|
|
194
|
+
updateGlobalStore();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Track previous values to detect changes from SvelteFlow
|
|
199
|
+
let previousNodes = $state<WorkflowNodeType[]>([]);
|
|
200
|
+
let previousEdges = $state<WorkflowEdge[]>([]);
|
|
201
|
+
|
|
202
|
+
// Watch for changes from SvelteFlow and update currentWorkflow
|
|
203
|
+
$effect(() => {
|
|
204
|
+
// Check if nodes have changed from SvelteFlow
|
|
205
|
+
const nodesChanged = JSON.stringify(flowNodes) !== JSON.stringify(previousNodes);
|
|
206
|
+
const edgesChanged = JSON.stringify(flowEdges) !== JSON.stringify(previousEdges);
|
|
207
|
+
|
|
208
|
+
if ((nodesChanged || edgesChanged) && currentWorkflow) {
|
|
209
|
+
console.log('🔍 WorkflowEditor: SvelteFlow changed nodes/edges, updating currentWorkflow');
|
|
210
|
+
updateCurrentWorkflowFromSvelteFlow();
|
|
211
|
+
|
|
212
|
+
// Update previous values
|
|
213
|
+
previousNodes = JSON.parse(JSON.stringify(flowNodes));
|
|
214
|
+
previousEdges = JSON.parse(JSON.stringify(flowEdges));
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// The global store should be initialized by the parent App component
|
|
219
|
+
|
|
220
|
+
// Sidebar is now always visible - removed toggle functionality
|
|
221
|
+
|
|
222
|
+
// Node types for Svelte Flow - using UniversalNode for all node types
|
|
223
|
+
// All nodes use 'universalNode' type, and UniversalNode handles internal switching
|
|
224
|
+
// Include legacy types for backward compatibility with existing workflows
|
|
225
|
+
const nodeTypes = {
|
|
226
|
+
universalNode: UniversalNode,
|
|
227
|
+
// Legacy types for backward compatibility
|
|
228
|
+
workflowNode: UniversalNode,
|
|
229
|
+
note: UniversalNode,
|
|
230
|
+
simple: UniversalNode,
|
|
231
|
+
square: UniversalNode,
|
|
232
|
+
tool: UniversalNode,
|
|
233
|
+
gateway: UniversalNode
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// Handle arrows in our custom connection handler
|
|
237
|
+
const defaultEdgeOptions = {};
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Handle new connections between nodes
|
|
241
|
+
* Let SvelteFlow handle edge creation, styling will be applied via reactive effects
|
|
242
|
+
*/
|
|
243
|
+
async function handleConnect(connection: {
|
|
244
|
+
source: string;
|
|
245
|
+
target: string;
|
|
246
|
+
sourceHandle?: string;
|
|
247
|
+
targetHandle?: string;
|
|
248
|
+
}): Promise<void> {
|
|
249
|
+
// SvelteFlow will automatically create the edge due to bind:edges
|
|
250
|
+
console.log('Connection created:', connection);
|
|
251
|
+
|
|
252
|
+
// Wait for DOM update before applying styling
|
|
253
|
+
await tick();
|
|
254
|
+
|
|
255
|
+
// Apply styling to the new edge (including arrows)
|
|
256
|
+
updateExistingEdgeStyles();
|
|
257
|
+
|
|
258
|
+
// Update currentWorkflow with the new edge
|
|
259
|
+
if (currentWorkflow) {
|
|
260
|
+
updateCurrentWorkflowFromSvelteFlow();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Apply custom styling to connection edges based on rules:
|
|
266
|
+
* - Dashed lines for connections to tool nodes
|
|
267
|
+
* - Arrow markers pointing towards input ports
|
|
268
|
+
*/
|
|
269
|
+
function applyConnectionStyling(
|
|
270
|
+
edge: WorkflowEdge,
|
|
271
|
+
sourceNode: WorkflowNodeType,
|
|
272
|
+
targetNode: WorkflowNodeType
|
|
273
|
+
): void {
|
|
274
|
+
// Rule 1: Dashed lines for tool nodes
|
|
275
|
+
// A node is a tool node when it uses the ToolNode component,
|
|
276
|
+
// which happens when sourceNode.type === 'tool'
|
|
277
|
+
const isToolNode = sourceNode.type === 'tool';
|
|
278
|
+
|
|
279
|
+
// Use inline styles for dashed lines (more reliable than CSS classes)
|
|
280
|
+
if (isToolNode) {
|
|
281
|
+
edge.style = 'stroke-dasharray: 0 4 0; stroke: amber !important;';
|
|
282
|
+
edge.class = 'flowdrop--edge--tool';
|
|
283
|
+
} else {
|
|
284
|
+
edge.style = 'stroke: grey;';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Store metadata in edge data for debugging
|
|
288
|
+
edge.data = {
|
|
289
|
+
...edge.data,
|
|
290
|
+
isToolConnection: isToolNode,
|
|
291
|
+
targetNodeType: targetNode.type,
|
|
292
|
+
targetCategory: targetNode.data.metadata.category
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// Rule 2: Always add arrow pointing towards input port
|
|
296
|
+
// This replaces the default arrows we removed
|
|
297
|
+
if (!isToolNode) {
|
|
298
|
+
edge.markerEnd = {
|
|
299
|
+
type: MarkerType.ArrowClosed,
|
|
300
|
+
width: 16,
|
|
301
|
+
height: 16,
|
|
302
|
+
color: 'grey'
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Update existing edges with our custom styling rules
|
|
309
|
+
* This ensures all edges (including existing ones) follow our rules
|
|
310
|
+
*/
|
|
311
|
+
async function updateExistingEdgeStyles(): Promise<void> {
|
|
312
|
+
// Wait for any pending DOM updates
|
|
313
|
+
await tick();
|
|
314
|
+
|
|
315
|
+
const updatedEdges = flowEdges.map((edge) => {
|
|
316
|
+
// Find source and target nodes
|
|
317
|
+
const sourceNode = flowNodes.find((node) => node.id === edge.source);
|
|
318
|
+
const targetNode = flowNodes.find((node) => node.id === edge.target);
|
|
319
|
+
|
|
320
|
+
if (!sourceNode || !targetNode) {
|
|
321
|
+
console.warn('Could not find nodes for edge:', edge.id);
|
|
322
|
+
return edge;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Create a copy of the edge and apply styling
|
|
326
|
+
const updatedEdge = { ...edge };
|
|
327
|
+
applyConnectionStyling(updatedEdge, sourceNode, targetNode);
|
|
328
|
+
|
|
329
|
+
return updatedEdge;
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Update currentWorkflow with the styled edges
|
|
333
|
+
if (currentWorkflow) {
|
|
334
|
+
currentWorkflow = {
|
|
335
|
+
...currentWorkflow,
|
|
336
|
+
edges: updatedEdges,
|
|
337
|
+
metadata: {
|
|
338
|
+
...currentWorkflow.metadata,
|
|
339
|
+
updatedAt: new Date().toISOString(),
|
|
340
|
+
versionId: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
341
|
+
updateNumber: (currentWorkflow.metadata?.updateNumber || 0) + 1
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// Update the global store
|
|
346
|
+
updateGlobalStore();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Edge styling will be handled when edges are first created or manually updated
|
|
351
|
+
|
|
352
|
+
// Configure endpoints and load nodes when props change
|
|
353
|
+
$effect(() => {
|
|
354
|
+
if (props.endpointConfig) {
|
|
355
|
+
setEndpointConfig(props.endpointConfig);
|
|
356
|
+
// Load nodes after setting endpoint config
|
|
357
|
+
loadNodesFromApi();
|
|
358
|
+
} else if (props.nodes) {
|
|
359
|
+
// If we have nodes prop, use them directly
|
|
360
|
+
availableNodes = props.nodes;
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Load nodes from API if not provided
|
|
366
|
+
*/
|
|
367
|
+
async function loadNodesFromApi(): Promise<void> {
|
|
368
|
+
// If nodes are provided via props, use them
|
|
369
|
+
if (props.nodes && props.nodes.length > 0) {
|
|
370
|
+
availableNodes = props.nodes;
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Otherwise, load from API
|
|
375
|
+
try {
|
|
376
|
+
const fetchedNodes = await nodeApi.getNodes();
|
|
377
|
+
|
|
378
|
+
availableNodes = fetchedNodes;
|
|
379
|
+
} catch (error) {
|
|
380
|
+
console.error('❌ Failed to load nodes from API:', error);
|
|
381
|
+
|
|
382
|
+
// Use fallback sample nodes
|
|
383
|
+
availableNodes = [
|
|
384
|
+
{
|
|
385
|
+
id: 'text-input',
|
|
386
|
+
name: 'Text Input',
|
|
387
|
+
category: 'inputs',
|
|
388
|
+
description: 'Simple text input field',
|
|
389
|
+
version: '1.0.0',
|
|
390
|
+
icon: 'mdi:text-box',
|
|
391
|
+
inputs: [],
|
|
392
|
+
outputs: [{ id: 'text', name: 'text', type: 'output', dataType: 'string' }]
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
id: 'text-output',
|
|
396
|
+
name: 'Text Output',
|
|
397
|
+
category: 'outputs',
|
|
398
|
+
description: 'Display text output',
|
|
399
|
+
version: '1.0.0',
|
|
400
|
+
icon: 'mdi:text-box-outline',
|
|
401
|
+
inputs: [{ id: 'text', name: 'text', type: 'input', dataType: 'string' }],
|
|
402
|
+
outputs: []
|
|
403
|
+
}
|
|
404
|
+
];
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Clear workflow
|
|
410
|
+
*/
|
|
411
|
+
function clearWorkflow(): void {
|
|
412
|
+
if (currentWorkflow) {
|
|
413
|
+
currentWorkflow = {
|
|
414
|
+
...currentWorkflow,
|
|
415
|
+
nodes: [],
|
|
416
|
+
edges: [],
|
|
417
|
+
metadata: {
|
|
418
|
+
...currentWorkflow.metadata,
|
|
419
|
+
updatedAt: new Date().toISOString(),
|
|
420
|
+
versionId: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
421
|
+
updateNumber: (currentWorkflow.metadata?.updateNumber || 0) + 1
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// Update the global store
|
|
426
|
+
updateGlobalStore();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ConfigSidebar functions are now handled by the parent App component
|
|
431
|
+
|
|
432
|
+
async function handleConfigSave(newConfig: Record<string, unknown>): Promise<void> {
|
|
433
|
+
console.log('🔧 WorkflowEditor: handleConfigSave called with:', newConfig);
|
|
434
|
+
|
|
435
|
+
if (props.selectedNodeForConfig) {
|
|
436
|
+
console.log('🔧 WorkflowEditor: Updating config for node:', props.selectedNodeForConfig.id);
|
|
437
|
+
|
|
438
|
+
// Wait for any pending DOM updates
|
|
439
|
+
await tick();
|
|
440
|
+
|
|
441
|
+
// Update the node's config
|
|
442
|
+
props.selectedNodeForConfig.data.config = { ...newConfig };
|
|
443
|
+
|
|
444
|
+
// Update the node in currentWorkflow
|
|
445
|
+
// NOTE: We do NOT change the node's type field anymore
|
|
446
|
+
// All nodes use 'universalNode' and UniversalNode handles internal switching
|
|
447
|
+
if (currentWorkflow) {
|
|
448
|
+
currentWorkflow = {
|
|
449
|
+
...currentWorkflow,
|
|
450
|
+
nodes: currentWorkflow.nodes.map((node) =>
|
|
451
|
+
node.id === props.selectedNodeForConfig.id
|
|
452
|
+
? {
|
|
453
|
+
...node,
|
|
454
|
+
data: { ...node.data, config: { ...newConfig } }
|
|
455
|
+
}
|
|
456
|
+
: node
|
|
457
|
+
),
|
|
458
|
+
metadata: {
|
|
459
|
+
...currentWorkflow.metadata,
|
|
460
|
+
updatedAt: new Date().toISOString(),
|
|
461
|
+
versionId: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
462
|
+
updateNumber: (currentWorkflow.metadata?.updateNumber || 0) + 1
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
console.log('🔧 WorkflowEditor: Updated currentWorkflow, calling updateGlobalStore');
|
|
467
|
+
// Update the global store
|
|
468
|
+
updateGlobalStore();
|
|
469
|
+
} else {
|
|
470
|
+
console.warn('⚠️ WorkflowEditor: No currentWorkflow available for config update');
|
|
471
|
+
}
|
|
472
|
+
} else {
|
|
473
|
+
console.warn('⚠️ WorkflowEditor: No selectedNodeForConfig available for config update');
|
|
474
|
+
}
|
|
475
|
+
props.closeConfigSidebar?.();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Save workflow
|
|
480
|
+
*/
|
|
481
|
+
async function saveWorkflow(): Promise<void> {
|
|
482
|
+
try {
|
|
483
|
+
// Wait for any pending DOM updates before saving
|
|
484
|
+
await tick();
|
|
485
|
+
|
|
486
|
+
// Use current workflow from local variable
|
|
487
|
+
if (!currentWorkflow) {
|
|
488
|
+
console.warn('⚠️ No workflow data available to save');
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Determine the workflow ID based on whether we have an existing workflow
|
|
493
|
+
let workflowId: string;
|
|
494
|
+
if (currentWorkflow.id) {
|
|
495
|
+
// Use the existing workflow ID
|
|
496
|
+
workflowId = currentWorkflow.id;
|
|
497
|
+
} else {
|
|
498
|
+
// Generate a new UUID for a new workflow
|
|
499
|
+
workflowId = uuidv4();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const workflow: Workflow = {
|
|
503
|
+
id: workflowId,
|
|
504
|
+
name: currentWorkflow.name || 'Untitled Workflow',
|
|
505
|
+
nodes: currentWorkflow.nodes || [],
|
|
506
|
+
edges: currentWorkflow.edges || [],
|
|
507
|
+
metadata: {
|
|
508
|
+
version: '1.0.0',
|
|
509
|
+
createdAt: currentWorkflow.metadata?.createdAt || new Date().toISOString(),
|
|
510
|
+
updatedAt: new Date().toISOString()
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
console.log('💾 WorkflowEditor: Saving workflow to Drupal:');
|
|
515
|
+
console.log(' - ID:', workflow.id);
|
|
516
|
+
console.log(' - Name:', workflow.name);
|
|
517
|
+
console.log(' - Nodes count:', workflow.nodes.length);
|
|
518
|
+
console.log(' - Edges count:', workflow.edges.length);
|
|
519
|
+
console.log(' - Full workflow object:', JSON.stringify(workflow, null, 2));
|
|
520
|
+
|
|
521
|
+
const savedWorkflow = await workflowApi.saveWorkflow(workflow);
|
|
522
|
+
|
|
523
|
+
console.log('✅ WorkflowEditor: Received workflow from Drupal:');
|
|
524
|
+
console.log(' - ID:', savedWorkflow.id);
|
|
525
|
+
console.log(' - Name:', savedWorkflow.name);
|
|
526
|
+
console.log(' - Nodes count:', savedWorkflow.nodes?.length || 0);
|
|
527
|
+
console.log(' - Edges count:', savedWorkflow.edges?.length || 0);
|
|
528
|
+
|
|
529
|
+
// Update the workflow ID if it changed (new workflow)
|
|
530
|
+
// Keep our current workflow state, only update ID and metadata from Drupal
|
|
531
|
+
if (savedWorkflow.id && savedWorkflow.id !== workflow.id) {
|
|
532
|
+
console.log('🔄 Updating workflow ID from', workflow.id, 'to', savedWorkflow.id);
|
|
533
|
+
workflowActions.batchUpdate({
|
|
534
|
+
nodes: workflow.nodes,
|
|
535
|
+
edges: workflow.edges,
|
|
536
|
+
name: workflow.name,
|
|
537
|
+
metadata: {
|
|
538
|
+
...workflow.metadata,
|
|
539
|
+
...savedWorkflow.metadata
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
console.log('🔍 WorkflowEditor: Workflow store after save:', $workflowStore);
|
|
545
|
+
|
|
546
|
+
// Note: Notes node configurations (content, noteType) are automatically
|
|
547
|
+
// saved as part of the node.data.config object and will be restored
|
|
548
|
+
// when the workflow is loaded.
|
|
549
|
+
|
|
550
|
+
// Update the workflow ID if it was a new workflow
|
|
551
|
+
if (!currentWorkflow.id) {
|
|
552
|
+
console.log('🆕 New workflow created with ID:', savedWorkflow.id);
|
|
553
|
+
} else {
|
|
554
|
+
console.log('🔄 Existing workflow updated with ID:', savedWorkflow.id);
|
|
555
|
+
}
|
|
556
|
+
} catch (error) {
|
|
557
|
+
console.error('❌ Failed to save workflow:', error);
|
|
558
|
+
// Here you would typically show a user-friendly error message
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Export workflow
|
|
564
|
+
*/
|
|
565
|
+
async function exportWorkflow(): Promise<void> {
|
|
566
|
+
// Wait for any pending DOM updates before exporting
|
|
567
|
+
await tick();
|
|
568
|
+
|
|
569
|
+
// Use current workflow from local variable
|
|
570
|
+
if (!currentWorkflow) {
|
|
571
|
+
console.warn('⚠️ No workflow data available to export');
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Use the same ID logic as saveWorkflow
|
|
576
|
+
const workflowId = currentWorkflow.id || uuidv4();
|
|
577
|
+
|
|
578
|
+
const workflow: Workflow = {
|
|
579
|
+
id: workflowId,
|
|
580
|
+
name: currentWorkflow.name || 'Untitled Workflow',
|
|
581
|
+
nodes: currentWorkflow.nodes || [],
|
|
582
|
+
edges: currentWorkflow.edges || [],
|
|
583
|
+
metadata: {
|
|
584
|
+
version: '1.0.0',
|
|
585
|
+
createdAt: currentWorkflow.metadata?.createdAt || new Date().toISOString(),
|
|
586
|
+
updatedAt: new Date().toISOString()
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
const dataStr = JSON.stringify(workflow, null, 2);
|
|
591
|
+
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
|
592
|
+
const url = URL.createObjectURL(dataBlob);
|
|
593
|
+
const link = document.createElement('a');
|
|
594
|
+
link.href = url;
|
|
595
|
+
link.download = `${workflow.name}.json`;
|
|
596
|
+
link.click();
|
|
597
|
+
URL.revokeObjectURL(url);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Check if workflow has cycles
|
|
602
|
+
*/
|
|
603
|
+
function checkWorkflowCycles(): boolean {
|
|
604
|
+
return hasCycles(flowNodes, flowEdges);
|
|
605
|
+
}
|
|
188
606
|
</script>
|
|
189
607
|
|
|
190
608
|
<SvelteFlowProvider>
|
|
191
|
-
<div class="flowdrop-workflow-editor">
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
bind:nodes={flowNodes}
|
|
360
|
-
bind:edges={flowEdges}
|
|
361
|
-
{nodeTypes}
|
|
362
|
-
clickConnect={true}
|
|
363
|
-
elevateEdgesOnSelect={true}
|
|
364
|
-
connectionLineType={ConnectionLineType.Bezier}
|
|
365
|
-
fitView
|
|
366
|
-
/>
|
|
367
|
-
<Controls />
|
|
368
|
-
<Background />
|
|
369
|
-
<MiniMap />
|
|
370
|
-
|
|
371
|
-
<!-- Drop Zone Indicator -->
|
|
372
|
-
{#if flowNodes.length === 0}
|
|
373
|
-
<CanvasBanner title="Drag components here to start building" description="Use the sidebar to add components to your workflow" iconName="mdi:graph" />
|
|
374
|
-
{/if}
|
|
375
|
-
</div>
|
|
376
|
-
|
|
377
|
-
<!-- Status Bar -->
|
|
378
|
-
<div class="flowdrop-status-bar">
|
|
379
|
-
<div class="flowdrop-status-bar__content">
|
|
380
|
-
<div class="flowdrop-flex flowdrop-gap--4">
|
|
381
|
-
<span class="flowdrop-text--xs flowdrop-text--gray">All systems ready. You can start building your workflow.</span>
|
|
382
|
-
</div>
|
|
383
|
-
</div>
|
|
384
|
-
</div>
|
|
385
|
-
</div>
|
|
386
|
-
</div>
|
|
609
|
+
<div class="flowdrop-workflow-editor">
|
|
610
|
+
<!-- Main Editor Area -->
|
|
611
|
+
<div class="flowdrop-workflow-editor__main">
|
|
612
|
+
<!-- Flow Canvas -->
|
|
613
|
+
<div
|
|
614
|
+
class="flowdrop-canvas"
|
|
615
|
+
role="application"
|
|
616
|
+
aria-label="Workflow canvas"
|
|
617
|
+
ondragover={(e: DragEvent) => {
|
|
618
|
+
e.preventDefault();
|
|
619
|
+
e.dataTransfer!.dropEffect = 'copy';
|
|
620
|
+
}}
|
|
621
|
+
ondrop={async (e: DragEvent) => {
|
|
622
|
+
e.preventDefault();
|
|
623
|
+
|
|
624
|
+
// Get the data from the drag event
|
|
625
|
+
const nodeTypeData = e.dataTransfer?.getData('application/json');
|
|
626
|
+
if (nodeTypeData) {
|
|
627
|
+
// Get the position relative to the canvas
|
|
628
|
+
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
629
|
+
const position = {
|
|
630
|
+
x: e.clientX - rect.left,
|
|
631
|
+
y: e.clientY - rect.top
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
// Create the node manually since SvelteFlow isn't receiving the event
|
|
635
|
+
try {
|
|
636
|
+
const parsedData = JSON.parse(nodeTypeData);
|
|
637
|
+
|
|
638
|
+
// Handle both old format (with type: "node") and new format (direct NodeMetadata)
|
|
639
|
+
let nodeType: NodeMetadata;
|
|
640
|
+
let nodeData: {
|
|
641
|
+
label: string;
|
|
642
|
+
config: Record<string, unknown>;
|
|
643
|
+
metadata: NodeMetadata;
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
if (parsedData.type === 'node') {
|
|
647
|
+
// Old format from sidebar
|
|
648
|
+
nodeType = parsedData.nodeData.metadata;
|
|
649
|
+
nodeData = parsedData.nodeData;
|
|
650
|
+
} else {
|
|
651
|
+
// New format (direct NodeMetadata)
|
|
652
|
+
nodeType = parsedData;
|
|
653
|
+
|
|
654
|
+
// Extract initial config from configSchema
|
|
655
|
+
let initialConfig = {};
|
|
656
|
+
if (nodeType.configSchema && typeof nodeType.configSchema === 'object') {
|
|
657
|
+
// If configSchema is a JSON Schema, extract default values
|
|
658
|
+
if (nodeType.configSchema.properties) {
|
|
659
|
+
// JSON Schema format - extract defaults
|
|
660
|
+
Object.entries(nodeType.configSchema.properties).forEach(([key, prop]) => {
|
|
661
|
+
if (prop && typeof prop === 'object' && 'default' in prop) {
|
|
662
|
+
initialConfig[key] = prop.default;
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
} else {
|
|
666
|
+
// Simple object format - use as is
|
|
667
|
+
initialConfig = { ...nodeType.configSchema };
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
nodeData = {
|
|
672
|
+
label: nodeType.name,
|
|
673
|
+
config: initialConfig,
|
|
674
|
+
metadata: nodeType
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const newNodeId = uuidv4();
|
|
679
|
+
|
|
680
|
+
// All nodes use 'universalNode' type
|
|
681
|
+
// UniversalNode component handles internal switching based on metadata and config
|
|
682
|
+
const newNode: WorkflowNodeType = {
|
|
683
|
+
id: newNodeId,
|
|
684
|
+
type: 'universalNode',
|
|
685
|
+
position, // Use the position calculated from the drop event
|
|
686
|
+
deletable: true,
|
|
687
|
+
data: {
|
|
688
|
+
...nodeData,
|
|
689
|
+
nodeId: newNodeId // Use the same ID
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
// Add node to currentWorkflow
|
|
694
|
+
if (currentWorkflow) {
|
|
695
|
+
console.log('🔧 WorkflowEditor: Adding new node to currentWorkflow:', newNode.id);
|
|
696
|
+
currentWorkflow = {
|
|
697
|
+
...currentWorkflow,
|
|
698
|
+
nodes: [...currentWorkflow.nodes, newNode],
|
|
699
|
+
metadata: {
|
|
700
|
+
...currentWorkflow.metadata,
|
|
701
|
+
updatedAt: new Date().toISOString(),
|
|
702
|
+
versionId: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
703
|
+
updateNumber: (currentWorkflow.metadata?.updateNumber || 0) + 1
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
console.log(
|
|
708
|
+
'🔧 WorkflowEditor: Updated currentWorkflow with new node, calling updateGlobalStore'
|
|
709
|
+
);
|
|
710
|
+
// Update the global store
|
|
711
|
+
updateGlobalStore();
|
|
712
|
+
} else {
|
|
713
|
+
console.warn('⚠️ WorkflowEditor: No currentWorkflow available for new node');
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Wait for DOM update to ensure SvelteFlow updates
|
|
717
|
+
await tick();
|
|
718
|
+
} catch (error) {
|
|
719
|
+
console.error('Error parsing node data:', error);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}}
|
|
723
|
+
>
|
|
724
|
+
<SvelteFlow
|
|
725
|
+
bind:nodes={flowNodes}
|
|
726
|
+
bind:edges={flowEdges}
|
|
727
|
+
{nodeTypes}
|
|
728
|
+
{defaultEdgeOptions}
|
|
729
|
+
onconnect={handleConnect}
|
|
730
|
+
minZoom={0.2}
|
|
731
|
+
maxZoom={3}
|
|
732
|
+
clickConnect={true}
|
|
733
|
+
elevateEdgesOnSelect={true}
|
|
734
|
+
connectionLineType={ConnectionLineType.Bezier}
|
|
735
|
+
connectionLineComponent={ConnectionLine}
|
|
736
|
+
snapGrid={[10, 10]}
|
|
737
|
+
fitView
|
|
738
|
+
>
|
|
739
|
+
<Controls />
|
|
740
|
+
<Background
|
|
741
|
+
gap={10}
|
|
742
|
+
bgColor="var(--flowdrop-background-color)"
|
|
743
|
+
variant={BackgroundVariant.Dots}
|
|
744
|
+
/>
|
|
745
|
+
<MiniMap />
|
|
746
|
+
</SvelteFlow>
|
|
747
|
+
<!-- Drop Zone Indicator -->
|
|
748
|
+
{#if flowNodes.length === 0}
|
|
749
|
+
<CanvasBanner
|
|
750
|
+
title="Drag components here to start building"
|
|
751
|
+
description="Use the sidebar to add components to your workflow"
|
|
752
|
+
iconName="mdi:graph"
|
|
753
|
+
/>
|
|
754
|
+
{/if}
|
|
755
|
+
</div>
|
|
756
|
+
|
|
757
|
+
<!-- Status Bar -->
|
|
758
|
+
<div class="flowdrop-status-bar">
|
|
759
|
+
<div class="flowdrop-status-bar__content">
|
|
760
|
+
<div class="flowdrop-flex flowdrop-gap--4">
|
|
761
|
+
<span class="flowdrop-text--xs flowdrop-text--gray">{flowNodes.length} nodes</span>
|
|
762
|
+
<span class="flowdrop-text--xs flowdrop-text--gray">•</span>
|
|
763
|
+
<span class="flowdrop-text--xs flowdrop-text--gray">{flowEdges.length} connections</span
|
|
764
|
+
>
|
|
765
|
+
|
|
766
|
+
{#if checkWorkflowCycles()}
|
|
767
|
+
<span class="flowdrop-text--xs flowdrop-text--gray">•</span>
|
|
768
|
+
<span class="flowdrop-text--xs flowdrop-font--medium flowdrop-text--error"
|
|
769
|
+
>⚠️ Cycles detected</span
|
|
770
|
+
>
|
|
771
|
+
{/if}
|
|
772
|
+
</div>
|
|
773
|
+
</div>
|
|
774
|
+
</div>
|
|
775
|
+
</div>
|
|
776
|
+
</div>
|
|
387
777
|
</SvelteFlowProvider>
|
|
388
778
|
|
|
389
779
|
<style>
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
:global(.flowdrop-workflow-editor .svelte-flow__edge path) {
|
|
497
|
-
stroke-width: 2 !important;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
:global(.flowdrop-workflow-editor .svelte-flow__edge:hover) {
|
|
501
|
-
stroke: #3b82f6 !important;
|
|
502
|
-
stroke-width: 3 !important;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
:global(.flowdrop-workflow-editor .svelte-flow__edge:hover path) {
|
|
506
|
-
stroke-width: 3 !important;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
:global(.flowdrop-workflow-editor .svelte-flow__edge.selected) {
|
|
510
|
-
stroke: #3b82f6 !important;
|
|
511
|
-
stroke-width: 3 !important;
|
|
512
|
-
filter: drop-shadow(0 0 4px rgba(59, 130, 246, 0.5));
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
:global(.flowdrop-workflow-editor .svelte-flow__edge.selected path) {
|
|
516
|
-
stroke-width: 3 !important;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
/* Ensure edge paths are clickable */
|
|
520
|
-
:global(.flowdrop-workflow-editor .svelte-flow__edge path) {
|
|
521
|
-
pointer-events: all;
|
|
522
|
-
cursor: pointer;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
:global(.flowdrop-workflow-editor .svelte-flow__handle) {
|
|
526
|
-
width: 18px;
|
|
527
|
-
height: 18px;
|
|
528
|
-
border: 2px solid white;
|
|
529
|
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
530
|
-
z-index: 10;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
:global(.flowdrop-workflow-editor .svelte-flow__handle:hover) {
|
|
534
|
-
transform: scale(1.2);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
/* Ensure our custom handles are clickable */
|
|
538
|
-
:global(.flowdrop-workflow-editor .svelte-flow__handle) {
|
|
539
|
-
pointer-events: all;
|
|
540
|
-
cursor: crosshair;
|
|
541
|
-
}
|
|
542
|
-
</style>
|
|
780
|
+
.flowdrop-workflow-editor {
|
|
781
|
+
display: flex;
|
|
782
|
+
flex-direction: row; /* Side by side layout */
|
|
783
|
+
height: 100%;
|
|
784
|
+
position: relative;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
.flowdrop-workflow-editor__main {
|
|
788
|
+
flex: 1;
|
|
789
|
+
display: flex;
|
|
790
|
+
flex-direction: column;
|
|
791
|
+
min-height: 0;
|
|
792
|
+
transition: margin-left 0.3s ease-in-out;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
.flowdrop-text--error {
|
|
796
|
+
color: #dc2626;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
.flowdrop-canvas {
|
|
800
|
+
flex: 1;
|
|
801
|
+
min-height: 0;
|
|
802
|
+
position: relative;
|
|
803
|
+
background: transparent;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
.flowdrop-status-bar {
|
|
807
|
+
background-color: rgba(255, 255, 255, 0.8);
|
|
808
|
+
backdrop-filter: blur(8px);
|
|
809
|
+
border-top: 1px solid #e5e7eb;
|
|
810
|
+
padding: 0.75rem;
|
|
811
|
+
height: 40px;
|
|
812
|
+
min-height: 40px;
|
|
813
|
+
max-height: 40px;
|
|
814
|
+
display: flex;
|
|
815
|
+
align-items: center;
|
|
816
|
+
flex-shrink: 0;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
.flowdrop-status-bar__content {
|
|
820
|
+
display: flex;
|
|
821
|
+
align-items: center;
|
|
822
|
+
justify-content: space-between;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
:global(.flowdrop-workflow-editor .svelte-flow__node:hover) {
|
|
826
|
+
transform: translateY(-2px);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
:global(.flowdrop-workflow-editor .svelte-flow__edge) {
|
|
830
|
+
stroke-width: 2 !important;
|
|
831
|
+
cursor: pointer;
|
|
832
|
+
pointer-events: all;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
:global(.flowdrop-workflow-editor .svelte-flow__edge path) {
|
|
836
|
+
stroke-width: 2 !important;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
:global(.flowdrop-workflow-editor .svelte-flow__edge:hover) {
|
|
840
|
+
stroke: #3b82f6 !important;
|
|
841
|
+
stroke-width: 3 !important;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
:global(.flowdrop-workflow-editor .svelte-flow__edge:hover path) {
|
|
845
|
+
stroke-width: 3 !important;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
:global(.flowdrop-workflow-editor .svelte-flow__edge.selected) {
|
|
849
|
+
stroke: #3b82f6 !important;
|
|
850
|
+
stroke-width: 3 !important;
|
|
851
|
+
filter: drop-shadow(0 0 4px rgba(59, 130, 246, 0.5));
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
:global(.flowdrop-workflow-editor .svelte-flow__edge.selected path) {
|
|
855
|
+
stroke-width: 3 !important;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/* Ensure edge paths are clickable */
|
|
859
|
+
:global(.flowdrop-workflow-editor .svelte-flow__edge path) {
|
|
860
|
+
pointer-events: all;
|
|
861
|
+
cursor: pointer;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/* Enhanced arrow markers for input ports */
|
|
865
|
+
:global(.flowdrop-workflow-editor .svelte-flow__edge-marker) {
|
|
866
|
+
fill: currentColor;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
:global(.flowdrop-workflow-editor .svelte-flow__handle) {
|
|
870
|
+
width: 18px;
|
|
871
|
+
height: 18px;
|
|
872
|
+
border: 2px solid white;
|
|
873
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
874
|
+
z-index: 10;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/* Ensure our custom handles are clickable */
|
|
878
|
+
:global(.flowdrop-workflow-editor .svelte-flow__handle) {
|
|
879
|
+
pointer-events: all;
|
|
880
|
+
cursor: crosshair;
|
|
881
|
+
}
|
|
882
|
+
:global(.flowdrop--edge--tool path.svelte-flow__edge-path) {
|
|
883
|
+
stroke-dasharray: 5 5;
|
|
884
|
+
}
|
|
885
|
+
</style>
|