@d34dman/flowdrop 0.0.61 → 0.0.63

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 (204) hide show
  1. package/README.md +6 -0
  2. package/dist/adapters/WorkflowAdapter.d.ts +1 -1
  3. package/dist/adapters/agentspec/AgentSpecAdapter.js +3 -1
  4. package/dist/api/client.d.ts +4 -0
  5. package/dist/api/client.js +6 -1
  6. package/dist/api/enhanced-client.js +7 -6
  7. package/dist/components/App.svelte +143 -219
  8. package/dist/components/CanvasBanner.stories.svelte +25 -0
  9. package/dist/components/CanvasBanner.stories.svelte.d.ts +27 -0
  10. package/dist/components/CanvasBanner.svelte +2 -2
  11. package/dist/components/ConfigForm.svelte +37 -36
  12. package/dist/components/ConfigPanel.stories.svelte +38 -0
  13. package/dist/components/ConfigPanel.stories.svelte.d.ts +27 -0
  14. package/dist/components/ConfigPanel.svelte +2 -2
  15. package/dist/components/ConnectionLine.svelte +2 -2
  16. package/dist/components/FlowDropZone.svelte +18 -2
  17. package/dist/components/FlowDropZone.svelte.d.ts +2 -0
  18. package/dist/components/LoadingSpinner.stories.svelte +30 -0
  19. package/dist/components/LoadingSpinner.stories.svelte.d.ts +27 -0
  20. package/dist/components/Logo.stories.svelte +22 -0
  21. package/dist/components/Logo.stories.svelte.d.ts +27 -0
  22. package/dist/components/Logo.svelte +33 -13
  23. package/dist/components/Logo.svelte.d.ts +1 -1
  24. package/dist/components/MarkdownDisplay.stories.svelte +21 -0
  25. package/dist/components/MarkdownDisplay.stories.svelte.d.ts +27 -0
  26. package/dist/components/MarkdownDisplay.svelte +4 -3
  27. package/dist/components/Navbar.stories.svelte +41 -0
  28. package/dist/components/Navbar.stories.svelte.d.ts +27 -0
  29. package/dist/components/Navbar.svelte +4 -4
  30. package/dist/components/NodeSidebar.svelte +12 -12
  31. package/dist/components/NodeStatusOverlay.stories.svelte +74 -0
  32. package/dist/components/NodeStatusOverlay.stories.svelte.d.ts +27 -0
  33. package/dist/components/PipelineStatus.svelte +11 -4
  34. package/dist/components/PortCoordinateTracker.svelte +1 -1
  35. package/dist/components/SchemaForm.stories.svelte +101 -0
  36. package/dist/components/SchemaForm.stories.svelte.d.ts +27 -0
  37. package/dist/components/SchemaForm.svelte +17 -12
  38. package/dist/components/SettingsModal.svelte +3 -3
  39. package/dist/components/SettingsPanel.svelte +23 -22
  40. package/dist/components/StatusIcon.stories.svelte +60 -0
  41. package/dist/components/StatusIcon.stories.svelte.d.ts +27 -0
  42. package/dist/components/StatusIcon.svelte +7 -0
  43. package/dist/components/StatusLabel.stories.svelte +17 -0
  44. package/dist/components/StatusLabel.stories.svelte.d.ts +27 -0
  45. package/dist/components/ThemeToggle.stories.svelte +25 -0
  46. package/dist/components/ThemeToggle.stories.svelte.d.ts +27 -0
  47. package/dist/components/ThemeToggle.svelte +8 -8
  48. package/dist/components/UniversalNode.svelte +1 -1
  49. package/dist/components/WorkflowEditor.svelte +298 -298
  50. package/dist/components/form/FormAutocomplete.svelte +20 -19
  51. package/dist/components/form/FormCheckboxGroup.stories.svelte +28 -0
  52. package/dist/components/form/FormCheckboxGroup.stories.svelte.d.ts +27 -0
  53. package/dist/components/form/FormField.svelte +3 -3
  54. package/dist/components/form/FormFieldLight.svelte +2 -2
  55. package/dist/components/form/FormFieldWrapper.stories.svelte +31 -0
  56. package/dist/components/form/FormFieldWrapper.stories.svelte.d.ts +27 -0
  57. package/dist/components/form/FormFieldset.svelte +7 -7
  58. package/dist/components/form/FormNumberField.stories.svelte +33 -0
  59. package/dist/components/form/FormNumberField.stories.svelte.d.ts +27 -0
  60. package/dist/components/form/FormRangeField.stories.svelte +31 -0
  61. package/dist/components/form/FormRangeField.stories.svelte.d.ts +27 -0
  62. package/dist/components/form/FormSelect.stories.svelte +50 -0
  63. package/dist/components/form/FormSelect.stories.svelte.d.ts +27 -0
  64. package/dist/components/form/FormTemplateEditor.svelte +2 -1
  65. package/dist/components/form/FormTextField.stories.svelte +30 -0
  66. package/dist/components/form/FormTextField.stories.svelte.d.ts +27 -0
  67. package/dist/components/form/FormTextarea.stories.svelte +31 -0
  68. package/dist/components/form/FormTextarea.stories.svelte.d.ts +27 -0
  69. package/dist/components/form/FormToggle.stories.svelte +30 -0
  70. package/dist/components/form/FormToggle.stories.svelte.d.ts +27 -0
  71. package/dist/components/form/FormUISchemaRenderer.svelte +1 -1
  72. package/dist/components/form/types.d.ts +15 -47
  73. package/dist/components/interrupt/ChoicePrompt.stories.svelte +43 -0
  74. package/dist/components/interrupt/ChoicePrompt.stories.svelte.d.ts +27 -0
  75. package/dist/components/interrupt/ChoicePrompt.svelte +24 -24
  76. package/dist/components/interrupt/ConfirmationPrompt.stories.svelte +49 -0
  77. package/dist/components/interrupt/ConfirmationPrompt.stories.svelte.d.ts +27 -0
  78. package/dist/components/interrupt/ConfirmationPrompt.svelte +19 -19
  79. package/dist/components/interrupt/FormPrompt.svelte +15 -15
  80. package/dist/components/interrupt/InterruptBubble.svelte +202 -236
  81. package/dist/components/interrupt/InterruptBubble.svelte.d.ts +1 -1
  82. package/dist/components/interrupt/ReviewPrompt.stories.svelte +46 -0
  83. package/dist/components/interrupt/ReviewPrompt.stories.svelte.d.ts +27 -0
  84. package/dist/components/interrupt/ReviewPrompt.svelte +842 -0
  85. package/dist/components/interrupt/ReviewPrompt.svelte.d.ts +23 -0
  86. package/dist/components/interrupt/TextInputPrompt.stories.svelte +34 -0
  87. package/dist/components/interrupt/TextInputPrompt.stories.svelte.d.ts +27 -0
  88. package/dist/components/interrupt/TextInputPrompt.svelte +21 -21
  89. package/dist/components/nodes/GatewayNode.stories.svelte +76 -0
  90. package/dist/components/nodes/GatewayNode.stories.svelte.d.ts +26 -0
  91. package/dist/components/nodes/GatewayNode.svelte +19 -17
  92. package/dist/components/nodes/IdeaNode.stories.svelte +48 -0
  93. package/dist/components/nodes/IdeaNode.stories.svelte.d.ts +26 -0
  94. package/dist/components/nodes/IdeaNode.svelte +10 -26
  95. package/dist/components/nodes/NotesNode.stories.svelte +69 -0
  96. package/dist/components/nodes/NotesNode.stories.svelte.d.ts +26 -0
  97. package/dist/components/nodes/NotesNode.svelte +8 -8
  98. package/dist/components/nodes/SimpleNode.stories.svelte +101 -0
  99. package/dist/components/nodes/SimpleNode.stories.svelte.d.ts +26 -0
  100. package/dist/components/nodes/SimpleNode.svelte +16 -24
  101. package/dist/components/nodes/SquareNode.stories.svelte +56 -0
  102. package/dist/components/nodes/SquareNode.stories.svelte.d.ts +26 -0
  103. package/dist/components/nodes/SquareNode.svelte +13 -21
  104. package/dist/components/nodes/TerminalNode.stories.svelte +25 -0
  105. package/dist/components/nodes/TerminalNode.stories.svelte.d.ts +26 -0
  106. package/dist/components/nodes/TerminalNode.svelte +7 -7
  107. package/dist/components/nodes/ToolNode.stories.svelte +71 -0
  108. package/dist/components/nodes/ToolNode.stories.svelte.d.ts +26 -0
  109. package/dist/components/nodes/ToolNode.svelte +7 -15
  110. package/dist/components/nodes/WorkflowNode.stories.svelte +50 -0
  111. package/dist/components/nodes/WorkflowNode.stories.svelte.d.ts +26 -0
  112. package/dist/components/nodes/WorkflowNode.svelte +13 -13
  113. package/dist/components/playground/ChatPanel.svelte +48 -48
  114. package/dist/components/playground/ExecutionLogs.svelte +23 -23
  115. package/dist/components/playground/InputCollector.svelte +24 -24
  116. package/dist/components/playground/MessageBubble.stories.svelte +49 -0
  117. package/dist/components/playground/MessageBubble.stories.svelte.d.ts +27 -0
  118. package/dist/components/playground/MessageBubble.svelte +49 -46
  119. package/dist/components/playground/Playground.svelte +194 -129
  120. package/dist/components/playground/PlaygroundModal.svelte +5 -5
  121. package/dist/components/playground/SessionManager.svelte +26 -26
  122. package/dist/config/constants.d.ts +22 -0
  123. package/dist/config/constants.js +22 -0
  124. package/dist/config/endpoints.d.ts +19 -0
  125. package/dist/config/runtimeConfig.js +2 -1
  126. package/dist/core/index.d.ts +5 -2
  127. package/dist/core/index.js +9 -1
  128. package/dist/editor/index.d.ts +13 -9
  129. package/dist/editor/index.js +15 -11
  130. package/dist/form/code.d.ts +2 -1
  131. package/dist/form/code.js +1 -3
  132. package/dist/form/markdown.d.ts +2 -1
  133. package/dist/form/markdown.js +1 -3
  134. package/dist/helpers/workflowEditorHelper.js +18 -33
  135. package/dist/mocks/app-forms.js +1 -0
  136. package/dist/mocks/app-navigation.js +3 -1
  137. package/dist/mocks/app-stores.d.ts +4 -4
  138. package/dist/playground/index.d.ts +4 -3
  139. package/dist/playground/index.js +12 -10
  140. package/dist/playground/mount.js +6 -13
  141. package/dist/services/agentSpecExecutionService.js +2 -1
  142. package/dist/services/api.js +10 -18
  143. package/dist/services/apiVariableService.js +2 -1
  144. package/dist/services/autoSaveService.d.ts +3 -3
  145. package/dist/services/autoSaveService.js +21 -17
  146. package/dist/services/categoriesApi.js +13 -5
  147. package/dist/services/draftStorage.js +5 -4
  148. package/dist/services/dynamicSchemaService.js +4 -4
  149. package/dist/services/globalSave.d.ts +60 -11
  150. package/dist/services/globalSave.js +160 -83
  151. package/dist/services/historyService.d.ts +2 -1
  152. package/dist/services/historyService.js +7 -3
  153. package/dist/services/interruptService.js +9 -8
  154. package/dist/services/nodeExecutionService.js +14 -6
  155. package/dist/services/playgroundService.js +2 -1
  156. package/dist/services/portConfigApi.js +11 -7
  157. package/dist/services/toastService.d.ts +1 -1
  158. package/dist/services/toastService.js +6 -5
  159. package/dist/services/variableService.js +3 -2
  160. package/dist/settings/index.d.ts +1 -1
  161. package/dist/settings/index.js +1 -1
  162. package/dist/stores/{categoriesStore.d.ts → categoriesStore.svelte.d.ts} +3 -3
  163. package/dist/stores/{categoriesStore.js → categoriesStore.svelte.js} +15 -18
  164. package/dist/stores/editorStateMachine.svelte.d.ts +42 -0
  165. package/dist/stores/editorStateMachine.svelte.js +132 -0
  166. package/dist/stores/{historyStore.d.ts → historyStore.svelte.d.ts} +18 -15
  167. package/dist/stores/{historyStore.js → historyStore.svelte.js} +40 -21
  168. package/dist/stores/{interruptStore.d.ts → interruptStore.svelte.d.ts} +16 -15
  169. package/dist/stores/{interruptStore.js → interruptStore.svelte.js} +85 -94
  170. package/dist/stores/{playgroundStore.d.ts → playgroundStore.svelte.d.ts} +41 -33
  171. package/dist/stores/{playgroundStore.js → playgroundStore.svelte.js} +164 -84
  172. package/dist/stores/{portCoordinateStore.d.ts → portCoordinateStore.svelte.d.ts} +10 -4
  173. package/dist/stores/{portCoordinateStore.js → portCoordinateStore.svelte.js} +38 -35
  174. package/dist/stores/{settingsStore.d.ts → settingsStore.svelte.d.ts} +45 -28
  175. package/dist/stores/{settingsStore.js → settingsStore.svelte.js} +169 -128
  176. package/dist/stores/{workflowStore.d.ts → workflowStore.svelte.d.ts} +101 -65
  177. package/dist/stores/{workflowStore.js → workflowStore.svelte.js} +285 -239
  178. package/dist/stories/CanvasDecorator.svelte +50 -0
  179. package/dist/stories/CanvasDecorator.svelte.d.ts +8 -0
  180. package/dist/stories/NodeDecorator.svelte +74 -0
  181. package/dist/stories/NodeDecorator.svelte.d.ts +8 -0
  182. package/dist/stories/utils.d.ts +93 -0
  183. package/dist/stories/utils.js +122 -0
  184. package/dist/styles/base.css +114 -61
  185. package/dist/styles/toast.css +2 -2
  186. package/dist/styles/tokens.css +250 -185
  187. package/dist/svelte-app.d.ts +0 -6
  188. package/dist/svelte-app.js +13 -31
  189. package/dist/types/index.d.ts +2 -0
  190. package/dist/types/interrupt.d.ts +89 -5
  191. package/dist/types/interrupt.js +13 -1
  192. package/dist/types/playground.d.ts +5 -0
  193. package/dist/types/settings.js +1 -1
  194. package/dist/utils/colors.js +4 -4
  195. package/dist/utils/connections.js +33 -8
  196. package/dist/utils/icons.js +1 -1
  197. package/dist/utils/logger.d.ts +47 -0
  198. package/dist/utils/logger.js +72 -0
  199. package/dist/utils/nodeWrapper.js +1 -1
  200. package/dist/utils/sanitize.d.ts +19 -0
  201. package/dist/utils/sanitize.js +31 -0
  202. package/dist/utils/validation.d.ts +29 -0
  203. package/dist/utils/validation.js +39 -0
  204. package/package.json +243 -232
