@d34dman/flowdrop 0.0.22 → 0.0.23

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 (42) hide show
  1. package/dist/components/App.svelte +26 -25
  2. package/dist/components/ConfigForm.svelte +140 -519
  3. package/dist/components/ConfigForm.svelte.d.ts +5 -3
  4. package/dist/components/form/FormArray.svelte +1049 -0
  5. package/dist/components/form/FormArray.svelte.d.ts +22 -0
  6. package/dist/components/form/FormCheckboxGroup.svelte +152 -0
  7. package/dist/components/form/FormCheckboxGroup.svelte.d.ts +15 -0
  8. package/dist/components/form/FormField.svelte +279 -0
  9. package/dist/components/form/FormField.svelte.d.ts +18 -0
  10. package/dist/components/form/FormFieldWrapper.svelte +133 -0
  11. package/dist/components/form/FormFieldWrapper.svelte.d.ts +18 -0
  12. package/dist/components/form/FormNumberField.svelte +109 -0
  13. package/dist/components/form/FormNumberField.svelte.d.ts +23 -0
  14. package/dist/components/form/FormSelect.svelte +126 -0
  15. package/dist/components/form/FormSelect.svelte.d.ts +18 -0
  16. package/dist/components/form/FormTextField.svelte +88 -0
  17. package/dist/components/form/FormTextField.svelte.d.ts +17 -0
  18. package/dist/components/form/FormTextarea.svelte +94 -0
  19. package/dist/components/form/FormTextarea.svelte.d.ts +19 -0
  20. package/dist/components/form/FormToggle.svelte +123 -0
  21. package/dist/components/form/FormToggle.svelte.d.ts +17 -0
  22. package/dist/components/form/index.d.ts +41 -0
  23. package/dist/components/form/index.js +45 -0
  24. package/dist/components/form/types.d.ts +208 -0
  25. package/dist/components/form/types.js +29 -0
  26. package/dist/components/nodes/GatewayNode.svelte +84 -12
  27. package/dist/components/nodes/SimpleNode.svelte +41 -5
  28. package/dist/components/nodes/SimpleNode.svelte.d.ts +2 -1
  29. package/dist/components/nodes/SquareNode.svelte +41 -5
  30. package/dist/components/nodes/SquareNode.svelte.d.ts +2 -1
  31. package/dist/components/nodes/WorkflowNode.svelte +88 -5
  32. package/dist/index.d.ts +2 -3
  33. package/dist/index.js +1 -3
  34. package/dist/stores/workflowStore.d.ts +15 -0
  35. package/dist/stores/workflowStore.js +28 -0
  36. package/dist/types/index.d.ts +77 -0
  37. package/dist/types/index.js +16 -0
  38. package/package.json +3 -3
  39. package/dist/config/demo.d.ts +0 -58
  40. package/dist/config/demo.js +0 -142
  41. package/dist/data/samples.d.ts +0 -51
  42. package/dist/data/samples.js +0 -3245
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Form Components Module
3
+ *
4
+ * Modular form components for dynamic form rendering based on JSON Schema.
5
+ * Designed for extensibility to support complex types like arrays and objects.
6
+ *
7
+ * @module form
8
+ *
9
+ * @example
10
+ * ```svelte
11
+ * <script>
12
+ * import { FormField } from "./";
13
+ * import type { FieldSchema } from "./";
14
+ *
15
+ * const schema: FieldSchema = {
16
+ * type: "string",
17
+ * title: "Name",
18
+ * description: "Enter your name"
19
+ * };
20
+ *
21
+ * let value = $state("");
22
+ * </script>
23
+ *
24
+ * <FormField
25
+ * fieldKey="name"
26
+ * {schema}
27
+ * {value}
28
+ * onChange={(v) => value = v}
29
+ * />
30
+ * ```
31
+ */
32
+ // Types
33
+ export * from "./types.js";
34
+ // Main factory component
35
+ export { default as FormField } from "./FormField.svelte";
36
+ // Wrapper component
37
+ export { default as FormFieldWrapper } from "./FormFieldWrapper.svelte";
38
+ // Individual field components
39
+ export { default as FormTextField } from "./FormTextField.svelte";
40
+ export { default as FormTextarea } from "./FormTextarea.svelte";
41
+ export { default as FormNumberField } from "./FormNumberField.svelte";
42
+ export { default as FormToggle } from "./FormToggle.svelte";
43
+ export { default as FormSelect } from "./FormSelect.svelte";
44
+ export { default as FormCheckboxGroup } from "./FormCheckboxGroup.svelte";
45
+ export { default as FormArray } from "./FormArray.svelte";
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Form Field Types
3
+ * Shared types for form components in the FlowDrop workflow editor
4
+ *
5
+ * These types provide a foundation for dynamic form rendering based on JSON Schema
6
+ * and support extensibility for complex field types like arrays and objects.
7
+ */
8
+ /**
9
+ * Supported field types for form rendering
10
+ * Can be extended to support complex types like 'array' and 'object'
11
+ */
12
+ export type FieldType = "string" | "number" | "integer" | "boolean" | "select" | "array" | "object";
13
+ /**
14
+ * Field format for specialized rendering
15
+ * - multiline: Renders as textarea
16
+ * - hidden: Field is hidden from UI but included in form submission
17
+ */
18
+ export type FieldFormat = "multiline" | "hidden" | string;
19
+ /**
20
+ * Option type for select and checkbox group fields
21
+ */
22
+ export interface FieldOption {
23
+ /** The value stored when this option is selected */
24
+ value: string;
25
+ /** The display label for this option */
26
+ label: string;
27
+ }
28
+ /**
29
+ * Base properties shared by all form fields
30
+ */
31
+ export interface BaseFieldProps {
32
+ /** Unique identifier for the field (used for id and name attributes) */
33
+ id: string;
34
+ /** Current value of the field */
35
+ value: unknown;
36
+ /** Whether the field is required */
37
+ required?: boolean;
38
+ /** Whether the field is disabled */
39
+ disabled?: boolean;
40
+ /** Placeholder text for input fields */
41
+ placeholder?: string;
42
+ /** ARIA description ID for accessibility */
43
+ ariaDescribedBy?: string;
44
+ /** Callback when the field value changes */
45
+ onChange: (value: unknown) => void;
46
+ }
47
+ /**
48
+ * Properties for text input fields
49
+ */
50
+ export interface TextFieldProps extends BaseFieldProps {
51
+ value: string;
52
+ onChange: (value: string) => void;
53
+ }
54
+ /**
55
+ * Properties for multiline text fields (textarea)
56
+ */
57
+ export interface TextareaFieldProps extends BaseFieldProps {
58
+ value: string;
59
+ /** Number of visible rows */
60
+ rows?: number;
61
+ onChange: (value: string) => void;
62
+ }
63
+ /**
64
+ * Properties for number input fields
65
+ */
66
+ export interface NumberFieldProps extends BaseFieldProps {
67
+ value: number | string;
68
+ /** Minimum allowed value */
69
+ min?: number;
70
+ /** Maximum allowed value */
71
+ max?: number;
72
+ /** Step increment for the input */
73
+ step?: number;
74
+ onChange: (value: number | string) => void;
75
+ }
76
+ /**
77
+ * Properties for boolean toggle fields
78
+ */
79
+ export interface ToggleFieldProps extends BaseFieldProps {
80
+ value: boolean;
81
+ /** Label shown when toggle is on */
82
+ onLabel?: string;
83
+ /** Label shown when toggle is off */
84
+ offLabel?: string;
85
+ onChange: (value: boolean) => void;
86
+ }
87
+ /**
88
+ * Properties for select dropdown fields
89
+ */
90
+ export interface SelectFieldProps extends BaseFieldProps {
91
+ value: string;
92
+ /** Available options for selection */
93
+ options: FieldOption[] | string[];
94
+ onChange: (value: string) => void;
95
+ }
96
+ /**
97
+ * Properties for checkbox group fields (multiple selection)
98
+ */
99
+ export interface CheckboxGroupFieldProps extends BaseFieldProps {
100
+ value: string[];
101
+ /** Available options for selection */
102
+ options: string[];
103
+ onChange: (value: string[]) => void;
104
+ }
105
+ /**
106
+ * Properties for array fields (dynamic lists)
107
+ */
108
+ export interface ArrayFieldProps extends BaseFieldProps {
109
+ value: unknown[];
110
+ /** Schema for array items */
111
+ itemSchema: FieldSchema;
112
+ /** Minimum number of items required */
113
+ minItems?: number;
114
+ /** Maximum number of items allowed */
115
+ maxItems?: number;
116
+ /** Label for the add button */
117
+ addLabel?: string;
118
+ onChange: (value: unknown[]) => void;
119
+ }
120
+ /**
121
+ * Field schema definition derived from JSON Schema property
122
+ * Used to determine which field component to render
123
+ */
124
+ export interface FieldSchema {
125
+ /** The field type from JSON Schema */
126
+ type: FieldType | string;
127
+ /** Display title for the field */
128
+ title?: string;
129
+ /** Description for help text */
130
+ description?: string;
131
+ /** Default value for the field */
132
+ default?: unknown;
133
+ /** Enum values for select/checkbox fields */
134
+ enum?: unknown[];
135
+ /** Whether multiple values can be selected */
136
+ multiple?: boolean;
137
+ /** Format hint for specialized rendering */
138
+ format?: FieldFormat;
139
+ /** Options for select type fields (alternative to enum) */
140
+ options?: FieldOption[];
141
+ /** Placeholder text */
142
+ placeholder?: string;
143
+ /** Minimum value for numbers */
144
+ minimum?: number;
145
+ /** Maximum value for numbers */
146
+ maximum?: number;
147
+ /** Minimum length for strings */
148
+ minLength?: number;
149
+ /** Maximum length for strings */
150
+ maxLength?: number;
151
+ /** Validation pattern for strings */
152
+ pattern?: string;
153
+ /** Item schema for array fields */
154
+ items?: FieldSchema;
155
+ /** Minimum number of items for array fields */
156
+ minItems?: number;
157
+ /** Maximum number of items for array fields */
158
+ maxItems?: number;
159
+ /** Property schemas for object fields (future use) */
160
+ properties?: Record<string, FieldSchema>;
161
+ /** Required properties for object fields (future use) */
162
+ required?: string[];
163
+ /** Allow additional properties not defined by the schema */
164
+ [key: string]: unknown;
165
+ }
166
+ /**
167
+ * Props for the FormField factory component
168
+ * Renders the appropriate field component based on schema
169
+ */
170
+ export interface FormFieldFactoryProps {
171
+ /** Unique key/id for the field */
172
+ fieldKey: string;
173
+ /** Field schema definition */
174
+ schema: FieldSchema;
175
+ /** Current field value */
176
+ value: unknown;
177
+ /** Whether the field is required */
178
+ required?: boolean;
179
+ /** Animation delay index for staggered animations */
180
+ animationIndex?: number;
181
+ /** Callback when the field value changes */
182
+ onChange: (value: unknown) => void;
183
+ }
184
+ /**
185
+ * Props for the FormFieldWrapper component
186
+ * Provides label, description, and layout for field content
187
+ */
188
+ export interface FormFieldWrapperProps {
189
+ /** Field identifier for label association */
190
+ id: string;
191
+ /** Display label text */
192
+ label: string;
193
+ /** Whether the field is required */
194
+ required?: boolean;
195
+ /** Description/help text for the field */
196
+ description?: string;
197
+ /** Animation delay in milliseconds */
198
+ animationDelay?: number;
199
+ }
200
+ /**
201
+ * Type guard to check if options are FieldOption objects
202
+ */
203
+ export declare function isFieldOptionArray(options: FieldOption[] | string[]): options is FieldOption[];
204
+ /**
205
+ * Normalize options to FieldOption format
206
+ * Converts string arrays to FieldOption arrays for consistent handling
207
+ */
208
+ export declare function normalizeOptions(options: FieldOption[] | string[] | unknown[]): FieldOption[];
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Form Field Types
3
+ * Shared types for form components in the FlowDrop workflow editor
4
+ *
5
+ * These types provide a foundation for dynamic form rendering based on JSON Schema
6
+ * and support extensibility for complex field types like arrays and objects.
7
+ */
8
+ /**
9
+ * Type guard to check if options are FieldOption objects
10
+ */
11
+ export function isFieldOptionArray(options) {
12
+ return options.length > 0 && typeof options[0] === "object" && "value" in options[0];
13
+ }
14
+ /**
15
+ * Normalize options to FieldOption format
16
+ * Converts string arrays to FieldOption arrays for consistent handling
17
+ */
18
+ export function normalizeOptions(options) {
19
+ if (!options || options.length === 0) {
20
+ return [];
21
+ }
22
+ if (isFieldOptionArray(options)) {
23
+ return options;
24
+ }
25
+ return options.map((opt) => ({
26
+ value: String(opt),
27
+ label: String(opt)
28
+ }));
29
+ }
@@ -3,19 +3,29 @@
3
3
  Visual representation of gateway/branch nodes with branching flow indicators
