@flowdrop/flowdrop 1.2.1 → 1.3.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.
@@ -38,13 +38,7 @@
38
38
  "$ref": "#/$defs/WorkflowMetadata"
39
39
  }
40
40
  },
41
- "required": [
42
- "id",
43
- "name",
44
- "nodes",
45
- "edges",
46
- "metadata"
47
- ],
41
+ "required": ["id", "name", "nodes", "edges", "metadata"],
48
42
  "$defs": {
49
43
  "AutocompleteConfig": {
50
44
  "type": "object",
@@ -95,9 +89,7 @@
95
89
  "description": "Whether to allow multiple selections.\nWhen true, users can select multiple values displayed as tags.\n"
96
90
  }
97
91
  },
98
- "required": [
99
- "url"
100
- ]
92
+ "required": ["url"]
101
93
  },
102
94
  "Branch": {
103
95
  "type": "object",
@@ -125,10 +117,7 @@
125
117
  "description": "Whether this is the default/fallback branch"
126
118
  }
127
119
  },
128
- "required": [
129
- "name",
130
- "label"
131
- ]
120
+ "required": ["name", "label"]
132
121
  },
133
122
  "ConfigProperty": {
134
123
  "type": "object",
@@ -136,14 +125,7 @@
136
125
  "properties": {
137
126
  "type": {
138
127
  "type": "string",
139
- "enum": [
140
- "string",
141
- "number",
142
- "boolean",
143
- "array",
144
- "object",
145
- "integer"
146
- ],
128
+ "enum": ["string", "number", "boolean", "array", "object", "integer"],
147
129
  "description": "JSON Schema type"
148
130
  },
149
131
  "title": {
@@ -269,18 +251,14 @@
269
251
  ]
270
252
  }
271
253
  },
272
- "required": [
273
- "type"
274
- ]
254
+ "required": ["type"]
275
255
  },
276
256
  "ConfigSchema": {
277
257
  "type": "object",
278
258
  "properties": {
279
259
  "type": {
280
260
  "type": "string",
281
- "enum": [
282
- "object"
283
- ]
261
+ "enum": ["object"]
284
262
  },
285
263
  "properties": {
286
264
  "type": "object",
@@ -301,10 +279,7 @@
301
279
  "default": false
302
280
  }
303
281
  },
304
- "required": [
305
- "type",
306
- "properties"
307
- ]
282
+ "required": ["type", "properties"]
308
283
  },
309
284
  "DynamicPort": {
310
285
  "type": "object",
@@ -338,11 +313,7 @@
338
313
  "description": "Whether this port is required for execution"
339
314
  }
340
315
  },
341
- "required": [
342
- "name",
343
- "label",
344
- "dataType"
345
- ]
316
+ "required": ["name", "label", "dataType"]
346
317
  },
347
318
  "NodeCategory": {
348
319
  "type": "string",
@@ -433,11 +404,7 @@
433
404
  "description": "Whether the node is currently being executed"
434
405
  }
435
406
  },
436
- "required": [
437
- "status",
438
- "executionCount",
439
- "isExecuting"
440
- ]
407
+ "required": ["status", "executionCount", "isExecuting"]
441
408
  },
442
409
  "NodeExecutionStatus": {
443
410
  "type": "string",
@@ -581,11 +548,7 @@
581
548
  },
582
549
  "type": {
583
550
  "type": "string",
584
- "enum": [
585
- "input",
586
- "output",
587
- "metadata"
588
- ],
551
+ "enum": ["input", "output", "metadata"],
589
552
  "description": "Port direction (input receives data, output sends data)"
590
553
  },
591
554
  "dataType": {
@@ -614,12 +577,7 @@
614
577
  ]
615
578
  }
616
579
  },
617
- "required": [
618
- "id",
619
- "name",
620
- "type",
621
- "dataType"
622
- ]
580
+ "required": ["id", "name", "type", "dataType"]
623
581
  },
624
582
  "NodeType": {
625
583
  "type": "string",
@@ -677,9 +635,7 @@
677
635
  "description": "Optional description for this option"
678
636
  }
