@tai42/studio-sdk 22.4.1 → 24.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 (45) hide show
  1. package/dist/hooks/useApi.d.ts +194 -24
  2. package/dist/hooks/useApi.d.ts.map +1 -1
  3. package/dist/index.d.ts +2 -2
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/schema-form/SchemaForm.d.ts +10 -12
  8. package/dist/schema-form/SchemaForm.d.ts.map +1 -1
  9. package/dist/schema-form/SchemaForm.js.map +1 -1
  10. package/dist/schema-form/array-field.d.ts +9 -2
  11. package/dist/schema-form/array-field.d.ts.map +1 -1
  12. package/dist/schema-form/array-field.js +5 -3
  13. package/dist/schema-form/array-field.js.map +1 -1
  14. package/dist/schema-form/date-range-field.d.ts +30 -7
  15. package/dist/schema-form/date-range-field.d.ts.map +1 -1
  16. package/dist/schema-form/date-range-field.js +73 -15
  17. package/dist/schema-form/date-range-field.js.map +1 -1
  18. package/dist/schema-form/field-node.d.ts +7 -8
  19. package/dist/schema-form/field-node.d.ts.map +1 -1
  20. package/dist/schema-form/field-node.js +2 -1
  21. package/dist/schema-form/field-node.js.map +1 -1
  22. package/dist/schema-form/index.d.ts +2 -2
  23. package/dist/schema-form/index.d.ts.map +1 -1
  24. package/dist/schema-form/index.js +1 -1
  25. package/dist/schema-form/index.js.map +1 -1
  26. package/dist/schema-form/object-fields.d.ts +6 -8
  27. package/dist/schema-form/object-fields.d.ts.map +1 -1
  28. package/dist/schema-form/object-fields.js +15 -6
  29. package/dist/schema-form/object-fields.js.map +1 -1
  30. package/dist/schema-form/types.d.ts +29 -0
  31. package/dist/schema-form/types.d.ts.map +1 -1
  32. package/dist/schema-form/validate.d.ts +9 -8
  33. package/dist/schema-form/validate.d.ts.map +1 -1
  34. package/dist/schema-form/validate.js +104 -20
  35. package/dist/schema-form/validate.js.map +1 -1
  36. package/package.json +2 -2
  37. package/src/index.ts +2 -0
  38. package/src/schema-form/SchemaForm.tsx +10 -9
  39. package/src/schema-form/array-field.tsx +19 -6
  40. package/src/schema-form/date-range-field.tsx +112 -25
  41. package/src/schema-form/field-node.tsx +11 -6
  42. package/src/schema-form/index.ts +2 -1
  43. package/src/schema-form/object-fields.tsx +47 -25
  44. package/src/schema-form/types.ts +30 -0
  45. package/src/schema-form/validate.ts +121 -24
@@ -3,12 +3,14 @@
3
3
  * `string`/`format: date` properties where the END names its START in `rangeStart`;
4
4
  * the answer stays the two date strings, nothing is recombined. Where a pair is
5
5
  * drawable as one control (below), `ObjectFields` renders this {@link DateRangeField}
6
- * — a group with the two ends' labels over two native date inputs — at the START's
7
- * slot and skips the END; otherwise the two properties draw as two ordinary date
8
- * fields (a documented, valid layout).
6
+ * — a group with one column per end, each the end's labelled native date input and,
7
+ * when the end has a `timeOf` time, that time's own control under it — at the START's
8
+ * slot and skips the END and the times; otherwise the properties draw as ordinary
9
+ * fields, each time right after its date (a documented, valid layout).
9
10
  *
10
- * {@link dateRangePairs} is the ONE definition of "drawable as one control", reused
11
- * by every surface so each surface's merge decision agrees.
11
+ * {@link dateRangePairs} is the ONE definition of "drawable as one control", and
12
+ * {@link fieldDrawingOrder} the one drawing order, both reused by every surface so each
13
+ * surface's merge decision and field order agree.
12
14
  */
13
15
  import type { ReactNode } from 'react';
14
16
 
