@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.
@@ -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
- const typeDefault =
62
- props.data.metadata?.extensions?.ui?.hideUnconnectedHandles ?? false;
63
- const instanceOverride = props.data.extensions?.ui?.hideUnconnectedHandles;
64
- return instanceOverride ?? typeDefault;
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, inputIndex (port.id)}
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 trigger ports that are not connected to reduce visual clutter
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 the hideUnconnectedHandles setting from extensions
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
- const typeDefault =
51
- props.data.metadata?.extensions?.ui?.hideUnconnectedHandles ?? false;
52
- const instanceOverride = props.data.extensions?.ui?.hideUnconnectedHandles;
53
- return instanceOverride ?? typeDefault;
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
- * Check if a port is connected
121
- * @param portId - The port ID to check
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
- // Get first non-trigger ports for data connections
158
- let firstConnectedDataInputPort = $derived(
159
- props.data.metadata?.inputs?.find(
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
- let firstDataInputPort = $derived(
166
- props.data.metadata?.inputs?.find(
167
- (port: NodePort) => port.dataType !== "trigger",
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
- let firstConnectedDataOutputPort = $derived(
172
- props.data.metadata?.outputs?.find(
173
- (port: NodePort) =>
174
- port.dataType !== "trigger" && isPortConnected(port.id, "output"),
175
- ),
176
- );
177
- let firstDataOutputPort = $derived(
178
- props.data.metadata?.outputs?.find(
179
- (port: NodePort) => port.dataType !== "trigger",
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
- let inputPorts = $derived.by(() => {
184
- return [
185
- ...(firstConnectedDataInputPort
186
- ? [firstConnectedDataInputPort]
187
- : firstDataInputPort
188
- ? [firstDataInputPort]
189
- : []),
190
- ...(triggerInputPort &&
191
- shouldShowTriggerPort(triggerInputPort.id, "input")
192
- ? [triggerInputPort]
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: center at 20/40/60px (multiple of 10), 20px connection area -->
212
- {#each inputPorts as port, index}
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: {inputPorts.length >
219
- 1
220
- ? index === 0
221
- ? 20
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: center at 20/40/60px (multiple of 10), 20px connection area -->
294
- {#each outputPorts as port, index}
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: {outputPorts.length >
301
- 1
302
- ? index === 0
303
- ? 20
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>