@d34dman/flowdrop 0.0.45 → 0.0.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +2 -2
  2. package/dist/components/App.svelte +6 -0
  3. package/dist/components/ConfigForm.svelte +56 -22
  4. package/dist/components/ConfigForm.svelte.d.ts +11 -1
  5. package/dist/components/Navbar.svelte +6 -7
  6. package/dist/components/SchemaForm.svelte +2 -10
  7. package/dist/components/SettingsPanel.svelte +5 -2
  8. package/dist/components/WorkflowEditor.svelte +158 -4
  9. package/dist/components/WorkflowEditor.svelte.d.ts +1 -0
  10. package/dist/components/form/FormAutocomplete.svelte +5 -9
  11. package/dist/components/form/FormCheckboxGroup.svelte +11 -1
  12. package/dist/components/form/FormCheckboxGroup.svelte.d.ts +2 -0
  13. package/dist/components/form/FormCodeEditor.svelte +16 -7
  14. package/dist/components/form/FormCodeEditor.svelte.d.ts +2 -0
  15. package/dist/components/form/FormField.svelte +33 -12
  16. package/dist/components/form/FormFieldLight.svelte +16 -12
  17. package/dist/components/form/FormMarkdownEditor.svelte +29 -19
  18. package/dist/components/form/FormMarkdownEditor.svelte.d.ts +2 -0
  19. package/dist/components/form/FormNumberField.svelte +4 -0
  20. package/dist/components/form/FormNumberField.svelte.d.ts +2 -0
  21. package/dist/components/form/FormRangeField.svelte +4 -0
  22. package/dist/components/form/FormRangeField.svelte.d.ts +2 -0
  23. package/dist/components/form/FormSelect.svelte +4 -0
  24. package/dist/components/form/FormSelect.svelte.d.ts +2 -0
  25. package/dist/components/form/FormTemplateEditor.svelte +140 -17
  26. package/dist/components/form/FormTemplateEditor.svelte.d.ts +19 -1
  27. package/dist/components/form/FormTextField.svelte +4 -0
  28. package/dist/components/form/FormTextField.svelte.d.ts +2 -0
  29. package/dist/components/form/FormTextarea.svelte +4 -0
  30. package/dist/components/form/FormTextarea.svelte.d.ts +2 -0
  31. package/dist/components/form/FormToggle.svelte +4 -0
  32. package/dist/components/form/FormToggle.svelte.d.ts +2 -0
  33. package/dist/components/form/index.d.ts +1 -0
  34. package/dist/components/form/index.js +2 -0
  35. package/dist/components/form/templateAutocomplete.d.ts +38 -0
  36. package/dist/components/form/templateAutocomplete.js +309 -0
  37. package/dist/components/form/types.d.ts +39 -2
  38. package/dist/components/form/types.js +1 -1
  39. package/dist/components/layouts/MainLayout.svelte +5 -2
  40. package/dist/components/nodes/GatewayNode.svelte +0 -8
  41. package/dist/components/nodes/SimpleNode.svelte +2 -3
  42. package/dist/components/nodes/WorkflowNode.svelte +0 -8
  43. package/dist/components/playground/Playground.svelte +43 -38
  44. package/dist/editor/index.d.ts +3 -1
  45. package/dist/editor/index.js +5 -1
  46. package/dist/helpers/workflowEditorHelper.js +1 -2
  47. package/dist/registry/nodeComponentRegistry.d.ts +9 -9
  48. package/dist/registry/nodeComponentRegistry.js +10 -10
  49. package/dist/services/autoSaveService.js +5 -5
  50. package/dist/services/historyService.d.ts +207 -0
  51. package/dist/services/historyService.js +317 -0
  52. package/dist/services/settingsService.d.ts +2 -2
  53. package/dist/services/settingsService.js +15 -21
  54. package/dist/services/toastService.d.ts +1 -1
  55. package/dist/services/toastService.js +10 -10
  56. package/dist/services/variableService.d.ts +100 -0
  57. package/dist/services/variableService.js +367 -0
  58. package/dist/stores/historyStore.d.ts +133 -0
  59. package/dist/stores/historyStore.js +188 -0
  60. package/dist/stores/settingsStore.d.ts +1 -1
  61. package/dist/stores/settingsStore.js +40 -42
  62. package/dist/stores/themeStore.d.ts +2 -2
  63. package/dist/stores/themeStore.js +30 -32
  64. package/dist/stores/workflowStore.d.ts +52 -2
  65. package/dist/stores/workflowStore.js +102 -2
  66. package/dist/styles/base.css +28 -8
  67. package/dist/styles/toast.css +3 -1
  68. package/dist/styles/tokens.css +2 -2
  69. package/dist/types/index.d.ts +120 -0
  70. package/dist/types/settings.d.ts +3 -3
  71. package/dist/types/settings.js +13 -19
  72. package/dist/utils/colors.js +17 -17
  73. package/dist/utils/nodeTypes.d.ts +15 -10
  74. package/dist/utils/nodeTypes.js +24 -22
  75. package/package.json +1 -1
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Template Variable Autocomplete Extension for CodeMirror
3
+ * Provides autocomplete suggestions for {{ variable }} syntax in template editors.
4
+ *
5
+ * Features:
6
+ * - Triggers on `{{` to show top-level variables
7
+ * - Triggers on `.` to show child properties for objects
8
+ * - Triggers on `[` to show array index options
9
+ * - Supports deep nesting (e.g., `user.address.city`)
10
+ *
11
+ * @module components/form/templateAutocomplete
12
+ */
13
+ import { autocompletion } from "@codemirror/autocomplete";
14
+ import { getChildVariables, getArrayIndexSuggestions, isArrayVariable, hasChildren } from "../../services/variableService.js";
15
+ /**
16
+ * Icon type hints for different variable types in autocomplete dropdown.
17
+ */
18
+ const TYPE_ICONS = {
19
+ string: "𝑆",
20
+ number: "#",
21
+ integer: "#",
22
+ float: "#",
23
+ boolean: "☑",
24
+ array: "[]",
25
+ object: "{}",
26
+ mixed: "⋯"
27
+ };
28
+ /**
29
+ * Extracts the current variable path being typed inside {{ }}.
30
+ * Returns null if cursor is not inside a template expression.
31
+ *
32
+ * @param text - The full document text
33
+ * @param pos - Current cursor position
34
+ * @returns Object with the path and start position, or null
35
+ *
36
+ * @example
37
+ * For text "Hello {{ user.name }}" with cursor after "user."
38
+ * Returns { path: "user.", startPos: 9 }
39
+ */
40
+ function extractVariablePath(text, pos) {
41
+ // Look backwards from cursor to find the opening {{
42
+ let openBracePos = -1;
43
+ let searchPos = pos - 1;
44
+ while (searchPos >= 0) {
45
+ // Check for opening {{
46
+ if (text[searchPos] === "{" && searchPos > 0 && text[searchPos - 1] === "{") {
47
+ openBracePos = searchPos - 1;
48
+ break;
49
+ }
50
+ // Check for closing }} - means we're outside an expression
51
+ if (text[searchPos] === "}" && searchPos > 0 && text[searchPos - 1] === "}") {
52
+ return null;
53
+ }
54
+ searchPos--;
55
+ }
56
+ if (openBracePos === -1) {
57
+ return null;
58
+ }
59
+ // Check if there's a closing }} after cursor (still inside expression)
60
+ const afterCursor = text.slice(pos);
61
+ const closingMatch = afterCursor.match(/^\s*\}\}/);
62
+ const hasClosing = closingMatch !== null;
63
+ // Extract the content between {{ and cursor
64
+ const contentStart = openBracePos + 2;
65
+ const content = text.slice(contentStart, pos).trimStart();
66
+ return {
67
+ path: content,
68
+ startPos: contentStart + (text.slice(contentStart, pos).length - text.slice(contentStart, pos).trimStart().length),
69
+ isInsideExpression: true
70
+ };
71
+ }
72
+ /**
73
+ * Determines the completion type based on the current input.
74
+ *
75
+ * @param path - The current variable path being typed
76
+ * @returns The type of completion to provide
77
+ */
78
+ function getCompletionType(path) {
79
+ // Empty or only whitespace - show top-level variables
80
+ if (path.trim() === "") {
81
+ return { type: "top-level" };
82
+ }
83
+ // Ends with [ - show array indices
84
+ if (path.endsWith("[")) {
85
+ const parentPath = path.slice(0, -1);
86
+ return { type: "array-index", parentPath };
87
+ }
88
+ // Ends with . - show child properties
89
+ if (path.endsWith(".")) {
90
+ const parentPath = path.slice(0, -1);
91
+ return { type: "property", parentPath };
92
+ }
93
+ // Otherwise, we're typing a variable name - show matching options
94
+ const lastDotIndex = path.lastIndexOf(".");
95
+ const lastBracketIndex = path.lastIndexOf("[");
96
+ const lastSeparator = Math.max(lastDotIndex, lastBracketIndex);
97
+ if (lastSeparator === -1) {
98
+ // Typing at top level
99
+ return { type: "top-level" };
100
+ }
101
+ // Extract parent path based on separator
102
+ if (lastDotIndex > lastBracketIndex) {
103
+ // Last separator was a dot
104
+ return { type: "property", parentPath: path.slice(0, lastDotIndex) };
105
+ }
106
+ else {
107
+ // Last separator was a bracket
108
+ return { type: "array-index", parentPath: path.slice(0, lastBracketIndex) };
109
+ }
110
+ }
111
+ /**
112
+ * Converts a TemplateVariable to a CodeMirror Completion object.
113
+ *
114
+ * @param variable - The template variable
115
+ * @param prefix - Prefix to add to the completion label
116
+ * @returns A CodeMirror Completion object
117
+ */
118
+ function variableToCompletion(variable, prefix = "") {
119
+ const icon = TYPE_ICONS[variable.type] ?? TYPE_ICONS.mixed;
120
+ const hasChildProps = variable.properties && Object.keys(variable.properties).length > 0;
121
+ const isArray = variable.type === "array";
122
+ // Add indicator if variable can be drilled into
123
+ let suffix = "";
124
+ if (hasChildProps)
125
+ suffix = ".";
126
+ else if (isArray)
127
+ suffix = "[";
128
+ return {
129
+ label: `${prefix}${variable.name}`,
130
+ displayLabel: `${icon} ${variable.label ?? variable.name}${suffix ? " " + suffix : ""}`,
131
+ detail: variable.type,
132
+ info: variable.description,
133
+ type: "variable",
134
+ boost: hasChildProps || isArray ? 1 : 0 // Boost drillable variables
135
+ };
136
+ }
137
+ /**
138
+ * Creates the completion source function for template variables.
139
+ *
140
+ * @param schema - The variable schema containing available variables
141
+ * @returns A completion source function for CodeMirror
142
+ */
143
+ function createTemplateCompletionSource(schema) {
144
+ return (context) => {
145
+ const { state, pos } = context;
146
+ const text = state.doc.toString();
147
+ // Check if we're inside a {{ }} expression
148
+ const pathInfo = extractVariablePath(text, pos);
149
+ if (!pathInfo) {
150
+ // Check if user just typed {{
151
+ const beforeCursor = text.slice(Math.max(0, pos - 2), pos);
152
+ if (beforeCursor === "{{") {
153
+ // Show top-level variables
154
+ const options = Object.values(schema.variables).map((v) => variableToCompletion(v));
155
+ return {
156
+ from: pos,
157
+ options,
158
+ validFor: /^[\w.[\]]*$/
159
+ };
160
+ }
161
+ return null;
162
+ }
163
+ const { path, startPos } = pathInfo;
164
+ const completionType = getCompletionType(path);
165
+ let options = [];
166
+ let from = pos;
167
+ switch (completionType.type) {
168
+ case "top-level": {
169
+ // Show all top-level variables
170
+ const currentWord = path.trim();
171
+ options = Object.values(schema.variables)
172
+ .filter((v) => currentWord === "" || v.name.toLowerCase().startsWith(currentWord.toLowerCase()))
173
+ .map((v) => variableToCompletion(v));
174
+ // Calculate from position for replacement
175
+ from = startPos + (path.length - path.trimStart().length);
176
+ break;
177
+ }
178
+ case "property": {
179
+ // Show child properties of the parent
180
+ const children = getChildVariables(schema, completionType.parentPath);
181
+ const currentWord = path.slice(path.lastIndexOf(".") + 1);
182
+ options = children
183
+ .filter((v) => currentWord === "" || v.name.toLowerCase().startsWith(currentWord.toLowerCase()))
184
+ .map((v) => variableToCompletion(v));
185
+ // From should be right after the last dot
186
+ from = startPos + path.lastIndexOf(".") + 1;
187
+ break;
188
+ }
189
+ case "array-index": {
190
+ // Check if the parent is actually an array
191
+ if (isArrayVariable(schema, completionType.parentPath)) {
192
+ const indices = getArrayIndexSuggestions(5);
193
+ const currentIndex = path.slice(path.lastIndexOf("[") + 1);
194
+ options = indices
195
+ .filter((idx) => currentIndex === "" || idx.startsWith(currentIndex))
196
+ .map((idx) => ({
197
+ label: idx,
198
+ displayLabel: idx === "*]" ? "* (all items)" : `[${idx}`,
199
+ detail: idx === "*]" ? "Iterate all items" : `Index ${idx.slice(0, -1)}`,
200
+ type: "keyword"
201
+ }));
202
+ // From should be right after the [
203
+ from = startPos + path.lastIndexOf("[") + 1;
204
+ }
205
+ break;
206
+ }
207
+ }
208
+ if (options.length === 0) {
209
+ return null;
210
+ }
211
+ return {
212
+ from,
213
+ options,
214
+ validFor: /^[\w\]*]*$/
215
+ };
216
+ };
217
+ }
218
+ /**
219
+ * Creates a CodeMirror extension for template variable autocomplete.
220
+ *
221
+ * @param schema - The variable schema containing available variables
222
+ * @returns A CodeMirror extension that provides autocomplete
223
+ *
224
+ * @example
225
+ * ```typescript
226
+ * const extensions = [
227
+ * // ... other extensions
228
+ * createTemplateAutocomplete(variableSchema)
229
+ * ];
230
+ * ```
231
+ */
232
+ export function createTemplateAutocomplete(schema) {
233
+ return autocompletion({
234
+ override: [createTemplateCompletionSource(schema)],
235
+ activateOnTyping: true,
236
+ defaultKeymap: true,
237
+ optionClass: () => "cm-template-autocomplete-option",
238
+ icons: false, // We use our own icons in displayLabel
239
+ addToOptions: [
240
+ {
241
+ render: (completion) => {
242
+ const el = document.createElement("span");
243
+ el.className = "cm-template-autocomplete-info";
244
+ if (completion.info) {
245
+ el.textContent = String(completion.info);
246
+ }
247
+ return el;
248
+ },
249
+ position: 100
250
+ }
251
+ ]
252
+ });
253
+ }
254
+ /**
255
+ * Creates a simple autocomplete extension that triggers when user types {{
256
+ * and shows top-level variables only (no drilling).
257
+ * Used as a fallback when full variable schema is not available.
258
+ *
259
+ * @param variableHints - Simple array of variable names
260
+ * @returns A CodeMirror extension that provides basic autocomplete
261
+ */
262
+ export function createSimpleTemplateAutocomplete(variableHints) {
263
+ const completionSource = (context) => {
264
+ const { state, pos } = context;
265
+ const text = state.doc.toString();
266
+ // Check if we're inside a {{ }} expression
267
+ const pathInfo = extractVariablePath(text, pos);
268
+ if (!pathInfo) {
269
+ // Check if user just typed {{
270
+ const beforeCursor = text.slice(Math.max(0, pos - 2), pos);
271
+ if (beforeCursor === "{{") {
272
+ const options = variableHints.map((hint) => ({
273
+ label: hint,
274
+ displayLabel: `⋯ ${hint}`,
275
+ type: "variable"
276
+ }));
277
+ return {
278
+ from: pos,
279
+ options,
280
+ validFor: /^[\w]*$/
281
+ };
282
+ }
283
+ return null;
284
+ }
285
+ // Filter based on current input
286
+ const currentWord = pathInfo.path.trim();
287
+ const options = variableHints
288
+ .filter((hint) => currentWord === "" || hint.toLowerCase().startsWith(currentWord.toLowerCase()))
289
+ .map((hint) => ({
290
+ label: hint,
291
+ displayLabel: `⋯ ${hint}`,
292
+ type: "variable"
293
+ }));
294
+ if (options.length === 0) {
295
+ return null;
296
+ }
297
+ return {
298
+ from: pathInfo.startPos + (pathInfo.path.length - pathInfo.path.trimStart().length),
299
+ options,
300
+ validFor: /^[\w]*$/
301
+ };
302
+ };
303
+ return autocompletion({
304
+ override: [completionSource],
305
+ activateOnTyping: true,
306
+ defaultKeymap: true,
307
+ icons: false
308
+ });
309
+ }
@@ -5,7 +5,7 @@
5
5
  * These types provide a foundation for dynamic form rendering based on JSON Schema
