@d34dman/flowdrop 0.0.11 → 0.0.13
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/PipelineStatus.svelte +4 -2
- package/dist/components/WorkflowEditor.svelte +29 -11
- package/dist/helpers/workflowEditorHelper.d.ts +7 -1
- package/dist/helpers/workflowEditorHelper.js +24 -2
- package/dist/services/api.js +47 -4
- package/dist/styles/base.css +0 -21
- 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}
|
|
@@ -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');
|
|
@@ -34,11 +34,7 @@
|
|
|
34
34
|
ConfigurationHelper
|
|
35
35
|
} from '../helpers/workflowEditorHelper.js';
|
|
36
36
|
import type { NodeExecutionInfo } from '../types/index.js';
|
|
37
|
-
import {
|
|
38
|
-
areNodeArraysEqual,
|
|
39
|
-
areEdgeArraysEqual,
|
|
40
|
-
throttle
|
|
41
|
-
} from '../utils/performanceUtils.js';
|
|
37
|
+
import { areNodeArraysEqual, areEdgeArraysEqual, throttle } from '../utils/performanceUtils.js';
|
|
42
38
|
|
|
43
39
|
interface Props {
|
|
44
40
|
nodes?: NodeMetadata[];
|
|
@@ -115,7 +111,7 @@
|
|
|
115
111
|
previousPipelineId = props.pipelineId;
|
|
116
112
|
|
|
117
113
|
// Use requestIdleCallback for non-critical updates (falls back to setTimeout)
|
|
118
|
-
if (typeof requestIdleCallback !==
|
|
114
|
+
if (typeof requestIdleCallback !== 'undefined') {
|
|
119
115
|
loadExecutionInfoTimeout = requestIdleCallback(
|
|
120
116
|
() => {
|
|
121
117
|
loadNodeExecutionInfo();
|
|
@@ -167,7 +163,7 @@
|
|
|
167
163
|
|
|
168
164
|
// Default execution info for nodes without data
|
|
169
165
|
const defaultExecutionInfo: NodeExecutionInfo = {
|
|
170
|
-
status:
|
|
166
|
+
status: 'idle' as const,
|
|
171
167
|
executionCount: 0,
|
|
172
168
|
isExecuting: false
|
|
173
169
|
};
|
|
@@ -191,8 +187,8 @@
|
|
|
191
187
|
executionInfoAbortController = null;
|
|
192
188
|
} catch (error) {
|
|
193
189
|
// Only log if it's not an abort error
|
|
194
|
-
if (error instanceof Error && error.name !==
|
|
195
|
-
console.error(
|
|
190
|
+
if (error instanceof Error && error.name !== 'AbortError') {
|
|
191
|
+
console.error('Failed to load node execution info:', error);
|
|
196
192
|
}
|
|
197
193
|
}
|
|
198
194
|
}
|
|
@@ -278,6 +274,23 @@
|
|
|
278
274
|
}
|
|
279
275
|
}
|
|
280
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Handle node deletion - automatically remove connected edges
|
|
279
|
+
*/
|
|
280
|
+
function handleNodesDelete(event: { detail: { nodes: WorkflowNodeType[] } }): void {
|
|
281
|
+
const deletedNodeIds = new Set(event.detail.nodes.map((node) => node.id));
|
|
282
|
+
|
|
283
|
+
// Filter out edges connected to deleted nodes
|
|
284
|
+
flowEdges = flowEdges.filter(
|
|
285
|
+
(edge) => !deletedNodeIds.has(edge.source) && !deletedNodeIds.has(edge.target)
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
// Update currentWorkflow
|
|
289
|
+
if (currentWorkflow) {
|
|
290
|
+
updateCurrentWorkflowFromSvelteFlow();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
281
294
|
/**
|
|
282
295
|
* Update existing edges with our custom styling rules
|
|
283
296
|
* This ensures all edges (including existing ones) follow our rules
|
|
@@ -344,8 +357,12 @@
|
|
|
344
357
|
y: e.clientY - rect.top
|
|
345
358
|
};
|
|
346
359
|
|
|
347
|
-
// Create the node using the helper
|
|
348
|
-
const newNode = NodeOperationsHelper.createNodeFromDrop(
|
|
360
|
+
// Create the node using the helper, passing existing nodes for ID generation
|
|
361
|
+
const newNode = NodeOperationsHelper.createNodeFromDrop(
|
|
362
|
+
nodeTypeData,
|
|
363
|
+
position,
|
|
364
|
+
flowNodes
|
|
365
|
+
);
|
|
349
366
|
|
|
350
367
|
if (newNode && currentWorkflow) {
|
|
351
368
|
currentWorkflow = WorkflowOperationsHelper.addNode(currentWorkflow, newNode);
|
|
@@ -367,6 +384,7 @@
|
|
|
367
384
|
{nodeTypes}
|
|
368
385
|
{defaultEdgeOptions}
|
|
369
386
|
onconnect={handleConnect}
|
|
387
|
+
onnodesdelete={handleNodesDelete}
|
|
370
388
|
minZoom={0.2}
|
|
371
389
|
maxZoom={3}
|
|
372
390
|
clickConnect={true}
|
|
@@ -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');
|
package/dist/styles/base.css
CHANGED
|
@@ -5,27 +5,6 @@
|
|
|
5
5
|
box-sizing: border-box;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
html,
|
|
9
|
-
body {
|
|
10
|
-
height: 100%;
|
|
11
|
-
margin: 0;
|
|
12
|
-
padding: 0;
|
|
13
|
-
font-family:
|
|
14
|
-
system-ui,
|
|
15
|
-
-apple-system,
|
|
16
|
-
BlinkMacSystemFont,
|
|
17
|
-
'Segoe UI',
|
|
18
|
-
Roboto,
|
|
19
|
-
'Helvetica Neue',
|
|
20
|
-
Arial,
|
|
21
|
-
sans-serif;
|
|
22
|
-
overflow: hidden;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
#svelte {
|
|
26
|
-
height: 100%;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
8
|
/* Layout utilities */
|
|
30
9
|
.flowdrop-layout {
|
|
31
10
|
display: flex;
|
|
@@ -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
|