@d34dman/flowdrop 0.0.45 → 0.0.47

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.
Files changed (75) hide show
  1. package/README.md +2 -2
  2. package/dist/components/App.svelte +6 -0
  3. package/dist/components/ConfigForm.svelte +56 -22
  4. package/dist/components/ConfigForm.svelte.d.ts +11 -1
  5. package/dist/components/Navbar.svelte +6 -7
  6. package/dist/components/SchemaForm.svelte +2 -10
  7. package/dist/components/SettingsPanel.svelte +5 -2
  8. package/dist/components/WorkflowEditor.svelte +158 -4
  9. package/dist/components/WorkflowEditor.svelte.d.ts +1 -0
  10. package/dist/components/form/FormAutocomplete.svelte +5 -9
  11. package/dist/components/form/FormCheckboxGroup.svelte +11 -1
  12. package/dist/components/form/FormCheckboxGroup.svelte.d.ts +2 -0
  13. package/dist/components/form/FormCodeEditor.svelte +16 -7
  14. package/dist/components/form/FormCodeEditor.svelte.d.ts +2 -0
  15. package/dist/components/form/FormField.svelte +33 -12
  16. package/dist/components/form/FormFieldLight.svelte +16 -12
  17. package/dist/components/form/FormMarkdownEditor.svelte +29 -19
  18. package/dist/components/form/FormMarkdownEditor.svelte.d.ts +2 -0
  19. package/dist/components/form/FormNumberField.svelte +4 -0
  20. package/dist/components/form/FormNumberField.svelte.d.ts +2 -0
  21. package/dist/components/form/FormRangeField.svelte +4 -0
  22. package/dist/components/form/FormRangeField.svelte.d.ts +2 -0
  23. package/dist/components/form/FormSelect.svelte +4 -0
  24. package/dist/components/form/FormSelect.svelte.d.ts +2 -0
  25. package/dist/components/form/FormTemplateEditor.svelte +140 -17
  26. package/dist/components/form/FormTemplateEditor.svelte.d.ts +19 -1
  27. package/dist/components/form/FormTextField.svelte +4 -0
  28. package/dist/components/form/FormTextField.svelte.d.ts +2 -0
  29. package/dist/components/form/FormTextarea.svelte +4 -0
  30. package/dist/components/form/FormTextarea.svelte.d.ts +2 -0
  31. package/dist/components/form/FormToggle.svelte +4 -0
  32. package/dist/components/form/FormToggle.svelte.d.ts +2 -0
  33. package/dist/components/form/index.d.ts +1 -0
  34. package/dist/components/form/index.js +2 -0
  35. package/dist/components/form/templateAutocomplete.d.ts +38 -0
  36. package/dist/components/form/templateAutocomplete.js +309 -0
  37. package/dist/components/form/types.d.ts +39 -2
  38. package/dist/components/form/types.js +1 -1
  39. package/dist/components/layouts/MainLayout.svelte +5 -2
  40. package/dist/components/nodes/GatewayNode.svelte +0 -8
  41. package/dist/components/nodes/SimpleNode.svelte +2 -3
  42. package/dist/components/nodes/WorkflowNode.svelte +0 -8
  43. package/dist/components/playground/Playground.svelte +43 -38
  44. package/dist/editor/index.d.ts +3 -1
  45. package/dist/editor/index.js +5 -1
  46. package/dist/helpers/workflowEditorHelper.js +1 -2
  47. package/dist/registry/nodeComponentRegistry.d.ts +9 -9
  48. package/dist/registry/nodeComponentRegistry.js +10 -10
  49. package/dist/services/autoSaveService.js +5 -5
  50. package/dist/services/historyService.d.ts +207 -0
  51. package/dist/services/historyService.js +317 -0
  52. package/dist/services/settingsService.d.ts +2 -2
  53. package/dist/services/settingsService.js +15 -21
  54. package/dist/services/toastService.d.ts +1 -1
  55. package/dist/services/toastService.js +10 -10
  56. package/dist/services/variableService.d.ts +100 -0
  57. package/dist/services/variableService.js +367 -0
  58. package/dist/stores/historyStore.d.ts +133 -0
  59. package/dist/stores/historyStore.js +188 -0
  60. package/dist/stores/settingsStore.d.ts +1 -1
  61. package/dist/stores/settingsStore.js +40 -42
  62. package/dist/stores/themeStore.d.ts +2 -2
  63. package/dist/stores/themeStore.js +30 -32
  64. package/dist/stores/workflowStore.d.ts +52 -2
  65. package/dist/stores/workflowStore.js +102 -2
  66. package/dist/styles/base.css +28 -8
  67. package/dist/styles/toast.css +3 -1
  68. package/dist/styles/tokens.css +2 -2
  69. package/dist/types/index.d.ts +120 -0
  70. package/dist/types/settings.d.ts +3 -3
  71. package/dist/types/settings.js +13 -19
  72. package/dist/utils/colors.js +17 -17
  73. package/dist/utils/nodeTypes.d.ts +15 -10
  74. package/dist/utils/nodeTypes.js +24 -22
  75. package/package.json +1 -1
