@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.
@@ -2,9 +2,11 @@
2
2
  Square Node Component
3
3
  A simple square 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,7 +16,9 @@
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,
@@ -22,6 +26,11 @@
22
26
  } from "../../utils/colors.js";
23
27
  import { getNodeIcon } from "../../utils/icons.js";
24
28
  import { getConnectedHandles } from "../../stores/workflowStore.svelte.js";
29
+ import {
30
+ applyPortOrder,
31
+ getPortTop,
32
+ isPortVisible,
33
+ } from "../../utils/portUtils.js";
25
34
  import CogIcon from "../icons/CogIcon.svelte";
26
35
  import AlertCircleIcon from "../icons/AlertCircleIcon.svelte";
27
36
 
@@ -44,15 +53,25 @@
44
53
  }>();
45
54
 
46
55
  /**
47
- * Get the hideUnconnectedHandles setting from extensions
48
- * Merges node type defaults with instance overrides
56
+ * Get UI extension settings from extensions, merging node type defaults with instance overrides.
49
57
  */
50
- const hideUnconnectedHandles = $derived(() => {
51
- const typeDefault =
52
- props.data.metadata?.extensions?.ui?.hideUnconnectedHandles ?? false;
53
- const instanceOverride = props.data.extensions?.ui?.hideUnconnectedHandles;
54
- return instanceOverride ?? typeDefault;
55
- });
58
+ const hideUnconnectedHandles = $derived(
59
+ props.data.extensions?.ui?.hideUnconnectedHandles ??
60
+ props.data.metadata?.extensions?.ui?.hideUnconnectedHandles ??
61
+ false,
62
+ );
63
+
64
+ const hiddenPorts = $derived(
65
+ props.data.extensions?.ui?.hiddenPorts ??
66
+ props.data.metadata?.extensions?.ui?.hiddenPorts ??
67
+ {},
68
+ );
69
+
70
+ const portOrder = $derived(
71
+ props.data.extensions?.ui?.portOrder ??
72
+ props.data.metadata?.extensions?.ui?.portOrder ??
73
+ {},
74
+ );
56
75
 
