@d34dman/flowdrop 0.0.47 → 0.0.49
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/dist/components/App.svelte +11 -0
- package/dist/components/App.svelte.d.ts +2 -0
- package/dist/components/ConfigForm.svelte +31 -5
- package/dist/components/ConfigForm.svelte.d.ts +3 -1
- package/dist/components/NodeSidebar.svelte +1 -0
- package/dist/components/SchemaForm.svelte +4 -2
- package/dist/components/SettingsPanel.svelte +3 -3
- package/dist/components/form/FormAutocomplete.svelte +9 -4
- package/dist/components/form/FormCodeEditor.svelte +17 -15
- package/dist/components/form/FormField.svelte +32 -4
- package/dist/components/form/FormField.svelte.d.ts +11 -0
- package/dist/components/form/FormFieldLight.svelte +0 -1
- package/dist/components/form/FormTemplateEditor.svelte +300 -79
- package/dist/components/form/FormTemplateEditor.svelte.d.ts +11 -7
- package/dist/components/form/index.d.ts +1 -1
- package/dist/components/form/index.js +1 -1
- package/dist/components/form/templateAutocomplete.d.ts +2 -11
- package/dist/components/form/templateAutocomplete.js +49 -104
- package/dist/components/form/types.d.ts +0 -6
- package/dist/components/nodes/TerminalNode.svelte +27 -15
- package/dist/components/nodes/ToolNode.svelte +4 -6
- package/dist/services/apiVariableService.d.ts +116 -0
- package/dist/services/apiVariableService.js +338 -0
- package/dist/services/globalSave.js +6 -0
- package/dist/services/variableService.d.ts +50 -9
- package/dist/services/variableService.js +139 -44
- package/dist/svelte-app.d.ts +5 -0
- package/dist/svelte-app.js +7 -1
- package/dist/types/index.d.ts +138 -1
- package/dist/types/settings.js +4 -4
- package/dist/utils/colors.js +1 -0
- package/dist/utils/handlePositioning.d.ts +31 -0
- package/dist/utils/handlePositioning.js +35 -0
- package/dist/utils/icons.js +1 -0
- package/dist/utils/nodeTypes.js +3 -3
- package/package.json +8 -4
|
@@ -71,6 +71,8 @@
|
|
|
71
71
|
variant?: 'primary' | 'secondary' | 'outline';
|
|
72
72
|
onclick?: (event: Event) => void;
|
|
73
73
|
}>;
|
|
74
|
+
/** Show settings gear icon in navbar */
|
|
75
|
+
showSettings?: boolean;
|
|
74
76
|
/** API base URL */
|
|
75
77
|
apiBaseUrl?: string;
|
|
76
78
|
/** Endpoint configuration */
|
|
@@ -96,6 +98,7 @@
|
|
|
96
98
|
pipelineId,
|
|
97
99
|
navbarTitle,
|
|
98
100
|
navbarActions = [],
|
|
101
|
+
showSettings = true,
|
|
99
102
|
apiBaseUrl,
|
|
100
103
|
endpointConfig: propEndpointConfig,
|
|
101
104
|
authProvider,
|
|
@@ -390,6 +393,13 @@
|
|
|
390
393
|
* Uses enhanced API client with authProvider support when available.
|
|
391
394
|
*/
|
|
392
395
|
async function saveWorkflow(): Promise<void> {
|
|
396
|
+
// Flush any pending form changes by blurring the active element.
|
|
397
|
+
// This ensures focusout handlers (like ConfigForm's handleFormBlur)
|
|
398
|
+
// sync local state to the global store before we read it.
|
|
399
|
+
if (document.activeElement instanceof HTMLElement) {
|
|
400
|
+
document.activeElement.blur();
|
|
401
|
+
}
|
|
402
|
+
|
|
393
403
|
// Wait for any pending DOM updates before saving
|
|
394
404
|
await tick();
|
|
395
405
|
|
|
@@ -705,6 +715,7 @@
|
|
|
705
715
|
}
|
|
706
716
|
]}
|
|
707
717
|
showStatus={true}
|
|
718
|
+
{showSettings}
|
|
708
719
|
/>
|
|
709
720
|
{/snippet}
|
|
710
721
|
|
|
@@ -36,6 +36,8 @@ interface Props {
|
|
|
36
36
|
variant?: 'primary' | 'secondary' | 'outline';
|
|
37
37
|
onclick?: (event: Event) => void;
|
|
38
38
|
}>;
|
|
39
|
+
/** Show settings gear icon in navbar */
|
|
40
|
+
showSettings?: boolean;
|
|
39
41
|
/** API base URL */
|
|
40
42
|
apiBaseUrl?: string;
|
|
41
43
|
/** Endpoint configuration */
|
|
@@ -18,13 +18,15 @@
|
|
|
18
18
|
-->
|
|
19
19
|
|
|
20
20
|
<script lang="ts">
|
|
21
|
+
import { setContext } from 'svelte';
|
|
21
22
|
import Icon from '@iconify/svelte';
|
|
22
23
|
import type {
|
|
23
24
|
ConfigSchema,
|
|
24
25
|
WorkflowNode,
|
|
25
26
|
WorkflowEdge,
|
|
26
27
|
NodeUIExtensions,
|
|
27
|
-
ConfigEditOptions
|
|
28
|
+
ConfigEditOptions,
|
|
29
|
+
AuthProvider
|
|
28
30
|
} from '../types/index.js';
|
|
29
31
|
import { FormField, FormFieldWrapper, FormToggle } from './form/index.js';
|
|
30
32
|
import type { FieldSchema } from './form/index.js';
|
|
@@ -61,6 +63,8 @@
|
|
|
61
63
|
* When provided along with workflowNodes, enables autocomplete for template fields.
|
|
62
64
|
*/
|
|
63
65
|
workflowEdges?: WorkflowEdge[];
|
|
66
|
+
/** Auth provider for API requests (used for template variable API mode) */
|
|
67
|
+
authProvider?: AuthProvider;
|
|
64
68
|
/** Callback when any field value changes (fired on blur for immediate sync) */
|
|
65
69
|
onChange?: (config: Record<string, unknown>, uiExtensions?: NodeUIExtensions) => void;
|
|
66
70
|
/** Callback when form is saved (includes both config and extensions if enabled) */
|
|
@@ -78,11 +82,17 @@
|
|
|
78
82
|
saveWorkflowWhenSavingConfig = false,
|
|
79
83
|
workflowNodes = [],
|
|
80
84
|
workflowEdges = [],
|
|
85
|
+
authProvider,
|
|
81
86
|
onChange,
|
|
82
87
|
onSave,
|
|
83
88
|
onCancel
|
|
84
89
|
}: Props = $props();
|
|
85
90
|
|
|
91
|
+
// Set context for child components (e.g., FormAutocomplete)
|
|
92
|
+
// Use getter functions to ensure child components always get the current prop value,
|
|
93
|
+
// even if the prop changes after initial mount
|
|
94
|
+
setContext<() => AuthProvider | undefined>('flowdrop:getAuthProvider', () => authProvider);
|
|
95
|
+
|
|
86
96
|
/**
|
|
87
97
|
* State for dynamic schema loading
|
|
88
98
|
*/
|
|
@@ -132,13 +142,19 @@
|
|
|
132
142
|
});
|
|
133
143
|
|
|
134
144
|
/**
|
|
135
|
-
* Check if the node
|
|
145
|
+
* Check if the node needs dynamic schema loading
|
|
146
|
+
* Loads when: no static schema OR preferDynamicSchema is true
|
|
136
147
|
*/
|
|
137
148
|
const needsDynamicSchemaLoad = $derived.by(() => {
|
|
138
149
|
if (!node) return false;
|
|
139
150
|
const staticSchema = schema ?? node.data.metadata?.configSchema;
|
|
140
|
-
// Need to load if: no static schema AND dynamic schema is configured
|
|
141
|
-
return
|
|
151
|
+
// Need to load if: (no static schema OR preferDynamicSchema is true) AND dynamic schema is configured
|
|
152
|
+
return (
|
|
153
|
+
(!staticSchema || configEditOptions?.preferDynamicSchema === true) &&
|
|
154
|
+
useDynamicSchema &&
|
|
155
|
+
!fetchedDynamicSchema &&
|
|
156
|
+
!dynamicSchemaLoading
|
|
157
|
+
);
|
|
142
158
|
});
|
|
143
159
|
|
|
144
160
|
/**
|
|
@@ -388,7 +404,12 @@
|
|
|
388
404
|
const fieldSchema = property as FieldSchema;
|
|
389
405
|
|
|
390
406
|
// Process template fields to compute variable schema
|
|
391
|
-
if (
|
|
407
|
+
if (
|
|
408
|
+
fieldSchema.format === 'template' &&
|
|
409
|
+
node &&
|
|
410
|
+
workflowNodes.length > 0 &&
|
|
411
|
+
workflowEdges.length > 0
|
|
412
|
+
) {
|
|
392
413
|
// Get the variables config (may be undefined or partially defined)
|
|
393
414
|
const variablesConfig = fieldSchema.variables;
|
|
394
415
|
|
|
@@ -537,6 +558,11 @@
|
|
|
537
558
|
value={configValues[key]}
|
|
538
559
|
{required}
|
|
539
560
|
animationIndex={index}
|
|
561
|
+
{node}
|
|
562
|
+
nodes={workflowNodes}
|
|
563
|
+
edges={workflowEdges}
|
|
564
|
+
{workflowId}
|
|
565
|
+
{authProvider}
|
|
540
566
|
onChange={(val) => handleFieldChange(key, val)}
|
|
541
567
|
/>
|
|
542
568
|
{/each}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ConfigSchema, WorkflowNode, WorkflowEdge, NodeUIExtensions } from '../types/index.js';
|
|
1
|
+
import type { ConfigSchema, WorkflowNode, WorkflowEdge, NodeUIExtensions, AuthProvider } from '../types/index.js';
|
|
2
2
|
interface Props {
|
|
3
3
|
/** Optional workflow node (if provided, schema and values are derived from it) */
|
|
4
4
|
node?: WorkflowNode;
|
|
@@ -22,6 +22,8 @@ interface Props {
|
|
|
22
22
|
* When provided along with workflowNodes, enables autocomplete for template fields.
|
|
23
23
|
*/
|
|
24
24
|
workflowEdges?: WorkflowEdge[];
|
|
25
|
+
/** Auth provider for API requests (used for template variable API mode) */
|
|
26
|
+
authProvider?: AuthProvider;
|
|
25
27
|
/** Callback when any field value changes (fired on blur for immediate sync) */
|
|
26
28
|
onChange?: (config: Record<string, unknown>, uiExtensions?: NodeUIExtensions) => void;
|
|
27
29
|
/** Callback when form is saved (includes both config and extensions if enabled) */
|
|
@@ -157,8 +157,10 @@
|
|
|
157
157
|
}: Props = $props();
|
|
158
158
|
|
|
159
159
|
// Set context for child components (e.g., FormAutocomplete)
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
// Use getter functions to ensure child components always get the current prop value,
|
|
161
|
+
// even if the prop changes after initial mount
|
|
162
|
+
setContext<() => AuthProvider | undefined>('flowdrop:getAuthProvider', () => authProvider);
|
|
163
|
+
setContext<() => string>('flowdrop:getBaseUrl', () => baseUrl);
|
|
162
164
|
|
|
163
165
|
/**
|
|
164
166
|
* Internal reactive state for form values
|
|
@@ -188,10 +188,10 @@
|
|
|
188
188
|
undoHistoryLimit: {
|
|
189
189
|
type: 'number',
|
|
190
190
|
title: 'Undo History Limit',
|
|
191
|
-
description: 'Maximum number of undo steps',
|
|
192
|
-
minimum:
|
|
191
|
+
description: 'Maximum number of undo steps (0 to disable)',
|
|
192
|
+
minimum: 0,
|
|
193
193
|
maximum: 200,
|
|
194
|
-
default:
|
|
194
|
+
default: 0
|
|
195
195
|
},
|
|
196
196
|
confirmDelete: {
|
|
197
197
|
type: 'boolean',
|
|
@@ -59,9 +59,12 @@
|
|
|
59
59
|
onChange
|
|
60
60
|
}: Props = $props();
|
|
61
61
|
|
|
62
|
-
// Get AuthProvider from context
|
|
63
|
-
|
|
64
|
-
const
|
|
62
|
+
// Get AuthProvider and baseUrl from context via getter functions
|
|
63
|
+
// This pattern ensures we always get the current value, even if props change after mount
|
|
64
|
+
const getAuthProvider = getContext<(() => AuthProvider | undefined) | undefined>(
|
|
65
|
+
'flowdrop:getAuthProvider'
|
|
66
|
+
);
|
|
67
|
+
const getBaseUrl = getContext<(() => string) | undefined>('flowdrop:getBaseUrl');
|
|
65
68
|
|
|
66
69
|
// Configuration with defaults
|
|
67
70
|
const queryParam = $derived(autocomplete.queryParam ?? 'q');
|
|
@@ -142,6 +145,7 @@
|
|
|
142
145
|
* @returns Full URL with query parameter
|
|
143
146
|
*/
|
|
144
147
|
function buildUrl(query: string): string {
|
|
148
|
+
const baseUrl = getBaseUrl?.() ?? '';
|
|
145
149
|
const url = autocomplete.url.startsWith('http')
|
|
146
150
|
? autocomplete.url
|
|
147
151
|
: `${baseUrl}${autocomplete.url}`;
|
|
@@ -194,7 +198,8 @@
|
|
|
194
198
|
'Content-Type': 'application/json'
|
|
195
199
|
};
|
|
196
200
|
|
|
197
|
-
// Add auth headers if provider is available
|
|
201
|
+
// Add auth headers if provider is available (call getter to get current value)
|
|
202
|
+
const authProvider = getAuthProvider?.();
|
|
198
203
|
if (authProvider) {
|
|
199
204
|
const authHeaders = await authProvider.getAuthHeaders();
|
|
200
205
|
Object.assign(headers, authHeaders);
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
import { EditorState } from '@codemirror/state';
|
|
26
26
|
import { history, historyKeymap } from '@codemirror/commands';
|
|
27
27
|
import { highlightSpecialChars, highlightActiveLine } from '@codemirror/view';
|
|
28
|
-
import { syntaxHighlighting, defaultHighlightStyle
|
|
28
|
+
import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
|
|
29
29
|
import { keymap } from '@codemirror/view';
|
|
30
30
|
import { defaultKeymap, indentWithTab } from '@codemirror/commands';
|
|
31
31
|
import { json, jsonParseLinter } from '@codemirror/lang-json';
|
|
32
32
|
import { oneDark } from '@codemirror/theme-one-dark';
|
|
33
|
-
import { linter
|
|
33
|
+
import { linter } from '@codemirror/lint';
|
|
34
34
|
|
|
35
35
|
interface Props {
|
|
36
36
|
/** Field identifier */
|
|
@@ -80,6 +80,9 @@
|
|
|
80
80
|
/** Flag to prevent update loops */
|
|
81
81
|
let isInternalUpdate = false;
|
|
82
82
|
|
|
83
|
+
/** Flag to skip $effect when change originated from the editor */
|
|
84
|
+
let isEditorUpdate = false;
|
|
85
|
+
|
|
83
86
|
/**
|
|
84
87
|
* Convert value to JSON string for editor display
|
|
85
88
|
*/
|
|
@@ -129,6 +132,7 @@
|
|
|
129
132
|
const content = update.state.doc.toString();
|
|
130
133
|
const result = validateAndParse(content);
|
|
131
134
|
|
|
135
|
+
isEditorUpdate = true;
|
|
132
136
|
if (result.valid) {
|
|
133
137
|
validationError = undefined;
|
|
134
138
|
// Emit the parsed value (object) not the string
|
|
@@ -184,22 +188,17 @@
|
|
|
184
188
|
// Editing features (skip when read-only)
|
|
185
189
|
...(disabled
|
|
186
190
|
? []
|
|
187
|
-
: [
|
|
188
|
-
history(),
|
|
189
|
-
indentOnInput(),
|
|
190
|
-
keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab])
|
|
191
|
-
]),
|
|
191
|
+
: [history(), keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab])]),
|
|
192
192
|
|
|
193
193
|
// Read-only: prevent document changes and mark content as non-editable
|
|
194
194
|
...(disabled ? [EditorState.readOnly.of(true), EditorView.editable.of(false)] : []),
|
|
195
195
|
|
|
196
|
-
// Syntax highlighting
|
|
197
|
-
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
|
196
|
+
// Syntax highlighting - use default for light mode, oneDark handles dark mode
|
|
197
|
+
...(darkTheme ? [oneDark] : [syntaxHighlighting(defaultHighlightStyle, { fallback: true })]),
|
|
198
198
|
|
|
199
199
|
// JSON-specific features
|
|
200
200
|
json(),
|
|
201
|
-
linter(jsonParseLinter()),
|
|
202
|
-
lintGutter(),
|
|
201
|
+
linter(jsonParseLinter(), { delay: 1000 }),
|
|
203
202
|
|
|
204
203
|
// Update listener (only fires on user edit when not disabled)
|
|
205
204
|
EditorView.updateListener.of(handleUpdate),
|
|
@@ -224,10 +223,6 @@
|
|
|
224
223
|
EditorView.lineWrapping
|
|
225
224
|
];
|
|
226
225
|
|
|
227
|
-
if (darkTheme) {
|
|
228
|
-
extensions.push(oneDark);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
226
|
return extensions;
|
|
232
227
|
}
|
|
233
228
|
|
|
@@ -276,6 +271,13 @@
|
|
|
276
271
|
}
|
|
277
272
|
|
|
278
273
|
const newContent = valueToString(value);
|
|
274
|
+
|
|
275
|
+
// Skip if the change originated from the editor itself
|
|
276
|
+
if (isEditorUpdate) {
|
|
277
|
+
isEditorUpdate = false;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
279
281
|
const currentContent = editorView.state.doc.toString();
|
|
280
282
|
|
|
281
283
|
// Only update if content actually changed and wasn't from internal edit
|
|
@@ -43,6 +43,8 @@
|
|
|
43
43
|
import FormAutocomplete from './FormAutocomplete.svelte';
|
|
44
44
|
import type { FieldSchema } from './types.js';
|
|
45
45
|
import { getSchemaOptions } from './types.js';
|
|
46
|
+
import type { WorkflowNode, WorkflowEdge, AuthProvider } from '../../types/index.js';
|
|
47
|
+
import { resolvedTheme } from '../../stores/settingsStore.js';
|
|
46
48
|
|
|
47
49
|
interface Props {
|
|
48
50
|
/** Unique key/id for the field */
|
|
@@ -57,9 +59,31 @@
|
|
|
57
59
|
animationIndex?: number;
|
|
58
60
|
/** Callback when the field value changes */
|
|
59
61
|
onChange: (value: unknown) => void;
|
|
62
|
+
/** Current workflow node (optional, used for template variable API mode) */
|
|
63
|
+
node?: WorkflowNode;
|
|
64
|
+
/** All workflow nodes (optional, used for port-derived variables) */
|
|
65
|
+
nodes?: WorkflowNode[];
|
|
66
|
+
/** All workflow edges (optional, used for port-derived variables) */
|
|
67
|
+
edges?: WorkflowEdge[];
|
|
68
|
+
/** Workflow ID (optional, used for template variable API mode) */
|
|
69
|
+
workflowId?: string;
|
|
70
|
+
/** Auth provider (optional, used for API requests) */
|
|
71
|
+
authProvider?: AuthProvider;
|
|
60
72
|
}
|
|
61
73
|
|
|
62
|
-
let {
|
|
74
|
+
let {
|
|
75
|
+
fieldKey,
|
|
76
|
+
schema,
|
|
77
|
+
value,
|
|
78
|
+
required = false,
|
|
79
|
+
animationIndex = 0,
|
|
80
|
+
onChange,
|
|
81
|
+
node,
|
|
82
|
+
nodes,
|
|
83
|
+
edges,
|
|
84
|
+
workflowId,
|
|
85
|
+
authProvider
|
|
86
|
+
}: Props = $props();
|
|
63
87
|
|
|
64
88
|
/**
|
|
65
89
|
* When schema.readOnly is true, disable all inputs (no editing).
|
|
@@ -322,7 +346,7 @@
|
|
|
322
346
|
placeholder={schema.placeholder ?? '{}'}
|
|
323
347
|
{required}
|
|
324
348
|
height={(schema.height as string | undefined) ?? '200px'}
|
|
325
|
-
darkTheme={(schema.darkTheme as boolean | undefined) ??
|
|
349
|
+
darkTheme={(schema.darkTheme as boolean | undefined) ?? $resolvedTheme === 'dark'}
|
|
326
350
|
autoFormat={(schema.autoFormat as boolean | undefined) ?? true}
|
|
327
351
|
ariaDescribedBy={descriptionId}
|
|
328
352
|
disabled={isReadOnly}
|
|
@@ -350,13 +374,17 @@
|
|
|
350
374
|
'Enter your template here...\nUse {{ variable }} for dynamic values.'}
|
|
351
375
|
{required}
|
|
352
376
|
height={(schema.height as string | undefined) ?? '250px'}
|
|
353
|
-
darkTheme={(schema.darkTheme as boolean | undefined) ??
|
|
377
|
+
darkTheme={(schema.darkTheme as boolean | undefined) ?? $resolvedTheme === 'dark'}
|
|
354
378
|
variables={schema.variables}
|
|
355
|
-
variableHints={(schema.variableHints as string[] | undefined) ?? []}
|
|
356
379
|
placeholderExample={(schema.placeholderExample as string | undefined) ??
|
|
357
380
|
'Hello {{ name }}, your order #{{ order_id }} is ready!'}
|
|
358
381
|
ariaDescribedBy={descriptionId}
|
|
359
382
|
disabled={isReadOnly}
|
|
383
|
+
{node}
|
|
384
|
+
{nodes}
|
|
385
|
+
{edges}
|
|
386
|
+
{workflowId}
|
|
387
|
+
{authProvider}
|
|
360
388
|
onChange={(val) => onChange(val)}
|
|
361
389
|
/>
|
|
362
390
|
{:else if fieldType === 'autocomplete' && schema.autocomplete}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FieldSchema } from './types.js';
|
|
2
|
+
import type { WorkflowNode, WorkflowEdge, AuthProvider } from '../../types/index.js';
|
|
2
3
|
interface Props {
|
|
3
4
|
/** Unique key/id for the field */
|
|
4
5
|
fieldKey: string;
|
|
@@ -12,6 +13,16 @@ interface Props {
|
|
|
12
13
|
animationIndex?: number;
|
|
13
14
|
/** Callback when the field value changes */
|
|
14
15
|
onChange: (value: unknown) => void;
|
|
16
|
+
/** Current workflow node (optional, used for template variable API mode) */
|
|
17
|
+
node?: WorkflowNode;
|
|
18
|
+
/** All workflow nodes (optional, used for port-derived variables) */
|
|
19
|
+
nodes?: WorkflowNode[];
|
|
20
|
+
/** All workflow edges (optional, used for port-derived variables) */
|
|
21
|
+
edges?: WorkflowEdge[];
|
|
22
|
+
/** Workflow ID (optional, used for template variable API mode) */
|
|
23
|
+
workflowId?: string;
|
|
24
|
+
/** Auth provider (optional, used for API requests) */
|
|
25
|
+
authProvider?: AuthProvider;
|
|
15
26
|
}
|
|
16
27
|
declare const FormField: import("svelte").Component<Props, {}, "">;
|
|
17
28
|
type FormField = ReturnType<typeof FormField>;
|
|
@@ -242,7 +242,6 @@
|
|
|
242
242
|
showStatusBar={schema.showStatusBar as boolean | undefined}
|
|
243
243
|
spellChecker={schema.spellChecker as boolean | undefined}
|
|
244
244
|
variables={schema.variables}
|
|
245
|
-
variableHints={schema.variableHints as string[] | undefined}
|
|
246
245
|
placeholderExample={schema.placeholderExample as string | undefined}
|
|
247
246
|
onChange={(val: unknown) => onChange(val)}
|
|
248
247
|
/>
|