4
4
  Shows active branches and execution paths
5
5
  Styled with BEM syntax following WorkflowNode pattern
6
+
7
+ UI Extensions Support:
8
+ - hideUnconnectedHandles: Hides ports that are not connected to reduce visual clutter
6
9
  -->
7
10
 
8
11
  <script lang="ts">
9
12
  import { Position, Handle } from '@xyflow/svelte';
10
- import type { WorkflowNode } from '../../types/index.js';
13
+ import type { WorkflowNode, NodePort } from '../../types/index.js';
11
14
  import Icon from '@iconify/svelte';
12
15
  import { getNodeIcon } from '../../utils/icons.js';
13
16
  import { getDataTypeColorToken, getCategoryColorToken } from '../../utils/colors.js';
17
+ import { connectedHandles } from '../../stores/workflowStore.js';
14
18
 
15
- // Define simplified branch interface - conditions are handled by backend
19
+ /**
20
+ * Branch interface for gateway nodes
21
+ * - name: Internal identifier used for handle IDs and connections
22
+ * - label: Display label shown in the UI (optional, defaults to name)
23
+ * - value: Optional value associated with the branch (e.g., for Switch matching)
24
+ */
16
25
  interface Branch {
17
26
  name: string;
18
- label: string;
27
+ label?: string;
28
+ value?: string;
19
29
  }
