@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.
@@ -25,9 +25,12 @@
25
25
  WorkflowNode,
26
26
  WorkflowEdge,
27
27
  NodeUIExtensions,
28
+ NodePort,
29
+ DynamicPort,
28
30
  ConfigEditOptions,
29
31
  AuthProvider,
30
32
  } from "../types/index.js";
33
+ import { dynamicPortToNodePort } from "../types/index.js";
31
34
  import type { UISchemaElement } from "../types/uischema.js";
32
35
  import {
33
36
  FormField,
@@ -46,6 +49,11 @@
46
49
  import { globalSaveWorkflow } from "../services/globalSave.js";
47
50
  import { getAvailableVariables } from "../services/variableService.js";
48
51
  import { logger } from "../utils/logger.js";
52
+ import {
53
+ getDataTypeColorToken,
54
+ getPortBackgroundColor,
55
+ } from "../utils/colors.js";
56
+ import { applyPortOrder } from "../utils/portUtils.js";
49
57
 
50
58
  interface Props {
51
59
  /** Optional workflow node (if provided, schema and values are derived from it) */
@@ -338,9 +346,118 @@
338
346
  uiExtensionValues = {
339
347
  hideUnconnectedHandles:
340
348
  initialUIExtensions.hideUnconnectedHandles ?? false,
349
+ portOrder: initialUIExtensions.portOrder
350
+ ? {
351
+ inputs: initialUIExtensions.portOrder.inputs
352
+ ? [...initialUIExtensions.portOrder.inputs]
353
+ : undefined,
354
+ outputs: initialUIExtensions.portOrder.outputs
355
+ ? [...initialUIExtensions.portOrder.outputs]
356
+ : undefined,
357
+ }
358
+ : undefined,
359
+ hiddenPorts: initialUIExtensions.hiddenPorts
360
+ ? {
361
+ inputs: initialUIExtensions.hiddenPorts.inputs
362
+ ? [...initialUIExtensions.hiddenPorts.inputs]
363
+ : undefined,
364
+ outputs: initialUIExtensions.hiddenPorts.outputs
365
+ ? [...initialUIExtensions.hiddenPorts.outputs]
366
+ : undefined,
367
+ }
368
+ : undefined,
341
369
  };
342
370
  });
343
371
 
