@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.
Files changed (83) hide show
  1. package/README.md +4 -4
  2. package/dist/adapters/WorkflowAdapter.d.ts +2 -1
  3. package/dist/adapters/agentspec/AgentSpecAdapter.d.ts +96 -0
  4. package/dist/adapters/agentspec/AgentSpecAdapter.js +663 -0
  5. package/dist/adapters/agentspec/agentAdapter.d.ts +59 -0
  6. package/dist/adapters/agentspec/agentAdapter.js +91 -0
  7. package/dist/adapters/agentspec/autoLayout.d.ts +34 -0
  8. package/dist/adapters/agentspec/autoLayout.js +127 -0
  9. package/dist/adapters/agentspec/componentTypeDefaults.d.ts +73 -0
  10. package/dist/adapters/agentspec/componentTypeDefaults.js +238 -0
  11. package/dist/adapters/agentspec/defaultNodeTypes.d.ts +53 -0
  12. package/dist/adapters/agentspec/defaultNodeTypes.js +561 -0
  13. package/dist/adapters/agentspec/index.d.ts +37 -0
  14. package/dist/adapters/agentspec/index.js +39 -0
  15. package/dist/adapters/agentspec/validator.d.ts +34 -0
  16. package/dist/adapters/agentspec/validator.js +169 -0
  17. package/dist/components/App.svelte +57 -13
  18. package/dist/components/ConfigForm.svelte +46 -12
  19. package/dist/components/ConfigForm.svelte.d.ts +8 -0
  20. package/dist/components/NodeSidebar.svelte +20 -8
  21. package/dist/components/NodeSidebar.svelte.d.ts +2 -1
  22. package/dist/components/SchemaForm.svelte +34 -12
  23. package/dist/components/SchemaForm.svelte.d.ts +8 -0
  24. package/dist/components/WorkflowEditor.svelte +14 -13
  25. package/dist/components/form/FormFieldset.svelte +142 -0
  26. package/dist/components/form/FormFieldset.svelte.d.ts +11 -0
  27. package/dist/components/form/FormMarkdownEditor.svelte +546 -422
  28. package/dist/components/form/FormMarkdownEditor.svelte.d.ts +2 -0
  29. package/dist/components/form/FormUISchemaRenderer.svelte +136 -0
  30. package/dist/components/form/FormUISchemaRenderer.svelte.d.ts +32 -0
  31. package/dist/components/form/index.d.ts +2 -0
  32. package/dist/components/form/index.js +3 -0
  33. package/dist/components/form/types.d.ts +1 -1
  34. package/dist/components/nodes/WorkflowNode.svelte +1 -2
  35. package/dist/config/agentSpecEndpoints.d.ts +70 -0
  36. package/dist/config/agentSpecEndpoints.js +65 -0
  37. package/dist/config/endpoints.d.ts +6 -0
  38. package/dist/core/index.d.ts +29 -3
  39. package/dist/core/index.js +31 -1
  40. package/dist/form/code.js +6 -1
  41. package/dist/form/fieldRegistry.d.ts +79 -15
  42. package/dist/form/fieldRegistry.js +104 -49
  43. package/dist/form/full.d.ts +2 -2
  44. package/dist/form/full.js +2 -2
  45. package/dist/form/index.d.ts +5 -3
  46. package/dist/form/index.js +9 -2
  47. package/dist/form/markdown.d.ts +3 -3
  48. package/dist/form/markdown.js +8 -4
  49. package/dist/helpers/workflowEditorHelper.d.ts +24 -0
  50. package/dist/helpers/workflowEditorHelper.js +55 -0
  51. package/dist/index.d.ts +2 -2
  52. package/dist/index.js +2 -2
  53. package/dist/registry/BaseRegistry.d.ts +92 -0
  54. package/dist/registry/BaseRegistry.js +124 -0
  55. package/dist/registry/builtinFormats.d.ts +23 -0
  56. package/dist/registry/builtinFormats.js +70 -0
  57. package/dist/registry/builtinNodes.js +4 -0
  58. package/dist/registry/index.d.ts +2 -1
  59. package/dist/registry/index.js +2 -0
  60. package/dist/registry/nodeComponentRegistry.d.ts +26 -57
  61. package/dist/registry/nodeComponentRegistry.js +29 -82
  62. package/dist/registry/workflowFormatRegistry.d.ts +122 -0
  63. package/dist/registry/workflowFormatRegistry.js +96 -0
  64. package/dist/schema/index.d.ts +23 -0
  65. package/dist/schema/index.js +23 -0
  66. package/dist/services/agentSpecExecutionService.d.ts +106 -0
  67. package/dist/services/agentSpecExecutionService.js +333 -0
  68. package/dist/stores/portCoordinateStore.js +1 -4
  69. package/dist/stores/workflowStore.d.ts +3 -0
  70. package/dist/stores/workflowStore.js +3 -0
  71. package/dist/svelte-app.d.ts +4 -0
  72. package/dist/svelte-app.js +9 -1
  73. package/dist/types/agentspec.d.ts +318 -0
  74. package/dist/types/agentspec.js +48 -0
  75. package/dist/types/events.d.ts +28 -1
  76. package/dist/types/index.d.ts +31 -0
  77. package/dist/types/index.js +5 -0
  78. package/dist/types/uischema.d.ts +144 -0
  79. package/dist/types/uischema.js +51 -0
  80. package/dist/utils/uischema.d.ts +52 -0
  81. package/dist/utils/uischema.js +88 -0
  82. package/package.json +231 -225
  83. 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
- const workflowConfigSchema: ConfigSchema = {
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 = propNodes;
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 = fetchedNodes;
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
- version: '1.0.0',
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
- version: '1.0.0',
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
- {#each Object.entries(configSchema.properties) as [key, field], index (key)}
552
- {@const fieldSchema = toFieldSchema(field as Record<string, unknown>)}
553
- {@const required = isFieldRequired(key)}
554
-
555
- <FormField
556
- fieldKey={key}
557
- schema={fieldSchema}
558
- value={configValues[key]}
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
- {/each}
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
- const nodes = props.nodes || [];
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 nodes) {
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
- // Use actual node types from props
62
- let filtered = props.nodes || [];
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
- const nodes = props.nodes || [];
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
- {#each Object.entries(schema.properties) as [key, field], index (key)}
301
- {@const fieldSchema = toFieldSchema(field as Record<string, unknown>)}
302
- {@const required = isFieldRequired(key)}
303
-
304
- <FormField
305
- fieldKey={key}
306
- schema={fieldSchema}
307
- value={formValues[key]}
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
- {/each}
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 = portCoordinates.size > 0
391
- ? ProximityConnectHelper.findCompatibleEdgesByPortCoordinates(
392
- targetNode.id,
393
- portCoordinates,
394
- baseEdges,
395
- $editorSettings.proximityConnectDistance
396
- )
397
- : ProximityConnectHelper.findCompatibleEdges(
398
- targetNode,
399
- flowNodes,
400
- baseEdges,
401
- $editorSettings.proximityConnectDistance
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);