@d34dman/flowdrop 0.0.16 → 0.0.17

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 (35) hide show
  1. package/README.md +64 -0
  2. package/dist/api/enhanced-client.d.ts +3 -3
  3. package/dist/api/enhanced-client.js +57 -57
  4. package/dist/components/FlowDropZone.svelte +4 -5
  5. package/dist/components/FlowDropZone.svelte.d.ts +1 -1
  6. package/dist/components/UniversalNode.svelte +94 -34
  7. package/dist/components/WorkflowEditor.svelte +63 -3
  8. package/dist/config/runtimeConfig.d.ts +2 -2
  9. package/dist/config/runtimeConfig.js +7 -7
  10. package/dist/helpers/workflowEditorHelper.d.ts +44 -4
  11. package/dist/helpers/workflowEditorHelper.js +161 -30
  12. package/dist/index.d.ts +16 -13
  13. package/dist/index.js +19 -8
  14. package/dist/registry/builtinNodes.d.ts +77 -0
  15. package/dist/registry/builtinNodes.js +181 -0
  16. package/dist/registry/index.d.ts +7 -0
  17. package/dist/registry/index.js +10 -0
  18. package/dist/registry/nodeComponentRegistry.d.ts +307 -0
  19. package/dist/registry/nodeComponentRegistry.js +315 -0
  20. package/dist/registry/plugin.d.ts +215 -0
  21. package/dist/registry/plugin.js +249 -0
  22. package/dist/services/draftStorage.d.ts +1 -1
  23. package/dist/services/draftStorage.js +5 -5
  24. package/dist/stores/workflowStore.d.ts +2 -2
  25. package/dist/stores/workflowStore.js +16 -16
  26. package/dist/styles/base.css +15 -0
  27. package/dist/svelte-app.d.ts +6 -6
  28. package/dist/svelte-app.js +25 -25
  29. package/dist/types/auth.d.ts +2 -2
  30. package/dist/types/auth.js +7 -7
  31. package/dist/types/events.d.ts +2 -2
  32. package/dist/types/index.d.ts +38 -3
  33. package/dist/utils/nodeTypes.d.ts +76 -21
  34. package/dist/utils/nodeTypes.js +180 -32
  35. package/package.json +2 -2
package/README.md CHANGED
@@ -14,6 +14,14 @@ A visual workflow editor component library built with Svelte 5 and @xyflow/svelt
14
14
  - **TypeScript Support**: Full type definitions included
15
15
  - **Docker Ready**: Production-ready Dockerfile and Docker Compose configuration
16
16
 
17
+ ### Enterprise Features (v0.0.16+)
18
+
19
+ - **Pluggable Authentication**: AuthProvider interface for OAuth, JWT, SSO integration
20
+ - **Workflow Lifecycle Events**: Hooks for save, load, change, and unmount events
21
+ - **Dirty State Tracking**: Monitor unsaved changes with reactive stores
22
+ - **Draft Auto-Save**: Automatic localStorage drafts with configurable intervals
23
+ - **Feature Flags**: Customize behavior for security and UX requirements
24
+
17
25
  ## 📦 Installation
18
26
 
19
27
  ```bash
@@ -117,6 +125,52 @@ const editor = mountWorkflowEditor(container, {
117
125
  editor.destroy();
118
126
  ```
119
127
 
128
+ #### 3. Enterprise Integration (v0.0.16+)
129
+
130
+ ```javascript
131
+ import { mountFlowDropApp, createEndpointConfig, CallbackAuthProvider } from '@d34dman/flowdrop';
132
+
133
+ const app = await mountFlowDropApp(container, {
134
+ workflow: myWorkflow,
135
+ endpointConfig: createEndpointConfig('/api/flowdrop'),
136
+
137
+ // Dynamic authentication with token refresh
138
+ authProvider: new CallbackAuthProvider({
139
+ getToken: () => authService.getAccessToken(),
140
+ onUnauthorized: () => authService.refreshToken()
141
+ }),
142
+
143
+ // Workflow lifecycle hooks
144
+ eventHandlers: {
145
+ onDirtyStateChange: (isDirty) => updateSaveButton(isDirty),
146
+ onAfterSave: (workflow) => showSuccess('Saved!'),
147
+ onBeforeUnmount: (workflow, isDirty) => {
148
+ if (isDirty) saveDraft(workflow);
149
+ }
150
+ },
151
+
152
+ // Feature configuration
153
+ features: {
154
+ autoSaveDraft: true,
155
+ autoSaveDraftInterval: 30000,
156
+ showToasts: true
157
+ }
158
+ });
159
+
160
+ // Access instance methods
161
+ if (app.isDirty()) {
162
+ await app.save();
163
+ }
164
+
165
+ // Get current workflow data
166
+ const workflow = app.getWorkflow();
167
+
168
+ // Cleanup
169
+ app.destroy();
170
+ ```
171
+
172
+ See the [Enterprise Integration Guide](./docs/enterprise-integration.md) for React, Vue, Angular, and Drupal examples.
173
+
120
174
  #### 3. Integration with Backend Frameworks
