@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.
- package/README.md +2 -2
- package/dist/components/App.svelte +6 -0
- package/dist/components/ConfigForm.svelte +56 -22
- package/dist/components/ConfigForm.svelte.d.ts +11 -1
- package/dist/components/Navbar.svelte +6 -7
- package/dist/components/SchemaForm.svelte +2 -10
- package/dist/components/SettingsPanel.svelte +5 -2
- package/dist/components/WorkflowEditor.svelte +158 -4
- package/dist/components/WorkflowEditor.svelte.d.ts +1 -0
- package/dist/components/form/FormAutocomplete.svelte +5 -9
- package/dist/components/form/FormCheckboxGroup.svelte +11 -1
- package/dist/components/form/FormCheckboxGroup.svelte.d.ts +2 -0
- package/dist/components/form/FormCodeEditor.svelte +16 -7
- package/dist/components/form/FormCodeEditor.svelte.d.ts +2 -0
- package/dist/components/form/FormField.svelte +33 -12
- package/dist/components/form/FormFieldLight.svelte +16 -12
- package/dist/components/form/FormMarkdownEditor.svelte +29 -19
- package/dist/components/form/FormMarkdownEditor.svelte.d.ts +2 -0
- package/dist/components/form/FormNumberField.svelte +4 -0
- package/dist/components/form/FormNumberField.svelte.d.ts +2 -0
- package/dist/components/form/FormRangeField.svelte +4 -0
- package/dist/components/form/FormRangeField.svelte.d.ts +2 -0
- package/dist/components/form/FormSelect.svelte +4 -0
- package/dist/components/form/FormSelect.svelte.d.ts +2 -0
- package/dist/components/form/FormTemplateEditor.svelte +140 -17
- package/dist/components/form/FormTemplateEditor.svelte.d.ts +19 -1
- package/dist/components/form/FormTextField.svelte +4 -0
- package/dist/components/form/FormTextField.svelte.d.ts +2 -0
- package/dist/components/form/FormTextarea.svelte +4 -0
- package/dist/components/form/FormTextarea.svelte.d.ts +2 -0
- package/dist/components/form/FormToggle.svelte +4 -0
- package/dist/components/form/FormToggle.svelte.d.ts +2 -0
- package/dist/components/form/index.d.ts +1 -0
- package/dist/components/form/index.js +2 -0
- package/dist/components/form/templateAutocomplete.d.ts +38 -0
- package/dist/components/form/templateAutocomplete.js +309 -0
- package/dist/components/form/types.d.ts +39 -2
- package/dist/components/form/types.js +1 -1
- package/dist/components/layouts/MainLayout.svelte +5 -2
- package/dist/components/nodes/GatewayNode.svelte +0 -8
- package/dist/components/nodes/SimpleNode.svelte +2 -3
- package/dist/components/nodes/WorkflowNode.svelte +0 -8
- package/dist/components/playground/Playground.svelte +43 -38
- package/dist/editor/index.d.ts +3 -1
- package/dist/editor/index.js +5 -1
- package/dist/helpers/workflowEditorHelper.js +1 -2
- package/dist/registry/nodeComponentRegistry.d.ts +9 -9
- package/dist/registry/nodeComponentRegistry.js +10 -10
- package/dist/services/autoSaveService.js +5 -5
- package/dist/services/historyService.d.ts +207 -0
- package/dist/services/historyService.js +317 -0
- package/dist/services/settingsService.d.ts +2 -2
- package/dist/services/settingsService.js +15 -21
- package/dist/services/toastService.d.ts +1 -1
- package/dist/services/toastService.js +10 -10
- package/dist/services/variableService.d.ts +100 -0
- package/dist/services/variableService.js +367 -0
- package/dist/stores/historyStore.d.ts +133 -0
- package/dist/stores/historyStore.js +188 -0
- package/dist/stores/settingsStore.d.ts +1 -1
- package/dist/stores/settingsStore.js +40 -42
- package/dist/stores/themeStore.d.ts +2 -2
- package/dist/stores/themeStore.js +30 -32
- package/dist/stores/workflowStore.d.ts +52 -2
- package/dist/stores/workflowStore.js +102 -2
- package/dist/styles/base.css +28 -8
- package/dist/styles/toast.css +3 -1
- package/dist/styles/tokens.css +2 -2
- package/dist/types/index.d.ts +120 -0
- package/dist/types/settings.d.ts +3 -3
- package/dist/types/settings.js +13 -19
- package/dist/utils/colors.js +17 -17
- package/dist/utils/nodeTypes.d.ts +15 -10
- package/dist/utils/nodeTypes.js +24 -22
- 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
|
-
|
|
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:
|
|
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:
|
|
709
|
-
padding:
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
403
|
+
if (!confirmed) {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
342
406
|
}
|
|
343
407
|
|
|
344
|
-
//
|
|
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 {
|
|
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">
|
|
@@ -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
|
-
|
|
182
|
-
|
|
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 */
|