@@ -22,6 +24,59 @@ export interface DateRangePair {
22
24
  readonly startSchema: JsonSchema;
23
25
  readonly endName: string;
24
26
  readonly endSchema: JsonSchema;
27
+ /** The START's `timeOf` time property among the same properties, when it has one. */
28
+ readonly startTimeName?: string;
29
+ readonly startTimeSchema?: JsonSchema;
30
+ /** The END's `timeOf` time property among the same properties, when it has one. */
31
+ readonly endTimeName?: string;
32
+ readonly endTimeSchema?: JsonSchema;
33
+ }
34
+
35
+ /**
36
+ * `names` in drawing order: each `timeOf` time moved to sit right after the date it
37
+ * names. `names` is one page's fields, or every property of a page-less form or a
38
+ * readback, in their given order. A time moves only when the name it points at is in
39
+ * `names` and does not itself point at another listed name; any other time (its date
40
+ * not listed, a chain of times, a cycle) keeps its place. Every name appears exactly
41
+ * once and every other name keeps its relative order. Pure and idempotent, so every
42
+ * walker that orders the same list draws it the same way.
43
+ */
44
+ export function fieldDrawingOrder(
45
+ names: readonly string[],
46
+ properties: Readonly<Record<string, JsonSchema>>,
47
+ ): string[] {
48
+ const listed = new Set(names);
49
+ const targetOf = new Map<string, string>();
50
+ for (const name of names) {
51
+ const dateName = Object.hasOwn(properties, name) ? properties[name]?.timeOf : undefined;
52
+ if (typeof dateName === 'string' && dateName !== name && listed.has(dateName)) {
53
+ targetOf.set(name, dateName);
54
+ }
55
+ }
56
+ const timesOf = new Map<string, string[]>();
57
+ for (const name of names) {
58
+ const dateName = targetOf.get(name);
59
+ if (dateName === undefined || targetOf.has(dateName)) continue;
60
+ const times = timesOf.get(dateName);
61
+ if (times === undefined) timesOf.set(dateName, [name]);
62
+ else times.push(name);
63
+ }
64
+ const moved = new Set([...timesOf.values()].flat());
65
+ const ordered: string[] = [];
66
+ for (const name of names) {
67
+ if (moved.has(name)) continue;
68
+ ordered.push(name);
69
+ ordered.push(...(timesOf.get(name) ?? []));
70
+ }
71
+ return ordered;
72
+ }
73
+
74
+ /** The `timeOf` time paired with the date `dateName` among `properties`, if any. */
75
+ function pairedTime(
76
+ dateName: string,
77
+ properties: readonly (readonly [string, JsonSchema])[],
78
+ ): readonly [string, JsonSchema] | undefined {
79
+ return properties.find(([name, schema]) => schema.timeOf === dateName && name !== dateName);
25
80
  }
26
81
 
27
82
  /** A property's `visibleWhen` as a stable key for deep-equality (absent ⇒ `''`). */