121
175
 
122
176
  ##### Drupal Example
@@ -313,6 +367,13 @@ npm run format
313
367
  - **CHANGELOG.md** - Version history
314
368
  - **Storybook** - Component documentation (run `npm run storybook`)
315
369
 
370
+ ### Enterprise Features (v0.0.16+)
371
+
372
+ - **[Enterprise Integration Guide](./docs/enterprise-integration.md)** - Complete integration patterns for React, Vue, Angular, Drupal
373
+ - **[Authentication Guide](./docs/authentication-guide.md)** - OAuth, JWT, SSO, and custom auth providers
374
+ - **[Event Handlers](./docs/event-handlers.md)** - Workflow lifecycle events and hooks
375
+ - **[Features Configuration](./docs/features-configuration.md)** - Feature flags, draft auto-save, and customization
376
+
316
377
  ## 🤝 Contributing
317
378
 
318
379
  Not accepting contributions until the library stabilizes. Stay tuned.
@@ -347,6 +408,7 @@ docker-compose up -d
347
408
  ### Environment Variables
348
409
 
349
410
  **Production (Runtime):**
411
+
350
412
  - `FLOWDROP_API_BASE_URL` - Backend API URL
351
413
  - `FLOWDROP_THEME` - UI theme (light/dark/auto)
352
414
  - `FLOWDROP_TIMEOUT` - Request timeout in milliseconds
@@ -354,6 +416,7 @@ docker-compose up -d
354
416
  - `FLOWDROP_AUTH_TOKEN` - Authentication token
355
417
 
356
418
  **Development (Build-time):**
419
+
357
420
  - `VITE_API_BASE_URL` - Dev API URL (used only during `npm run dev`)
358
421
 
359
422
  ### Build for Production
