@d34dman/flowdrop 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,34 +42,34 @@ npm install @d34dman/flowdrop
42
42
  ### Basic Import
43
43
 
44
44
  ```javascript
45
- import { WorkflowEditor } from "@d34dman/flowdrop";
46
- import "@d34dman/flowdrop/styles/base.css";
45
+ import { WorkflowEditor } from '@d34dman/flowdrop';
46
+ import '@d34dman/flowdrop/styles/base.css';
47
47
  ```
48
48
 
49
49
  ### Using the WorkflowEditor Component
50
50
 
51
51
  ```svelte
52
52
  <script lang="ts">
53
- import { WorkflowEditor } from "@d34dman/flowdrop";
54
- import type { NodeMetadata, Workflow } from "@d34dman/flowdrop";
55
-
56
- let nodes: NodeMetadata[] = [
57
- {
58
- id: "text_input",
59
- name: "Text Input",
60
- category: "input_output",
61
- description: "User input field",
62
- inputs: [],
63
- outputs: [{ id: "value", name: "Value", type: "output", dataType: "string" }]
64
- }
65
- ];
66
-
67
- let workflow: Workflow = {
68
- id: "workflow_1",
69
- name: "My Workflow",
70
- nodes: [],
71
- edges: []
72
- };
53
+ import { WorkflowEditor } from '@d34dman/flowdrop';
54
+ import type { NodeMetadata, Workflow } from '@d34dman/flowdrop';
55
+
56
+ let nodes: NodeMetadata[] = [
57
+ {
58
+ id: 'text_input',
59
+ name: 'Text Input',
60
+ category: 'input_output',
61
+ description: 'User input field',
62
+ inputs: [],
63
+ outputs: [{ id: 'value', name: 'Value', type: 'output', dataType: 'string' }]
64
+ }
65
+ ];
66
+
67
+ let workflow: Workflow = {
68
+ id: 'workflow_1',
69
+ name: 'My Workflow',
70
+ nodes: [],
71
+ edges: []
72
+ };
73
73
  </script>
74
74
 
75
75
  <WorkflowEditor {nodes} />
@@ -81,34 +81,34 @@ import "@d34dman/flowdrop/styles/base.css";
81
81
 
82
82
  ```svelte
83
83
  <script>
84
- import { WorkflowEditor, NodeSidebar } from "@d34dman/flowdrop";
84
+ import { WorkflowEditor, NodeSidebar } from '@d34dman/flowdrop';
85
85
  </script>
86
86
 
87
87
  <div class="editor-container">
88
- <NodeSidebar {nodes} />
89
- <WorkflowEditor {nodes} />
88
+ <NodeSidebar {nodes} />
89
+ <WorkflowEditor {nodes} />
90
90
  </div>
91
91
  ```
92
92
 
93
93
  #### 2. Using Mount Functions (Vanilla JS/Other Frameworks)
94
94
 
95
95
  ```javascript
96
- import { mountWorkflowEditor } from "@d34dman/flowdrop";
96
+ import { mountWorkflowEditor } from '@d34dman/flowdrop';
97
97
 
98
- const container = document.getElementById("workflow-container");
98
+ const container = document.getElementById('workflow-container');
99
99
  const editor = mountWorkflowEditor(container, {
100
- nodes: availableNodes,
101
- endpointConfig: {
102
- baseUrl: "/api/flowdrop",
103
- endpoints: {
104
- workflows: {
105
- list: "/workflows",
106
- get: "/workflows/{id}",
107
- create: "/workflows",
108
- update: "/workflows/{id}"
109
- }
110
- }
111
- }
100
+ nodes: availableNodes,
101
+ endpointConfig: {
102
+ baseUrl: '/api/flowdrop',
103
+ endpoints: {
104
+ workflows: {
105
+ list: '/workflows',
106
+ get: '/workflows/{id}',
107
+ create: '/workflows',
108
+ update: '/workflows/{id}'
109
+ }
110
+ }
111
+ }
112
112
  });
113
113
 
114
114
  // Cleanup
@@ -121,15 +121,15 @@ editor.destroy();
121
121
 
