@flowdrop/flowdrop 2.0.0-beta.3 → 2.0.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/README.md +5 -5
- package/dist/adapters/WorkflowAdapter.js +4 -5
- package/dist/adapters/agentspec/AgentSpecAdapter.js +3 -3
- package/dist/adapters/agentspec/defaultNodeTypes.js +9 -9
- package/dist/commands/executor.js +5 -6
- package/dist/commands/types.js +5 -5
- package/dist/components/App.svelte +19 -150
- package/dist/components/Button.stories.svelte +65 -0
- package/dist/components/Button.stories.svelte.d.ts +19 -0
- package/dist/components/Button.svelte +62 -0
- package/dist/components/Button.svelte.d.ts +24 -0
- package/dist/components/ConfigForm.svelte +4 -4
- package/dist/components/EditorStatusBar.stories.svelte +44 -0
- package/dist/components/EditorStatusBar.stories.svelte.d.ts +27 -0
- package/dist/components/EditorStatusBar.svelte +99 -0
- package/dist/components/EditorStatusBar.svelte.d.ts +15 -0
- package/dist/components/IconButton.svelte +80 -0
- package/dist/components/IconButton.svelte.d.ts +30 -0
- package/dist/components/Input.svelte +74 -0
- package/dist/components/Input.svelte.d.ts +17 -0
- package/dist/components/Navbar.svelte +9 -4
- package/dist/components/Navbar.svelte.d.ts +3 -0
- package/dist/components/NodeSidebar.svelte +17 -115
- package/dist/components/NodeSwapPicker.svelte +12 -28
- package/dist/components/Select.svelte +53 -0
- package/dist/components/Select.svelte.d.ts +15 -0
- package/dist/components/Textarea.svelte +39 -0
- package/dist/components/Textarea.svelte.d.ts +12 -0
- package/dist/components/ThemeToggle.svelte +15 -89
- package/dist/components/UniversalNode.svelte +5 -4
- package/dist/components/UniversalNode.svelte.d.ts +1 -1
- package/dist/components/console/ConsoleInput.svelte +3 -3
- package/dist/components/form/FormArray.svelte +37 -157
- package/dist/components/form/FormCheckboxGroup.svelte +1 -1
- package/dist/components/form/FormField.svelte +5 -44
- package/dist/components/form/FormFieldLight.svelte +5 -44
- package/dist/components/form/FormFieldset.svelte +1 -1
- package/dist/components/form/FormNumberField.svelte +4 -32
- package/dist/components/form/FormRangeField.svelte +17 -7
- package/dist/components/form/FormSelect.svelte +13 -79
- package/dist/components/form/FormTextField.svelte +3 -39
- package/dist/components/form/FormTextarea.svelte +4 -43
- package/dist/components/form/resolveFieldType.d.ts +24 -0
- package/dist/components/form/resolveFieldType.js +55 -0
- package/dist/components/icons/CloseIcon.svelte +6 -0
- package/dist/components/icons/CloseIcon.svelte.d.ts +26 -0
- package/dist/components/nodes/AtomNode.svelte +3 -3
- package/dist/components/nodes/AtomNode.svelte.d.ts +1 -1
- package/dist/components/nodes/GatewayNode.svelte +8 -11
- package/dist/components/nodes/GatewayNode.svelte.d.ts +1 -1
- package/dist/components/nodes/IdeaNode.svelte +10 -8
- package/dist/components/nodes/IdeaNode.svelte.d.ts +8 -4
- package/dist/components/nodes/NotesNode.svelte +6 -4
- package/dist/components/nodes/NotesNode.svelte.d.ts +8 -4
- package/dist/components/nodes/SimpleNode.svelte +10 -8
- package/dist/components/nodes/SimpleNode.svelte.d.ts +4 -4
- package/dist/components/nodes/SquareNode.svelte +10 -8
- package/dist/components/nodes/SquareNode.svelte.d.ts +4 -4
- package/dist/components/nodes/TerminalNode.svelte +10 -8
- package/dist/components/nodes/TerminalNode.svelte.d.ts +4 -4
- package/dist/components/nodes/ToolNode.svelte +10 -8
- package/dist/components/nodes/ToolNode.svelte.d.ts +6 -6
- package/dist/components/nodes/WorkflowNode.svelte +7 -10
- package/dist/components/nodes/WorkflowNode.svelte.d.ts +1 -1
- package/dist/components/playground/InputCollector.svelte +11 -46
- package/dist/components/playground/PipelineKanbanView.svelte +2 -2
- package/dist/components/playground/PipelineTableView.svelte +2 -2
- package/dist/helpers/workflowEditorHelper.js +4 -5
- package/dist/messages/index.d.ts +1 -1
- package/dist/messages/index.js +1 -1
- package/dist/openapi/v1/openapi.yaml +2 -2
- package/dist/registry/nodeComponentRegistry.d.ts +2 -1
- package/dist/services/dynamicSchemaService.d.ts +2 -2
- package/dist/services/dynamicSchemaService.js +8 -8
- package/dist/skins/drafter.js +41 -28
- package/dist/stores/playgroundStore.svelte.js +1 -1
- package/dist/stores/workflowStore.svelte.js +0 -18
- package/dist/styles/base.css +247 -5
- package/dist/styles/tokens.css +6 -0
- package/dist/svelte-app.js +68 -107
- package/dist/types/index.d.ts +6 -7
- package/dist/utils/connections.js +20 -56
- package/dist/utils/nodeIds.d.ts +1 -1
- package/dist/utils/nodeIds.js +1 -1
- package/dist/utils/nodeSwap.js +3 -6
- package/package.json +1 -1
package/dist/svelte-app.js
CHANGED
|
@@ -55,6 +55,58 @@ function releaseInstance(fd, isDefault) {
|
|
|
55
55
|
fd.destroy();
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Resolve endpoint config, port config and categories from mount options and
|
|
60
|
+
* apply them to the given instance (API context, port-compatibility checker,
|
|
61
|
+
* categories). Shared by `mountFlowDropApp` and `mountWorkflowEditor`.
|
|
62
|
+
*
|
|
63
|
+
* @returns the resolved {@link EndpointConfig} (merged with defaults), which the
|
|
64
|
+
* callers forward to their mounted component.
|
|
65
|
+
*/
|
|
66
|
+
async function configureInstance(fd, options) {
|
|
67
|
+
const { endpointConfig, portConfig, categories, authProvider } = options;
|
|
68
|
+
// Create endpoint configuration, merging with defaults so all required
|
|
69
|
+
// endpoints are present.
|
|
70
|
+
const { defaultEndpointConfig } = await import('./config/endpoints.js');
|
|
71
|
+
const config = endpointConfig
|
|
72
|
+
? {
|
|
73
|
+
...defaultEndpointConfig,
|
|
74
|
+
...endpointConfig,
|
|
75
|
+
endpoints: {
|
|
76
|
+
...defaultEndpointConfig.endpoints,
|
|
77
|
+
...endpointConfig.endpoints
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
: defaultEndpointConfig;
|
|
81
|
+
// Initialize port configuration (fetch from API when not supplied).
|
|
82
|
+
let finalPortConfig = portConfig;
|
|
83
|
+
if (!finalPortConfig) {
|
|
84
|
+
try {
|
|
85
|
+
finalPortConfig = await fetchPortConfig(config, authProvider);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
logger.warn('Failed to fetch port config from API, using default:', error);
|
|
89
|
+
finalPortConfig = DEFAULT_PORT_CONFIG;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Configure this instance's API context and port compatibility checker.
|
|
93
|
+
fd.api.configure(config, authProvider);
|
|
94
|
+
fd.portCompatibility.reinitialize(finalPortConfig);
|
|
95
|
+
// Initialize this instance's categories (fetch from API when not supplied).
|
|
96
|
+
if (categories) {
|
|
97
|
+
fd.categories.initialize(categories);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
try {
|
|
101
|
+
const fetchedCategories = await fetchCategories(config, authProvider);
|
|
102
|
+
fd.categories.initialize(fetchedCategories);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
logger.warn('Failed to fetch categories from API, using defaults:', error);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return config;
|
|
109
|
+
}
|
|
58
110
|
/**
|
|
59
111
|
* Mount the full FlowDrop App with navbar, sidebars, and workflow editor
|
|
60
112
|
*
|
|
@@ -103,61 +155,14 @@ export async function mountFlowDropApp(container, options = {}) {
|
|
|
103
155
|
await initializeSettings({
|
|
104
156
|
defaults: initialSettings
|
|
105
157
|
});
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
endpoints: {
|
|
115
|
-
...defaultEndpointConfig.endpoints,
|
|
116
|
-
...endpointConfig.endpoints
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
// Use default configuration if none provided
|
|
122
|
-
const { defaultEndpointConfig } = await import('./config/endpoints.js');
|
|
123
|
-
config = defaultEndpointConfig;
|
|
124
|
-
}
|
|
125
|
-
// Initialize port configuration
|
|
126
|
-
let finalPortConfig = portConfig;
|
|
127
|
-
if (!finalPortConfig && config) {
|
|
128
|
-
// Try to fetch port configuration from API
|
|
129
|
-
try {
|
|
130
|
-
finalPortConfig = await fetchPortConfig(config, authProvider);
|
|
131
|
-
}
|
|
132
|
-
catch (error) {
|
|
133
|
-
logger.warn('Failed to fetch port config from API, using default:', error);
|
|
134
|
-
finalPortConfig = DEFAULT_PORT_CONFIG;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
else if (!finalPortConfig) {
|
|
138
|
-
finalPortConfig = DEFAULT_PORT_CONFIG;
|
|
139
|
-
}
|
|
140
|
-
// Configure this instance's API context (endpoints + auth provider) so
|
|
141
|
-
// <App> and services resolve it via getInstance().api.
|
|
142
|
-
if (config) {
|
|
143
|
-
fd.api.configure(config, authProvider);
|
|
144
|
-
}
|
|
145
|
-
// Re-initialize this instance's port compatibility checker with the resolved
|
|
146
|
-
// config (it was seeded with DEFAULT_PORT_CONFIG at construction).
|
|
147
|
-
fd.portCompatibility.reinitialize(finalPortConfig);
|
|
148
|
-
// Initialize this instance's categories
|
|
149
|
-
if (categories) {
|
|
150
|
-
fd.categories.initialize(categories);
|
|
151
|
-
}
|
|
152
|
-
else if (config) {
|
|
153
|
-
try {
|
|
154
|
-
const fetchedCategories = await fetchCategories(config, authProvider);
|
|
155
|
-
fd.categories.initialize(fetchedCategories);
|
|
156
|
-
}
|
|
157
|
-
catch (error) {
|
|
158
|
-
logger.warn('Failed to fetch categories from API, using defaults:', error);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
158
|
+
// Resolve and apply endpoint config, port config and categories to this
|
|
159
|
+
// instance (see configureInstance).
|
|
160
|
+
const config = await configureInstance(fd, {
|
|
161
|
+
endpointConfig,
|
|
162
|
+
portConfig,
|
|
163
|
+
categories,
|
|
164
|
+
authProvider
|
|
165
|
+
});
|
|
161
166
|
// Set up event handler callbacks in this instance's store
|
|
162
167
|
if (eventHandlers?.onDirtyStateChange) {
|
|
163
168
|
fd.workflow.setOnDirtyStateChange(eventHandlers.onDirtyStateChange);
|
|
@@ -338,58 +343,14 @@ export async function mountWorkflowEditor(container, options = {}) {
|
|
|
338
343
|
const { workflow, endpointConfig, portConfig, categories, authProvider, instanceId, builtinEditors } = options;
|
|
339
344
|
// Per-instance state container (see mountFlowDropApp)
|
|
340
345
|
const { fd, isDefault } = acquireInstance(instanceId);
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
endpoints: {
|
|
350
|
-
...defaultEndpointConfig.endpoints,
|
|
351
|
-
...endpointConfig.endpoints
|
|
352
|
-
}
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
else {
|
|
356
|
-
// Use default configuration if none provided
|
|
357
|
-
const { defaultEndpointConfig } = await import('./config/endpoints.js');
|
|
358
|
-
config = defaultEndpointConfig;
|
|
359
|
-
}
|
|
360
|
-
// Initialize port configuration
|
|
361
|
-
let finalPortConfig = portConfig;
|
|
362
|
-
if (!finalPortConfig && config) {
|
|
363
|
-
// Try to fetch port configuration from API
|
|
364
|
-
try {
|
|
365
|
-
finalPortConfig = await fetchPortConfig(config, authProvider);
|
|
366
|
-
}
|
|
367
|
-
catch (error) {
|
|
368
|
-
logger.warn('Failed to fetch port config from API, using default:', error);
|
|
369
|
-
finalPortConfig = DEFAULT_PORT_CONFIG;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
else if (!finalPortConfig) {
|
|
373
|
-
finalPortConfig = DEFAULT_PORT_CONFIG;
|
|
374
|
-
}
|
|
375
|
-
// Configure this instance's API context and port compatibility checker.
|
|
376
|
-
if (config) {
|
|
377
|
-
fd.api.configure(config, authProvider);
|
|
378
|
-
}
|
|
379
|
-
fd.portCompatibility.reinitialize(finalPortConfig);
|
|
380
|
-
// Initialize this instance's categories
|
|
381
|
-
if (categories) {
|
|
382
|
-
fd.categories.initialize(categories);
|
|
383
|
-
}
|
|
384
|
-
else if (config) {
|
|
385
|
-
try {
|
|
386
|
-
const fetchedCategories = await fetchCategories(config, authProvider);
|
|
387
|
-
fd.categories.initialize(fetchedCategories);
|
|
388
|
-
}
|
|
389
|
-
catch (error) {
|
|
390
|
-
logger.warn('Failed to fetch categories from API, using defaults:', error);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
346
|
+
// Resolve and apply endpoint config, port config and categories to this
|
|
347
|
+
// instance (see configureInstance).
|
|
348
|
+
const config = await configureInstance(fd, {
|
|
349
|
+
endpointConfig,
|
|
350
|
+
portConfig,
|
|
351
|
+
categories,
|
|
352
|
+
authProvider
|
|
353
|
+
});
|
|
393
354
|
// Seed the instance's workflow before mounting so the editor renders it
|
|
394
355
|
// immediately. (1.x accepted this option but silently ignored it.)
|
|
395
356
|
if (workflow) {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -327,7 +327,7 @@ export interface AutocompleteConfig {
|
|
|
327
327
|
* method: "GET",
|
|
328
328
|
* headers: { "X-Custom-Header": "value" },
|
|
329
329
|
* parameterMapping: {
|
|
330
|
-
* nodeTypeId: "metadata.
|
|
330
|
+
* nodeTypeId: "metadata.node_type_id",
|
|
331
331
|
* instanceId: "id"
|
|
332
332
|
* }
|
|
333
333
|
* };
|
|
@@ -352,7 +352,7 @@ export interface DynamicSchemaEndpoint {
|
|
|
352
352
|
/**
|
|
353
353
|
* Maps template variables to their source paths.
|
|
354
354
|
* Keys are variable names used in the URL, values are dot-notation paths
|
|
355
|
-
* to resolve from the node context (e.g., "metadata.
|
|
355
|
+
* to resolve from the node context (e.g., "metadata.node_type_id", "config.apiKey", "id")
|
|
356
356
|
*/
|
|
357
357
|
parameterMapping?: Record<string, string>;
|
|
358
358
|
/**
|
|
@@ -381,7 +381,7 @@ export interface DynamicSchemaEndpoint {
|
|
|
381
381
|
* url: "https://admin.example.com/nodes/{nodeTypeId}/edit/{instanceId}",
|
|
382
382
|
* label: "Configure in Admin Panel",
|
|
383
383
|
* parameterMapping: {
|
|
384
|
-
* nodeTypeId: "metadata.
|
|
384
|
+
* nodeTypeId: "metadata.node_type_id",
|
|
385
385
|
* instanceId: "id"
|
|
386
386
|
* },
|
|
387
387
|
* openInNewTab: true
|
|
@@ -408,7 +408,7 @@ export interface ExternalEditLink {
|
|
|
408
408
|
/**
|
|
409
409
|
* Maps template variables to their source paths.
|
|
410
410
|
* Keys are variable names used in the URL, values are dot-notation paths
|
|
411
|
-
* to resolve from the node context (e.g., "metadata.
|
|
411
|
+
* to resolve from the node context (e.g., "metadata.node_type_id", "config.apiKey", "id")
|
|
412
412
|
*/
|
|
413
413
|
parameterMapping?: Record<string, string>;
|
|
414
414
|
/**
|
|
@@ -602,7 +602,8 @@ export interface NodeExtensions {
|
|
|
602
602
|
* Node configuration metadata
|
|
603
603
|
*/
|
|
604
604
|
export interface NodeMetadata {
|
|
605
|
-
id
|
|
605
|
+
/** The node type entity id — the runtime's resolution anchor. */
|
|
606
|
+
node_type_id: string;
|
|
606
607
|
name: string;
|
|
607
608
|
type?: NodeType;
|
|
608
609
|
/**
|
|
@@ -1132,8 +1133,6 @@ export interface WorkflowNode extends Node {
|
|
|
1132
1133
|
isProcessing?: boolean;
|
|
1133
1134
|
/** Error message if the node execution failed */
|
|
1134
1135
|
error?: string;
|
|
1135
|
-
/** Alternative node identifier */
|
|
1136
|
-
nodeId?: string;
|
|
1137
1136
|
/** Node execution tracking information */
|
|
1138
1137
|
executionInfo?: NodeExecutionInfo;
|
|
1139
1138
|
/**
|
|
@@ -119,7 +119,7 @@ export class PortCompatibilityChecker {
|
|
|
119
119
|
* Get all possible connections from a source node to target nodes
|
|
120
120
|
*/
|
|
121
121
|
export function getPossibleConnections(checker, sourceNode, targetNodes, nodeTypes) {
|
|
122
|
-
const sourceMetadata = nodeTypes.find((nt) => nt.
|
|
122
|
+
const sourceMetadata = nodeTypes.find((nt) => nt.node_type_id === sourceNode.data.metadata.node_type_id);
|
|
123
123
|
if (!sourceMetadata)
|
|
124
124
|
return [];
|
|
125
125
|
const possibleConnections = [];
|
|
@@ -129,7 +129,7 @@ export function getPossibleConnections(checker, sourceNode, targetNodes, nodeTyp
|
|
|
129
129
|
for (const targetNode of targetNodes) {
|
|
130
130
|
if (targetNode.id === sourceNode.id)
|
|
131
131
|
continue; // Skip self-connection
|
|
132
|
-
const targetMetadata = nodeTypes.find((nt) => nt.
|
|
132
|
+
const targetMetadata = nodeTypes.find((nt) => nt.node_type_id === targetNode.data.metadata.node_type_id);
|
|
133
133
|
if (!targetMetadata)
|
|
134
134
|
continue;
|
|
135
135
|
// Get all input ports from target node
|
|
@@ -170,8 +170,8 @@ export function validateConnection(checker, sourceNodeId, sourcePortId, targetNo
|
|
|
170
170
|
return { valid: false, error: 'Cannot connect node to itself' };
|
|
171
171
|
}
|
|
172
172
|
// Get node metadata
|
|
173
|
-
const sourceMetadata = nodeTypes.find((nt) => nt.
|
|
174
|
-
const targetMetadata = nodeTypes.find((nt) => nt.
|
|
173
|
+
const sourceMetadata = nodeTypes.find((nt) => nt.node_type_id === sourceNode.data.metadata.node_type_id);
|
|
174
|
+
const targetMetadata = nodeTypes.find((nt) => nt.node_type_id === targetNode.data.metadata.node_type_id);
|
|
175
175
|
if (!sourceMetadata || !targetMetadata) {
|
|
176
176
|
return { valid: false, error: 'Node metadata not found' };
|
|
177
177
|
}
|
|
@@ -200,14 +200,14 @@ export function getConnectionSuggestions(checker, nodeId, nodes, nodeTypes) {
|
|
|
200
200
|
const node = nodes.find((n) => n.id === nodeId);
|
|
201
201
|
if (!node)
|
|
202
202
|
return [];
|
|
203
|
-
const metadata = nodeTypes.find((nt) => nt.
|
|
203
|
+
const metadata = nodeTypes.find((nt) => nt.node_type_id === node.data.metadata.node_type_id);
|
|
204
204
|
if (!metadata)
|
|
205
205
|
return [];
|
|
206
206
|
const suggestions = [];
|
|
207
207
|
// Get all other nodes
|
|
208
208
|
const otherNodes = nodes.filter((n) => n.id !== nodeId);
|
|
209
209
|
for (const otherNode of otherNodes) {
|
|
210
|
-
const otherMetadata = nodeTypes.find((nt) => nt.
|
|
210
|
+
const otherMetadata = nodeTypes.find((nt) => nt.node_type_id === otherNode.data.metadata.node_type_id);
|
|
211
211
|
if (!otherMetadata)
|
|
212
212
|
continue;
|
|
213
213
|
// Check outputs from other nodes to inputs of current node
|
|
@@ -253,8 +253,17 @@ export function getConnectionSuggestions(checker, nodeId, nodes, nodeTypes) {
|
|
|
253
253
|
* @returns True if any cycle exists in the workflow
|
|
254
254
|
*/
|
|
255
255
|
export function hasCycles(nodes, edges) {
|
|
256
|
-
|
|
257
|
-
|
|
256
|
+
return detectCycles(nodes, edges);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Detect whether a directed graph contains a cycle, using DFS over a pre-built
|
|
260
|
+
* adjacency map (O(E) build, O(1) per neighbour lookup — avoids the O(V*E) of
|
|
261
|
+
* re-scanning every edge on each recursive call).
|
|
262
|
+
*
|
|
263
|
+
* Shared by `hasCycles` (all edges) and `hasInvalidCycles` (loopback edges
|
|
264
|
+
* pre-filtered out by the caller).
|
|
265
|
+
*/
|
|
266
|
+
function detectCycles(nodes, edges) {
|
|
258
267
|
const adjacencyMap = new Map();
|
|
259
268
|
for (const node of nodes) {
|
|
260
269
|
adjacencyMap.set(node.id, []);
|
|
@@ -274,7 +283,6 @@ export function hasCycles(nodes, edges) {
|
|
|
274
283
|
return false;
|
|
275
284
|
visited.add(nodeId);
|
|
276
285
|
recursionStack.add(nodeId);
|
|
277
|
-
// Use pre-built adjacency map instead of filtering all edges each call
|
|
278
286
|
const neighbors = adjacencyMap.get(nodeId) || [];
|
|
279
287
|
for (const target of neighbors) {
|
|
280
288
|
if (hasCycleUtil(target))
|
|
@@ -283,7 +291,6 @@ export function hasCycles(nodes, edges) {
|
|
|
283
291
|
recursionStack.delete(nodeId);
|
|
284
292
|
return false;
|
|
285
293
|
}
|
|
286
|
-
// Check each node
|
|
287
294
|
for (const node of nodes) {
|
|
288
295
|
if (!visited.has(node.id)) {
|
|
289
296
|
if (hasCycleUtil(node.id))
|
|
@@ -309,53 +316,10 @@ export function hasCycles(nodes, edges) {
|
|
|
309
316
|
* ```
|
|
310
317
|
*/
|
|
311
318
|
export function hasInvalidCycles(nodes, edges) {
|
|
312
|
-
// Filter out loopback edges - these create valid cycles for loop iteration
|
|
319
|
+
// Filter out loopback edges - these create valid cycles for loop iteration,
|
|
320
|
+
// then reuse the shared detector over the remaining edges.
|
|
313
321
|
const nonLoopbackEdges = edges.filter((edge) => !isLoopbackEdge(edge));
|
|
314
|
-
|
|
315
|
-
// loop is O(1) per lookup instead of scanning all edges on every recursive
|
|
316
|
-
// call (which was O(V*E)).
|
|
317
|
-
const adjacencyMap = new Map();
|
|
318
|
-
for (const node of nodes) {
|
|
319
|
-
adjacencyMap.set(node.id, []);
|
|
320
|
-
}
|
|
321
|
-
for (const edge of nonLoopbackEdges) {
|
|
322
|
-
const neighbors = adjacencyMap.get(edge.source);
|
|
323
|
-
if (neighbors) {
|
|
324
|
-
neighbors.push(edge.target);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
// Check for cycles using only non-loopback edges
|
|
328
|
-
const visited = new Set();
|
|
329
|
-
const recursionStack = new Set();
|
|
330
|
-
/**
|
|
331
|
-
* DFS utility to detect cycles in the graph
|
|
332
|
-
* @param nodeId - Current node being visited
|
|
333
|
-
* @returns True if a cycle is found from this node
|
|
334
|
-
*/
|
|
335
|
-
function hasCycleUtil(nodeId) {
|
|
336
|
-
if (recursionStack.has(nodeId))
|
|
337
|
-
return true;
|
|
338
|
-
if (visited.has(nodeId))
|
|
339
|
-
return false;
|
|
340
|
-
visited.add(nodeId);
|
|
341
|
-
recursionStack.add(nodeId);
|
|
342
|
-
// Use pre-built adjacency map instead of filtering all edges each call
|
|
343
|
-
const neighbors = adjacencyMap.get(nodeId) || [];
|
|
344
|
-
for (const target of neighbors) {
|
|
345
|
-
if (hasCycleUtil(target))
|
|
346
|
-
return true;
|
|
347
|
-
}
|
|
348
|
-
recursionStack.delete(nodeId);
|
|
349
|
-
return false;
|
|
350
|
-
}
|
|
351
|
-
// Check each node for cycles
|
|
352
|
-
for (const node of nodes) {
|
|
353
|
-
if (!visited.has(node.id)) {
|
|
354
|
-
if (hasCycleUtil(node.id))
|
|
355
|
-
return true;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
return false;
|
|
322
|
+
return detectCycles(nodes, nonLoopbackEdges);
|
|
359
323
|
}
|
|
360
324
|
/**
|
|
361
325
|
* Get the execution order for a workflow (topological sort)
|
package/dist/utils/nodeIds.d.ts
CHANGED
package/dist/utils/nodeIds.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
export function generateNodeId(nodeTypeId, existingNodes) {
|
|
12
12
|
// Count how many nodes of this type already exist
|
|
13
13
|
const existingNodeIds = existingNodes
|
|
14
|
-
.filter((node) => node.data?.metadata?.
|
|
14
|
+
.filter((node) => node.data?.metadata?.node_type_id === nodeTypeId)
|
|
15
15
|
.map((node) => node.id);
|
|
16
16
|
// Extract the numbers from existing IDs with the same prefix
|
|
17
17
|
const existingNumbers = existingNodeIds
|
package/dist/utils/nodeSwap.js
CHANGED
|
@@ -176,9 +176,6 @@ export function executeSwap(oldNode, newMetadata, preview, allNodes, allEdges) {
|
|
|
176
176
|
label: newMetadata.name,
|
|
177
177
|
config: mappedConfig,
|
|
178
178
|
metadata: newMetadata,
|
|
179
|
-
// Node components derive their handle IDs from data.nodeId — without it
|
|
180
|
-
// every edge anchored to this node silently disappears from the canvas.
|
|
181
|
-
nodeId: newNodeId,
|
|
182
179
|
extensions
|
|
183
180
|
}
|
|
184
181
|
};
|
|
@@ -218,7 +215,7 @@ export function executeSwap(oldNode, newMetadata, preview, allNodes, allEdges) {
|
|
|
218
215
|
* @returns The newer NodeMetadata if an upgrade is available, null otherwise
|
|
219
216
|
*/
|
|
220
217
|
export function getVersionUpgrade(currentMetadata, allNodeTypes) {
|
|
221
|
-
const available = allNodeTypes.find((n) => n.
|
|
218
|
+
const available = allNodeTypes.find((n) => n.node_type_id === currentMetadata.node_type_id);
|
|
222
219
|
if (!available)
|
|
223
220
|
return null;
|
|
224
221
|
if (compareSemver(available.version, currentMetadata.version) > 0) {
|
|
@@ -253,7 +250,7 @@ function classifyMatch(oldPort, matchedPort) {
|
|
|
253
250
|
export function computeSwapPreviewWithOptions(oldNode, newMetadata, edges, allNodes, options) {
|
|
254
251
|
const checker = options.checker ?? null;
|
|
255
252
|
const oldNodeId = oldNode.id;
|
|
256
|
-
const newNodeId = generateNodeId(newMetadata.
|
|
253
|
+
const newNodeId = generateNodeId(newMetadata.node_type_id, allNodes);
|
|
257
254
|
// Collect connected edges
|
|
258
255
|
const connectedEdges = edges.filter((e) => e.source === oldNodeId || e.target === oldNodeId);
|
|
259
256
|
// Try strategy-based port mapping
|
|
@@ -420,7 +417,7 @@ export function computeSwapPreviewWithOptions(oldNode, newMetadata, edges, allNo
|
|
|
420
417
|
*/
|
|
421
418
|
export function computeInteractiveState(oldNode, newMetadata, edges, allNodes, options = {}) {
|
|
422
419
|
const oldNodeId = oldNode.id;
|
|
423
|
-
const newNodeId = generateNodeId(newMetadata.
|
|
420
|
+
const newNodeId = generateNodeId(newMetadata.node_type_id, allNodes);
|
|
424
421
|
const connectedEdges = edges.filter((e) => e.source === oldNodeId || e.target === oldNodeId);
|
|
425
422
|
// Compute the base preview to get auto-matched ports
|
|
426
423
|
const preview = computeSwapPreviewWithOptions(oldNode, newMetadata, edges, allNodes, options);
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "A drop-in visual workflow editor for any web application. You own the backend. You own the data. You own the orchestration.",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"private": false,
|
|
6
|
-
"version": "2.0.0-beta.
|
|
6
|
+
"version": "2.0.0-beta.5",
|
|
7
7
|
"author": "Shibin Das (D34dMan)",
|
|
8
8
|
"bugs": {
|
|
9
9
|
"url": "https://github.com/flowdrop-io/flowdrop/issues"
|