@@ -371,6 +434,7 @@ node build
371
434
  ```
372
435
 
373
436
  For detailed deployment instructions, see:
437
+
374
438
  - [DOCKER.md](./DOCKER.md) - Docker quick start
375
439
 
376
440
  ---
@@ -5,9 +5,9 @@
5
5
  *
6
6
  * @module api/enhanced-client
7
7
  */
8
- import type { NodeMetadata, Workflow, ExecutionResult } from "../types/index.js";
9
- import type { EndpointConfig } from "../config/endpoints.js";
10
- import type { AuthProvider } from "../types/auth.js";
8
+ import type { NodeMetadata, Workflow, ExecutionResult } from '../types/index.js';
9
+ import type { EndpointConfig } from '../config/endpoints.js';
10
+ import type { AuthProvider } from '../types/auth.js';
11
11
  /**
12
12
  * API error with additional context
13
13
  */
@@ -5,8 +5,8 @@
5
5
  *
6
6
  * @module api/enhanced-client
7
7
  */
8
- import { buildEndpointUrl, getEndpointMethod, getEndpointHeaders } from "../config/endpoints.js";
9
- import { createAuthProviderFromLegacyConfig } from "../types/auth.js";
8
+ import { buildEndpointUrl, getEndpointMethod, getEndpointHeaders } from '../config/endpoints.js';
9
+ import { createAuthProviderFromLegacyConfig } from '../types/auth.js';
10
10
  /**
11
11
  * API error with additional context
12
12
  */
@@ -19,7 +19,7 @@ export class ApiError extends Error {
19
19
  operation;
20
20
  constructor(message, status, operation, errorData = {}) {
21
21
  super(message);
22
- this.name = "ApiError";
22
+ this.name = 'ApiError';
23
23
  this.status = status;
24
24
  this.operation = operation;
25
25
  this.errorData = errorData;
@@ -62,7 +62,7 @@ export class EnhancedFlowDropApiClient {
62
62
  * @param options - Additional fetch options
63
63
  * @param operation - Description of the operation (for error messages)
64
64
  */
65
- async request(endpointKey, endpointPath, params, options = {}, operation = "API request") {
65
+ async request(endpointKey, endpointPath, params, options = {}, operation = 'API request') {
66
66
  const url = buildEndpointUrl(this.config, endpointPath, params);
67
67
  const method = options.method ?? getEndpointMethod(this.config, endpointKey);
68
68
  const configHeaders = getEndpointHeaders(this.config, endpointKey);
@@ -99,14 +99,14 @@ export class EnhancedFlowDropApiClient {
99
99
  continue; // Retry with new headers
100
100
  }
101
101
  }
102
- throw new ApiError("Unauthorized", 401, operation, {});
102
+ throw new ApiError('Unauthorized', 401, operation, {});
103
103
  }
104
104
  // Handle 403 Forbidden
105
105
  if (response.status === 403) {
106
106
  if (this.authProvider.onForbidden) {
107
107
  await this.authProvider.onForbidden();
108
108
  }
109
- throw new ApiError("Forbidden", 403, operation, {});
109
+ throw new ApiError('Forbidden', 403, operation, {});
110
110
  }
111
111
  // Handle other errors
112
112
  if (!response.ok) {
@@ -133,7 +133,7 @@ export class EnhancedFlowDropApiClient {
133
133
  }
134
134
  // Wait before retry
135
135
  const delay = this.config.retry?.delay ?? 1000;
136
- const backoffDelay = this.config.retry?.backoff === "exponential" ? delay * Math.pow(2, attempt - 1) : delay;
136
+ const backoffDelay = this.config.retry?.backoff === 'exponential' ? delay * Math.pow(2, attempt - 1) : delay;
137
137
  await new Promise((resolve) => setTimeout(resolve, backoffDelay));
138
138
  }
139
139
  }
@@ -166,9 +166,9 @@ export class EnhancedFlowDropApiClient {
166
166
  * Fetch all available node types
167
167
  */
168
168
  async getAvailableNodes() {
169
- const response = await this.request("nodes.list", this.config.endpoints.nodes.list, undefined, {}, "fetch available nodes");
169
+ const response = await this.request('nodes.list', this.config.endpoints.nodes.list, undefined, {}, 'fetch available nodes');
170
170
  if (!response.success || !response.data) {
171
- throw new Error(response.error ?? "Failed to fetch available nodes");
171
+ throw new Error(response.error ?? 'Failed to fetch available nodes');
172
172
  }
173
173
  return response.data;
174
174
  }
@@ -176,9 +176,9 @@ export class EnhancedFlowDropApiClient {
176
176
  * Fetch nodes filtered by category
177
177
  */
178
178
  async getNodesByCategory(category) {
179
- const response = await this.request("nodes.byCategory", this.config.endpoints.nodes.byCategory, { category }, {}, "fetch nodes by category");
179
+ const response = await this.request('nodes.byCategory', this.config.endpoints.nodes.byCategory, { category }, {}, 'fetch nodes by category');
180
180
  if (!response.success || !response.data) {
181
- throw new Error(response.error ?? "Failed to fetch nodes by category");
181
+ throw new Error(response.error ?? 'Failed to fetch nodes by category');
182
182
  }
183
183
  return response.data;
184
184
  }
@@ -186,9 +186,9 @@ export class EnhancedFlowDropApiClient {
186
186
  * Fetch metadata for a specific node type
187
187
  */
188
188
  async getNodeMetadata(nodeId) {
189
- const response = await this.request("nodes.metadata", this.config.endpoints.nodes.metadata, { id: nodeId }, {}, "fetch node metadata");
189
+ const response = await this.request('nodes.metadata', this.config.endpoints.nodes.metadata, { id: nodeId }, {}, 'fetch node metadata');
190
190
  if (!response.success || !response.data) {
191
- throw new Error(response.error ?? "Failed to fetch node metadata");
191
+ throw new Error(response.error ?? 'Failed to fetch node metadata');
192
192
  }
193
193
  return response.data;
194
194
  }
@@ -199,12 +199,12 @@ export class EnhancedFlowDropApiClient {
199
199
  * Save a new workflow
200
200
  */
201
201
  async saveWorkflow(workflow) {
202
- const response = await this.request("workflows.create", this.config.endpoints.workflows.create, undefined, {
203
- method: "POST",
202
+ const response = await this.request('workflows.create', this.config.endpoints.workflows.create, undefined, {
203
+ method: 'POST',
204
204
  body: JSON.stringify(workflow)
205
- }, "save workflow");
205
+ }, 'save workflow');
206
206
  if (!response.success || !response.data) {
207
- throw new Error(response.error ?? "Failed to save workflow");
207
+ throw new Error(response.error ?? 'Failed to save workflow');
208
208
  }
209
209
  return response.data;
210
210
  }
@@ -212,12 +212,12 @@ export class EnhancedFlowDropApiClient {
212
212
  * Update an existing workflow
213
213
  */
214
214
  async updateWorkflow(workflowId, workflow) {
215
- const response = await this.request("workflows.update", this.config.endpoints.workflows.update, { id: workflowId }, {
216
- method: "PUT",
215
+ const response = await this.request('workflows.update', this.config.endpoints.workflows.update, { id: workflowId }, {
216
+ method: 'PUT',
217
217
  body: JSON.stringify(workflow)
218
- }, "update workflow");
218
+ }, 'update workflow');
219
219
  if (!response.success || !response.data) {
220
- throw new Error(response.error ?? "Failed to update workflow");
220
+ throw new Error(response.error ?? 'Failed to update workflow');
221
221
  }
222
222
  return response.data;
223
223
  }
@@ -225,9 +225,9 @@ export class EnhancedFlowDropApiClient {
225
225
  * Load a workflow by ID
226
226
  */
227
227
  async loadWorkflow(workflowId) {
228
- const response = await this.request("workflows.get", this.config.endpoints.workflows.get, { id: workflowId }, {}, "load workflow");
228
+ const response = await this.request('workflows.get', this.config.endpoints.workflows.get, { id: workflowId }, {}, 'load workflow');
229
229
  if (!response.success || !response.data) {
230
- throw new Error(response.error ?? "Failed to load workflow");
230
+ throw new Error(response.error ?? 'Failed to load workflow');
231
231
  }
232
232
  return response.data;
233
233
  }
@@ -235,9 +235,9 @@ export class EnhancedFlowDropApiClient {
235
235
  * List all workflows
236
236
  */
237
237
  async listWorkflows() {
238
- const response = await this.request("workflows.list", this.config.endpoints.workflows.list, undefined, {}, "list workflows");
238
+ const response = await this.request('workflows.list', this.config.endpoints.workflows.list, undefined, {}, 'list workflows');
239
239
  if (!response.success || !response.data) {
240
- throw new Error(response.error ?? "Failed to list workflows");
240
+ throw new Error(response.error ?? 'Failed to list workflows');
241
241
  }
242
242
  return response.data;
243
243
  }
@@ -245,21 +245,21 @@ export class EnhancedFlowDropApiClient {
245
245
  * Delete a workflow
246
246
  */
247
247
  async deleteWorkflow(workflowId) {
248
- const response = await this.request("workflows.delete", this.config.endpoints.workflows.delete, { id: workflowId }, { method: "DELETE" }, "delete workflow");
248
+ const response = await this.request('workflows.delete', this.config.endpoints.workflows.delete, { id: workflowId }, { method: 'DELETE' }, 'delete workflow');
249
249
  if (!response.success) {
250
- throw new Error(response.error ?? "Failed to delete workflow");
250
+ throw new Error(response.error ?? 'Failed to delete workflow');
251
251
  }
252
252
  }
253
253
  /**
254
254
  * Validate a workflow
255
255
  */
256
256
  async validateWorkflow(workflow) {
257
- const response = await this.request("workflows.validate", this.config.endpoints.workflows.validate, undefined, {
258
- method: "POST",
257
+ const response = await this.request('workflows.validate', this.config.endpoints.workflows.validate, undefined, {
258
+ method: 'POST',
259
259
  body: JSON.stringify(workflow)
260
- }, "validate workflow");
260
+ }, 'validate workflow');
261
261
  if (!response.success || !response.data) {
262
- throw new Error(response.error ?? "Failed to validate workflow");
262
+ throw new Error(response.error ?? 'Failed to validate workflow');
263
263
  }
264
264
  return response.data;
265
265
  }
@@ -267,9 +267,9 @@ export class EnhancedFlowDropApiClient {
267
267
  * Export a workflow as JSON string
268
268
  */
269
269
  async exportWorkflow(workflowId) {
270
- const response = await this.request("workflows.export", this.config.endpoints.workflows.export, { id: workflowId }, {}, "export workflow");
270
+ const response = await this.request('workflows.export', this.config.endpoints.workflows.export, { id: workflowId }, {}, 'export workflow');
271
271
  if (!response.success || !response.data) {
272
- throw new Error(response.error ?? "Failed to export workflow");
272
+ throw new Error(response.error ?? 'Failed to export workflow');
273
273
  }
274
274
  return response.data;
275
275
  }
@@ -277,12 +277,12 @@ export class EnhancedFlowDropApiClient {
277
277
  * Import a workflow from JSON
278
278
  */
279
279
  async importWorkflow(workflowJson) {
280
- const response = await this.request("workflows.import", this.config.endpoints.workflows.import, undefined, {
281
- method: "POST",
280
+ const response = await this.request('workflows.import', this.config.endpoints.workflows.import, undefined, {
281
+ method: 'POST',
282
282
  body: JSON.stringify({ workflow: workflowJson })
283
- }, "import workflow");
283
+ }, 'import workflow');
284
284
  if (!response.success || !response.data) {
285
- throw new Error(response.error ?? "Failed to import workflow");
285
+ throw new Error(response.error ?? 'Failed to import workflow');
286
286
  }
287
287
  return response.data;
288
288
  }
@@ -293,12 +293,12 @@ export class EnhancedFlowDropApiClient {
293
293
  * Execute a workflow
294
294
  */
295
295
  async executeWorkflow(workflowId, inputs) {
296
- const response = await this.request("executions.execute", this.config.endpoints.executions.execute, { id: workflowId }, {
297
- method: "POST",
296
+ const response = await this.request('executions.execute', this.config.endpoints.executions.execute, { id: workflowId }, {
297
+ method: 'POST',
298
298
  body: JSON.stringify({ inputs })
299
- }, "execute workflow");
299
+ }, 'execute workflow');
300
300
  if (!response.success || !response.data) {
301
- throw new Error(response.error ?? "Failed to execute workflow");
301
+ throw new Error(response.error ?? 'Failed to execute workflow');
302
302
  }
303
303
  return response.data;
304
304
  }
@@ -306,9 +306,9 @@ export class EnhancedFlowDropApiClient {
306
306
  * Get execution status
307
307
  */
308
308
  async getExecutionStatus(executionId) {
309
- const response = await this.request("executions.status", this.config.endpoints.executions.status, { id: executionId }, {}, "get execution status");
309
+ const response = await this.request('executions.status', this.config.endpoints.executions.status, { id: executionId }, {}, 'get execution status');
310
310
  if (!response.success || !response.data) {
311
- throw new Error(response.error ?? "Failed to get execution status");
311
+ throw new Error(response.error ?? 'Failed to get execution status');
312
312
  }
313
313
  return response.data;
314
314
  }
@@ -316,18 +316,18 @@ export class EnhancedFlowDropApiClient {
316
316
  * Cancel a running execution
317
317
  */
318
318
  async cancelExecution(executionId) {
319
- const response = await this.request("executions.cancel", this.config.endpoints.executions.cancel, { id: executionId }, { method: "POST" }, "cancel execution");
319
+ const response = await this.request('executions.cancel', this.config.endpoints.executions.cancel, { id: executionId }, { method: 'POST' }, 'cancel execution');
320
320
  if (!response.success) {
321
- throw new Error(response.error ?? "Failed to cancel execution");
321
+ throw new Error(response.error ?? 'Failed to cancel execution');
322
322
  }
323
323
  }
324
324
  /**
325
325
  * Get execution logs
326
326
  */
327
327
  async getExecutionLogs(executionId) {
328
- const response = await this.request("executions.logs", this.config.endpoints.executions.logs, { id: executionId }, {}, "get execution logs");
328
+ const response = await this.request('executions.logs', this.config.endpoints.executions.logs, { id: executionId }, {}, 'get execution logs');
329
329
  if (!response.success || !response.data) {
330
- throw new Error(response.error ?? "Failed to get execution logs");
330
+ throw new Error(response.error ?? 'Failed to get execution logs');
331
331
  }
332
332
  return response.data;
333
333
  }
@@ -338,9 +338,9 @@ export class EnhancedFlowDropApiClient {
338
338
  * List available templates
339
339
  */
340
340
  async listTemplates() {
341
- const response = await this.request("templates.list", this.config.endpoints.templates.list, undefined, {}, "list templates");
341
+ const response = await this.request('templates.list', this.config.endpoints.templates.list, undefined, {}, 'list templates');
342
342
  if (!response.success || !response.data) {
343
- throw new Error(response.error ?? "Failed to list templates");
343
+ throw new Error(response.error ?? 'Failed to list templates');
344
344
  }
345
345
  return response.data;
346
346
  }
@@ -348,9 +348,9 @@ export class EnhancedFlowDropApiClient {
348
348
  * Get a template by ID
349
349
  */
350
350
  async getTemplate(templateId) {
351
- const response = await this.request("templates.get", this.config.endpoints.templates.get, { id: templateId }, {}, "get template");
351
+ const response = await this.request('templates.get', this.config.endpoints.templates.get, { id: templateId }, {}, 'get template');
352
352
  if (!response.success || !response.data) {
353
- throw new Error(response.error ?? "Failed to get template");
353
+ throw new Error(response.error ?? 'Failed to get template');
354
354
  }
355
355
  return response.data;
356
356
  }
@@ -361,9 +361,9 @@ export class EnhancedFlowDropApiClient {
361
361
  * Get system health status
362
362
  */
363
363
  async getSystemHealth() {
364
- const response = await this.request("system.health", this.config.endpoints.system.health, undefined, {}, "get system health");
364
+ const response = await this.request('system.health', this.config.endpoints.system.health, undefined, {}, 'get system health');
365
365
  if (!response.success || !response.data) {
366
- throw new Error(response.error ?? "Failed to get system health");
366
+ throw new Error(response.error ?? 'Failed to get system health');
367
367
  }
368
368
  return response.data;
369
369
  }
@@ -371,9 +371,9 @@ export class EnhancedFlowDropApiClient {
371
371
  * Get system configuration
372
372
  */
373
373
  async getSystemConfig() {
374
- const response = await this.request("system.config", this.config.endpoints.system.config, undefined, {}, "get system config");
374
+ const response = await this.request('system.config', this.config.endpoints.system.config, undefined, {}, 'get system config');
375
375
  if (!response.success || !response.data) {
376
- throw new Error(response.error ?? "Failed to get system config");
376
+ throw new Error(response.error ?? 'Failed to get system config');
377
377
  }
378
378
  return response.data;
379
379
  }
@@ -381,9 +381,9 @@ export class EnhancedFlowDropApiClient {
381
381
  * Get system version information
382
382
  */
383
383
  async getSystemVersion() {
384
- const response = await this.request("system.version", this.config.endpoints.system.version, undefined, {}, "get system version");
384
+ const response = await this.request('system.version', this.config.endpoints.system.version, undefined, {}, 'get system version');
385
385
  if (!response.success || !response.data) {
386
- throw new Error(response.error ?? "Failed to get system version");
386
+ throw new Error(response.error ?? 'Failed to get system version');
387
387
  }
388
388
  return response.data;
389
389
  }
@@ -5,8 +5,8 @@
5
5
  -->
6
6
 
7
7
  <script lang="ts">
8
- import { useSvelteFlow } from "@xyflow/svelte";
9
- import type { Snippet } from "svelte";
8
+ import { useSvelteFlow } from '@xyflow/svelte';
9
+ import type { Snippet } from 'svelte';
10
10
 
11
11
  interface Props {
12
12
  ondrop: (nodeTypeData: string, position: { x: number; y: number }) => void;
@@ -24,7 +24,7 @@
24
24
  function handleDragOver(e: DragEvent): void {
25
25
  e.preventDefault();
26
26
  if (e.dataTransfer) {
27
- e.dataTransfer.dropEffect = "copy";
27
+ e.dataTransfer.dropEffect = 'copy';
28
28
  }
29
29
  }
30
30
 
@@ -35,7 +35,7 @@
35
35
  e.preventDefault();
36
36
 
37
37
  // Get the data from the drag event
38
- const nodeTypeData = e.dataTransfer?.getData("application/json");
38
+ const nodeTypeData = e.dataTransfer?.getData('application/json');
39
39
  if (nodeTypeData) {
40
40
  // Convert screen coordinates to flow coordinates (accounts for zoom and pan)
41
41
  const position = screenToFlowPosition({
@@ -65,4 +65,3 @@
65
65
  height: 100%;
66
66
  }
67
67
  </style>
68
-
@@ -1,4 +1,4 @@
1
- import type { Snippet } from "svelte";
1
+ import type { Snippet } from 'svelte';
2
2
  interface Props {
3
3
  ondrop: (nodeTypeData: string, position: {
4
4
  x: number;
@@ -1,24 +1,28 @@
1
1
  <!--
2
2
  Universal Node Component
3
- Renders any node type with automatic status overlay injection
4
- This component can replace individual node components in SvelteFlow
3
+ Renders any node type with automatic status overlay injection.
4
+ This component can replace individual node components in SvelteFlow.
5
+
6
+ Uses the node component registry to resolve which component to render,
7
+ enabling custom node types to be registered and used dynamically.
5
8
  -->
6
9
 
7
10
  <script lang="ts">
8
11
  import type { WorkflowNode } from '../types/index.js';
12
+ import { nodeComponentRegistry } from '../registry/nodeComponentRegistry.js';
13
+ import { resolveBuiltinAlias } from '../registry/builtinNodes.js';
14
+ import NodeStatusOverlay from './NodeStatusOverlay.svelte';
15
+ import { shouldShowNodeStatus } from '../utils/nodeWrapper.js';
16
+ import { resolveComponentName } from '../utils/nodeTypes.js';
17
+
18
+ // Fallback components for when registry is not available
19
+ // These are only used as last-resort fallbacks
9
20
  import WorkflowNodeComponent from './WorkflowNode.svelte';
10
21
  import NotesNode from './NotesNode.svelte';
11
22
  import SimpleNode from './SimpleNode.svelte';
12
23
  import SquareNode from './SquareNode.svelte';
13
24
  import ToolNode from './ToolNode.svelte';
14
25
  import GatewayNode from './GatewayNode.svelte';
15
- import NodeStatusOverlay from './NodeStatusOverlay.svelte';
16
- import {
17
- shouldShowNodeStatus,
18
- getOptimalStatusPosition,
19
- getOptimalStatusSize
20
- } from '../utils/nodeWrapper.js';
21
- import { resolveComponentName } from '../utils/nodeTypes.js';
22
26
 
23
27
  let {
24
28
  data,
@@ -31,26 +35,60 @@
31
35
  selected?: boolean;
32
36
  } = $props();
33
37
 
34
- // Determine which node component to render based on node type
35
- // Priority: config.nodeType > metadata.type
36
- // Explicitly track config.nodeType to ensure reactivity
38
+ /**
39
+ * Determine which node component to render based on node type.
40
+ * Priority: config.nodeType > metadata.type
41
+ * Explicitly track config.nodeType to ensure reactivity.
42
+ */
37
43
  let configNodeType = $derived(data.config?.nodeType as string | undefined);
44
+
45
+ /**
46
+ * Resolve the component name from metadata and config.
47
+ * This handles the logic of choosing between config.nodeType and metadata.type.
48
+ */
38
49
  let resolvedComponentName = $derived(
39
50
  data.metadata ? resolveComponentName(data.metadata, configNodeType) : 'workflowNode'
40
51
  );
52
+
53
+ /**
54
+ * Get the node component from the registry.
55
+ * Falls back to built-in components if registry lookup fails.
56
+ */
41
57
  let nodeComponent = $derived(getNodeComponent(resolvedComponentName));
42
58
 
43
- // Get execution info
59
+ /**
60
+ * Get execution info for status overlay
61
+ */
44
62
  let executionInfo = $derived(data.executionInfo);
63
+
64
+ /**
65
+ * Determine if status overlay should be shown.
66
+ * Hide for note nodes as they have their own styling.
67
+ */
45
68
  let shouldShowStatus = $derived(
46
69
  shouldShowNodeStatus(executionInfo) && resolvedComponentName !== 'note'
47
70
  );
48
71
 
49
72
  /**
50
- * Get the appropriate node component based on type
73
+ * Get the node component for the given type.
74
+ * First tries the registry, then falls back to hardcoded components.
75
+ *
76
+ * @param nodeType - The node type identifier
77
+ * @returns The Svelte component to render
51
78
  */
52
79
  function getNodeComponent(nodeType: string) {
53
- switch (nodeType) {
80
+ // Resolve any aliases (e.g., "default" -> "workflowNode")
81
+ const resolvedType = resolveBuiltinAlias(nodeType);
82
+
83
+ // Try registry first
84
+ const registeredComponent = nodeComponentRegistry.getComponent(resolvedType);
85
+ if (registeredComponent) {
86
+ return registeredComponent;
87
+ }
88
+
89
+ // Fallback to hardcoded switch for backwards compatibility
90
+ // This ensures the component works even if registry fails to initialize
91
+ switch (resolvedType) {
54
92
  case 'note':
55
93
  return NotesNode;
56
94
  case 'simple':
@@ -68,40 +106,62 @@
68
106
  }
69
107
 
70
108
  /**
71
- * Get optimal status position for this node type
109
+ * Get optimal status position for this node type.
110
+ * Uses registry if available, otherwise falls back to defaults.
72
111
  */
73
112
  function getStatusPosition(): 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' {
74
- return getOptimalStatusPosition(resolvedComponentName);
113
+ // Try registry first
114
+ const position = nodeComponentRegistry.getStatusPosition(resolvedComponentName);
115
+ if (position) {
116
+ return position;
117
+ }
118
+
119
+ // Fallback based on node type
120
+ switch (resolvedComponentName) {
121
+ case 'tool':
122
+ return 'top-left';
123
+ case 'note':
124
+ return 'bottom-right';
125
+ case 'simple':
126
+ case 'square':
127
+ default:
128
+ return 'top-right';
129
+ }
75
130
  }
76
131
 
77
132
  /**
78
- * Get optimal status size for this node type
133
+ * Get optimal status size for this node type.
134
+ * Uses registry if available, otherwise falls back to defaults.
79
135
  */
80
136
  function getStatusSize(): 'sm' | 'md' | 'lg' {
81
- return getOptimalStatusSize(resolvedComponentName);
137
+ // Try registry first
138
+ const size = nodeComponentRegistry.getStatusSize(resolvedComponentName);
139
+ if (size) {
140
+ return size;
141
+ }
142
+
143
+ // Fallback based on node type
144
+ switch (resolvedComponentName) {
145
+ case 'tool':
146
+ case 'note':
147
+ case 'square':
148
+ return 'sm';
149
+ case 'simple':
150
+ default:
151
+ return 'md';
152
+ }
82
153
  }
83
154
  </script>
84
155
 
85
156
  <div class="universal-node">
86
- <!-- Render the appropriate node component -->
87
- {#if nodeComponent === WorkflowNodeComponent}
88
- <WorkflowNodeComponent {data} {selected} />
89
- {:else if nodeComponent === NotesNode}
90
- <NotesNode {data} {selected} />
91
- {:else if nodeComponent === SimpleNode}
92
- <SimpleNode {data} {selected} />
93
- {:else if nodeComponent === SquareNode}
94
- <SquareNode {data} {selected} />
95
- {:else if nodeComponent === ToolNode}
96
- <ToolNode {data} {selected} />
97
- {:else if nodeComponent === GatewayNode}
98
- <GatewayNode {data} {selected} />
99
- {/if}
157
+ <!-- Render the node component dynamically -->
158
+ <!-- svelte-ignore binding_property_non_reactive -->
159
+ <svelte:component this={nodeComponent} {data} {selected} />
100
160
 
101
161
  <!-- Status overlay - only show if there's meaningful status information -->
102
162
  {#if shouldShowStatus}
103
163
  <NodeStatusOverlay
104
- nodeId={data.nodeId || 'unknown'}
164
+ nodeId={data.nodeId ?? 'unknown'}
105
165
  {executionInfo}
106
166
  position={getStatusPosition()}
107
167
  size={getStatusSize()}