Skip to content

Commit b7623fd

Browse files
committed
feat: configure input schema
Signed-off-by: Simon Emms <simon@simonemms.com>
1 parent 8ee1b35 commit b7623fd

12 files changed

Lines changed: 1210 additions & 83 deletions

src/lib/i18n/messages/en.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,14 @@
429429
"errorNameDuplicate": "Duplicate name"
430430
}
431431
},
432+
"valueSource": {
433+
"sourceLabel": "Source",
434+
"valueLabel": "Value",
435+
"input": "Input",
436+
"literal": "Literal",
437+
"expression": "Expression",
438+
"noInputPaths": "No input fields defined"
439+
},
432440
"validation": {
433441
"loop": {
434442
"collectionRequired": "Collection is required",

src/lib/ui/Inspector.svelte

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
SwitchNode,
3232
} from '$lib/tasks/model';
3333
34+
import ValueSourceSelector from './ValueSourceSelector.svelte';
3435
import CommonFields from './inspector/CommonFields.svelte';
3536
import { getNodeEditor } from './node-editors/registry';
3637
@@ -56,6 +57,8 @@
5657
// Workflow list for switch branch target selection
5758
workflows?: NamedWorkflow[];
5859
currentWorkflowName?: string;
60+
/** Flat dot-notation paths from the workflow input schema. */
61+
inputPaths?: string[];
5962
}
6063
6164
let {
@@ -72,6 +75,7 @@
7275
onremovebranch,
7376
workflows = [],
7477
currentWorkflowName = '',
78+
inputPaths = [],
7579
}: Props = $props();
7680
7781
// ---------------------------------------------------------------------------
@@ -263,22 +267,16 @@
263267
{/if}
264268