20
30
 
21
31
  interface Props {
@@ -28,10 +38,70 @@
28
38
 
29
39
  let props: Props = $props();
30
40
 
41
+ /**
42
+ * Get the hideUnconnectedHandles setting from extensions
43
+ * Merges node type defaults with instance overrides
44
+ */
45
+ const hideUnconnectedHandles = $derived(() => {
46
+ const typeDefault = props.data.metadata?.extensions?.ui?.hideUnconnectedHandles ?? false;
47
+ const instanceOverride = props.data.extensions?.ui?.hideUnconnectedHandles;
48
+ return instanceOverride ?? typeDefault;
49
+ });
50
+
51
+ /**
52
+ * Check if a port should be visible based on connection state and settings
53
+ * @param port - The port to check
54
+ * @param type - Whether this is an 'input' or 'output' port
55
+ * @returns true if the port should be visible
56
+ */
57
+ function isPortVisible(port: NodePort, type: 'input' | 'output'): boolean {
58
+ // Always show if hideUnconnectedHandles is disabled
59
+ if (!hideUnconnectedHandles()) {
60
+ return true;
61
+ }
62
+
63
+ // Always show required ports
64
+ if (port.required) {
65
+ return true;
66
+ }
67
+
68
+ // Check if port is connected
69
+ const handleId = `${props.data.nodeId}-${type}-${port.id}`;
70
+ return $connectedHandles.has(handleId);
71
+ }
72
+
73
+ /**
74
+ * Derived list of visible input ports based on hideUnconnectedHandles setting
75
+ */
76
+ const visibleInputPorts = $derived(
77
+ props.data.metadata.inputs.filter((port) => isPortVisible(port, 'input'))
78
+ );
79
+
80
+ /**
81
+ * Check if a branch output should be visible based on connection state
82
+ * @param branchName - The branch name to check
83
+ * @returns true if the branch should be visible
84
+ */
85
+ function isBranchVisible(branchName: string): boolean {
86
+ // Always show if hideUnconnectedHandles is disabled
87
+ if (!hideUnconnectedHandles()) {
88
+ return true;
89
+ }
90
+
91
+ // Check if branch output is connected
92
+ const handleId = `${props.data.nodeId}-output-${branchName}`;
93
+ return $connectedHandles.has(handleId);
94
+ }
95
+
31
96
  // Gateway-specific data - branches are calculated at runtime from config
32
97
  let branches = $derived((props.data.config?.branches as Branch[]) || []);
33
98
  let activeBranches = $derived((props.data.executionInfo as any)?.output?.active_branches || []);
34
99
 
100
+ /**
101
+ * Derived list of visible branches based on hideUnconnectedHandles setting
102
+ */
103
+ const visibleBranches = $derived(branches.filter((branch) => isBranchVisible(branch.name)));
104
+
35
105
  /**
36
106
  * Handle node click - only handle selection, no config opening
37
107
  */
@@ -107,14 +177,14 @@
107
177
  </p>
108
178
  </div>
109
179
 
110
- <!-- Input Ports Container -->
111
- {#if props.data.metadata.inputs.length > 0}
180
+ <!-- Input Ports Container (filtered based on hideUnconnectedHandles) -->
181
+ {#if visibleInputPorts.length > 0}
112
182
  <div class="flowdrop-workflow-node__ports">
113
183
  <div class="flowdrop-workflow-node__ports-header">
114
184
  <h5 class="flowdrop-workflow-node__ports-title">Inputs</h5>
115
185
  </div>
116
186
  <div class="flowdrop-workflow-node__ports-list">
117
- {#each props.data.metadata.inputs as port (port.id)}
187
+ {#each visibleInputPorts as port (port.id)}
118
188
  <div class="flowdrop-workflow-node__port">
119
189
  <!-- Input Handle -->
120
190
  <Handle
@@ -158,17 +228,17 @@
158
228
  </div>
159
229
  {/if}
160
230
 
161
- <!-- Branches Section (Output Ports) -->
162
- {#if branches.length > 0}
231
+ <!-- Branches Section (Output Ports) - filtered based on hideUnconnectedHandles -->
232
+ {#if visibleBranches.length > 0}
163
233
  <div class="flowdrop-workflow-node__ports">
164
234
  <div class="flowdrop-workflow-node__ports-header">
165
235
  <h5 class="flowdrop-workflow-node__ports-title">
166
236
  <Icon icon="mdi:source-branch" />
167
- <span>Branches ({branches.length})</span>
237
+ <span>Branches ({visibleBranches.length})</span>
168
238
  </h5>
169
239
  </div>
170
240
  <div class="flowdrop-workflow-node__ports-list">
171
- {#each branches as branch (branch.name)}
241
+ {#each visibleBranches as branch (branch.name)}
172
242
  {@const isActive = isBranchActive(branch.name)}
173
243
  <div class="flowdrop-workflow-node__port">
174
244
  <!-- Port Info -->
@@ -185,7 +255,7 @@
185
255
  class="flowdrop-text--xs flowdrop-font--medium"
186
256
  class:flowdrop-text--active={isActive}
187
257
  >
188
- {branch.name}
258
+ {branch.label || branch.name}
189
259
  </span>
190
260
  <span
191
261
  class="flowdrop-badge flowdrop-badge--sm"
@@ -213,7 +283,8 @@
213
283
  {/each}
214
284
  </div>
215
285
  </div>
216
- {:else}
286
+ {:else if branches.length === 0}
287
+ <!-- No branches configured at all -->
217
288
  <div class="flowdrop-workflow-node__ports">
218
289
  <div class="workflow-node__no-branches">
219
290
  <Icon icon="mdi:alert-circle-outline" />
@@ -221,6 +292,7 @@
221
292
  </div>
222
293
  </div>
223
294
  {/if}
295
+ <!-- Note: When all branches are hidden due to hideUnconnectedHandles, we don't show anything -->
224
296
 
225
297
  <!-- Config button -->
226
298
  <button
@@ -2,13 +2,17 @@
2
2
  Simple Node Component
3
3
  A simple node with optional input and output ports
4
4
  Styled with BEM syntax
5
+
6
+ UI Extensions Support:
7
+ - hideUnconnectedHandles: Hides trigger ports that are not connected to reduce visual clutter
5
8
  -->
6
9
 
7
10
  <script lang="ts">
8
11
  import { Position, Handle } from '@xyflow/svelte';
9
- import type { ConfigValues, NodeMetadata } from '../../types/index.js';
12
+ import type { ConfigValues, NodeMetadata, NodeExtensions } from '../../types/index.js';
10
13
  import Icon from '@iconify/svelte';
11
14
  import { getDataTypeColor } from '../../utils/colors.js';
15
+ import { connectedHandles } from '../../stores/workflowStore.js';
12
16
 
13
17
  const props = $props<{
14
18
  data: {
@@ -16,6 +20,7 @@
16
20
  config: ConfigValues;
17
21
  metadata: NodeMetadata;
18
22
  nodeId?: string;
23
+ extensions?: NodeExtensions;
19
24
  onConfigOpen?: (node: {
20
25
  id: string;
21
26
  type: string;
@@ -27,6 +32,37 @@
27
32
  isError?: boolean;
28
33
  }>();
29
34
 
35
+ /**
36
+ * Get the hideUnconnectedHandles setting from extensions
37
+ * Merges node type defaults with instance overrides
38
+ */
39
+ const hideUnconnectedHandles = $derived(() => {
40
+ const typeDefault = props.data.metadata?.extensions?.ui?.hideUnconnectedHandles ?? false;
41
+ const instanceOverride = props.data.extensions?.ui?.hideUnconnectedHandles;
42
+ return instanceOverride ?? typeDefault;
43
+ });
44
+
45
+ /**
46
+ * Check if a trigger port is connected
47
+ * @param portId - The port ID to check
48
+ * @param type - Whether this is an 'input' or 'output' port
49
+ */
50
+ function isTriggerPortConnected(portId: string, type: 'input' | 'output'): boolean {
51
+ const handleId = `${props.data.nodeId}-${type}-${portId}`;
52
+ return $connectedHandles.has(handleId);
53
+ }
54
+
55
+ /**
56
+ * Check if a trigger port should be visible
57
+ * Always shows if hideUnconnectedHandles is disabled or if port is connected
58
+ */
59
+ function shouldShowTriggerPort(portId: string, type: 'input' | 'output'): boolean {
60
+ if (!hideUnconnectedHandles()) {
61
+ return true;
62
+ }
63
+ return isTriggerPortConnected(portId, type);
64
+ }
65
+
30
66
  // Removed local config state - now using global ConfigSidebar
31
67
 
32
68
  // Prioritize metadata icon over config icon for simple nodes (metadata is the node definition)
@@ -124,8 +160,8 @@
124
160
  id={`${props.data.nodeId}-input-${firstDataInputPort.id}`}
125
161
  />
126
162
  {/if}
127
- {#if triggerInputPort}
128
- <!-- Trigger Input - positioned at bottom-left -->
163
+ {#if triggerInputPort && shouldShowTriggerPort(triggerInputPort.id, 'input')}
164
+ <!-- Trigger Input - positioned at bottom-left (hidden if hideUnconnectedHandles enabled and not connected) -->
129
165
  <Handle
130
166
  type="target"
131
167
  position={Position.Left}
@@ -217,8 +253,8 @@
217
253
  )}; border-color: '#ffffff'; top: {hasBothOutputTypes ? '25%' : '50%'}; z-index: 30;"
218
254
  />
219
255
  {/if}
220
- {#if triggerOutputPort}
221
- <!-- Trigger Output - positioned at bottom-right -->
256
+ {#if triggerOutputPort && shouldShowTriggerPort(triggerOutputPort.id, 'output')}
257
+ <!-- Trigger Output - positioned at bottom-right (hidden if hideUnconnectedHandles enabled and not connected) -->
222
258
  <Handle
223
259
  type="source"
224
260
  position={Position.Right}
@@ -1,10 +1,11 @@
1
- import type { ConfigValues, NodeMetadata } from '../../types/index.js';
1
+ import type { ConfigValues, NodeMetadata, NodeExtensions } from '../../types/index.js';
2
2
  type $$ComponentProps = {
3
3
  data: {
4
4
  label: string;
5
5
  config: ConfigValues;
6
6
  metadata: NodeMetadata;
7
7
  nodeId?: string;
8
+ extensions?: NodeExtensions;
8
9
  onConfigOpen?: (node: {
9
10
  id: string;
10
11
  type: string;
@@ -2,13 +2,17 @@
2
2
  Square Node Component
3
3
  A simple square node with optional input and output ports
4
4
  Styled with BEM syntax
5
+
6
+ UI Extensions Support:
7
+ - hideUnconnectedHandles: Hides trigger ports that are not connected to reduce visual clutter
5
8
  -->
6
9
 
7
10
  <script lang="ts">
8
11
  import { Position, Handle } from '@xyflow/svelte';
9
- import type { ConfigValues, NodeMetadata } from '../../types/index.js';
12
+ import type { ConfigValues, NodeMetadata, NodeExtensions } from '../../types/index.js';
10
13
  import Icon from '@iconify/svelte';
11
14
  import { getDataTypeColor } from '../../utils/colors.js';
15
+ import { connectedHandles } from '../../stores/workflowStore.js';
12
16
 
13
17
  const props = $props<{
14
18
  data: {
@@ -16,6 +20,7 @@
16
20
  config: ConfigValues;
17
21
  metadata: NodeMetadata;
18
22
  nodeId?: string;
23
+ extensions?: NodeExtensions;
19
24
  onConfigOpen?: (node: {
20
25
  id: string;
21
26
  type: string;
@@ -27,6 +32,37 @@
27
32
  isError?: boolean;
28
33
  }>();
29
34
 
35
+ /**
36
+ * Get the hideUnconnectedHandles setting from extensions
37
+ * Merges node type defaults with instance overrides
38
+ */
39
+ const hideUnconnectedHandles = $derived(() => {
40
+ const typeDefault = props.data.metadata?.extensions?.ui?.hideUnconnectedHandles ?? false;
41
+ const instanceOverride = props.data.extensions?.ui?.hideUnconnectedHandles;
42
+ return instanceOverride ?? typeDefault;
43
+ });
44
+
45
+ /**
46
+ * Check if a trigger port is connected
47
+ * @param portId - The port ID to check
48
+ * @param type - Whether this is an 'input' or 'output' port
49
+ */
50
+ function isTriggerPortConnected(portId: string, type: 'input' | 'output'): boolean {
51
+ const handleId = `${props.data.nodeId}-${type}-${portId}`;
52
+ return $connectedHandles.has(handleId);
53
+ }
54
+
55
+ /**
56
+ * Check if a trigger port should be visible
57
+ * Always shows if hideUnconnectedHandles is disabled or if port is connected
58
+ */
59
+ function shouldShowTriggerPort(portId: string, type: 'input' | 'output'): boolean {
60
+ if (!hideUnconnectedHandles()) {
61
+ return true;
62
+ }
63
+ return isTriggerPortConnected(portId, type);
64
+ }
65
+
30
66
  // Removed local config state - now using global ConfigSidebar
31
67
 
32
68
  // Prioritize metadata icon over config icon for square nodes (metadata is the node definition)
@@ -110,8 +146,8 @@
110
146
  id={`${props.data.nodeId}-input-${firstDataInputPort.id}`}
111
147
  />
112
148
  {/if}
113
- {#if triggerInputPort}
114
- <!-- Trigger Input - positioned at bottom-left -->
149
+ {#if triggerInputPort && shouldShowTriggerPort(triggerInputPort.id, 'input')}
150
+ <!-- Trigger Input - positioned at bottom-left (hidden if hideUnconnectedHandles enabled and not connected) -->
115
151
  <Handle
116
152
  type="target"
117
153
  position={Position.Left}
@@ -179,8 +215,8 @@
179
215
  )}; border-color: '#ffffff'; top: {hasBothOutputTypes ? '25%' : '50%'}; z-index: 30;"
180
216
  />
181
217
  {/if}
182
- {#if triggerOutputPort}
183
- <!-- Trigger Output - positioned at bottom-right -->
218
+ {#if triggerOutputPort && shouldShowTriggerPort(triggerOutputPort.id, 'output')}
219
+ <!-- Trigger Output - positioned at bottom-right (hidden if hideUnconnectedHandles enabled and not connected) -->
184
220
  <Handle
185
221
  type="source"
186
222
  position={Position.Right}
@@ -1,10 +1,11 @@
1
- import type { ConfigValues, NodeMetadata } from '../../types/index.js';
1
+ import type { ConfigValues, NodeMetadata, NodeExtensions } from '../../types/index.js';
2
2
  type $$ComponentProps = {
3
3
  data: {
4
4
  label: string;
5
5
  config: ConfigValues;
6
6
  metadata: NodeMetadata;
7
7
  nodeId?: string;
8
+ extensions?: NodeExtensions;
8
9
  onConfigOpen?: (node: {
9
10
  id: string;
10
11
  type: string;