@@ -40,7 +95,8 @@ function isDateString(schema: JsonSchema): boolean {
40
95
  * no OTHER field's `visibleWhen` names either end — otherwise merging the pair would
41
96
  * leave that field's predicate pointing at a control that no longer draws on its own,
42
97
  * so the pair stays two date fields (the documented degrade). `properties` is ONE
43
- * page's ordered list; a field is used in at most one pair.
98
+ * page's ordered list; a field is used in at most one pair. Each pair also names each
99
+ * end's `timeOf` time among the same properties, which draws inside the group.
44
100
  */
45
101
  export function dateRangePairs(
46
102
  properties: readonly (readonly [string, JsonSchema])[],
@@ -65,7 +121,18 @@ export function dateRangePairs(
65
121
  (schema.visibleWhen.field === startName || schema.visibleWhen.field === endName),
66
122
  );
67
123
  if (dependedOn) continue;
68
- pairs.push({ startName, startSchema, endName, endSchema });
124
+ const startTime = pairedTime(startName, properties);
125
+ const endTime = pairedTime(endName, properties);
126
+ pairs.push({
127
+ startName,
128
+ startSchema,
129
+ endName,
130
+ endSchema,
131
+ ...(startTime === undefined
132
+ ? {}
133
+ : { startTimeName: startTime[0], startTimeSchema: startTime[1] }),
134
+ ...(endTime === undefined ? {} : { endTimeName: endTime[0], endTimeSchema: endTime[1] }),
135
+ });
69
136
  paired.add(startName);
70
137
  paired.add(endName);
71
138
  }
@@ -77,7 +144,10 @@ function titleOf(schema: JsonSchema, name: string): string {
77
144
  return typeof schema.title === 'string' && schema.title.length > 0 ? schema.title : name;
78
145
  }
79
146
 
80
- /** One end of the range: a labelled native date input emitting the date string. */
147
+ /**
148
+ * One end of the range, one column: a labelled native date input emitting the date
149
+ * string, then the end's time control (when it has a `timeOf` time) under it.
150
+ */
81
151
  function RangeEnd({
82
152
  heading,
83
153
  schema,
@@ -85,6 +155,7 @@ function RangeEnd({
85
155
  required,
86
156
  error,
87
157
  onChange,
158
+ time,
88
159
  }: {
89
160
  heading: string;
90
161
  schema: JsonSchema;
@@ -92,26 +163,34 @@ function RangeEnd({
92
163
  required: boolean;
93
164
  error: string | undefined;
94
165
  onChange: (value: unknown) => void;
166
+ time: ReactNode;
95
167
  }): ReactNode {
96
168
  const current = typeof value === 'string' ? value : '';
97
169
  return (
98
- <Field label={heading} error={error}>
99
- <TextInput
100
- type="date"
101
- // The native control enforces its inclusive bounds; the validator enforces
102
- // the rest (unavailable days, the range span) that the control cannot draw.
103
- min={typeof schema.minDate === 'string' ? schema.minDate : undefined}
104
- max={typeof schema.maxDate === 'string' ? schema.maxDate : undefined}
105
- required={required}
106
- value={current}
107
- onChange={(event) => {
108
- const next = event.target.value;
109
- // An empty optional end drops its key; a required one keeps the empty
110
- // string so the validator can flag it.
111
- onChange(next === '' && !required ? undefined : next);
112
- }}
113
- />
114
- </Field>
170
+ <div className="tai-stack tai-stack-3">
171
+ <Field
172
+ label={heading}
173
+ description={typeof schema.description === 'string' ? schema.description : undefined}
174
+ error={error}
175
+ >
176
+ <TextInput
177
+ type="date"
178
+ // The native control enforces its inclusive bounds; the validator enforces
179
+ // the rest (unavailable days, the range span) that the control cannot draw.
180
+ min={typeof schema.minDate === 'string' ? schema.minDate : undefined}
181
+ max={typeof schema.maxDate === 'string' ? schema.maxDate : undefined}
182
+ required={required}
183
+ value={current}
184
+ onChange={(event) => {
185
+ const next = event.target.value;
186
+ // An empty optional end drops its key; a required one keeps the empty
187
+ // string so the validator can flag it.
188
+ onChange(next === '' && !required ? undefined : next);
189
+ }}
190
+ />
191
+ </Field>
192
+ {time}
193
+ </div>
115
194
  );
116
195
  }
117
196
 
@@ -126,6 +205,8 @@ export function DateRangeField({
126
205
  endError,
127
206
  onChangeStart,
128
207
  onChangeEnd,
208
+ startTime,
209
+ endTime,
129
210
  }: {
130
211
  readonly pair: DateRangePair;
131
212
  readonly startValue: unknown;
@@ -137,6 +218,10 @@ export function DateRangeField({
137
218
  readonly endError: string | undefined;
138
219
  readonly onChangeStart: (value: unknown) => void;
139
220
  readonly onChangeEnd: (value: unknown) => void;
221
+ /** The START's time control, drawn under its date; `null` when the end has no time. */
222
+ readonly startTime: ReactNode;
223
+ /** The END's time control, drawn under its date; `null` when the end has no time. */
224
+ readonly endTime: ReactNode;
140
225
  }): ReactNode {
141
226
  const startTitle = titleOf(pair.startSchema, pair.startName);
142
227
  const endTitle = titleOf(pair.endSchema, pair.endName);
@@ -152,6 +237,7 @@ export function DateRangeField({
152
237
  required={startRequired}
153
238
  error={startError}
154
239
  onChange={onChangeStart}
240
+ time={startTime}
155
241
  />
156
242
  <RangeEnd
157
243
  heading={mark(endTitle, endRequired)}
@@ -160,6 +246,7 @@ export function DateRangeField({
160
246
  required={endRequired}
161
247
  error={endError}
162
248
  onChange={onChangeEnd}
249
+ time={endTime}
163
250
  />
164
251
  </div>
165
252
  </Field>
@@ -21,7 +21,7 @@ import { ObjectFields } from './object-fields';
21
21
  import { RecordField } from './record-field';
22
22
  import { scalarLabel } from './resolve';
23
23
  import { StringField } from './string-field';
24
- import type { JsonSchema, SchemaFormErrors } from './types';
24
+ import type { FieldWords, JsonSchema, SchemaFormErrors } from './types';
25
25
  import { UnionField } from './union-field';
26
26
 
27
27
  interface FieldNodeProps {
@@ -43,13 +43,15 @@ interface FieldNodeProps {
43
43
  readonly errors: SchemaFormErrors | undefined;
44
44
  readonly idPrefix: string;
45
45
  /**
46
- * Per-field picker placeholders keyed by property name, handed to an OBJECT node so
47
- * its direct `enum`-select children draw their placeholder. Carried by the root form
48
- * only; nested objects receive none.
46
+ * Per-field words keyed by property name, handed to an OBJECT node so its direct
47
+ * children draw them (an `enum` select's placeholder, a list's words). Carried by the
48
+ * root form only; nested objects receive none.
49
49
  */
50
- readonly fieldWording?: Readonly<Record<string, { choiceLabel?: string; invalid?: string }>>;
50
+ readonly fieldWording?: Readonly<Record<string, FieldWords>>;
51
51
  /** This field's own picker placeholder, when it draws as an `enum` select. */
52
52
  readonly choiceLabel?: string;
53
+ /** This field's own list words, when it draws as a list. */
54
+ readonly listWords?: FieldWords;
53
55
  }
54
56
 
55
57
  function labelText(explicit: string | undefined, label: string | undefined): string {
@@ -84,6 +86,7 @@ function FieldNodeImpl(props: FieldNodeProps): ReactNode {
84
86
  idPrefix,
85
87
  fieldWording: props.fieldWording,
86
88
  choiceLabel: props.choiceLabel,
89
+ listWords: props.listWords,
87
90
  });
88
91
  }
89
92
 
@@ -101,8 +104,9 @@ interface RenderFieldArgs {
101
104
  readonly required: boolean;
102
105
  readonly errors: SchemaFormErrors | undefined;
103
106
  readonly idPrefix: string;
104
- readonly fieldWording?: Readonly<Record<string, { choiceLabel?: string; invalid?: string }>>;
107
+ readonly fieldWording?: Readonly<Record<string, FieldWords>>;
105
108
  readonly choiceLabel?: string;
109
+ readonly listWords?: FieldWords;
106
110
  }
107
111
 
108
112
  /** Render the field component matching the classified model kind. */
@@ -220,6 +224,7 @@ function renderCompositeField(
220
224
  path={path}
221
225
  errors={errors}
222
226
  idPrefix={idPrefix}
227
+ words={args.listWords}
223
228
  />
224
229
  );
225
230
  case 'object':
@@ -39,7 +39,7 @@ export type {
39
39
  export { RecordEntryRendererContext } from './context';
40
40
  export { ExpressionFieldContext } from './context';
41
41
  export type { DateRangePair } from './date-range-field';
42
- export { dateRangePairs } from './date-range-field';
42
+ export { dateRangePairs, fieldDrawingOrder } from './date-range-field';
43
43
  export { defaultValueForSchema } from './default-value';
44
44
  export { resolveRef } from './resolve';
45
45
  export type { CompletionProvider, SchemaFormProps } from './SchemaForm';
@@ -48,6 +48,7 @@ export type { SecretRef, SecretRefFieldProps } from './SecretRefField';
48
48
  export { SecretRefField } from './SecretRefField';
49
49
  export type {
50
50
  Discriminator,
51
+ FieldWords,
51
52
  FormControl,
52
53
  FormInputMode,
53
54
  JsonSchema,
@@ -7,10 +7,10 @@
7
7
  import type { ReactNode } from 'react';
8
8
 
9
9
  import { ownValue } from '../guards';
10
- import { DateRangeField, dateRangePairs } from './date-range-field';
10
+ import { DateRangeField, dateRangePairs, fieldDrawingOrder } from './date-range-field';
11
11
  import { FieldNode } from './field-node';
12
12
  import { useKeyedCallbacks } from './keyed-callbacks';
13
- import type { JsonSchema, SchemaFormErrors } from './types';
13
+ import type { FieldWords, JsonSchema, SchemaFormErrors } from './types';
14
14
  import { isFieldVisible } from './visibility';
15
15
 
16
16
  function isPlainObject(value: unknown): value is Record<string, unknown> {
@@ -42,11 +42,12 @@ export function ObjectFields({
42
42
  /** Mark each required property's label with the DS required marker. */
43
43
  markRequired?: boolean;
44
44
  /**
45
- * Per-property picker placeholders, keyed by property name. Handed to the root form's
46
- * object only; a property whose `enum` draws as a select shows its entry's
47
- * `choiceLabel` as the select placeholder. Absent for nested objects.
45
+ * Per-property words, keyed by property name. Handed to the root form's object only;
46
+ * a property whose `enum` draws as a select shows its entry's `choiceLabel` as the
47
+ * select placeholder, and a list property draws its entry's list words. Absent for
48
+ * nested objects.
48
49
  */
49
- fieldWording?: Readonly<Record<string, { choiceLabel?: string; invalid?: string }>>;
50
+ fieldWording?: Readonly<Record<string, FieldWords>>;
50
51
  }): ReactNode {
51
52
  const obj = isPlainObject(value) ? value : {};
52
53
  const changeFor = useKeyedCallbacks<string, unknown>(
@@ -58,15 +59,49 @@ export function ObjectFields({
58
59
  // A drawable date-range pair renders as ONE DateRangeField at the START's slot; the
59
60
  // END is skipped. The pairing is `dateRangePairs`, the one definition every surface
60
61
  // shares. Properties here are one page's, so the "same page" rule holds by construction.
62
+ // A pair's `timeOf` times draw inside its group, so they are skipped too. The fields
63
+ // walk in `fieldDrawingOrder`: each time right after its date, wherever it is declared.
61
64
  const pairs = dateRangePairs(properties);
62
65
  const pairByStart = new Map(pairs.map((pair) => [pair.startName, pair]));
63
- const skippedEnds = new Set(pairs.map((pair) => pair.endName));
66
+ const skipped = new Set(
67
+ pairs.flatMap((pair) =>
68
+ [pair.endName, pair.startTimeName, pair.endTimeName].filter(
69
+ (name): name is string => name !== undefined,
70
+ ),
71
+ ),
72
+ );
73
+ const schemaByName = new Map(properties);
74
+ const drawn = fieldDrawingOrder(
75
+ properties.map(([name]) => name),
76
+ Object.fromEntries(properties),
77
+ );
64
78
  const childPathOf = (name: string): string => (path === '' ? name : `${path}.${name}`);
79
+ const fieldOf = (name: string, propSchema: JsonSchema): ReactNode => (
80
+ <FieldNode
81
+ key={name}
82
+ schema={propSchema}
83
+ root={root}
84
+ value={ownValue(obj, name)}
85
+ onChange={changeFor(name)}
86
+ path={childPathOf(name)}
87
+ label={name}
88
+ required={requiredKeys.has(name)}
89
+ markRequired={markRequired}
90
+ errors={errors}
91
+ idPrefix={idPrefix}
92
+ choiceLabel={ownValue(fieldWording, name)?.choiceLabel}
93
+ listWords={ownValue(fieldWording, name)}
94
+ />
95
+ );
96
+ const timeOf = (name: string | undefined, propSchema: JsonSchema | undefined): ReactNode =>
97
+ name === undefined || propSchema === undefined ? null : fieldOf(name, propSchema);
65
98
  return (
66
99
  <>
67
- {properties.map(([name, propSchema]) => {
100
+ {drawn.map((name) => {
101
+ const propSchema = schemaByName.get(name);
102
+ if (propSchema === undefined) return null;
68
103
  if (skip !== undefined && name === skip) return null;
69
- if (skippedEnds.has(name)) return null;
104
+ if (skipped.has(name)) return null;
70
105
  // A field hidden by its `visibleWhen` predicate is not rendered; the
71
106
  // validator skips it in step, so a hidden field is neither shown nor required.
72
107
  if (!isFieldVisible(propSchema, obj)) return null;
@@ -85,25 +120,12 @@ export function ObjectFields({
85
120
  endError={ownValue(errors, childPathOf(pair.endName))}
86
121
  onChangeStart={changeFor(pair.startName)}
87
122
  onChangeEnd={changeFor(pair.endName)}
123
+ startTime={timeOf(pair.startTimeName, pair.startTimeSchema)}
124
+ endTime={timeOf(pair.endTimeName, pair.endTimeSchema)}
88
125
  />
89
126
  );
90
127
  }
91
- return (
92
- <FieldNode
93
- key={name}
94
- schema={propSchema}
95
- root={root}
96
- value={ownValue(obj, name)}
97
- onChange={changeFor(name)}
98
- path={childPathOf(name)}
99
- label={name}
100
- required={requiredKeys.has(name)}
101
- markRequired={markRequired}
102
- errors={errors}
103
- idPrefix={idPrefix}
104
- choiceLabel={fieldWording?.[name]?.choiceLabel}
105
- />
106
- );
128
+ return fieldOf(name, propSchema);
107
129
  })}
108
130
  </>
109
131
  );
@@ -115,6 +115,13 @@ export interface JsonSchema {
115
115
  readonly rangeStart?: string;
116
116
  readonly minDays?: number;
117
117
  readonly maxDays?: number;
118
+ /**
119
+ * On a `string`/`format: time` property: the name of the range-end `date` property this
120
+ * is the time of. The time draws right after its date (inside the range group when the
121
+ * pair merges), and two ends on the same date are then ordered by their times. Absent =
122
+ * a time field of its own.
123
+ */
124
+ readonly timeOf?: string;
118
125
  /**
119
126
  * A declarative visibility predicate on a property: the field shows only when the
120
127
  * predicate holds against the current form values. Static form description evaluated by
@@ -146,3 +153,26 @@ export interface VisibleWhen {
146
153
  * each entry is a loud, human-readable message. Empty object = valid.
147
154
  */
148
155
  export type SchemaFormErrors = Readonly<Record<string, string>>;
156
+
157
+ /**
158
+ * A form field's own words, keyed by the root object's property name in a form's
159
+ * `fieldWording`. Every member is optional; an unset one draws the SDK's own word.
160
+ *
161
+ * - `choiceLabel`: the placeholder of the field's `enum` select (in place of `Select…`).
162
+ * - `invalid`: the sentence that replaces every shape error on the field and anything
163
+ * inside it (a format, pattern, length, bound, type or item error).
164
+ * - `required`: the sentence shown when the field's required value is missing, at the
165
+ * field or anywhere inside it (in place of `"<name>" is required`).
166
+ * - `addItem` / `removeItem` / `itemLabel` / `noItems`: a list field's add button, each
167
+ * item's remove button, each item's heading (its `{n}` token takes the item's number),
168
+ * and the line shown while the list is empty.
169
+ */
170
+ export interface FieldWords {
171
+ readonly choiceLabel?: string;
172
+ readonly invalid?: string;
173
+ readonly required?: string;
174
+ readonly addItem?: string;
175
+ readonly removeItem?: string;
176
+ readonly itemLabel?: string;
177
+ readonly noItems?: string;
178
+ }