package/README.md CHANGED
@@ -66,8 +66,8 @@ You get a production-ready workflow UI. You keep full control of everything else
66
66
 
67
67
  ## Features
68
68
 
69
- | | |
70
- | --------------------------- | ------------------------------------------------------------------------- |
69
+ | | |
70
+ | ---------------------------- | ------------------------------------------------------------------------- |
71
71
  | 🎨 **Visual Editor Only** | Pure UI component. No hidden backend, no external dependencies |
72
72
  | 🔐 **You Own Everything** | Your data, your servers, your orchestration logic, your security policies |
73
73
  | 🔌 **Backend Agnostic** | Connect to any API: Drupal, Laravel, Express, FastAPI, or your own |
@@ -756,6 +756,8 @@
756
756
  <ConfigForm
757
757
  node={currentNode}
758
758
  workflowId={$workflowStore?.id}
759
+ workflowNodes={$workflowStore?.nodes}
760
+ workflowEdges={$workflowStore?.edges}
759
761
  onChange={async (updatedConfig, uiExtensions) => {
760
762
  // Sync config changes to workflow immediately on field blur
761
763
  if (selectedNodeId && currentNode) {
@@ -780,6 +782,10 @@
780
782
 
781
783
  workflowActions.updateNode(selectedNodeId, nodeUpdates);
782
784
 
785
+ // Update the local editor state to reflect config changes immediately
786
+ // This is needed for nodeType changes to take effect visually
787
+ workflowEditorRef.updateNodeData(selectedNodeId, updatedData);
788
+
783
789
  // Refresh edge positions in case config changes affect handles
784
790
  await workflowEditorRef.refreshEdgePositions(selectedNodeId);
785
791
  }
@@ -22,6 +22,7 @@
22
22
  import type {
23
23
  ConfigSchema,
24
24
  WorkflowNode,
25
+ WorkflowEdge,
25
26
  NodeUIExtensions,
26
27
  ConfigEditOptions
27
28
  } from '../types/index.js';
@@ -35,6 +36,7 @@
35
36
  type DynamicSchemaResult
36
37
  } from '../services/dynamicSchemaService.js';
37
38
  import { globalSaveWorkflow } from '../services/globalSave.js';
39
+ import { getAvailableVariables } from '../services/variableService.js';
38
40
 
39
41
  interface Props {
40
42
  /** Optional workflow node (if provided, schema and values are derived from it) */
@@ -49,6 +51,16 @@
49
51
  workflowId?: string;
50
52
  /** Whether to also save the workflow when saving config */
51
53
  saveWorkflowWhenSavingConfig?: boolean;
54
+ /**
55
+ * All workflow nodes (used for deriving template variables from connected nodes).
56
+ * When provided along with workflowEdges, enables autocomplete for template fields.
57
+ */
58
+ workflowNodes?: WorkflowNode[];
59
+ /**
60
+ * All workflow edges (used for finding connections to derive template variables).
61
+ * When provided along with workflowNodes, enables autocomplete for template fields.
62
+ */
63
+ workflowEdges?: WorkflowEdge[];
52
64
  /** Callback when any field value changes (fired on blur for immediate sync) */
53
65
  onChange?: (config: Record<string, unknown>, uiExtensions?: NodeUIExtensions) => void;
54
66
  /** Callback when form is saved (includes both config and extensions if enabled) */
@@ -64,6 +76,8 @@
64
76
  showUIExtensions = true,
65
77
  workflowId,
66
78
  saveWorkflowWhenSavingConfig = false,
79
+ workflowNodes = [],
80
+ workflowEdges = [],
67
81
  onChange,
68
82
  onSave,
69
83
  onCancel
@@ -364,10 +378,46 @@
364
378
  }
365
379
 
366
380
  /**
367
- * Convert ConfigProperty to FieldSchema for FormField component
381
+ * Convert ConfigProperty to FieldSchema for FormField component.
382
+ * Processes template fields to inject computed variable schema.
383
+ *
384
+ * For template fields, the `variables` config controls which input ports
385
+ * provide variables for autocomplete.
368
386
  */
369
387
  function toFieldSchema(property: Record<string, unknown>): FieldSchema {
370
- return property as FieldSchema;
388
+ const fieldSchema = property as FieldSchema;
389
+
390
+ // Process template fields to compute variable schema
391
+ if (fieldSchema.format === 'template' && node && workflowNodes.length > 0 && workflowEdges.length > 0) {
392
+ // Get the variables config (may be undefined or partially defined)
393
+ const variablesConfig = fieldSchema.variables;
394
+
395
+ // Compute the variable schema with optional port filtering and port name prefixing
396
+ const computedSchema = getAvailableVariables(node, workflowNodes, workflowEdges, {
397
+ targetPortIds: variablesConfig?.ports,
398
+ includePortName: variablesConfig?.includePortName
399
+ });
400
+
401
+ // Merge computed schema with any pre-defined schema
402
+ const mergedSchema = variablesConfig?.schema
403
+ ? {
404
+ variables: {
405
+ ...computedSchema.variables,
406
+ ...variablesConfig.schema.variables
407
+ }
408
+ }
409
+ : computedSchema;
410
+
411
+ return {
412
+ ...fieldSchema,
413
+ variables: {
414
+ ...variablesConfig,
415
+ schema: mergedSchema
416
+ }
417
+ } as FieldSchema;
418
+ }
419
+
420
+ return fieldSchema;
371
421
  }
372
422
  </script>
373
423
 
@@ -670,11 +720,7 @@
670
720
  }
