@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.
Files changed (68) hide show
  1. package/dist/components/components.css +19 -0
  2. package/dist/components/templated-text-field.d.ts +98 -0
  3. package/dist/components/templated-text-field.d.ts.map +1 -0
  4. package/dist/components/templated-text-field.js +217 -0
  5. package/dist/components/templated-text-field.js.map +1 -0
  6. package/dist/hooks/useApi.d.ts +581 -238
  7. package/dist/hooks/useApi.d.ts.map +1 -1
  8. package/dist/index.d.ts +4 -2
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +2 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/schema-editor/SchemaEditor.d.ts +8 -1
  13. package/dist/schema-editor/SchemaEditor.d.ts.map +1 -1
  14. package/dist/schema-editor/SchemaEditor.js +2 -2
  15. package/dist/schema-editor/SchemaEditor.js.map +1 -1
  16. package/dist/state-binding/BindingJqField.d.ts +8 -1
  17. package/dist/state-binding/BindingJqField.d.ts.map +1 -1
  18. package/dist/state-binding/BindingJqField.js +5 -2
  19. package/dist/state-binding/BindingJqField.js.map +1 -1
  20. package/dist/state-binding/BindingTemplatedJqField.d.ts +24 -0
  21. package/dist/state-binding/BindingTemplatedJqField.d.ts.map +1 -0
  22. package/dist/state-binding/BindingTemplatedJqField.js +7 -0
  23. package/dist/state-binding/BindingTemplatedJqField.js.map +1 -0
  24. package/dist/state-binding/InputInjectionRow.d.ts +4 -3
  25. package/dist/state-binding/InputInjectionRow.d.ts.map +1 -1
  26. package/dist/state-binding/InputInjectionRow.js +9 -9
  27. package/dist/state-binding/InputInjectionRow.js.map +1 -1
  28. package/dist/state-binding/StateAttachRow.d.ts +6 -4
  29. package/dist/state-binding/StateAttachRow.d.ts.map +1 -1
  30. package/dist/state-binding/StateAttachRow.js +13 -12
  31. package/dist/state-binding/StateAttachRow.js.map +1 -1
  32. package/dist/state-binding/StateBindingEditor.d.ts +4 -2
  33. package/dist/state-binding/StateBindingEditor.d.ts.map +1 -1
  34. package/dist/state-binding/StateBindingEditor.js +10 -8
  35. package/dist/state-binding/StateBindingEditor.js.map +1 -1
  36. package/dist/state-binding/SubjectScopeFields.d.ts +12 -9
  37. package/dist/state-binding/SubjectScopeFields.d.ts.map +1 -1
  38. package/dist/state-binding/SubjectScopeFields.js +5 -5
  39. package/dist/state-binding/SubjectScopeFields.js.map +1 -1
  40. package/dist/state-binding/UpdateRow.d.ts +4 -3
  41. package/dist/state-binding/UpdateRow.d.ts.map +1 -1
  42. package/dist/state-binding/UpdateRow.js +29 -16
  43. package/dist/state-binding/UpdateRow.js.map +1 -1
  44. package/dist/state-binding/index.d.ts +5 -1
  45. package/dist/state-binding/index.d.ts.map +1 -1
  46. package/dist/state-binding/index.js +2 -0
  47. package/dist/state-binding/index.js.map +1 -1
  48. package/dist/state-binding/templated-text-catalog.d.ts +21 -0
  49. package/dist/state-binding/templated-text-catalog.d.ts.map +1 -0
  50. package/dist/state-binding/templated-text-catalog.js +48 -0
  51. package/dist/state-binding/templated-text-catalog.js.map +1 -0
  52. package/dist/state-binding/types.d.ts +23 -2
  53. package/dist/state-binding/types.d.ts.map +1 -1
  54. package/package.json +2 -2
  55. package/src/components/components.css +19 -0
  56. package/src/components/templated-text-field.tsx +508 -0
  57. package/src/index.ts +13 -0
  58. package/src/schema-editor/SchemaEditor.tsx +9 -1
  59. package/src/state-binding/BindingJqField.tsx +12 -0
  60. package/src/state-binding/BindingTemplatedJqField.tsx +73 -0
  61. package/src/state-binding/InputInjectionRow.tsx +16 -7
  62. package/src/state-binding/StateAttachRow.tsx +19 -11
  63. package/src/state-binding/StateBindingEditor.tsx +14 -6
  64. package/src/state-binding/SubjectScopeFields.tsx +23 -15
  65. package/src/state-binding/UpdateRow.tsx +57 -24
  66. package/src/state-binding/index.ts +6 -0
  67. package/src/state-binding/templated-text-catalog.ts +69 -0
  68. 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 { BindingJqField, type TemplateJqSuggestion } from './BindingJqField';
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) onChange({ template_jq: null, jq: '', into: injection.into });
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
- <BindingJqField
75
+ <BindingTemplatedJqField
72
76
  label={`Custom injection jq ${String(index + 1)}`}
73
- value={injection.jq ?? ''}
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: string;
31
- readonly scope_expr: string | null;
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 || '(unset)'}`];
37
- if (attach.scope_expr !== null && attach.scope_expr !== '')
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 && update.adapter !== '' ? ` adapter ${update.adapter}` : ''}`,
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 && inherited.scope_expr !== ''
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 jq (contract: min_length=1); an empty one is
110
- // caught inline once a state is chosen, never sent for the server to 422.
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 !== '' && attach.subject_expr.trim() === ''
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 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.
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 { BindingJqField, type TemplateJqSuggestion } from './BindingJqField';
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: string;
12
- readonly scopeExpr: string | null;
13
- readonly onSubjectChange: (value: string) => void;
14
- readonly onScopeChange: (value: string | null) => void;
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
- <BindingJqField
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
- <BindingJqField
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 { BindingJqField, type TemplateJqSuggestion } from './BindingJqField';
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({ template_jq: null, jq: '', adapter: null, op_id: update.op_id });
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
- <BindingJqField
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({ template_jq: null, jq: next, adapter: null, op_id: update.op_id });
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
- <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>
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
- <BindingJqField
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.trim() === '' ? null : 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 { StateBinding, StateAttach, StateInjection, StateUpdate } from '@tai42/api-client';
10
+ import type {
11
+ StateBinding,
12
+ StateAttach,
13
+ StateInjection,
14
+ StateUpdate,
15
+ TemplatedText,
16
+ } from '@tai42/api-client';
11
17
 
12
- export type { StateBinding, StateAttach, StateInjection, StateUpdate };
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 {