@d34dman/flowdrop 0.0.55 → 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.
- package/dist/adapters/agentspec/AgentSpecAdapter.d.ts +92 -0
- package/dist/adapters/agentspec/AgentSpecAdapter.js +658 -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/index.d.ts +35 -0
- package/dist/adapters/agentspec/index.js +37 -0
- package/dist/adapters/agentspec/nodeTypeRegistry.d.ts +62 -0
- package/dist/adapters/agentspec/nodeTypeRegistry.js +589 -0
- package/dist/adapters/agentspec/validator.d.ts +34 -0
- package/dist/adapters/agentspec/validator.js +169 -0
- package/dist/components/ConfigForm.svelte +46 -12
- package/dist/components/ConfigForm.svelte.d.ts +8 -0
- package/dist/components/SchemaForm.svelte +34 -12
- package/dist/components/SchemaForm.svelte.d.ts +8 -0
- package/dist/components/form/FormFieldset.svelte +142 -0
- package/dist/components/form/FormFieldset.svelte.d.ts +11 -0
- package/dist/components/form/FormUISchemaRenderer.svelte +140 -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/config/agentSpecEndpoints.d.ts +70 -0
- package/dist/config/agentSpecEndpoints.js +65 -0
- package/dist/config/defaultPortConfig.js +9 -0
- package/dist/config/endpoints.d.ts +6 -0
- package/dist/core/index.d.ts +17 -1
- package/dist/core/index.js +17 -0
- package/dist/form/index.d.ts +2 -0
- package/dist/form/index.js +3 -0
- package/dist/helpers/workflowEditorHelper.d.ts +24 -0
- package/dist/helpers/workflowEditorHelper.js +55 -0
- package/dist/services/agentSpecExecutionService.d.ts +106 -0
- package/dist/services/agentSpecExecutionService.js +333 -0
- 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 +13 -0
- package/dist/types/index.js +1 -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 +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
|
-
{#
|
|
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 */
|
|
@@ -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.
|
|
@@ -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>
|