@d34dman/flowdrop 0.0.56 → 0.0.57

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 (43) hide show
  1. package/dist/adapters/agentspec/AgentSpecAdapter.d.ts +92 -0
  2. package/dist/adapters/agentspec/AgentSpecAdapter.js +658 -0
  3. package/dist/adapters/agentspec/agentAdapter.d.ts +59 -0
  4. package/dist/adapters/agentspec/agentAdapter.js +91 -0
  5. package/dist/adapters/agentspec/autoLayout.d.ts +34 -0
  6. package/dist/adapters/agentspec/autoLayout.js +127 -0
  7. package/dist/adapters/agentspec/index.d.ts +35 -0
  8. package/dist/adapters/agentspec/index.js +37 -0
  9. package/dist/adapters/agentspec/nodeTypeRegistry.d.ts +62 -0
  10. package/dist/adapters/agentspec/nodeTypeRegistry.js +589 -0
  11. package/dist/adapters/agentspec/validator.d.ts +34 -0
  12. package/dist/adapters/agentspec/validator.js +169 -0
  13. package/dist/components/ConfigForm.svelte +46 -12
  14. package/dist/components/ConfigForm.svelte.d.ts +8 -0
  15. package/dist/components/SchemaForm.svelte +34 -12
  16. package/dist/components/SchemaForm.svelte.d.ts +8 -0
  17. package/dist/components/form/FormFieldset.svelte +142 -0
  18. package/dist/components/form/FormFieldset.svelte.d.ts +11 -0
  19. package/dist/components/form/FormUISchemaRenderer.svelte +140 -0
  20. package/dist/components/form/FormUISchemaRenderer.svelte.d.ts +32 -0
  21. package/dist/components/form/index.d.ts +2 -0
  22. package/dist/components/form/index.js +3 -0
  23. package/dist/config/agentSpecEndpoints.d.ts +70 -0
  24. package/dist/config/agentSpecEndpoints.js +65 -0
  25. package/dist/config/endpoints.d.ts +6 -0
  26. package/dist/core/index.d.ts +17 -1
  27. package/dist/core/index.js +17 -0
  28. package/dist/form/index.d.ts +2 -0
  29. package/dist/form/index.js +3 -0
  30. package/dist/helpers/workflowEditorHelper.d.ts +24 -0
  31. package/dist/helpers/workflowEditorHelper.js +55 -0
  32. package/dist/services/agentSpecExecutionService.d.ts +106 -0
  33. package/dist/services/agentSpecExecutionService.js +333 -0
  34. package/dist/types/agentspec.d.ts +318 -0
  35. package/dist/types/agentspec.js +48 -0
  36. package/dist/types/events.d.ts +28 -1
  37. package/dist/types/index.d.ts +13 -0
  38. package/dist/types/index.js +1 -0
  39. package/dist/types/uischema.d.ts +144 -0
  40. package/dist/types/uischema.js +51 -0
  41. package/dist/utils/uischema.d.ts +52 -0
  42. package/dist/utils/uischema.js +88 -0
  43. package/package.json +1 -1
