@tai42/studio-sdk 14.0.0 → 16.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/components.css +19 -0
- package/dist/components/templated-text-field.d.ts +98 -0
- package/dist/components/templated-text-field.d.ts.map +1 -0
- package/dist/components/templated-text-field.js +217 -0
- package/dist/components/templated-text-field.js.map +1 -0
- package/dist/hooks/useApi.d.ts +581 -238
- 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 +2 -1
- package/dist/index.js.map +1 -1
- package/dist/schema-editor/SchemaEditor.d.ts +8 -1
- package/dist/schema-editor/SchemaEditor.d.ts.map +1 -1
- package/dist/schema-editor/SchemaEditor.js +2 -2
- package/dist/schema-editor/SchemaEditor.js.map +1 -1
- package/dist/state-binding/BindingJqField.d.ts +8 -1
- package/dist/state-binding/BindingJqField.d.ts.map +1 -1
- package/dist/state-binding/BindingJqField.js +5 -2
- package/dist/state-binding/BindingJqField.js.map +1 -1
- package/dist/state-binding/BindingTemplatedJqField.d.ts +24 -0
- package/dist/state-binding/BindingTemplatedJqField.d.ts.map +1 -0
- package/dist/state-binding/BindingTemplatedJqField.js +7 -0
- package/dist/state-binding/BindingTemplatedJqField.js.map +1 -0
- package/dist/state-binding/InputInjectionRow.d.ts +4 -3
- package/dist/state-binding/InputInjectionRow.d.ts.map +1 -1
- package/dist/state-binding/InputInjectionRow.js +9 -9
- package/dist/state-binding/InputInjectionRow.js.map +1 -1
- package/dist/state-binding/StateAttachRow.d.ts +6 -4
- package/dist/state-binding/StateAttachRow.d.ts.map +1 -1
- package/dist/state-binding/StateAttachRow.js +13 -12
- package/dist/state-binding/StateAttachRow.js.map +1 -1
- package/dist/state-binding/StateBindingEditor.d.ts +4 -2
- package/dist/state-binding/StateBindingEditor.d.ts.map +1 -1
- package/dist/state-binding/StateBindingEditor.js +10 -8
- package/dist/state-binding/StateBindingEditor.js.map +1 -1
- package/dist/state-binding/SubjectScopeFields.d.ts +12 -9
- package/dist/state-binding/SubjectScopeFields.d.ts.map +1 -1
- package/dist/state-binding/SubjectScopeFields.js +5 -5
- package/dist/state-binding/SubjectScopeFields.js.map +1 -1
- package/dist/state-binding/UpdateRow.d.ts +4 -3
- package/dist/state-binding/UpdateRow.d.ts.map +1 -1
- package/dist/state-binding/UpdateRow.js +29 -16
- package/dist/state-binding/UpdateRow.js.map +1 -1
- package/dist/state-binding/index.d.ts +5 -1
- package/dist/state-binding/index.d.ts.map +1 -1
- package/dist/state-binding/index.js +2 -0
- package/dist/state-binding/index.js.map +1 -1
- package/dist/state-binding/templated-text-catalog.d.ts +21 -0
- package/dist/state-binding/templated-text-catalog.d.ts.map +1 -0
- package/dist/state-binding/templated-text-catalog.js +48 -0
- package/dist/state-binding/templated-text-catalog.js.map +1 -0
- package/dist/state-binding/types.d.ts +23 -2
- package/dist/state-binding/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/components/components.css +19 -0
- package/src/components/templated-text-field.tsx +508 -0
- package/src/index.ts +13 -0
- package/src/schema-editor/SchemaEditor.tsx +9 -1
- package/src/state-binding/BindingJqField.tsx +12 -0
- package/src/state-binding/BindingTemplatedJqField.tsx +73 -0
- package/src/state-binding/InputInjectionRow.tsx +16 -7
- package/src/state-binding/StateAttachRow.tsx +19 -11
- package/src/state-binding/StateBindingEditor.tsx +14 -6
- package/src/state-binding/SubjectScopeFields.tsx +23 -15
- package/src/state-binding/UpdateRow.tsx +57 -24
- package/src/state-binding/index.ts +6 -0
- package/src/state-binding/templated-text-catalog.ts +69 -0
- package/src/state-binding/types.ts +31 -2
|
@@ -10,9 +10,10 @@ import { Field } from '../components/field';
|
|
|
10
10
|
import { Select } from '../components/select';
|
|
11
11
|
import { TextInput } from '../components/inputs';
|
|
12
12
|
import { CloseIcon } from '../components/icons';
|
|
13
|
-
import {
|
|
13
|
+
import { BindingTemplatedJqField } from './BindingTemplatedJqField';
|
|
14
|
+
import type { TemplateJqSuggestion } from './BindingJqField';
|
|
14
15
|
import type { ResolvedTemplateJq } from './catalog';
|
|
15
|
-
import type { StateInjection } from './types';
|
|
16
|
+
import type { StateInjection, TemplatedTextCatalog } from './types';
|
|
16
17
|
|
|
17
18
|
function optionLabel(entry: ResolvedTemplateJq): string {
|
|
18
19
|
return entry.description !== undefined && entry.description !== ''
|
|
@@ -27,6 +28,7 @@ function InjectionRow({
|
|
|
27
28
|
index,
|
|
28
29
|
inputJq,
|
|
29
30
|
suggestions,
|
|
31
|
+
templates,
|
|
30
32
|
onChange,
|
|
31
33
|
onRemove,
|
|
32
34
|
}: {
|
|
@@ -34,6 +36,7 @@ function InjectionRow({
|
|
|
34
36
|
readonly index: number;
|
|
35
37
|
readonly inputJq: readonly ResolvedTemplateJq[];
|
|
36
38
|
readonly suggestions?: readonly TemplateJqSuggestion[];
|
|
39
|
+
readonly templates?: TemplatedTextCatalog;
|
|
37
40
|
readonly onChange: (injection: StateInjection) => void;
|
|
38
41
|
readonly onRemove: () => void;
|
|
39
42
|
}): ReactNode {
|
|
@@ -55,7 +58,8 @@ function InjectionRow({
|
|
|
55
58
|
placeholder="Choose a template jq"
|
|
56
59
|
value={selectValue}
|
|
57
60
|
onValueChange={(next) => {
|
|
58
|
-
if (next === CUSTOM)
|
|
61
|
+
if (next === CUSTOM)
|
|
62
|
+
onChange({ template_jq: null, jq: { content: '' }, into: injection.into });
|
|
59
63
|
else onChange({ template_jq: next, jq: null, into: injection.into });
|
|
60
64
|
}}
|
|
61
65
|
groups={[
|
|
@@ -68,13 +72,15 @@ function InjectionRow({
|
|
|
68
72
|
/>
|
|
69
73
|
</Field>
|
|
70
74
|
{isCustom ? (
|
|
71
|
-
<
|
|
75
|
+
<BindingTemplatedJqField
|
|
72
76
|
label={`Custom injection jq ${String(index + 1)}`}
|
|
73
|
-
|
|
77
|
+
required
|
|
78
|
+
value={injection.jq}
|
|
74
79
|
onChange={(next) => {
|
|
75
|
-
onChange({ template_jq: null, jq: next, into: injection.into });
|
|
80
|
+
onChange({ template_jq: null, jq: next ?? { content: '' }, into: injection.into });
|
|
76
81
|
}}
|
|
77
82
|
suggestions={suggestions}
|
|
83
|
+
templates={templates}
|
|
78
84
|
/>
|
|
79
85
|
) : null}
|
|
80
86
|
<Field label={`Into field ${String(index + 1)}`} hideLabel>
|
|
@@ -103,6 +109,7 @@ export interface InjectionListProps {
|
|
|
103
109
|
readonly onChange: (injections: readonly StateInjection[]) => void;
|
|
104
110
|
readonly inputJq: readonly ResolvedTemplateJq[];
|
|
105
111
|
readonly suggestions?: readonly TemplateJqSuggestion[];
|
|
112
|
+
readonly templates?: TemplatedTextCatalog;
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
export function InjectionList({
|
|
@@ -110,13 +117,14 @@ export function InjectionList({
|
|
|
110
117
|
onChange,
|
|
111
118
|
inputJq,
|
|
112
119
|
suggestions,
|
|
120
|
+
templates,
|
|
113
121
|
}: InjectionListProps): ReactNode {
|
|
114
122
|
const add = (): void => {
|
|
115
123
|
const first = inputJq[0];
|
|
116
124
|
const next: StateInjection =
|
|
117
125
|
first !== undefined
|
|
118
126
|
? { template_jq: first.ref, jq: null, into: '' }
|
|
119
|
-
: { template_jq: null, jq: '', into: '' };
|
|
127
|
+
: { template_jq: null, jq: { content: '' }, into: '' };
|
|
120
128
|
onChange([...injections, next]);
|
|
121
129
|
};
|
|
122
130
|
|
|
@@ -130,6 +138,7 @@ export function InjectionList({
|
|
|
130
138
|
index={index}
|
|
131
139
|
inputJq={inputJq}
|
|
132
140
|
suggestions={suggestions}
|
|
141
|
+
templates={templates}
|
|
133
142
|
onChange={(next) => {
|
|
134
143
|
onChange(injections.map((current, position) => (position === index ? next : current)));
|
|
135
144
|
}}
|
|
@@ -10,6 +10,7 @@ import { CodeBlock } from '../components/code-block';
|
|
|
10
10
|
import { Checkbox } from '../components/checkbox';
|
|
11
11
|
import { Field } from '../components/field';
|
|
12
12
|
import { Select } from '../components/select';
|
|
13
|
+
import { templatedTextSummary } from '../components/templated-text-field';
|
|
13
14
|
import { AlertTriangleIcon, CloseIcon } from '../components/icons';
|
|
14
15
|
import { InjectionList } from './InputInjectionRow';
|
|
15
16
|
import { UpdateList } from './UpdateRow';
|
|
@@ -21,30 +22,31 @@ import type {
|
|
|
21
22
|
BindingStateOption,
|
|
22
23
|
BindingTemplateOption,
|
|
23
24
|
StateAttach,
|
|
25
|
+
TemplatedText,
|
|
26
|
+
TemplatedTextCatalog,
|
|
24
27
|
} from './types';
|
|
25
28
|
|
|
26
29
|
const ATTACH_ON_USE_HINT = 'This template will be attached to the state when you save.';
|
|
27
30
|
|
|
28
31
|
/** The subject/scope of the target's own (inherited) binding for a state this one also names. */
|
|
29
32
|
export interface InheritedSubject {
|
|
30
|
-
readonly subject_expr:
|
|
31
|
-
readonly scope_expr:
|
|
33
|
+
readonly subject_expr: TemplatedText;
|
|
34
|
+
readonly scope_expr: TemplatedText | null;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
/** The stored binding's raw jq, for the unresolved-catalog read-only fallback. */
|
|
35
38
|
function rawBindingLines(attach: StateAttach): string {
|
|
36
|
-
const lines = [`subject: ${attach.subject_expr
|
|
37
|
-
if (attach.scope_expr !== null
|
|
38
|
-
lines.push(`scope: ${attach.scope_expr}`);
|
|
39
|
+
const lines = [`subject: ${templatedTextSummary(attach.subject_expr)}`];
|
|
40
|
+
if (attach.scope_expr !== null) lines.push(`scope: ${templatedTextSummary(attach.scope_expr)}`);
|
|
39
41
|
for (const injection of attach.input_injections) {
|
|
40
42
|
lines.push(
|
|
41
|
-
`input ${injection.into || '(unset)'} <- ${injection.template_jq ?? injection.jq
|
|
43
|
+
`input ${injection.into || '(unset)'} <- ${injection.template_jq ?? templatedTextSummary(injection.jq)}`,
|
|
42
44
|
);
|
|
43
45
|
}
|
|
44
46
|
for (const update of attach.updates) {
|
|
45
|
-
const ref = update.template_jq ?? update.jq
|
|
47
|
+
const ref = update.template_jq ?? templatedTextSummary(update.jq);
|
|
46
48
|
lines.push(
|
|
47
|
-
`update ${ref}${update.adapter !== null
|
|
49
|
+
`update ${ref}${update.adapter !== null ? ` adapter ${templatedTextSummary(update.adapter)}` : ''}`,
|
|
48
50
|
);
|
|
49
51
|
}
|
|
50
52
|
return lines.join('\n');
|
|
@@ -60,6 +62,8 @@ export interface StateAttachRowProps {
|
|
|
60
62
|
readonly subjectError?: string;
|
|
61
63
|
/** The target's own binding subject for this state, when it also binds it (precedence). */
|
|
62
64
|
readonly inherited?: InheritedSubject;
|
|
65
|
+
/** The stored templates a templated-text field's id picker offers. */
|
|
66
|
+
readonly templates?: TemplatedTextCatalog;
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
export function StateAttachRow({
|
|
@@ -71,6 +75,7 @@ export function StateAttachRow({
|
|
|
71
75
|
sources,
|
|
72
76
|
subjectError,
|
|
73
77
|
inherited,
|
|
78
|
+
templates,
|
|
74
79
|
}: StateAttachRowProps): ReactNode {
|
|
75
80
|
const stateOption = statesCatalog.find((option) => option.name === attach.state);
|
|
76
81
|
const attachedTemplates = stateOption?.attachedTemplates ?? [];
|
|
@@ -175,9 +180,9 @@ export function StateAttachRow({
|
|
|
175
180
|
Preset default:
|
|
176
181
|
</span>
|
|
177
182
|
<span style={{ fontFamily: 'var(--tai-font-mono)', fontSize: 'var(--tai-text-sm)' }}>
|
|
178
|
-
{`subject: ${inherited.subject_expr}`}
|
|
179
|
-
{inherited.scope_expr !== null
|
|
180
|
-
? `, scope: ${inherited.scope_expr}`
|
|
183
|
+
{`subject: ${templatedTextSummary(inherited.subject_expr)}`}
|
|
184
|
+
{inherited.scope_expr !== null
|
|
185
|
+
? `, scope: ${templatedTextSummary(inherited.scope_expr)}`
|
|
181
186
|
: ''}
|
|
182
187
|
</span>
|
|
183
188
|
</div>
|
|
@@ -244,6 +249,7 @@ export function StateAttachRow({
|
|
|
244
249
|
scopeExpr={attach.scope_expr}
|
|
245
250
|
subjectError={subjectError}
|
|
246
251
|
suggestions={suggestions}
|
|
252
|
+
templates={templates}
|
|
247
253
|
onSubjectChange={(next) => {
|
|
248
254
|
onChange({ ...attach, subject_expr: next });
|
|
249
255
|
}}
|
|
@@ -257,6 +263,7 @@ export function StateAttachRow({
|
|
|
257
263
|
injections={attach.input_injections}
|
|
258
264
|
inputJq={inputJq}
|
|
259
265
|
suggestions={suggestions}
|
|
266
|
+
templates={templates}
|
|
260
267
|
onChange={(next) => {
|
|
261
268
|
onChange({ ...attach, input_injections: [...next] });
|
|
262
269
|
}}
|
|
@@ -269,6 +276,7 @@ export function StateAttachRow({
|
|
|
269
276
|
updateJq={updateJq}
|
|
270
277
|
sources={sources}
|
|
271
278
|
suggestions={suggestions}
|
|
279
|
+
templates={templates}
|
|
272
280
|
onChange={(next) => {
|
|
273
281
|
onChange({ ...attach, updates: [...next] });
|
|
274
282
|
}}
|
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
BindingTemplateOption,
|
|
19
19
|
StateAttach,
|
|
20
20
|
StateBinding,
|
|
21
|
+
TemplatedTextCatalog,
|
|
21
22
|
} from './types';
|
|
22
23
|
|
|
23
24
|
export interface StateBindingEditorProps {
|
|
@@ -40,13 +41,15 @@ export interface StateBindingEditorProps {
|
|
|
40
41
|
readonly loading?: boolean;
|
|
41
42
|
/** The catalog fetch failed. */
|
|
42
43
|
readonly error?: string;
|
|
44
|
+
/** The stored templates a templated-text field's id picker offers, resolved by the door. */
|
|
45
|
+
readonly templatedTextTemplates?: TemplatedTextCatalog;
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
function blankAttach(): StateAttach {
|
|
46
49
|
return {
|
|
47
50
|
state: '',
|
|
48
51
|
templates: [],
|
|
49
|
-
subject_expr: '',
|
|
52
|
+
subject_expr: { content: '' },
|
|
50
53
|
scope_expr: null,
|
|
51
54
|
input_injections: [],
|
|
52
55
|
updates: [],
|
|
@@ -62,6 +65,7 @@ export function StateBindingEditor({
|
|
|
62
65
|
inherited,
|
|
63
66
|
loading = false,
|
|
64
67
|
error,
|
|
68
|
+
templatedTextTemplates,
|
|
65
69
|
}: StateBindingEditorProps): ReactNode {
|
|
66
70
|
if (loading) {
|
|
67
71
|
return (
|
|
@@ -106,12 +110,15 @@ export function StateBindingEditor({
|
|
|
106
110
|
attach.state === ''
|
|
107
111
|
? undefined
|
|
108
112
|
: inherited?.states.find((entry) => entry.state === attach.state);
|
|
109
|
-
// The subject is a required
|
|
110
|
-
// caught inline once a state is chosen, never sent for
|
|
113
|
+
// The subject is a required authored value; an empty one (no stored id and
|
|
114
|
+
// no inline text) is caught inline once a state is chosen, never sent for
|
|
115
|
+
// the server to 422.
|
|
116
|
+
const subjectEmpty =
|
|
117
|
+
attach.subject_expr.id !== undefined
|
|
118
|
+
? attach.subject_expr.id.trim() === ''
|
|
119
|
+
: (attach.subject_expr.content ?? '').trim() === '';
|
|
111
120
|
const subjectError =
|
|
112
|
-
attach.state !== '' &&
|
|
113
|
-
? 'Subject is required.'
|
|
114
|
-
: undefined;
|
|
121
|
+
attach.state !== '' && subjectEmpty ? 'Subject is required.' : undefined;
|
|
115
122
|
return (
|
|
116
123
|
<StateAttachRow
|
|
117
124
|
// Positional identity within a save; a state name may be blank while picking.
|
|
@@ -121,6 +128,7 @@ export function StateBindingEditor({
|
|
|
121
128
|
templatesCatalog={templatesCatalog}
|
|
122
129
|
sources={sources}
|
|
123
130
|
subjectError={subjectError}
|
|
131
|
+
templates={templatedTextTemplates}
|
|
124
132
|
inherited={
|
|
125
133
|
inheritedAttach === undefined
|
|
126
134
|
? undefined
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The subject + scope
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The subject + scope of one attached state: the SUBJECT (required) yields either a
|
|
3
|
+
* bare record-key string or a full subject object; the optional SCOPE is a boolean
|
|
4
|
+
* jq predicate — when it is false the state is skipped for the run. Each is an
|
|
5
|
+
* authored templated-text value (inline jq or a stored template that renders to it).
|
|
5
6
|
*/
|
|
6
7
|
import type { ReactNode } from 'react';
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
+
import { BindingTemplatedJqField } from './BindingTemplatedJqField';
|
|
10
|
+
import type { TemplateJqSuggestion } from './BindingJqField';
|
|
11
|
+
import type { TemplatedText, TemplatedTextCatalog } from './types';
|
|
9
12
|
|
|
10
13
|
export interface SubjectScopeFieldsProps {
|
|
11
|
-
readonly subjectExpr:
|
|
12
|
-
readonly scopeExpr:
|
|
13
|
-
readonly onSubjectChange: (value:
|
|
14
|
-
readonly onScopeChange: (value:
|
|
14
|
+
readonly subjectExpr: TemplatedText;
|
|
15
|
+
readonly scopeExpr: TemplatedText | null;
|
|
16
|
+
readonly onSubjectChange: (value: TemplatedText) => void;
|
|
17
|
+
readonly onScopeChange: (value: TemplatedText | null) => void;
|
|
15
18
|
readonly subjectError?: string;
|
|
16
19
|
readonly suggestions?: readonly TemplateJqSuggestion[];
|
|
20
|
+
readonly templates?: TemplatedTextCatalog;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
export function SubjectScopeFields({
|
|
@@ -23,26 +27,30 @@ export function SubjectScopeFields({
|
|
|
23
27
|
onScopeChange,
|
|
24
28
|
subjectError,
|
|
25
29
|
suggestions,
|
|
30
|
+
templates,
|
|
26
31
|
}: SubjectScopeFieldsProps): ReactNode {
|
|
27
32
|
return (
|
|
28
33
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--tai-space-2)' }}>
|
|
29
|
-
<
|
|
34
|
+
<BindingTemplatedJqField
|
|
30
35
|
label="Subject"
|
|
36
|
+
required
|
|
31
37
|
description="The record key this binding reads and updates: a key string, or a full subject object."
|
|
32
38
|
value={subjectExpr}
|
|
33
|
-
onChange={onSubjectChange}
|
|
34
39
|
error={subjectError}
|
|
35
40
|
suggestions={suggestions}
|
|
41
|
+
templates={templates}
|
|
36
42
|
placeholder=".subject_id"
|
|
43
|
+
onChange={(next) => {
|
|
44
|
+
onSubjectChange(next ?? { content: '' });
|
|
45
|
+
}}
|
|
37
46
|
/>
|
|
38
|
-
<
|
|
47
|
+
<BindingTemplatedJqField
|
|
39
48
|
label="Scope"
|
|
40
49
|
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
|
-
}}
|
|
50
|
+
value={scopeExpr}
|
|
45
51
|
suggestions={suggestions}
|
|
52
|
+
templates={templates}
|
|
53
|
+
onChange={onScopeChange}
|
|
46
54
|
/>
|
|
47
55
|
</div>
|
|
48
56
|
);
|
|
@@ -10,11 +10,13 @@ import { Badge } from '../components/badge';
|
|
|
10
10
|
import { Button } from '../components/primitives';
|
|
11
11
|
import { Field } from '../components/field';
|
|
12
12
|
import { Select } from '../components/select';
|
|
13
|
+
import { TemplatedTextField } from '../components/templated-text-field';
|
|
13
14
|
import { CloseIcon } from '../components/icons';
|
|
14
15
|
import { AdapterMapping } from './AdapterMapping';
|
|
15
|
-
import {
|
|
16
|
+
import { BindingTemplatedJqField } from './BindingTemplatedJqField';
|
|
17
|
+
import type { TemplateJqSuggestion } from './BindingJqField';
|
|
16
18
|
import { findByRef, type ResolvedTemplateJq } from './catalog';
|
|
17
|
-
import type { BindingSourceSchemas, StateUpdate } from './types';
|
|
19
|
+
import type { BindingSourceSchemas, StateUpdate, TemplatedTextCatalog } from './types';
|
|
18
20
|
|
|
19
21
|
const CUSTOM = '__custom__';
|
|
20
22
|
|
|
@@ -35,6 +37,7 @@ function UpdateRowEditor({
|
|
|
35
37
|
updateJq,
|
|
36
38
|
sources,
|
|
37
39
|
suggestions,
|
|
40
|
+
templates,
|
|
38
41
|
onChange,
|
|
39
42
|
onRemove,
|
|
40
43
|
}: {
|
|
@@ -43,6 +46,7 @@ function UpdateRowEditor({
|
|
|
43
46
|
readonly updateJq: readonly ResolvedTemplateJq[];
|
|
44
47
|
readonly sources?: BindingSourceSchemas;
|
|
45
48
|
readonly suggestions?: readonly TemplateJqSuggestion[];
|
|
49
|
+
readonly templates?: TemplatedTextCatalog;
|
|
46
50
|
readonly onChange: (update: StateUpdate) => void;
|
|
47
51
|
readonly onRemove: () => void;
|
|
48
52
|
}): ReactNode {
|
|
@@ -78,7 +82,12 @@ function UpdateRowEditor({
|
|
|
78
82
|
value={selectValue}
|
|
79
83
|
onValueChange={(next) => {
|
|
80
84
|
if (next === CUSTOM)
|
|
81
|
-
onChange({
|
|
85
|
+
onChange({
|
|
86
|
+
template_jq: null,
|
|
87
|
+
jq: { content: '' },
|
|
88
|
+
adapter: null,
|
|
89
|
+
op_id: update.op_id,
|
|
90
|
+
});
|
|
82
91
|
else onChange({ template_jq: next, jq: null, adapter: null, op_id: update.op_id });
|
|
83
92
|
}}
|
|
84
93
|
groups={[
|
|
@@ -102,40 +111,61 @@ function UpdateRowEditor({
|
|
|
102
111
|
</div>
|
|
103
112
|
|
|
104
113
|
{isCustom ? (
|
|
105
|
-
<
|
|
114
|
+
<BindingTemplatedJqField
|
|
106
115
|
label={`Custom update jq ${String(index + 1)}`}
|
|
116
|
+
required
|
|
107
117
|
description="A jq over `{ record, output, input }` returning a template-relative op batch."
|
|
108
|
-
value={update.jq
|
|
118
|
+
value={update.jq}
|
|
109
119
|
onChange={(next) => {
|
|
110
|
-
onChange({
|
|
120
|
+
onChange({
|
|
121
|
+
template_jq: null,
|
|
122
|
+
jq: next ?? { content: '' },
|
|
123
|
+
adapter: null,
|
|
124
|
+
op_id: update.op_id,
|
|
125
|
+
});
|
|
111
126
|
}}
|
|
112
127
|
suggestions={suggestions}
|
|
128
|
+
templates={templates}
|
|
113
129
|
/>
|
|
114
130
|
) : (
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
131
|
+
<TemplatedTextField
|
|
132
|
+
// Remount on a template change so the mapping rebuilds from the newly
|
|
133
|
+
// declared input (and re-seeds its shown default) instead of stale rows.
|
|
134
|
+
key={resolved?.ref ?? '__none__'}
|
|
135
|
+
label="Adapter"
|
|
136
|
+
value={update.adapter}
|
|
137
|
+
templates={templates?.templates}
|
|
138
|
+
templatesLoading={templates?.loading}
|
|
139
|
+
templatesError={templates?.error}
|
|
140
|
+
onTemplatesRetry={templates?.onRetry}
|
|
141
|
+
storageAbsent={templates?.storageAbsent}
|
|
142
|
+
storagePresenceLoading={templates?.storagePresenceLoading}
|
|
143
|
+
onChange={(next) => {
|
|
144
|
+
onChange({ ...update, adapter: next });
|
|
145
|
+
}}
|
|
146
|
+
renderInline={({ value: inlineValue, onChange: onInline }) => (
|
|
147
|
+
<AdapterMapping
|
|
148
|
+
declaredInput={resolved?.params ?? []}
|
|
149
|
+
value={inlineValue}
|
|
150
|
+
sources={sources}
|
|
151
|
+
suggestions={suggestions}
|
|
152
|
+
onChange={(adapterJq) => {
|
|
153
|
+
onInline(adapterJq ?? '');
|
|
154
|
+
}}
|
|
155
|
+
/>
|
|
156
|
+
)}
|
|
157
|
+
/>
|
|
129
158
|
)}
|
|
130
159
|
|
|
131
|
-
<
|
|
160
|
+
<BindingTemplatedJqField
|
|
132
161
|
label="Op id"
|
|
133
162
|
description="Optional — an idempotency-key expression."
|
|
134
|
-
value={update.op_id
|
|
163
|
+
value={update.op_id}
|
|
135
164
|
onChange={(next) => {
|
|
136
|
-
onChange({ ...update, op_id: next
|
|
165
|
+
onChange({ ...update, op_id: next });
|
|
137
166
|
}}
|
|
138
167
|
suggestions={suggestions}
|
|
168
|
+
templates={templates}
|
|
139
169
|
/>
|
|
140
170
|
</div>
|
|
141
171
|
);
|
|
@@ -147,6 +177,7 @@ export interface UpdateListProps {
|
|
|
147
177
|
readonly updateJq: readonly ResolvedTemplateJq[];
|
|
148
178
|
readonly sources?: BindingSourceSchemas;
|
|
149
179
|
readonly suggestions?: readonly TemplateJqSuggestion[];
|
|
180
|
+
readonly templates?: TemplatedTextCatalog;
|
|
150
181
|
}
|
|
151
182
|
|
|
152
183
|
export function UpdateList({
|
|
@@ -155,13 +186,14 @@ export function UpdateList({
|
|
|
155
186
|
updateJq,
|
|
156
187
|
sources,
|
|
157
188
|
suggestions,
|
|
189
|
+
templates,
|
|
158
190
|
}: UpdateListProps): ReactNode {
|
|
159
191
|
const add = (): void => {
|
|
160
192
|
const first = updateJq[0];
|
|
161
193
|
const next: StateUpdate =
|
|
162
194
|
first !== undefined
|
|
163
195
|
? { template_jq: first.ref, jq: null, adapter: null, op_id: null }
|
|
164
|
-
: { template_jq: null, jq: '', adapter: null, op_id: null };
|
|
196
|
+
: { template_jq: null, jq: { content: '' }, adapter: null, op_id: null };
|
|
165
197
|
onChange([...updates, next]);
|
|
166
198
|
};
|
|
167
199
|
|
|
@@ -176,6 +208,7 @@ export function UpdateList({
|
|
|
176
208
|
updateJq={updateJq}
|
|
177
209
|
sources={sources}
|
|
178
210
|
suggestions={suggestions}
|
|
211
|
+
templates={templates}
|
|
179
212
|
onChange={(next) => {
|
|
180
213
|
onChange(updates.map((current, position) => (position === index ? next : current)));
|
|
181
214
|
}}
|
|
@@ -20,8 +20,12 @@ export { SubjectScopeFields } from './SubjectScopeFields';
|
|
|
20
20
|
export type { SubjectScopeFieldsProps } from './SubjectScopeFields';
|
|
21
21
|
export { BindingJqField, appendTjq } from './BindingJqField';
|
|
22
22
|
export type { BindingJqFieldProps, TemplateJqSuggestion } from './BindingJqField';
|
|
23
|
+
export { BindingTemplatedJqField } from './BindingTemplatedJqField';
|
|
24
|
+
export type { BindingTemplatedJqFieldProps } from './BindingTemplatedJqField';
|
|
23
25
|
export { resolveTemplateJq, findByRef, resolveCallName } from './catalog';
|
|
24
26
|
export type { ResolvedTemplateJq } from './catalog';
|
|
27
|
+
export { templatedTextCatalog } from './templated-text-catalog';
|
|
28
|
+
export type { StoragePresenceQueryLike, TemplateNamesQueryLike } from './templated-text-catalog';
|
|
25
29
|
export {
|
|
26
30
|
templatesCatalogFromList,
|
|
27
31
|
statesCatalogFromList,
|
|
@@ -58,4 +62,6 @@ export type {
|
|
|
58
62
|
StateAttach,
|
|
59
63
|
StateInjection,
|
|
60
64
|
StateUpdate,
|
|
65
|
+
TemplatedText,
|
|
66
|
+
TemplatedTextCatalog,
|
|
61
67
|
} from './types';
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the {@link TemplatedTextCatalog} a door form feeds its templated-text
|
|
3
|
+
* fields, from the two queries every such form already runs: the storage-presence
|
|
4
|
+
* signal (`GET /api/storage`) and the stored-template name list (`GET /api/templates`).
|
|
5
|
+
*
|
|
6
|
+
* Storage is dead by default — a deployment with no storage backend is supported —
|
|
7
|
+
* and the template list door cannot answer without one. So the CALLER gates its
|
|
8
|
+
* template-names query on presence (`enabled: storage.data?.present === true`), and
|
|
9
|
+
* this helper maps the two query states onto the catalog the field reads: when
|
|
10
|
+
* storage is absent it exposes no templates and marks the field absent (the field
|
|
11
|
+
* then offers no stored source); when present it exposes the fetched names. A
|
|
12
|
+
* presence-fetch failure surfaces ahead of a template-fetch failure, since presence
|
|
13
|
+
* is the prerequisite.
|
|
14
|
+
*
|
|
15
|
+
* It takes the query RESULTS structurally (the fields it reads), so this module adds
|
|
16
|
+
* no data-layer dependency to the SDK.
|
|
17
|
+
*/
|
|
18
|
+
import { errorMessage } from '../errors';
|
|
19
|
+
|
|
20
|
+
import type { TemplatedTextCatalog } from './types';
|
|
21
|
+
|
|
22
|
+
/** The storage-presence query result (`GET /api/storage`) fields this helper reads. */
|
|
23
|
+
export interface StoragePresenceQueryLike {
|
|
24
|
+
readonly data?: { readonly present: boolean };
|
|
25
|
+
readonly isPending: boolean;
|
|
26
|
+
readonly isError: boolean;
|
|
27
|
+
readonly error: unknown;
|
|
28
|
+
readonly refetch: () => unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The stored-template-names query result (`GET /api/templates`) fields this helper reads. */
|
|
32
|
+
export interface TemplateNamesQueryLike {
|
|
33
|
+
readonly data?: readonly string[];
|
|
34
|
+
readonly isPending: boolean;
|
|
35
|
+
readonly isError: boolean;
|
|
36
|
+
readonly error: unknown;
|
|
37
|
+
readonly refetch: () => unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function templatedTextCatalog(
|
|
41
|
+
storage: StoragePresenceQueryLike,
|
|
42
|
+
templates: TemplateNamesQueryLike,
|
|
43
|
+
): TemplatedTextCatalog {
|
|
44
|
+
const presenceKnown = storage.data !== undefined;
|
|
45
|
+
const present = storage.data?.present === true;
|
|
46
|
+
// A presence failure matters only until presence is first known: once it is in
|
|
47
|
+
// hand, a later background-refetch failure must not blow away a field already
|
|
48
|
+
// working from it. Presence is the prerequisite of the catalog, so its failure
|
|
49
|
+
// surfaces ahead of a catalog-fetch failure.
|
|
50
|
+
const presenceFailed = storage.isError && !presenceKnown;
|
|
51
|
+
return {
|
|
52
|
+
// Never expose template names while storage is absent or unknown — the caller's
|
|
53
|
+
// gate keeps the list query from firing there, and its pending/stale data must
|
|
54
|
+
// not leak into a picker the field will not show.
|
|
55
|
+
templates: present ? (templates.data ?? []).map((id) => ({ id })) : [],
|
|
56
|
+
loading: present && templates.isPending,
|
|
57
|
+
error: presenceFailed
|
|
58
|
+
? errorMessage(storage.error)
|
|
59
|
+
: templates.isError
|
|
60
|
+
? errorMessage(templates.error)
|
|
61
|
+
: undefined,
|
|
62
|
+
onRetry: () => {
|
|
63
|
+
if (presenceFailed) void storage.refetch();
|
|
64
|
+
else void templates.refetch();
|
|
65
|
+
},
|
|
66
|
+
storageAbsent: storage.data?.present === false,
|
|
67
|
+
storagePresenceLoading: !presenceKnown && storage.isPending,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -7,9 +7,38 @@
|
|
|
7
7
|
* the states it may bind, the templates each may attach, and — when a schema is
|
|
8
8
|
* known — the fields a picker offers.
|
|
9
9
|
*/
|
|
10
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
StateBinding,
|
|
12
|
+
StateAttach,
|
|
13
|
+
StateInjection,
|
|
14
|
+
StateUpdate,
|
|
15
|
+
TemplatedText,
|
|
16
|
+
} from '@tai42/api-client';
|
|
11
17
|
|
|
12
|
-
|
|
18
|
+
import type { TemplatedTextTemplateOption } from '../components/templated-text-field';
|
|
19
|
+
|
|
20
|
+
export type { StateBinding, StateAttach, StateInjection, StateUpdate, TemplatedText };
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The stored templates a {@link TemplatedText} field's id picker offers, with the
|
|
24
|
+
* fetch's loading/error state — resolved by the door screen and passed to the
|
|
25
|
+
* editor, which holds no data edge of its own. Absent leaves every templated-text
|
|
26
|
+
* field with the inline source only.
|
|
27
|
+
*/
|
|
28
|
+
export interface TemplatedTextCatalog {
|
|
29
|
+
readonly templates?: readonly TemplatedTextTemplateOption[];
|
|
30
|
+
readonly loading?: boolean;
|
|
31
|
+
readonly error?: string;
|
|
32
|
+
readonly onRetry?: () => void;
|
|
33
|
+
/**
|
|
34
|
+
* Storage is registered absent (`GET /api/storage` → `present: false`), so the
|
|
35
|
+
* catalog was not fetched and the stored source is not offered on the fields this
|
|
36
|
+
* catalog feeds; an already-stored id is shown read-only.
|
|
37
|
+
*/
|
|
38
|
+
readonly storageAbsent?: boolean;
|
|
39
|
+
/** Storage presence is not yet known — the fields render a placeholder, not a toggle. */
|
|
40
|
+
readonly storagePresenceLoading?: boolean;
|
|
41
|
+
}
|
|
13
42
|
|
|
14
43
|
/** One selectable field of a schema: the record path + a human label. */
|
|
15
44
|
export interface SchemaFieldPath {
|