@@ -1,26 +1,31 @@
1
1
  /**
2
2
  * Global Save Service
3
- * Provides save and export functionality that can be accessed from anywhere in the app
4
- * This allows the main navbar to save workflows without being tied to a specific component
3
+ * Provides save and export functionality that can be accessed from anywhere in the app.
4
+ * This is the single source of truth for save/export logic.
5
+ *
6
+ * App.svelte delegates to globalSaveWorkflow() / globalExportWorkflow() rather than
7
+ * reimplementing the logic, ensuring "blur active element" flushing and metadata
8
+ * construction happen in exactly one place.
5
9
  */
6
- import { get } from 'svelte/store';
7
- import { workflowStore } from '../stores/workflowStore.js';
10
+ import { tick } from 'svelte';
11
+ import { getWorkflowStore, workflowActions, markAsSaved as storeMarkAsSaved } from '../stores/workflowStore.svelte.js';
8
12
  import { workflowApi, setEndpointConfig } from './api.js';
9
13
  import { createEndpointConfig } from '../config/endpoints.js';
10
14
  import { v4 as uuidv4 } from 'uuid';
15
+ import { DEFAULT_WORKFLOW_FORMAT } from '../types/index.js';
11
16
  import { apiToasts, workflowToasts, dismissToast } from './toastService.js';
