@d34dman/flowdrop 0.0.44 → 0.0.46
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/ConfigForm.svelte +4 -20
- package/dist/components/Navbar.svelte +6 -7
- package/dist/components/NodeSidebar.svelte +6 -2
- package/dist/components/SchemaForm.svelte +2 -10
- package/dist/components/WorkflowEditor.svelte +143 -13
- 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 +20 -1
- 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 +16 -7
- package/dist/components/form/FormTemplateEditor.svelte.d.ts +2 -0
- 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/types.d.ts +5 -0
- package/dist/components/form/types.js +1 -1
- package/dist/components/layouts/MainLayout.svelte +5 -2
- package/dist/components/nodes/GatewayNode.svelte +99 -86
- package/dist/components/nodes/IdeaNode.svelte +20 -35
- package/dist/components/nodes/NotesNode.svelte +6 -2
- package/dist/components/nodes/SimpleNode.svelte +32 -31
- package/dist/components/nodes/SquareNode.svelte +35 -45
- package/dist/components/nodes/TerminalNode.svelte +25 -61
- package/dist/components/nodes/ToolNode.svelte +36 -18
- package/dist/components/nodes/WorkflowNode.svelte +97 -73
- 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/nodeLayoutHelper.d.ts +14 -0
- package/dist/helpers/nodeLayoutHelper.js +19 -0
- package/dist/helpers/workflowEditorHelper.js +1 -2
- 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/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 +67 -7
- package/dist/styles/toast.css +3 -1
- package/dist/styles/tokens.css +38 -2
- package/dist/types/settings.d.ts +3 -3
- package/dist/types/settings.js +13 -19
- package/dist/utils/colors.js +18 -18
- 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 |
|
|
@@ -670,11 +670,7 @@
|
|
|
670
670
|
}
|
|
671
671
|
|
|
672
672
|
.config-form__button--primary {
|
|
673
|
-
background: linear-gradient(
|
|
674
|
-
135deg,
|
|
675
|
-
var(--fd-primary) 0%,
|
|
676
|
-
var(--fd-primary-hover) 100%
|
|
677
|
-
);
|
|
673
|
+
background: linear-gradient(135deg, var(--fd-primary) 0%, var(--fd-primary-hover) 100%);
|
|
678
674
|
color: var(--fd-primary-foreground);
|
|
679
675
|
box-shadow:
|
|
680
676
|
0 1px 3px rgba(59, 130, 246, 0.3),
|
|
@@ -682,11 +678,7 @@
|
|
|
682
678
|
}
|
|
683
679
|
|
|
684
680
|
.config-form__button--primary:hover {
|
|
685
|
-
background: linear-gradient(
|
|
686
|
-
135deg,
|
|
687
|
-
var(--fd-primary-hover) 0%,
|
|
688
|
-
var(--fd-primary-hover) 100%
|
|
689
|
-
);
|
|
681
|
+
background: linear-gradient(135deg, var(--fd-primary-hover) 0%, var(--fd-primary-hover) 100%);
|
|
690
682
|
box-shadow:
|
|
691
683
|
0 4px 12px rgba(59, 130, 246, 0.35),
|
|
692
684
|
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
@@ -1006,11 +998,7 @@
|
|
|
1006
998
|
============================================ */
|
|
1007
999
|
|
|
1008
1000
|
.config-form__button--external {
|
|
1009
|
-
background: linear-gradient(
|
|
1010
|
-
135deg,
|
|
1011
|
-
var(--fd-accent) 0%,
|
|
1012
|
-
var(--fd-primary) 100%
|
|
1013
|
-
);
|
|
1001
|
+
background: linear-gradient(135deg, var(--fd-accent) 0%, var(--fd-primary) 100%);
|
|
1014
1002
|
color: var(--fd-accent-foreground);
|
|
1015
1003
|
box-shadow:
|
|
1016
1004
|
0 1px 3px rgba(99, 102, 241, 0.3),
|
|
@@ -1018,11 +1006,7 @@
|
|
|
1018
1006
|
}
|
|
1019
1007
|
|
|
1020
1008
|
.config-form__button--external:hover {
|
|
1021
|
-
background: linear-gradient(
|
|
1022
|
-
135deg,
|
|
1023
|
-
var(--fd-accent-hover) 0%,
|
|
1024
|
-
var(--fd-primary-hover) 100%
|
|
1025
|
-
);
|
|
1009
|
+
background: linear-gradient(135deg, var(--fd-accent-hover) 0%, var(--fd-primary-hover) 100%);
|
|
1026
1010
|
box-shadow:
|
|
1027
1011
|
0 4px 12px rgba(99, 102, 241, 0.35),
|
|
1028
1012
|
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
@@ -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
|
|
|
@@ -598,7 +598,7 @@
|
|
|
598
598
|
width: 2rem;
|
|
599
599
|
height: 2rem;
|
|
600
600
|
border-radius: 0.5rem;
|
|
601
|
-
background: color-mix(in srgb, var(--_icon-color)
|
|
601
|
+
background: color-mix(in srgb, var(--_icon-color) var(--fd-node-icon-bg-opacity), transparent);
|
|
602
602
|
color: var(--fd-node-icon);
|
|
603
603
|
font-size: var(--fd-text-sm);
|
|
604
604
|
display: flex;
|
|
@@ -610,7 +610,11 @@
|
|
|
610
610
|
|
|
611
611
|
.flowdrop-node-item:hover .flowdrop-node-icon,
|
|
612
612
|
.flowdrop-details__summary:hover .flowdrop-node-icon {
|
|
613
|
-
background: color-mix(
|
|
613
|
+
background: color-mix(
|
|
614
|
+
in srgb,
|
|
615
|
+
var(--_icon-color) var(--fd-node-icon-bg-opacity-hover),
|
|
616
|
+
transparent
|
|
617
|
+
);
|
|
614
618
|
transform: scale(1.05);
|
|
615
619
|
}
|
|
616
620
|
|
|
@@ -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);
|
|
@@ -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,
|
|
@@ -39,8 +40,8 @@
|
|
|
39
40
|
} from '../helpers/workflowEditorHelper.js';
|
|
40
41
|
import type { NodeExecutionInfo } from '../types/index.js';
|
|
41
42
|
import { areNodeArraysEqual, areEdgeArraysEqual, throttle } from '../utils/performanceUtils.js';
|
|
42
|
-
import { Toaster } from
|
|
43
|
-
import { flowdropToastOptions, FLOWDROP_TOASTER_CLASS } from
|
|
43
|
+
import { Toaster } from 'svelte-5-french-toast';
|
|
44
|
+
import { flowdropToastOptions, FLOWDROP_TOASTER_CLASS } from '../services/toastService.js';
|
|
44
45
|
|
|
45
46
|
interface Props {
|
|
46
47
|
nodes?: NodeMetadata[];
|
|
@@ -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
|
}
|
|
@@ -450,8 +535,49 @@
|
|
|
450
535
|
function handleEdgeRefreshComplete(): void {
|
|
451
536
|
nodeIdToRefresh = null;
|
|
452
537
|
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Handle keyboard shortcuts for undo/redo
|
|
541
|
+
*
|
|
542
|
+
* - Ctrl+Z (or Cmd+Z on Mac): Undo
|
|
543
|
+
* - Ctrl+Shift+Z (or Cmd+Shift+Z): Redo
|
|
544
|
+
* - Ctrl+Y (or Cmd+Y): Redo (Windows convention)
|
|
545
|
+
*/
|
|
546
|
+
function handleKeydown(event: KeyboardEvent): void {
|
|
547
|
+
// Check for Ctrl (Windows/Linux) or Cmd (Mac)
|
|
548
|
+
const isModifierPressed = event.ctrlKey || event.metaKey;
|
|
549
|
+
|
|
550
|
+
if (!isModifierPressed) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Don't handle shortcuts if user is typing in an input, textarea, or contenteditable
|
|
555
|
+
const target = event.target as HTMLElement;
|
|
556
|
+
const isInputElement =
|
|
557
|
+
target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable;
|
|
558
|
+
|
|
559
|
+
if (isInputElement) {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Undo: Ctrl+Z (without Shift)
|
|
564
|
+
if (event.key === 'z' && !event.shiftKey) {
|
|
565
|
+
event.preventDefault();
|
|
566
|
+
historyActions.undo();
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Redo: Ctrl+Shift+Z or Ctrl+Y
|
|
571
|
+
if ((event.key === 'z' && event.shiftKey) || event.key === 'y') {
|
|
572
|
+
event.preventDefault();
|
|
573
|
+
historyActions.redo();
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
453
577
|
</script>
|
|
454
578
|
|
|
579
|
+
<svelte:window onkeydown={handleKeydown} />
|
|
580
|
+
|
|
455
581
|
<SvelteFlowProvider>
|
|
456
582
|
<!-- EdgeRefresher component - handles updateNodeInternals calls -->
|
|
457
583
|
<EdgeRefresher {nodeIdToRefresh} onRefreshComplete={handleEdgeRefreshComplete} />
|
|
@@ -471,6 +597,8 @@
|
|
|
471
597
|
onconnect={handleConnect}
|
|
472
598
|
onbeforedelete={handleBeforeDelete}
|
|
473
599
|
ondelete={handleNodesDelete}
|
|
600
|
+
onnodedragstart={handleNodeDragStart}
|
|
601
|
+
onnodedragstop={handleNodeDragStop}
|
|
474
602
|
minZoom={0.2}
|
|
475
603
|
maxZoom={3}
|
|
476
604
|
clickConnect={true}
|
|
@@ -530,10 +658,10 @@
|
|
|
530
658
|
|
|
531
659
|
<!-- Toast notifications container -->
|
|
532
660
|
<Toaster
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
661
|
+
position="bottom-center"
|
|
662
|
+
containerClassName={FLOWDROP_TOASTER_CLASS}
|
|
663
|
+
toastOptions={flowdropToastOptions}
|
|
664
|
+
/>
|
|
537
665
|
|
|
538
666
|
<style>
|
|
539
667
|
.flowdrop-workflow-editor {
|
|
@@ -625,10 +753,10 @@
|
|
|
625
753
|
fill: currentColor;
|
|
626
754
|
}
|
|
627
755
|
|
|
756
|
+
/* Handle size/position only; colors come from inline --fd-handle-fill and base.css ::before */
|
|
628
757
|
:global(.flowdrop-workflow-editor .svelte-flow__handle) {
|
|
629
|
-
width:
|
|
630
|
-
height:
|
|
631
|
-
border: 2px solid white;
|
|
758
|
+
width: var(--fd-handle-size);
|
|
759
|
+
height: var(--fd-handle-size);
|
|
632
760
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
633
761
|
z-index: 20;
|
|
634
762
|
}
|
|
@@ -637,6 +765,8 @@
|
|
|
637
765
|
:global(.flowdrop-workflow-editor .svelte-flow__handle) {
|
|
638
766
|
pointer-events: all;
|
|
639
767
|
cursor: crosshair;
|
|
768
|
+
background-color: var(--fd-handle-fill);
|
|
769
|
+
border-color: var(--fd-handle-border-color);
|
|
640
770
|
}
|
|
641
771
|
|
|
642
772
|
/**
|
|
@@ -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 */
|