372
+ /**
373
+ * All input ports in current display order for the port management UI.
374
+ * Combines static metadata inputs + dynamic config inputs, sorted by portOrder.
375
+ */
376
+ const allInputPortsForUI = $derived.by<NodePort[]>(() => {
377
+ if (!node) return [];
378
+ const staticInputs = node.data.metadata.inputs ?? [];
379
+ const dynInputs = (
380
+ (node.data.config?.dynamicInputs as DynamicPort[]) || []
381
+ ).map((p) => dynamicPortToNodePort(p, "input"));
382
+ return applyPortOrder(
383
+ [...staticInputs, ...dynInputs],
384
+ uiExtensionValues.portOrder?.inputs,
385
+ );
386
+ });
387
+
388
+ /**
389
+ * All output ports in current display order for the port management UI.
390
+ * Combines static metadata outputs + dynamic config outputs, sorted by portOrder.
391
+ */
392
+ const allOutputPortsForUI = $derived.by<NodePort[]>(() => {
393
+ if (!node) return [];
394
+ const staticOutputs = node.data.metadata.outputs ?? [];
395
+ const dynOutputs = (
396
+ (node.data.config?.dynamicOutputs as DynamicPort[]) || []
397
+ ).map((p) => dynamicPortToNodePort(p, "output"));
398
+ return applyPortOrder(
399
+ [...staticOutputs, ...dynOutputs],
400
+ uiExtensionValues.portOrder?.outputs,
401
+ );
402
+ });
403
+
404
+ /**
405
+ * Move a port one position up or down in the display order.
406
+ */
407
+ function movePort(
408
+ direction: "inputs" | "outputs",
409
+ portId: string,
410
+ delta: -1 | 1,
411
+ ): void {
412
+ const list =
413
+ direction === "inputs" ? allInputPortsForUI : allOutputPortsForUI;
414
+ const idx = list.findIndex((p) => p.id === portId);
415
+ if (idx === -1) return;
416
+ const newIdx = idx + delta;
417
+ if (newIdx < 0 || newIdx >= list.length) return;
418
+ const newOrder = list.map((p) => p.id);
419
+ [newOrder[idx], newOrder[newIdx]] = [newOrder[newIdx], newOrder[idx]];
420
+ uiExtensionValues.portOrder = {
421
+ ...uiExtensionValues.portOrder,
422
+ [direction]: newOrder,
423
+ };
424
+ handleFormBlur();
425
+ }
426
+
427
+ /**
428
+ * Toggle manual visibility of a port. Required ports cannot be hidden.
429
+ */
430
+ function togglePortHidden(
431
+ direction: "inputs" | "outputs",
432
+ portId: string,
433
+ ): void {
434
+ const current = uiExtensionValues.hiddenPorts?.[direction] ?? [];
435
+ const isHidden = current.includes(portId);
436
+ const next = isHidden
437
+ ? current.filter((id) => id !== portId)
438
+ : [...current, portId];
439
+ uiExtensionValues.hiddenPorts = {
440
+ ...uiExtensionValues.hiddenPorts,
441
+ [direction]: next.length > 0 ? next : undefined,
442
+ };
443
+ handleFormBlur();
444
+ }
445
+
446
+ /**
447
+ * Reset all port customizations (order + hidden) for a direction back to defaults.
448
+ */
449
+ function resetPortCustomizations(direction: "inputs" | "outputs"): void {
450
+ const order = { ...uiExtensionValues.portOrder };
451
+ const hidden = { ...uiExtensionValues.hiddenPorts };
452
+ delete order[direction];
453
+ delete hidden[direction];
454
+ uiExtensionValues.portOrder =
455
+ Object.keys(order).length > 0 ? order : undefined;
456
+ uiExtensionValues.hiddenPorts =
457
+ Object.keys(hidden).length > 0 ? hidden : undefined;
458
+ handleFormBlur();
459
+ }
460
+
344
461
  /**
345
462
  * Check if a field is required based on schema
346
463
  */
@@ -708,6 +825,176 @@
708
825
  }}
709
826
  />
710
827
  </FormFieldWrapper>
