@d34dman/flowdrop 0.0.5 → 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,12 +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 default API base URL - can be overridden via runtime configuration
189
- const apiBaseUrl = '/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();
190
190
 
191
- 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, {
192
201
  auth: {
193
202
  type: 'none' // No authentication for now
194
203
  },
@@ -268,60 +277,56 @@
268
277
  * Save workflow - exposed API function
269
278
  */
270
279
  async function saveWorkflow(): Promise<void> {
271
- try {
272
- // Wait for any pending DOM updates before saving
273
- await tick();
280
+ // Wait for any pending DOM updates before saving
281
+ await tick();
274
282
 
275
- // Import necessary modules
276
- const { workflowApi } = await import('../services/api.js');
277
- 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');
278
286
 
279
- // Use current workflow from global store
280
- const workflowToSave = $workflowStore;
287
+ // Use current workflow from global store
288
+ const workflowToSave = $workflowStore;
281
289
 
282
- if (!workflowToSave) {
283
- return;
284
- }
290
+ if (!workflowToSave) {
291
+ return;
292
+ }
285
293
 
286
- // Determine the workflow ID
287
- let workflowId: string;
288
- if (workflowToSave.id) {
289
- workflowId = workflowToSave.id;
290
- } else {
291
- 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()
292
313
  }
314
+ };
293
315
 
294
- // Create workflow object for saving
295
- const finalWorkflow = {
296
- id: workflowId,
297
- name: workflowToSave.name || 'Untitled Workflow',
298
- description: workflowToSave.description || '',
299
- nodes: workflowToSave.nodes || [],
300
- 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,
301
325
  metadata: {
302
- version: '1.0.0',
303
- createdAt: workflowToSave.metadata?.createdAt || new Date().toISOString(),
304
- updatedAt: new Date().toISOString()
326
+ ...finalWorkflow.metadata,
327
+ ...savedWorkflow.metadata
305
328
  }
306
- };
307
-
308
- const savedWorkflow = await workflowApi.saveWorkflow(finalWorkflow);
309
-
310
- // Update the workflow ID if it changed (new workflow)
311
- // Keep our current workflow state, only update ID and metadata from backend
312
- if (savedWorkflow.id && savedWorkflow.id !== finalWorkflow.id) {
313
- workflowActions.batchUpdate({
314
- nodes: finalWorkflow.nodes,
315
- edges: finalWorkflow.edges,
316
- name: finalWorkflow.name,
317
- metadata: {
318
- ...finalWorkflow.metadata,
319
- ...savedWorkflow.metadata
320
- }
321
- });
322
- }
323
- } catch (error) {
324
- throw error; // Re-throw so caller can handle
329
+ });
325
330
  }
326
331
  }
327
332
 
@@ -362,16 +367,16 @@
362
367
  link.download = `${finalWorkflow.name}.json`;
363
368
  link.click();
364
369
  URL.revokeObjectURL(url);
365
- } catch (error) {
370
+ } catch {
366
371
  // Export failed
367
372
  }
368
373
  }
369
374
 
370
375
  // Expose save and export functions globally for external access
371
376
  if (typeof window !== 'undefined') {
372
- // @ts-ignore - Adding to window for external access
377
+ // @ts-expect-error - Adding to window for external access
373
378
  window.flowdropSave = saveWorkflow;
374
- // @ts-ignore - Adding to window for external access
379
+ // @ts-expect-error - Adding to window for external access
375
380
  window.flowdropExport = exportWorkflow;
376
381
  }
377
382
 
@@ -649,7 +654,7 @@
649
654
 
650
655
  <!-- Render configuration fields based on schema -->
651
656
  {#if configSchema.properties}
652
- {#each Object.entries(configSchema.properties) as [key, field]}
657
+ {#each Object.entries(configSchema.properties) as [key, field] (key)}
653
658
  {@const fieldConfig = field as any}
654
659
  {#if fieldConfig.format !== 'hidden'}
655
660
  <div class="flowdrop-config-sidebar__field">
@@ -659,7 +664,7 @@
659
664
  {#if fieldConfig.enum && fieldConfig.multiple}
660
665
  <!-- Checkboxes for enum with multiple selection -->
661
666
  <div class="flowdrop-config-sidebar__checkbox-group">
662
- {#each fieldConfig.enum as option}
667
+ {#each fieldConfig.enum as option (String(option))}
663
668
  <label class="flowdrop-config-sidebar__checkbox-item">
664
669
  <input
665
670
  type="checkbox"
@@ -696,7 +701,7 @@
696
701
  class="flowdrop-config-sidebar__select"
697
702
  bind:value={configValues[key]}
698
703
  >
699
- {#each fieldConfig.enum as option}
704
+ {#each fieldConfig.enum as option (String(option))}
700
705
  <option value={String(option)}>{String(option)}</option>
701
706
  {/each}
702
707
  </select>
@@ -742,7 +747,7 @@
742
747
  bind:value={configValues[key]}
743
748
  >
744
749
  {#if fieldConfig.options}
745
- {#each fieldConfig.options as option}
750
+ {#each fieldConfig.options as option (String(option.value))}
746
751
  {@const optionConfig = option as any}
747
752
  <option value={String(optionConfig.value)}
748
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);