679
637
  },
680
- "required": [
681
- "const"
682
- ]
638
+ "required": ["const"]
683
639
  },
684
640
  "PortDataSchema": {
685
641
  "type": "object",
@@ -687,14 +643,7 @@
687
643
  "properties": {
688
644
  "type": {
689
645
  "type": "string",
690
- "enum": [
691
- "object",
692
- "array",
693
- "string",
694
- "number",
695
- "integer",
696
- "boolean"
697
- ],
646
+ "enum": ["object", "array", "string", "number", "integer", "boolean"],
698
647
  "description": "The JSON Schema type. Use `object` for nested properties,\n`array` for lists with `items` schema.\n"
699
648
  },
700
649
  "title": {
@@ -743,10 +692,7 @@
743
692
  "description": "Y coordinate"
744
693
  }
745
694
  },
746
- "required": [
747
- "x",
748
- "y"
749
- ]
695
+ "required": ["x", "y"]
750
696
  },
751
697
  "TemplateVariable": {
752
698
  "type": "object",
@@ -804,10 +750,7 @@
804
750
  "description": "Source node ID"
805
751
  }
806
752
  },
807
- "required": [
808
- "name",
809
- "type"
810
- ]
753
+ "required": ["name", "type"]
811
754
  },
812
755
  "TemplateVariablesConfig": {
813
756
  "type": "object",
@@ -854,9 +797,7 @@
854
797
  "description": "Map of available variables keyed by variable name"
855
798
  }
856
799
  },
857
- "required": [
858
- "variables"
859
- ]
800
+ "required": ["variables"]
860
801
  },
861
802
  "WorkflowEdge": {
862
803
  "type": "object",
@@ -887,12 +828,7 @@
887
828
  "type": {
888
829
  "type": "string",
889
830
  "description": "Connection line type",
890
- "enum": [
891
- "default",
892
- "straight",
893
- "step",
894
- "smoothstep"
895
- ]
831
+ "enum": ["default", "straight", "step", "smoothstep"]
896
832
  },
897
833
  "selectable": {
898
834
  "type": "boolean",
@@ -919,11 +855,7 @@
919
855
  "properties": {
920
856
  "edgeType": {
921
857
  "type": "string",
922
- "enum": [
923
- "trigger",
924
- "tool",
925
- "data"
926
- ],
858
+ "enum": ["trigger", "tool", "data"],
927
859
  "description": "Edge type for visual styling"
928
860
  },
929
861
  "sourcePortDataType": {
@@ -947,11 +879,7 @@
947
879
  }
948
880
  }
949
881
  },
950
- "required": [
951
- "id",
952
- "source",
953
- "target"
954
- ]
882
+ "required": ["id", "source", "target"]
955
883
  },
956
884
  "WorkflowMetadata": {
957
885
  "type": "object",
@@ -995,11 +923,7 @@
995
923
  "default": "flowdrop"
996
924
  }
997
925
  },
998
- "required": [
999
- "version",
1000
- "createdAt",
1001
- "updatedAt"
1002
- ]
926
+ "required": ["version", "createdAt", "updatedAt"]
1003
927
  },
1004
928
  "WorkflowNode": {
1005
929
  "type": "object",
@@ -1060,19 +984,10 @@
1060
984
  ]
1061
985
  }
1062
986
  },
1063
- "required": [
1064
- "label",
1065
- "config",
1066
- "metadata"
1067
- ]
987
+ "required": ["label", "config", "metadata"]
1068
988
  }
1069
989
  },
1070
- "required": [
1071
- "id",
1072
- "type",
1073
- "position",
1074
- "data"
1075
- ]
990
+ "required": ["id", "type", "position", "data"]
1076
991
  }
1077
992
  }
1078
993
  }
@@ -13,6 +13,7 @@
13
13
  * CSS positioning logic and stays automatically accurate.
14
14
  */
15
15
  import { SvelteMap } from "svelte/reactivity";
16
+ import { untrack } from "svelte";
16
17
  import { ProximityConnectHelper } from "../helpers/proximityConnect.js";