828
+
829
+ <!-- Input Port Order & Visibility -->
830
+ {#if allInputPortsForUI.length > 0}
831
+ <div class="config-form__port-order">
832
+ <div class="config-form__port-order-header">
833
+ <span class="config-form__port-order-label">Input Ports</span>
834
+ {#if uiExtensionValues.portOrder?.inputs?.length || uiExtensionValues.hiddenPorts?.inputs?.length}
835
+ <button
836
+ type="button"
837
+ class="config-form__port-order-reset"
838
+ onclick={() => resetPortCustomizations("inputs")}
839
+ title="Reset to default order and visibility"
840
+ >
841
+ <Icon icon="heroicons:arrow-uturn-left" />
842
+ Reset
843
+ </button>
844
+ {/if}
845
+ </div>
846
+ <ul class="config-form__port-order-list">
847
+ {#each allInputPortsForUI as port, i (port.id)}
848
+ {@const isHidden =
849
+ uiExtensionValues.hiddenPorts?.inputs?.includes(port.id) ??
850
+ false}
851
+ {@const isRequired = port.required ?? false}
852
+ <li
853
+ class="config-form__port-order-item"
854
+ class:config-form__port-order-item--hidden={isHidden}
855
+ >
856
+ <span class="config-form__port-order-name">{port.name}</span
857
+ >
858
+ <span
859
+ class="config-form__port-order-badge"
860
+ style="background-color:{getPortBackgroundColor(
861
+ port.dataType,
862
+ 15,
863
+ )};color:{getDataTypeColorToken(
864
+ port.dataType,
865
+ )};border:1px solid {getPortBackgroundColor(
866
+ port.dataType,
867
+ 30,
868
+ )}"
869
+ >
870
+ {port.dataType}
871
+ </span>
872
+ <div class="config-form__port-order-actions">
873
+ <button
874
+ type="button"
875
+ disabled={isRequired}
876
+ title={isRequired
877
+ ? "Required ports cannot be hidden"
878
+ : isHidden
879
+ ? "Show port"
880
+ : "Hide port"}
881
+ class:active={isHidden}
882
+ onclick={() => togglePortHidden("inputs", port.id)}
883
+ >
884
+ <Icon
885
+ icon={isHidden
886
+ ? "heroicons:eye-slash"
887
+ : "heroicons:eye"}
888
+ />
889
+ </button>
890
+ <button
891
+ type="button"
892
+ disabled={i === 0 || allInputPortsForUI.length === 1}
893
+ onclick={() => movePort("inputs", port.id, -1)}
894
+ title="Move up"
895
+ >
896
+ <Icon icon="heroicons:chevron-up" />
897
+ </button>
898
+ <button
899
+ type="button"
900
+ disabled={i === allInputPortsForUI.length - 1 ||
901
+ allInputPortsForUI.length === 1}
902
+ onclick={() => movePort("inputs", port.id, 1)}
903
+ title="Move down"
904
+ >
905
+ <Icon icon="heroicons:chevron-down" />
906
+ </button>
907
+ </div>
908
+ </li>
909
+ {/each}
910
+ </ul>
911
+ </div>
912
+ {/if}
913
+
914
+ <!-- Output Port Order & Visibility -->
915
+ {#if allOutputPortsForUI.length > 0}
916
+ <div class="config-form__port-order">
917
+ <div class="config-form__port-order-header">
918
+ <span class="config-form__port-order-label">Output Ports</span>
919
+ {#if uiExtensionValues.portOrder?.outputs?.length || uiExtensionValues.hiddenPorts?.outputs?.length}
920
+ <button
921
+ type="button"
922
+ class="config-form__port-order-reset"
923
+ onclick={() => resetPortCustomizations("outputs")}
924
+ title="Reset to default order and visibility"
925
+ >
926
+ <Icon icon="heroicons:arrow-uturn-left" />
927
+ Reset
928
+ </button>
929
+ {/if}
930
+ </div>
931
+ <ul class="config-form__port-order-list">
932
+ {#each allOutputPortsForUI as port, i (port.id)}
933
+ {@const isHidden =
934
+ uiExtensionValues.hiddenPorts?.outputs?.includes(port.id) ??
935
+ false}
936
+ {@const isRequired = port.required ?? false}
937
+ <li
938
+ class="config-form__port-order-item"
939
+ class:config-form__port-order-item--hidden={isHidden}
940
+ >
941
+ <span class="config-form__port-order-name">{port.name}</span
942
+ >
943
+ <span
944
+ class="config-form__port-order-badge"
945
+ style="background-color:{getPortBackgroundColor(
946
+ port.dataType,
947
+ 15,
948
+ )};color:{getDataTypeColorToken(
949
+ port.dataType,
950
+ )};border:1px solid {getPortBackgroundColor(
951
+ port.dataType,
952
+ 30,
953
+ )}"
954
+ >
955
+ {port.dataType}
956
+ </span>
957
+ <div class="config-form__port-order-actions">
958
+ <button
959
+ type="button"
960
+ disabled={isRequired}
961
+ title={isRequired
962
+ ? "Required ports cannot be hidden"
963
+ : isHidden
964
+ ? "Show port"
965
+ : "Hide port"}
966
+ class:active={isHidden}
967
+ onclick={() => togglePortHidden("outputs", port.id)}
968
+ >
969
+ <Icon
970
+ icon={isHidden
971
+ ? "heroicons:eye-slash"
972
+ : "heroicons:eye"}
973
+ />
974
+ </button>
975
+ <button
976
+ type="button"
977
+ disabled={i === 0 || allOutputPortsForUI.length === 1}
978
+ onclick={() => movePort("outputs", port.id, -1)}
979
+ title="Move up"
980
+ >
981
+ <Icon icon="heroicons:chevron-up" />
982
+ </button>
983
+ <button
984
+ type="button"
985
+ disabled={i === allOutputPortsForUI.length - 1 ||
986
+ allOutputPortsForUI.length === 1}
987
+ onclick={() => movePort("outputs", port.id, 1)}
988
+ title="Move down"
989
+ >
990
+ <Icon icon="heroicons:chevron-down" />
991
+ </button>
992
+ </div>
993
+ </li>
994
+ {/each}
995
+ </ul>
996
+ </div>
997
+ {/if}
711
998
  </div>
712
999
  </div>
713
1000
  {/if}
@@ -928,6 +1215,140 @@
928
1215
  gap: var(--fd-space-xl);
929
1216
  }
930
1217
 
1218
+ /* ============================================
1219
+ PORT ORDER & VISIBILITY
1220
+ ============================================ */
1221
+
1222
+ .config-form__port-order {
1223
+ border-top: 1px solid var(--fd-border-muted);
1224
+ padding-top: var(--fd-space-md);
1225
+ margin-top: calc(var(--fd-space-xl) * -0.25);
1226
+ }
1227
+
1228
+ .config-form__port-order-header {
1229
+ display: flex;
1230
+ align-items: center;
1231
+ justify-content: space-between;
1232
+ margin-bottom: var(--fd-space-xs);
1233
+ }
1234
+
1235
+ .config-form__port-order-label {
1236
+ font-size: var(--fd-text-xs);
1237
+ font-weight: 600;
1238
+ color: var(--fd-muted-foreground);
1239
+ text-transform: uppercase;
1240
+ letter-spacing: 0.05em;
1241
+ }
1242
+
1243
+ .config-form__port-order-reset {
1244
+ background: none;
1245
+ border: none;
1246
+ font-size: var(--fd-text-xs);
1247
+ color: var(--fd-muted-foreground);
1248
+ cursor: pointer;
1249
+ display: inline-flex;
1250
+ align-items: center;
1251
+ gap: var(--fd-space-3xs);
1252
+ padding: 0;
1253
+ transition: color var(--fd-transition-fast);
1254
+ }
1255
+
1256
+ .config-form__port-order-reset:hover {
1257
+ color: var(--fd-foreground);
1258
+ }
1259
+
1260
+ .config-form__port-order-reset :global(svg) {
1261
+ width: 0.75rem;
1262
+ height: 0.75rem;
1263
+ }
1264
+
1265
+ .config-form__port-order-list {
1266
+ list-style: none;
1267
+ margin: 0;
1268
+ padding: 0;
1269
+ display: flex;
1270
+ flex-direction: column;
1271
+ gap: var(--fd-space-3xs);
1272
+ }
1273
+
1274
+ .config-form__port-order-item {
1275
+ display: flex;
1276
+ align-items: center;
1277
+ gap: var(--fd-space-xs);
1278
+ padding: var(--fd-space-3xs) var(--fd-space-xs);
1279
+ background: var(--fd-muted);
1280
+ border-radius: var(--fd-radius-sm);
1281
+ border: 1px solid var(--fd-border-muted);
1282
+ transition: opacity var(--fd-transition-fast);
1283
+ }
1284
+
1285
+ .config-form__port-order-item--hidden {
1286
+ opacity: 0.4;
1287
+ }
1288
+
1289
+ .config-form__port-order-name {
1290
+ flex: 1;
1291
+ font-size: var(--fd-text-xs);
1292
+ font-weight: 500;
1293
+ color: var(--fd-foreground);
1294
+ min-width: 0;
1295
+ overflow: hidden;
1296
+ text-overflow: ellipsis;
1297
+ white-space: nowrap;
1298
+ }
1299
+
1300
+ .config-form__port-order-badge {
1301
+ padding: 0.125rem var(--fd-space-3xs);
1302
+ border-radius: var(--fd-radius-sm);
1303
+ font-size: 0.625rem;
1304
+ font-weight: 500;
1305
+ text-transform: uppercase;
1306
+ letter-spacing: 0.04em;
1307
+ flex-shrink: 0;
1308
+ }
1309
+
1310
+ .config-form__port-order-actions {
1311
+ display: flex;
1312
+ gap: var(--fd-space-3xs);
1313
+ flex-shrink: 0;
1314
+ }
1315
+
1316
+ .config-form__port-order-actions button {
1317
+ width: 1.25rem;
1318
+ height: 1.25rem;
1319
+ display: flex;
1320
+ align-items: center;
1321
+ justify-content: center;
1322
+ background: var(--fd-card);
1323
+ border: 1px solid var(--fd-border);
1324
+ border-radius: var(--fd-radius-sm);
1325
+ color: var(--fd-muted-foreground);
1326
+ cursor: pointer;
1327
+ padding: 0;
1328
+ transition: all var(--fd-transition-fast);
1329
+ }
1330
+
1331
+ .config-form__port-order-actions button:hover:not(:disabled) {
1332
+ background: var(--fd-backdrop);
1333
+ color: var(--fd-foreground);
1334
+ border-color: var(--fd-border-strong);
1335
+ }
1336
+
1337
+ .config-form__port-order-actions button:disabled {
1338
+ opacity: 0.3;
1339
+ cursor: not-allowed;
1340
+ }
1341
+
1342
+ .config-form__port-order-actions button.active {
1343
+ color: var(--fd-foreground);
1344
+ border-color: var(--fd-border-strong);
1345
+ }
1346
+
1347
+ .config-form__port-order-actions button :global(svg) {
1348
+ width: 0.75rem;
1349
+ height: 0.75rem;
1350
+ }
1351
+
931
1352
  /* ============================================
932
1353
  DEBUG SECTION
933
1354
  ============================================ */
@@ -276,10 +276,22 @@
276
276
  </div>
277
277
  {:else}
278
278
  <div class="flowdrop-hero__icon">
279
- <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
280
- <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
281
- <polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
282
- <line x1="12" y1="22.08" x2="12" y2="12"/>
279
+ <svg
280
+ xmlns="http://www.w3.org/2000/svg"
281
+ width="1em"
282
+ height="1em"
283
+ viewBox="0 0 24 24"
284
+ fill="none"
285
+ stroke="currentColor"
286
+ stroke-width="1.5"
287
+ stroke-linecap="round"
288
+ stroke-linejoin="round"
289
+ >
290
+ <path
291
+ d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"
292
+ />
293
+ <polyline points="3.27 6.96 12 12.01 20.73 6.96" />
294
+ <line x1="12" y1="22.08" x2="12" y2="12" />
283
295
  </svg>
284
296
  </div>
285
297
  <h3 class="flowdrop-hero__title">No node types available</h3>
@@ -299,9 +311,19 @@
299
311
  <div class="flowdrop-hero">
300
312
  <div class="flowdrop-hero__content">
301
313
  <div class="flowdrop-hero__icon">
302
- <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
303
- <circle cx="11" cy="11" r="8"/>
304
- <line x1="21" y1="21" x2="16.65" y2="16.65"/>
314
+ <svg
315
+ xmlns="http://www.w3.org/2000/svg"
316
+ width="1em"
317
+ height="1em"
318
+ viewBox="0 0 24 24"
319
+ fill="none"
320
+ stroke="currentColor"
321
+ stroke-width="1.5"
322
+ stroke-linecap="round"
323
+ stroke-linejoin="round"
324
+ >
325
+ <circle cx="11" cy="11" r="8" />
326
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
305
327
  </svg>
306
328
  </div>
307
329
  <h3 class="flowdrop-hero__title">No components found</h3>
@@ -859,12 +859,22 @@
859
859
  description="Use the sidebar to add components to your workflow"
860
860
  >
861
861
  {#snippet icon()}
862
- <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
863
- <circle cx="18" cy="5" r="3"/>
864
- <circle cx="6" cy="12" r="3"/>
865
- <circle cx="18" cy="19" r="3"/>
866
- <line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/>
867
- <line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
862
+ <svg
863
+ xmlns="http://www.w3.org/2000/svg"
864
+ width="1em"
865
+ height="1em"
866
+ viewBox="0 0 24 24"
867
+ fill="none"
868
+ stroke="currentColor"
869
+ stroke-width="1.5"
870
+ stroke-linecap="round"
871
+ stroke-linejoin="round"
872
+ >
873
+ <circle cx="18" cy="5" r="3" />
874
+ <circle cx="6" cy="12" r="3" />
875
+ <circle cx="18" cy="19" r="3" />
876
+ <line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
877
+ <line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
868
878
  </svg>
869
879
  {/snippet}
870
880
  </CanvasBanner>
@@ -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