6
6
  * and support extensibility for complex field types like arrays and objects.
7
7
  */
8
- import type { AutocompleteConfig } from '../../types/index.js';
8
+ import type { AutocompleteConfig, VariableSchema, TemplateVariablesConfig } from '../../types/index.js';
9
9
  /**
10
10
  * Supported field types for form rendering
11
11
  * Aligned with JSON Schema specification (draft-07)
@@ -220,7 +220,22 @@ export interface TemplateEditorFieldProps extends BaseFieldProps {
220
220
  darkTheme?: boolean;
221
221
  /** Editor height in pixels or CSS value */
222
222
  height?: string;
223
- /** Available variable names for hints (optional) */
223
+ /**
224
+ * Configuration for template variable autocomplete.
225
+ * Controls which variables are available and how they are displayed.
226
+ */
227
+ variables?: TemplateVariablesConfig;
228
+ /**
229
+ * Variable schema for advanced autocomplete with nested drilling.
230
+ * When provided, enables dot notation (user.name) and array access (items[0]).
231
+ * @deprecated Use `variables.schema` instead
232
+ */
233
+ variableSchema?: VariableSchema;
234
+ /**
235
+ * Simple variable names for basic hints (backward compatible).
236
+ * Used when variableSchema is not provided.
237
+ * @deprecated Use `variables.schema` instead
238
+ */
224
239
  variableHints?: string[];