671
721
 
672
722
  .config-form__button--primary {
673
- background: linear-gradient(
674
- 135deg,
675
- var(--fd-primary) 0%,
676
- var(--fd-primary-hover) 100%
677
- );
723
+ background: linear-gradient(135deg, var(--fd-primary) 0%, var(--fd-primary-hover) 100%);
678
724
  color: var(--fd-primary-foreground);
679
725
  box-shadow:
680
726
  0 1px 3px rgba(59, 130, 246, 0.3),
@@ -682,11 +728,7 @@
682
728
  }
683
729
 
684
730
  .config-form__button--primary:hover {
685
- background: linear-gradient(
686
- 135deg,
687
- var(--fd-primary-hover) 0%,
688
- var(--fd-primary-hover) 100%
689
- );
731
+ background: linear-gradient(135deg, var(--fd-primary-hover) 0%, var(--fd-primary-hover) 100%);
690
732
  box-shadow:
691
733
  0 4px 12px rgba(59, 130, 246, 0.35),
692
734
  inset 0 1px 0 rgba(255, 255, 255, 0.1);
@@ -1006,11 +1048,7 @@
1006
1048
  ============================================ */
1007
1049
 
1008
1050
  .config-form__button--external {
1009
- background: linear-gradient(
1010
- 135deg,
1011
- var(--fd-accent) 0%,
1012
- var(--fd-primary) 100%
1013
- );
1051
+ background: linear-gradient(135deg, var(--fd-accent) 0%, var(--fd-primary) 100%);
1014
1052
  color: var(--fd-accent-foreground);
1015
1053
  box-shadow:
1016
1054
  0 1px 3px rgba(99, 102, 241, 0.3),
@@ -1018,11 +1056,7 @@
1018
1056
  }
1019
1057
 
1020
1058
  .config-form__button--external:hover {
1021
- background: linear-gradient(
1022
- 135deg,
1023
- var(--fd-accent-hover) 0%,
1024
- var(--fd-primary-hover) 100%
1025
- );
1059
+ background: linear-gradient(135deg, var(--fd-accent-hover) 0%, var(--fd-primary-hover) 100%);
1026
1060
  box-shadow:
1027
1061
  0 4px 12px rgba(99, 102, 241, 0.35),
1028
1062
  inset 0 1px 0 rgba(255, 255, 255, 0.1);
@@ -1,4 +1,4 @@
1
- import type { ConfigSchema, WorkflowNode, NodeUIExtensions } from '../types/index.js';
1
+ import type { ConfigSchema, WorkflowNode, WorkflowEdge, NodeUIExtensions } from '../types/index.js';
2
2
  interface Props {
3
3
  /** Optional workflow node (if provided, schema and values are derived from it) */
4
4
  node?: WorkflowNode;
@@ -12,6 +12,16 @@ interface Props {
12
12
  workflowId?: string;
13
13
  /** Whether to also save the workflow when saving config */
14
14
  saveWorkflowWhenSavingConfig?: boolean;
15
+ /**
16
+ * All workflow nodes (used for deriving template variables from connected nodes).
17
+ * When provided along with workflowEdges, enables autocomplete for template fields.
18
+ */
19
+ workflowNodes?: WorkflowNode[];
20
+ /**
21
+ * All workflow edges (used for finding connections to derive template variables).
22
+ * When provided along with workflowNodes, enables autocomplete for template fields.
23
+ */
24
+ workflowEdges?: WorkflowEdge[];
15
25
  /** Callback when any field value changes (fired on blur for immediate sync) */
16
26
  onChange?: (config: Record<string, unknown>, uiExtensions?: NodeUIExtensions) => void;
17
27
  /** Callback when form is saved (includes both config and extensions if enabled) */
@@ -377,12 +377,11 @@
377
377
  }
378
378
 
379
379
  .flowdrop-navbar__status {
380
- display: flex;
380
+ display: inline-flex;
381
381
  align-items: center;
382
382
  gap: 0.375rem;
383
- padding: 0.125rem 0.5rem;
383
+ padding: var(--fd-space-1) var(--fd-space-2);
384
384
  background-color: var(--fd-success-muted);
385
- border: 1px solid var(--fd-success);
386
385
  border-radius: var(--fd-radius-md);
387
386
  font-size: var(--fd-text-xs);
388
387
  font-weight: 500;
@@ -391,13 +390,13 @@
391
390
  .flowdrop-navbar__status-indicator {
392
391
  width: 0.375rem;
393
392
  height: 0.375rem;
394
- background-color: var(--fd-success);
393
+ background-color: var(--fd-success-hover);
395
394
  border-radius: 50%;
396
395
  animation: pulse 2s infinite;
397
396
  }
398
397
 
399
398
  .flowdrop-navbar__status-text {
400
- color: var(--fd-success);
399
+ color: var(--fd-success-hover);
401
400
  font-size: var(--fd-text-xs);
402
401
  font-weight: 500;
403
402
  }
@@ -705,8 +704,8 @@
705
704
  }
706
705
 
707
706
  .flowdrop-navbar__status {
708
- font-size: 0.625rem;
709
- padding: 0.125rem 0.375rem;
707
+ font-size: var(--fd-text-xs);
708
+ padding: var(--fd-space-1) var(--fd-space-2);
710
709
  }
711
710
  }
712
711
 
@@ -427,11 +427,7 @@
427
427
  }
428
428
 
429
429
  .schema-form__button--primary {
430
- background: linear-gradient(
431
- 135deg,
432
- var(--fd-primary) 0%,
433
- var(--fd-primary-hover) 100%
434
- );
430
+ background: linear-gradient(135deg, var(--fd-primary) 0%, var(--fd-primary-hover) 100%);
435
431
  color: var(--fd-primary-foreground);
436
432
  box-shadow:
437
433
  0 1px 3px rgba(59, 130, 246, 0.3),
@@ -439,11 +435,7 @@
439
435
  }
440
436
 
441
437
  .schema-form__button--primary:hover:not(:disabled) {
442
- background: linear-gradient(
443
- 135deg,
444
- var(--fd-primary-hover) 0%,
445
- var(--fd-primary-hover) 100%
446
- );
438
+ background: linear-gradient(135deg, var(--fd-primary-hover) 0%, var(--fd-primary-hover) 100%);
447
439
  box-shadow:
448
440
  0 4px 12px rgba(59, 130, 246, 0.35),
449
441
  inset 0 1px 0 rgba(255, 255, 255, 0.1);
@@ -89,8 +89,11 @@
89
89
  type: 'string',
90
90
  title: 'Theme Preference',
91
91
  description: 'Choose your preferred color scheme',
92
- enum: ['light', 'dark', 'auto'],
93
- enumLabels: ['Light', 'Dark', 'Auto (System)'],
92
+ oneOf: [
93
+ { const: 'light', title: 'Light' },
94
+ { const: 'dark', title: 'Dark' },
95
+ { const: 'auto', title: 'Auto (System)' }
96
+ ],
94
97
  default: 'auto'
95
98
  }
96
99
  }