122
122
  ```javascript
123
123
  Drupal.behaviors.flowdropEditor = {
124
- attach: function (context, settings) {
125
- const container = context.querySelector(".flowdrop-container");
126
- if (container && window.FlowDrop) {
127
- window.FlowDrop.mountWorkflowEditor(container, {
128
- endpointConfig: settings.flowdrop.endpointConfig,
129
- nodes: settings.flowdrop.nodes
130
- });
131
- }
132
- }
124
+ attach: function (context, settings) {
125
+ const container = context.querySelector('.flowdrop-container');
126
+ if (container && window.FlowDrop) {
127
+ window.FlowDrop.mountWorkflowEditor(container, {
128
+ endpointConfig: settings.flowdrop.endpointConfig,
129
+ nodes: settings.flowdrop.nodes
130
+ });
131
+ }
132
+ }
133
133
  };
134
134
  ```
135
135
 
@@ -140,6 +140,7 @@ Drupal.behaviors.flowdropEditor = {
140
140
  Main editor component for creating and editing workflows.
141
141
 
142
142
  **Props:**
143
+
143
144
  - `nodes`: Array of available node types
144
145
  - `endpointConfig`: API endpoint configuration
145
146
  - `height`: Editor height (default: "100vh")
@@ -152,6 +153,7 @@ Main editor component for creating and editing workflows.
152
153
  Sidebar displaying available node types.
153
154
 
154
155
  **Props:**
156
+
155
157
  - `nodes`: Array of node types to display
156
158
 
157
159
  ### ConfigSidebar
@@ -159,6 +161,7 @@ Sidebar displaying available node types.
159
161
  Configuration panel for selected nodes.
160
162
 
161
163
  **Props:**
164
+
162
165
  - `isOpen`: Sidebar visibility
163
166
  - `configSchema`: JSON schema for configuration
164
167
  - `configValues`: Current configuration values
@@ -170,29 +173,29 @@ Configuration panel for selected nodes.
170
173
  Configure the API client to connect to your backend:
171
174
 
172
175
  ```typescript
173
- import { createEndpointConfig } from "@d34dman/flowdrop";
176
+ import { createEndpointConfig } from '@d34dman/flowdrop';
174
177
 
175
178
  const config = createEndpointConfig({
176
- baseUrl: "https://api.example.com",
177
- endpoints: {
178
- nodes: {
179
- list: "/nodes",
180
- get: "/nodes/{id}"
181
- },
182
- workflows: {
183
- list: "/workflows",
184
- get: "/workflows/{id}",
185
- create: "/workflows",
186
- update: "/workflows/{id}",
187
- delete: "/workflows/{id}",
188
- execute: "/workflows/{id}/execute"
189
- }
190
- },
191
- timeout: 30000,
192
- auth: {
193
- type: "bearer",
194
- token: "your-token"
195
- }
179
+ baseUrl: 'https://api.example.com',
180
+ endpoints: {
181
+ nodes: {
182
+ list: '/nodes',
183
+ get: '/nodes/{id}'
184
+ },
185
+ workflows: {
186
+ list: '/workflows',
187
+ get: '/workflows/{id}',
188
+ create: '/workflows',
189
+ update: '/workflows/{id}',
190
+ delete: '/workflows/{id}',
191
+ execute: '/workflows/{id}/execute'
192
+ }
193
+ },
194
+ timeout: 30000,
195
+ auth: {
196
+ type: 'bearer',
197
+ token: 'your-token'
198
+ }
196
199
  });
197
200
  ```
198
201
 
@@ -204,10 +207,10 @@ Override CSS custom properties:
204
207
 
205
208
  ```css
206
209
  :root {
207
- --flowdrop-background-color: #f9fafb;
208
- --flowdrop-primary-color: #3b82f6;
209
- --flowdrop-border-color: #e5e7eb;
210
- --flowdrop-text-color: #1f2937;
210
+ --flowdrop-background-color: #f9fafb;
211
+ --flowdrop-primary-color: #3b82f6;
212
+ --flowdrop-border-color: #e5e7eb;
213
+ --flowdrop-text-color: #1f2937;
211
214
  }
212
215
  ```
213
216
 
@@ -217,37 +220,37 @@ Define custom node types:
217
220
 
218
221
  ```typescript
219
222
  const customNode: NodeMetadata = {
220
- id: "custom_processor",
221
- name: "Custom Processor",
222
- category: "data_processing",
223
- description: "Process data with custom logic",
224
- icon: "mdi:cog",
225
- color: "#3b82f6",
226
- inputs: [
227
- {
228
- id: "input",
229
- name: "Input",
230
- type: "input",
231
- dataType: "mixed"
232
- }
233
- ],
234
- outputs: [
235
- {
236
- id: "output",
237
- name: "Output",
238
- type: "output",
239
- dataType: "mixed"
240
- }
241
- ],
242
- configSchema: {
243
- type: "object",
244
- properties: {
245
- operation: {
246
- type: "string",
247
- title: "Operation"
248
- }
249
- }
250
- }
223
+ id: 'custom_processor',
224
+ name: 'Custom Processor',
225
+ category: 'data_processing',
226
+ description: 'Process data with custom logic',
227
+ icon: 'mdi:cog',
228
+ color: '#3b82f6',
229
+ inputs: [
230
+ {
231
+ id: 'input',
232
+ name: 'Input',
233
+ type: 'input',
234
+ dataType: 'mixed'
235
+ }
236
+ ],
237
+ outputs: [
238
+ {
239
+ id: 'output',
240
+ name: 'Output',
241
+ type: 'output',
242
+ dataType: 'mixed'
243
+ }
244
+ ],
245
+ configSchema: {
246
+ type: 'object',
247
+ properties: {
248
+ operation: {
249
+ type: 'string',
250
+ title: 'Operation'
251
+ }
252
+ }
253
+ }
251
254
  };
252
255
  ```
253
256
 
@@ -17,7 +17,6 @@
17
17
  import { createEndpointConfig } from '../config/endpoints.js';
18
18
  import type { EndpointConfig } from '../config/endpoints.js';
19
19
  import { workflowStore, workflowActions, workflowName } from '../stores/workflowStore.js';
20
- import { resolveComponentName } from '../utils/nodeTypes.js';
21
20
  import { apiToasts, dismissToast } from '../services/toastService.js';
22
21
 
23
22
  // Configuration props for runtime customization
@@ -42,6 +41,8 @@
42
41
  variant?: 'primary' | 'secondary' | 'outline';
43
42
  onclick?: (event: Event) => void;
44
43
  }>;