17
18
  /** Reactive state holding all port absolute coordinates, keyed by handleId */
18
19
  let coordinates = $state(new SvelteMap());
@@ -125,11 +126,21 @@ export function updateNodePortCoordinates(node, getInternalNode) {
125
126
  const internalNode = getInternalNode(node.id);
126
127
  if (!internalNode)
127
128
  return;
128
- // Remove old entries for this node
129
- for (const [key, coord] of coordinates) {
130
- if (coord.nodeId === node.id) {
131
- coordinates.delete(key);
129
+ // Remove old entries for this node.
130
+ // untrack prevents this read from creating a reactive dependency on `coordinates`
131
+ // inside any $effect that calls this function — otherwise the effect would re-run
132
+ // every time we mutate `coordinates`, creating an infinite reactive loop during drag.
133
+ const keysToDelete = untrack(() => {
134
+ const keys = [];
135
+ for (const [key, coord] of coordinates) {
136
+ if (coord.nodeId === node.id) {
137
+ keys.push(key);
138
+ }
132
139
  }
140
+ return keys;
141
+ });
142
+ for (const key of keysToDelete) {
143
+ coordinates.delete(key);
133
144
  }
134
145
  // Add new entries
135
146
  const coords = computeNodePortCoordinates(node, internalNode);
@@ -143,10 +154,17 @@ export function updateNodePortCoordinates(node, getInternalNode) {
143
154
  * @param nodeId - ID of the node to remove
144
155
  */
145
156
  export function removeNodePortCoordinates(nodeId) {
146
- for (const [key, coord] of coordinates) {
147
- if (coord.nodeId === nodeId) {
148
- coordinates.delete(key);
157
+ const keysToDelete = untrack(() => {
158
+ const keys = [];
159
+ for (const [key, coord] of coordinates) {
160
+ if (coord.nodeId === nodeId) {
161
+ keys.push(key);
162
+ }
149
163
  }
164
+ return keys;
165
+ });
166
+ for (const key of keysToDelete) {
167
+ coordinates.delete(key);
150
168
  }
151
169
  }
152
170
  /**
@@ -484,6 +484,23 @@ export interface ConfigEditOptions {
484
484
  export interface NodeUIExtensions {
485
485
  /** Show/hide unconnected handles (ports) to reduce visual noise */
486
486
  hideUnconnectedHandles?: boolean;
487
+ /**
488
+ * Visual-only port display order (no effect on execution).
489
+ * Arrays of port IDs in the desired render order.
490
+ * Ports not listed appear at the end in metadata order.
491
+ */
492
+ portOrder?: {
493
+ inputs?: string[];
494
+ outputs?: string[];
495
+ };
496
+ /**
497
+ * Manually hidden ports (visual only, no effect on execution).
498
+ * Required ports cannot be added here — enforced in the UI.
499
+ */
500
+ hiddenPorts?: {
501
+ inputs?: string[];
502
+ outputs?: string[];
503
+ };
487
504
  /** Custom styles or theme overrides */
488
505
  style?: Record<string, unknown>;
489
506
  /** Any other UI-specific settings */
@@ -0,0 +1,24 @@
1
+ import type { NodePort } from "../types/index.js";
2
+ /**
3
+ * Sort ports by an ordered array of port IDs.
4
+ * Ports not listed appear at the end in their original order.
5
+ */
6
+ export declare function applyPortOrder(ports: NodePort[], orderedIds: string[] | undefined): NodePort[];
7
+ /**
8
+ * Compute the CSS `top` offset (px) for a port handle.
9
+ * - 1 port: centered at 40px
10
+ * - N ports: 20px start, 40px gap between each
11
+ */
12
+ export declare function getPortTop(index: number, count: number): number;
13
+ /**
14
+ * Determine whether a port should be rendered.
15
+ *
16
+ * Priority:
17
+ * 1. Manual hide (`hiddenPorts`) — always wins, regardless of connections
18
+ * 2. `hideUnconnectedHandles` — hide if not present in the connected handles set
19
+ * 3. Default — visible
20
+ */
21
+ export declare function isPortVisible(port: NodePort, direction: "input" | "output", hiddenPorts: {
22
+ inputs?: string[];
23
+ outputs?: string[];
24
+ }, hideUnconnectedHandles: boolean, connectedHandles: Set<string>, nodeId: string | undefined): boolean;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Sort ports by an ordered array of port IDs.
3
+ * Ports not listed appear at the end in their original order.
4
+ */
5
+ export function applyPortOrder(ports, orderedIds) {
6
+ if (!orderedIds || orderedIds.length === 0)
7
+ return ports;
8
+ const orderMap = new Map(orderedIds.map((id, i) => [id, i]));
9
+ return [...ports].sort((a, b) => {
10
+ const aIdx = orderMap.get(a.id) ?? Infinity;
11
+ const bIdx = orderMap.get(b.id) ?? Infinity;
12
+ // Guard: Infinity - Infinity = NaN, which corrupts sort for two unlisted ports
13
+ return aIdx === bIdx ? 0 : aIdx - bIdx;
14
+ });
15
+ }
16
+ /**
17
+ * Compute the CSS `top` offset (px) for a port handle.
18
+ * - 1 port: centered at 40px
19
+ * - N ports: 20px start, 40px gap between each
20
+ */
21
+ export function getPortTop(index, count) {
22
+ if (count === 1)
23
+ return 40;
24
+ return 20 + index * 40;
25
+ }
26
+ /**
27
+ * Determine whether a port should be rendered.
28
+ *
29
+ * Priority:
30
+ * 1. Manual hide (`hiddenPorts`) — always wins, regardless of connections
31
+ * 2. `hideUnconnectedHandles` — hide if not present in the connected handles set
32
+ * 3. Default — visible
33
+ */
34
+ export function isPortVisible(port, direction, hiddenPorts, hideUnconnectedHandles, connectedHandles, nodeId) {
35
+ const hiddenList = direction === "input" ? hiddenPorts.inputs : hiddenPorts.outputs;
36
+ if (hiddenList?.includes(port.id))
37
+ return false;
38
+ if (hideUnconnectedHandles) {
39
+ return connectedHandles.has(`${nodeId}-${direction}-${port.id}`);
40
+ }
41
+ return true;
42
+ }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "A drop-in visual workflow editor for any web application. You own the backend. You own the data. You own the orchestration.",
4
4
  "license": "MIT",
5
5
  "private": false,
6
- "version": "1.2.1",
6
+ "version": "1.3.0",
7
7
  "author": "Shibin Das (D34dMan)",
8
8
  "bugs": {
9
9
  "url": "https://github.com/flowdrop-io/flowdrop/issues"
@@ -172,7 +172,6 @@
172
172
  },
173
173
  "devDependencies": {
174
174
  "@chromatic-com/storybook": "^5.0.1",
175
- "@xyflow/svelte": "^1.2",
176
175
  "@codemirror/autocomplete": "^6.20.0",
177
176
  "@codemirror/commands": "^6.10.2",
178
177
  "@codemirror/lang-json": "^6.0.2",
@@ -182,7 +181,6 @@
182
181
  "@codemirror/state": "^6.5.4",
183
182
  "@codemirror/theme-one-dark": "^6.1.3",
184
183
  "@codemirror/view": "^6.39.14",
185
- "codemirror": "^6.0.2",
186
184
  "@eslint/compat": "^1.2.5",
187
185
  "@eslint/js": "^9.18.0",
188
186
  "@iconify/svelte": "^5.0.0",
@@ -204,6 +202,8 @@
204
202
  "@vitest/browser": "^3.2.3",
205
203
  "@vitest/coverage-v8": "^3.2.4",
206
204
  "@vitest/ui": "^3.2.4",
205
+ "@xyflow/svelte": "^1.2",
206
+ "codemirror": "^6.0.2",
207
207
  "eslint": "^9.18.0",
208
208
  "eslint-config-prettier": "^10.0.1",
209
209
  "eslint-plugin-storybook": "^10.2.15",