@@ -30,6 +30,7 @@
30
30
  import type { EndpointConfig } from '../config/endpoints.js';
31
31
  import ConnectionLine from './ConnectionLine.svelte';
32
32
  import { workflowStore, workflowActions } from '../stores/workflowStore.js';
33
+ import { historyActions, setOnRestoreCallback } from '../stores/historyStore.js';
33
34
  import UniversalNode from './UniversalNode.svelte';
34
35
  import {
35
36
  EdgeStylingHelper,
@@ -65,13 +66,45 @@
65
66
  // Create a local currentWorkflow variable that we can control directly
66
67
  let currentWorkflow = $state<Workflow | null>(null);
67
68
 
69
+ // Track if we're currently dragging a node (for history debouncing)
70
+ let isDraggingNode = $state(false);
71
+
72
+ // Track the workflow ID we're currently editing to detect workflow switches
73
+ let currentWorkflowId: string | null = null;
74
+
68
75
  // Initialize currentWorkflow from global store
76
+ // Only sync when workflow ID changes (new workflow loaded) or on initial load
69
77
  $effect(() => {
70
78
  if ($workflowStore) {
71
- currentWorkflow = $workflowStore;
79
+ const storeWorkflowId = $workflowStore.id;
80
+
81
+ // Sync on initial load or when a different workflow is loaded
82
+ if (currentWorkflowId !== storeWorkflowId) {
83
+ currentWorkflow = $workflowStore;
84
+ currentWorkflowId = storeWorkflowId;
85
+ }
86
+ } else if (currentWorkflow !== null) {
87
+ // Store was cleared
88
+ currentWorkflow = null;
89
+ currentWorkflowId = null;
72
90
  }
73
91
  });
74
92
 
93
+ // Set up the history restore callback to update workflow when undo/redo is triggered
94
+ $effect(() => {
95
+ setOnRestoreCallback((restoredWorkflow: Workflow) => {
96
+ // Directly update local state (bypass store sync effect)
97
+ currentWorkflow = restoredWorkflow;
98
+ // Also update the store without triggering history
99
+ workflowActions.restoreFromHistory(restoredWorkflow);
100
+ });
101
+
102
+ // Cleanup on unmount
103
+ return () => {
104
+ setOnRestoreCallback(null);
105
+ };
106
+ });
107
+
75
108
  // Create local reactive variables that sync with currentWorkflow
76
109
  let flowNodes = $state<WorkflowNodeType[]>([]);
77
110
  let flowEdges = $state<WorkflowEdge[]>([]);
@@ -282,6 +315,29 @@
282
315
  // Handle arrows in our custom connection handler
283
316
  const defaultEdgeOptions = {};
284
317
 
318
+ /**
319
+ * Handle node drag start
320
+ *
321
+ * Marks the beginning of a drag operation.
322
+ */
323
+ function handleNodeDragStart(): void {
324
+ isDraggingNode = true;
325
+ }
326
+
327
+ /**
328
+ * Handle node drag stop
329
+ *
330
+ * Push the NEW state (after drag) to history.
331
+ * Undo will then restore to the previous state (before drag).
332
+ */
333
+ function handleNodeDragStop(): void {
334
+ isDraggingNode = false;
335
+ // Push the current state AFTER the drag completed
336
+ if (currentWorkflow) {
337
+ workflowActions.pushHistory('Move node', currentWorkflow);
338
+ }
339
+ }
340
+
285
341
  /**
286
342
  * Handle new connections between nodes
287
343
  * Let SvelteFlow handle edge creation, styling will be applied via reactive effects
@@ -303,6 +359,12 @@
303
359
  if (currentWorkflow) {
304
360
  updateCurrentWorkflowFromSvelteFlow();
305
361
  }
362
+
363
+ // Push to history AFTER the connection is made
364
+ // This way undo will restore to the state before the connection
365
+ if (currentWorkflow) {
366
+ workflowActions.pushHistory('Add connection', currentWorkflow);
367
+ }
306
368
  }
307
369
 
308
370
  /**
@@ -338,15 +400,18 @@
338
400
 
339
401
  // Show native confirmation dialog
340
402
  const confirmed = window.confirm(message);
341
- return confirmed;
403
+ if (!confirmed) {
404
+ return false;
405
+ }
342
406
  }
343
407
 
344
- // If confirmDelete is disabled, proceed with deletion
408
+ // Don't push to history here - we'll push AFTER deletion in handleNodesDelete
409
+ // This ensures undo will restore the state before deletion
345
410
  return true;
346
411
  }
347
412
 
348
413
  /**
349
- * Handle node deletion - automatically remove connected edges
414
+ * Handle node deletion - automatically remove connected edges and push to history
350
415
  */
351
416
  function handleNodesDelete(params: { nodes: WorkflowNodeType[]; edges: WorkflowEdge[] }): void {
352
417
  const deletedNodeIds = new Set(params.nodes.map((node) => node.id));
@@ -360,6 +425,21 @@
360
425
  if (currentWorkflow) {
361
426
  updateCurrentWorkflowFromSvelteFlow();
362
427
  }
428
+
429
+ // Push to history AFTER the deletion so undo restores the previous state
430
+ const nodeCount = params.nodes.length;
431
+ const edgeCount = params.edges.length;
432
+ let description = 'Delete';
433
+ if (nodeCount > 0 && edgeCount > 0) {
434
+ description = `Delete ${nodeCount} node${nodeCount > 1 ? 's' : ''} and ${edgeCount} connection${edgeCount > 1 ? 's' : ''}`;
435
+ } else if (nodeCount > 0) {
436
+ description = `Delete ${nodeCount} node${nodeCount > 1 ? 's' : ''}`;
437
+ } else if (edgeCount > 0) {
438
+ description = `Delete ${edgeCount} connection${edgeCount > 1 ? 's' : ''}`;
439
+ }
440
+ if (currentWorkflow) {
441
+ workflowActions.pushHistory(description, currentWorkflow);
442
+ }
363
443
  }
364
444
 
365
445
  /**
@@ -413,6 +493,7 @@
413
493
  const newNode = NodeOperationsHelper.createNodeFromDrop(nodeTypeData, position, flowNodes);
414
494
 
415
495
  if (newNode && currentWorkflow) {
496
+ // Add the node first
416
497
  currentWorkflow = WorkflowOperationsHelper.addNode(currentWorkflow, newNode);
417
498
 
418
499
  // Update the global store
@@ -420,6 +501,10 @@
420
501
 
421
502
  // Wait for DOM update to ensure SvelteFlow updates
422
503
  await tick();
504
+
505
+ // Push to history AFTER adding the node
506
+ // This way undo will restore to the state before the add
507
+ workflowActions.pushHistory('Add node', currentWorkflow);
423
508
  } else if (!currentWorkflow) {
424
509
  console.warn('No currentWorkflow available for new node');
425
510
  }
@@ -430,6 +515,32 @@
430
515
  */
431
516
  let nodeIdToRefresh = $state<string | null>(null);
432
517
 
518
+ /**
519
+ * Update a node's data in the local editor state.
520
+ * This should be called after updating the node in the global store to ensure
521
+ * the visual representation is updated immediately (e.g., for nodeType changes).
522
+ *
523
+ * @param nodeId - The ID of the node to update
524
+ * @param dataUpdates - Partial data updates to merge into the node's data
525
+ */
526
+ export function updateNodeData(
527
+ nodeId: string,
528
+ dataUpdates: Partial<WorkflowNodeType['data']>
529
+ ): void {
530
+ flowNodes = flowNodes.map((node) => {
531
+ if (node.id === nodeId) {
532
+ return {
533
+ ...node,
534
+ data: {
535
+ ...node.data,
536
+ ...dataUpdates
537
+ }
538
+ };
539
+ }
540
+ return node;
541
+ });
542
+ }
543
+
433
544
  /**
434
545
  * Force edge position recalculation after node config changes
435
546
  * This should be called after saving gateway/switch node configs where branches are reordered
@@ -450,8 +561,49 @@
450
561
  function handleEdgeRefreshComplete(): void {
451
562
  nodeIdToRefresh = null;
452
563
  }
564
+
565
+ /**
566
+ * Handle keyboard shortcuts for undo/redo
567
+ *
568
+ * - Ctrl+Z (or Cmd+Z on Mac): Undo
569
+ * - Ctrl+Shift+Z (or Cmd+Shift+Z): Redo
570
+ * - Ctrl+Y (or Cmd+Y): Redo (Windows convention)
571
+ */
572
+ function handleKeydown(event: KeyboardEvent): void {
573
+ // Check for Ctrl (Windows/Linux) or Cmd (Mac)
574
+ const isModifierPressed = event.ctrlKey || event.metaKey;
575
+
576
+ if (!isModifierPressed) {
577
+ return;
578
+ }
579
+
580
+ // Don't handle shortcuts if user is typing in an input, textarea, or contenteditable
581
+ const target = event.target as HTMLElement;
582
+ const isInputElement =
583
+ target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable;
584
+
585
+ if (isInputElement) {
586
+ return;
587
+ }
588
+
589
+ // Undo: Ctrl+Z (without Shift)
590
+ if (event.key === 'z' && !event.shiftKey) {
591
+ event.preventDefault();
592
+ historyActions.undo();
593
+ return;
594
+ }
595
+
596
+ // Redo: Ctrl+Shift+Z or Ctrl+Y
597
+ if ((event.key === 'z' && event.shiftKey) || event.key === 'y') {
598
+ event.preventDefault();
599
+ historyActions.redo();
600
+ return;
601
+ }
602
+ }
453
603
  </script>
454
604
 
605
+ <svelte:window onkeydown={handleKeydown} />
606
+
455
607
  <SvelteFlowProvider>
456
608
  <!-- EdgeRefresher component - handles updateNodeInternals calls -->
457
609
  <EdgeRefresher {nodeIdToRefresh} onRefreshComplete={handleEdgeRefreshComplete} />
@@ -471,6 +623,8 @@
471
623
  onconnect={handleConnect}
472
624
  onbeforedelete={handleBeforeDelete}
473
625
  ondelete={handleNodesDelete}
626
+ onnodedragstart={handleNodeDragStart}
627
+ onnodedragstop={handleNodeDragStop}
474
628
  minZoom={0.2}
475
629
  maxZoom={3}
476
630
  clickConnect={true}
@@ -16,6 +16,7 @@ interface Props {
16
16
  pipelineId?: string;
17
17
  }
18
18
  declare const WorkflowEditor: import("svelte").Component<Props, {
19
+ updateNodeData: (nodeId: string, dataUpdates: Partial<WorkflowNodeType["data"]>) => void;
19
20
  refreshEdgePositions: (nodeId: string) => Promise<void>;
20
21
  }, "">;
21
22
  type WorkflowEditor = ReturnType<typeof WorkflowEditor>;
@@ -262,7 +262,7 @@
262
262
  function handleInput(event: Event): void {
263
263
  const target = event.currentTarget as HTMLInputElement;
264
264
  inputValue = target.value;
265
-
265
+
266
266
  // Open dropdown
267
267
  showDropdown();
268
268
 
@@ -493,9 +493,9 @@
493
493
  */
494
494
  function showDropdown(): void {
495
495
  if (!popoverElement || disabled) return;
496
-
496
+
497
497
  updatePopoverPosition();
498
-
498
+
499
499
  try {
500
500
  popoverElement.showPopover();
501
501
  isOpen = true;
@@ -510,7 +510,7 @@
510
510
  */
511
511
  function hideDropdown(): void {
512
512
  if (!popoverElement) return;
513
-
513
+
514
514
  try {
515
515
  popoverElement.hidePopover();
516
516
  } catch {
@@ -658,11 +658,7 @@
658
658
  style={popoverStyle}
659
659
  onmousedown={(e) => e.preventDefault()}
660
660
  >
661
- <ul
662
- class="form-autocomplete__listbox"
663
- role="listbox"
664
- aria-label="Suggestions"
665
- >
661
+ <ul class="form-autocomplete__listbox" role="listbox" aria-label="Suggestions">
666
662
  {#if isLoading}
667
663
  <li class="form-autocomplete__status form-autocomplete__status--loading">
668
664
  <Icon icon="heroicons:arrow-path" class="form-autocomplete__status-icon" />
@@ -18,13 +18,22 @@
18
18
  value: string[];
19
19
  /** Available options */
20
20
  options: string[];
21
+ /** Whether the field is disabled (read-only) */
22
+ disabled?: boolean;
21
23
  /** ARIA description ID */
22
24
  ariaDescribedBy?: string;
23
25
  /** Callback when value changes */
24
26
  onChange: (value: string[]) => void;
25
27
  }
26
28
 
27
- let { id, value = [], options = [], ariaDescribedBy, onChange }: Props = $props();
29
+ let {
30
+ id,
31
+ value = [],
32
+ options = [],
33
+ disabled = false,
34
+ ariaDescribedBy,
35
+ onChange
36
+ }: Props = $props();
28
37
 
29
38
  /**
30
39
  * Handle checkbox toggle
@@ -56,6 +65,7 @@
56
65
  class="form-checkbox__input"
57
66
  value={option}
58
67
  checked={isChecked}
68
+ {disabled}
59
69
  onchange={(e) => handleCheckboxChange(option, e.currentTarget.checked)}
60
70
  />
61
71
  <span class="form-checkbox__custom" aria-hidden="true">
@@ -5,6 +5,8 @@ interface Props {
5
5
  value: string[];
6
6
  /** Available options */
7
7
  options: string[];
8
+ /** Whether the field is disabled (read-only) */
9
+ disabled?: boolean;
8
10
  /** ARIA description ID */
9
11
  ariaDescribedBy?: string;
10
12
  /** Callback when value changes */
@@ -47,6 +47,8 @@
47
47
  height?: string;
48
48
  /** Whether to auto-format JSON on blur */
49
49
  autoFormat?: boolean;
50
+ /** Whether the field is disabled (read-only) */
51
+ disabled?: boolean;
50
52
  /** ARIA description ID */
51
53
  ariaDescribedBy?: string;
52
54
  /** Callback when value changes */
@@ -61,6 +63,7 @@
61
63
  darkTheme = false,
62
64
  height = '200px',
63
65
  autoFormat = true,
66
+ disabled = false,
64
67
  ariaDescribedBy,
65
68
  onChange
66
69
  }: Props = $props();
@@ -167,6 +170,7 @@
167
170
  /**
168
171
  * Create editor extensions array
169
172
  * Uses minimal setup for better performance (no auto-closing brackets, no autocompletion)
173
+ * When disabled is true, adds readOnly/editable so the editor cannot be modified
170
174
  */
171
175
  function createExtensions() {
172
176
  const extensions = [
@@ -177,22 +181,27 @@
177
181
  highlightActiveLine(),
178
182
  drawSelection(),
179
183
 
180
- // Editing features
181
- history(),
182
- indentOnInput(),
184
+ // Editing features (skip when read-only)
185
+ ...(disabled
186
+ ? []
187
+ : [
188
+ history(),
189
+ indentOnInput(),
190
+ keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab])
191
+ ]),
192
+
193
+ // Read-only: prevent document changes and mark content as non-editable
194
+ ...(disabled ? [EditorState.readOnly.of(true), EditorView.editable.of(false)] : []),
183
195
 
184
196
  // Syntax highlighting
185
197
  syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
186
198
 
187
- // Keymaps for basic editing
188
- keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]),
189
-
190
199
  // JSON-specific features
191
200
  json(),
192
201
  linter(jsonParseLinter()),
193
202
  lintGutter(),
194
203
 
195
- // Update listener
204
+ // Update listener (only fires on user edit when not disabled)
196
205
  EditorView.updateListener.of(handleUpdate),
197
206
 
198
207
  // Custom theme
@@ -13,6 +13,8 @@ interface Props {
13
13
  height?: string;
14
14
  /** Whether to auto-format JSON on blur */
15
15
  autoFormat?: boolean;
16
+ /** Whether the field is disabled (read-only) */
17
+ disabled?: boolean;
16
18
  /** ARIA description ID */
17
19
  ariaDescribedBy?: string;
18
20
  /** Callback when value changes */