225
240
  /** Placeholder variable example for the hint */
226
241
  placeholderExample?: string;
@@ -318,6 +333,28 @@ export interface FieldSchema {
318
333
  required?: string[];
319
334
  /** Autocomplete configuration for fetching suggestions from callback URL */
320
335
  autocomplete?: AutocompleteConfig;
336
+ /**
337
+ * Whether the field is read-only (JSON Schema readOnly keyword).
338
+ * When true, the field is displayed but cannot be edited.
339
+ */
340
+ readOnly?: boolean;
341
+ /**
342
+ * Configuration for template variable autocomplete.
343
+ * Controls which input ports provide variables and how they are displayed.
344
+ *
345
+ * @example
346
+ * ```json
347
+ * {
348
+ * "type": "string",
349
+ * "format": "template",
350
+ * "variables": {
351
+ * "ports": ["data", "context"],
352
+ * "showHints": true
353
+ * }
354
+ * }
355
+ * ```
356
+ */
357
+ variables?: TemplateVariablesConfig;
321
358
  /** Allow additional properties not defined by the schema */
322
359
  [key: string]: unknown;
323
360
  }
@@ -15,7 +15,7 @@ export function isFieldOptionArray(options) {
15
15
  * Type guard to check if items are OneOfItem objects (JSON Schema oneOf pattern)
16
16
  */
17
17
  export function isOneOfArray(items) {
18
- return items.length > 0 && typeof items[0] === 'object' && items[0] !== null && 'const' in items[0];
18
+ return (items.length > 0 && typeof items[0] === 'object' && items[0] !== null && 'const' in items[0]);
19
19
  }
20
20
  /**
21
21
  * Convert JSON Schema oneOf items to FieldOption format
@@ -377,7 +377,9 @@
377
377
  aria-label="Resize bottom panel"
378
378
  tabindex="0"
379
379
  >
380
- <div class="flowdrop-main-layout__divider-handle flowdrop-main-layout__divider-handle--horizontal"></div>
380
+ <div
381
+ class="flowdrop-main-layout__divider-handle flowdrop-main-layout__divider-handle--horizontal"
382
+ ></div>
381
383
  </div>
382
384
  {/if}
383
385
 
@@ -630,7 +632,8 @@
630
632
  transform: scaleX(1.2);
631
633
  }
632
634
 
633
- .flowdrop-main-layout__divider--bottom.flowdrop-main-layout__divider--active .flowdrop-main-layout__divider-handle--horizontal {
635
+ .flowdrop-main-layout__divider--bottom.flowdrop-main-layout__divider--active
636
+ .flowdrop-main-layout__divider-handle--horizontal {
634
637
  transform: scaleX(1.4);
635
638
  }
636
639
 
@@ -511,10 +511,6 @@
511
511
  gap: var(--fd-space-2);
512
512
  }
513
513
 
514
- .flowdrop-gap--3 {
515
- gap: var(--fd-space-3);
516
- }
517
-
518
514
  .flowdrop-items--center {
519
515
  align-items: center;
520
516
  }
@@ -556,10 +552,6 @@
556
552
  white-space: nowrap;
557
553
  }
558
554
 
559
- .flowdrop-mt--1 {
560
- margin-top: var(--fd-space-1);
561
- }
562
-
563
555
  .flowdrop-text--right {
564
556
  text-align: right;
565
557
  }
@@ -283,11 +283,10 @@
283
283
  color: var(--fd-foreground);
284
284
  }
285
285
 
286
- /* Normal layout (default) */
286
+ /* Normal layout (default): min-height allows variable height for longer descriptions */
287
287
  .flowdrop-simple-node--normal {
288
288
  width: var(--fd-node-default-width);
289
- height: var(--fd-node-simple-height);
290
- overflow: hidden;
289
+ min-height: var(--fd-node-simple-height);
291
290
  }
292
291
 
293
292
  .flowdrop-simple-node:hover {
@@ -509,10 +509,6 @@
509
509
  gap: var(--fd-space-2);
510
510
  }
511
511
 
512
- .flowdrop-gap--3 {
513
- gap: var(--fd-space-3);
514
- }
515
-
516
512
  .flowdrop-items--center {
517
513
  align-items: center;
518
514
  }
@@ -549,10 +545,6 @@
549
545
  white-space: nowrap;
550
546
  }