265269
{#if !isDefault}
266-
<label class="field-label" for="branch-cond-{branch.id}">
270+
<p class="field-label">
267271
{t('inspector.switch.condition')}
268-
</label>
269-
<input
270-
id="branch-cond-{branch.id}"
271-
class="text-input text-input--mono"
272-
type="text"
273-
aria-label={t('inspector.switch.condition')}
272+
</p>
273+
<ValueSourceSelector
274274
value={branch.condition ?? ''}
275-
oninput={(e) =>
275+
{inputPaths}
276+
ariaLabel={t('inspector.switch.condition')}
277+
onchange={(v) =>
276278
handleSwitchUpdate(
277-
updateSwitchBranchCondition(
278-
switchNode,
279-
branch.id,
280-
e.currentTarget.value,
281-
),
279+
updateSwitchBranchCondition(switchNode, branch.id, v),
282280
)}
283281
/>
284282
{#if emptyCondition}
@@ -474,7 +472,7 @@
474472

475473
<!-- Task-specific or structural property editor -->
476474
{#if NodeEditor}
477-
<NodeEditor {node} onupdate={(n) => onupdate?.(n)} />
475+
<NodeEditor {node} {inputPaths} onupdate={(n) => onupdate?.(n)} />
478476
{/if}
479477

480478
<!-- Common fields: if + metadata — present on all node types -->
@@ -702,10 +700,6 @@
702700
margin-bottom: 0.2rem;
703701
}
704702
705-
.text-input--mono {
706-
font-family: monospace;
707-
}
708-
709703
.text-input:focus {
710704
outline: none;
711705
border-color: #1a56cc;
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
<!--
2+
~ Copyright 2025 - 2026 Zigflow authors <https://github.com/zigflow/studio/graphs/contributors>
3+
~
4+
~ Licensed under the Apache License, Version 2.0 (the "License");
5+
~ you may not use this file except in compliance with the License.
6+
~ You may obtain a copy of the License at
7+
~
8+
~ http://www.apache.org/licenses/LICENSE-2.0
9+
~
10+
~ Unless required by applicable law or agreed to in writing, software
11+
~ distributed under the License is distributed on an "AS IS" BASIS,
12+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
~ See the License for the specific language governing permissions and
14+
~ limitations under the License.
15+
-->
16+
17+
<!--
18+
Two-part value selector: [ Source ▼ ] [ Value input ]
19+
20+
Source modes:
21+
input — dropdown of input schema paths → stored as ${ $input.<path> }
22+
literal — plain text input → stored verbatim
23+
expression — text input → stored verbatim (full ${ } allowed)
24+
-->
25+
26+
<script lang="ts">
27+
import { t } from '$lib/i18n/index.svelte';
28+
29+
import {
30+
type ValueSource,
31+
buildInputValue,
32+
detectSource,
33+
parseInputPath,
34+
} from './value-source';
35+
36+
interface Props {
37+
/** The stored value string (e.g. `${ $input.items }` or `"hello"`). */
38+
value: string;
39+
/** Flat dot-notation paths extracted from the input schema. */
40+
inputPaths: string[];
41+
/** Emitted whenever the stored value changes. */
42+
onchange: (value: string) => void;
43+
/** Optional aria-label for the value input / select. */
44+
ariaLabel?: string;
45+
}
46+
47+
let { value, inputPaths, onchange, ariaLabel = '' }: Props = $props();
48+
49+
// ---------------------------------------------------------------------------
50+
// Derive initial source + inner value from the incoming stored value.
51+
// Re-derived whenever `value` changes from the outside (e.g. on reload).
52+
// ---------------------------------------------------------------------------
53+
54+
// Initialize with defaults; $effect below sets real values on first run
55+
// and re-syncs whenever the parent `value` prop changes.
56+
let source: ValueSource = $state('literal');
57+
let innerValue: string = $state('');
58+
59+
// Sync local state from the external `value` prop.
60+
// Only reads `value` (the prop) — no circular dependency.
61+
$effect(() => {
62+
const newSource = detectSource(value);
63+
source = newSource;
64+
innerValue = newSource === 'input' ? parseInputPath(value) : value;
65+
});
66+
67+
// ---------------------------------------------------------------------------
68+
// Handlers
69+
// ---------------------------------------------------------------------------
70+
71+
function handleSourceChange(newSource: ValueSource): void {
72+
source = newSource;
73+
// Reset inner value and emit a sensible stored value.
74+
if (newSource === 'input') {
75+
const firstPath = inputPaths[0] ?? '';
76+
innerValue = firstPath;
77+
onchange(firstPath ? buildInputValue(firstPath) : '');
78+
} else {
79+
innerValue = '';
80+
onchange('');
81+
}
82+
}
83+
84+
function handleInputPathChange(path: string): void {
85+
innerValue = path;
86+
onchange(path ? buildInputValue(path) : '');
87+
}
88+
89+
function handleTextChange(text: string): void {
90+
innerValue = text;
91+
onchange(text);
92+
}
93+
</script>
94+
95+
<div class="vss">
96+
<select
97+
class="vss-source"
98+
aria-label={t('valueSource.sourceLabel')}
99+
value={source}
100+
onchange={(e) => handleSourceChange(e.currentTarget.value as ValueSource)}
101+
>
102+
<option value="input">{t('valueSource.input')}</option>
103+
<option value="literal">{t('valueSource.literal')}</option>
104+
<option value="expression">{t('valueSource.expression')}</option>
105+
</select>
106+
107+
{#if source === 'input'}
108+
{#if inputPaths.length === 0}
109+
<span class="vss-no-paths">{t('valueSource.noInputPaths')}</span>
110+
{:else}
111+
<select
112+
class="vss-value vss-value--select"
113+
aria-label={ariaLabel || t('valueSource.valueLabel')}
114+
value={innerValue}
115+
onchange={(e) => handleInputPathChange(e.currentTarget.value)}
116+
>
117+
{#each inputPaths as path (path)}
118+
<option value={path}>{path}</option>
119+
{/each}
120+
</select>
121+
{/if}
122+
{:else}
123+
<input
124+
class="vss-value vss-value--text"
125+
type="text"
126+
aria-label={ariaLabel || t('valueSource.valueLabel')}
127+
value={innerValue}
128+
oninput={(e) => handleTextChange(e.currentTarget.value)}
129+
/>
130+
{/if}
131+
</div>
132+
133+
<style>
134+
.vss {
135+
display: flex;
136+
gap: 0.375rem;
137+
align-items: center;
138+
width: 100%;
139+
}
140+
141+
.vss-source {
142+
flex-shrink: 0;
143+
padding: 0.2rem 0.375rem;
144+
border: 1px solid #ddd;
145+
border-radius: 4px;
146+
font-size: 0.78rem;
147+
color: #444;
148+
background: #fafafa;
149+
cursor: pointer;
150+
}
151+
152+
.vss-source:focus {
153+
outline: none;
154+
border-color: #1a56cc;
155+
box-shadow: 0 0 0 2px rgba(26, 86, 204, 0.15);
156+
}
157+
158+
.vss-value {
159+
flex: 1;
160+
min-width: 0;
161+
padding: 0.2rem 0.375rem;
162+
border: 1px solid #ddd;
163+
border-radius: 4px;
164+
font-size: 0.78rem;
165+
box-sizing: border-box;
166+
}
167+
168+
.vss-value--text {
169+
font-family: monospace;
170+
color: #111;
171+
background: #fff;
172+
}
173+
174+
.vss-value--select {
175+
color: #111;
176+
background: #fff;
177+
cursor: pointer;
178+
}
179+
180+
.vss-value:focus {
181+
outline: none;
182+
border-color: #1a56cc;
183+
box-shadow: 0 0 0 2px rgba(26, 86, 204, 0.15);
184+
}
185+
186+
.vss-no-paths {
187+
flex: 1;
188+
font-size: 0.75rem;
189+
color: #999;
190+
font-style: italic;
191+
padding: 0.2rem 0;
192+
}
193+
</style>

src/lib/ui/node-editors/LoopNodeEditor.svelte

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@
1717
<script lang="ts">
1818
import { t } from '$lib/i18n/index.svelte';
1919
import type { LoopNode, Node } from '$lib/tasks/model';
20+
import ValueSourceSelector from '$lib/ui/ValueSourceSelector.svelte';
2021
2122
interface Props {
2223
node: Node;
2324
onupdate: (node: Node) => void;
25+
inputPaths?: string[];
2426
}
2527
26-
let { node, onupdate }: Props = $props();
28+
let { node, onupdate, inputPaths = [] }: Props = $props();
2729
2830
// Safe narrowing: registry guarantees this editor only receives loop nodes.
2931
const loopNode = $derived(node as LoopNode);
@@ -54,14 +56,12 @@
5456

5557
<dl class="loop-fields">
5658
<dt class="field-label">{t('inspector.loop.collection')}</dt>
57-
<dd class="field-value">
58-
<input
59-
class="loop-input"
60-
class:loop-input--invalid={collectionEmpty}
61-
type="text"
62-
aria-label={t('inspector.loop.collection')}
59+
<dd class="field-value" class:field-value--invalid={collectionEmpty}>
60+
<ValueSourceSelector
6361
value={loopNode.in}
64-
oninput={(e) => onupdate({ ...loopNode, in: e.currentTarget.value })}
62+
{inputPaths}
63+
ariaLabel={t('inspector.loop.collection')}
64+
onchange={(v) => onupdate({ ...loopNode, in: v })}
6565
/>
6666
{#if collectionEmpty}
6767
<p class="field-warning">

0 commit comments

Comments
 (0)