@flowdrop/flowdrop 1.2.2 → 1.4.0
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/components/App.svelte +80 -5
- package/dist/components/App.svelte.d.ts +7 -0
- package/dist/components/CanvasBanner.stories.svelte +6 -2
- package/dist/components/ConfigForm.svelte +421 -0
- package/dist/components/FlowDropEdge.svelte +7 -48
- package/dist/components/Logo.svelte +14 -14
- package/dist/components/Navbar.svelte +58 -10
- package/dist/components/Navbar.svelte.d.ts +7 -0
- package/dist/components/NodeSidebar.svelte +236 -304
- package/dist/components/WorkflowEditor.svelte +16 -6
- package/dist/components/nodes/GatewayNode.svelte +8 -9
- package/dist/components/nodes/SimpleNode.stories.svelte +64 -0
- package/dist/components/nodes/SimpleNode.svelte +93 -104
- package/dist/components/nodes/SquareNode.stories.svelte +45 -0
- package/dist/components/nodes/SquareNode.svelte +94 -104
- package/dist/components/nodes/TerminalNode.svelte +6 -7
- package/dist/components/nodes/WorkflowNode.stories.svelte +63 -0
- package/dist/components/nodes/WorkflowNode.svelte +54 -19
- package/dist/schemas/v1/workflow.schema.json +22 -107
- package/dist/skins/slate.js +16 -0
- package/dist/stories/EdgeDecorator.svelte +4 -4
- package/dist/styles/base.css +48 -0
- package/dist/svelte-app.d.ts +7 -1
- package/dist/svelte-app.js +4 -1
- package/dist/types/index.d.ts +17 -0
- package/dist/utils/portUtils.d.ts +24 -0
- package/dist/utils/portUtils.js +42 -0
- package/package.json +3 -3
|
@@ -57,12 +57,11 @@
|
|
|
57
57
|
* Get the hideUnconnectedHandles setting from extensions
|
|
58
58
|
* Merges node type defaults with instance overrides
|
|
59
59
|
*/
|
|
60
|
-
const hideUnconnectedHandles = $derived(
|
|
61
|
-
|
|
62
|
-
props.data.metadata?.extensions?.ui?.hideUnconnectedHandles ??
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
});
|
|
60
|
+
const hideUnconnectedHandles = $derived(
|
|
61
|
+
props.data.extensions?.ui?.hideUnconnectedHandles ??
|
|
62
|
+
props.data.metadata?.extensions?.ui?.hideUnconnectedHandles ??
|
|
63
|
+
false,
|
|
64
|
+
);
|
|
66
65
|
|
|
67
66
|
/**
|
|
68
67
|
* Check if a port should be visible based on connection state and settings
|
|
@@ -72,7 +71,7 @@
|
|
|
72
71
|
*/
|
|
73
72
|
function isPortVisible(port: NodePort, type: "input" | "output"): boolean {
|
|
74
73
|
// Always show if hideUnconnectedHandles is disabled
|
|
75
|
-
if (!hideUnconnectedHandles
|
|
74
|
+
if (!hideUnconnectedHandles) {
|
|
76
75
|
return true;
|
|
77
76
|
}
|
|
78
77
|
|
|
@@ -100,7 +99,7 @@
|
|
|
100
99
|
*/
|
|
101
100
|
function isBranchVisible(branchName: string): boolean {
|
|
102
101
|
// Always show if hideUnconnectedHandles is disabled
|
|
103
|
-
if (!hideUnconnectedHandles
|
|
102
|
+
if (!hideUnconnectedHandles) {
|
|
104
103
|
return true;
|
|
105
104
|
}
|
|
106
105
|
|
|
@@ -211,7 +210,7 @@
|
|
|
211
210
|
{#if visibleInputPorts.length > 0}
|
|
212
211
|
<div class="flowdrop-workflow-node__ports">
|
|
213
212
|
<div class="flowdrop-workflow-node__ports-list">
|
|
214
|
-
{#each visibleInputPorts as port
|
|
213
|
+
{#each visibleInputPorts as port (port.id)}
|
|
215
214
|
<div class="flowdrop-workflow-node__port">
|
|
216
215
|
<!-- Input Handle: centered in row, at node edge (ports have no padding) -->
|
|
217
216
|
<Handle
|
|
@@ -158,3 +158,67 @@
|
|
|
158
158
|
})}
|
|
159
159
|
/>
|
|
160
160
|
</Story>
|
|
161
|
+
|
|
162
|
+
<Story name="Dynamic Ports">
|
|
163
|
+
<NodeDecorator
|
|
164
|
+
data={createSampleNodeData({
|
|
165
|
+
label: "Custom Function",
|
|
166
|
+
config: {
|
|
167
|
+
dynamicInputs: [
|
|
168
|
+
{
|
|
169
|
+
name: "param_a",
|
|
170
|
+
label: "Parameter A",
|
|
171
|
+
description: "First parameter",
|
|
172
|
+
dataType: "string",
|
|
173
|
+
required: true,
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: "param_b",
|
|
177
|
+
label: "Parameter B",
|
|
178
|
+
description: "Second parameter",
|
|
179
|
+
dataType: "number",
|
|
180
|
+
required: false,
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
dynamicOutputs: [
|
|
184
|
+
{
|
|
185
|
+
name: "result",
|
|
186
|
+
label: "Result",
|
|
187
|
+
description: "Function result",
|
|
188
|
+
dataType: "json",
|
|
189
|
+
required: false,
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
metadata: {
|
|
194
|
+
id: "custom_function",
|
|
195
|
+
name: "Custom Function",
|
|
196
|
+
description: "A node with dynamic input and output ports",
|
|
197
|
+
category: "processing",
|
|
198
|
+
version: "1.0.0",
|
|
199
|
+
type: "simple",
|
|
200
|
+
supportedTypes: ["simple", "square", "default"],
|
|
201
|
+
icon: "mdi:function-variant",
|
|
202
|
+
color: "#8b5cf6",
|
|
203
|
+
inputs: [
|
|
204
|
+
{
|
|
205
|
+
id: "trigger",
|
|
206
|
+
name: "Trigger",
|
|
207
|
+
type: "input",
|
|
208
|
+
dataType: "trigger",
|
|
209
|
+
required: false,
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
outputs: [
|
|
213
|
+
{
|
|
214
|
+
id: "done",
|
|
215
|
+
name: "Done",
|
|
216
|
+
type: "output",
|
|
217
|
+
dataType: "trigger",
|
|
218
|
+
required: false,
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
},
|
|
222
|
+
})}
|
|
223
|
+
/>
|
|
224
|
+
</Story>
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
Simple Node Component
|
|
3
3
|
A simple node with optional input and output ports
|
|
4
4
|
Styled with BEM syntax
|
|
5
|
-
|
|
5
|
+
|
|
6
6
|
UI Extensions Support:
|
|
7
|
-
- hideUnconnectedHandles: Hides
|
|
7
|
+
- hideUnconnectedHandles: Hides ports that are not connected to reduce visual clutter
|
|
8
|
+
- hiddenPorts: Manually hide individual ports (visual-only, no effect on execution)
|
|
9
|
+
- portOrder: Reorder ports by ID array (unspecified ports appear at end in original order)
|
|
8
10
|
-->
|
|
9
11
|
|
|
10
12
|
<script lang="ts">
|
|
@@ -14,13 +16,20 @@
|
|
|
14
16
|
NodeMetadata,
|
|
15
17
|
NodeExtensions,
|
|
16
18
|
NodePort,
|
|
19
|
+
DynamicPort,
|
|
17
20
|
} from "../../types/index.js";
|
|
21
|
+
import { dynamicPortToNodePort } from "../../types/index.js";
|
|
18
22
|
import Icon from "@iconify/svelte";
|
|
19
23
|
import {
|
|
20
24
|
getDataTypeColor,
|
|
21
25
|
getCategoryColorToken,
|
|
22
26
|
} from "../../utils/colors.js";
|
|
23
27
|
import { getConnectedHandles } from "../../stores/workflowStore.svelte.js";
|
|
28
|
+
import {
|
|
29
|
+
applyPortOrder,
|
|
30
|
+
getPortTop,
|
|
31
|
+
isPortVisible,
|
|
32
|
+
} from "../../utils/portUtils.js";
|
|
24
33
|
import CogIcon from "../icons/CogIcon.svelte";
|
|
25
34
|
import AlertCircleIcon from "../icons/AlertCircleIcon.svelte";
|
|
26
35
|
|
|
@@ -43,15 +52,25 @@
|
|
|
43
52
|
}>();
|
|
44
53
|
|
|
45
54
|
/**
|
|
46
|
-
* Get
|
|
47
|
-
* Merges node type defaults with instance overrides
|
|
55
|
+
* Get UI extension settings from extensions, merging node type defaults with instance overrides.
|
|
48
56
|
*/
|
|
49
|
-
const hideUnconnectedHandles = $derived(
|
|
50
|
-
|
|
51
|
-
props.data.metadata?.extensions?.ui?.hideUnconnectedHandles ??
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
const hideUnconnectedHandles = $derived(
|
|
58
|
+
props.data.extensions?.ui?.hideUnconnectedHandles ??
|
|
59
|
+
props.data.metadata?.extensions?.ui?.hideUnconnectedHandles ??
|
|
60
|
+
false,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const hiddenPorts = $derived(
|
|
64
|
+
props.data.extensions?.ui?.hiddenPorts ??
|
|
65
|
+
props.data.metadata?.extensions?.ui?.hiddenPorts ??
|
|
66
|
+
{},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const portOrder = $derived(
|
|
70
|
+
props.data.extensions?.ui?.portOrder ??
|
|
71
|
+
props.data.metadata?.extensions?.ui?.portOrder ??
|
|
72
|
+
{},
|
|
73
|
+
);
|
|
55
74
|
|
|
56
75
|
// Prioritize metadata icon over config icon for simple nodes (metadata is the node definition)
|
|
57
76
|
let nodeIcon = $derived(
|
|
@@ -116,111 +135,82 @@
|
|
|
116
135
|
}
|
|
117
136
|
}
|
|
118
137
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
* @param type - Whether this is an 'input' or 'output' port
|
|
123
|
-
* @returns true if the port is connected
|
|
124
|
-
*/
|
|
125
|
-
function isPortConnected(portId: string, type: "input" | "output"): boolean {
|
|
126
|
-
const handleId = `${props.data.nodeId}-${type}-${portId}`;
|
|
127
|
-
return getConnectedHandles().has(handleId);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Check if a trigger port should be visible
|
|
132
|
-
* Always shows if hideUnconnectedHandles is disabled or if port is connected
|
|
133
|
-
*/
|
|
134
|
-
function shouldShowTriggerPort(
|
|
135
|
-
portId: string,
|
|
136
|
-
type: "input" | "output",
|
|
137
|
-
): boolean {
|
|
138
|
-
if (!hideUnconnectedHandles()) {
|
|
139
|
-
return true;
|
|
140
|
-
}
|
|
141
|
-
return isPortConnected(portId, type);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Get first input/output ports for simple node representation
|
|
145
|
-
// Special handling for trigger ports - they should always be shown if present
|
|
146
|
-
let triggerInputPort = $derived(
|
|
147
|
-
props.data.metadata?.inputs?.find(
|
|
148
|
-
(port: NodePort) => port.dataType === "trigger",
|
|
149
|
-
),
|
|
150
|
-
);
|
|
151
|
-
let triggerOutputPort = $derived(
|
|
152
|
-
props.data.metadata?.outputs?.find(
|
|
153
|
-
(port: NodePort) => port.dataType === "trigger",
|
|
138
|
+
const dynamicInputs = $derived(
|
|
139
|
+
((props.data.config?.dynamicInputs as DynamicPort[]) || []).map((port) =>
|
|
140
|
+
dynamicPortToNodePort(port, "input"),
|
|
154
141
|
),
|
|
155
142
|
);
|
|
156
143
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
(port: NodePort) =>
|
|
161
|
-
port.dataType !== "trigger" && isPortConnected(port.id, "input"),
|
|
144
|
+
const dynamicOutputs = $derived(
|
|
145
|
+
((props.data.config?.dynamicOutputs as DynamicPort[]) || []).map((port) =>
|
|
146
|
+
dynamicPortToNodePort(port, "output"),
|
|
162
147
|
),
|
|
163
148
|
);
|
|
164
149
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
150
|
+
/**
|
|
151
|
+
* All visible input ports in user-defined order.
|
|
152
|
+
*/
|
|
153
|
+
const visibleInputPorts = $derived(
|
|
154
|
+
applyPortOrder(
|
|
155
|
+
[...(props.data.metadata?.inputs ?? []), ...dynamicInputs],
|
|
156
|
+
portOrder.inputs,
|
|
157
|
+
).filter((p: NodePort) =>
|
|
158
|
+
isPortVisible(
|
|
159
|
+
p,
|
|
160
|
+
"input",
|
|
161
|
+
hiddenPorts,
|
|
162
|
+
hideUnconnectedHandles,
|
|
163
|
+
getConnectedHandles(),
|
|
164
|
+
props.data.nodeId,
|
|
165
|
+
),
|
|
168
166
|
),
|
|
169
167
|
);
|
|
170
168
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
(
|
|
169
|
+
/**
|
|
170
|
+
* All visible output ports in user-defined order.
|
|
171
|
+
*/
|
|
172
|
+
const visibleOutputPorts = $derived(
|
|
173
|
+
applyPortOrder(
|
|
174
|
+
[...(props.data.metadata?.outputs ?? []), ...dynamicOutputs],
|
|
175
|
+
portOrder.outputs,
|
|
176
|
+
).filter((p: NodePort) =>
|
|
177
|
+
isPortVisible(
|
|
178
|
+
p,
|
|
179
|
+
"output",
|
|
180
|
+
hiddenPorts,
|
|
181
|
+
hideUnconnectedHandles,
|
|
182
|
+
getConnectedHandles(),
|
|
183
|
+
props.data.nodeId,
|
|
184
|
+
),
|
|
180
185
|
),
|
|
181
186
|
);
|
|
182
187
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
let outputPorts = $derived.by(() => {
|
|
197
|
-
return [
|
|
198
|
-
...(firstConnectedDataOutputPort
|
|
199
|
-
? [firstConnectedDataOutputPort]
|
|
200
|
-
: firstDataOutputPort
|
|
201
|
-
? [firstDataOutputPort]
|
|
202
|
-
: []),
|
|
203
|
-
...(triggerOutputPort &&
|
|
204
|
-
shouldShowTriggerPort(triggerOutputPort.id, "output")
|
|
205
|
-
? [triggerOutputPort]
|
|
206
|
-
: []),
|
|
207
|
-
];
|
|
208
|
-
});
|
|
188
|
+
/**
|
|
189
|
+
* Dynamic node min-height so handles never render outside the node body.
|
|
190
|
+
*/
|
|
191
|
+
const nodeMinHeight = $derived(
|
|
192
|
+
(() => {
|
|
193
|
+
const maxPorts = Math.max(
|
|
194
|
+
visibleInputPorts.length,
|
|
195
|
+
visibleOutputPorts.length,
|
|
196
|
+
1,
|
|
197
|
+
);
|
|
198
|
+
return maxPorts <= 1 ? 80 : 20 + maxPorts * 40;
|
|
199
|
+
})(),
|
|
200
|
+
);
|
|
209
201
|
</script>
|
|
210
202
|
|
|
211
|
-
<!-- Input Handles:
|
|
212
|
-
{#each
|
|
203
|
+
<!-- Input Handles: 1 port centered at 40px; N ports at 20px start, 40px gap -->
|
|
204
|
+
{#each visibleInputPorts as port, index}
|
|
213
205
|
<Handle
|
|
214
206
|
type="target"
|
|
215
207
|
position={Position.Left}
|
|
216
208
|
style="--fd-handle-fill: var(--fd-port-skin-color, {getDataTypeColor(
|
|
217
209
|
port.dataType,
|
|
218
|
-
)}); --fd-handle-border-color: var(--fd-handle-border); top: {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
: 60
|
|
223
|
-
: 40}px; transform: translateY(-50%); z-index: 30;"
|
|
210
|
+
)}); --fd-handle-border-color: var(--fd-handle-border); top: {getPortTop(
|
|
211
|
+
index,
|
|
212
|
+
visibleInputPorts.length,
|
|
213
|
+
)}px; transform: translateY(-50%); z-index: 30;"
|
|
224
214
|
id={`${props.data.nodeId}-input-${port.id}`}
|
|
225
215
|
/>
|
|
226
216
|
{/each}
|
|
@@ -231,6 +221,7 @@
|
|
|
231
221
|
class:flowdrop-simple-node--selected={props.selected}
|
|
232
222
|
class:flowdrop-simple-node--processing={props.isProcessing}
|
|
233
223
|
class:flowdrop-simple-node--error={props.isError}
|
|
224
|
+
style="min-height: {nodeMinHeight}px"
|
|
234
225
|
onclick={handleClick}
|
|
235
226
|
ondblclick={handleDoubleClick}
|
|
236
227
|
onkeydown={handleKeydown}
|
|
@@ -290,19 +281,17 @@
|
|
|
290
281
|
</button>
|
|
291
282
|
</div>
|
|
292
283
|
|
|
293
|
-
<!-- Output Handles:
|
|
294
|
-
{#each
|
|
284
|
+
<!-- Output Handles: 1 port centered at 40px; N ports at 20px start, 40px gap -->
|
|
285
|
+
{#each visibleOutputPorts as port, index}
|
|
295
286
|
<Handle
|
|
296
287
|
type="source"
|
|
297
288
|
position={Position.Right}
|
|
298
289
|
style="--fd-handle-fill: var(--fd-port-skin-color, {getDataTypeColor(
|
|
299
290
|
port.dataType,
|
|
300
|
-
)}); --fd-handle-border-color: var(--fd-handle-border); top: {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
: 60
|
|
305
|
-
: 40}px; transform: translateY(-50%); z-index: 30;"
|
|
291
|
+
)}); --fd-handle-border-color: var(--fd-handle-border); top: {getPortTop(
|
|
292
|
+
index,
|
|
293
|
+
visibleOutputPorts.length,
|
|
294
|
+
)}px; transform: translateY(-50%); z-index: 30;"
|
|
306
295
|
id={`${props.data.nodeId}-output-${port.id}`}
|
|
307
296
|
/>
|
|
308
297
|
{/each}
|
|
@@ -80,3 +80,48 @@
|
|
|
80
80
|
selected={true}
|
|
81
81
|
/>
|
|
82
82
|
</Story>
|
|
83
|
+
|
|
84
|
+
<Story name="Dynamic Ports">
|
|
85
|
+
<NodeDecorator
|
|
86
|
+
data={createSampleNodeData({
|
|
87
|
+
label: "Data Mapper",
|
|
88
|
+
config: {
|
|
89
|
+
dynamicInputs: [
|
|
90
|
+
{
|
|
91
|
+
name: "source",
|
|
92
|
+
label: "Source Data",
|
|
93
|
+
dataType: "json",
|
|
94
|
+
required: true,
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
dynamicOutputs: [
|
|
98
|
+
{
|
|
99
|
+
name: "mapped",
|
|
100
|
+
label: "Mapped Output",
|
|
101
|
+
dataType: "json",
|
|
102
|
+
required: false,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "errors",
|
|
106
|
+
label: "Errors",
|
|
107
|
+
dataType: "string",
|
|
108
|
+
required: false,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
metadata: {
|
|
113
|
+
id: "data_mapper",
|
|
114
|
+
name: "Data Mapper",
|
|
115
|
+
description: "Map data between formats",
|
|
116
|
+
category: "processing",
|
|
117
|
+
version: "1.0.0",
|
|
118
|
+
type: "square",
|
|
119
|
+
supportedTypes: ["square"],
|
|
120
|
+
icon: "mdi:swap-horizontal",
|
|
121
|
+
color: "#0ea5e9",
|
|
122
|
+
inputs: [],
|
|
123
|
+
outputs: [],
|
|
124
|
+
},
|
|
125
|
+
})}
|
|
126
|
+
/>
|
|
127
|
+
</Story>
|