@tai42/studio-sdk 13.3.0 → 14.0.0
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/icons.d.ts +2 -0
- package/dist/components/icons.d.ts.map +1 -1
- package/dist/components/icons.js +2 -0
- package/dist/components/icons.js.map +1 -1
- package/dist/hooks/useApi.d.ts +199 -24
- package/dist/hooks/useApi.d.ts.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/navigation/types.d.ts +2 -1
- package/dist/navigation/types.d.ts.map +1 -1
- package/dist/query-keys.d.ts +13 -0
- package/dist/query-keys.d.ts.map +1 -1
- package/dist/query-keys.js +13 -0
- package/dist/query-keys.js.map +1 -1
- package/dist/state-binding/AdapterMapping.d.ts +33 -0
- package/dist/state-binding/AdapterMapping.d.ts.map +1 -0
- package/dist/state-binding/AdapterMapping.js +205 -0
- package/dist/state-binding/AdapterMapping.js.map +1 -0
- package/dist/state-binding/BindingJqField.d.ts +36 -0
- package/dist/state-binding/BindingJqField.d.ts.map +1 -0
- package/dist/state-binding/BindingJqField.js +54 -0
- package/dist/state-binding/BindingJqField.js.map +1 -0
- package/dist/state-binding/InputInjectionRow.d.ts +17 -0
- package/dist/state-binding/InputInjectionRow.d.ts.map +1 -0
- package/dist/state-binding/InputInjectionRow.js +55 -0
- package/dist/state-binding/InputInjectionRow.js.map +1 -0
- package/dist/state-binding/StateAttachRow.d.ts +25 -0
- package/dist/state-binding/StateAttachRow.d.ts.map +1 -0
- package/dist/state-binding/StateAttachRow.js +101 -0
- package/dist/state-binding/StateAttachRow.js.map +1 -0
- package/dist/state-binding/StateBindingEditor.d.ts +35 -0
- package/dist/state-binding/StateBindingEditor.d.ts.map +1 -0
- package/dist/state-binding/StateBindingEditor.js +52 -0
- package/dist/state-binding/StateBindingEditor.js.map +1 -0
- package/dist/state-binding/StateBindingSection.d.ts +14 -0
- package/dist/state-binding/StateBindingSection.d.ts.map +1 -0
- package/dist/state-binding/StateBindingSection.js +17 -0
- package/dist/state-binding/StateBindingSection.js.map +1 -0
- package/dist/state-binding/SubjectScopeFields.d.ts +17 -0
- package/dist/state-binding/SubjectScopeFields.d.ts.map +1 -0
- package/dist/state-binding/SubjectScopeFields.js +8 -0
- package/dist/state-binding/SubjectScopeFields.js.map +1 -0
- package/dist/state-binding/UpdateRow.d.ts +19 -0
- package/dist/state-binding/UpdateRow.d.ts.map +1 -0
- package/dist/state-binding/UpdateRow.js +76 -0
- package/dist/state-binding/UpdateRow.js.map +1 -0
- package/dist/state-binding/adapter.d.ts +93 -0
- package/dist/state-binding/adapter.d.ts.map +1 -0
- package/dist/state-binding/adapter.js +251 -0
- package/dist/state-binding/adapter.js.map +1 -0
- package/dist/state-binding/build-catalog.d.ts +19 -0
- package/dist/state-binding/build-catalog.d.ts.map +1 -0
- package/dist/state-binding/build-catalog.js +43 -0
- package/dist/state-binding/build-catalog.js.map +1 -0
- package/dist/state-binding/catalog.d.ts +26 -0
- package/dist/state-binding/catalog.d.ts.map +1 -0
- package/dist/state-binding/catalog.js +71 -0
- package/dist/state-binding/catalog.js.map +1 -0
- package/dist/state-binding/index.d.ts +29 -0
- package/dist/state-binding/index.d.ts.map +1 -0
- package/dist/state-binding/index.js +18 -0
- package/dist/state-binding/index.js.map +1 -0
- package/dist/state-binding/types.d.ts +57 -0
- package/dist/state-binding/types.d.ts.map +1 -0
- package/dist/state-binding/types.js +2 -0
- package/dist/state-binding/types.js.map +1 -0
- package/package.json +5 -5
- package/src/components/icons.tsx +8 -0
- package/src/index.ts +63 -1
- package/src/navigation/types.ts +4 -1
- package/src/query-keys.ts +15 -0
- package/src/state-binding/AdapterMapping.tsx +468 -0
- package/src/state-binding/BindingJqField.tsx +134 -0
- package/src/state-binding/InputInjectionRow.tsx +148 -0
- package/src/state-binding/README.md +46 -0
- package/src/state-binding/StateAttachRow.tsx +281 -0
- package/src/state-binding/StateBindingEditor.tsx +158 -0
- package/src/state-binding/StateBindingSection.tsx +37 -0
- package/src/state-binding/SubjectScopeFields.tsx +49 -0
- package/src/state-binding/UpdateRow.tsx +194 -0
- package/src/state-binding/adapter.ts +276 -0
- package/src/state-binding/build-catalog.ts +59 -0
- package/src/state-binding/catalog.ts +87 -0
- package/src/state-binding/index.ts +61 -0
- package/src/state-binding/types.ts +62 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The optional state-binding editor — one shape on every door (a preset, a channel
|
|
3
|
+
* route, a schedule, a hook) and, in the flow engine, per node. It attaches one or
|
|
4
|
+
* more states, each with the templates it attaches on save, a subject/scope, input
|
|
5
|
+
* injections evaluated before the run, and updates applied after it.
|
|
6
|
+
*
|
|
7
|
+
* It lives in the Studio SDK so every plugin door reuses it; it holds NO edge to jq
|
|
8
|
+
* (the jq fields render through the host's ambient expression door, else a plain
|
|
9
|
+
* textarea) and paints from SDK tokens only.
|
|
10
|
+
*/
|
|
11
|
+
import type { ReactNode } from 'react';
|
|
12
|
+
|
|
13
|
+
import { Button, EmptyState, ErrorState, Skeleton } from '../components/primitives';
|
|
14
|
+
import { StateAttachRow } from './StateAttachRow';
|
|
15
|
+
import type {
|
|
16
|
+
BindingSourceSchemas,
|
|
17
|
+
BindingStateOption,
|
|
18
|
+
BindingTemplateOption,
|
|
19
|
+
StateAttach,
|
|
20
|
+
StateBinding,
|
|
21
|
+
} from './types';
|
|
22
|
+
|
|
23
|
+
export interface StateBindingEditorProps {
|
|
24
|
+
/** The binding, or `null` when the door binds no state. */
|
|
25
|
+
readonly value: StateBinding | null;
|
|
26
|
+
readonly onChange: (value: StateBinding | null) => void;
|
|
27
|
+
/** The states this door may bind, each with its attached templates and fields. */
|
|
28
|
+
readonly statesCatalog: readonly BindingStateOption[];
|
|
29
|
+
/** The templates the editor may attach (attach-on-use), each with its template jq. */
|
|
30
|
+
readonly templatesCatalog: readonly BindingTemplateOption[];
|
|
31
|
+
/** The run's output/input field paths a picker offers when a schema is known. */
|
|
32
|
+
readonly sources?: BindingSourceSchemas;
|
|
33
|
+
/**
|
|
34
|
+
* The TARGET's own binding (e.g. the preset a route dispatches), passed by the door
|
|
35
|
+
* screen — never fetched here. A state this binding names that the inherited one
|
|
36
|
+
* also names shows a precedence warning on its card.
|
|
37
|
+
*/
|
|
38
|
+
readonly inherited?: StateBinding | null;
|
|
39
|
+
/** The catalog fetch is in flight. */
|
|
40
|
+
readonly loading?: boolean;
|
|
41
|
+
/** The catalog fetch failed. */
|
|
42
|
+
readonly error?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function blankAttach(): StateAttach {
|
|
46
|
+
return {
|
|
47
|
+
state: '',
|
|
48
|
+
templates: [],
|
|
49
|
+
subject_expr: '',
|
|
50
|
+
scope_expr: null,
|
|
51
|
+
input_injections: [],
|
|
52
|
+
updates: [],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function StateBindingEditor({
|
|
57
|
+
value,
|
|
58
|
+
onChange,
|
|
59
|
+
statesCatalog,
|
|
60
|
+
templatesCatalog,
|
|
61
|
+
sources,
|
|
62
|
+
inherited,
|
|
63
|
+
loading = false,
|
|
64
|
+
error,
|
|
65
|
+
}: StateBindingEditorProps): ReactNode {
|
|
66
|
+
if (loading) {
|
|
67
|
+
return (
|
|
68
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--tai-space-2)' }}>
|
|
69
|
+
<Skeleton height={32} />
|
|
70
|
+
<Skeleton height={32} />
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (error !== undefined) {
|
|
75
|
+
return <ErrorState message="Couldn't load states and templates." />;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const states = value?.states ?? [];
|
|
79
|
+
|
|
80
|
+
const setStates = (next: readonly StateAttach[]): void => {
|
|
81
|
+
onChange(next.length === 0 ? null : { states: [...next] });
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--tai-space-3)' }}>
|
|
86
|
+
{states.length === 0 ? (
|
|
87
|
+
<EmptyState
|
|
88
|
+
title="No state bound"
|
|
89
|
+
description="Attach a state to inject a value into the run and update the record after it."
|
|
90
|
+
action={
|
|
91
|
+
<Button
|
|
92
|
+
type="button"
|
|
93
|
+
variant="primary"
|
|
94
|
+
onClick={() => {
|
|
95
|
+
setStates([blankAttach()]);
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
Attach a state
|
|
99
|
+
</Button>
|
|
100
|
+
}
|
|
101
|
+
/>
|
|
102
|
+
) : (
|
|
103
|
+
<>
|
|
104
|
+
{states.map((attach, index) => {
|
|
105
|
+
const inheritedAttach =
|
|
106
|
+
attach.state === ''
|
|
107
|
+
? undefined
|
|
108
|
+
: inherited?.states.find((entry) => entry.state === attach.state);
|
|
109
|
+
// The subject is a required jq (contract: min_length=1); an empty one is
|
|
110
|
+
// caught inline once a state is chosen, never sent for the server to 422.
|
|
111
|
+
const subjectError =
|
|
112
|
+
attach.state !== '' && attach.subject_expr.trim() === ''
|
|
113
|
+
? 'Subject is required.'
|
|
114
|
+
: undefined;
|
|
115
|
+
return (
|
|
116
|
+
<StateAttachRow
|
|
117
|
+
// Positional identity within a save; a state name may be blank while picking.
|
|
118
|
+
key={index}
|
|
119
|
+
attach={attach}
|
|
120
|
+
statesCatalog={statesCatalog}
|
|
121
|
+
templatesCatalog={templatesCatalog}
|
|
122
|
+
sources={sources}
|
|
123
|
+
subjectError={subjectError}
|
|
124
|
+
inherited={
|
|
125
|
+
inheritedAttach === undefined
|
|
126
|
+
? undefined
|
|
127
|
+
: {
|
|
128
|
+
subject_expr: inheritedAttach.subject_expr,
|
|
129
|
+
scope_expr: inheritedAttach.scope_expr,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
onChange={(next) => {
|
|
133
|
+
setStates(
|
|
134
|
+
states.map((current, position) => (position === index ? next : current)),
|
|
135
|
+
);
|
|
136
|
+
}}
|
|
137
|
+
onRemove={() => {
|
|
138
|
+
setStates(states.filter((_, position) => position !== index));
|
|
139
|
+
}}
|
|
140
|
+
/>
|
|
141
|
+
);
|
|
142
|
+
})}
|
|
143
|
+
<div>
|
|
144
|
+
<Button
|
|
145
|
+
type="button"
|
|
146
|
+
variant="ghost"
|
|
147
|
+
onClick={() => {
|
|
148
|
+
setStates([...states, blankAttach()]);
|
|
149
|
+
}}
|
|
150
|
+
>
|
|
151
|
+
Attach a state
|
|
152
|
+
</Button>
|
|
153
|
+
</div>
|
|
154
|
+
</>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The collapsed "Bind state (optional)" section every platform door screen adds
|
|
3
|
+
* below its own fields. It is chrome only — the door fetches the catalogs (via its
|
|
4
|
+
* own api-client + query stack) and passes them in; this wraps `StateBindingEditor`
|
|
5
|
+
* in a keyboard-native disclosure that opens when a binding already exists.
|
|
6
|
+
*/
|
|
7
|
+
import { useState, type ReactNode } from 'react';
|
|
8
|
+
|
|
9
|
+
import { Button } from '../components/primitives';
|
|
10
|
+
import { StateBindingEditor, type StateBindingEditorProps } from './StateBindingEditor';
|
|
11
|
+
|
|
12
|
+
export interface StateBindingSectionProps extends StateBindingEditorProps {
|
|
13
|
+
/** Copy on the disclosure toggle. */
|
|
14
|
+
readonly label?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function StateBindingSection({
|
|
18
|
+
label = 'Bind state (optional)',
|
|
19
|
+
...editor
|
|
20
|
+
}: StateBindingSectionProps): ReactNode {
|
|
21
|
+
const [open, setOpen] = useState((editor.value?.states.length ?? 0) > 0);
|
|
22
|
+
return (
|
|
23
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--tai-space-2)' }}>
|
|
24
|
+
<Button
|
|
25
|
+
type="button"
|
|
26
|
+
variant="ghost"
|
|
27
|
+
aria-expanded={open}
|
|
28
|
+
onClick={() => {
|
|
29
|
+
setOpen((current) => !current);
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
{label}
|
|
33
|
+
</Button>
|
|
34
|
+
{open ? <StateBindingEditor {...editor} /> : null}
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The subject + scope jq of one attached state: the SUBJECT expression (required)
|
|
3
|
+
* yields either a bare record-key string or a full subject object; the optional SCOPE
|
|
4
|
+
* expression is a boolean jq predicate — when it is false the state is skipped for the run.
|
|
5
|
+
*/
|
|
6
|
+
import type { ReactNode } from 'react';
|
|
7
|
+
|
|
8
|
+
import { BindingJqField, type TemplateJqSuggestion } from './BindingJqField';
|
|
9
|
+
|
|
10
|
+
export interface SubjectScopeFieldsProps {
|
|
11
|
+
readonly subjectExpr: string;
|
|
12
|
+
readonly scopeExpr: string | null;
|
|
13
|
+
readonly onSubjectChange: (value: string) => void;
|
|
14
|
+
readonly onScopeChange: (value: string | null) => void;
|
|
15
|
+
readonly subjectError?: string;
|
|
16
|
+
readonly suggestions?: readonly TemplateJqSuggestion[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function SubjectScopeFields({
|
|
20
|
+
subjectExpr,
|
|
21
|
+
scopeExpr,
|
|
22
|
+
onSubjectChange,
|
|
23
|
+
onScopeChange,
|
|
24
|
+
subjectError,
|
|
25
|
+
suggestions,
|
|
26
|
+
}: SubjectScopeFieldsProps): ReactNode {
|
|
27
|
+
return (
|
|
28
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--tai-space-2)' }}>
|
|
29
|
+
<BindingJqField
|
|
30
|
+
label="Subject"
|
|
31
|
+
description="The record key this binding reads and updates: a key string, or a full subject object."
|
|
32
|
+
value={subjectExpr}
|
|
33
|
+
onChange={onSubjectChange}
|
|
34
|
+
error={subjectError}
|
|
35
|
+
suggestions={suggestions}
|
|
36
|
+
placeholder=".subject_id"
|
|
37
|
+
/>
|
|
38
|
+
<BindingJqField
|
|
39
|
+
label="Scope"
|
|
40
|
+
description="Optional. A jq condition; when it is false this state is skipped for the run."
|
|
41
|
+
value={scopeExpr ?? ''}
|
|
42
|
+
onChange={(next) => {
|
|
43
|
+
onScopeChange(next.trim() === '' ? null : next);
|
|
44
|
+
}}
|
|
45
|
+
suggestions={suggestions}
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The update list of one attached state. Each update applies AFTER the run: either
|
|
3
|
+
* a template `update` jq (picked by name, whose declared input is authored through
|
|
4
|
+
* the adapter mapping) or a custom jq over `{ record, output, input }` that authors
|
|
5
|
+
* the whole op batch itself. An optional op-id expression carries idempotency.
|
|
6
|
+
*/
|
|
7
|
+
import type { ReactNode } from 'react';
|
|
8
|
+
|
|
9
|
+
import { Badge } from '../components/badge';
|
|
10
|
+
import { Button } from '../components/primitives';
|
|
11
|
+
import { Field } from '../components/field';
|
|
12
|
+
import { Select } from '../components/select';
|
|
13
|
+
import { CloseIcon } from '../components/icons';
|
|
14
|
+
import { AdapterMapping } from './AdapterMapping';
|
|
15
|
+
import { BindingJqField, type TemplateJqSuggestion } from './BindingJqField';
|
|
16
|
+
import { findByRef, type ResolvedTemplateJq } from './catalog';
|
|
17
|
+
import type { BindingSourceSchemas, StateUpdate } from './types';
|
|
18
|
+
|
|
19
|
+
const CUSTOM = '__custom__';
|
|
20
|
+
|
|
21
|
+
function optionLabel(entry: ResolvedTemplateJq): string {
|
|
22
|
+
return entry.description !== undefined && entry.description !== ''
|
|
23
|
+
? `${entry.ref} — ${entry.description}`
|
|
24
|
+
: entry.ref;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function writesLabel(entry: ResolvedTemplateJq | undefined): string | null {
|
|
28
|
+
if (entry?.writes === undefined || entry.writes.length === 0) return null;
|
|
29
|
+
return entry.writes.map((path) => path.join('.')).join(', ');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function UpdateRowEditor({
|
|
33
|
+
update,
|
|
34
|
+
index,
|
|
35
|
+
updateJq,
|
|
36
|
+
sources,
|
|
37
|
+
suggestions,
|
|
38
|
+
onChange,
|
|
39
|
+
onRemove,
|
|
40
|
+
}: {
|
|
41
|
+
readonly update: StateUpdate;
|
|
42
|
+
readonly index: number;
|
|
43
|
+
readonly updateJq: readonly ResolvedTemplateJq[];
|
|
44
|
+
readonly sources?: BindingSourceSchemas;
|
|
45
|
+
readonly suggestions?: readonly TemplateJqSuggestion[];
|
|
46
|
+
readonly onChange: (update: StateUpdate) => void;
|
|
47
|
+
readonly onRemove: () => void;
|
|
48
|
+
}): ReactNode {
|
|
49
|
+
const isCustom = update.jq !== null;
|
|
50
|
+
const selectValue = isCustom ? CUSTOM : (update.template_jq ?? '');
|
|
51
|
+
const resolved =
|
|
52
|
+
update.template_jq !== null ? findByRef(update.template_jq, updateJq) : undefined;
|
|
53
|
+
const writes = writesLabel(resolved);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div
|
|
57
|
+
data-testid={`update-row-${String(index)}`}
|
|
58
|
+
style={{
|
|
59
|
+
display: 'flex',
|
|
60
|
+
flexDirection: 'column',
|
|
61
|
+
gap: 'var(--tai-space-2)',
|
|
62
|
+
padding: 'var(--tai-space-2)',
|
|
63
|
+
border: '1px solid var(--tai-color-border)',
|
|
64
|
+
borderRadius: 'var(--tai-radius-md)',
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
<div
|
|
68
|
+
style={{
|
|
69
|
+
display: 'flex',
|
|
70
|
+
gap: 'var(--tai-space-2)',
|
|
71
|
+
alignItems: 'center',
|
|
72
|
+
flexWrap: 'wrap',
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
<Field label={`Update source ${String(index + 1)}`} hideLabel>
|
|
76
|
+
<Select
|
|
77
|
+
placeholder="Choose a template jq"
|
|
78
|
+
value={selectValue}
|
|
79
|
+
onValueChange={(next) => {
|
|
80
|
+
if (next === CUSTOM)
|
|
81
|
+
onChange({ template_jq: null, jq: '', adapter: null, op_id: update.op_id });
|
|
82
|
+
else onChange({ template_jq: next, jq: null, adapter: null, op_id: update.op_id });
|
|
83
|
+
}}
|
|
84
|
+
groups={[
|
|
85
|
+
{
|
|
86
|
+
label: 'Template jq',
|
|
87
|
+
options: updateJq.map((entry) => ({ value: entry.ref, label: optionLabel(entry) })),
|
|
88
|
+
},
|
|
89
|
+
{ label: 'Custom', options: [{ value: CUSTOM, label: 'Custom jq' }] },
|
|
90
|
+
]}
|
|
91
|
+
/>
|
|
92
|
+
</Field>
|
|
93
|
+
{writes !== null ? <Badge variant="neutral">{`writes: ${writes}`}</Badge> : null}
|
|
94
|
+
<Button
|
|
95
|
+
type="button"
|
|
96
|
+
variant="ghost"
|
|
97
|
+
aria-label={`Remove update ${String(index + 1)}`}
|
|
98
|
+
onClick={onRemove}
|
|
99
|
+
>
|
|
100
|
+
<CloseIcon aria-hidden="true" />
|
|
101
|
+
</Button>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{isCustom ? (
|
|
105
|
+
<BindingJqField
|
|
106
|
+
label={`Custom update jq ${String(index + 1)}`}
|
|
107
|
+
description="A jq over `{ record, output, input }` returning a template-relative op batch."
|
|
108
|
+
value={update.jq ?? ''}
|
|
109
|
+
onChange={(next) => {
|
|
110
|
+
onChange({ template_jq: null, jq: next, adapter: null, op_id: update.op_id });
|
|
111
|
+
}}
|
|
112
|
+
suggestions={suggestions}
|
|
113
|
+
/>
|
|
114
|
+
) : (
|
|
115
|
+
<Field label="Adapter" group>
|
|
116
|
+
<AdapterMapping
|
|
117
|
+
// Remount on a template change so the mapping rebuilds from the newly
|
|
118
|
+
// declared input (and re-seeds its shown default) instead of stale rows.
|
|
119
|
+
key={resolved?.ref ?? '__none__'}
|
|
120
|
+
declaredInput={resolved?.params ?? []}
|
|
121
|
+
value={update.adapter ?? ''}
|
|
122
|
+
sources={sources}
|
|
123
|
+
suggestions={suggestions}
|
|
124
|
+
onChange={(adapterJq) => {
|
|
125
|
+
onChange({ ...update, adapter: adapterJq });
|
|
126
|
+
}}
|
|
127
|
+
/>
|
|
128
|
+
</Field>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
<BindingJqField
|
|
132
|
+
label="Op id"
|
|
133
|
+
description="Optional — an idempotency-key expression."
|
|
134
|
+
value={update.op_id ?? ''}
|
|
135
|
+
onChange={(next) => {
|
|
136
|
+
onChange({ ...update, op_id: next.trim() === '' ? null : next });
|
|
137
|
+
}}
|
|
138
|
+
suggestions={suggestions}
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface UpdateListProps {
|
|
145
|
+
readonly updates: readonly StateUpdate[];
|
|
146
|
+
readonly onChange: (updates: readonly StateUpdate[]) => void;
|
|
147
|
+
readonly updateJq: readonly ResolvedTemplateJq[];
|
|
148
|
+
readonly sources?: BindingSourceSchemas;
|
|
149
|
+
readonly suggestions?: readonly TemplateJqSuggestion[];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function UpdateList({
|
|
153
|
+
updates,
|
|
154
|
+
onChange,
|
|
155
|
+
updateJq,
|
|
156
|
+
sources,
|
|
157
|
+
suggestions,
|
|
158
|
+
}: UpdateListProps): ReactNode {
|
|
159
|
+
const add = (): void => {
|
|
160
|
+
const first = updateJq[0];
|
|
161
|
+
const next: StateUpdate =
|
|
162
|
+
first !== undefined
|
|
163
|
+
? { template_jq: first.ref, jq: null, adapter: null, op_id: null }
|
|
164
|
+
: { template_jq: null, jq: '', adapter: null, op_id: null };
|
|
165
|
+
onChange([...updates, next]);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--tai-space-2)' }}>
|
|
170
|
+
{updates.map((update, index) => (
|
|
171
|
+
<UpdateRowEditor
|
|
172
|
+
// Rows are positional; the index is their stable identity within a save.
|
|
173
|
+
key={index}
|
|
174
|
+
update={update}
|
|
175
|
+
index={index}
|
|
176
|
+
updateJq={updateJq}
|
|
177
|
+
sources={sources}
|
|
178
|
+
suggestions={suggestions}
|
|
179
|
+
onChange={(next) => {
|
|
180
|
+
onChange(updates.map((current, position) => (position === index ? next : current)));
|
|
181
|
+
}}
|
|
182
|
+
onRemove={() => {
|
|
183
|
+
onChange(updates.filter((_, position) => position !== index));
|
|
184
|
+
}}
|
|
185
|
+
/>
|
|
186
|
+
))}
|
|
187
|
+
<div>
|
|
188
|
+
<Button type="button" variant="ghost" onClick={add}>
|
|
189
|
+
Add update
|
|
190
|
+
</Button>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
}
|