@d34dman/flowdrop 0.0.48 → 0.0.50
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/dist/components/App.svelte +7 -0
- package/dist/components/ConfigForm.svelte +12 -2
- package/dist/components/NodeSidebar.svelte +1 -0
- package/dist/components/SettingsPanel.svelte +3 -3
- package/dist/components/WorkflowEditor.svelte +14 -2
- package/dist/components/form/FormAutocomplete.svelte +3 -1
- package/dist/components/form/FormCodeEditor.svelte +17 -15
- package/dist/components/form/FormField.svelte +3 -2
- package/dist/components/form/FormTemplateEditor.svelte +62 -49
- package/dist/components/form/templateAutocomplete.d.ts +2 -2
- package/dist/components/form/templateAutocomplete.js +49 -48
- package/dist/components/nodes/ToolNode.svelte +4 -6
- package/dist/services/globalSave.js +6 -0
- package/dist/services/variableService.d.ts +1 -1
- package/dist/services/variableService.js +30 -30
- package/dist/svelte-app.d.ts +3 -0
- package/dist/svelte-app.js +6 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/settings.js +4 -4
- package/dist/utils/colors.js +1 -0
- package/dist/utils/icons.js +1 -0
- package/dist/utils/nodeTypes.js +3 -3
- package/package.json +1 -1
|
@@ -393,6 +393,13 @@
|
|
|
393
393
|
* Uses enhanced API client with authProvider support when available.
|
|
394
394
|
*/
|
|
395
395
|
async function saveWorkflow(): Promise<void> {
|
|
396
|
+
// Flush any pending form changes by blurring the active element.
|
|
397
|
+
// This ensures focusout handlers (like ConfigForm's handleFormBlur)
|
|
398
|
+
// sync local state to the global store before we read it.
|
|
399
|
+
if (document.activeElement instanceof HTMLElement) {
|
|
400
|
+
document.activeElement.blur();
|
|
401
|
+
}
|
|
402
|
+
|
|
396
403
|
// Wait for any pending DOM updates before saving
|
|
397
404
|
await tick();
|
|
398
405
|
|
|
@@ -149,7 +149,12 @@
|
|
|
149
149
|
if (!node) return false;
|
|
150
150
|
const staticSchema = schema ?? node.data.metadata?.configSchema;
|
|
151
151
|
// Need to load if: (no static schema OR preferDynamicSchema is true) AND dynamic schema is configured
|
|
152
|
-
return (
|
|
152
|
+
return (
|
|
153
|
+
(!staticSchema || configEditOptions?.preferDynamicSchema === true) &&
|
|
154
|
+
useDynamicSchema &&
|
|
155
|
+
!fetchedDynamicSchema &&
|
|
156
|
+
!dynamicSchemaLoading
|
|
157
|
+
);
|
|
153
158
|
});
|
|
154
159
|
|
|
155
160
|
/**
|
|
@@ -399,7 +404,12 @@
|
|
|
399
404
|
const fieldSchema = property as FieldSchema;
|
|
400
405
|
|
|
401
406
|
// Process template fields to compute variable schema
|
|
402
|
-
if (
|
|
407
|
+
if (
|
|
408
|
+
fieldSchema.format === 'template' &&
|
|
409
|
+
node &&
|
|
410
|
+
workflowNodes.length > 0 &&
|
|
411
|
+
workflowEdges.length > 0
|
|
412
|
+
) {
|
|
403
413
|
// Get the variables config (may be undefined or partially defined)
|
|
404
414
|
const variablesConfig = fieldSchema.variables;
|
|
405
415
|
|
|
@@ -188,10 +188,10 @@
|
|
|
188
188
|
undoHistoryLimit: {
|
|
189
189
|
type: 'number',
|
|
190
190
|
title: 'Undo History Limit',
|
|
191
|
-
description: 'Maximum number of undo steps',
|
|
192
|
-
minimum:
|
|
191
|
+
description: 'Maximum number of undo steps (0 to disable)',
|
|
192
|
+
minimum: 0,
|
|
193
193
|
maximum: 200,
|
|
194
|
-
default:
|
|
194
|
+
default: 0
|
|
195
195
|
},
|
|
196
196
|
confirmDelete: {
|
|
197
197
|
type: 'boolean',
|
|
@@ -72,16 +72,25 @@
|
|
|
72
72
|
// Track the workflow ID we're currently editing to detect workflow switches
|
|
73
73
|
let currentWorkflowId: string | null = null;
|
|
74
74
|
|
|
75
|
+
// Track the last store value written by this editor to distinguish
|
|
76
|
+
// external programmatic changes from our own echoed writes
|
|
77
|
+
let lastEditorStoreValue: Workflow | null = null;
|
|
78
|
+
|
|
75
79
|
// Initialize currentWorkflow from global store
|
|
76
|
-
//
|
|
80
|
+
// Sync on workflow ID change (new workflow loaded) or external programmatic changes
|
|
77
81
|
$effect(() => {
|
|
78
82
|
if ($workflowStore) {
|
|
79
83
|
const storeWorkflowId = $workflowStore.id;
|
|
80
84
|
|
|
81
|
-
// Sync on initial load or when a different workflow is loaded
|
|
82
85
|
if (currentWorkflowId !== storeWorkflowId) {
|
|
86
|
+
// New workflow loaded
|
|
83
87
|
currentWorkflow = $workflowStore;
|
|
84
88
|
currentWorkflowId = storeWorkflowId;
|
|
89
|
+
lastEditorStoreValue = null;
|
|
90
|
+
} else if ($workflowStore !== lastEditorStoreValue) {
|
|
91
|
+
// External programmatic change (e.g. addEdge, updateNode, updateEdges)
|
|
92
|
+
// The store value differs from what this editor last wrote, so sync it
|
|
93
|
+
currentWorkflow = $workflowStore;
|
|
85
94
|
}
|
|
86
95
|
} else if (currentWorkflow !== null) {
|
|
87
96
|
// Store was cleared
|
|
@@ -95,6 +104,8 @@
|
|
|
95
104
|
setOnRestoreCallback((restoredWorkflow: Workflow) => {
|
|
96
105
|
// Directly update local state (bypass store sync effect)
|
|
97
106
|
currentWorkflow = restoredWorkflow;
|
|
107
|
+
// Mark as our own write so sync effect doesn't re-process it
|
|
108
|
+
lastEditorStoreValue = restoredWorkflow;
|
|
98
109
|
// Also update the store without triggering history
|
|
99
110
|
workflowActions.restoreFromHistory(restoredWorkflow);
|
|
100
111
|
});
|
|
@@ -207,6 +218,7 @@
|
|
|
207
218
|
*/
|
|
208
219
|
const updateGlobalStore = throttle((): void => {
|
|
209
220
|
if (currentWorkflow) {
|
|
221
|
+
lastEditorStoreValue = currentWorkflow;
|
|
210
222
|
workflowActions.updateWorkflow(currentWorkflow);
|
|
211
223
|
}
|
|
212
224
|
}, 16);
|
|
@@ -61,7 +61,9 @@
|
|
|
61
61
|
|
|
62
62
|
// Get AuthProvider and baseUrl from context via getter functions
|
|
63
63
|
// This pattern ensures we always get the current value, even if props change after mount
|
|
64
|
-
const getAuthProvider = getContext<(() => AuthProvider | undefined) | undefined>(
|
|
64
|
+
const getAuthProvider = getContext<(() => AuthProvider | undefined) | undefined>(
|
|
65
|
+
'flowdrop:getAuthProvider'
|
|
66
|
+
);
|
|
65
67
|
const getBaseUrl = getContext<(() => string) | undefined>('flowdrop:getBaseUrl');
|
|
66
68
|
|
|
67
69
|
// Configuration with defaults
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
import { EditorState } from '@codemirror/state';
|
|
26
26
|
import { history, historyKeymap } from '@codemirror/commands';
|
|
27
27
|
import { highlightSpecialChars, highlightActiveLine } from '@codemirror/view';
|
|
28
|
-
import { syntaxHighlighting, defaultHighlightStyle
|
|
28
|
+
import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
|
|
29
29
|
import { keymap } from '@codemirror/view';
|
|
30
30
|
import { defaultKeymap, indentWithTab } from '@codemirror/commands';
|
|
31
31
|
import { json, jsonParseLinter } from '@codemirror/lang-json';
|
|
32
32
|
import { oneDark } from '@codemirror/theme-one-dark';
|
|
33
|
-
import { linter
|
|
33
|
+
import { linter } from '@codemirror/lint';
|
|
34
34
|
|
|
35
35
|
interface Props {
|
|
36
36
|
/** Field identifier */
|
|
@@ -80,6 +80,9 @@
|
|
|
80
80
|
/** Flag to prevent update loops */
|
|
81
81
|
let isInternalUpdate = false;
|
|
82
82
|
|
|
83
|
+
/** Flag to skip $effect when change originated from the editor */
|
|
84
|
+
let isEditorUpdate = false;
|
|
85
|
+
|
|
83
86
|
/**
|
|
84
87
|
* Convert value to JSON string for editor display
|
|
85
88
|
*/
|
|
@@ -129,6 +132,7 @@
|
|
|
129
132
|
const content = update.state.doc.toString();
|
|
130
133
|
const result = validateAndParse(content);
|
|
131
134
|
|
|
135
|
+
isEditorUpdate = true;
|
|
132
136
|
if (result.valid) {
|
|
133
137
|
validationError = undefined;
|
|
134
138
|
// Emit the parsed value (object) not the string
|
|
@@ -184,22 +188,17 @@
|
|
|
184
188
|
// Editing features (skip when read-only)
|
|
185
189
|
...(disabled
|
|
186
190
|
? []
|
|
187
|
-
: [
|
|
188
|
-
history(),
|
|
189
|
-
indentOnInput(),
|
|
190
|
-
keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab])
|
|
191
|
-
]),
|
|
191
|
+
: [history(), keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab])]),
|
|
192
192
|
|
|
193
193
|
// Read-only: prevent document changes and mark content as non-editable
|
|
194
194
|
...(disabled ? [EditorState.readOnly.of(true), EditorView.editable.of(false)] : []),
|
|
195
195
|
|
|
196
|
-
// Syntax highlighting
|
|
197
|
-
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
|
196
|
+
// Syntax highlighting - use default for light mode, oneDark handles dark mode
|
|
197
|
+
...(darkTheme ? [oneDark] : [syntaxHighlighting(defaultHighlightStyle, { fallback: true })]),
|
|
198
198
|
|
|
199
199
|
// JSON-specific features
|
|
200
200
|
json(),
|
|
201
|
-
linter(jsonParseLinter()),
|
|
202
|
-
lintGutter(),
|
|
201
|
+
linter(jsonParseLinter(), { delay: 1000 }),
|
|
203
202
|
|
|
204
203
|
// Update listener (only fires on user edit when not disabled)
|
|
205
204
|
EditorView.updateListener.of(handleUpdate),
|
|
@@ -224,10 +223,6 @@
|
|
|
224
223
|
EditorView.lineWrapping
|
|
225
224
|
];
|
|
226
225
|
|
|
227
|
-
if (darkTheme) {
|
|
228
|
-
extensions.push(oneDark);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
226
|
return extensions;
|
|
232
227
|
}
|
|
233
228
|
|
|
@@ -276,6 +271,13 @@
|
|
|
276
271
|
}
|
|
277
272
|
|
|
278
273
|
const newContent = valueToString(value);
|
|
274
|
+
|
|
275
|
+
// Skip if the change originated from the editor itself
|
|
276
|
+
if (isEditorUpdate) {
|
|
277
|
+
isEditorUpdate = false;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
279
281
|
const currentContent = editorView.state.doc.toString();
|
|
280
282
|
|
|
281
283
|
// Only update if content actually changed and wasn't from internal edit
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
import type { FieldSchema } from './types.js';
|
|
45
45
|
import { getSchemaOptions } from './types.js';
|
|
46
46
|
import type { WorkflowNode, WorkflowEdge, AuthProvider } from '../../types/index.js';
|
|
47
|
+
import { resolvedTheme } from '../../stores/settingsStore.js';
|
|
47
48
|
|
|
48
49
|
interface Props {
|
|
49
50
|
/** Unique key/id for the field */
|
|
@@ -345,7 +346,7 @@
|
|
|
345
346
|
placeholder={schema.placeholder ?? '{}'}
|
|
346
347
|
{required}
|
|
347
348
|
height={(schema.height as string | undefined) ?? '200px'}
|
|
348
|
-
darkTheme={(schema.darkTheme as boolean | undefined) ??
|
|
349
|
+
darkTheme={(schema.darkTheme as boolean | undefined) ?? $resolvedTheme === 'dark'}
|
|
349
350
|
autoFormat={(schema.autoFormat as boolean | undefined) ?? true}
|
|
350
351
|
ariaDescribedBy={descriptionId}
|
|
351
352
|
disabled={isReadOnly}
|
|
@@ -373,7 +374,7 @@
|
|
|
373
374
|
'Enter your template here...\nUse {{ variable }} for dynamic values.'}
|
|
374
375
|
{required}
|
|
375
376
|
height={(schema.height as string | undefined) ?? '250px'}
|
|
376
|
-
darkTheme={(schema.darkTheme as boolean | undefined) ??
|
|
377
|
+
darkTheme={(schema.darkTheme as boolean | undefined) ?? $resolvedTheme === 'dark'}
|
|
377
378
|
variables={schema.variables}
|
|
378
379
|
placeholderExample={(schema.placeholderExample as string | undefined) ??
|
|
379
380
|
'Hello {{ name }}, your order #{{ order_id }} is ready!'}
|
|
@@ -27,15 +27,14 @@
|
|
|
27
27
|
highlightSpecialChars,
|
|
28
28
|
highlightActiveLine,
|
|
29
29
|
keymap,
|
|
30
|
+
tooltips,
|
|
30
31
|
Decoration,
|
|
31
|
-
type DecorationSet,
|
|
32
32
|
ViewPlugin,
|
|
33
|
-
type ViewUpdate,
|
|
34
33
|
MatchDecorator
|
|
35
34
|
} from '@codemirror/view';
|
|
36
35
|
import { EditorState, Compartment } from '@codemirror/state';
|
|
37
36
|
import { history, historyKeymap, defaultKeymap, indentWithTab } from '@codemirror/commands';
|
|
38
|
-
import { syntaxHighlighting, defaultHighlightStyle
|
|
37
|
+
import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
|
|
39
38
|
import { oneDark } from '@codemirror/theme-one-dark';
|
|
40
39
|
import type {
|
|
41
40
|
VariableSchema,
|
|
@@ -207,36 +206,37 @@
|
|
|
207
206
|
const autocompleteCompartment = new Compartment();
|
|
208
207
|
|
|
209
208
|
/**
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
* -
|
|
214
|
-
* -
|
|
215
|
-
* - Array access: {{ items[0] }}, {{ items[0].name }}
|
|
216
|
-
* - Mixed: {{ orders[0].items[1].price }}
|
|
209
|
+
* Custom Twig syntax highlighter using MatchDecorator
|
|
210
|
+
* Highlights three Twig delimiter types with different styles:
|
|
211
|
+
* - {{ expression }} — variables/output (purple)
|
|
212
|
+
* - {% block %} — control structures (teal)
|
|
213
|
+
* - {# comment #} — comments (gray/italic)
|
|
217
214
|
*/
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
215
|
+
const twigMatcher = new MatchDecorator({
|
|
216
|
+
regexp: /\{\{.*?\}\}|\{%.*?%\}|\{#.*?#\}/g,
|
|
217
|
+
decoration: (match) => {
|
|
218
|
+
const text = match[0];
|
|
219
|
+
if (text.startsWith('{{')) {
|
|
220
|
+
return Decoration.mark({ class: 'cm-twig-expression' });
|
|
221
|
+
} else if (text.startsWith('{%')) {
|
|
222
|
+
return Decoration.mark({ class: 'cm-twig-block' });
|
|
223
|
+
} else {
|
|
224
|
+
return Decoration.mark({ class: 'cm-twig-comment' });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
222
227
|
});
|
|
223
228
|
|
|
224
|
-
|
|
225
|
-
* ViewPlugin that applies the variable highlighting decorations
|
|
226
|
-
*/
|
|
227
|
-
const variableHighlighter = ViewPlugin.fromClass(
|
|
229
|
+
const twigHighlighter = ViewPlugin.fromClass(
|
|
228
230
|
class {
|
|
229
|
-
decorations
|
|
231
|
+
decorations;
|
|
230
232
|
constructor(view: EditorView) {
|
|
231
|
-
this.decorations =
|
|
233
|
+
this.decorations = twigMatcher.createDeco(view);
|
|
232
234
|
}
|
|
233
|
-
update(update: ViewUpdate) {
|
|
234
|
-
this.decorations =
|
|
235
|
+
update(update: import('@codemirror/view').ViewUpdate) {
|
|
236
|
+
this.decorations = twigMatcher.updateDeco(update, this.decorations);
|
|
235
237
|
}
|
|
236
238
|
},
|
|
237
|
-
{
|
|
238
|
-
decorations: (v) => v.decorations
|
|
239
|
-
}
|
|
239
|
+
{ decorations: (v) => v.decorations }
|
|
240
240
|
);
|
|
241
241
|
|
|
242
242
|
/**
|
|
@@ -258,6 +258,9 @@
|
|
|
258
258
|
*/
|
|
259
259
|
function createExtensions() {
|
|
260
260
|
const extensions = [
|
|
261
|
+
// Position tooltips using fixed strategy so they aren't clipped by container overflow
|
|
262
|
+
tooltips({ position: 'fixed' }),
|
|
263
|
+
|
|
261
264
|
// Essential visual features
|
|
262
265
|
lineNumbers(),
|
|
263
266
|
highlightActiveLineGutter(),
|
|
@@ -268,20 +271,16 @@
|
|
|
268
271
|
// Editing features (skip when read-only)
|
|
269
272
|
...(disabled
|
|
270
273
|
? []
|
|
271
|
-
: [
|
|
272
|
-
history(),
|
|
273
|
-
indentOnInput(),
|
|
274
|
-
keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab])
|
|
275
|
-
]),
|
|
274
|
+
: [history(), keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab])]),
|
|
276
275
|
|
|
277
276
|
// Read-only: prevent document changes and mark content as non-editable
|
|
278
277
|
...(disabled ? [EditorState.readOnly.of(true), EditorView.editable.of(false)] : []),
|
|
279
278
|
|
|
280
|
-
// Syntax highlighting
|
|
281
|
-
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
|
279
|
+
// Syntax highlighting - use default for light mode, oneDark handles dark mode
|
|
280
|
+
...(darkTheme ? [] : [syntaxHighlighting(defaultHighlightStyle, { fallback: true })]),
|
|
282
281
|
|
|
283
|
-
//
|
|
284
|
-
|
|
282
|
+
// Twig syntax highlighting ({{ expressions }}, {% blocks %}, {# comments #})
|
|
283
|
+
twigHighlighter,
|
|
285
284
|
|
|
286
285
|
// Update listener (only fires on user edit when not disabled)
|
|
287
286
|
EditorView.updateListener.of(handleUpdate),
|
|
@@ -306,14 +305,27 @@
|
|
|
306
305
|
'.cm-line': {
|
|
307
306
|
padding: '0 0.5rem'
|
|
308
307
|
},
|
|
309
|
-
//
|
|
310
|
-
'.cm-
|
|
308
|
+
// Twig expression: {{ variable }}
|
|
309
|
+
'.cm-twig-expression': {
|
|
311
310
|
color: '#a855f7',
|
|
312
311
|
backgroundColor: 'rgba(168, 85, 247, 0.1)',
|
|
313
312
|
borderRadius: '3px',
|
|
314
313
|
padding: '1px 2px',
|
|
315
314
|
fontWeight: '500'
|
|
316
315
|
},
|
|
316
|
+
// Twig block: {% for ... %}
|
|
317
|
+
'.cm-twig-block': {
|
|
318
|
+
color: '#14b8a6',
|
|
319
|
+
backgroundColor: 'rgba(20, 184, 166, 0.1)',
|
|
320
|
+
borderRadius: '3px',
|
|
321
|
+
padding: '1px 2px',
|
|
322
|
+
fontWeight: '500'
|
|
323
|
+
},
|
|
324
|
+
// Twig comment: {# ... #}
|
|
325
|
+
'.cm-twig-comment': {
|
|
326
|
+
color: '#6b7280',
|
|
327
|
+
fontStyle: 'italic'
|
|
328
|
+
},
|
|
317
329
|
// Autocomplete dropdown styling
|
|
318
330
|
'.cm-tooltip.cm-tooltip-autocomplete': {
|
|
319
331
|
backgroundColor: 'var(--fd-background, #ffffff)',
|
|
@@ -355,20 +367,29 @@
|
|
|
355
367
|
// Add autocomplete compartment (can be reconfigured dynamically)
|
|
356
368
|
// When disabled or no schema, use empty array
|
|
357
369
|
if (!disabled && effectiveVariableSchema) {
|
|
358
|
-
extensions.push(
|
|
370
|
+
extensions.push(
|
|
371
|
+
autocompleteCompartment.of(createTemplateAutocomplete(effectiveVariableSchema))
|
|
372
|
+
);
|
|
359
373
|
} else {
|
|
360
374
|
extensions.push(autocompleteCompartment.of([]));
|
|
361
375
|
}
|
|
362
376
|
|
|
363
377
|
if (darkTheme) {
|
|
364
378
|
extensions.push(oneDark);
|
|
365
|
-
// Add dark theme
|
|
379
|
+
// Add dark theme overrides for Twig highlighting and autocomplete
|
|
366
380
|
extensions.push(
|
|
367
381
|
EditorView.theme({
|
|
368
|
-
'.cm-
|
|
382
|
+
'.cm-twig-expression': {
|
|
369
383
|
color: '#c084fc',
|
|
370
384
|
backgroundColor: 'rgba(192, 132, 252, 0.15)'
|
|
371
385
|
},
|
|
386
|
+
'.cm-twig-block': {
|
|
387
|
+
color: '#5eead4',
|
|
388
|
+
backgroundColor: 'rgba(94, 234, 212, 0.1)'
|
|
389
|
+
},
|
|
390
|
+
'.cm-twig-comment': {
|
|
391
|
+
color: '#6b7280'
|
|
392
|
+
},
|
|
372
393
|
'.cm-tooltip.cm-tooltip-autocomplete': {
|
|
373
394
|
backgroundColor: '#1e1e1e',
|
|
374
395
|
border: '1px solid #3e4451',
|
|
@@ -471,9 +492,7 @@
|
|
|
471
492
|
|
|
472
493
|
// When effectiveVariableSchema changes, reconfigure the autocomplete compartment
|
|
473
494
|
// This happens after async API loading completes
|
|
474
|
-
const newAutocomplete = !disabled && schema
|
|
475
|
-
? createTemplateAutocomplete(schema)
|
|
476
|
-
: [];
|
|
495
|
+
const newAutocomplete = !disabled && schema ? createTemplateAutocomplete(schema) : [];
|
|
477
496
|
|
|
478
497
|
editorView.dispatch({
|
|
479
498
|
effects: [autocompleteCompartment.reconfigure(newAutocomplete)]
|
|
@@ -511,13 +530,7 @@
|
|
|
511
530
|
fill="none"
|
|
512
531
|
viewBox="0 0 24 24"
|
|
513
532
|
>
|
|
514
|
-
<circle
|
|
515
|
-
class="opacity-25"
|
|
516
|
-
cx="12"
|
|
517
|
-
cy="12"
|
|
518
|
-
r="10"
|
|
519
|
-
stroke="currentColor"
|
|
520
|
-
stroke-width="4"
|
|
533
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"
|
|
521
534
|
></circle>
|
|
522
535
|
<path
|
|
523
536
|
class="opacity-75"
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @module components/form/templateAutocomplete
|
|
12
12
|
*/
|
|
13
|
-
import type { Extension } from
|
|
14
|
-
import type { VariableSchema } from
|
|
13
|
+
import type { Extension } from '@codemirror/state';
|
|
14
|
+
import type { VariableSchema } from '../../types/index.js';
|
|
15
15
|
/**
|
|
16
16
|
* Creates a CodeMirror extension for template variable autocomplete.
|
|
17
17
|
*
|
|
@@ -10,20 +10,20 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @module components/form/templateAutocomplete
|
|
12
12
|
*/
|
|
13
|
-
import { autocompletion } from
|
|
14
|
-
import { getChildVariables, getArrayIndexSuggestions, isArrayVariable, hasChildren } from
|
|
13
|
+
import { autocompletion } from '@codemirror/autocomplete';
|
|
14
|
+
import { getChildVariables, getArrayIndexSuggestions, isArrayVariable, hasChildren } from '../../services/variableService.js';
|
|
15
15
|
/**
|
|
16
16
|
* Icon type hints for different variable types in autocomplete dropdown.
|
|
17
17
|
*/
|
|
18
18
|
const TYPE_ICONS = {
|
|
19
|
-
string:
|
|
20
|
-
number:
|
|
21
|
-
integer:
|
|
22
|
-
float:
|
|
23
|
-
boolean:
|
|
24
|
-
array:
|
|
25
|
-
object:
|
|
26
|
-
mixed:
|
|
19
|
+
string: '𝑆',
|
|
20
|
+
number: '#',
|
|
21
|
+
integer: '#',
|
|
22
|
+
float: '#',
|
|
23
|
+
boolean: '☑',
|
|
24
|
+
array: '[]',
|
|
25
|
+
object: '{}',
|
|
26
|
+
mixed: '⋯'
|
|
27
27
|
};
|
|
28
28
|
/**
|
|
29
29
|
* Extracts the current variable path being typed inside {{ }}.
|
|
@@ -43,12 +43,12 @@ function extractVariablePath(text, pos) {
|
|
|
43
43
|
let searchPos = pos - 1;
|
|
44
44
|
while (searchPos >= 0) {
|
|
45
45
|
// Check for opening {{
|
|
46
|
-
if (text[searchPos] ===
|
|
46
|
+
if (text[searchPos] === '{' && searchPos > 0 && text[searchPos - 1] === '{') {
|
|
47
47
|
openBracePos = searchPos - 1;
|
|
48
48
|
break;
|
|
49
49
|
}
|
|
50
50
|
// Check for closing }} - means we're outside an expression
|
|
51
|
-
if (text[searchPos] ===
|
|
51
|
+
if (text[searchPos] === '}' && searchPos > 0 && text[searchPos - 1] === '}') {
|
|
52
52
|
return null;
|
|
53
53
|
}
|
|
54
54
|
searchPos--;
|
|
@@ -65,7 +65,8 @@ function extractVariablePath(text, pos) {
|
|
|
65
65
|
const content = text.slice(contentStart, pos).trimStart();
|
|
66
66
|
return {
|
|
67
67
|
path: content,
|
|
68
|
-
startPos: contentStart +
|
|
68
|
+
startPos: contentStart +
|
|
69
|
+
(text.slice(contentStart, pos).length - text.slice(contentStart, pos).trimStart().length),
|
|
69
70
|
isInsideExpression: true
|
|
70
71
|
};
|
|
71
72
|
}
|
|
@@ -77,35 +78,35 @@ function extractVariablePath(text, pos) {
|
|
|
77
78
|
*/
|
|
78
79
|
function getCompletionType(path) {
|
|
79
80
|
// Empty or only whitespace - show top-level variables
|
|
80
|
-
if (path.trim() ===
|
|
81
|
-
return { type:
|
|
81
|
+
if (path.trim() === '') {
|
|
82
|
+
return { type: 'top-level' };
|
|
82
83
|
}
|
|
83
84
|
// Ends with [ - show array indices
|
|
84
|
-
if (path.endsWith(
|
|
85
|
+
if (path.endsWith('[')) {
|
|
85
86
|
const parentPath = path.slice(0, -1);
|
|
86
|
-
return { type:
|
|
87
|
+
return { type: 'array-index', parentPath };
|
|
87
88
|
}
|
|
88
89
|
// Ends with . - show child properties
|
|
89
|
-
if (path.endsWith(
|
|
90
|
+
if (path.endsWith('.')) {
|
|
90
91
|
const parentPath = path.slice(0, -1);
|
|
91
|
-
return { type:
|
|
92
|
+
return { type: 'property', parentPath };
|
|
92
93
|
}
|
|
93
94
|
// Otherwise, we're typing a variable name - show matching options
|
|
94
|
-
const lastDotIndex = path.lastIndexOf(
|
|
95
|
-
const lastBracketIndex = path.lastIndexOf(
|
|
95
|
+
const lastDotIndex = path.lastIndexOf('.');
|
|
96
|
+
const lastBracketIndex = path.lastIndexOf('[');
|
|
96
97
|
const lastSeparator = Math.max(lastDotIndex, lastBracketIndex);
|
|
97
98
|
if (lastSeparator === -1) {
|
|
98
99
|
// Typing at top level
|
|
99
|
-
return { type:
|
|
100
|
+
return { type: 'top-level' };
|
|
100
101
|
}
|
|
101
102
|
// Extract parent path based on separator
|
|
102
103
|
if (lastDotIndex > lastBracketIndex) {
|
|
103
104
|
// Last separator was a dot
|
|
104
|
-
return { type:
|
|
105
|
+
return { type: 'property', parentPath: path.slice(0, lastDotIndex) };
|
|
105
106
|
}
|
|
106
107
|
else {
|
|
107
108
|
// Last separator was a bracket
|
|
108
|
-
return { type:
|
|
109
|
+
return { type: 'array-index', parentPath: path.slice(0, lastBracketIndex) };
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
112
|
/**
|
|
@@ -115,22 +116,22 @@ function getCompletionType(path) {
|
|
|
115
116
|
* @param prefix - Prefix to add to the completion label
|
|
116
117
|
* @returns A CodeMirror Completion object
|
|
117
118
|
*/
|
|
118
|
-
function variableToCompletion(variable, prefix =
|
|
119
|
+
function variableToCompletion(variable, prefix = '') {
|
|
119
120
|
const icon = TYPE_ICONS[variable.type] ?? TYPE_ICONS.mixed;
|
|
120
121
|
const hasChildProps = variable.properties && Object.keys(variable.properties).length > 0;
|
|
121
|
-
const isArray = variable.type ===
|
|
122
|
+
const isArray = variable.type === 'array';
|
|
122
123
|
// Add indicator if variable can be drilled into
|
|
123
|
-
let suffix =
|
|
124
|
+
let suffix = '';
|
|
124
125
|
if (hasChildProps)
|
|
125
|
-
suffix =
|
|
126
|
+
suffix = '.';
|
|
126
127
|
else if (isArray)
|
|
127
|
-
suffix =
|
|
128
|
+
suffix = '[';
|
|
128
129
|
return {
|
|
129
130
|
label: `${prefix}${variable.name}`,
|
|
130
|
-
displayLabel: `${icon} ${variable.label ?? variable.name}${suffix ?
|
|
131
|
+
displayLabel: `${icon} ${variable.label ?? variable.name}${suffix ? ' ' + suffix : ''}`,
|
|
131
132
|
detail: variable.type,
|
|
132
133
|
info: variable.description,
|
|
133
|
-
type:
|
|
134
|
+
type: 'variable',
|
|
134
135
|
boost: hasChildProps || isArray ? 1 : 0 // Boost drillable variables
|
|
135
136
|
};
|
|
136
137
|
}
|
|
@@ -149,7 +150,7 @@ function createTemplateCompletionSource(schema) {
|
|
|
149
150
|
if (!pathInfo) {
|
|
150
151
|
// Check if user just typed {{
|
|
151
152
|
const beforeCursor = text.slice(Math.max(0, pos - 2), pos);
|
|
152
|
-
if (beforeCursor ===
|
|
153
|
+
if (beforeCursor === '{{') {
|
|
153
154
|
// Show top-level variables
|
|
154
155
|
const options = Object.values(schema.variables).map((v) => variableToCompletion(v));
|
|
155
156
|
return {
|
|
@@ -165,42 +166,42 @@ function createTemplateCompletionSource(schema) {
|
|
|
165
166
|
let options = [];
|
|
166
167
|
let from = pos;
|
|
167
168
|
switch (completionType.type) {
|
|
168
|
-
case
|
|
169
|
+
case 'top-level': {
|
|
169
170
|
// Show all top-level variables
|
|
170
171
|
const currentWord = path.trim();
|
|
171
172
|
options = Object.values(schema.variables)
|
|
172
|
-
.filter((v) => currentWord ===
|
|
173
|
+
.filter((v) => currentWord === '' || v.name.toLowerCase().startsWith(currentWord.toLowerCase()))
|
|
173
174
|
.map((v) => variableToCompletion(v));
|
|
174
175
|
// Calculate from position for replacement
|
|
175
176
|
from = startPos + (path.length - path.trimStart().length);
|
|
176
177
|
break;
|
|
177
178
|
}
|
|
178
|
-
case
|
|
179
|
+
case 'property': {
|
|
179
180
|
// Show child properties of the parent
|
|
180
181
|
const children = getChildVariables(schema, completionType.parentPath);
|
|
181
|
-
const currentWord = path.slice(path.lastIndexOf(
|
|
182
|
+
const currentWord = path.slice(path.lastIndexOf('.') + 1);
|
|
182
183
|
options = children
|
|
183
|
-
.filter((v) => currentWord ===
|
|
184
|
+
.filter((v) => currentWord === '' || v.name.toLowerCase().startsWith(currentWord.toLowerCase()))
|
|
184
185
|
.map((v) => variableToCompletion(v));
|
|
185
186
|
// From should be right after the last dot
|
|
186
|
-
from = startPos + path.lastIndexOf(
|
|
187
|
+
from = startPos + path.lastIndexOf('.') + 1;
|
|
187
188
|
break;
|
|
188
189
|
}
|
|
189
|
-
case
|
|
190
|
+
case 'array-index': {
|
|
190
191
|
// Check if the parent is actually an array
|
|
191
192
|
if (isArrayVariable(schema, completionType.parentPath)) {
|
|
192
193
|
const indices = getArrayIndexSuggestions(5);
|
|
193
|
-
const currentIndex = path.slice(path.lastIndexOf(
|
|
194
|
+
const currentIndex = path.slice(path.lastIndexOf('[') + 1);
|
|
194
195
|
options = indices
|
|
195
|
-
.filter((idx) => currentIndex ===
|
|
196
|
+
.filter((idx) => currentIndex === '' || idx.startsWith(currentIndex))
|
|
196
197
|
.map((idx) => ({
|
|
197
198
|
label: idx,
|
|
198
|
-
displayLabel: idx ===
|
|
199
|
-
detail: idx ===
|
|
200
|
-
type:
|
|
199
|
+
displayLabel: idx === '*]' ? '* (all items)' : `[${idx}`,
|
|
200
|
+
detail: idx === '*]' ? 'Iterate all items' : `Index ${idx.slice(0, -1)}`,
|
|
201
|
+
type: 'keyword'
|
|
201
202
|
}));
|
|
202
203
|
// From should be right after the [
|
|
203
|
-
from = startPos + path.lastIndexOf(
|
|
204
|
+
from = startPos + path.lastIndexOf('[') + 1;
|
|
204
205
|
}
|
|
205
206
|
break;
|
|
206
207
|
}
|
|
@@ -234,13 +235,13 @@ export function createTemplateAutocomplete(schema) {
|
|
|
234
235
|
override: [createTemplateCompletionSource(schema)],
|
|
235
236
|
activateOnTyping: true,
|
|
236
237
|
defaultKeymap: true,
|
|
237
|
-
optionClass: () =>
|
|
238
|
+
optionClass: () => 'cm-template-autocomplete-option',
|
|
238
239
|
icons: false, // We use our own icons in displayLabel
|
|
239
240
|
addToOptions: [
|
|
240
241
|
{
|
|
241
242
|
render: (completion) => {
|
|
242
|
-
const el = document.createElement(
|
|
243
|
-
el.className =
|
|
243
|
+
const el = document.createElement('span');
|
|
244
|
+
el.className = 'cm-template-autocomplete-info';
|
|
244
245
|
if (completion.info) {
|
|
245
246
|
el.textContent = String(completion.info);
|
|
246
247
|
}
|
|
@@ -230,7 +230,7 @@
|
|
|
230
230
|
.flowdrop-tool-node {
|
|
231
231
|
position: relative;
|
|
232
232
|
background-color: var(--fd-card);
|
|
233
|
-
border: 1.5px solid var(--fd-node-
|
|
233
|
+
border: 1.5px solid var(--fd-tool-node-color);
|
|
234
234
|
border-radius: var(--fd-radius-xl);
|
|
235
235
|
width: var(--fd-node-default-width);
|
|
236
236
|
min-height: var(--fd-node-tool-min-height);
|
|
@@ -246,7 +246,7 @@
|
|
|
246
246
|
|
|
247
247
|
.flowdrop-tool-node:hover {
|
|
248
248
|
box-shadow: var(--fd-shadow-lg);
|
|
249
|
-
border-color: var(--fd-node-
|
|
249
|
+
border-color: var(--fd-tool-node-color);
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
.flowdrop-tool-node--selected {
|
|
@@ -282,16 +282,14 @@
|
|
|
282
282
|
/* Light mode: mix tool color with white (95%) for subtle tint */
|
|
283
283
|
background-color: color-mix(in srgb, var(--fd-tool-node-color) 5%, white);
|
|
284
284
|
border-radius: var(--fd-radius-xl);
|
|
285
|
-
|
|
286
|
-
border: 1px solid color-mix(in srgb, var(--fd-tool-node-color) 40%, white);
|
|
285
|
+
border: none;
|
|
287
286
|
}
|
|
288
287
|
|
|
289
288
|
/* Dark mode header styles */
|
|
290
289
|
:global([data-theme='dark']) .flowdrop-tool-node__header {
|
|
291
290
|
/* Dark mode: mix tool color with dark background (15%) for subtle tint */
|
|
292
291
|
background-color: color-mix(in srgb, var(--fd-tool-node-color) 15%, #1a1a1e);
|
|
293
|
-
|
|
294
|
-
border-color: color-mix(in srgb, var(--fd-tool-node-color) 35%, #1a1a1e);
|
|
292
|
+
border: none;
|
|
295
293
|
}
|
|
296
294
|
|
|
297
295
|
.flowdrop-tool-node__header-content {
|
|
@@ -62,6 +62,12 @@ async function ensureApiConfiguration() {
|
|
|
62
62
|
* Uses the current workflow from the global store
|
|
63
63
|
*/
|
|
64
64
|
export async function globalSaveWorkflow() {
|
|
65
|
+
// Flush any pending form changes by blurring the active element.
|
|
66
|
+
// This ensures focusout handlers (like ConfigForm's handleFormBlur)
|
|
67
|
+
// sync local state to the global store before we read it.
|
|
68
|
+
if (typeof document !== 'undefined' && document.activeElement instanceof HTMLElement) {
|
|
69
|
+
document.activeElement.blur();
|
|
70
|
+
}
|
|
65
71
|
let loadingToast;
|
|
66
72
|
try {
|
|
67
73
|
// Show loading toast
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @module services/variableService
|
|
7
7
|
*/
|
|
8
|
-
import type { WorkflowNode, WorkflowEdge, VariableSchema, TemplateVariable, TemplateVariablesConfig, AuthProvider } from
|
|
8
|
+
import type { WorkflowNode, WorkflowEdge, VariableSchema, TemplateVariable, TemplateVariablesConfig, AuthProvider } from '../types/index.js';
|
|
9
9
|
/**
|
|
10
10
|
* Options for deriving available variables.
|
|
11
11
|
*/
|
|
@@ -13,22 +13,22 @@
|
|
|
13
13
|
*/
|
|
14
14
|
function toTemplateVariableType(schemaType) {
|
|
15
15
|
switch (schemaType) {
|
|
16
|
-
case
|
|
17
|
-
return
|
|
18
|
-
case
|
|
19
|
-
return
|
|
20
|
-
case
|
|
21
|
-
return
|
|
22
|
-
case
|
|
23
|
-
return
|
|
24
|
-
case
|
|
25
|
-
return
|
|
26
|
-
case
|
|
27
|
-
return
|
|
28
|
-
case
|
|
29
|
-
return
|
|
16
|
+
case 'string':
|
|
17
|
+
return 'string';
|
|
18
|
+
case 'number':
|
|
19
|
+
return 'number';
|
|
20
|
+
case 'integer':
|
|
21
|
+
return 'integer';
|
|
22
|
+
case 'boolean':
|
|
23
|
+
return 'boolean';
|
|
24
|
+
case 'array':
|
|
25
|
+
return 'array';
|
|
26
|
+
case 'object':
|
|
27
|
+
return 'object';
|
|
28
|
+
case 'float':
|
|
29
|
+
return 'float';
|
|
30
30
|
default:
|
|
31
|
-
return
|
|
31
|
+
return 'mixed';
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
/**
|
|
@@ -51,15 +51,15 @@ function propertyToTemplateVariable(name, property, sourcePort, sourceNode) {
|
|
|
51
51
|
sourceNode
|
|
52
52
|
};
|
|
53
53
|
// Handle nested object properties
|
|
54
|
-
if (property.type ===
|
|
54
|
+
if (property.type === 'object' && property.properties) {
|
|
55
55
|
variable.properties = {};
|
|
56
56
|
for (const [propName, propValue] of Object.entries(property.properties)) {
|
|
57
57
|
variable.properties[propName] = propertyToTemplateVariable(propName, propValue, sourcePort, sourceNode);
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
// Handle array items
|
|
61
|
-
if (property.type ===
|
|
62
|
-
variable.items = propertyToTemplateVariable(
|
|
61
|
+
if (property.type === 'array' && property.items) {
|
|
62
|
+
variable.items = propertyToTemplateVariable('item', property.items, sourcePort, sourceNode);
|
|
63
63
|
}
|
|
64
64
|
return variable;
|
|
65
65
|
}
|
|
@@ -78,7 +78,7 @@ function portToTemplateVariable(port, sourceNode) {
|
|
|
78
78
|
name: port.id,
|
|
79
79
|
label: port.name,
|
|
80
80
|
description: port.description,
|
|
81
|
-
type:
|
|
81
|
+
type: 'object',
|
|
82
82
|
sourcePort: port.id,
|
|
83
83
|
sourceNode,
|
|
84
84
|
properties: {}
|
|
@@ -186,12 +186,12 @@ export function getAvailableVariables(node, nodes, edges, options) {
|
|
|
186
186
|
for (const connection of connections) {
|
|
187
187
|
const { sourceNode, sourcePort, targetPort } = connection;
|
|
188
188
|
// Skip trigger ports - they don't carry data
|
|
189
|
-
if (sourcePort?.dataType ===
|
|
189
|
+
if (sourcePort?.dataType === 'trigger')
|
|
190
190
|
continue;
|
|
191
|
-
if (targetPort?.dataType ===
|
|
191
|
+
if (targetPort?.dataType === 'trigger')
|
|
192
192
|
continue;
|
|
193
193
|
// Get the target port ID for filtering
|
|
194
|
-
const targetPortId = targetPort?.id ?? sourcePort?.id ??
|
|
194
|
+
const targetPortId = targetPort?.id ?? sourcePort?.id ?? 'data';
|
|
195
195
|
// Filter by target port IDs if specified
|
|
196
196
|
if (targetPortIds !== undefined) {
|
|
197
197
|
if (!targetPortIds.includes(targetPortId))
|
|
@@ -212,7 +212,7 @@ export function getAvailableVariables(node, nodes, edges, options) {
|
|
|
212
212
|
}
|
|
213
213
|
else {
|
|
214
214
|
// No schema or includePortName is true - use port name as the variable
|
|
215
|
-
const variableName = includePortName ? targetPortId :
|
|
215
|
+
const variableName = includePortName ? targetPortId : targetPortId;
|
|
216
216
|
// Skip if we already have a variable with this name
|
|
217
217
|
if (variables[variableName])
|
|
218
218
|
continue;
|
|
@@ -244,7 +244,7 @@ export function getAvailableVariables(node, nodes, edges, options) {
|
|
|
244
244
|
* ```
|
|
245
245
|
*/
|
|
246
246
|
export function getChildVariables(schema, path) {
|
|
247
|
-
const parts = path.split(
|
|
247
|
+
const parts = path.split('.');
|
|
248
248
|
let current;
|
|
249
249
|
// Navigate to the target variable
|
|
250
250
|
for (let i = 0; i < parts.length; i++) {
|
|
@@ -295,7 +295,7 @@ export function getArrayIndexSuggestions(maxIndex = 2) {
|
|
|
295
295
|
suggestions.push(`${i}]`);
|
|
296
296
|
}
|
|
297
297
|
// Add wildcard for "all items"
|
|
298
|
-
suggestions.push(
|
|
298
|
+
suggestions.push('*]');
|
|
299
299
|
return suggestions;
|
|
300
300
|
}
|
|
301
301
|
/**
|
|
@@ -306,7 +306,7 @@ export function getArrayIndexSuggestions(maxIndex = 2) {
|
|
|
306
306
|
* @returns True if the variable is an array type
|
|
307
307
|
*/
|
|
308
308
|
export function isArrayVariable(schema, path) {
|
|
309
|
-
const parts = path.split(
|
|
309
|
+
const parts = path.split('.');
|
|
310
310
|
let current;
|
|
311
311
|
for (let i = 0; i < parts.length; i++) {
|
|
312
312
|
const part = parts[i];
|
|
@@ -335,7 +335,7 @@ export function isArrayVariable(schema, path) {
|
|
|
335
335
|
return false;
|
|
336
336
|
}
|
|
337
337
|
}
|
|
338
|
-
return current?.type ===
|
|
338
|
+
return current?.type === 'array';
|
|
339
339
|
}
|
|
340
340
|
/**
|
|
341
341
|
* Checks if a variable at the given path has child properties.
|
|
@@ -410,7 +410,7 @@ export async function getVariableSchema(node, nodes, edges, config, workflowId,
|
|
|
410
410
|
if (config.api) {
|
|
411
411
|
try {
|
|
412
412
|
// Import API variable service dynamically to avoid circular dependencies
|
|
413
|
-
const { fetchVariableSchema } = await import(
|
|
413
|
+
const { fetchVariableSchema } = await import('./apiVariableService.js');
|
|
414
414
|
const apiResult = await fetchVariableSchema(workflowId, node.id, config.api, authProvider);
|
|
415
415
|
if (apiResult.success && apiResult.schema) {
|
|
416
416
|
resultSchema = apiResult.schema;
|
|
@@ -430,13 +430,13 @@ export async function getVariableSchema(node, nodes, edges, config, workflowId,
|
|
|
430
430
|
}
|
|
431
431
|
else if (!config.api.fallbackOnError) {
|
|
432
432
|
// API failed and fallback is disabled - return empty schema
|
|
433
|
-
console.error(
|
|
433
|
+
console.error('Failed to fetch variables from API:', apiResult.error);
|
|
434
434
|
return { variables: {} };
|
|
435
435
|
}
|
|
436
436
|
// If fallback is enabled (default), continue to schema-based mode below
|
|
437
437
|
}
|
|
438
438
|
catch (error) {
|
|
439
|
-
console.error(
|
|
439
|
+
console.error('Error fetching variables from API:', error);
|
|
440
440
|
// If fallback is disabled, return empty schema
|
|
441
441
|
if (config.api.fallbackOnError === false) {
|
|
442
442
|
return { variables: {} };
|
package/dist/svelte-app.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { Workflow, NodeMetadata, PortConfig } from './types/index.js';
|
|
|
10
10
|
import type { EndpointConfig } from './config/endpoints.js';
|
|
11
11
|
import type { AuthProvider } from './types/auth.js';
|
|
12
12
|
import type { FlowDropEventHandlers, FlowDropFeatures } from './types/events.js';
|
|
13
|
+
import type { PartialSettings } from './types/settings.js';
|
|
13
14
|
declare global {
|
|
14
15
|
interface Window {
|
|
15
16
|
flowdropSave?: () => Promise<void>;
|
|
@@ -66,6 +67,8 @@ export interface FlowDropMountOptions {
|
|
|
66
67
|
eventHandlers?: FlowDropEventHandlers;
|
|
67
68
|
/** Feature configuration */
|
|
68
69
|
features?: FlowDropFeatures;
|
|
70
|
+
/** Initial settings overrides (theme, behavior, editor, ui, api) */
|
|
71
|
+
settings?: PartialSettings;
|
|
69
72
|
/** Custom storage key for localStorage drafts */
|
|
70
73
|
draftStorageKey?: string;
|
|
71
74
|
}
|
package/dist/svelte-app.js
CHANGED
|
@@ -15,6 +15,7 @@ import { fetchPortConfig } from './services/portConfigApi.js';
|
|
|
15
15
|
import { isDirty, markAsSaved, getWorkflow as getWorkflowFromStore, setOnDirtyStateChange, setOnWorkflowChange } from './stores/workflowStore.js';
|
|
16
16
|
import { DraftAutoSaveManager, getDraftStorageKey } from './services/draftStorage.js';
|
|
17
17
|
import { mergeFeatures } from './types/events.js';
|
|
18
|
+
import { initializeSettings } from './stores/settingsStore.js';
|
|
18
19
|
/**
|
|
19
20
|
* Mount the full FlowDrop App with navbar, sidebars, and workflow editor
|
|
20
21
|
*
|
|
@@ -40,9 +41,13 @@ import { mergeFeatures } from './types/events.js';
|
|
|
40
41
|
* ```
|
|
41
42
|
*/
|
|
42
43
|
export async function mountFlowDropApp(container, options = {}) {
|
|
43
|
-
const { workflow, nodes, endpointConfig, portConfig, height = '100vh', width = '100%', showNavbar = false, disableSidebar, lockWorkflow, readOnly, nodeStatuses, pipelineId, navbarTitle, navbarActions, showSettings, authProvider, eventHandlers, features: userFeatures, draftStorageKey: customDraftKey } = options;
|
|
44
|
+
const { workflow, nodes, endpointConfig, portConfig, height = '100vh', width = '100%', showNavbar = false, disableSidebar, lockWorkflow, readOnly, nodeStatuses, pipelineId, navbarTitle, navbarActions, showSettings, authProvider, eventHandlers, features: userFeatures, settings: initialSettings, draftStorageKey: customDraftKey } = options;
|
|
44
45
|
// Merge features with defaults
|
|
45
46
|
const features = mergeFeatures(userFeatures);
|
|
47
|
+
// Apply initial settings overrides and initialize theme
|
|
48
|
+
await initializeSettings({
|
|
49
|
+
defaults: initialSettings
|
|
50
|
+
});
|
|
46
51
|
// Create endpoint configuration
|
|
47
52
|
let config;
|
|
48
53
|
if (endpointConfig) {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { EndpointConfig } from '../config/endpoints.js';
|
|
|
8
8
|
* Node category types for organizing nodes in the sidebar
|
|
9
9
|
* Based on actual API response categories
|
|
10
10
|
*/
|
|
11
|
-
export type NodeCategory = 'triggers' | 'inputs' | 'outputs' | 'prompts' | 'models' | 'processing' | 'logic' | 'data' | 'tools' | 'helpers' | 'vector stores' | 'embeddings' | 'memories' | 'agents' | 'ai' | 'bundles';
|
|
11
|
+
export type NodeCategory = 'triggers' | 'inputs' | 'outputs' | 'prompts' | 'models' | 'processing' | 'logic' | 'data' | 'tools' | 'helpers' | 'vector stores' | 'embeddings' | 'memories' | 'agents' | 'ai' | 'interrupts' | 'bundles';
|
|
12
12
|
/**
|
|
13
13
|
* Port data type configuration
|
|
14
14
|
*/
|
package/dist/types/settings.js
CHANGED
|
@@ -37,7 +37,7 @@ export const SETTINGS_CATEGORY_ICONS = {
|
|
|
37
37
|
* Default theme settings
|
|
38
38
|
*/
|
|
39
39
|
export const DEFAULT_THEME_SETTINGS = {
|
|
40
|
-
preference: '
|
|
40
|
+
preference: 'light'
|
|
41
41
|
};
|
|
42
42
|
/**
|
|
43
43
|
* Default editor settings
|
|
@@ -64,8 +64,8 @@ export const DEFAULT_UI_SETTINGS = {
|
|
|
64
64
|
export const DEFAULT_BEHAVIOR_SETTINGS = {
|
|
65
65
|
autoSave: false,
|
|
66
66
|
autoSaveInterval: 30000,
|
|
67
|
-
undoHistoryLimit:
|
|
68
|
-
confirmDelete:
|
|
67
|
+
undoHistoryLimit: 0,
|
|
68
|
+
confirmDelete: false
|
|
69
69
|
};
|
|
70
70
|
/**
|
|
71
71
|
* Default API settings
|
|
@@ -74,7 +74,7 @@ export const DEFAULT_API_SETTINGS = {
|
|
|
74
74
|
timeout: 30000,
|
|
75
75
|
retryEnabled: true,
|
|
76
76
|
retryAttempts: 3,
|
|
77
|
-
cacheEnabled:
|
|
77
|
+
cacheEnabled: false
|
|
78
78
|
};
|
|
79
79
|
/**
|
|
80
80
|
* Complete default settings object
|
package/dist/utils/colors.js
CHANGED
package/dist/utils/icons.js
CHANGED
package/dist/utils/nodeTypes.js
CHANGED
|
@@ -194,9 +194,9 @@ export function createNodeTypeConfigProperty(metadata, defaultType) {
|
|
|
194
194
|
const oneOf = getNodeTypeOneOfOptions(metadata);
|
|
195
195
|
const primaryType = defaultType ?? getPrimaryNodeType(metadata);
|
|
196
196
|
return {
|
|
197
|
-
type:
|
|
198
|
-
title:
|
|
199
|
-
description:
|
|
197
|
+
type: 'string',
|
|
198
|
+
title: 'Node Type',
|
|
199
|
+
description: 'Choose the visual representation for this node',
|
|
200
200
|
default: primaryType,
|
|
201
201
|
oneOf
|
|
202
202
|
};
|