@d34dman/flowdrop 0.0.12 → 0.0.14
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/dist/adapters/WorkflowAdapter.js +33 -2
- package/dist/components/App.svelte +114 -43
- package/dist/components/ConfigForm.svelte +47 -58
- package/dist/components/ConfigForm.svelte.d.ts +1 -1
- package/dist/components/FlowDropZone.svelte +68 -0
- package/dist/components/FlowDropZone.svelte.d.ts +11 -0
- package/dist/components/PipelineStatus.svelte +4 -2
- package/dist/components/WorkflowEditor.svelte +82 -78
- package/dist/helpers/workflowEditorHelper.d.ts +7 -1
- package/dist/helpers/workflowEditorHelper.js +24 -2
- package/dist/services/api.js +47 -4
- package/dist/utils/performanceUtils.d.ts +1 -1
- package/dist/utils/performanceUtils.js +1 -2
- package/package.json +1 -1
|
@@ -14,6 +14,27 @@
|
|
|
14
14
|
* - Systems that need to generate or modify workflows programmatically
|
|
15
15
|
*/
|
|
16
16
|
import { v4 as uuidv4 } from 'uuid';
|
|
17
|
+
/**
|
|
18
|
+
* Generate a unique node ID based on node type and existing nodes
|
|
19
|
+
* Format: <node_type>.<number>
|
|
20
|
+
* Example: boolean_gateway.1, calculator.2
|
|
21
|
+
*/
|
|
22
|
+
function generateStandardNodeId(nodeTypeId, existingNodes) {
|
|
23
|
+
// Count how many nodes of this type already exist
|
|
24
|
+
const existingNodeIds = existingNodes
|
|
25
|
+
.filter((node) => node.data?.metadata?.id === nodeTypeId)
|
|
26
|
+
.map((node) => node.id);
|
|
27
|
+
// Extract the numbers from existing IDs with the same prefix
|
|
28
|
+
const existingNumbers = existingNodeIds
|
|
29
|
+
.map((id) => {
|
|
30
|
+
const match = id.match(new RegExp(`^${nodeTypeId}\\.(\\d+)$`));
|
|
31
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
32
|
+
})
|
|
33
|
+
.filter((num) => num > 0);
|
|
34
|
+
// Find the next available number (highest + 1)
|
|
35
|
+
const nextNumber = existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : 1;
|
|
36
|
+
return `${nodeTypeId}.${nextNumber}`;
|
|
37
|
+
}
|
|
17
38
|
/**
|
|
18
39
|
* Workflow Adapter Class
|
|
19
40
|
* Provides a clean API for workflow operations without exposing SvelteFlow internals
|
|
@@ -48,8 +69,10 @@ export class WorkflowAdapter {
|
|
|
48
69
|
if (!metadata) {
|
|
49
70
|
throw new Error(`Node type '${nodeType}' not found`);
|
|
50
71
|
}
|
|
72
|
+
// Generate node ID based on node type and existing nodes
|
|
73
|
+
const nodeId = generateStandardNodeId(nodeType, workflow.nodes);
|
|
51
74
|
const node = {
|
|
52
|
-
id:
|
|
75
|
+
id: nodeId,
|
|
53
76
|
type: nodeType,
|
|
54
77
|
position,
|
|
55
78
|
data: {
|
|
@@ -313,9 +336,17 @@ export class WorkflowAdapter {
|
|
|
313
336
|
const cloned = JSON.parse(JSON.stringify(workflow));
|
|
314
337
|
// Generate new IDs for all nodes and edges
|
|
315
338
|
const idMapping = new Map();
|
|
339
|
+
// Count nodes by type to generate proper sequential IDs
|
|
340
|
+
const nodeTypeCounts = new Map();
|
|
316
341
|
cloned.nodes.forEach((node) => {
|
|
317
342
|
const oldId = node.id;
|
|
318
|
-
node.id
|
|
343
|
+
const nodeTypeId = node.data.metadata.id;
|
|
344
|
+
// Get the current count for this node type
|
|
345
|
+
const currentCount = nodeTypeCounts.get(nodeTypeId) || 0;
|
|
346
|
+
const newCount = currentCount + 1;
|
|
347
|
+
nodeTypeCounts.set(nodeTypeId, newCount);
|
|
348
|
+
// Generate new ID with the sequential number
|
|
349
|
+
node.id = `${nodeTypeId}.${newCount}`;
|
|
319
350
|
idMapping.set(oldId, node.id);
|
|
320
351
|
});
|
|
321
352
|
cloned.edges.forEach((edge) => {
|
|
@@ -167,19 +167,19 @@
|
|
|
167
167
|
*/
|
|
168
168
|
async function testApiConnection(): Promise<void> {
|
|
169
169
|
try {
|
|
170
|
-
const baseUrl = endpointConfig?.baseUrl || apiBaseUrl ||
|
|
170
|
+
const baseUrl = endpointConfig?.baseUrl || apiBaseUrl || '/api/flowdrop';
|
|
171
171
|
const testUrl = `${baseUrl}/nodes`;
|
|
172
172
|
|
|
173
173
|
const response = await fetch(testUrl);
|
|
174
174
|
const data = await response.json();
|
|
175
175
|
|
|
176
176
|
if (response.ok && data.success) {
|
|
177
|
-
apiToasts.success(
|
|
177
|
+
apiToasts.success('API connection test', 'Connection successful');
|
|
178
178
|
} else {
|
|
179
|
-
apiToasts.error(
|
|
179
|
+
apiToasts.error('API connection test', 'Connection failed');
|
|
180
180
|
}
|
|
181
181
|
} catch (err) {
|
|
182
|
-
apiToasts.error(
|
|
182
|
+
apiToasts.error('API connection test', err instanceof Error ? err.message : 'Unknown error');
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
|
|
@@ -288,53 +288,69 @@
|
|
|
288
288
|
// Wait for any pending DOM updates before saving
|
|
289
289
|
await tick();
|
|
290
290
|
|
|
291
|
-
//
|
|
292
|
-
const
|
|
293
|
-
const { v4: uuidv4 } = await import('uuid');
|
|
294
|
-
|
|
295
|
-
// Use current workflow from global store
|
|
296
|
-
const workflowToSave = $workflowStore;
|
|
291
|
+
// Show loading toast
|
|
292
|
+
const loadingToast = apiToasts.loading('Saving workflow');
|
|
297
293
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
294
|
+
try {
|
|
295
|
+
// Import necessary modules
|
|
296
|
+
const { workflowApi } = await import('../services/api.js');
|
|
297
|
+
const { v4: uuidv4 } = await import('uuid');
|
|
301
298
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
if (workflowToSave.id) {
|
|
305
|
-
workflowId = workflowToSave.id;
|
|
306
|
-
} else {
|
|
307
|
-
workflowId = uuidv4();
|
|
308
|
-
}
|
|
299
|
+
// Use current workflow from global store
|
|
300
|
+
const workflowToSave = $workflowStore;
|
|
309
301
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
name: workflowToSave.name || 'Untitled Workflow',
|
|
314
|
-
description: workflowToSave.description || '',
|
|
315
|
-
nodes: workflowToSave.nodes || [],
|
|
316
|
-
edges: workflowToSave.edges || [],
|
|
317
|
-
metadata: {
|
|
318
|
-
version: '1.0.0',
|
|
319
|
-
createdAt: workflowToSave.metadata?.createdAt || new Date().toISOString(),
|
|
320
|
-
updatedAt: new Date().toISOString()
|
|
302
|
+
if (!workflowToSave) {
|
|
303
|
+
dismissToast(loadingToast);
|
|
304
|
+
return;
|
|
321
305
|
}
|
|
322
|
-
};
|
|
323
306
|
|
|
324
|
-
|
|
307
|
+
// Determine the workflow ID
|
|
308
|
+
let workflowId: string;
|
|
309
|
+
if (workflowToSave.id) {
|
|
310
|
+
workflowId = workflowToSave.id;
|
|
311
|
+
} else {
|
|
312
|
+
workflowId = uuidv4();
|
|
313
|
+
}
|
|
325
314
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
315
|
+
// Create workflow object for saving
|
|
316
|
+
const finalWorkflow = {
|
|
317
|
+
id: workflowId,
|
|
318
|
+
name: workflowToSave.name || 'Untitled Workflow',
|
|
319
|
+
description: workflowToSave.description || '',
|
|
320
|
+
nodes: workflowToSave.nodes || [],
|
|
321
|
+
edges: workflowToSave.edges || [],
|
|
333
322
|
metadata: {
|
|
334
|
-
|
|
335
|
-
|
|
323
|
+
version: '1.0.0',
|
|
324
|
+
createdAt: workflowToSave.metadata?.createdAt || new Date().toISOString(),
|
|
325
|
+
updatedAt: new Date().toISOString()
|
|
336
326
|
}
|
|
337
|
-
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const savedWorkflow = await workflowApi.saveWorkflow(finalWorkflow);
|
|
330
|
+
|
|
331
|
+
// Update the workflow ID if it changed (new workflow)
|
|
332
|
+
// Keep our current workflow state, only update ID and metadata from backend
|
|
333
|
+
if (savedWorkflow.id && savedWorkflow.id !== finalWorkflow.id) {
|
|
334
|
+
workflowActions.batchUpdate({
|
|
335
|
+
nodes: finalWorkflow.nodes,
|
|
336
|
+
edges: finalWorkflow.edges,
|
|
337
|
+
name: finalWorkflow.name,
|
|
338
|
+
metadata: {
|
|
339
|
+
...finalWorkflow.metadata,
|
|
340
|
+
...savedWorkflow.metadata
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Dismiss loading toast and show success
|
|
346
|
+
dismissToast(loadingToast);
|
|
347
|
+
apiToasts.success('Save workflow', 'Workflow saved successfully');
|
|
348
|
+
} catch (error) {
|
|
349
|
+
// Dismiss loading toast and show error
|
|
350
|
+
dismissToast(loadingToast);
|
|
351
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
352
|
+
apiToasts.error('Save workflow', errorMessage);
|
|
353
|
+
throw error; // Re-throw to allow calling code to handle if needed
|
|
338
354
|
}
|
|
339
355
|
}
|
|
340
356
|
|
|
@@ -616,6 +632,24 @@
|
|
|
616
632
|
<div class="flowdrop-config-sidebar__section">
|
|
617
633
|
<h3 class="flowdrop-config-sidebar__section-title">Node Details</h3>
|
|
618
634
|
<div class="flowdrop-config-sidebar__details">
|
|
635
|
+
<div class="flowdrop-config-sidebar__detail">
|
|
636
|
+
<span class="flowdrop-config-sidebar__detail-label">Node ID:</span>
|
|
637
|
+
<div class="flowdrop-config-sidebar__detail-value-with-copy">
|
|
638
|
+
<span class="flowdrop-config-sidebar__detail-value" style="font-family: monospace;">
|
|
639
|
+
{selectedNodeForConfig().id}
|
|
640
|
+
</span>
|
|
641
|
+
<button
|
|
642
|
+
class="flowdrop-config-sidebar__copy-btn"
|
|
643
|
+
onclick={() => {
|
|
644
|
+
navigator.clipboard.writeText(selectedNodeForConfig().id);
|
|
645
|
+
}}
|
|
646
|
+
title="Copy Node ID"
|
|
647
|
+
aria-label="Copy node ID to clipboard"
|
|
648
|
+
>
|
|
649
|
+
📋
|
|
650
|
+
</button>
|
|
651
|
+
</div>
|
|
652
|
+
</div>
|
|
619
653
|
<div class="flowdrop-config-sidebar__detail">
|
|
620
654
|
<span class="flowdrop-config-sidebar__detail-label">Type:</span>
|
|
621
655
|
<span class="flowdrop-config-sidebar__detail-value"
|
|
@@ -942,6 +976,43 @@
|
|
|
942
976
|
font-weight: 500;
|
|
943
977
|
}
|
|
944
978
|
|
|
979
|
+
.flowdrop-config-sidebar__detail-value-with-copy {
|
|
980
|
+
display: flex;
|
|
981
|
+
align-items: center;
|
|
982
|
+
gap: 0.5rem;
|
|
983
|
+
background-color: #f3f4f6;
|
|
984
|
+
padding: 0.5rem;
|
|
985
|
+
border-radius: 0.375rem;
|
|
986
|
+
border: 1px solid #e5e7eb;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
.flowdrop-config-sidebar__detail-value-with-copy .flowdrop-config-sidebar__detail-value {
|
|
990
|
+
flex: 1;
|
|
991
|
+
font-size: 0.8125rem;
|
|
992
|
+
word-break: break-all;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
.flowdrop-config-sidebar__copy-btn {
|
|
996
|
+
background: white;
|
|
997
|
+
border: 1px solid #d1d5db;
|
|
998
|
+
border-radius: 0.25rem;
|
|
999
|
+
padding: 0.25rem 0.5rem;
|
|
1000
|
+
cursor: pointer;
|
|
1001
|
+
font-size: 1rem;
|
|
1002
|
+
transition: all 0.2s;
|
|
1003
|
+
flex-shrink: 0;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
.flowdrop-config-sidebar__copy-btn:hover {
|
|
1007
|
+
background-color: #f9fafb;
|
|
1008
|
+
border-color: #9ca3af;
|
|
1009
|
+
transform: scale(1.05);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
.flowdrop-config-sidebar__copy-btn:active {
|
|
1013
|
+
transform: scale(0.95);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
945
1016
|
.flowdrop-config-sidebar__detail-description {
|
|
946
1017
|
margin: 0;
|
|
947
1018
|
font-size: 0.875rem;
|
|
@@ -5,96 +5,89 @@
|
|
|
5
5
|
-->
|
|
6
6
|
|
|
7
7
|
<script lang="ts">
|
|
8
|
-
import type { ConfigSchema, WorkflowNode } from
|
|
8
|
+
import type { ConfigSchema, WorkflowNode } from '../types/index.js';
|
|
9
9
|
|
|
10
10
|
interface Props {
|
|
11
|
-
node: WorkflowNode
|
|
12
|
-
onSave: (config: Record<string, unknown>) => void
|
|
13
|
-
onCancel: () => void
|
|
11
|
+
node: WorkflowNode;
|
|
12
|
+
onSave: (config: Record<string, unknown>) => void;
|
|
13
|
+
onCancel: () => void;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
let { node, onSave, onCancel }: Props = $props()
|
|
16
|
+
let { node, onSave, onCancel }: Props = $props();
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Get the configuration schema from node metadata
|
|
20
20
|
*/
|
|
21
|
-
const configSchema = $derived(
|
|
22
|
-
node.data.metadata?.configSchema as ConfigSchema | undefined
|
|
23
|
-
)
|
|
21
|
+
const configSchema = $derived(node.data.metadata?.configSchema as ConfigSchema | undefined);
|
|
24
22
|
|
|
25
23
|
/**
|
|
26
24
|
* Get the current node configuration
|
|
27
25
|
*/
|
|
28
|
-
const nodeConfig = $derived(node.data.config || {})
|
|
26
|
+
const nodeConfig = $derived(node.data.config || {});
|
|
29
27
|
|
|
30
28
|
/**
|
|
31
29
|
* Create reactive configuration values using $state
|
|
32
30
|
* This fixes the Svelte 5 reactivity warnings
|
|
33
31
|
*/
|
|
34
|
-
let configValues = $state<Record<string, unknown>>({})
|
|
32
|
+
let configValues = $state<Record<string, unknown>>({});
|
|
35
33
|
|
|
36
34
|
/**
|
|
37
35
|
* Initialize config values when node or schema changes
|
|
38
36
|
*/
|
|
39
37
|
$effect(() => {
|
|
40
38
|
if (configSchema?.properties) {
|
|
41
|
-
const mergedConfig: Record<string, unknown> = {}
|
|
39
|
+
const mergedConfig: Record<string, unknown> = {};
|
|
42
40
|
Object.entries(configSchema.properties).forEach(([key, field]) => {
|
|
43
|
-
const fieldConfig = field as any
|
|
41
|
+
const fieldConfig = field as any;
|
|
44
42
|
// Use existing value if available, otherwise use default
|
|
45
|
-
mergedConfig[key] =
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
configValues = mergedConfig
|
|
43
|
+
mergedConfig[key] = nodeConfig[key] !== undefined ? nodeConfig[key] : fieldConfig.default;
|
|
44
|
+
});
|
|
45
|
+
configValues = mergedConfig;
|
|
49
46
|
}
|
|
50
|
-
})
|
|
47
|
+
});
|
|
51
48
|
|
|
52
49
|
/**
|
|
53
50
|
* Handle form submission
|
|
54
51
|
*/
|
|
55
52
|
function handleSave(): void {
|
|
56
53
|
// Collect all form values including hidden fields
|
|
57
|
-
const form = document.querySelector(
|
|
58
|
-
const updatedConfig: Record<string, unknown> = { ...configValues }
|
|
54
|
+
const form = document.querySelector('.flowdrop-config-sidebar__form');
|
|
55
|
+
const updatedConfig: Record<string, unknown> = { ...configValues };
|
|
59
56
|
|
|
60
57
|
if (form) {
|
|
61
|
-
const inputs = form.querySelectorAll(
|
|
58
|
+
const inputs = form.querySelectorAll('input, select, textarea');
|
|
62
59
|
inputs.forEach((input: any) => {
|
|
63
60
|
if (input.id) {
|
|
64
|
-
if (input.type ===
|
|
65
|
-
updatedConfig[input.id] = input.checked
|
|
66
|
-
} else if (input.type ===
|
|
67
|
-
updatedConfig[input.id] = input.value ? Number(input.value) : input.value
|
|
68
|
-
} else if (input.type ===
|
|
61
|
+
if (input.type === 'checkbox') {
|
|
62
|
+
updatedConfig[input.id] = input.checked;
|
|
63
|
+
} else if (input.type === 'number') {
|
|
64
|
+
updatedConfig[input.id] = input.value ? Number(input.value) : input.value;
|
|
65
|
+
} else if (input.type === 'hidden') {
|
|
69
66
|
// Parse hidden field values that might be JSON
|
|
70
67
|
try {
|
|
71
|
-
const parsed = JSON.parse(input.value)
|
|
72
|
-
updatedConfig[input.id] = parsed
|
|
68
|
+
const parsed = JSON.parse(input.value);
|
|
69
|
+
updatedConfig[input.id] = parsed;
|
|
73
70
|
} catch {
|
|
74
71
|
// If not JSON, use raw value
|
|
75
|
-
updatedConfig[input.id] = input.value
|
|
72
|
+
updatedConfig[input.id] = input.value;
|
|
76
73
|
}
|
|
77
74
|
} else {
|
|
78
|
-
updatedConfig[input.id] = input.value
|
|
75
|
+
updatedConfig[input.id] = input.value;
|
|
79
76
|
}
|
|
80
77
|
}
|
|
81
|
-
})
|
|
78
|
+
});
|
|
82
79
|
}
|
|
83
80
|
|
|
84
81
|
// Preserve hidden field values from original config if not collected from form
|
|
85
82
|
if (node.data.config && configSchema?.properties) {
|
|
86
83
|
Object.entries(configSchema.properties).forEach(([key, property]: [string, any]) => {
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
!(key in updatedConfig) &&
|
|
90
|
-
key in node.data.config
|
|
91
|
-
) {
|
|
92
|
-
updatedConfig[key] = node.data.config[key]
|
|
84
|
+
if (property.format === 'hidden' && !(key in updatedConfig) && key in node.data.config) {
|
|
85
|
+
updatedConfig[key] = node.data.config[key];
|
|
93
86
|
}
|
|
94
|
-
})
|
|
87
|
+
});
|
|
95
88
|
}
|
|
96
89
|
|
|
97
|
-
onSave(updatedConfig)
|
|
90
|
+
onSave(updatedConfig);
|
|
98
91
|
}
|
|
99
92
|
</script>
|
|
100
93
|
|
|
@@ -103,7 +96,7 @@
|
|
|
103
96
|
{#if configSchema.properties}
|
|
104
97
|
{#each Object.entries(configSchema.properties) as [key, field] (key)}
|
|
105
98
|
{@const fieldConfig = field as any}
|
|
106
|
-
{#if fieldConfig.format !==
|
|
99
|
+
{#if fieldConfig.format !== 'hidden'}
|
|
107
100
|
<div class="flowdrop-config-sidebar__field">
|
|
108
101
|
<label class="flowdrop-config-sidebar__field-label" for={key}>
|
|
109
102
|
{fieldConfig.title || fieldConfig.description || key}
|
|
@@ -120,18 +113,16 @@
|
|
|
120
113
|
checked={Array.isArray(configValues[key]) &&
|
|
121
114
|
configValues[key].includes(String(option))}
|
|
122
115
|
onchange={(e) => {
|
|
123
|
-
const checked = e.currentTarget.checked
|
|
116
|
+
const checked = e.currentTarget.checked;
|
|
124
117
|
const currentValues = Array.isArray(configValues[key])
|
|
125
118
|
? [...(configValues[key] as unknown[])]
|
|
126
|
-
: []
|
|
119
|
+
: [];
|
|
127
120
|
if (checked) {
|
|
128
121
|
if (!currentValues.includes(String(option))) {
|
|
129
|
-
configValues[key] = [...currentValues, String(option)]
|
|
122
|
+
configValues[key] = [...currentValues, String(option)];
|
|
130
123
|
}
|
|
131
124
|
} else {
|
|
132
|
-
configValues[key] = currentValues.filter(
|
|
133
|
-
(v) => v !== String(option)
|
|
134
|
-
)
|
|
125
|
+
configValues[key] = currentValues.filter((v) => v !== String(option));
|
|
135
126
|
}
|
|
136
127
|
}}
|
|
137
128
|
/>
|
|
@@ -152,42 +143,42 @@
|
|
|
152
143
|
<option value={String(option)}>{String(option)}</option>
|
|
153
144
|
{/each}
|
|
154
145
|
</select>
|
|
155
|
-
{:else if fieldConfig.type ===
|
|
146
|
+
{:else if fieldConfig.type === 'string' && fieldConfig.format === 'multiline'}
|
|
156
147
|
<!-- Textarea for multiline strings -->
|
|
157
148
|
<textarea
|
|
158
149
|
id={key}
|
|
159
150
|
class="flowdrop-config-sidebar__textarea"
|
|
160
151
|
bind:value={configValues[key]}
|
|
161
|
-
placeholder={String(fieldConfig.placeholder ||
|
|
152
|
+
placeholder={String(fieldConfig.placeholder || '')}
|
|
162
153
|
rows="4"
|
|
163
154
|
></textarea>
|
|
164
|
-
{:else if fieldConfig.type ===
|
|
155
|
+
{:else if fieldConfig.type === 'string'}
|
|
165
156
|
<input
|
|
166
157
|
id={key}
|
|
167
158
|
type="text"
|
|
168
159
|
class="flowdrop-config-sidebar__input"
|
|
169
160
|
bind:value={configValues[key]}
|
|
170
|
-
placeholder={String(fieldConfig.placeholder ||
|
|
161
|
+
placeholder={String(fieldConfig.placeholder || '')}
|
|
171
162
|
/>
|
|
172
|
-
{:else if fieldConfig.type ===
|
|
163
|
+
{:else if fieldConfig.type === 'number'}
|
|
173
164
|
<input
|
|
174
165
|
id={key}
|
|
175
166
|
type="number"
|
|
176
167
|
class="flowdrop-config-sidebar__input"
|
|
177
168
|
bind:value={configValues[key]}
|
|
178
|
-
placeholder={String(fieldConfig.placeholder ||
|
|
169
|
+
placeholder={String(fieldConfig.placeholder || '')}
|
|
179
170
|
/>
|
|
180
|
-
{:else if fieldConfig.type ===
|
|
171
|
+
{:else if fieldConfig.type === 'boolean'}
|
|
181
172
|
<input
|
|
182
173
|
id={key}
|
|
183
174
|
type="checkbox"
|
|
184
175
|
class="flowdrop-config-sidebar__checkbox"
|
|
185
176
|
checked={Boolean(configValues[key] || fieldConfig.default || false)}
|
|
186
177
|
onchange={(e) => {
|
|
187
|
-
configValues[key] = e.currentTarget.checked
|
|
178
|
+
configValues[key] = e.currentTarget.checked;
|
|
188
179
|
}}
|
|
189
180
|
/>
|
|
190
|
-
{:else if fieldConfig.type ===
|
|
181
|
+
{:else if fieldConfig.type === 'select' || fieldConfig.options}
|
|
191
182
|
<select
|
|
192
183
|
id={key}
|
|
193
184
|
class="flowdrop-config-sidebar__select"
|
|
@@ -196,9 +187,7 @@
|
|
|
196
187
|
{#if fieldConfig.options}
|
|
197
188
|
{#each fieldConfig.options as option (String(option.value))}
|
|
198
189
|
{@const optionConfig = option as any}
|
|
199
|
-
<option value={String(optionConfig.value)}
|
|
200
|
-
>{String(optionConfig.label)}</option
|
|
201
|
-
>
|
|
190
|
+
<option value={String(optionConfig.value)}>{String(optionConfig.label)}</option>
|
|
202
191
|
{/each}
|
|
203
192
|
{/if}
|
|
204
193
|
</select>
|
|
@@ -209,7 +198,7 @@
|
|
|
209
198
|
type="text"
|
|
210
199
|
class="flowdrop-config-sidebar__input"
|
|
211
200
|
bind:value={configValues[key]}
|
|
212
|
-
placeholder={String(fieldConfig.placeholder ||
|
|
201
|
+
placeholder={String(fieldConfig.placeholder || '')}
|
|
213
202
|
/>
|
|
214
203
|
{/if}
|
|
215
204
|
{#if fieldConfig.description}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Flow Drop Zone Component
|
|
3
|
+
Handles drag and drop with proper coordinate transformation
|
|
4
|
+
Must be used inside SvelteFlowProvider
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
import { useSvelteFlow } from "@xyflow/svelte";
|
|
9
|
+
import type { Snippet } from "svelte";
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
ondrop: (nodeTypeData: string, position: { x: number; y: number }) => void;
|
|
13
|
+
children: Snippet;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let props: Props = $props();
|
|
17
|
+
|
|
18
|
+
// Access SvelteFlow instance for coordinate transformation
|
|
19
|
+
const { screenToFlowPosition } = useSvelteFlow();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Handle drag over event
|
|
23
|
+
*/
|
|
24
|
+
function handleDragOver(e: DragEvent): void {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
if (e.dataTransfer) {
|
|
27
|
+
e.dataTransfer.dropEffect = "copy";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Handle drop event with proper coordinate transformation
|
|
33
|
+
*/
|
|
34
|
+
function handleDrop(e: DragEvent): void {
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
|
|
37
|
+
// Get the data from the drag event
|
|
38
|
+
const nodeTypeData = e.dataTransfer?.getData("application/json");
|
|
39
|
+
if (nodeTypeData) {
|
|
40
|
+
// Convert screen coordinates to flow coordinates (accounts for zoom and pan)
|
|
41
|
+
const position = screenToFlowPosition({
|
|
42
|
+
x: e.clientX,
|
|
43
|
+
y: e.clientY
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Call the parent handler with the converted position
|
|
47
|
+
props.ondrop(nodeTypeData, position);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<div
|
|
53
|
+
class="flow-drop-zone"
|
|
54
|
+
role="application"
|
|
55
|
+
aria-label="Workflow canvas"
|
|
56
|
+
ondragover={handleDragOver}
|
|
57
|
+
ondrop={handleDrop}
|
|
58
|
+
>
|
|
59
|
+
{@render props.children()}
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<style>
|
|
63
|
+
.flow-drop-zone {
|
|
64
|
+
width: 100%;
|
|
65
|
+
height: 100%;
|
|
66
|
+
}
|
|
67
|
+
</style>
|
|
68
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
interface Props {
|
|
3
|
+
ondrop: (nodeTypeData: string, position: {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
}) => void;
|
|
7
|
+
children: Snippet;
|
|
8
|
+
}
|
|
9
|
+
declare const FlowDropZone: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type FlowDropZone = ReturnType<typeof FlowDropZone>;
|
|
11
|
+
export default FlowDropZone;
|
|
@@ -29,10 +29,12 @@
|
|
|
29
29
|
) => void;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
let { pipelineId, workflow, apiClient, baseUrl, endpointConfig, onActionsReady }: Props =
|
|
32
|
+
let { pipelineId, workflow, apiClient, baseUrl, endpointConfig, onActionsReady }: Props =
|
|
33
|
+
$props();
|
|
33
34
|
|
|
34
35
|
// Initialize API client if not provided
|
|
35
|
-
const client =
|
|
36
|
+
const client =
|
|
37
|
+
apiClient || new FlowDropApiClient(endpointConfig?.baseUrl || baseUrl || '/api/flowdrop');
|
|
36
38
|
|
|
37
39
|
// Pipeline status and job data
|
|
38
40
|
let pipelineStatus = $state<string>('unknown');
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
WorkflowEdge
|
|
23
23
|
} from '../types/index.js';
|
|
24
24
|
import CanvasBanner from './CanvasBanner.svelte';
|
|
25
|
+
import FlowDropZone from './FlowDropZone.svelte';
|
|
25
26
|
import { tick } from 'svelte';
|
|
26
27
|
import type { EndpointConfig } from '../config/endpoints.js';
|
|
27
28
|
import ConnectionLine from './ConnectionLine.svelte';
|
|
@@ -34,11 +35,7 @@
|
|
|
34
35
|
ConfigurationHelper
|
|
35
36
|
} from '../helpers/workflowEditorHelper.js';
|
|
36
37
|
import type { NodeExecutionInfo } from '../types/index.js';
|
|
37
|
-
import {
|
|
38
|
-
areNodeArraysEqual,
|
|
39
|
-
areEdgeArraysEqual,
|
|
40
|
-
throttle
|
|
41
|
-
} from '../utils/performanceUtils.js';
|
|
38
|
+
import { areNodeArraysEqual, areEdgeArraysEqual, throttle } from '../utils/performanceUtils.js';
|
|
42
39
|
|
|
43
40
|
interface Props {
|
|
44
41
|
nodes?: NodeMetadata[];
|
|
@@ -115,7 +112,7 @@
|
|
|
115
112
|
previousPipelineId = props.pipelineId;
|
|
116
113
|
|
|
117
114
|
// Use requestIdleCallback for non-critical updates (falls back to setTimeout)
|
|
118
|
-
if (typeof requestIdleCallback !==
|
|
115
|
+
if (typeof requestIdleCallback !== 'undefined') {
|
|
119
116
|
loadExecutionInfoTimeout = requestIdleCallback(
|
|
120
117
|
() => {
|
|
121
118
|
loadNodeExecutionInfo();
|
|
@@ -167,7 +164,7 @@
|
|
|
167
164
|
|
|
168
165
|
// Default execution info for nodes without data
|
|
169
166
|
const defaultExecutionInfo: NodeExecutionInfo = {
|
|
170
|
-
status:
|
|
167
|
+
status: 'idle' as const,
|
|
171
168
|
executionCount: 0,
|
|
172
169
|
isExecuting: false
|
|
173
170
|
};
|
|
@@ -191,8 +188,8 @@
|
|
|
191
188
|
executionInfoAbortController = null;
|
|
192
189
|
} catch (error) {
|
|
193
190
|
// Only log if it's not an abort error
|
|
194
|
-
if (error instanceof Error && error.name !==
|
|
195
|
-
console.error(
|
|
191
|
+
if (error instanceof Error && error.name !== 'AbortError') {
|
|
192
|
+
console.error('Failed to load node execution info:', error);
|
|
196
193
|
}
|
|
197
194
|
}
|
|
198
195
|
}
|
|
@@ -278,6 +275,23 @@
|
|
|
278
275
|
}
|
|
279
276
|
}
|
|
280
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Handle node deletion - automatically remove connected edges
|
|
280
|
+
*/
|
|
281
|
+
function handleNodesDelete(params: { nodes: WorkflowNodeType[]; edges: WorkflowEdge[] }): void {
|
|
282
|
+
const deletedNodeIds = new Set(params.nodes.map((node) => node.id));
|
|
283
|
+
|
|
284
|
+
// Filter out edges connected to deleted nodes
|
|
285
|
+
flowEdges = flowEdges.filter(
|
|
286
|
+
(edge) => !deletedNodeIds.has(edge.source) && !deletedNodeIds.has(edge.target)
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
// Update currentWorkflow
|
|
290
|
+
if (currentWorkflow) {
|
|
291
|
+
updateCurrentWorkflowFromSvelteFlow();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
281
295
|
/**
|
|
282
296
|
* Update existing edges with our custom styling rules
|
|
283
297
|
* This ensures all edges (including existing ones) follow our rules
|
|
@@ -316,6 +330,30 @@
|
|
|
316
330
|
function checkWorkflowCycles(): boolean {
|
|
317
331
|
return WorkflowOperationsHelper.checkWorkflowCycles(flowNodes, flowEdges);
|
|
318
332
|
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Handle drop event and add new node to canvas
|
|
336
|
+
* This will be called from the inner DropZone component
|
|
337
|
+
*/
|
|
338
|
+
async function handleNodeDrop(
|
|
339
|
+
nodeTypeData: string,
|
|
340
|
+
position: { x: number; y: number }
|
|
341
|
+
): Promise<void> {
|
|
342
|
+
// Create the node using the helper, passing existing nodes for ID generation
|
|
343
|
+
const newNode = NodeOperationsHelper.createNodeFromDrop(nodeTypeData, position, flowNodes);
|
|
344
|
+
|
|
345
|
+
if (newNode && currentWorkflow) {
|
|
346
|
+
currentWorkflow = WorkflowOperationsHelper.addNode(currentWorkflow, newNode);
|
|
347
|
+
|
|
348
|
+
// Update the global store
|
|
349
|
+
updateGlobalStore();
|
|
350
|
+
|
|
351
|
+
// Wait for DOM update to ensure SvelteFlow updates
|
|
352
|
+
await tick();
|
|
353
|
+
} else if (!currentWorkflow) {
|
|
354
|
+
console.warn('No currentWorkflow available for new node');
|
|
355
|
+
}
|
|
356
|
+
}
|
|
319
357
|
</script>
|
|
320
358
|
|
|
321
359
|
<SvelteFlowProvider>
|
|
@@ -323,75 +361,41 @@
|
|
|
323
361
|
<!-- Main Editor Area -->
|
|
324
362
|
<div class="flowdrop-workflow-editor__main">
|
|
325
363
|
<!-- Flow Canvas -->
|
|
326
|
-
<div
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
}}
|
|
363
|
-
>
|
|
364
|
-
<SvelteFlow
|
|
365
|
-
bind:nodes={flowNodes}
|
|
366
|
-
bind:edges={flowEdges}
|
|
367
|
-
{nodeTypes}
|
|
368
|
-
{defaultEdgeOptions}
|
|
369
|
-
onconnect={handleConnect}
|
|
370
|
-
minZoom={0.2}
|
|
371
|
-
maxZoom={3}
|
|
372
|
-
clickConnect={true}
|
|
373
|
-
elevateEdgesOnSelect={true}
|
|
374
|
-
connectionLineType={ConnectionLineType.Bezier}
|
|
375
|
-
connectionLineComponent={ConnectionLine}
|
|
376
|
-
snapGrid={[10, 10]}
|
|
377
|
-
fitView
|
|
378
|
-
>
|
|
379
|
-
<Controls />
|
|
380
|
-
<Background
|
|
381
|
-
gap={10}
|
|
382
|
-
bgColor="var(--flowdrop-background-color)"
|
|
383
|
-
variant={BackgroundVariant.Dots}
|
|
384
|
-
/>
|
|
385
|
-
<MiniMap />
|
|
386
|
-
</SvelteFlow>
|
|
387
|
-
<!-- Drop Zone Indicator -->
|
|
388
|
-
{#if flowNodes.length === 0}
|
|
389
|
-
<CanvasBanner
|
|
390
|
-
title="Drag components here to start building"
|
|
391
|
-
description="Use the sidebar to add components to your workflow"
|
|
392
|
-
iconName="mdi:graph"
|
|
393
|
-
/>
|
|
394
|
-
{/if}
|
|
364
|
+
<div class="flowdrop-canvas">
|
|
365
|
+
<FlowDropZone ondrop={handleNodeDrop}>
|
|
366
|
+
<SvelteFlow
|
|
367
|
+
bind:nodes={flowNodes}
|
|
368
|
+
bind:edges={flowEdges}
|
|
369
|
+
{nodeTypes}
|
|
370
|
+
{defaultEdgeOptions}
|
|
371
|
+
onconnect={handleConnect}
|
|
372
|
+
ondelete={handleNodesDelete}
|
|
373
|
+
minZoom={0.2}
|
|
374
|
+
maxZoom={3}
|
|
375
|
+
clickConnect={true}
|
|
376
|
+
elevateEdgesOnSelect={true}
|
|
377
|
+
connectionLineType={ConnectionLineType.Bezier}
|
|
378
|
+
connectionLineComponent={ConnectionLine}
|
|
379
|
+
snapGrid={[10, 10]}
|
|
380
|
+
fitView
|
|
381
|
+
>
|
|
382
|
+
<Controls />
|
|
383
|
+
<Background
|
|
384
|
+
gap={10}
|
|
385
|
+
bgColor="var(--flowdrop-background-color)"
|
|
386
|
+
variant={BackgroundVariant.Dots}
|
|
387
|
+
/>
|
|
388
|
+
<MiniMap />
|
|
389
|
+
</SvelteFlow>
|
|
390
|
+
<!-- Drop Zone Indicator -->
|
|
391
|
+
{#if flowNodes.length === 0}
|
|
392
|
+
<CanvasBanner
|
|
393
|
+
title="Drag components here to start building"
|
|
394
|
+
description="Use the sidebar to add components to your workflow"
|
|
395
|
+
iconName="mdi:graph"
|
|
396
|
+
/>
|
|
397
|
+
{/if}
|
|
398
|
+
</FlowDropZone>
|
|
395
399
|
</div>
|
|
396
400
|
|
|
397
401
|
<!-- Status Bar -->
|
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { WorkflowNode as WorkflowNodeType, NodeMetadata, Workflow, WorkflowEdge, NodeExecutionInfo } from '../types/index.js';
|
|
6
6
|
import type { EndpointConfig } from '../config/endpoints.js';
|
|
7
|
+
/**
|
|
8
|
+
* Generate a unique node ID based on node type and existing nodes
|
|
9
|
+
* Format: <node_type>.<number>
|
|
10
|
+
* Example: boolean_gateway.1, calculator.2
|
|
11
|
+
*/
|
|
12
|
+
export declare function generateNodeId(nodeTypeId: string, existingNodes: WorkflowNodeType[]): string;
|
|
7
13
|
/**
|
|
8
14
|
* Edge styling configuration
|
|
9
15
|
*/
|
|
@@ -37,7 +43,7 @@ export declare class NodeOperationsHelper {
|
|
|
37
43
|
static createNodeFromDrop(nodeTypeData: string, position: {
|
|
38
44
|
x: number;
|
|
39
45
|
y: number;
|
|
40
|
-
}): WorkflowNodeType | null;
|
|
46
|
+
}, existingNodes?: WorkflowNodeType[]): WorkflowNodeType | null;
|
|
41
47
|
}
|
|
42
48
|
/**
|
|
43
49
|
* Workflow operations helper
|
|
@@ -8,6 +8,27 @@ import { workflowApi, nodeApi, setEndpointConfig } from '../services/api.js';
|
|
|
8
8
|
import { v4 as uuidv4 } from 'uuid';
|
|
9
9
|
import { workflowActions } from '../stores/workflowStore.js';
|
|
10
10
|
import { nodeExecutionService } from '../services/nodeExecutionService.js';
|
|
11
|
+
/**
|
|
12
|
+
* Generate a unique node ID based on node type and existing nodes
|
|
13
|
+
* Format: <node_type>.<number>
|
|
14
|
+
* Example: boolean_gateway.1, calculator.2
|
|
15
|
+
*/
|
|
16
|
+
export function generateNodeId(nodeTypeId, existingNodes) {
|
|
17
|
+
// Count how many nodes of this type already exist
|
|
18
|
+
const existingNodeIds = existingNodes
|
|
19
|
+
.filter((node) => node.data?.metadata?.id === nodeTypeId)
|
|
20
|
+
.map((node) => node.id);
|
|
21
|
+
// Extract the numbers from existing IDs with the same prefix
|
|
22
|
+
const existingNumbers = existingNodeIds
|
|
23
|
+
.map((id) => {
|
|
24
|
+
const match = id.match(new RegExp(`^${nodeTypeId}\\.(\\d+)$`));
|
|
25
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
26
|
+
})
|
|
27
|
+
.filter((num) => num > 0);
|
|
28
|
+
// Find the next available number (highest + 1)
|
|
29
|
+
const nextNumber = existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : 1;
|
|
30
|
+
return `${nodeTypeId}.${nextNumber}`;
|
|
31
|
+
}
|
|
11
32
|
/**
|
|
12
33
|
* Edge styling configuration
|
|
13
34
|
*/
|
|
@@ -132,7 +153,7 @@ export class NodeOperationsHelper {
|
|
|
132
153
|
/**
|
|
133
154
|
* Create a new node from dropped data
|
|
134
155
|
*/
|
|
135
|
-
static createNodeFromDrop(nodeTypeData, position) {
|
|
156
|
+
static createNodeFromDrop(nodeTypeData, position, existingNodes = []) {
|
|
136
157
|
try {
|
|
137
158
|
const parsedData = JSON.parse(nodeTypeData);
|
|
138
159
|
// Handle both old format (with type: "node") and new format (direct NodeMetadata)
|
|
@@ -169,7 +190,8 @@ export class NodeOperationsHelper {
|
|
|
169
190
|
metadata: nodeType
|
|
170
191
|
};
|
|
171
192
|
}
|
|
172
|
-
|
|
193
|
+
// Generate node ID based on node type and existing nodes
|
|
194
|
+
const newNodeId = generateNodeId(nodeType.id, existingNodes);
|
|
173
195
|
// All nodes use "universalNode" type
|
|
174
196
|
// UniversalNode component handles internal switching based on metadata and config
|
|
175
197
|
const newNode = {
|
package/dist/services/api.js
CHANGED
|
@@ -51,10 +51,37 @@ async function apiRequest(endpointKey, endpointPath, params, options = {}) {
|
|
|
51
51
|
headers,
|
|
52
52
|
...options
|
|
53
53
|
});
|
|
54
|
-
|
|
54
|
+
// Check if response is JSON
|
|
55
|
+
const contentType = response.headers.get('content-type');
|
|
56
|
+
const isJson = contentType?.includes('application/json');
|
|
55
57
|
if (!response.ok) {
|
|
56
|
-
|
|
58
|
+
// Try to get error details
|
|
59
|
+
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
|
60
|
+
if (isJson) {
|
|
61
|
+
try {
|
|
62
|
+
const data = await response.json();
|
|
63
|
+
errorMessage = data.error || data.message || errorMessage;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Failed to parse JSON, use default error message
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
// Response is not JSON (probably HTML error page)
|
|
71
|
+
try {
|
|
72
|
+
const text = await response.text();
|
|
73
|
+
// Extract first 200 characters for debugging
|
|
74
|
+
const preview = text.substring(0, 200).trim();
|
|
75
|
+
errorMessage = `${errorMessage}. Server returned: ${preview}...`;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Failed to read response text
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
throw new Error(errorMessage);
|
|
57
82
|
}
|
|
83
|
+
// Parse successful response
|
|
84
|
+
const data = await response.json();
|
|
58
85
|
return data;
|
|
59
86
|
}
|
|
60
87
|
/**
|
|
@@ -135,9 +162,16 @@ export const workflowApi = {
|
|
|
135
162
|
if (!endpointConfig) {
|
|
136
163
|
throw new Error('Endpoint configuration not set');
|
|
137
164
|
}
|
|
165
|
+
// Transform workflow data for Drupal backend compatibility
|
|
166
|
+
// Drupal expects "label" instead of "name"
|
|
167
|
+
const drupalWorkflow = {
|
|
168
|
+
...workflow,
|
|
169
|
+
label: workflow.name, // Map name to label for Drupal
|
|
170
|
+
name: workflow.name // Keep name as well for compatibility
|
|
171
|
+
};
|
|
138
172
|
const response = await apiRequest('workflows.create', endpointConfig.endpoints.workflows.create, undefined, {
|
|
139
173
|
method: 'POST',
|
|
140
|
-
body: JSON.stringify(
|
|
174
|
+
body: JSON.stringify(drupalWorkflow)
|
|
141
175
|
});
|
|
142
176
|
if (!response.data) {
|
|
143
177
|
throw new Error('Failed to create workflow');
|
|
@@ -151,9 +185,18 @@ export const workflowApi = {
|
|
|
151
185
|
if (!endpointConfig) {
|
|
152
186
|
throw new Error('Endpoint configuration not set');
|
|
153
187
|
}
|
|
188
|
+
// Transform workflow data for Drupal backend compatibility
|
|
189
|
+
// Drupal expects "label" instead of "name"
|
|
190
|
+
const drupalWorkflow = workflow.name
|
|
191
|
+
? {
|
|
192
|
+
...workflow,
|
|
193
|
+
label: workflow.name, // Map name to label for Drupal
|
|
194
|
+
name: workflow.name // Keep name as well for compatibility
|
|
195
|
+
}
|
|
196
|
+
: workflow;
|
|
154
197
|
const response = await apiRequest('workflows.update', endpointConfig.endpoints.workflows.update, { id }, {
|
|
155
198
|
method: 'PUT',
|
|
156
|
-
body: JSON.stringify(
|
|
199
|
+
body: JSON.stringify(drupalWorkflow)
|
|
157
200
|
});
|
|
158
201
|
if (!response.data) {
|
|
159
202
|
throw new Error('Failed to update workflow');
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Performance Utilities
|
|
3
3
|
* Helper functions for optimizing performance in the FlowDrop app
|
|
4
4
|
*/
|
|
5
|
-
import type { WorkflowNode, WorkflowEdge } from
|
|
5
|
+
import type { WorkflowNode, WorkflowEdge } from '../types/index.js';
|
|
6
6
|
/**
|
|
7
7
|
* Fast shallow comparison for workflow nodes
|
|
8
8
|
* Avoids expensive JSON.stringify operations
|
|
@@ -16,8 +16,7 @@ export function areNodeArraysEqual(nodes1, nodes2) {
|
|
|
16
16
|
if (node1?.id !== node2?.id)
|
|
17
17
|
return false;
|
|
18
18
|
// Check position (most common change during drag)
|
|
19
|
-
if (node1?.position?.x !== node2?.position?.x ||
|
|
20
|
-
node1?.position?.y !== node2?.position?.y) {
|
|
19
|
+
if (node1?.position?.x !== node2?.position?.x || node1?.position?.y !== node2?.position?.y) {
|
|
21
20
|
return false;
|
|
22
21
|
}
|
|
23
22
|
// Check selected state
|