17
+ import { DEFAULT_FEATURES } from '../types/events.js';
18
+ // ---------------------------------------------------------------------------
19
+ // Internal helpers
20
+ // ---------------------------------------------------------------------------
12
21
  /**
13
- * Ensure API configuration is initialized
22
+ * Ensure API configuration is initialized.
14
23
  * This is needed when the global save function is called from the layout component
15
- * which doesn't initialize the API configuration like the App component does
24
+ * which doesn't initialize the API configuration like the App component does.
16
25
  */
17
26
  async function ensureApiConfiguration() {
18
- // Check if we need to initialize the API configuration
19
- // We'll check if the endpointConfig is already set by importing the api module
20
27
  try {
21
- // Import the api module to check if endpointConfig is already set
22
28
  const { getEndpointConfig } = await import('./api.js');
23
- // Try to get the current configuration
24
29
  const currentConfig = getEndpointConfig();
25
30
  if (currentConfig && currentConfig.baseUrl) {
26
31
  return;
@@ -29,25 +34,15 @@ async function ensureApiConfiguration() {
29
34
  catch {
30
35
  // Could not check existing API configuration, initializing
31
36
  }
32
- // API configuration is not initialized, so let's initialize it
33
- // Use runtime detection to determine appropriate API base URL
34
- const apiBaseUrl = (() => {
35
- // If we're in development (localhost:5173), use relative path
36
- if (typeof window !== 'undefined') {
37
- if (window.location.hostname === 'localhost' && window.location.port === '5173') {
38
- return '/api/flowdrop';
39
- }
40
- // Otherwise, use the current domain
41
- return `${window.location.protocol}//${window.location.host}/api/flowdrop`;
42
- }
43
- // Fallback to relative path
44
- return '/api/flowdrop';
45
- })();
37
+ // API configuration is not initialized derive URL from window.location when available
38
+ const apiBaseUrl = typeof window !== 'undefined'
39
+ ? `${window.location.protocol}//${window.location.host}/api/flowdrop`
40
+ : '/api/flowdrop';
46
41
  const config = createEndpointConfig(apiBaseUrl, {
47
42
  auth: {
48
- type: 'none' // No authentication for now
43
+ type: 'none'
49
44
  },
50
- timeout: 10000, // 10 second timeout
45
+ timeout: 10000,
51
46
  retry: {
52
47
  enabled: true,
53
48
  maxAttempts: 2,
@@ -58,89 +53,182 @@ async function ensureApiConfiguration() {
58
53
  setEndpointConfig(config);
59
54
  }
60
55
  /**
61
- * Global save function that can be called from anywhere
62
- * Uses the current workflow from the global store
56
+ * Flush any pending form changes by blurring the active element.
57
+ * This ensures focusout handlers (like ConfigForm's handleFormBlur)
58
+ * sync local state to the global store before we read it.
59
+ *
60
+ * Must be called once, in this file only, so the logic lives in exactly one place.
63
61
  */
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.
62
+ async function flushPendingFormChanges() {
68
63
  if (typeof document !== 'undefined' && document.activeElement instanceof HTMLElement) {
69
64
  document.activeElement.blur();
70
65
  }
71
- let loadingToast;
72
- try {
73
- // Show loading toast
74
- loadingToast = apiToasts.loading('Saving workflow');
75
- // Ensure API configuration is initialized
76
- await ensureApiConfiguration();
77
- // Get current workflow from global store
78
- const currentWorkflow = get(workflowStore);
79
- if (!currentWorkflow) {
80
- if (loadingToast)
81
- dismissToast(loadingToast);
66
+ // Wait for any pending DOM / Svelte reactive updates before reading the store
67
+ await tick();
68
+ }
69
+ // ---------------------------------------------------------------------------
70
+ // Public API
71
+ // ---------------------------------------------------------------------------
72
+ /**
73
+ * Save the current workflow to the backend.
74
+ *
75
+ * This is the single source of truth for save logic. App.svelte delegates
76
+ * to this function rather than reimplementing the steps.
77
+ *
78
+ * Steps performed:
79
+ * 1. Flush pending form changes (blur active element + tick)
80
+ * 2. Optionally call onBeforeSave — return false cancels the save
81
+ * 3. Build the canonical Workflow object (preserving metadata, format, etc.)
82
+ * 4. Persist via enhanced apiClient or legacy workflowApi
83
+ * 5. Update the store if the server assigned a new ID
84
+ * 6. Call onMarkAsSaved / onAfterSave hooks
85
+ * 7. Show toast notifications (respecting features.showToasts)
86
+ */
87
+ export async function globalSaveWorkflow(options = {}) {
88
+ const { apiClient, eventHandlers, onMarkAsSaved } = options;
89
+ const features = { ...DEFAULT_FEATURES, ...options.features };
90
+ // Step 1 — Flush pending form changes (single location for this logic)
91
+ await flushPendingFormChanges();
92
+ // Get current workflow from global store after flush
93
+ const currentWorkflow = getWorkflowStore();
94
+ if (!currentWorkflow) {
95
+ if (features.showToasts) {
82
96
  apiToasts.error('Save workflow', 'No workflow to save');
83
- return;
84
- }
85
- // Determine the workflow ID
86
- let workflowId;
87
- if (currentWorkflow.id) {
88
- workflowId = currentWorkflow.id;
89
97
  }
90
- else {
91
- workflowId = uuidv4();
98
+ return;
99
+ }
100
+ // Step 2 — Allow the parent to cancel the save
101
+ if (eventHandlers?.onBeforeSave) {
102
+ const shouldContinue = await eventHandlers.onBeforeSave(currentWorkflow);
103
+ if (shouldContinue === false) {
104
+ return;
92
105
  }
93
- // Create workflow object for saving
106
+ }
107
+ const loadingToast = features.showToasts ? apiToasts.loading('Saving workflow') : null;
108
+ try {
109
+ // Ensure API configuration is initialised (needed when called outside App.svelte)
110
+ await ensureApiConfiguration();
111
+ // Step 3 — Build the canonical workflow object.
112
+ // Preserve all existing metadata fields (format, tags, etc.) so nothing is dropped.
113
+ const workflowId = currentWorkflow.id || uuidv4();
94
114
  const finalWorkflow = {
95
115
  id: workflowId,
96
116
  name: currentWorkflow.name || 'Untitled Workflow',
117
+ description: currentWorkflow.description || '',
97
118
  nodes: currentWorkflow.nodes || [],
98
119
  edges: currentWorkflow.edges || [],
99
120
  metadata: {
100
- version: '1.0.0',
121
+ ...currentWorkflow.metadata,
122
+ version: currentWorkflow.metadata?.version || '1.0.0',
123
+ format: currentWorkflow.metadata?.format || DEFAULT_WORKFLOW_FORMAT,
101
124
  createdAt: currentWorkflow.metadata?.createdAt || new Date().toISOString(),
102
125
  updatedAt: new Date().toISOString()
103
126
  }
104
127
  };
105
- await workflowApi.saveWorkflow(finalWorkflow);
106
- // Dismiss loading toast and show success toast
128
+ // Step 4 — Persist
129
+ let savedWorkflow;
130
+ if (apiClient) {
131
+ // Enhanced client path — detects existing workflows by non-UUID ID
132
+ const isExistingWorkflow = finalWorkflow.id.length > 0 &&
133
+ !finalWorkflow.id.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
134
+ if (isExistingWorkflow) {
135
+ savedWorkflow = await apiClient.updateWorkflow(finalWorkflow.id, finalWorkflow);
136
+ }
137
+ else {
138
+ savedWorkflow = await apiClient.saveWorkflow(finalWorkflow);
139
+ }
140
+ }
141
+ else {
142
+ // Legacy path
143
+ savedWorkflow = await workflowApi.saveWorkflow(finalWorkflow);
144
+ }
145
+ // Step 5 — If the server assigned a new ID, sync the store
146
+ if (savedWorkflow.id && savedWorkflow.id !== finalWorkflow.id) {
147
+ workflowActions.batchUpdate({
148
+ nodes: finalWorkflow.nodes,
149
+ edges: finalWorkflow.edges,
150
+ name: finalWorkflow.name,
151
+ metadata: {
152
+ ...finalWorkflow.metadata,
153
+ ...savedWorkflow.metadata
154
+ }
155
+ });
156
+ }
157
+ // Step 6a — Mark dirty state as clean
158
+ if (onMarkAsSaved) {
159
+ onMarkAsSaved();
160
+ }
161
+ else {
162
+ // Fallback: call the store's own markAsSaved if no callback was provided
163
+ storeMarkAsSaved();
164
+ }
165
+ // Show success toast
107
166
  if (loadingToast)
108
167
  dismissToast(loadingToast);
109
- workflowToasts.saved(finalWorkflow.name);
168
+ if (features.showToasts) {
169
+ workflowToasts.saved(finalWorkflow.name);
170
+ }
171
+ // Step 6b — After-save hook
172
+ if (eventHandlers?.onAfterSave) {
173
+ await eventHandlers.onAfterSave(savedWorkflow);
174
+ }
110
175
  }
111
176
  catch (error) {
112
- // Dismiss loading toast and show error toast
113
177
  if (loadingToast)
114
178
  dismissToast(loadingToast);
115
- apiToasts.error('Save workflow', error instanceof Error ? error.message : 'Unknown error');
179
+ const errorObj = error instanceof Error ? error : new Error('Unknown error occurred');
180
+ // onSaveError hook
181
+ const currentWorkflowForError = getWorkflowStore();
182
+ if (eventHandlers?.onSaveError && currentWorkflowForError) {
183
+ await eventHandlers.onSaveError(errorObj, currentWorkflowForError);
184
+ }
185
+ // onApiError hook — return true suppresses the default toast
186
+ let suppressToast = false;
187
+ if (eventHandlers?.onApiError) {
188
+ suppressToast = eventHandlers.onApiError(errorObj, 'save') === true;
189
+ }
190
+ if (features.showToasts && !suppressToast) {
191
+ apiToasts.error('Save workflow', errorObj.message);
192
+ }
116
193
  throw error;
117
194
  }
118
195
  }
119
196
  /**
120
- * Global export function that can be called from anywhere
121
- * Uses the current workflow from the global store
197
+ * Export the current workflow as a downloadable JSON file.
198
+ *
199
+ * This is the single source of truth for export logic. App.svelte delegates
200
+ * to this function rather than reimplementing the steps.
201
+ *
202
+ * Preserves all metadata fields (format, tags, etc.) consistently with save.
122
203
  */
123
- export async function globalExportWorkflow() {
204
+ export async function globalExportWorkflow(options = {}) {
205
+ const features = { ...DEFAULT_FEATURES, ...options.features };
124
206
  try {
125
- // Get current workflow from global store
126
- const currentWorkflow = get(workflowStore);
207
+ // Flush pending changes before exporting (same discipline as save)
208
+ await flushPendingFormChanges();
209
+ const currentWorkflow = getWorkflowStore();
127
210
  if (!currentWorkflow) {
128
- apiToasts.error('Export workflow', 'No workflow to export');
211
+ if (features.showToasts) {
212
+ apiToasts.error('Export workflow', 'No workflow to export');
213
+ }
129
214
  return;
130
215
  }
131
- // Create workflow object for export
216
+ // Build the canonical export object preserve all metadata fields
132
217
  const finalWorkflow = {
133
218
  id: currentWorkflow.id || 'untitled-workflow',
134
219
  name: currentWorkflow.name || 'Untitled Workflow',
220
+ description: currentWorkflow.description || '',
135
221
  nodes: currentWorkflow.nodes || [],
136
222
  edges: currentWorkflow.edges || [],
137
223
  metadata: {
138
- version: '1.0.0',
224
+ ...currentWorkflow.metadata,
225
+ version: currentWorkflow.metadata?.version || '1.0.0',
226
+ format: currentWorkflow.metadata?.format || DEFAULT_WORKFLOW_FORMAT,
139
227
  createdAt: currentWorkflow.metadata?.createdAt || new Date().toISOString(),
140
228
  updatedAt: new Date().toISOString()
141
229
  }
142
230
  };
143
- // Create and download the file
231
+ // Trigger browser download
144
232
  const dataStr = JSON.stringify(finalWorkflow, null, 2);
145
233
  const dataBlob = new Blob([dataStr], { type: 'application/json' });
146
234
  const url = URL.createObjectURL(dataBlob);
@@ -149,23 +237,12 @@ export async function globalExportWorkflow() {
149
237
  link.download = `${finalWorkflow.name}.json`;
150
238
  link.click();
151
239
  URL.revokeObjectURL(url);
152
- // Show success toast
153
- workflowToasts.exported(finalWorkflow.name);
240
+ if (features.showToasts) {
241
+ workflowToasts.exported(finalWorkflow.name);
242
+ }
154
243
  }
155
244
  catch (error) {
156
- // Export failed
157
- apiToasts.error('Export workflow', error instanceof Error ? error.message : 'Unknown error');
158
- }
159
- }
160
- /**
161
- * Initialize global save functions on window object for external access
162
- * This allows the functions to be called from anywhere in the application
163
- */
164
- export function initializeGlobalSave() {
165
- if (typeof window !== 'undefined') {
166
- // @ts-expect-error - Adding to window for external access
167
- window.flowdropGlobalSave = globalSaveWorkflow;
168
- // @ts-expect-error - Adding to window for external access
169
- window.flowdropGlobalExport = globalExportWorkflow;
245
+ const errorObj = error instanceof Error ? error : new Error('Unknown error occurred');
246
+ apiToasts.error('Export workflow', errorObj.message);
170
247
  }
171
248
  }
@@ -182,7 +182,8 @@ export declare class HistoryService {
182
182
  */
183
183
  private pushInternal;
184
184
  /**
185
- * Trim history to stay within maxEntries limit
185
+ * Trim history to stay within maxEntries limit.
186
+ * A maxEntries of 0 means unlimited (no trimming).
186
187
  */
187
188
  private trimHistory;
188
189
  /**
@@ -7,6 +7,7 @@
7
7
  * @module services/historyService
8
8
  */
9
9
  import { DEFAULT_BEHAVIOR_SETTINGS } from '../types/settings.js';
10
+ import { logger } from '../utils/logger.js';
10
11
  // =========================================================================
11
12
  // History Service Class
12
13
  // =========================================================================
@@ -153,7 +154,7 @@ export class HistoryService {
153
154
  */
154
155
  startTransaction(workflow, description) {
155
156
  if (this.inTransaction) {
156
- console.warn('HistoryService: Transaction already in progress, ignoring startTransaction');
157
+ logger.warn('HistoryService: Transaction already in progress, ignoring startTransaction');
157
158
  return;
158
159
  }
159
160
  this.inTransaction = true;
@@ -167,7 +168,7 @@ export class HistoryService {
167
168
  */
168
169
  commitTransaction() {
169
170
  if (!this.inTransaction || !this.transactionSnapshot) {
170
- console.warn('HistoryService: No transaction in progress, ignoring commitTransaction');
171
+ logger.warn('HistoryService: No transaction in progress, ignoring commitTransaction');
171
172
  return;
172
173
  }
173
174
  // Push the snapshot captured at transaction start
@@ -278,9 +279,12 @@ export class HistoryService {
278
279
  this.notifyChange();
279
280
  }
280
281
  /**
281
- * Trim history to stay within maxEntries limit
282
+ * Trim history to stay within maxEntries limit.
283
+ * A maxEntries of 0 means unlimited (no trimming).
282
284
  */
283
285
  trimHistory() {
286
+ if (this.maxEntries <= 0)
287
+ return;
284
288
  while (this.undoStack.length > this.maxEntries) {
285
289
  this.undoStack.shift();
286
290
  }
@@ -10,6 +10,7 @@
10
10
  import { defaultInterruptPollingConfig } from '../types/interrupt.js';
11
11
  import { buildEndpointUrl, getEndpointHeaders } from '../config/endpoints.js';
12
12
  import { getEndpointConfig } from './api.js';
13
+ import { logger } from '../utils/logger.js';
13
14
  /**
14
15
  * Interrupt Service class
15
16
  *
@@ -28,7 +29,7 @@ export class InterruptService {
28
29
  */
29
30
  constructor() {
30
31
  this.pollingConfig = { ...defaultInterruptPollingConfig };
31
- this.currentBackoff = this.pollingConfig.interval ?? 2000;
32
+ this.currentBackoff = this.pollingConfig.interval ?? defaultInterruptPollingConfig.interval;
32
33
  }
33
34
  /**
34
35
  * Get the singleton instance of InterruptService
@@ -48,7 +49,7 @@ export class InterruptService {
48
49
  */
49
50
  setPollingConfig(config) {
50
51
  this.pollingConfig = { ...this.pollingConfig, ...config };
51
- this.currentBackoff = this.pollingConfig.interval ?? 2000;
52
+ this.currentBackoff = this.pollingConfig.interval ?? defaultInterruptPollingConfig.interval;
52
53
  }
53
54
  /**
54
55
  * Get the current polling configuration
@@ -212,13 +213,13 @@ export class InterruptService {
212
213
  */
213
214
  startPolling(sessionId, callback) {
214
215
  if (!this.pollingConfig.enabled) {
215
- console.warn('[InterruptService] Polling is disabled. Enable via setPollingConfig().');
216
+ logger.warn('[InterruptService] Polling is disabled. Enable via setPollingConfig().');
216
217
  return;
217
218
  }
218
219
  // Stop any existing polling
219
220
  this.stopPolling();
220
221
  this.pollingSessionId = sessionId;
221
- this.currentBackoff = this.pollingConfig.interval ?? 2000;
222
+ this.currentBackoff = this.pollingConfig.interval ?? defaultInterruptPollingConfig.interval;
222
223
  const poll = async () => {
223
224
  if (this.pollingSessionId !== sessionId) {
224
225
  return;
@@ -227,14 +228,14 @@ export class InterruptService {
227
228
  const interrupts = await this.listSessionInterrupts(sessionId);
228
229
  const pendingInterrupts = interrupts.filter((i) => i.status === 'pending');
229
230
  // Reset backoff on successful request
230
- this.currentBackoff = this.pollingConfig.interval ?? 2000;
231
+ this.currentBackoff = this.pollingConfig.interval ?? defaultInterruptPollingConfig.interval;
231
232
  // Call the callback with pending interrupts
232
233
  callback(pendingInterrupts);
233
234
  }
234
235
  catch (error) {
235
- console.error('[InterruptService] Polling error:', error);
236
+ logger.error('[InterruptService] Polling error:', error);
236
237
  // Exponential backoff on error
237
- const maxBackoff = this.pollingConfig.maxBackoff ?? 10000;
238
+ const maxBackoff = this.pollingConfig.maxBackoff ?? defaultInterruptPollingConfig.maxBackoff;
238
239
  this.currentBackoff = Math.min(this.currentBackoff * 2, maxBackoff);
239
240
  }
240
241
  // Schedule next poll
@@ -254,7 +255,7 @@ export class InterruptService {
254
255
  this.pollingInterval = null;
255
256
  }
256
257
  this.pollingSessionId = null;
257
- this.currentBackoff = this.pollingConfig.interval ?? 2000;
258
+ this.currentBackoff = this.pollingConfig.interval ?? defaultInterruptPollingConfig.interval;
258
259
  }
259
260
  /**
260
261
  * Check if polling is active
@@ -4,13 +4,15 @@
4
4
  */
5
5
  import { getEndpointConfig } from './api.js';
6
6
  import { buildEndpointUrl } from '../config/endpoints.js';
7
+ import { NODE_EXECUTION_CACHE_TIMEOUT_MS, PIPELINE_API_UNAVAILABLE_DURATION_MS } from '../config/constants.js';
8
+ import { logger } from '../utils/logger.js';
7
9
  /**
8
10
  * Service for managing node execution information
9
11
  */
10
12
  export class NodeExecutionService {
11
13
  static instance;
12
14
  cache = new Map();
13
- cacheTimeout = 30000; // 30 seconds
15
+ cacheTimeout = NODE_EXECUTION_CACHE_TIMEOUT_MS;
14
16
  lastFetch = 0;
15
17
  apiUnavailable = false;
16
18
  apiUnavailableUntil = 0;
@@ -30,6 +32,8 @@ export class NodeExecutionService {
30
32
  }
31
33
  try {
32
34
  const endpointConfig = getEndpointConfig();
35
+ if (!endpointConfig)
36
+ throw new Error('Endpoint config not available');
33
37
  const url = buildEndpointUrl(endpointConfig, endpointConfig.endpoints.pipelines.get, {
34
38
  id: pipelineId
35
39
  });
@@ -62,7 +66,7 @@ export class NodeExecutionService {
62
66
  return executionInfo;
63
67
  }
64
68
  catch (error) {
65
- console.error('Failed to fetch node execution info:', error);
69
+ logger.error('Failed to fetch node execution info:', error);
66
70
  return null;
67
71
  }
68
72
  }
@@ -87,6 +91,8 @@ export class NodeExecutionService {
87
91
  }
88
92
  try {
89
93
  const endpointConfig = getEndpointConfig();
94
+ if (!endpointConfig)
95
+ throw new Error('Endpoint config not available');
90
96
  const url = buildEndpointUrl(endpointConfig, endpointConfig.endpoints.pipelines.get, {
91
97
  id: pipelineId
92
98
  });
@@ -95,9 +101,9 @@ export class NodeExecutionService {
95
101
  // If the endpoint returns 404, it means the pipeline API is not available
96
102
  // Mark API as unavailable for 5 minutes to prevent repeated calls
97
103
  if (response.status === 404) {
98
- console.warn(`Pipeline API endpoint not available for pipeline ${pipelineId}`);
104
+ logger.warn(`Pipeline API endpoint not available for pipeline ${pipelineId}`);
99
105
  this.apiUnavailable = true;
100
- this.apiUnavailableUntil = Date.now() + 5 * 60 * 1000; // 5 minutes
106
+ this.apiUnavailableUntil = Date.now() + PIPELINE_API_UNAVAILABLE_DURATION_MS;
101
107
  const defaultExecutionInfo = {};
102
108
  nodeIds.forEach((nodeId) => {
103
109
  defaultExecutionInfo[nodeId] = {
@@ -140,7 +146,7 @@ export class NodeExecutionService {
140
146
  return executionInfoMap;
141
147
  }
142
148
  catch (error) {
143
- console.error('Failed to fetch multiple node execution info:', error);
149
+ logger.error('Failed to fetch multiple node execution info:', error);
144
150
  // Return default values instead of empty object to prevent repeated calls
145
151
  const defaultExecutionInfo = {};
146
152
  nodeIds.forEach((nodeId) => {
@@ -159,6 +165,8 @@ export class NodeExecutionService {
159
165
  async getAllNodeExecutionCounts() {
160
166
  try {
161
167
  const endpointConfig = getEndpointConfig();
168
+ if (!endpointConfig)
169
+ throw new Error('Endpoint config not available');
162
170
  const url = buildEndpointUrl(endpointConfig, '/node-execution-counts');
163
171
  const response = await fetch(url);
164
172
  if (!response.ok) {
@@ -171,7 +179,7 @@ export class NodeExecutionService {
171
179
  return {};
172
180
  }
173
181
  catch (error) {
174
- console.error('Failed to fetch all node execution counts:', error);
182
+ logger.error('Failed to fetch all node execution counts:', error);
175
183
  return {};
176
184
  }
177
185
  }
@@ -9,6 +9,7 @@
9
9
  import { defaultShouldStopPolling } from '../types/playground.js';
10
10
  import { buildEndpointUrl, getEndpointHeaders } from '../config/endpoints.js';
11
11
  import { getEndpointConfig } from './api.js';
12
+ import { logger } from '../utils/logger.js';
12
13
  /**
13
14
  * Default polling interval in milliseconds
14
15
  */
@@ -273,7 +274,7 @@ export class PlaygroundService {
273
274
  }
274
275
  }
275
276
  catch (error) {
276
- console.error('Polling error:', error);
277
+ logger.error('Polling error:', error);
277
278
  // Exponential backoff on error
278
279
  this.currentBackoff = Math.min(this.currentBackoff * 2, MAX_POLLING_BACKOFF);
279
280
  }
@@ -4,26 +4,30 @@
4
4
  */
5
5
  import { buildEndpointUrl } from '../config/endpoints.js';
6
6
  import { DEFAULT_PORT_CONFIG } from '../config/defaultPortConfig.js';
7
- import { FlowDropApiClient } from '../api/client.js';
7
+ import { logger } from '../utils/logger.js';
8
8
  /**
9
9
  * Fetch port configuration from API
10
10
  */
11
11
  export async function fetchPortConfig(endpointConfig) {
12
12
  try {
13
13
  const url = buildEndpointUrl(endpointConfig, endpointConfig.endpoints.portConfig);
14
- // Create API client instance
15
- const client = new FlowDropApiClient(endpointConfig.baseUrl);
16
- // Use the client to fetch port configuration
17
- const portConfig = await client.getPortConfig();
14
+ const response = await fetch(url, {
15
+ headers: { 'Content-Type': 'application/json' }
16
+ });
17
+ if (!response.ok) {
18
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
19
+ }
20
+ const data = await response.json();
21
+ const portConfig = data.data ?? data;
18
22
  // Validate the configuration has required fields
19
23
  if (!portConfig.dataTypes || !Array.isArray(portConfig.dataTypes)) {
20
- console.warn('Invalid port config received from API, using default');
24
+ logger.warn('Invalid port config received from API, using default');
21
25
  return DEFAULT_PORT_CONFIG;
22
26
  }
23
27
  return portConfig;
24
28
  }
25
29
  catch (error) {
26
- console.error('Error fetching port configuration:', error);
30
+ logger.error('Error fetching port configuration:', error);
27
31
  return DEFAULT_PORT_CONFIG;
28
32
  }
29
33
  }
@@ -57,7 +57,7 @@ export declare function dismissAllToasts(): void;
57
57
  export declare function showPromise<T>(promise: Promise<T>, { loading, success, error, options }: {
58
58
  loading: string;
59
59
  success: string | ((data: T) => string);
60
- error: string | ((error: any) => string);
60
+ error: string | ((error: unknown) => string);
61
61
  options?: ToastOptions;
62
62
  }): Promise<T>;
63
63
  /**
@@ -4,6 +4,7 @@
4
4
  * Provides consistent toast notifications across the FlowDrop application
5
5
  */
6
6
  import { toast } from 'svelte-5-french-toast';
7
+ import { TOAST_DURATION } from '../config/constants.js';
7
8
  /**
8
9
  * Default toast options themed with FlowDrop design tokens.
9
10
  * Use with <Toaster toastOptions={flowdropToastOptions} containerClassName="flowdrop-toaster" />
@@ -38,7 +39,7 @@ export const FLOWDROP_TOASTER_CLASS = 'flowdrop-toaster';
38
39
  */
39
40
  export function showSuccess(message, options) {
40
41
  return toast.success(message, {
41
- duration: options?.duration || 4000,
42
+ duration: options?.duration || TOAST_DURATION.SUCCESS,
42
43
  position: options?.position || 'bottom-center'
43
44
  });
44
45
  }
@@ -47,7 +48,7 @@ export function showSuccess(message, options) {
47
48
  */
48
49
  export function showError(message, options) {
49
50
  return toast.error(message, {
50
- duration: options?.duration || 6000,
51
+ duration: options?.duration || TOAST_DURATION.ERROR,
51
52
  position: options?.position || 'bottom-center'
52
53
  });
53
54
  }
@@ -56,7 +57,7 @@ export function showError(message, options) {
56
57
  */
57
58
  export function showWarning(message, options) {
58
59
  return toast.error(message, {
59
- duration: options?.duration || 5000,
60
+ duration: options?.duration || TOAST_DURATION.WARNING,
60
61
  position: options?.position || 'bottom-center'
61
62
  });
62
63
  }
@@ -65,7 +66,7 @@ export function showWarning(message, options) {
65
66
  */
66
67
  export function showInfo(message, options) {
67
68
  return toast.success(message, {
68
- duration: options?.duration || 4000,
69
+ duration: options?.duration || TOAST_DURATION.INFO,
69
70
  position: options?.position || 'bottom-center'
70
71
  });
71
72
  }
@@ -106,7 +107,7 @@ export function showPromise(promise, { loading, success, error, options }) {
106
107
  */
107
108
  export function showConfirmation(message, options) {
108
109
  return toast(message, {
109
- duration: options?.duration || 5000,
110
+ duration: options?.duration || TOAST_DURATION.CONFIRMATION,
110
111
  position: options?.position || 'bottom-center'
111
112
  });
112
113
  }