@@ -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
+ }
@@ -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 */
@@ -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.
@@ -0,0 +1,142 @@
1
+ <!--
2
+ FormFieldset Component
3
+ Renders a UISchema Group element as a collapsible or static fieldset.
4
+
5
+ Two rendering modes:
6
+ - Collapsible (default): Uses HTML5 <details>/<summary> with .flowdrop-details CSS
7
+ - Static (collapsible: false): Uses a styled <fieldset> with <legend>
8
+
9
+ Features:
10
+ - HTML5 <details> for native accessible collapse behavior
11
+ - Reuses existing .flowdrop-details CSS pattern from base.css
12
+ - Optional description text below the group title
13
+ - Chevron rotation animation on open/close
14
+ -->
15
+
16
+ <script lang="ts">
17
+ import type { Snippet } from 'svelte';
18
+ import type { UISchemaGroup } from '../../types/uischema.js';
19
+ import Icon from '@iconify/svelte';
20
+
21
+ interface Props {
22
+ /** The UISchema Group element to render */
23
+ group: UISchemaGroup;
24
+ /** Slot content for the group's child elements */
25
+ children: Snippet;
26
+ }
27
+
28
+ let { group, children }: Props = $props();
29
+
30
+ const isCollapsible = $derived(group.collapsible !== false);
31
+ const isDefaultOpen = $derived(group.defaultOpen !== false);
32
+ </script>
33
+
34
+ {#if isCollapsible}
35
+ <details class="flowdrop-details form-fieldset" open={isDefaultOpen}>
36
+ <summary class="flowdrop-details__summary form-fieldset__summary">
37
+ <div class="form-fieldset__label">
38
+ <Icon icon="heroicons:chevron-right" class="form-fieldset__chevron" />
39
+ <span class="form-fieldset__title">{group.label}</span>
40
+ </div>
41
+ {#if group.description}
42
+ <span class="form-fieldset__badge">{group.description}</span>
43
+ {/if}
44
+ </summary>
45
+ <div class="flowdrop-details__content form-fieldset__content">
46
+ <div class="form-fieldset__fields">
47
+ {@render children()}
48
+ </div>
49
+ </div>
50
+ </details>
51
+ {:else}
52
+ <fieldset class="form-fieldset form-fieldset--static">
53
+ <legend class="form-fieldset__legend">{group.label}</legend>
54
+ {#if group.description}
55
+ <p class="form-fieldset__description">{group.description}</p>
56
+ {/if}
57
+ <div class="form-fieldset__fields">
58
+ {@render children()}
59
+ </div>
60
+ </fieldset>
61
+ {/if}
62
+
63
+ <style>
64
+ /* ============================================
65
+ COLLAPSIBLE FIELDSET
66
+ Extends .flowdrop-details from base.css
67
+ ============================================ */
68
+
69
+ .form-fieldset__summary {
70
+ gap: var(--fd-space-2);
71
+ }
72
+
73
+ .form-fieldset__label {
74
+ display: flex;
75
+ align-items: center;
76
+ gap: var(--fd-space-2);
77
+ }
78
+
79
+ .form-fieldset__title {
80
+ font-size: var(--fd-text-sm);
81
+ font-weight: 600;
82
+ color: var(--fd-foreground);
83
+ }
84
+
85
+ .form-fieldset :global(.form-fieldset__chevron) {
86
+ width: 1rem;
87
+ height: 1rem;
88
+ color: var(--fd-muted-foreground);
89
+ transition: transform var(--fd-transition-fast);
90
+ flex-shrink: 0;
91
+ }
92
+
93
+ /* Rotate chevron when details is open */
94
+ details.form-fieldset[open] :global(.form-fieldset__chevron) {
95
+ transform: rotate(90deg);
96
+ }
97
+
98
+ .form-fieldset__badge {
99
+ font-size: var(--fd-text-xs);
100
+ color: var(--fd-muted-foreground);
101
+ line-height: 1.4;
102
+ white-space: nowrap;
103
+ overflow: hidden;
104
+ text-overflow: ellipsis;
105
+ max-width: 200px;
106
+ }
107
+
108
+ .form-fieldset__content {
109
+ padding-top: var(--fd-space-2);
110
+ }
111
+
112
+ .form-fieldset__fields {
113
+ display: flex;
114
+ flex-direction: column;
115
+ gap: var(--fd-space-5);
116
+ }
117
+
118
+ /* ============================================
119
+ STATIC FIELDSET (non-collapsible)
120
+ ============================================ */
121
+
122
+ .form-fieldset--static {
123
+ border: 1px solid var(--fd-border-muted);
124
+ border-radius: var(--fd-radius-lg);
125
+ padding: var(--fd-space-4);
126
+ margin: 0;
127
+ }
128
+
129
+ .form-fieldset__legend {
130
+ padding: 0 var(--fd-space-2);
131
+ font-size: var(--fd-text-sm);
132
+ font-weight: 600;
133
+ color: var(--fd-foreground);
134
+ }
135
+
136
+ .form-fieldset__description {
137
+ margin: 0 0 var(--fd-space-3) 0;
138
+ font-size: var(--fd-text-xs);
139
+ color: var(--fd-muted-foreground);
140
+ line-height: 1.4;
141
+ }
142
+ </style>
@@ -0,0 +1,11 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { UISchemaGroup } from '../../types/uischema.js';
3
+ interface Props {
4
+ /** The UISchema Group element to render */
5
+ group: UISchemaGroup;
6
+ /** Slot content for the group's child elements */
7
+ children: Snippet;
8
+ }
9
+ declare const FormFieldset: import("svelte").Component<Props, {}, "">;
10
+ type FormFieldset = ReturnType<typeof FormFieldset>;
11
+ export default FormFieldset;
@@ -0,0 +1,140 @@
1
+ <!--
2
+ FormUISchemaRenderer Component
3
+ Recursively renders UISchema elements: VerticalLayout, Group, and Control.
4
+
5
+ This component bridges the UISchema tree and the existing FormField components.
6
+ It resolves Control scopes to property keys and delegates field rendering to FormField.
7
+
8
+ Rendering logic:
9
+ - Control -> resolve scope to key, render FormField
10
+ - VerticalLayout -> vertical flex container with recursive children
11
+ - Group -> FormFieldset wrapping recursive children
12
+ -->
13
+
14
+ <script lang="ts">
15
+ import type { UISchemaElement } from '../../types/uischema.js';
16
+ import type {
17
+ ConfigSchema,
18
+ WorkflowNode,
19
+ WorkflowEdge,
20
+ AuthProvider
21
+ } from '../../types/index.js';
22
+ import type { FieldSchema } from './types.js';
23
+ import { resolveScopeToKey } from '../../utils/uischema.js';
24
+ import FormField from './FormField.svelte';
25
+ import FormFieldset from './FormFieldset.svelte';
26
+
27
+ interface Props {
28
+ /** The UISchema element to render */
29
+ element: UISchemaElement;
30
+ /** The data schema (for resolving field definitions) */
31
+ schema: ConfigSchema;
32
+ /** Current form values */
33
+ values: Record<string, unknown>;
34
+ /** Required field keys from the schema */
35
+ requiredFields?: string[];
36
+ /** Base animation index for staggered animations */
37
+ animationIndexBase?: number;
38
+ /** Callback when a field value changes */
39
+ onFieldChange: (key: string, value: unknown) => void;
40
+ /** Convert a property to FieldSchema (handles template variable injection etc.) */
41
+ toFieldSchema: (property: Record<string, unknown>) => FieldSchema;
42
+ /** Current workflow node (optional, passed through to FormField) */
43
+ node?: WorkflowNode;
44
+ /** All workflow nodes (optional, passed through to FormField) */
45
+ nodes?: WorkflowNode[];
46
+ /** All workflow edges (optional, passed through to FormField) */
47
+ edges?: WorkflowEdge[];
48
+ /** Workflow ID (optional, passed through to FormField) */
49
+ workflowId?: string;
50
+ /** Auth provider (optional, passed through to FormField) */
51
+ authProvider?: AuthProvider;
52
+ }
53
+
54
+ let {
55
+ element,
56
+ schema,
57
+ values,
58
+ requiredFields = [],
59
+ animationIndexBase = 0,
60
+ onFieldChange,
61
+ toFieldSchema,
62
+ node,
63
+ nodes,
64
+ edges,
65
+ workflowId,
66
+ authProvider
67
+ }: Props = $props();
68
+
69
+ function isRequired(key: string): boolean {
70
+ return requiredFields.includes(key);
71
+ }
72
+ </script>
73
+
74
+ {#if element.type === 'Control'}
75
+ {@const key = resolveScopeToKey(element.scope)}
76
+ {#if key && schema.properties[key]}
77
+ {@const fieldSchema = toFieldSchema(schema.properties[key] as Record<string, unknown>)}
78
+ <FormField
79
+ fieldKey={key}
80
+ schema={fieldSchema}
81
+ value={values[key]}
82
+ required={isRequired(key)}
83
+ animationIndex={animationIndexBase}
84
+ {node}
85
+ {nodes}
86
+ {edges}
87
+ {workflowId}
88
+ {authProvider}
89
+ onChange={(val) => onFieldChange(key, val)}
90
+ />
91
+ {/if}
92
+ {:else if element.type === 'VerticalLayout'}
93
+ <div class="form-uischema-layout form-uischema-layout--vertical">
94
+ {#each element.elements as child, idx (idx)}
95
+ <svelte:self
96
+ element={child}
97
+ {schema}
98
+ {values}
99
+ {requiredFields}
100
+ animationIndexBase={animationIndexBase + idx}
101
+ {onFieldChange}
102
+ {toFieldSchema}
103
+ {node}
104
+ {nodes}
105
+ {edges}
106
+ {workflowId}
107
+ {authProvider}
108
+ />
109
+ {/each}
110
+ </div>
111
+ {:else if element.type === 'Group'}
112
+ <FormFieldset group={element}>
113
+ <div class="form-uischema-layout form-uischema-layout--vertical">
114
+ {#each element.elements as child, idx (idx)}
115
+ <svelte:self
116
+ element={child}
117
+ {schema}
118
+ {values}
119
+ {requiredFields}
120
+ animationIndexBase={animationIndexBase + idx}
121
+ {onFieldChange}
122
+ {toFieldSchema}
123
+ {node}
124
+ {nodes}
125
+ {edges}
126
+ {workflowId}
127
+ {authProvider}
128
+ />
129
+ {/each}
130
+ </div>
131
+ </FormFieldset>
132
+ {/if}
133
+
134
+ <style>
135
+ .form-uischema-layout--vertical {
136
+ display: flex;
137
+ flex-direction: column;
138
+ gap: var(--fd-space-5);
139
+ }
140
+ </style>