44
+ // API configuration - optional, defaults to '/api/flowdrop'
45
+ apiBaseUrl?: string;
45
46
  }
46
47
 
47
48
  let {
@@ -55,7 +56,8 @@
55
56
  nodeStatuses = {},
56
57
  pipelineId,
57
58
  navbarTitle,
58
- navbarActions = []
59
+ navbarActions = [],
60
+ apiBaseUrl
59
61
  }: Props = $props();
60
62
 
61
63
  // Create breadcrumb-style title - at top level to avoid store subscription issues
@@ -75,7 +77,6 @@
75
77
  // Remove workflow prop - use global store directly
76
78
  // let workflow = $derived($workflowStore || initialWorkflow);
77
79
  let error = $state<string | null>(null);
78
- let loading = $state(true);
79
80
  let endpointConfig = $state<EndpointConfig | null>(null);
80
81
 
81
82
  // ConfigSidebar state
@@ -130,7 +131,6 @@
130
131
  // Show loading toast
131
132
  const loadingToast = apiToasts.loading('Loading node types');
132
133
  try {
133
- loading = true;
134
134
  error = null;
135
135
 
136
136
  const fetchedNodes = await api.nodes.getNodes();
@@ -149,8 +149,6 @@
149
149
 
150
150
  // Fallback to sample data
151
151
  nodes = sampleNodes;
152
- } finally {
153
- loading = false;
154
152
  }
155
153
  }
156
154
 
@@ -183,14 +181,23 @@
183
181
 
184
182
  /**
185
183
  * Initialize API endpoints
184
+ * Only initializes if not already configured (respects configuration from parent)
186
185
  */
