@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.
Files changed (119) hide show
  1. package/README.md +307 -215
  2. package/dist/adapters/WorkflowAdapter.d.ts +1 -1
  3. package/dist/adapters/WorkflowAdapter.js +30 -30
  4. package/dist/api/client.d.ts +24 -1
  5. package/dist/api/client.js +55 -38
  6. package/dist/api/enhanced-client.d.ts +46 -0
  7. package/dist/api/enhanced-client.js +211 -0
  8. package/dist/clients/ApiClient.d.ts +19 -23
  9. package/dist/clients/ApiClient.js +36 -34
  10. package/dist/components/App.svelte +1299 -230
  11. package/dist/components/App.svelte.d.ts +21 -1
  12. package/dist/components/CanvasBanner.svelte +50 -44
  13. package/dist/components/CanvasBanner.svelte.d.ts +5 -19
  14. package/dist/components/ConfigForm.svelte +555 -0
  15. package/dist/components/ConfigForm.svelte.d.ts +32 -0
  16. package/dist/components/ConfigModal.svelte +261 -0
  17. package/dist/components/ConfigModal.svelte.d.ts +31 -0
  18. package/dist/components/ConfigSidebar.svelte +934 -0
  19. package/dist/components/ConfigSidebar.svelte.d.ts +51 -0
  20. package/dist/components/ConnectionLine.svelte +32 -0
  21. package/dist/components/ConnectionLine.svelte.d.ts +3 -0
  22. package/dist/components/GatewayNode.svelte +471 -0
  23. package/dist/components/GatewayNode.svelte.d.ts +15 -0
  24. package/dist/components/LoadingSpinner.svelte +23 -23
  25. package/dist/components/LoadingSpinner.svelte.d.ts +1 -1
  26. package/dist/components/Logo.svelte +82 -0
  27. package/dist/components/Logo.svelte.d.ts +26 -0
  28. package/dist/components/LogsSidebar.svelte +565 -0
  29. package/dist/components/LogsSidebar.svelte.d.ts +34 -0
  30. package/dist/components/MarkdownDisplay.svelte +28 -0
  31. package/dist/components/MarkdownDisplay.svelte.d.ts +7 -0
  32. package/dist/components/Navbar.svelte +663 -0
  33. package/dist/components/Navbar.svelte.d.ts +21 -0
  34. package/dist/components/NodeSidebar.svelte +629 -488
  35. package/dist/components/NodeSidebar.svelte.d.ts +1 -2
  36. package/dist/components/NodeStatusOverlay.svelte +327 -0
  37. package/dist/components/NodeStatusOverlay.svelte.d.ts +11 -0
  38. package/dist/components/NotesNode.svelte +566 -0
  39. package/dist/components/NotesNode.svelte.d.ts +43 -0
  40. package/dist/components/PipelineStatus.svelte +331 -0
  41. package/dist/components/PipelineStatus.svelte.d.ts +18 -0
  42. package/dist/components/SimpleNode.svelte +447 -0
  43. package/dist/components/SimpleNode.svelte.d.ts +24 -0
  44. package/dist/components/SquareNode.svelte +346 -0
  45. package/dist/components/SquareNode.svelte.d.ts +24 -0
  46. package/dist/components/StatusIcon.svelte +112 -0
  47. package/dist/components/StatusIcon.svelte.d.ts +10 -0
  48. package/dist/components/StatusLabel.svelte +33 -0
  49. package/dist/components/StatusLabel.svelte.d.ts +7 -0
  50. package/dist/components/ToolNode.svelte +385 -0
  51. package/dist/components/ToolNode.svelte.d.ts +36 -0
  52. package/dist/components/UniversalNode.svelte +126 -0
  53. package/dist/components/UniversalNode.svelte.d.ts +15 -0
  54. package/dist/components/WorkflowEditor.svelte +871 -528
  55. package/dist/components/WorkflowEditor.svelte.d.ts +15 -5
  56. package/dist/components/WorkflowNode.svelte +428 -542
  57. package/dist/components/WorkflowNode.svelte.d.ts +7 -3
  58. package/dist/config/apiConfig.d.ts +33 -0
  59. package/dist/config/apiConfig.js +39 -0
  60. package/dist/config/defaultPortConfig.d.ts +6 -0
  61. package/dist/config/defaultPortConfig.js +192 -0
  62. package/dist/config/demo.d.ts +58 -0
  63. package/dist/config/demo.js +142 -0
  64. package/dist/config/endpoints.d.ts +106 -0
  65. package/dist/config/endpoints.js +128 -0
  66. package/dist/data/samples.d.ts +38 -4
  67. package/dist/data/samples.js +2789 -737
  68. package/dist/examples/adapter-usage.d.ts +4 -4
  69. package/dist/examples/adapter-usage.js +21 -26
  70. package/dist/examples/api-client-usage.d.ts +6 -6
  71. package/dist/examples/api-client-usage.js +55 -54
  72. package/dist/index.d.ts +23 -15
  73. package/dist/index.js +23 -15
  74. package/dist/mocks/app-environment.d.ts +8 -0
  75. package/dist/mocks/app-environment.js +16 -0
  76. package/dist/mocks/app-forms.d.ts +2 -0
  77. package/dist/mocks/app-forms.js +21 -0
  78. package/dist/mocks/app-navigation.d.ts +5 -0
  79. package/dist/mocks/app-navigation.js +34 -0
  80. package/dist/mocks/app-stores.d.ts +14 -0
  81. package/dist/mocks/app-stores.js +26 -0
  82. package/dist/services/api.d.ts +13 -3
  83. package/dist/services/api.js +91 -36
  84. package/dist/services/globalSave.d.ts +20 -0
  85. package/dist/services/globalSave.js +165 -0
  86. package/dist/services/nodeExecutionService.d.ts +63 -0
  87. package/dist/services/nodeExecutionService.js +261 -0
  88. package/dist/services/portConfigApi.d.ts +14 -0
  89. package/dist/services/portConfigApi.js +69 -0
  90. package/dist/services/toastService.d.ts +147 -0
  91. package/dist/services/toastService.js +235 -0
  92. package/dist/services/workflowStorage.d.ts +2 -2
  93. package/dist/services/workflowStorage.js +10 -10
  94. package/dist/stores/workflowStore.d.ts +53 -0
  95. package/dist/stores/workflowStore.js +264 -0
  96. package/dist/styles/base.css +896 -363
  97. package/dist/svelte-app.d.ts +52 -5
  98. package/dist/svelte-app.js +128 -6
  99. package/dist/types/config.d.ts +291 -0
  100. package/dist/types/config.js +4 -0
  101. package/dist/types/index.d.ts +231 -19
  102. package/dist/types/index.js +1 -1
  103. package/dist/utils/colors.d.ts +67 -33
  104. package/dist/utils/colors.js +183 -118
  105. package/dist/utils/config.d.ts +41 -0
  106. package/dist/utils/config.js +248 -0
  107. package/dist/utils/connections.d.ts +40 -3
  108. package/dist/utils/connections.js +115 -44
  109. package/dist/utils/icons.d.ts +1 -1
  110. package/dist/utils/icons.js +71 -70
  111. package/dist/utils/nodeStatus.d.ts +53 -0
  112. package/dist/utils/nodeStatus.js +183 -0
  113. package/dist/utils/nodeTypes.d.ts +57 -0
  114. package/dist/utils/nodeTypes.js +109 -0
  115. package/dist/utils/nodeWrapper.d.ts +39 -0
  116. package/dist/utils/nodeWrapper.js +62 -0
  117. package/package.json +129 -97
  118. package/dist/components/Node.svelte +0 -38
  119. package/dist/components/Node.svelte.d.ts +0 -4
