@d34dman/flowdrop 0.0.56 → 0.0.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/adapters/WorkflowAdapter.d.ts +2 -1
- package/dist/adapters/agentspec/AgentSpecAdapter.d.ts +96 -0
- package/dist/adapters/agentspec/AgentSpecAdapter.js +663 -0
- package/dist/adapters/agentspec/agentAdapter.d.ts +59 -0
- package/dist/adapters/agentspec/agentAdapter.js +91 -0
- package/dist/adapters/agentspec/autoLayout.d.ts +34 -0
- package/dist/adapters/agentspec/autoLayout.js +127 -0
- package/dist/adapters/agentspec/componentTypeDefaults.d.ts +73 -0
- package/dist/adapters/agentspec/componentTypeDefaults.js +238 -0
- package/dist/adapters/agentspec/defaultNodeTypes.d.ts +53 -0
- package/dist/adapters/agentspec/defaultNodeTypes.js +561 -0
- package/dist/adapters/agentspec/index.d.ts +37 -0
- package/dist/adapters/agentspec/index.js +39 -0
- package/dist/adapters/agentspec/validator.d.ts +34 -0
- package/dist/adapters/agentspec/validator.js +169 -0
- package/dist/components/App.svelte +57 -13
- package/dist/components/ConfigForm.svelte +46 -12
- package/dist/components/ConfigForm.svelte.d.ts +8 -0
- package/dist/components/NodeSidebar.svelte +20 -8
- package/dist/components/NodeSidebar.svelte.d.ts +2 -1
- package/dist/components/SchemaForm.svelte +34 -12
- package/dist/components/SchemaForm.svelte.d.ts +8 -0
- package/dist/components/WorkflowEditor.svelte +14 -13
- package/dist/components/form/FormFieldset.svelte +142 -0
- package/dist/components/form/FormFieldset.svelte.d.ts +11 -0
- package/dist/components/form/FormMarkdownEditor.svelte +546 -422
- package/dist/components/form/FormMarkdownEditor.svelte.d.ts +2 -0
- package/dist/components/form/FormUISchemaRenderer.svelte +136 -0
- package/dist/components/form/FormUISchemaRenderer.svelte.d.ts +32 -0
- package/dist/components/form/index.d.ts +2 -0
- package/dist/components/form/index.js +3 -0
- package/dist/components/form/types.d.ts +1 -1
- package/dist/components/nodes/WorkflowNode.svelte +1 -2
- package/dist/config/agentSpecEndpoints.d.ts +70 -0
- package/dist/config/agentSpecEndpoints.js +65 -0
- package/dist/config/endpoints.d.ts +6 -0
- package/dist/core/index.d.ts +29 -3
- package/dist/core/index.js +31 -1
- package/dist/form/code.js +6 -1
- package/dist/form/fieldRegistry.d.ts +79 -15
- package/dist/form/fieldRegistry.js +104 -49
- package/dist/form/full.d.ts +2 -2
- package/dist/form/full.js +2 -2
- package/dist/form/index.d.ts +5 -3
- package/dist/form/index.js +9 -2
- package/dist/form/markdown.d.ts +3 -3
- package/dist/form/markdown.js +8 -4
- package/dist/helpers/workflowEditorHelper.d.ts +24 -0
- package/dist/helpers/workflowEditorHelper.js +55 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/registry/BaseRegistry.d.ts +92 -0
- package/dist/registry/BaseRegistry.js +124 -0
- package/dist/registry/builtinFormats.d.ts +23 -0
- package/dist/registry/builtinFormats.js +70 -0
- package/dist/registry/builtinNodes.js +4 -0
- package/dist/registry/index.d.ts +2 -1
- package/dist/registry/index.js +2 -0
- package/dist/registry/nodeComponentRegistry.d.ts +26 -57
- package/dist/registry/nodeComponentRegistry.js +29 -82
- package/dist/registry/workflowFormatRegistry.d.ts +122 -0
- package/dist/registry/workflowFormatRegistry.js +96 -0
- package/dist/schema/index.d.ts +23 -0
- package/dist/schema/index.js +23 -0
- package/dist/services/agentSpecExecutionService.d.ts +106 -0
- package/dist/services/agentSpecExecutionService.js +333 -0
- package/dist/stores/portCoordinateStore.js +1 -4
- package/dist/stores/workflowStore.d.ts +3 -0
- package/dist/stores/workflowStore.js +3 -0
- package/dist/svelte-app.d.ts +4 -0
- package/dist/svelte-app.js +9 -1
- package/dist/types/agentspec.d.ts +318 -0
- package/dist/types/agentspec.js +48 -0
- package/dist/types/events.d.ts +28 -1
- package/dist/types/index.d.ts +31 -0
- package/dist/types/index.js +5 -0
- package/dist/types/uischema.d.ts +144 -0
- package/dist/types/uischema.js +51 -0
- package/dist/utils/uischema.d.ts +52 -0
- package/dist/utils/uischema.js +88 -0
- package/package.json +231 -225
- package/schemas/v1/workflow.schema.json +952 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Spec Validator
|
|
3
|
+
*
|
|
4
|
+
* Validates workflows against Agent Spec constraints for export,
|
|
5
|
+
* and validates imported Agent Spec documents for correctness.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Validate a FlowDrop StandardWorkflow for Agent Spec export compatibility.
|
|
9
|
+
*
|
|
10
|
+
* Checks:
|
|
11
|
+
* - Must have exactly 1 start node (terminal/triggers)
|
|
12
|
+
* - Must have at least 1 end node (terminal/outputs)
|
|
13
|
+
* - Gateway nodes must have branches defined
|
|
14
|
+
*/
|
|
15
|
+
export function validateForAgentSpecExport(workflow) {
|
|
16
|
+
const errors = [];
|
|
17
|
+
const warnings = [];
|
|
18
|
+
if (workflow.nodes.length === 0) {
|
|
19
|
+
errors.push('Workflow has no nodes');
|
|
20
|
+
return { valid: false, errors, warnings };
|
|
21
|
+
}
|
|
22
|
+
// Check for start nodes
|
|
23
|
+
const startNodes = workflow.nodes.filter((n) => {
|
|
24
|
+
const ext = n.data.metadata.extensions?.['agentspec:component_type'];
|
|
25
|
+
if (ext === 'start_node')
|
|
26
|
+
return true;
|
|
27
|
+
return n.data.metadata.type === 'terminal' && n.data.metadata.category === 'triggers';
|
|
28
|
+
});
|
|
29
|
+
if (startNodes.length === 0) {
|
|
30
|
+
errors.push('Agent Spec requires exactly one StartNode. No start node found (terminal node with triggers category).');
|
|
31
|
+
}
|
|
32
|
+
else if (startNodes.length > 1) {
|
|
33
|
+
errors.push(`Agent Spec requires exactly one StartNode. Found ${startNodes.length}: ${startNodes.map((n) => n.id).join(', ')}`);
|
|
34
|
+
}
|
|
35
|
+
// Check for end nodes
|
|
36
|
+
const endNodes = workflow.nodes.filter((n) => {
|
|
37
|
+
const ext = n.data.metadata.extensions?.['agentspec:component_type'];
|
|
38
|
+
if (ext === 'end_node')
|
|
39
|
+
return true;
|
|
40
|
+
return n.data.metadata.type === 'terminal' && n.data.metadata.category === 'outputs';
|
|
41
|
+
});
|
|
42
|
+
if (endNodes.length === 0) {
|
|
43
|
+
errors.push('Agent Spec requires at least one EndNode. No end node found (terminal node with outputs category).');
|
|
44
|
+
}
|
|
45
|
+
// Check gateway nodes have branches
|
|
46
|
+
const gatewayNodes = workflow.nodes.filter((n) => {
|
|
47
|
+
const ext = n.data.metadata.extensions?.['agentspec:component_type'];
|
|
48
|
+
return ext === 'branching_node' || n.data.metadata.type === 'gateway';
|
|
49
|
+
});
|
|
50
|
+
for (const gw of gatewayNodes) {
|
|
51
|
+
const branches = gw.data.config?.branches;
|
|
52
|
+
if (!branches || !Array.isArray(branches) || branches.length === 0) {
|
|
53
|
+
warnings.push(`Gateway node "${gw.data.label || gw.id}" has no branches defined. Agent Spec BranchingNode requires at least one branch.`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Check for disconnected nodes
|
|
57
|
+
const connectedNodes = new Set();
|
|
58
|
+
for (const edge of workflow.edges) {
|
|
59
|
+
connectedNodes.add(edge.source);
|
|
60
|
+
connectedNodes.add(edge.target);
|
|
61
|
+
}
|
|
62
|
+
const disconnected = workflow.nodes.filter((n) => !connectedNodes.has(n.id));
|
|
63
|
+
if (disconnected.length > 0) {
|
|
64
|
+
warnings.push(`${disconnected.length} node(s) are not connected to any edges: ${disconnected.map((n) => n.data.label || n.id).join(', ')}`);
|
|
65
|
+
}
|
|
66
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Validate an imported Agent Spec Flow document.
|
|
70
|
+
*
|
|
71
|
+
* Checks:
|
|
72
|
+
* - Has a start_node reference that exists
|
|
73
|
+
* - At least one end_node exists
|
|
74
|
+
* - All edge references point to existing nodes
|
|
75
|
+
* - BranchingNode has branches
|
|
76
|
+
* - Data flow edges reference valid properties
|
|
77
|
+
*/
|
|
78
|
+
export function validateAgentSpecFlow(flow) {
|
|
79
|
+
const errors = [];
|
|
80
|
+
const warnings = [];
|
|
81
|
+
if (!flow.nodes || flow.nodes.length === 0) {
|
|
82
|
+
errors.push('Flow has no nodes');
|
|
83
|
+
return { valid: false, errors, warnings };
|
|
84
|
+
}
|
|
85
|
+
// Build node name set
|
|
86
|
+
const nodeNames = new Set(flow.nodes.map((n) => n.name));
|
|
87
|
+
// Check start_node exists
|
|
88
|
+
if (!flow.start_node) {
|
|
89
|
+
errors.push('Flow is missing start_node reference');
|
|
90
|
+
}
|
|
91
|
+
else if (!nodeNames.has(flow.start_node)) {
|
|
92
|
+
errors.push(`start_node "${flow.start_node}" does not match any node name`);
|
|
93
|
+
}
|
|
94
|
+
// Check start_node is actually a start_node type
|
|
95
|
+
const startNode = flow.nodes.find((n) => n.name === flow.start_node);
|
|
96
|
+
if (startNode && startNode.component_type !== 'start_node') {
|
|
97
|
+
warnings.push(`start_node "${flow.start_node}" has component_type "${startNode.component_type}" instead of "start_node"`);
|
|
98
|
+
}
|
|
99
|
+
// Check for end nodes
|
|
100
|
+
const endNodes = flow.nodes.filter((n) => n.component_type === 'end_node');
|
|
101
|
+
if (endNodes.length === 0) {
|
|
102
|
+
warnings.push('Flow has no EndNode. Consider adding one for clarity.');
|
|
103
|
+
}
|
|
104
|
+
// Check for duplicate node names
|
|
105
|
+
const nameCount = new Map();
|
|
106
|
+
for (const node of flow.nodes) {
|
|
107
|
+
nameCount.set(node.name, (nameCount.get(node.name) || 0) + 1);
|
|
108
|
+
}
|
|
109
|
+
for (const [name, count] of nameCount) {
|
|
110
|
+
if (count > 1) {
|
|
111
|
+
errors.push(`Duplicate node name: "${name}" appears ${count} times`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Validate control-flow edges
|
|
115
|
+
for (const edge of flow.control_flow_connections) {
|
|
116
|
+
if (!nodeNames.has(edge.from_node)) {
|
|
117
|
+
errors.push(`Control flow edge "${edge.name}" references non-existent from_node "${edge.from_node}"`);
|
|
118
|
+
}
|
|
119
|
+
if (!nodeNames.has(edge.to_node)) {
|
|
120
|
+
errors.push(`Control flow edge "${edge.name}" references non-existent to_node "${edge.to_node}"`);
|
|
121
|
+
}
|
|
122
|
+
// Validate from_branch references
|
|
123
|
+
if (edge.from_branch) {
|
|
124
|
+
const fromNode = flow.nodes.find((n) => n.name === edge.from_node);
|
|
125
|
+
if (fromNode && fromNode.component_type === 'branching_node') {
|
|
126
|
+
const branchingNode = fromNode;
|
|
127
|
+
if (!branchingNode.branches?.some((b) => b.name === edge.from_branch)) {
|
|
128
|
+
errors.push(`Control flow edge "${edge.name}" references branch "${edge.from_branch}" which doesn't exist on node "${edge.from_node}"`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Validate data-flow edges
|
|
134
|
+
if (flow.data_flow_connections) {
|
|
135
|
+
for (const edge of flow.data_flow_connections) {
|
|
136
|
+
if (!nodeNames.has(edge.source_node)) {
|
|
137
|
+
errors.push(`Data flow edge "${edge.name}" references non-existent source_node "${edge.source_node}"`);
|
|
138
|
+
}
|
|
139
|
+
if (!nodeNames.has(edge.destination_node)) {
|
|
140
|
+
errors.push(`Data flow edge "${edge.name}" references non-existent destination_node "${edge.destination_node}"`);
|
|
141
|
+
}
|
|
142
|
+
// Check that output/input properties exist on the nodes
|
|
143
|
+
const sourceNode = flow.nodes.find((n) => n.name === edge.source_node);
|
|
144
|
+
if (sourceNode?.outputs) {
|
|
145
|
+
const hasOutput = sourceNode.outputs.some((o) => o.title === edge.source_output);
|
|
146
|
+
if (!hasOutput) {
|
|
147
|
+
warnings.push(`Data flow edge "${edge.name}": source node "${edge.source_node}" has no output named "${edge.source_output}"`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const destNode = flow.nodes.find((n) => n.name === edge.destination_node);
|
|
151
|
+
if (destNode?.inputs) {
|
|
152
|
+
const hasInput = destNode.inputs.some((i) => i.title === edge.destination_input);
|
|
153
|
+
if (!hasInput) {
|
|
154
|
+
warnings.push(`Data flow edge "${edge.name}": destination node "${edge.destination_node}" has no input named "${edge.destination_input}"`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Validate branching nodes have branches
|
|
160
|
+
for (const node of flow.nodes) {
|
|
161
|
+
if (node.component_type === 'branching_node') {
|
|
162
|
+
const bn = node;
|
|
163
|
+
if (!bn.branches || bn.branches.length === 0) {
|
|
164
|
+
warnings.push(`BranchingNode "${node.name}" has no branches defined`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
169
|
+
}
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
ConfigSchema,
|
|
23
23
|
NodeUIExtensions
|
|
24
24
|
} from '../types/index.js';
|
|
25
|
+
import { DEFAULT_WORKFLOW_FORMAT } from '../types/index.js';
|
|
25
26
|
import { createEndpointConfig } from '../config/endpoints.js';
|
|
26
27
|
import type { EndpointConfig } from '../config/endpoints.js';
|
|
27
28
|
import type { AuthProvider } from '../types/auth.js';
|
|
@@ -31,6 +32,7 @@
|
|
|
31
32
|
workflowStore,
|
|
32
33
|
workflowActions,
|
|
33
34
|
workflowName,
|
|
35
|
+
workflowFormat,
|
|
34
36
|
markAsSaved
|
|
35
37
|
} from '../stores/workflowStore.js';
|
|
36
38
|
import { apiToasts, dismissToast } from '../services/toastService.js';
|
|
@@ -38,6 +40,7 @@
|
|
|
38
40
|
import { uiSettings } from '../stores/settingsStore.js';
|
|
39
41
|
import { initializePortCompatibility } from '../utils/connections.js';
|
|
40
42
|
import { DEFAULT_PORT_CONFIG } from '../config/defaultPortConfig.js';
|
|
43
|
+
import { workflowFormatRegistry } from '../registry/workflowFormatRegistry.js';
|
|
41
44
|
|
|
42
45
|
/**
|
|
43
46
|
* Configuration props for runtime customization
|
|
@@ -143,9 +146,9 @@
|
|
|
143
146
|
// Workflow settings sidebar state
|
|
144
147
|
let isWorkflowSettingsOpen = $state(false);
|
|
145
148
|
|
|
146
|
-
// Workflow configuration schema
|
|
147
|
-
|
|
148
|
-
type: 'object',
|
|
149
|
+
// Workflow configuration schema (derived to pick up dynamic format options)
|
|
150
|
+
let workflowConfigSchema: ConfigSchema = $derived({
|
|
151
|
+
type: 'object' as const,
|
|
149
152
|
properties: {
|
|
150
153
|
name: {
|
|
151
154
|
type: 'string',
|
|
@@ -159,15 +162,23 @@
|
|
|
159
162
|
description: 'A description of the workflow',
|
|
160
163
|
format: 'multiline',
|
|
161
164
|
default: ''
|
|
165
|
+
},
|
|
166
|
+
format: {
|
|
167
|
+
type: 'string',
|
|
168
|
+
title: 'Workflow Format',
|
|
169
|
+
description: 'The specification format for this workflow',
|
|
170
|
+
oneOf: workflowFormatRegistry.getOneOfOptions(),
|
|
171
|
+
default: 'flowdrop'
|
|
162
172
|
}
|
|
163
173
|
},
|
|
164
174
|
required: ['name']
|
|
165
|
-
};
|
|
175
|
+
});
|
|
166
176
|
|
|
167
177
|
// Workflow configuration values
|
|
168
178
|
let workflowConfigValues = $derived({
|
|
169
179
|
name: $workflowName || '',
|
|
170
|
-
description: $workflowStore?.description || ''
|
|
180
|
+
description: $workflowStore?.description || '',
|
|
181
|
+
format: $workflowStore?.metadata?.format || 'flowdrop'
|
|
171
182
|
});
|
|
172
183
|
|
|
173
184
|
// Get the current node from the workflow store
|
|
@@ -188,7 +199,11 @@
|
|
|
188
199
|
async function fetchNodeTypes(): Promise<void> {
|
|
189
200
|
// If nodes were provided as props, use them directly (skip API fetch)
|
|
190
201
|
if (propNodes && propNodes.length > 0) {
|
|
191
|
-
nodes
|
|
202
|
+
// Merge format-provided nodes with prop nodes (deduplicate by ID, props take priority)
|
|
203
|
+
const formatNodes = workflowFormatRegistry.getAllFormatNodes();
|
|
204
|
+
const existingIds = new Set(propNodes.map((n) => n.id));
|
|
205
|
+
const uniqueFormatNodes = formatNodes.filter((n) => !existingIds.has(n.id));
|
|
206
|
+
nodes = [...propNodes, ...uniqueFormatNodes];
|
|
192
207
|
return;
|
|
193
208
|
}
|
|
194
209
|
|
|
@@ -205,7 +220,11 @@
|
|
|
205
220
|
fetchedNodes = await api.nodes.getNodes();
|
|
206
221
|
}
|
|
207
222
|
|
|
208
|
-
nodes
|
|
223
|
+
// Merge format-provided nodes with API nodes (deduplicate by ID, API takes priority)
|
|
224
|
+
const formatNodes = workflowFormatRegistry.getAllFormatNodes();
|
|
225
|
+
const existingIds = new Set(fetchedNodes.map((n) => n.id));
|
|
226
|
+
const uniqueFormatNodes = formatNodes.filter((n) => !existingIds.has(n.id));
|
|
227
|
+
nodes = [...fetchedNodes, ...uniqueFormatNodes];
|
|
209
228
|
error = null;
|
|
210
229
|
|
|
211
230
|
// Dismiss loading toast
|
|
@@ -430,7 +449,7 @@
|
|
|
430
449
|
workflowId = uuidv4();
|
|
431
450
|
}
|
|
432
451
|
|
|
433
|
-
// Create workflow object for saving
|
|
452
|
+
// Create workflow object for saving (spread existing metadata to preserve format, tags, etc.)
|
|
434
453
|
const finalWorkflow: Workflow = {
|
|
435
454
|
id: workflowId,
|
|
436
455
|
name: workflowToSave.name || 'Untitled Workflow',
|
|
@@ -438,7 +457,9 @@
|
|
|
438
457
|
nodes: workflowToSave.nodes || [],
|
|
439
458
|
edges: workflowToSave.edges || [],
|
|
440
459
|
metadata: {
|
|
441
|
-
|
|
460
|
+
...workflowToSave.metadata,
|
|
461
|
+
version: workflowToSave.metadata?.version || '1.0.0',
|
|
462
|
+
format: workflowToSave.metadata?.format || DEFAULT_WORKFLOW_FORMAT,
|
|
442
463
|
createdAt: workflowToSave.metadata?.createdAt || new Date().toISOString(),
|
|
443
464
|
updatedAt: new Date().toISOString()
|
|
444
465
|
}
|
|
@@ -538,14 +559,16 @@
|
|
|
538
559
|
return;
|
|
539
560
|
}
|
|
540
561
|
|
|
541
|
-
// Create workflow object for export
|
|
562
|
+
// Create workflow object for export (spread existing metadata to preserve format, tags, etc.)
|
|
542
563
|
const finalWorkflow = {
|
|
543
564
|
id: workflowToExport.id || 'untitled-workflow',
|
|
544
565
|
name: workflowToExport.name || 'Untitled Workflow',
|
|
545
566
|
nodes: workflowToExport.nodes || [],
|
|
546
567
|
edges: workflowToExport.edges || [],
|
|
547
568
|
metadata: {
|
|
548
|
-
|
|
569
|
+
...workflowToExport.metadata,
|
|
570
|
+
version: workflowToExport.metadata?.version || '1.0.0',
|
|
571
|
+
format: workflowToExport.metadata?.format || DEFAULT_WORKFLOW_FORMAT,
|
|
549
572
|
createdAt: workflowToExport.metadata?.createdAt || new Date().toISOString(),
|
|
550
573
|
updatedAt: new Date().toISOString()
|
|
551
574
|
}
|
|
@@ -722,7 +745,7 @@
|
|
|
722
745
|
|
|
723
746
|
<!-- Left Sidebar: Node Components -->
|
|
724
747
|
{#snippet leftSidebar()}
|
|
725
|
-
<NodeSidebar {nodes} />
|
|
748
|
+
<NodeSidebar {nodes} activeFormat={$workflowFormat} />
|
|
726
749
|
{/snippet}
|
|
727
750
|
|
|
728
751
|
<!-- Right Sidebar: Configuration or Workflow Settings -->
|
|
@@ -746,9 +769,30 @@
|
|
|
746
769
|
onChange={(config) => {
|
|
747
770
|
// Sync workflow settings changes immediately on field blur
|
|
748
771
|
if ($workflowStore) {
|
|
772
|
+
const newFormat = (config.format as string) || DEFAULT_WORKFLOW_FORMAT;
|
|
773
|
+
const currentFormat = $workflowStore.metadata?.format || DEFAULT_WORKFLOW_FORMAT;
|
|
774
|
+
|
|
775
|
+
// Warn about incompatible nodes when format changes
|
|
776
|
+
if (newFormat !== currentFormat) {
|
|
777
|
+
const incompatibleNodes = $workflowStore.nodes?.filter((node) => {
|
|
778
|
+
const formats = node.data?.metadata?.formats;
|
|
779
|
+
return formats && formats.length > 0 && !formats.includes(newFormat);
|
|
780
|
+
});
|
|
781
|
+
if (incompatibleNodes && incompatibleNodes.length > 0) {
|
|
782
|
+
console.warn(
|
|
783
|
+
`Format changed to '${newFormat}'. ${incompatibleNodes.length} node(s) are not compatible with this format and may not export correctly:`,
|
|
784
|
+
incompatibleNodes.map((n) => n.data?.label || n.type)
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
749
789
|
workflowActions.batchUpdate({
|
|
750
790
|
name: config.name as string,
|
|
751
|
-
description: config.description as string | undefined
|
|
791
|
+
description: config.description as string | undefined,
|
|
792
|
+
metadata: {
|
|
793
|
+
...$workflowStore.metadata,
|
|
794
|
+
format: newFormat
|
|
795
|
+
}
|
|
752
796
|
});
|
|
753
797
|
}
|
|
754
798
|
}}
|
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
ConfigEditOptions,
|
|
29
29
|
AuthProvider
|
|
30
30
|
} from '../types/index.js';
|
|
31
|
+
import type { UISchemaElement } from '../types/uischema.js';
|
|
31
32
|
import { FormField, FormFieldWrapper, FormToggle } from './form/index.js';
|
|
33
|
+
import FormUISchemaRenderer from './form/FormUISchemaRenderer.svelte';
|
|
32
34
|
import type { FieldSchema } from './form/index.js';
|
|
33
35
|
import {
|
|
34
36
|
getEffectiveConfigEditOptions,
|
|
@@ -45,6 +47,13 @@
|
|
|
45
47
|
node?: WorkflowNode;
|
|
46
48
|
/** Direct config schema (used when node is not provided) */
|
|
47
49
|
schema?: ConfigSchema;
|
|
50
|
+
/**
|
|
51
|
+
* Optional UI Schema that controls field layout and grouping.
|
|
52
|
+
* When provided, fields render according to the UISchema tree structure.
|
|
53
|
+
* When absent, falls back to node.data.metadata.uiSchema, then flat rendering.
|
|
54
|
+
* @see https://jsonforms.io/docs/uischema
|
|
55
|
+
*/
|
|
56
|
+
uiSchema?: UISchemaElement;
|
|
48
57
|
/** Direct config values (used when node is not provided) */
|
|
49
58
|
values?: Record<string, unknown>;
|
|
50
59
|
/** Whether to show UI extension settings section */
|
|
@@ -76,6 +85,7 @@
|
|
|
76
85
|
let {
|
|
77
86
|
node,
|
|
78
87
|
schema,
|
|
88
|
+
uiSchema,
|
|
79
89
|
values,
|
|
80
90
|
showUIExtensions = true,
|
|
81
91
|
workflowId,
|
|
@@ -141,6 +151,14 @@
|
|
|
141
151
|
return schema ?? (node?.data.metadata?.configSchema as ConfigSchema | undefined);
|
|
142
152
|
});
|
|
143
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Get the UI schema from direct prop or node metadata
|
|
156
|
+
* Priority: direct uiSchema prop > node metadata uiSchema
|
|
157
|
+
*/
|
|
158
|
+
const configUISchema = $derived.by<UISchemaElement | undefined>(() => {
|
|
159
|
+
return uiSchema ?? (node?.data.metadata?.uiSchema as UISchemaElement | undefined);
|
|
160
|
+
});
|
|
161
|
+
|
|
144
162
|
/**
|
|
145
163
|
* Check if the node needs dynamic schema loading
|
|
146
164
|
* Loads when: no static schema OR preferDynamicSchema is true
|
|
@@ -548,24 +566,40 @@
|
|
|
548
566
|
|
|
549
567
|
{#if configSchema.properties}
|
|
550
568
|
<div class="config-form__fields">
|
|
551
|
-
{#
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
{required}
|
|
560
|
-
animationIndex={index}
|
|
569
|
+
{#if configUISchema}
|
|
570
|
+
<FormUISchemaRenderer
|
|
571
|
+
element={configUISchema}
|
|
572
|
+
schema={configSchema}
|
|
573
|
+
values={configValues}
|
|
574
|
+
requiredFields={configSchema.required ?? []}
|
|
575
|
+
onFieldChange={handleFieldChange}
|
|
576
|
+
{toFieldSchema}
|
|
561
577
|
{node}
|
|
562
578
|
nodes={workflowNodes}
|
|
563
579
|
edges={workflowEdges}
|
|
564
580
|
{workflowId}
|
|
565
581
|
{authProvider}
|
|
566
|
-
onChange={(val) => handleFieldChange(key, val)}
|
|
567
582
|
/>
|
|
568
|
-
{
|
|
583
|
+
{:else}
|
|
584
|
+
{#each Object.entries(configSchema.properties) as [key, field], index (key)}
|
|
585
|
+
{@const fieldSchema = toFieldSchema(field as Record<string, unknown>)}
|
|
586
|
+
{@const required = isFieldRequired(key)}
|
|
587
|
+
|
|
588
|
+
<FormField
|
|
589
|
+
fieldKey={key}
|
|
590
|
+
schema={fieldSchema}
|
|
591
|
+
value={configValues[key]}
|
|
592
|
+
{required}
|
|
593
|
+
animationIndex={index}
|
|
594
|
+
{node}
|
|
595
|
+
nodes={workflowNodes}
|
|
596
|
+
edges={workflowEdges}
|
|
597
|
+
{workflowId}
|
|
598
|
+
{authProvider}
|
|
599
|
+
onChange={(val) => handleFieldChange(key, val)}
|
|
600
|
+
/>
|
|
601
|
+
{/each}
|
|
602
|
+
{/if}
|
|
569
603
|
</div>
|
|
570
604
|
{:else}
|
|
571
605
|
<!-- If no properties, show the raw schema for debugging -->
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import type { ConfigSchema, WorkflowNode, WorkflowEdge, NodeUIExtensions, AuthProvider } from '../types/index.js';
|
|
2
|
+
import type { UISchemaElement } from '../types/uischema.js';
|
|
2
3
|
interface Props {
|
|
3
4
|
/** Optional workflow node (if provided, schema and values are derived from it) */
|
|
4
5
|
node?: WorkflowNode;
|
|
5
6
|
/** Direct config schema (used when node is not provided) */
|
|
6
7
|
schema?: ConfigSchema;
|
|
8
|
+
/**
|
|
9
|
+
* Optional UI Schema that controls field layout and grouping.
|
|
10
|
+
* When provided, fields render according to the UISchema tree structure.
|
|
11
|
+
* When absent, falls back to node.data.metadata.uiSchema, then flat rendering.
|
|
12
|
+
* @see https://jsonforms.io/docs/uischema
|
|
13
|
+
*/
|
|
14
|
+
uiSchema?: UISchemaElement;
|
|
7
15
|
/** Direct config values (used when node is not provided) */
|
|
8
16
|
values?: Record<string, unknown>;
|
|
9
17
|
/** Whether to show UI extension settings section */
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
-->
|
|
6
6
|
|
|
7
7
|
<script lang="ts">
|
|
8
|
-
import type { NodeMetadata, NodeCategory } from '../types/index.js';
|
|
8
|
+
import type { NodeMetadata, NodeCategory, WorkflowFormat } from '../types/index.js';
|
|
9
9
|
import LoadingSpinner from './LoadingSpinner.svelte';
|
|
10
10
|
import Icon from '@iconify/svelte';
|
|
11
11
|
import { getNodeIcon, getCategoryIcon } from '../utils/icons.js';
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
interface Props {
|
|
18
18
|
nodes: NodeMetadata[];
|
|
19
19
|
selectedCategory?: NodeCategory;
|
|
20
|
+
activeFormat?: WorkflowFormat;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
let props: Props = $props();
|
|
@@ -31,6 +32,19 @@
|
|
|
31
32
|
updateSettings({ ui: { sidebarCollapsed: !$uiSettings.sidebarCollapsed } });
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Check if a node is compatible with the active workflow format.
|
|
37
|
+
* Nodes without formats are universal (compatible with all formats).
|
|
38
|
+
*/
|
|
39
|
+
function isNodeCompatibleWithFormat(node: NodeMetadata): boolean {
|
|
40
|
+
if (!props.activeFormat) return true;
|
|
41
|
+
if (!node.formats || node.formats.length === 0) return true;
|
|
42
|
+
return node.formats.includes(props.activeFormat);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Nodes filtered by format compatibility */
|
|
46
|
+
let formatCompatibleNodes = $derived((props.nodes || []).filter(isNodeCompatibleWithFormat));
|
|
47
|
+
|
|
34
48
|
let filteredNodes = $derived(getFilteredNodes());
|
|
35
49
|
let categories = $derived(getCategories());
|
|
36
50
|
|
|
@@ -39,12 +53,11 @@
|
|
|
39
53
|
* Categories appear in the order their first node appears in the API response
|
|
40
54
|
*/
|
|
41
55
|
function getCategories(): NodeCategory[] {
|
|
42
|
-
|
|
43
|
-
if (nodes.length === 0) return [];
|
|
56
|
+
if (formatCompatibleNodes.length === 0) return [];
|
|
44
57
|
// Use a Set to track uniqueness while preserving insertion order
|
|
45
58
|
const seen = new SvelteSet<NodeCategory>();
|
|
46
59
|
const orderedCategories: NodeCategory[] = [];
|
|
47
|
-
for (const node of
|
|
60
|
+
for (const node of formatCompatibleNodes) {
|
|
48
61
|
if (!seen.has(node.category)) {
|
|
49
62
|
seen.add(node.category);
|
|
50
63
|
orderedCategories.push(node.category);
|
|
@@ -58,8 +71,8 @@
|
|
|
58
71
|
* Preserves the API order - no client-side sorting applied
|
|
59
72
|
*/
|
|
60
73
|
function getFilteredNodes(): NodeMetadata[] {
|
|
61
|
-
//
|
|
62
|
-
let filtered =
|
|
74
|
+
// Start with format-compatible nodes
|
|
75
|
+
let filtered = formatCompatibleNodes;
|
|
63
76
|
|
|
64
77
|
// Filter by category
|
|
65
78
|
if (selectedCategory !== 'all') {
|
|
@@ -155,8 +168,7 @@
|
|
|
155
168
|
* Preserves the API order - no client-side sorting applied
|
|
156
169
|
*/
|
|
157
170
|
function getNodesForCategory(category: NodeCategory): NodeMetadata[] {
|
|
158
|
-
|
|
159
|
-
return nodes.filter((node) => node.category === category);
|
|
171
|
+
return formatCompatibleNodes.filter((node) => node.category === category);
|
|
160
172
|
}
|
|
161
173
|
|
|
162
174
|
/**
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { NodeMetadata, NodeCategory } from '../types/index.js';
|
|
1
|
+
import type { NodeMetadata, NodeCategory, WorkflowFormat } from '../types/index.js';
|
|
2
2
|
interface Props {
|
|
3
3
|
nodes: NodeMetadata[];
|
|
4
4
|
selectedCategory?: NodeCategory;
|
|
5
|
+
activeFormat?: WorkflowFormat;
|
|
5
6
|
}
|
|
6
7
|
declare const NodeSidebar: import("svelte").Component<Props, {}, "">;
|
|
7
8
|
type NodeSidebar = ReturnType<typeof NodeSidebar>;
|
|
@@ -53,7 +53,9 @@
|
|
|
53
53
|
import { setContext } from 'svelte';
|
|
54
54
|
import Icon from '@iconify/svelte';
|
|
55
55
|
import type { ConfigSchema, AuthProvider } from '../types/index.js';
|
|
56
|
+
import type { UISchemaElement } from '../types/uischema.js';
|
|
56
57
|
import { FormField } from './form/index.js';
|
|
58
|
+
import FormUISchemaRenderer from './form/FormUISchemaRenderer.svelte';
|
|
57
59
|
import type { FieldSchema } from './form/index.js';
|
|
58
60
|
|
|
59
61
|
/**
|
|
@@ -66,6 +68,14 @@
|
|
|
66
68
|
*/
|
|
67
69
|
schema: ConfigSchema;
|
|
68
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Optional UI Schema that controls field layout and grouping.
|
|
73
|
+
* When provided, fields render according to the UISchema tree structure.
|
|
74
|
+
* When absent, fields render in flat order from schema.properties.
|
|
75
|
+
* @see https://jsonforms.io/docs/uischema
|
|
76
|
+
*/
|
|
77
|
+
uiSchema?: UISchemaElement;
|
|
78
|
+
|
|
69
79
|
/**
|
|
70
80
|
* Current form values as key-value pairs.
|
|
71
81
|
* Keys should correspond to properties defined in the schema.
|
|
@@ -142,6 +152,7 @@
|
|
|
142
152
|
|
|
143
153
|
let {
|
|
144
154
|
schema,
|
|
155
|
+
uiSchema,
|
|
145
156
|
values = {},
|
|
146
157
|
onChange,
|
|
147
158
|
showActions = false,
|
|
@@ -297,19 +308,30 @@
|
|
|
297
308
|
}}
|
|
298
309
|
>
|
|
299
310
|
<div class="schema-form__fields">
|
|
300
|
-
{#
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
{required}
|
|
309
|
-
animationIndex={index}
|
|
310
|
-
onChange={(val) => handleFieldChange(key, val)}
|
|
311
|
+
{#if uiSchema}
|
|
312
|
+
<FormUISchemaRenderer
|
|
313
|
+
element={uiSchema}
|
|
314
|
+
{schema}
|
|
315
|
+
values={formValues}
|
|
316
|
+
requiredFields={schema.required ?? []}
|
|
317
|
+
onFieldChange={handleFieldChange}
|
|
318
|
+
{toFieldSchema}
|
|
311
319
|
/>
|
|
312
|
-
{
|
|
320
|
+
{:else}
|
|
321
|
+
{#each Object.entries(schema.properties) as [key, field], index (key)}
|
|
322
|
+
{@const fieldSchema = toFieldSchema(field as Record<string, unknown>)}
|
|
323
|
+
{@const required = isFieldRequired(key)}
|
|
324
|
+
|
|
325
|
+
<FormField
|
|
326
|
+
fieldKey={key}
|
|
327
|
+
schema={fieldSchema}
|
|
328
|
+
value={formValues[key]}
|
|
329
|
+
{required}
|
|
330
|
+
animationIndex={index}
|
|
331
|
+
onChange={(val) => handleFieldChange(key, val)}
|
|
332
|
+
/>
|
|
333
|
+
{/each}
|
|
334
|
+
{/if}
|
|
313
335
|
</div>
|
|
314
336
|
|
|
315
337
|
{#if showActions}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ConfigSchema, AuthProvider } from '../types/index.js';
|
|
2
|
+
import type { UISchemaElement } from '../types/uischema.js';
|
|
2
3
|
/**
|
|
3
4
|
* Props interface for SchemaForm component
|
|
4
5
|
*/
|
|
@@ -8,6 +9,13 @@ interface Props {
|
|
|
8
9
|
* Should follow JSON Schema draft-07 format with type: "object".
|
|
9
10
|
*/
|
|
10
11
|
schema: ConfigSchema;
|
|
12
|
+
/**
|
|
13
|
+
* Optional UI Schema that controls field layout and grouping.
|
|
14
|
+
* When provided, fields render according to the UISchema tree structure.
|
|
15
|
+
* When absent, fields render in flat order from schema.properties.
|
|
16
|
+
* @see https://jsonforms.io/docs/uischema
|
|
17
|
+
*/
|
|
18
|
+
uiSchema?: UISchemaElement;
|
|
11
19
|
/**
|
|
12
20
|
* Current form values as key-value pairs.
|
|
13
21
|
* Keys should correspond to properties defined in the schema.
|
|
@@ -387,19 +387,20 @@
|
|
|
387
387
|
|
|
388
388
|
// Find the best compatible edge using port-to-port distance
|
|
389
389
|
const portCoordinates = getPortCoordinateSnapshot();
|
|
390
|
-
const candidates =
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
390
|
+
const candidates =
|
|
391
|
+
portCoordinates.size > 0
|
|
392
|
+
? ProximityConnectHelper.findCompatibleEdgesByPortCoordinates(
|
|
393
|
+
targetNode.id,
|
|
394
|
+
portCoordinates,
|
|
395
|
+
baseEdges,
|
|
396
|
+
$editorSettings.proximityConnectDistance
|
|
397
|
+
)
|
|
398
|
+
: ProximityConnectHelper.findCompatibleEdges(
|
|
399
|
+
targetNode,
|
|
400
|
+
flowNodes,
|
|
401
|
+
baseEdges,
|
|
402
|
+
$editorSettings.proximityConnectDistance
|
|
403
|
+
);
|
|
403
404
|
|
|
404
405
|
// Create preview edges
|
|
405
406
|
const previews = ProximityConnectHelper.createPreviewEdges(candidates);
|