187
186
  async function initializeApiEndpoints(): Promise<void> {
188
- // Use the same environment variable priority as the global save function
189
- // Prioritize VITE_API_BASE_URL since it's configured correctly
190
- const apiBaseUrl =
191
- import.meta.env.VITE_API_BASE_URL || import.meta.env.VITE_DRUPAL_API_URL || '/api/flowdrop';
187
+ // Check if endpoint config is already set (e.g., by parent layout)
188
+ const { getEndpointConfig } = await import('../services/api.js');
189
+ const existingConfig = getEndpointConfig();
192
190
 
193
- const config = createEndpointConfig(apiBaseUrl, {
191
+ // If config already exists and no override provided, use existing
192
+ if (existingConfig && !apiBaseUrl) {
193
+ endpointConfig = existingConfig;
194
+ return;
195
+ }
196
+
197
+ // Use provided apiBaseUrl or default
198
+ const baseUrl = apiBaseUrl || '/api/flowdrop';
199
+
200
+ const config = createEndpointConfig(baseUrl, {
194
201
  auth: {
195
202
  type: 'none' // No authentication for now
196
203
  },
@@ -270,60 +277,56 @@
270
277
  * Save workflow - exposed API function
271
278
  */
272
279
  async function saveWorkflow(): Promise<void> {
273
- try {
274
- // Wait for any pending DOM updates before saving
275
- await tick();
280
+ // Wait for any pending DOM updates before saving
281
+ await tick();
276
282
 
277
- // Import necessary modules
278
- const { workflowApi } = await import('../services/api.js');
279
- const { v4: uuidv4 } = await import('uuid');
283
+ // Import necessary modules
284
+ const { workflowApi } = await import('../services/api.js');
285
+ const { v4: uuidv4 } = await import('uuid');
280
286
 
281
- // Use current workflow from global store
282
- const workflowToSave = $workflowStore;
287
+ // Use current workflow from global store
288
+ const workflowToSave = $workflowStore;
283
289
 
284
- if (!workflowToSave) {
285
- return;
286
- }
290
+ if (!workflowToSave) {
291
+ return;
292
+ }
287
293
 
288
- // Determine the workflow ID
289
- let workflowId: string;
290
- if (workflowToSave.id) {
291
- workflowId = workflowToSave.id;
292
- } else {
293
- workflowId = uuidv4();
294
+ // Determine the workflow ID
295
+ let workflowId: string;
296
+ if (workflowToSave.id) {
297
+ workflowId = workflowToSave.id;
298
+ } else {
299
+ workflowId = uuidv4();
300
+ }
301
+
302
+ // Create workflow object for saving
303
+ const finalWorkflow = {
304
+ id: workflowId,
305
+ name: workflowToSave.name || 'Untitled Workflow',
306
+ description: workflowToSave.description || '',
307
+ nodes: workflowToSave.nodes || [],
308
+ edges: workflowToSave.edges || [],
309
+ metadata: {
310
+ version: '1.0.0',
311
+ createdAt: workflowToSave.metadata?.createdAt || new Date().toISOString(),
312
+ updatedAt: new Date().toISOString()
294
313
  }
314
+ };
295
315
 
296
- // Create workflow object for saving
297
- const finalWorkflow = {
298
- id: workflowId,
299
- name: workflowToSave.name || 'Untitled Workflow',
300
- description: workflowToSave.description || '',
301
- nodes: workflowToSave.nodes || [],
302
- edges: workflowToSave.edges || [],
316
+ const savedWorkflow = await workflowApi.saveWorkflow(finalWorkflow);
317
+
318
+ // Update the workflow ID if it changed (new workflow)
319
+ // Keep our current workflow state, only update ID and metadata from backend
320
+ if (savedWorkflow.id && savedWorkflow.id !== finalWorkflow.id) {
321
+ workflowActions.batchUpdate({
322
+ nodes: finalWorkflow.nodes,
323
+ edges: finalWorkflow.edges,
324
+ name: finalWorkflow.name,
303
325
  metadata: {
304
- version: '1.0.0',
305
- createdAt: workflowToSave.metadata?.createdAt || new Date().toISOString(),
306
- updatedAt: new Date().toISOString()
326
+ ...finalWorkflow.metadata,
327
+ ...savedWorkflow.metadata
307
328
  }
308
- };
309
-
310
- const savedWorkflow = await workflowApi.saveWorkflow(finalWorkflow);
311
-
312
- // Update the workflow ID if it changed (new workflow)
313
- // Keep our current workflow state, only update ID and metadata from backend
314
- if (savedWorkflow.id && savedWorkflow.id !== finalWorkflow.id) {
315
- workflowActions.batchUpdate({
316
- nodes: finalWorkflow.nodes,
317
- edges: finalWorkflow.edges,
318
- name: finalWorkflow.name,
319
- metadata: {
320
- ...finalWorkflow.metadata,
321
- ...savedWorkflow.metadata
322
- }
323
- });
324
- }
325
- } catch (error) {
326
- throw error; // Re-throw so caller can handle
329
+ });
327
330
  }
328
331
  }
329
332
 
@@ -364,16 +367,16 @@
364
367
  link.download = `${finalWorkflow.name}.json`;
365
368
  link.click();
366
369
  URL.revokeObjectURL(url);
367
- } catch (error) {
370
+ } catch {
368
371
  // Export failed
369
372
  }
370
373
  }
371
374
 
372
375
  // Expose save and export functions globally for external access
373
376
  if (typeof window !== 'undefined') {
374
- // @ts-ignore - Adding to window for external access
377
+ // @ts-expect-error - Adding to window for external access
375
378
  window.flowdropSave = saveWorkflow;
376
- // @ts-ignore - Adding to window for external access
379
+ // @ts-expect-error - Adding to window for external access
377
380
  window.flowdropExport = exportWorkflow;
378
381
  }
379
382
 
@@ -651,7 +654,7 @@
651
654
 
652
655
  <!-- Render configuration fields based on schema -->
653
656
  {#if configSchema.properties}
654
- {#each Object.entries(configSchema.properties) as [key, field]}
657
+ {#each Object.entries(configSchema.properties) as [key, field] (key)}
655
658
  {@const fieldConfig = field as any}
656
659
  {#if fieldConfig.format !== 'hidden'}
657
660
  <div class="flowdrop-config-sidebar__field">
@@ -661,7 +664,7 @@
661
664
  {#if fieldConfig.enum && fieldConfig.multiple}
662
665
  <!-- Checkboxes for enum with multiple selection -->
663
666
  <div class="flowdrop-config-sidebar__checkbox-group">
664
- {#each fieldConfig.enum as option}
667
+ {#each fieldConfig.enum as option (String(option))}
665
668
  <label class="flowdrop-config-sidebar__checkbox-item">
666
669
  <input
667
670
  type="checkbox"
@@ -698,7 +701,7 @@
698
701
  class="flowdrop-config-sidebar__select"
699
702
  bind:value={configValues[key]}
700
703
  >
701
- {#each fieldConfig.enum as option}
704
+ {#each fieldConfig.enum as option (String(option))}
702
705
  <option value={String(option)}>{String(option)}</option>
703
706
  {/each}
704
707
  </select>
@@ -744,7 +747,7 @@
744
747
  bind:value={configValues[key]}
745
748
  >
746
749
  {#if fieldConfig.options}
747
- {#each fieldConfig.options as option}
750
+ {#each fieldConfig.options as option (String(option.value))}
748
751
  {@const optionConfig = option as any}
749
752
  <option value={String(optionConfig.value)}
750
753
  >{String(optionConfig.label)}</option
@@ -17,6 +17,7 @@ interface Props {
17
17
  variant?: 'primary' | 'secondary' | 'outline';
18
18
  onclick?: (event: Event) => void;
19
19
  }>;
20
+ apiBaseUrl?: string;
20
21
  }
21
22
  declare const App: import("svelte").Component<Props, {}, "">;
22
23
  type App = ReturnType<typeof App>;
@@ -33,19 +33,9 @@
33
33
 
34
34
  let { primaryActions = [], showStatus = true, title, breadcrumbs = [] }: Props = $props();
35
35
 
36
- // Simple current path tracking without SvelteKit dependency
37
- let currentPath = $state(typeof window !== 'undefined' ? window.location.pathname : '/');
38
-
39
36
  // Dropdown state
40
37
  let isDropdownOpen = $state(false);
41
38
 
42
- function isActive(href: string): boolean {
43
- if (href === '/') {
44
- return currentPath === '/';
45
- }
46
- return currentPath.startsWith(href);
47
- }
48
-
49
39
  // Close dropdown when clicking outside
50
40
  function handleClickOutside(event: MouseEvent) {
51
41
  const target = event.target as HTMLElement;
@@ -93,7 +83,7 @@
93
83
  <div class="flowdrop-navbar__breadcrumb-container">
94
84
  <nav class="flowdrop-navbar__breadcrumb" aria-label="Breadcrumb">
95
85
  <ol class="flowdrop-navbar__breadcrumb-list">
96
- {#each breadcrumbs as breadcrumb, index}
86
+ {#each breadcrumbs as breadcrumb, index (index)}
97
87
  <li class="flowdrop-navbar__breadcrumb-item">
98
88
  {#if breadcrumb.href && index < breadcrumbs.length - 1}
99
89
  <a href={breadcrumb.href} class="flowdrop-navbar__breadcrumb-link">
@@ -10,6 +10,7 @@
10
10
  import Icon from '@iconify/svelte';
11
11
  import { getNodeIcon, getCategoryIcon } from '../utils/icons.js';
12
12
  import { getCategoryColorToken } from '../utils/colors.js';
13
+ import { SvelteSet } from 'svelte/reactivity';
13
14
 
14
15
  interface Props {
15
16
  nodes: NodeMetadata[];
@@ -29,7 +30,7 @@
29
30
  function getCategories(): NodeCategory[] {
30
31
  const nodes = props.nodes || [];
31
32
  if (nodes.length === 0) return [];
32
- const categories = new Set<NodeCategory>();
33
+ const categories = new SvelteSet<NodeCategory>();
33
34
  nodes.forEach((node) => categories.add(node.category));
34
35
  return Array.from(categories).sort();
35
36
  }
@@ -19,7 +19,6 @@
19
19
  } from '../utils/nodeStatus.js';
20
20
 
21
21
  interface Props {
22
- nodeId: string;
23
22
  executionInfo?: NodeExecutionInfo;
24
23
  position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
25
24
  size?: 'sm' | 'md' | 'lg';
@@ -1,6 +1,5 @@
1
1
  import type { NodeExecutionInfo } from '../types/index.js';
2
2
  interface Props {
3
- nodeId: string;
4
3
  executionInfo?: NodeExecutionInfo;
5
4
  position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
6
5
  size?: 'sm' | 'md' | 'lg';
@@ -63,7 +63,6 @@
63
63
 
64
64
  // Loading and error states
65
65
  let isLoadingJobStatus = $state(false);
66
- let error = $state<string | null>(null);
67
66
 
68
67
  // Logs sidebar state
69
68
  let isLogsSidebarOpen = $state(false);
@@ -147,42 +146,6 @@
147
146
  isLogsSidebarOpen = !isLogsSidebarOpen;
148
147
  }
149
148
 
150
- /**
151
- * Get status color for visual indicators
152
- */
153
- function getStatusColor(status: string): string {
154
- switch (status) {
155
- case 'completed':
156
- return '#10b981'; // green
157
- case 'running':
158
- return '#3b82f6'; // blue
159
- case 'error':
160
- case 'failed':
161
- return '#ef4444'; // red
162
- case 'pending':
163
- default:
164
- return '#6b7280'; // gray
165
- }
166
- }
167
-
168
- /**
169
- * Get status icon for visual indicators
170
- */
171
- function getStatusIcon(status: string): string {
172
- switch (status) {
173
- case 'completed':
174
- return 'mdi:check-circle';
175
- case 'running':
176
- return 'mdi:loading';
177
- case 'error':
178
- case 'failed':
179
- return 'mdi:alert-circle';
180
- case 'pending':
181
- default:
182
- return 'mdi:clock-outline';
183
- }
184
- }
185
-
186
149
  /**
187
150
  * Get pipeline actions for the parent navbar
188
151
  */
@@ -107,13 +107,6 @@
107
107
  props.data.metadata?.outputs?.find((port) => port.dataType !== 'trigger')
108
108
  );
109
109
 
110
- // Use trigger port if present, otherwise use first data port
111
- let firstInputPort = $derived(triggerInputPort || firstDataInputPort);
112
- let firstOutputPort = $derived(triggerOutputPort || firstDataOutputPort);
113
-
114
- let hasInput = $derived(!!firstInputPort);
115
- let hasOutput = $derived(!!firstOutputPort);
116
-
117
110
  // Check if we need to show both trigger and data ports
118
111
  let hasBothInputTypes = $derived(!!triggerInputPort && !!firstDataInputPort);
119
112
  let hasBothOutputTypes = $derived(!!triggerOutputPort && !!firstDataOutputPort);