551
547
 
552
- .flowdrop-mt--1 {
553
- margin-top: var(--fd-space-1);
554
- }
555
-
556
548
  .flowdrop-text--right {
557
549
  text-align: right;
558
550
  }
@@ -556,43 +556,43 @@
556
556
  <p class="playground__sessions-hint">Click a session to load it</p>
557
557
  {/if}
558
558
  <div class="playground__sessions">
559
- {#if $sessions.length === 0 && !$isLoading}
560
- <div class="playground__sessions-empty">
561
- <span>No sessions yet</span>
562
- </div>
563
- {:else}
564
- {#each $sessions as session (session.id)}
565
- <div
566
- class="playground__session"
567
- class:playground__session--active={$currentSession?.id === session.id}
568
- role="button"
569
- tabindex="0"
570
- title="Click to load this session"
571
- aria-label="Load session: {session.name}"
572
- onclick={() => handleSelectSession(session.id)}
573
- onkeydown={(e) => e.key === 'Enter' && handleSelectSession(session.id)}
574
- >
575
- <span class="playground__session-name" title={session.name}>
576
- {session.name}
577
- </span>
578
- <button
579
- type="button"
580
- class="playground__session-menu"
581
- class:playground__session-menu--delete={pendingDeleteId === session.id}
582
- onclick={(e) => handleDeleteClick(e, session.id)}
583
- title={pendingDeleteId === session.id
584
- ? 'Click to confirm delete'
585
- : 'Delete session'}
586
- >
587
- {#if pendingDeleteId === session.id}
588
- <Icon icon="mdi:check" />
589
- {:else}
590
- <Icon icon="mdi:dots-horizontal" />
591
- {/if}
592
- </button>
559
+ {#if $sessions.length === 0 && !$isLoading}
560
+ <div class="playground__sessions-empty">
561
+ <span>No sessions yet</span>
593
562
  </div>
594
- {/each}
595
- {/if}
563
+ {:else}
564
+ {#each $sessions as session (session.id)}
565
+ <div
566
+ class="playground__session"
567
+ class:playground__session--active={$currentSession?.id === session.id}
568
+ role="button"
569
+ tabindex="0"
570
+ title="Click to load this session"
571
+ aria-label="Load session: {session.name}"
572
+ onclick={() => handleSelectSession(session.id)}
573
+ onkeydown={(e) => e.key === 'Enter' && handleSelectSession(session.id)}
574
+ >
575
+ <span class="playground__session-name" title={session.name}>
576
+ {session.name}
577
+ </span>
578
+ <button
579
+ type="button"
580
+ class="playground__session-menu"
581
+ class:playground__session-menu--delete={pendingDeleteId === session.id}
582
+ onclick={(e) => handleDeleteClick(e, session.id)}
583
+ title={pendingDeleteId === session.id
584
+ ? 'Click to confirm delete'
585
+ : 'Delete session'}
586
+ >
587
+ {#if pendingDeleteId === session.id}
588
+ <Icon icon="mdi:check" />
589
+ {:else}
590
+ <Icon icon="mdi:dots-horizontal" />
591
+ {/if}
592
+ </button>
593
+ </div>
594
+ {/each}
595
+ {/if}
596
596
  </div>
597
597
  </div>
598
598
  </div>
@@ -774,7 +774,10 @@
774
774
  font-size: 0.875rem;
775
775
  font-weight: 500;
776
776
  cursor: pointer;
777
- transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
777
+ transition:
778
+ background-color 0.15s ease,
779
+ border-color 0.15s ease,
780
+ transform 0.1s ease;
778
781
  box-sizing: border-box;
779
782
  }
780
783
 
@@ -839,7 +842,9 @@
839
842
  border-radius: var(--fd-radius-md);
840
843
  border-left: 3px solid transparent;
841
844
  cursor: pointer;
842
- transition: background-color 0.15s ease, border-left-color 0.15s ease;
845
+ transition:
846
+ background-color 0.15s ease,
847
+ border-left-color 0.15s ease;
843
848
  }
844
849
 
845
850
  .playground__session:hover {
@@ -63,7 +63,9 @@ export { default as MessageBubble } from '../components/playground/MessageBubble
63
63
  export { mountWorkflowEditor, mountFlowDropApp, unmountFlowDropApp } from '../svelte-app.js';
64
64
  export { nodeComponentRegistry, createNamespacedType, parseNamespacedType, BUILTIN_NODE_COMPONENTS, BUILTIN_NODE_TYPES, FLOWDROP_SOURCE, registerBuiltinNodes, areBuiltinsRegistered, isBuiltinType, getBuiltinTypes, resolveBuiltinAlias, registerFlowDropPlugin, unregisterFlowDropPlugin, registerCustomNode, createPlugin, isValidNamespace, getRegisteredPlugins, getPluginNodeCount } from '../registry/index.js';
65
65
  export { EdgeStylingHelper, NodeOperationsHelper, WorkflowOperationsHelper, ConfigurationHelper } from '../helpers/workflowEditorHelper.js';
66
- export { workflowStore, workflowActions, workflowId, workflowName, workflowNodes, workflowEdges, workflowMetadata, workflowChanged, workflowValidation, workflowMetadataChanged, connectedHandles, isDirtyStore, isDirty, markAsSaved, getWorkflow as getWorkflowFromStore, setOnDirtyStateChange, setOnWorkflowChange } from '../stores/workflowStore.js';
66
+ export { workflowStore, workflowActions, workflowId, workflowName, workflowNodes, workflowEdges, workflowMetadata, workflowChanged, workflowValidation, workflowMetadataChanged, connectedHandles, isDirtyStore, isDirty, markAsSaved, getWorkflow as getWorkflowFromStore, setOnDirtyStateChange, setOnWorkflowChange, setHistoryEnabled, isHistoryEnabled, setRestoringFromHistory } from '../stores/workflowStore.js';
67
+ export { historyStateStore, canUndo, canRedo, historyActions, setOnRestoreCallback, historyService, HistoryService } from '../stores/historyStore.js';
68
+ export type { HistoryEntry, HistoryState, PushOptions } from '../stores/historyStore.js';
67
69
  export * from '../services/api.js';
68
70
  export { showSuccess, showError, showWarning, showInfo, showLoading, dismissToast, dismissAllToasts, showPromise, showConfirmation, apiToasts, workflowToasts, pipelineToasts } from '../services/toastService.js';
69
71
  export { NodeExecutionService, nodeExecutionService } from '../services/nodeExecutionService.js';
@@ -98,7 +98,11 @@ export { EdgeStylingHelper, NodeOperationsHelper, WorkflowOperationsHelper, Conf
98
98
  // ============================================================================
99
99
  export { workflowStore, workflowActions, workflowId, workflowName, workflowNodes, workflowEdges, workflowMetadata, workflowChanged, workflowValidation, workflowMetadataChanged, connectedHandles,
100
100
  // Dirty state tracking
101
- isDirtyStore, isDirty, markAsSaved, getWorkflow as getWorkflowFromStore, setOnDirtyStateChange, setOnWorkflowChange } from '../stores/workflowStore.js';
101
+ isDirtyStore, isDirty, markAsSaved, getWorkflow as getWorkflowFromStore, setOnDirtyStateChange, setOnWorkflowChange,
102
+ // History control
103
+ setHistoryEnabled, isHistoryEnabled, setRestoringFromHistory } from '../stores/workflowStore.js';
104
+ // History Store and Service
105
+ export { historyStateStore, canUndo, canRedo, historyActions, setOnRestoreCallback, historyService, HistoryService } from '../stores/historyStore.js';
102
106
  // ============================================================================
103
107
  // Services
104
108
  // ============================================================================
@@ -189,8 +189,7 @@ export class EdgeStylingHelper {
189
189
  break;
190
190
  case 'trigger':
191
191
  // Trigger edges: solid dark line for control flow
192
- edge.style =
193
- 'stroke: var(--fd-edge-trigger); stroke-width: var(--fd-edge-trigger-width);';
192
+ edge.style = 'stroke: var(--fd-edge-trigger); stroke-width: var(--fd-edge-trigger-width);';
194
193
  edge.class = 'flowdrop--edge--trigger';
195
194
  edge.markerEnd = {
196
195
  type: MarkerType.ArrowClosed,
@@ -226,22 +226,22 @@ declare class NodeComponentRegistry {
226
226
  */
227
227
  private notifyListeners;
228
228
  /**
229
- * Get enum options for config forms.
230
- * Returns arrays suitable for JSON Schema enum/enumNames.
229
+ * Get oneOf options for config forms.
230
+ * Returns array suitable for JSON Schema oneOf with const/title.
231
231
  *
232
232
  * @param filterFn - Optional filter function to limit which types are included
233
- * @returns Object with enum (type values) and enumNames (display names)
233
+ * @returns Array of oneOf items with const (type value) and title (display name)
234
234
  *
235
235
  * @example
236
236
  * ```typescript
237
- * const { enum: types, enumNames } = nodeComponentRegistry.getEnumOptions();
238
- * // Use in configSchema: { type: "string", enum: types, enumNames }
237
+ * const oneOf = nodeComponentRegistry.getOneOfOptions();
238
+ * // Use in configSchema: { type: "string", oneOf }
239
239
  * ```
240
240
  */
241
- getEnumOptions(filterFn?: (reg: NodeComponentRegistration) => boolean): {
242
- enum: string[];
243
- enumNames: string[];
244
- };
241
+ getOneOfOptions(filterFn?: (reg: NodeComponentRegistration) => boolean): Array<{
242
+ const: string;
243
+ title: string;
244
+ }>;
245
245
  /**
246
246
  * Get the status position for a node type.
247
247
  *
@@ -215,24 +215,24 @@ class NodeComponentRegistry {
215
215
  }
216
216
  }
217
217
  /**
218
- * Get enum options for config forms.
219
- * Returns arrays suitable for JSON Schema enum/enumNames.
218
+ * Get oneOf options for config forms.
219
+ * Returns array suitable for JSON Schema oneOf with const/title.
220
220
  *
221
221
  * @param filterFn - Optional filter function to limit which types are included
222
- * @returns Object with enum (type values) and enumNames (display names)
222
+ * @returns Array of oneOf items with const (type value) and title (display name)
223
223
  *
224
224
  * @example
225
225
  * ```typescript
226
- * const { enum: types, enumNames } = nodeComponentRegistry.getEnumOptions();
227
- * // Use in configSchema: { type: "string", enum: types, enumNames }
226
+ * const oneOf = nodeComponentRegistry.getOneOfOptions();
227
+ * // Use in configSchema: { type: "string", oneOf }
228
228
  * ```
229
229
  */
230
- getEnumOptions(filterFn) {
230
+ getOneOfOptions(filterFn) {
231
231
  const registrations = filterFn ? this.getAll().filter(filterFn) : this.getAll();
232
- return {
233
- enum: registrations.map((r) => r.type),
234
- enumNames: registrations.map((r) => r.displayName)
235
- };
232
+ return registrations.map((r) => ({
233
+ const: r.type,
234
+ title: r.displayName
235
+ }));
236
236
  }
237
237
  /**
238
238
  * Get the status position for a node type.