57
76
  /**
58
77
  * Get icon using the same resolution as WorkflowNode
@@ -100,111 +119,83 @@
100
119
  openConfigSidebar();
101
120
  }
102
121
  }
103
- /**
104
- * Check if a port is connected
105
- * @param portId - The port ID to check
106
- * @param type - Whether this is an 'input' or 'output' port
107
- * @returns true if the port is connected
108
- */
109
- function isPortConnected(portId: string, type: "input" | "output"): boolean {
110
- const handleId = `${props.data.nodeId}-${type}-${portId}`;
111
- return getConnectedHandles().has(handleId);
112
- }
113
-
114
- /**
115
- * Check if a trigger port should be visible
116
- * Always shows if hideUnconnectedHandles is disabled or if port is connected
117
- */
118
- function shouldShowTriggerPort(
119
- portId: string,
120
- type: "input" | "output",
121
- ): boolean {
122
- if (!hideUnconnectedHandles()) {
123
- return true;
124
- }
125
- return isPortConnected(portId, type);
126
- }
127
-
128
- // Get first input/output ports for square node representation
129
- // Special handling for trigger ports - they should always be shown if present
130
- let triggerInputPort = $derived(
131
- props.data.metadata?.inputs?.find(
132
- (port: NodePort) => port.dataType === "trigger",
133
- ),
134
- );
135
- let triggerOutputPort = $derived(
136
- props.data.metadata?.outputs?.find(
137
- (port: NodePort) => port.dataType === "trigger",
122
+ const dynamicInputs = $derived(
123
+ ((props.data.config?.dynamicInputs as DynamicPort[]) || []).map((port) =>
124
+ dynamicPortToNodePort(port, "input"),
138
125
  ),
139
126
  );
140
127
 
141
- // Get first non-trigger ports for data connections
142
- let firstConnectedDataInputPort = $derived(
143
- props.data.metadata?.inputs?.find(
144
- (port: NodePort) =>
145
- port.dataType !== "trigger" && isPortConnected(port.id, "input"),
128
+ const dynamicOutputs = $derived(
129
+ ((props.data.config?.dynamicOutputs as DynamicPort[]) || []).map((port) =>
130
+ dynamicPortToNodePort(port, "output"),
146
131
  ),
147
132
  );
148
133
 
149
- let firstDataInputPort = $derived(
150
- props.data.metadata?.inputs?.find(
151
- (port: NodePort) => port.dataType !== "trigger",
134
+ /**
135
+ * All visible input ports in user-defined order.
136
+ */
137
+ const visibleInputPorts = $derived(
138
+ applyPortOrder(
139
+ [...(props.data.metadata?.inputs ?? []), ...dynamicInputs],
140
+ portOrder.inputs,
141
+ ).filter((p: NodePort) =>
142
+ isPortVisible(
143
+ p,
144
+ "input",
145
+ hiddenPorts,
146
+ hideUnconnectedHandles,
147
+ getConnectedHandles(),
148
+ props.data.nodeId,
149
+ ),
152
150
  ),
153
151
  );
154
152
 
155
- let firstConnectedDataOutputPort = $derived(
156
- props.data.metadata?.outputs?.find(
157
- (port: NodePort) =>
158
- port.dataType !== "trigger" && isPortConnected(port.id, "output"),
159
- ),
160
- );
161
- let firstDataOutputPort = $derived(
162
- props.data.metadata?.outputs?.find(
163
- (port: NodePort) => port.dataType !== "trigger",
153
+ /**
154
+ * All visible output ports in user-defined order.
155
+ */
156
+ const visibleOutputPorts = $derived(
157
+ applyPortOrder(
158
+ [...(props.data.metadata?.outputs ?? []), ...dynamicOutputs],
159
+ portOrder.outputs,
160
+ ).filter((p: NodePort) =>
161
+ isPortVisible(
162
+ p,
163
+ "output",
164
+ hiddenPorts,
165
+ hideUnconnectedHandles,
166
+ getConnectedHandles(),
167
+ props.data.nodeId,
168
+ ),
164
169
  ),
165
170
  );
166
171
 
167
- let inputPorts = $derived.by(() => {
168
- return [
169
- ...(firstConnectedDataInputPort
170
- ? [firstConnectedDataInputPort]
171
- : firstDataInputPort
172
- ? [firstDataInputPort]
173
- : []),
174
- ...(triggerInputPort &&
175
- shouldShowTriggerPort(triggerInputPort.id, "input")
176
- ? [triggerInputPort]
177
- : []),
178
- ];
179
- });
180
- let outputPorts = $derived.by(() => {
181
- return [
182
- ...(firstConnectedDataOutputPort
183
- ? [firstConnectedDataOutputPort]
184
- : firstDataOutputPort
185
- ? [firstDataOutputPort]
186
- : []),
187
- ...(triggerOutputPort &&
188
- shouldShowTriggerPort(triggerOutputPort.id, "output")
189
- ? [triggerOutputPort]
190
- : []),
191
- ];
192
- });
172
+ /**
173
+ * Dynamic node size so handles never render outside the node body.
174
+ * Overrides the fixed CSS height/width when more than 2 ports are visible on either side.
175
+ */
176
+ const nodeSize = $derived(
177
+ (() => {
178
+ const maxPorts = Math.max(
179
+ visibleInputPorts.length,
180
+ visibleOutputPorts.length,
181
+ 1,
182
+ );
183
+ return maxPorts <= 1 ? 80 : 20 + maxPorts * 40;
184
+ })(),
185
+ );
193
186
  </script>
194
187
 
195
- <!-- Input Handles: center at 20/40/60px (multiple of 10), 20px connection area -->
196
- {#each inputPorts as port, index}
188
+ <!-- Input Handles: 1 port centered at 40px; N ports at 20px start, 40px gap -->
189
+ {#each visibleInputPorts as port, index}
197
190
  <Handle
198
191
  type="target"
199
192
  position={Position.Left}
200
193
  style="--fd-handle-fill: var(--fd-port-skin-color, {getDataTypeColor(
201
194
  port.dataType,
202
- )}); --fd-handle-border-color: var(--fd-handle-border); top: {inputPorts.length >
203
- 1
204
- ? index === 0
205
- ? 20
206
- : 60
207
- : 40}px; transform: translateY(-50%); z-index: 30;"
195
+ )}); --fd-handle-border-color: var(--fd-handle-border); top: {getPortTop(
196
+ index,
197
+ visibleInputPorts.length,
198
+ )}px; transform: translateY(-50%); z-index: 30;"
208
199
  id={`${props.data.nodeId}-input-${port.id}`}
209
200
  />
210
201
  {/each}
@@ -215,6 +206,7 @@
215
206
  class:flowdrop-square-node--selected={props.selected}
216
207
  class:flowdrop-square-node--processing={props.isProcessing}
217
208
  class:flowdrop-square-node--error={props.isError}
209
+ style="height: {nodeSize}px; width: {nodeSize}px"
218
210
  onclick={handleClick}
219
211
  ondblclick={handleDoubleClick}
220
212
  onkeydown={handleKeydown}
@@ -261,19 +253,17 @@
261
253
  </button>
262
254
  </div>
263
255
 
264
- <!-- Output Handles: center at 20/40/60px (multiple of 10), 20px connection area -->
265
- {#each outputPorts as port, index}
256
+ <!-- Output Handles: 1 port centered at 40px; N ports at 20px start, 40px gap -->
257
+ {#each visibleOutputPorts as port, index}
266
258
  <Handle
267
259
  type="source"
268
260
  position={Position.Right}
269
261
  style="--fd-handle-fill: var(--fd-port-skin-color, {getDataTypeColor(
270
262
  port.dataType,
271
- )}); --fd-handle-border-color: var(--fd-handle-border); top: {outputPorts.length >
272
- 1
273
- ? index === 0
274
- ? 20
275
- : 60
276
- : 40}px; transform: translateY(-50%); z-index: 30;"
263
+ )}); --fd-handle-border-color: var(--fd-handle-border); top: {getPortTop(
264
+ index,
265
+ visibleOutputPorts.length,
266
+ )}px; transform: translateY(-50%); z-index: 30;"
277
267
  id={`${props.data.nodeId}-output-${port.id}`}