@@ -5,538 +5,881 @@
5
5
  -->
6
6
 
7
7
  <script lang="ts">
8
- import {
9
- SvelteFlow,
10
- ConnectionLineType,
11
- // @ts-ignore
12
- Controls,
13
- // @ts-ignore
14
- Background,
15
- // @ts-ignore
16
- MiniMap,
17
- // @ts-ignore
18
- SvelteFlowProvider,
19
- } from '@xyflow/svelte';
20
- import "@xyflow/svelte/dist/style.css";
21
- import NodeSidebar from "./NodeSidebar.svelte";
22
- import WorkflowNode from "./WorkflowNode.svelte";
23
- import type { WorkflowNode as WorkflowNodeType, NodeMetadata, Workflow, WorkflowEdge } from "../types/index.js";
24
- import { validateConnection, hasCycles } from "../utils/connections.js";
25
- import CanvasBanner from "./CanvasBanner.svelte";
26
- import { workflowApi, setApiBaseUrl } from "../services/api.js";
27
- import { v4 as uuidv4 } from "uuid";
28
-
29
- interface Props {
30
- nodes: NodeMetadata[];
31
- workflow?: Workflow;
32
- apiBaseUrl?: string;
33
- }
34
-
35
- let props: Props = $props();
36
-
37
- // Initialize from props only once, not on every re-render
38
- let isInitialized = $state(false);
39
- let flowNodes = $state<WorkflowNodeType[]>([]);
40
- let flowEdges = $state<WorkflowEdge[]>([]);
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
- <!-- Node Sidebar -->
193
- <NodeSidebar
194
- nodes={props.nodes}
195
- />
196
-
197
- <!-- Main Editor Area -->
198
- <div class="flowdrop-workflow-editor__main">
199
- <!-- Toolbar -->
200
- <div class="flowdrop-toolbar">
201
- <div class="flowdrop-toolbar__content">
202
- <!-- Left side - Workflow info -->
203
- <div class="flowdrop-toolbar__info">
204
- {#if isEditingTitle}
205
- <div class="flowdrop-flex flowdrop-gap--2">
206
- <input
207
- id="workflow-title"
208
- type="text"
209
- class="flowdrop-input flowdrop-input--lg"
210
- bind:value={workflowName}
211
- onkeydown={handleTitleKeydown}
212
- onblur={saveTitle}
213
- />
214
- <button
215
- class="flowdrop-btn flowdrop-btn--ghost flowdrop-btn--sm"
216
- onclick={saveTitle}
217
- type="button"
218
- >
219
-
220
- </button>
221
- <button
222
- class="flowdrop-btn flowdrop-btn--ghost flowdrop-btn--sm"
223
- onclick={cancelTitleEdit}
224
- type="button"
225
- >
226
-
227
- </button>
228
- </div>
229
- {:else}
230
- <button
231
- class="flowdrop-workflow-title"
232
- onclick={startTitleEdit}
233
- type="button"
234
- >
235
- {workflowName}
236
- </button>
237
- {/if}
238
- <div class="flowdrop-workflow-stats">
239
- <span class="flowdrop-text--sm flowdrop-text--gray">{flowNodes.length} nodes</span>
240
- <span class="flowdrop-text--sm flowdrop-text--gray">•</span>
241
- <span class="flowdrop-text--sm flowdrop-text--gray">{flowEdges.length} connections</span>
242
-
243
- {#if checkWorkflowCycles()}
244
- <span class="flowdrop-text--sm flowdrop-text--gray">•</span>
245
- <span class="flowdrop-text--sm flowdrop-font--medium flowdrop-text--error">⚠️ Cycles detected</span>
246
- {/if}
247
- </div>
248
- </div>
249
-
250
- <!-- Right side - Actions -->
251
- <div class="flowdrop-toolbar__actions">
252
- <!-- Workflow Actions -->
253
- <div class="flowdrop-join">
254
- <button
255
- class="flowdrop-btn flowdrop-btn--sm flowdrop-btn--outline flowdrop-join__item"
256
- onclick={clearWorkflow}
257
- type="button"
258
- >
259
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="flowdrop-icon">
260
- <path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
261
- </svg>
262
-
263
- Clear
264
- </button>
265
- <button
266
- class="flowdrop-btn flowdrop-btn--sm flowdrop-btn--outline flowdrop-join__item"
267
- onclick={exportWorkflow}
268
- type="button"
269
- >
270
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="flowdrop-icon">
271
- <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
272
- </svg>
273
-
274
- Export
275
- </button>
276
- <button
277
- class="flowdrop-btn flowdrop-btn--sm flowdrop-btn--outline flowdrop-join__item"
278
- onclick={saveWorkflow}
279
- type="button"
280
- >
281
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="flowdrop-icon">
282
- <path stroke-linecap="round" stroke-linejoin="round" d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0 1 11.186 0Z" />
283
- </svg>
284
-
285
- Save
286
- </button>
287
- </div>
288
- </div>
289
- </div>
290
- </div>
291
-
292
- <!-- Flow Canvas -->
293
- <div
294
- class="flowdrop-canvas"
295
- role="application"
296
- aria-label="Workflow canvas"
297
- ondragover={(e: DragEvent) => {
298
- e.preventDefault();
299
- e.dataTransfer!.dropEffect = "copy";
300
- }}
301
- ondrop={(e: DragEvent) => {
302
- e.preventDefault();
303
-
304
- // Get the data from the drag event
305
- const nodeTypeData = e.dataTransfer?.getData("application/json");
306
- if (nodeTypeData) {
307
- // Get the position relative to the canvas
308
- const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
309
- const position = {
310
- x: e.clientX - rect.left,
311
- y: e.clientY - rect.top
312
- };
313
-
314
- // Create the node manually since SvelteFlow isn't receiving the event
315
- try {
316
- const parsedData = JSON.parse(nodeTypeData);
317
-
318
- // Handle both old format (with type: "node") and new format (direct NodeMetadata)
319
- let nodeType: NodeMetadata;
320
- let nodeData: any;
321
-
322
- if (parsedData.type === "node") {
323
- // Old format from sidebar
324
- nodeType = parsedData.nodeData.metadata;
325
- nodeData = parsedData.nodeData;
326
- } else {
327
- // New format (direct NodeMetadata)
328
- nodeType = parsedData;
329
- nodeData = {
330
- label: nodeType.name,
331
- config: {},
332
- metadata: nodeType
333
- };
334
- }
335
-
336
- const newNodeId = uuidv4();
337
-
338
- const newNode: WorkflowNodeType = {
339
- id: newNodeId,
340
- type: "workflowNode",
341
- position, // Use the position calculated from the drop event
342
- deletable: true,
343
- data: {
344
- ...nodeData,
345
- nodeId: newNodeId // Use the same ID
346
- }
347
- };
348
-
349
- // Add node
350
- const updatedNodes = [...flowNodes, newNode];
351
- flowNodes = updatedNodes;
352
- } catch (error) {
353
- console.error("Error parsing node data:", error);
354
- }
355
- }
356
- }}
357
- >
358
- <SvelteFlow
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
- .flowdrop-workflow-editor {
391
- display: flex;
392
- height: 100%;
393
- background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
394
- }
395
-
396
- .flowdrop-workflow-editor__main {
397
- flex: 1;
398
- display: flex;
399
- flex-direction: column;
400
- min-height: 0;
401
- }
402
-
403
- .flowdrop-toolbar {
404
- background-color: rgba(255, 255, 255, 0.8);
405
- backdrop-filter: blur(8px);
406
- border-bottom: 1px solid #e5e7eb;
407
- padding: 1rem;
408
- box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
409
- }
410
-
411
- .flowdrop-toolbar__content {
412
- display: flex;
413
- align-items: center;
414
- justify-content: space-between;
415
- }
416
-
417
- .flowdrop-toolbar__info {
418
- display: flex;
419
- align-items: center;
420
- gap: 1rem;
421
- }
422
-
423
- .flowdrop-workflow-title {
424
- font-size: 1.25rem;
425
- font-weight: 700;
426
- color: #111827;
427
- cursor: pointer;
428
- transition: color 0.2s ease-in-out;
429
- background: transparent;
430
- border: none;
431
- padding: 0;
432
- }
433
-
434
- .flowdrop-workflow-title:hover {
435
- color: #3b82f6;
436
- }
437
-
438
- .flowdrop-workflow-stats {
439
- display: flex;
440
- align-items: center;
441
- gap: 0.5rem;
442
- }
443
-
444
- .flowdrop-text--error {
445
- color: #dc2626;
446
- }
447
-
448
- .flowdrop-toolbar__actions {
449
- display: flex;
450
- align-items: center;
451
- gap: 0.75rem;
452
- }
453
-
454
- .flowdrop-icon {
455
- width: 1.5rem;
456
- height: 1.5rem;
457
- }
458
-
459
- .flowdrop-canvas {
460
- flex: 1;
461
- min-height: 0;
462
- position: relative;
463
- }
464
-
465
- .flowdrop-status-bar {
466
- background-color: rgba(255, 255, 255, 0.8);
467
- backdrop-filter: blur(8px);
468
- border-top: 1px solid #e5e7eb;
469
- padding: 0.75rem;
470
- }
471
-
472
- .flowdrop-status-bar__content {
473
- display: flex;
474
- align-items: center;
475
- justify-content: space-between;
476
- }
477
-
478
- :global(.flowdrop-workflow-editor .svelte-flow) {
479
- background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
480
- background-image:
481
- radial-gradient(circle, #d1d5db 1px, transparent 1px);
482
- background-size: 20px 20px;
483
- background-position: 0 0, 10px 10px;
484
- }
485
-
486
- :global(.flowdrop-workflow-editor .svelte-flow__node:hover) {
487
- transform: translateY(-2px);
488
- }
489
-
490
- :global(.flowdrop-workflow-editor .svelte-flow__edge) {
491
- stroke-width: 2 !important;
492
- cursor: pointer;
493
- pointer-events: all;
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>