278
268
  />
279
269
  {/each}
@@ -155,18 +155,17 @@
155
155
  * Get the hideUnconnectedHandles setting from extensions
156
156
  * Merges node type defaults with instance overrides
157
157
  */
158
- const hideUnconnectedHandles = $derived(() => {
159
- const typeDefault =
160
- props.data.metadata?.extensions?.ui?.hideUnconnectedHandles ?? false;
161
- const instanceOverride = props.data.extensions?.ui?.hideUnconnectedHandles;
162
- return instanceOverride ?? typeDefault;
163
- });
158
+ const hideUnconnectedHandles = $derived(
159
+ props.data.extensions?.ui?.hideUnconnectedHandles ??
160
+ props.data.metadata?.extensions?.ui?.hideUnconnectedHandles ??
161
+ false,
162
+ );
164
163
 
165
164
  /**
166
165
  * Check if a port should be visible based on connection state and settings
167
166
  */
168
167
  function isPortVisible(port: NodePort, type: "input" | "output"): boolean {
169
- if (!hideUnconnectedHandles()) {
168
+ if (!hideUnconnectedHandles) {
170
169
  return true;
171
170
  }
172
171
  if (port.required) {
@@ -77,3 +77,66 @@
77
77
  selected={true}
78
78
  />
79
79
  </Story>
80
+
81
+ <Story name="Dynamic Ports">
82
+ <NodeDecorator
83
+ data={createSampleNodeData({
84
+ label: "Custom Function",
85
+ config: {
86
+ dynamicInputs: [
87
+ {
88
+ name: "input_1",
89
+ label: "First Input",
90
+ description: "The first input parameter",
91
+ dataType: "string",
92
+ required: true,
93
+ },
94
+ {
95
+ name: "input_2",
96
+ label: "Second Input",
97
+ description: "The second input parameter",
98
+ dataType: "number",
99
+ required: false,
100
+ },
101
+ ],
102
+ dynamicOutputs: [
103
+ {
104
+ name: "output_1",
105
+ label: "Primary Output",
106
+ description: "The main output value",
107
+ dataType: "string",
108
+ required: false,
109
+ },
110
+ ],
111
+ },
112
+ metadata: {
113
+ id: "custom-function",
114
+ name: "Custom Function",
115
+ description:
116
+ "Execute a custom function with dynamic inputs and outputs",
117
+ category: "processing",
118
+ version: "1.0.0",
119
+ type: "workflow",
120
+ icon: "mdi:function-variant",
121
+ inputs: [
122
+ {
123
+ id: "trigger",
124
+ name: "Trigger",
125
+ type: "input",
126
+ dataType: "trigger",
127
+ required: false,
128
+ },
129
+ ],
130
+ outputs: [
131
+ {
132
+ id: "done",
133
+ name: "Done",
134
+ type: "output",
135
+ dataType: "trigger",
136
+ required: false,
137
+ },
138
+ ],
139
+ },
140
+ })}
141
+ />
142
+ </Story>
@@ -6,6 +6,8 @@
6
6
 
7
7
  UI Extensions Support:
8
8
  - hideUnconnectedHandles: Hides ports that are not connected to reduce visual clutter
9
+ - portOrder: Visual-only reordering of input/output ports (no effect on execution)
10
+ - hiddenPorts: Manually hidden ports per direction (required ports cannot be hidden)
9
11
  -->
10
12
 
11
13
  <script lang="ts">
@@ -25,6 +27,7 @@
25
27
  getPortBackgroundColor,
26
28
  } from "../../utils/colors.js";
27
29
  import { getConnectedHandles } from "../../stores/workflowStore.svelte.js";
30
+ import { applyPortOrder } from "../../utils/portUtils.js";
28
31
 
29
32
  interface Props {
30
33
  data: WorkflowNode["data"] & {
@@ -64,12 +67,31 @@
64
67
  * Get the hideUnconnectedHandles setting from extensions
65
68
  * Merges node type defaults with instance overrides
66
69
  */
67
- const hideUnconnectedHandles = $derived(() => {
68
- const typeDefault =
69
- props.data.metadata?.extensions?.ui?.hideUnconnectedHandles ?? false;
70
- const instanceOverride = props.data.extensions?.ui?.hideUnconnectedHandles;
71
- return instanceOverride ?? typeDefault;
72
- });
70
+ const hideUnconnectedHandles = $derived(
71
+ props.data.extensions?.ui?.hideUnconnectedHandles ??
72
+ props.data.metadata?.extensions?.ui?.hideUnconnectedHandles ??
73
+ false,
74
+ );
75
+
76
+ /**
77
+ * Get the portOrder setting from extensions (visual-only, no effect on execution)
78
+ * Merges node type defaults with instance overrides
79
+ */
80
+ const portOrder = $derived(
81
+ props.data.extensions?.ui?.portOrder ??
82
+ props.data.metadata?.extensions?.ui?.portOrder ??
83
+ {},
84
+ );
85
+
86
+ /**
87
+ * Get the hiddenPorts setting from extensions (visual-only, no effect on execution)
88
+ * Merges node type defaults with instance overrides
89
+ */
90
+ const hiddenPorts = $derived(
91
+ props.data.extensions?.ui?.hiddenPorts ??
92
+ props.data.metadata?.extensions?.ui?.hiddenPorts ??
93
+ {},
94
+ );
73
95
 
74
96
  /**
75
97
  * Dynamic inputs from config - user-defined input ports
@@ -92,20 +114,26 @@
92
114
  );
93
115
 
94
116
  /**
95
- * Combined input ports: static metadata inputs + dynamic config inputs
117
+ * Combined input ports: static metadata inputs + dynamic config inputs,
118
+ * sorted by portOrder if set (visual-only)
96
119
  */
97
- const allInputPorts = $derived([
98
- ...props.data.metadata.inputs,
99
- ...dynamicInputs,
100
- ]);
120
+ const allInputPorts = $derived(
121
+ applyPortOrder(
122
+ [...props.data.metadata.inputs, ...dynamicInputs],
123
+ portOrder.inputs,
124
+ ),
125
+ );
101
126
 
102
127
  /**
103
- * Combined output ports: static metadata outputs + dynamic config outputs
128
+ * Combined output ports: static metadata outputs + dynamic config outputs,
129
+ * sorted by portOrder if set (visual-only)
104
130
  */
105
- const allOutputPorts = $derived([
106
- ...props.data.metadata.outputs,
107
- ...dynamicOutputs,
108
- ]);
131
+ const allOutputPorts = $derived(
132
+ applyPortOrder(
133
+ [...props.data.metadata.outputs, ...dynamicOutputs],
134
+ portOrder.outputs,
135
+ ),
136
+ );
109
137
 
110
138
  /**
111
139
  * Check if a port should be visible based on connection state and settings
@@ -114,8 +142,15 @@
114
142
  * @returns true if the port should be visible
115
143
  */
116
144
  function isPortVisible(port: NodePort, type: "input" | "output"): boolean {
145
+ // Manual hide takes precedence (required ports are prevented from being hidden in ConfigForm)
146
+ const manuallyHidden =
147
+ type === "input"
148
+ ? hiddenPorts.inputs?.includes(port.id)
149
+ : hiddenPorts.outputs?.includes(port.id);
150
+ if (manuallyHidden) return false;
151
+
117
152
  // Always show if hideUnconnectedHandles is disabled
118
- if (!hideUnconnectedHandles()) {
153
+ if (!hideUnconnectedHandles) {
119
154
  return true;
120
155
  }
121
156
 
@@ -245,7 +280,7 @@
245
280
  {#if visibleInputPorts.length > 0}
246
281
  <div class="flowdrop-workflow-node__ports">
247
282
  <div class="flowdrop-workflow-node__ports-list">
248
- {#each visibleInputPorts as port, inputIndex (port.id)}
283
+ {#each visibleInputPorts as port (port.id)}
249
284
  <div class="flowdrop-workflow-node__port">
250
285
  <!-- Input Handle: centered in row, at node edge (ports have no padding) -->
251
286
  <Handle
@@ -308,7 +343,7 @@
308
343
  {#if visibleOutputPorts.length > 0}
309
344
  <div class="flowdrop-workflow-node__ports">
310
345
  <div class="flowdrop-workflow-node__ports-list">
311
- {#each visibleOutputPorts as port, outputIndex (port.id)}
346
+ {#each visibleOutputPorts as port (port.id)}
312
347
  <div class="flowdrop-workflow-node__port">
313
348
  <!-- Port Info: padding lives here so handle position is simple -->
314
349
  <div