diff --git a/.changeset/18177-bulk-action-param-strict.md b/.changeset/18177-bulk-action-param-strict.md new file mode 100644 index 00000000000..be93c659d9d --- /dev/null +++ b/.changeset/18177-bulk-action-param-strict.md @@ -0,0 +1,67 @@ +--- +'@objectstack/spec': minor +--- + +**BREAKING for authored metadata** — `BulkActionParamSchema` is strict, matching its single-record twin `ActionParamSchema`, and declares `dependsOn` (#18177, decision batch #146 item 4, letter A). + +Clause-②: yes (narrowing) + + + +A list view's `bulkActionDefs[].params[]` entry was `.passthrough()`, so **the shape examined nothing** — and that is the whole finding, not the framing. Measured against installed spec 17.4.0, three parses per schema in one process: + +| | positive control (minimal valid) | negative control (nonsense key) | subject (`dependsOn`) | +| --- | --- | --- | --- | +| `BulkActionParamSchema` | parses | **ACCEPTED** | accepted | +| `ActionParamSchema` | parses | refused `unrecognized_keys` | refused `unrecognized_keys` | + +It accepted `zzz_nonsense_key_that_no_producer_emits_8755` in the **same run** that it accepted `dependsOn`. ⇒ "the bulk schema accepts it" was never evidence that a key was licensed, in either direction: a shape that examines nothing can neither authorise `dependsOn` nor refuse a typo. Both control legs are now pinned in `src/ui/bulk-action.test.ts` in their post-close form, together, so a future re-opening of the shape cannot pass as a green `dependsOn` assertion. + +The maintainer's ruling: 「Breaking for authored metadata」, one-shot — no grace window, no dual spelling. + +### `dependsOn` is DECLARED, not refused — and needs no edit + +It was already live on this surface and the renderer honours it, so this half is a contract catching up with behaviour. `bulkParamToField` does not destructure it out, so it rides the adapter's spread onto the field metadata, where **both** widget families read it: the option family (`SelectField` / `MultiSelectField` / `RadioField` / `CheckboxesField`) gates and refreshes the offered set through `useCascadingOptions`, and the reference-bearing pickers (`LookupField`, and `UserField` through it) lower it into a hard candidate filter. Retiring it was measured off the table — an ablation removing it from that spread reddens 7 of 12 cases in the consuming repo. + +Shape and description mirror **`FieldSchema.dependsOn`**, which is the single-record twin *for this key*: `ActionParamSchema` declares no `dependsOn` at all, because the single-record dialog reaches it through the field-backed route this surface does not have. One vocabulary, two doors. + +```ts +params: [ + { name: 'account', type: 'lookup', object: 'showcase_account' }, + { name: 'contact', type: 'lookup', object: 'showcase_contact', dependsOn: ['account'] }, + { name: 'owner', type: 'lookup', object: 'sys_user', + dependsOn: [{ field: 'account', param: 'account_id' }] }, // remote key differs +] +``` + +On a bulk param the "record" a binding resolves against is the dialog's own in-progress param values — a bulk run holds a selection, not a row — so a binding names a **sibling param of the same def**. + +### Migration — FROM → TO + +Every rejection names the surface, echoes the key and carries its own fix. Nothing below is mechanical, which is why this registers as an ADR-0087 **D3 structured TODO** rather than a D2 conversion: an arbitrary unknown key has no mapping target, and deleting it automatically is the silent data loss ADR-0078 bans. + +| You wrote on a bulk param | Write instead | +| --- | --- | +| `helpText: '…'` | `help: '…'` | +| `defaultValue: x` | `default: x` | +| `reference: 'sys_user'` | `object: 'sys_user'` | +| `displayField: 'name'` | `labelField: 'name'` | +| `field: 'owner'` (field-backed param) | declare it inline — `name` + `type`, plus `object` for a picker. The bulk surface has no field-backed route: `resolveActionParams` consults the object's field definitions for the single-record dialog, `toBulkParam` never does | +| `visible: '…'` on the param | move the predicate to the DEF (`bulkActionDefs[].visible`), which gates the button and narrows the run per record | +| `visibleWhen: '…'` on the param | it is a per-**option** key — write it inside `options[]` | +| `carryOver` / `defaultFromRow` / `requiresFeature` / `objectOverride` | ACTION-param contracts with no bulk equivalent: a bulk dialog runs over a selection and holds no row. Use `default` for a fixed prefill, or the def's `patch` for a value the user must not see; gate the button with the def's `visible` / `requiredPermissions` | +| `min` / `max` / `step` / `precision` / `scale` / `rows` / `accept` / `maxSize`, or the picker knobs `lookupFilters` / `lookupColumns` / `lookupPageSize` / `descriptionField` / `picker` / `subtitle` / `avatarField` / `idField` / `allowCreate` | remove the key — see the warning below | + +### ⚠️ The widget-config family really was honoured, and really is refused now + +This is the half of the narrowing that costs something, so it is stated rather than buried. Those keys rode the same `...extra` spread `dependsOn` rides, and whichever widget read one honoured it (`min`/`max`/`step` at NumberField / SliderField / CurrencyField / PercentField, `accept`/`maxSize` at FileField / ImageField, `rows` at TextAreaField / RichTextField, the picker knobs at LookupField). They are refused now, with one prescription naming `FieldSchema` as the shape they are real on. + +⛔ **Do not read that prescription as "declare it on the object's field instead"** — the bulk surface has no field-backed param route, so the value does not reach this dialog either. If a bulk param genuinely needs one of these keys, it has to be declared on `BulkActionParamSchema`; open an issue rather than working around it. They were not declared here because the census below found no author writing one, and a declared key is published contract whose removal costs a full retirement. + +**Census, with its boundary.** Taken at authoring time over the two repositories reachable from that session: `objectstack@176b03582e` (7 authored bulk-param literals) and `objectui@3e4f6324f7` (3) — **zero** carrying a key this shape does not declare. ⚠️ **hotcrm was NOT REACHABLE and is UNMEASURED, not clean.** If you keep your own metadata corpus, run `objectstack validate` before upgrading rather than inheriting this result. + +### What is deliberately NOT closed + +`params[].options[]` stays `.passthrough()`, on its own measurement rather than by symmetry with its parent: `bulkParamToField` spreads every option entry into the field metadata, and the option widgets read `color` / `icon` / `disabled` / `visibleWhen` beyond the declared `{ label, value }`. Closing it would delete widget config the renderer honours — the exact defect this change closes one level up. The declared pair is still type-checked. + +⛔ No renderer is edited and no key is removed from any other shape. `BulkActionDefSchema` was already strict and is untouched. diff --git a/content/docs/references/ui/bulk-action.mdx b/content/docs/references/ui/bulk-action.mdx index b57089eed28..3573a120c21 100644 --- a/content/docs/references/ui/bulk-action.mdx +++ b/content/docs/references/ui/bulk-action.mdx @@ -46,7 +46,7 @@ const result = BulkActionDefSchema.parse(data); | **operation** | `Enum<'update' \| 'delete' \| 'custom'>` | ✅ | What the executor does: 'update'/'delete' are data-plane mass mutations; 'custom' dispatches an object action (see `execution`). | | **execution** | `Enum<'perRecord' \| 'aggregate'>` | optional | For `operation: 'custom'` — 'aggregate' dispatches the named action ONCE for the whole selection, carrying every id in `params._selectedIds`. Required on a custom def: the per-record form is declared as `bulkActions: ['']` instead. | | **patch** | `Record` | optional | For `operation: 'update'` — static field values applied to every selected record, merged UNDER the user-supplied params so a fixed value can be declared without exposing it in the dialog. | -| **params** | `({ name: string; label?: string; help?: string; type: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; … } & Record)[]` | optional | Inputs collected once before the run. Omit to skip the params step and go straight to confirm. | +| **params** | `{ name: string; label?: string; help?: string; type: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; … }[]` | optional | Inputs collected once before the run. Omit to skip the params step and go straight to confirm. | | **confirmText** | `string` | optional | Confirmation text shown above the affected-record summary. | | **confirmLabel** | `string` | optional | Custom Confirm button label (default: "Run"). | | **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Eligibility predicate (CEL) — a string or a `{dialect, source}` envelope, i.e. `action.visible` without its boolean-literal arm: a per-record predicate has nothing to say as a constant. Evaluated once PER SELECTED RECORD with that record bound: the button is offered when at least one passes, the run covers only those, and the rest are reported as skipped. A record-free predicate (`features.x`, `current_user.y`) therefore behaves as a plain button-level gate. Fail-closed — a predicate that faults excludes the record. | @@ -69,6 +69,7 @@ const result = BulkActionDefSchema.parse(data); | **labelField** | `string` | optional | Related-object field used as the option label for a `lookup` widget (defaults to name/full_name/email/id). | | **multiple** | `boolean` | optional | Allow picking multiple values — the param value becomes an array and is written to the patch as-is. | | **placeholder** | `string` | optional | Placeholder text. | +| **dependsOn** | `(string \| { field: string; param?: string })[]` | optional | Declares that this param's available values depend on the value of other field(s) on the same record — the form gates the field until they are set and re-evaluates as they change. For `lookup`/`master_detail` it scopes the candidate query (string = same local/remote key; `{field,param}` when the remote filter key differs — the `{field,param}` form is lookup-only). For `select`/`multiselect`/`radio` the actual per-option rule lives in each option's `visibleWhen`; list the referenced fields here (string form) so the option list gates and refreshes with the parent. On a BULK param the record is the dialog's own in-progress param values — a bulk run holds a selection, not a row — so a binding names a SIBLING PARAM of the same def. | --- @@ -111,6 +112,7 @@ const result = BulkActionDefSchema.parse(data); | **labelField** | `string` | optional | Related-object field used as the option label for a `lookup` widget (defaults to name/full_name/email/id). | | **multiple** | `boolean` | optional | Allow picking multiple values — the param value becomes an array and is written to the patch as-is. | | **placeholder** | `string` | optional | Placeholder text. | +| **dependsOn** | `(string \| { field: string; param?: string })[]` | optional | Declares that this param's available values depend on the value of other field(s) on the same record — the form gates the field until they are set and re-evaluates as they change. For `lookup`/`master_detail` it scopes the candidate query (string = same local/remote key; `{field,param}` when the remote filter key differs — the `{field,param}` form is lookup-only). For `select`/`multiselect`/`radio` the actual per-option rule lives in each option's `visibleWhen`; list the referenced fields here (string form) so the option list gates and refreshes with the parent. On a BULK param the record is the dialog's own in-progress param values — a bulk run holds a selection, not a row — so a binding names a SIBLING PARAM of the same def. | ### Allowed Values: `BulkActionParam.type` diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index 00d53fa03c0..db7bad4b46f 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -1066,7 +1066,7 @@ View filter rule | **operation** | `Enum<'update' \| 'delete' \| 'custom'>` | ✅ | What the executor does: 'update'/'delete' are data-plane mass mutations; 'custom' dispatches an object action (see `execution`). | | **execution** | `Enum<'perRecord' \| 'aggregate'>` | optional | For `operation: 'custom'` — 'aggregate' dispatches the named action ONCE for the whole selection, carrying every id in `params._selectedIds`. Required on a custom def: the per-record form is declared as `bulkActions: ['']` instead. | | **patch** | `Record` | optional | For `operation: 'update'` — static field values applied to every selected record, merged UNDER the user-supplied params so a fixed value can be declared without exposing it in the dialog. | -| **params** | `({ name: string; label?: string; help?: string; type: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; … } & Record)[]` | optional | Inputs collected once before the run. Omit to skip the params step and go straight to confirm. | +| **params** | `{ name: string; label?: string; help?: string; type: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; … }[]` | optional | Inputs collected once before the run. Omit to skip the params step and go straight to confirm. | | **confirmText** | `string` | optional | Confirmation text shown above the affected-record summary. | | **confirmLabel** | `string` | optional | Custom Confirm button label (default: "Run"). | | **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Eligibility predicate (CEL) — a string or a `{dialect, source}` envelope, i.e. `action.visible` without its boolean-literal arm: a per-record predicate has nothing to say as a constant. Evaluated once PER SELECTED RECORD with that record bound: the button is offered when at least one passes, the run covers only those, and the rest are reported as skipped. A record-free predicate (`features.x`, `current_user.y`) therefore behaves as a plain button-level gate. Fail-closed — a predicate that faults excludes the record. | @@ -1465,7 +1465,7 @@ View filter rule | **operation** | `Enum<'update' \| 'delete' \| 'custom'>` | ✅ | What the executor does: 'update'/'delete' are data-plane mass mutations; 'custom' dispatches an object action (see `execution`). | | **execution** | `Enum<'perRecord' \| 'aggregate'>` | optional | For `operation: 'custom'` — 'aggregate' dispatches the named action ONCE for the whole selection, carrying every id in `params._selectedIds`. Required on a custom def: the per-record form is declared as `bulkActions: ['']` instead. | | **patch** | `Record` | optional | For `operation: 'update'` — static field values applied to every selected record, merged UNDER the user-supplied params so a fixed value can be declared without exposing it in the dialog. | -| **params** | `({ name: string; label?: string; help?: string; type: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; … } & Record)[]` | optional | Inputs collected once before the run. Omit to skip the params step and go straight to confirm. | +| **params** | `{ name: string; label?: string; help?: string; type: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; … }[]` | optional | Inputs collected once before the run. Omit to skip the params step and go straight to confirm. | | **confirmText** | `string` | optional | Confirmation text shown above the affected-record summary. | | **confirmLabel** | `string` | optional | Custom Confirm button label (default: "Run"). | | **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Eligibility predicate (CEL) — a string or a `{dialect, source}` envelope, i.e. `action.visible` without its boolean-literal arm: a per-record predicate has nothing to say as a constant. Evaluated once PER SELECTED RECORD with that record bound: the button is offered when at least one passes, the run covers only those, and the rest are reported as skipped. A record-free predicate (`features.x`, `current_user.y`) therefore behaves as a plain button-level gate. Fail-closed — a predicate that faults excludes the record. | diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md index 7fba48d5b2c..e24cfcb43ec 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md @@ -21,7 +21,7 @@ regenerate. | Measure | Value | |---|---| | Triaged directories | 5 | -| Object sites in them | 449 | +| Object sites in them | 450 | | Still-open (strip) sites | 126 | | Files carrying at least one | 22 | @@ -44,12 +44,12 @@ The `strict` column is the one the campaign schedules against; it counts both th | Dir | Sites | strict | passthrough | catchall | strip | |---|---|---|---|---|---| -| `ui/` | 175 | 165 | 3 | 0 | 7 | +| `ui/` | 176 | 167 | 2 | 0 | 7 | | `data/` | 159 | 76 | 1 | 0 | 82 | | `automation/` | 68 | 43 | 0 | 1 | 24 | | `security/` | 20 | 7 | 0 | 0 | 13 | | `studio/` | 27 | 27 | 0 | 0 | 0 | -| **total** | **449** | **318** | **4** | **1** | **126** | +| **total** | **450** | **320** | **3** | **1** | **126** | ## File-level triage — site counts @@ -64,7 +64,7 @@ classify and is not listed (it becomes reportable the day it grows its first sit | `action-params.zod.ts` | 1 | | `action.zod.ts` | 9 | | `app.zod.ts` | 18 | -| `bulk-action.zod.ts` | 3 | +| `bulk-action.zod.ts` | 4 | | `chart.zod.ts` | 8 | | `component.zod.ts` | 46 | | `dashboard.zod.ts` | 11 | @@ -76,7 +76,7 @@ classify and is not listed (it becomes reportable the day it grows its first sit | `sharing.zod.ts` | 1 | | `view.zod.ts` | 61 | | `widget.zod.ts` | 1 | -| **total** | **175** | +| **total** | **176** | ### `data/` — sites @@ -155,7 +155,7 @@ over it is here. ### `ui/` — open -**7 strip of 175**, in 4 file(s). +**7 strip of 176**, in 4 file(s). | File | Strip | Sites | |---|---|---| @@ -163,7 +163,7 @@ over it is here. | `app.zod.ts` | 1 | 18 | | `view.zod.ts` | 4 | 61 | | `widget.zod.ts` | 1 | 1 | -| **total** | **7** | **175** | +| **total** | **7** | **176** | | Bucket | Sites | |---|---| diff --git a/packages/spec/api-surface-declarations/api.txt b/packages/spec/api-surface-declarations/api.txt index a24ecbade42..438146119de 100644 --- a/packages/spec/api-surface-declarations/api.txt +++ b/packages/spec/api-surface-declarations/api.txt @@ -10380,7 +10380,11 @@ declare const GetUiViewResponseSchema: z.ZodObject<{ labelField: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "date" | "record" | "file" | "code" | "email" | "url" | "datetime" | "time" | "formula" | "text" | "textarea" | "phone" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "user" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "location" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "tags" | "vector"; label?: string | undefined; @@ -20282,6 +20293,10 @@ declare const ObjectDefinitionResponseSchema: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -21420,7 +21435,6 @@ declare const ObjectDefinitionResponseSchema: z.ZodObject<{ execution?: "aggregate" | "perRecord" | undefined; patch?: Record | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "date" | "record" | "file" | "code" | "email" | "url" | "datetime" | "time" | "formula" | "text" | "textarea" | "phone" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "user" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "location" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "tags" | "vector"; label?: string | undefined; @@ -21436,6 +21450,10 @@ declare const ObjectDefinitionResponseSchema: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -22575,7 +22593,6 @@ declare const ObjectDefinitionResponseSchema: z.ZodObject<{ execution?: "aggregate" | "perRecord" | undefined; patch?: Record | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "date" | "record" | "file" | "code" | "email" | "url" | "datetime" | "time" | "formula" | "text" | "textarea" | "phone" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "user" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "location" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "tags" | "vector"; label?: string | undefined; @@ -22591,6 +22608,10 @@ declare const ObjectDefinitionResponseSchema: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -23729,7 +23750,6 @@ declare const ObjectDefinitionResponseSchema: z.ZodObject<{ execution?: "aggregate" | "perRecord" | undefined; patch?: Record | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "date" | "record" | "file" | "code" | "email" | "url" | "datetime" | "time" | "formula" | "text" | "textarea" | "phone" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "user" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "location" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "tags" | "vector"; label?: string | undefined; @@ -23745,6 +23765,10 @@ declare const ObjectDefinitionResponseSchema: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -25390,7 +25414,11 @@ declare const ObjectDefinitionResponseSchema: z.ZodObject<{ labelField: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional, never>; }; + params: { [I_1 in keyof Ps]: Ps[I_1] & Record, never>; }; } : T_1 : never : never; }; } : unknown)) => Omit & Pick; }; diff --git a/packages/spec/api-surface-declarations/data.txt b/packages/spec/api-surface-declarations/data.txt index 2b387010e92..96f867a2a6f 100644 --- a/packages/spec/api-surface-declarations/data.txt +++ b/packages/spec/api-surface-declarations/data.txt @@ -16936,7 +16936,11 @@ declare const ObjectSchema: z.ZodObject<{ labelField: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "date" | "record" | "file" | "code" | "url" | "email" | "datetime" | "time" | "formula" | "text" | "textarea" | "phone" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "user" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "location" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "tags" | "vector"; label?: string | undefined; @@ -19187,6 +19190,10 @@ declare const ObjectSchema: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -20326,7 +20333,6 @@ declare const ObjectSchema: z.ZodObject<{ execution?: "aggregate" | "perRecord" | undefined; patch?: Record | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "date" | "record" | "file" | "code" | "url" | "email" | "datetime" | "time" | "formula" | "text" | "textarea" | "phone" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "user" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "location" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "tags" | "vector"; label?: string | undefined; @@ -20342,6 +20348,10 @@ declare const ObjectSchema: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; diff --git a/packages/spec/api-surface-declarations/root.txt b/packages/spec/api-surface-declarations/root.txt index f9464b07d8d..a7da07dc55c 100644 --- a/packages/spec/api-surface-declarations/root.txt +++ b/packages/spec/api-surface-declarations/root.txt @@ -2277,7 +2277,11 @@ declare const ObjectStackDefinitionSchema: z.ZodObject<{ labelField: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "date" | "record" | "file" | "code" | "url" | "email" | "datetime" | "time" | "formula" | "text" | "textarea" | "phone" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "user" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "location" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "tags" | "vector"; label?: string | undefined; @@ -4523,6 +4526,10 @@ declare const ObjectStackDefinitionSchema: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -5661,7 +5668,6 @@ declare const ObjectStackDefinitionSchema: z.ZodObject<{ execution?: "aggregate" | "perRecord" | undefined; patch?: Record | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "date" | "record" | "file" | "code" | "url" | "email" | "datetime" | "time" | "formula" | "text" | "textarea" | "phone" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "user" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "location" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "tags" | "vector"; label?: string | undefined; @@ -5677,6 +5683,10 @@ declare const ObjectStackDefinitionSchema: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -6816,7 +6826,6 @@ declare const ObjectStackDefinitionSchema: z.ZodObject<{ execution?: "aggregate" | "perRecord" | undefined; patch?: Record | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "date" | "record" | "file" | "code" | "url" | "email" | "datetime" | "time" | "formula" | "text" | "textarea" | "phone" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "user" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "location" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "tags" | "vector"; label?: string | undefined; @@ -6832,6 +6841,10 @@ declare const ObjectStackDefinitionSchema: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -7970,7 +7983,6 @@ declare const ObjectStackDefinitionSchema: z.ZodObject<{ execution?: "aggregate" | "perRecord" | undefined; patch?: Record | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "date" | "record" | "file" | "code" | "url" | "email" | "datetime" | "time" | "formula" | "text" | "textarea" | "phone" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "user" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "location" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "tags" | "vector"; label?: string | undefined; @@ -7986,6 +7998,10 @@ declare const ObjectStackDefinitionSchema: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -9470,7 +9486,11 @@ declare const ObjectStackDefinitionSchema: z.ZodObject<{ labelField: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional, never>; }; + params: { [I_1 in keyof Ps]: Ps[I_1] & Record, never>; }; } : T_1 : never : never; }; } : unknown)) => Omit & Pick; }>>; @@ -11890,7 +11910,11 @@ declare const ObjectStackDefinitionSchema: z.ZodObject<{ labelField: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "date" | "record" | "file" | "code" | "url" | "email" | "datetime" | "time" | "formula" | "text" | "textarea" | "phone" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "user" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "location" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "tags" | "vector"; label?: string | undefined; @@ -26370,6 +26401,10 @@ declare const ObjectStackSchema: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -27508,7 +27543,6 @@ declare const ObjectStackSchema: z.ZodObject<{ execution?: "aggregate" | "perRecord" | undefined; patch?: Record | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "date" | "record" | "file" | "code" | "url" | "email" | "datetime" | "time" | "formula" | "text" | "textarea" | "phone" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "user" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "location" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "tags" | "vector"; label?: string | undefined; @@ -27524,6 +27558,10 @@ declare const ObjectStackSchema: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -28663,7 +28701,6 @@ declare const ObjectStackSchema: z.ZodObject<{ execution?: "aggregate" | "perRecord" | undefined; patch?: Record | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "date" | "record" | "file" | "code" | "url" | "email" | "datetime" | "time" | "formula" | "text" | "textarea" | "phone" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "user" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "location" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "tags" | "vector"; label?: string | undefined; @@ -28679,6 +28716,10 @@ declare const ObjectStackSchema: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -29817,7 +29858,6 @@ declare const ObjectStackSchema: z.ZodObject<{ execution?: "aggregate" | "perRecord" | undefined; patch?: Record | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "date" | "record" | "file" | "code" | "url" | "email" | "datetime" | "time" | "formula" | "text" | "textarea" | "phone" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "user" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "location" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "tags" | "vector"; label?: string | undefined; @@ -29833,6 +29873,10 @@ declare const ObjectStackSchema: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -31317,7 +31361,11 @@ declare const ObjectStackSchema: z.ZodObject<{ labelField: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional, never>; }; + params: { [I_1 in keyof Ps]: Ps[I_1] & Record, never>; }; } : T_1 : never : never; }; } : unknown)) => Omit & Pick; }>>; @@ -33737,7 +33785,11 @@ declare const ObjectStackSchema: z.ZodObject<{ labelField: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "code" | "date" | "record" | "file" | "tags" | "email" | "phone" | "user" | "datetime" | "location" | "text" | "url" | "time" | "formula" | "textarea" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "vector"; label?: string | undefined; @@ -5439,6 +5442,10 @@ declare const ChangeSetSchema: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -6577,7 +6584,6 @@ declare const ChangeSetSchema: z.ZodObject<{ execution?: "aggregate" | "perRecord" | undefined; patch?: Record | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "code" | "date" | "record" | "file" | "tags" | "email" | "phone" | "user" | "datetime" | "location" | "text" | "url" | "time" | "formula" | "textarea" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "vector"; label?: string | undefined; @@ -6593,6 +6599,10 @@ declare const ChangeSetSchema: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -7732,7 +7742,6 @@ declare const ChangeSetSchema: z.ZodObject<{ execution?: "aggregate" | "perRecord" | undefined; patch?: Record | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "code" | "date" | "record" | "file" | "tags" | "email" | "phone" | "user" | "datetime" | "location" | "text" | "url" | "time" | "formula" | "textarea" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "vector"; label?: string | undefined; @@ -7748,6 +7757,10 @@ declare const ChangeSetSchema: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -8886,7 +8899,6 @@ declare const ChangeSetSchema: z.ZodObject<{ execution?: "aggregate" | "perRecord" | undefined; patch?: Record | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "code" | "date" | "record" | "file" | "tags" | "email" | "phone" | "user" | "datetime" | "location" | "text" | "url" | "time" | "formula" | "textarea" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "vector"; label?: string | undefined; @@ -8902,6 +8914,10 @@ declare const ChangeSetSchema: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -10557,7 +10573,11 @@ declare const ChangeSetSchema: z.ZodObject<{ labelField: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional, never>; }; + params: { [I_1 in keyof Ps]: Ps[I_1] & Record, never>; }; } : T_1 : never : never; }; } : unknown)) => Omit & Pick; }; @@ -13556,7 +13576,11 @@ declare const ChangeSetSchema: z.ZodObject<{ labelField: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "code" | "date" | "record" | "file" | "tags" | "email" | "phone" | "user" | "datetime" | "location" | "text" | "url" | "time" | "formula" | "textarea" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "vector"; label?: string | undefined; @@ -15631,6 +15654,10 @@ declare const ChangeSetSchema: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -16769,7 +16796,6 @@ declare const ChangeSetSchema: z.ZodObject<{ execution?: "aggregate" | "perRecord" | undefined; patch?: Record | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "code" | "date" | "record" | "file" | "tags" | "email" | "phone" | "user" | "datetime" | "location" | "text" | "url" | "time" | "formula" | "textarea" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "vector"; label?: string | undefined; @@ -16785,6 +16811,10 @@ declare const ChangeSetSchema: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -17924,7 +17954,6 @@ declare const ChangeSetSchema: z.ZodObject<{ execution?: "aggregate" | "perRecord" | undefined; patch?: Record | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "code" | "date" | "record" | "file" | "tags" | "email" | "phone" | "user" | "datetime" | "location" | "text" | "url" | "time" | "formula" | "textarea" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "vector"; label?: string | undefined; @@ -17940,6 +17969,10 @@ declare const ChangeSetSchema: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -19078,7 +19111,6 @@ declare const ChangeSetSchema: z.ZodObject<{ execution?: "aggregate" | "perRecord" | undefined; patch?: Record | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "code" | "date" | "record" | "file" | "tags" | "email" | "phone" | "user" | "datetime" | "location" | "text" | "url" | "time" | "formula" | "textarea" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "vector"; label?: string | undefined; @@ -19094,6 +19126,10 @@ declare const ChangeSetSchema: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -20749,7 +20785,11 @@ declare const ChangeSetSchema: z.ZodObject<{ labelField: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional, never>; }; + params: { [I_1 in keyof Ps]: Ps[I_1] & Record, never>; }; } : T_1 : never : never; }; } : unknown)) => Omit & Pick; }; @@ -23711,7 +23751,11 @@ declare const CreateObjectOperation: z.ZodObject<{ labelField: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "code" | "date" | "record" | "file" | "tags" | "email" | "phone" | "user" | "datetime" | "location" | "text" | "url" | "time" | "formula" | "textarea" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "vector"; label?: string | undefined; @@ -25786,6 +25829,10 @@ declare const CreateObjectOperation: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -26924,7 +26971,6 @@ declare const CreateObjectOperation: z.ZodObject<{ execution?: "aggregate" | "perRecord" | undefined; patch?: Record | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "code" | "date" | "record" | "file" | "tags" | "email" | "phone" | "user" | "datetime" | "location" | "text" | "url" | "time" | "formula" | "textarea" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "vector"; label?: string | undefined; @@ -26940,6 +26986,10 @@ declare const CreateObjectOperation: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -28079,7 +28129,6 @@ declare const CreateObjectOperation: z.ZodObject<{ execution?: "aggregate" | "perRecord" | undefined; patch?: Record | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "code" | "date" | "record" | "file" | "tags" | "email" | "phone" | "user" | "datetime" | "location" | "text" | "url" | "time" | "formula" | "textarea" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "vector"; label?: string | undefined; @@ -28095,6 +28144,10 @@ declare const CreateObjectOperation: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -29233,7 +29286,6 @@ declare const CreateObjectOperation: z.ZodObject<{ execution?: "aggregate" | "perRecord" | undefined; patch?: Record | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "code" | "date" | "record" | "file" | "tags" | "email" | "phone" | "user" | "datetime" | "location" | "text" | "url" | "time" | "formula" | "textarea" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "vector"; label?: string | undefined; @@ -29249,6 +29301,10 @@ declare const CreateObjectOperation: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -30904,7 +30960,11 @@ declare const CreateObjectOperation: z.ZodObject<{ labelField: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional, never>; }; + params: { [I_1 in keyof Ps]: Ps[I_1] & Record, never>; }; } : T_1 : never : never; }; } : unknown)) => Omit & Pick; }; @@ -34637,7 +34697,11 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ labelField: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "code" | "date" | "record" | "file" | "tags" | "email" | "phone" | "user" | "datetime" | "location" | "text" | "url" | "time" | "formula" | "textarea" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "vector"; label?: string | undefined; @@ -36712,6 +36775,10 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -37850,7 +37917,6 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ execution?: "aggregate" | "perRecord" | undefined; patch?: Record | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "code" | "date" | "record" | "file" | "tags" | "email" | "phone" | "user" | "datetime" | "location" | "text" | "url" | "time" | "formula" | "textarea" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "vector"; label?: string | undefined; @@ -37866,6 +37932,10 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -39005,7 +39075,6 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ execution?: "aggregate" | "perRecord" | undefined; patch?: Record | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "code" | "date" | "record" | "file" | "tags" | "email" | "phone" | "user" | "datetime" | "location" | "text" | "url" | "time" | "formula" | "textarea" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "vector"; label?: string | undefined; @@ -39021,6 +39090,10 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -40159,7 +40232,6 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ execution?: "aggregate" | "perRecord" | undefined; patch?: Record | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "code" | "date" | "record" | "file" | "tags" | "email" | "phone" | "user" | "datetime" | "location" | "text" | "url" | "time" | "formula" | "textarea" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "vector"; label?: string | undefined; @@ -40175,6 +40247,10 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -41830,7 +41906,11 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ labelField: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional, never>; }; + params: { [I_1 in keyof Ps]: Ps[I_1] & Record, never>; }; } : T_1 : never : never; }; } : unknown)) => Omit & Pick; }>>; @@ -44250,7 +44330,11 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ labelField: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "code" | "date" | "record" | "file" | "tags" | "email" | "phone" | "user" | "datetime" | "location" | "text" | "url" | "time" | "formula" | "textarea" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "vector"; label?: string | undefined; @@ -60994,6 +61085,10 @@ declare const MigrationOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -62132,7 +62227,6 @@ declare const MigrationOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ execution?: "aggregate" | "perRecord" | undefined; patch?: Record | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "code" | "date" | "record" | "file" | "tags" | "email" | "phone" | "user" | "datetime" | "location" | "text" | "url" | "time" | "formula" | "textarea" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "vector"; label?: string | undefined; @@ -62148,6 +62242,10 @@ declare const MigrationOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -63287,7 +63385,6 @@ declare const MigrationOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ execution?: "aggregate" | "perRecord" | undefined; patch?: Record | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "code" | "date" | "record" | "file" | "tags" | "email" | "phone" | "user" | "datetime" | "location" | "text" | "url" | "time" | "formula" | "textarea" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "vector"; label?: string | undefined; @@ -63303,6 +63400,10 @@ declare const MigrationOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -64441,7 +64542,6 @@ declare const MigrationOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ execution?: "aggregate" | "perRecord" | undefined; patch?: Record | undefined; params?: { - [x: string]: unknown; name: string; type: "number" | "boolean" | "code" | "date" | "record" | "file" | "tags" | "email" | "phone" | "user" | "datetime" | "location" | "text" | "url" | "time" | "formula" | "textarea" | "password" | "secret" | "markdown" | "html" | "richtext" | "currency" | "percent" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "avatar" | "video" | "audio" | "summary" | "autonumber" | "composite" | "repeater" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "vector"; label?: string | undefined; @@ -64457,6 +64557,10 @@ declare const MigrationOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ labelField?: string | undefined; multiple?: boolean | undefined; placeholder?: string | undefined; + dependsOn?: (string | { + field: string; + param?: string | undefined; + })[] | undefined; }[] | undefined; confirmText?: string | undefined; confirmLabel?: string | undefined; @@ -66112,7 +66216,11 @@ declare const MigrationOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ labelField: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional, never>; }; + params: { [I_1 in keyof Ps]: Ps[I_1] & Record, never>; }; } : T_1 : never : never; }; } : unknown)) => Omit & Pick; }; diff --git a/packages/spec/api-surface-declarations/ui.txt b/packages/spec/api-surface-declarations/ui.txt index 1b6f1460f7d..4ad9418d589 100644 --- a/packages/spec/api-surface-declarations/ui.txt +++ b/packages/spec/api-surface-declarations/ui.txt @@ -2047,7 +2047,11 @@ declare const BulkActionDefSchema: z.ZodObject<{ labelField: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; -}, z.core.$loose>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; +}, z.core.$strict>; // ── CHART_AGGREGATE_COMPARISON_SUFFIX (const) ── declare const CHART_AGGREGATE_COMPARISON_SUFFIX = "__comparison"; @@ -10268,7 +10276,11 @@ declare const ListViewSchema: z.ZodObject<{ labelField: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional; multiple: z.ZodOptional; placeholder: z.ZodOptional; - }, z.core.$loose>>>; + dependsOn: z.ZodOptional; + }, z.core.$strict>]>>>; + }, z.core.$strict>>>; confirmText: z.ZodOptional; confirmLabel: z.ZodOptional; visible: z.ZodOptional { @@ -56,13 +58,26 @@ describe('BulkActionDefSchema (#4457)', () => { expect(def.params?.[0]).toMatchObject({ object: 'sys_user', labelField: 'name' }); }); - it('forwards unknown WIDGET config on a param — the renderer declares a catch-all', () => { - const def = ok({ + // ── RE-JUDGED, NOT RE-SPELLED (#18177) ───────────────────────────────── + // This case used to be `forwards unknown WIDGET config on a param — the + // renderer declares a catch-all`, and it asserted that `min`/`max`/`step` + // rode through. That is the exact behaviour letter A removed, so the + // fixture could not be repaired by changing a spelling: what it pinned is + // gone, and the honest replacement pins the REFUSAL plus the prescription + // that now has to carry an author across. The keys really are read by a + // widget once they reach the field bag — which is why the message says so + // instead of implying the renderer ignores them. + it('REFUSES unknown widget config on a param, and the rejection names where the vocabulary IS real', () => { + const issues = reject({ name: 'reschedule', operation: 'update', params: [{ name: 'shift_days', type: 'number', min: 1, max: 90, step: 1 }], - }); - expect(def.params?.[0]).toMatchObject({ min: 1, max: 90, step: 1 }); + }).join('\n'); + expect(issues).toContain('min'); + expect(issues).toContain('FieldSchema'); + // ⛔ The prescription must NOT send the author to the field-backed route: + // the bulk surface has none, so that answer would be confidently wrong. + expect(issues).toContain('no field-backed param route'); }); // ── DELIBERATE OPENNESS — DO NOT "FIX" THIS INTO A STRICT SITE ────────── @@ -287,4 +302,131 @@ describe('BulkActionDefSchema (#4457)', () => { .toContain('`requiredCapabilities` → `requiredPermissions`'); }); }); + + // ───────────────────────────────────────────────────────────────────────── + // #18177 — maintainer ruling batch #146 item 4, letter A. + // + // The card that produced this ruling turned on ONE measurement: the open + // shape accepted `zzz_nonsense_key_that_no_producer_emits_8755` in the same + // run that it accepted `dependsOn`, so its accept licensed neither. Both legs + // are pinned here in their POST-close form — the nonsense key is refused, and + // `dependsOn` parses because it is declared, not because nothing is checked. + // Keep the pair together: a `dependsOn` assertion alone would go green again + // the day someone re-opens the shape. + // ───────────────────────────────────────────────────────────────────────── + describe('— the shape is closed, so its accept means something (#18177)', () => { + const NONSENSE = 'zzz_nonsense_key_that_no_producer_emits_8755'; + + it('CONTROL (negative): the nonsense key the card measured is now refused by name', () => { + const r = BulkActionParamSchema.safeParse({ name: 'p', type: 'text', [NONSENSE]: 1 }); + expect(r.success).toBe(false); + if (r.success) return; + expect(r.error.issues.some((i) => i.code === 'unrecognized_keys')).toBe(true); + expect(JSON.stringify(r.error.issues)).toContain(NONSENSE); + }); + + it('CONTROL (positive): a minimal valid param still parses — the door refuses, it does not jam', () => { + expect(BulkActionParamSchema.safeParse({ name: 'p', type: 'text' }).success).toBe(true); + }); + + it('the twin refuses the same nonsense key — one strictness across both param surfaces', () => { + // `ActionParamSchema` has been strict since #3746; this is the asymmetry + // the card was filed about, now closed from the other side. + const r = BulkActionParamSchema.safeParse({ name: 'p', type: 'text', [NONSENSE]: 1 }); + expect(r.success).toBe(false); + }); + + it('`dependsOn` is DECLARED — both authored forms parse', () => { + const def = ok({ + name: 'reassign', + operation: 'update', + params: [ + { name: 'account', type: 'lookup', object: 'showcase_account' }, + { name: 'contact', type: 'lookup', object: 'showcase_contact', dependsOn: ['account'] }, + { name: 'owner', type: 'lookup', object: 'sys_user', dependsOn: [{ field: 'account', param: 'account_id' }] }, + ], + }); + expect(def.params?.[1]).toMatchObject({ dependsOn: ['account'] }); + expect(def.params?.[2]).toMatchObject({ dependsOn: [{ field: 'account', param: 'account_id' }] }); + }); + + // ⚠️ Read through `formatZodError`, the door an author really meets — NOT + // `issue.message`. A `dependsOn` entry sits behind a string-or-object + // union, whose top-level message is the bare `Invalid input`; the curated + // refusal lives one level down and is surfaced by `formatZodIssue`'s union + // descent. A raw-issue pin here would assert the union wrapper and report + // the rename as missing when it is rendered perfectly well (the exact + // misreading `shared/union-author-message-pins.test.ts` exists to stop). + it('the entry inside `dependsOn` is strict too — strictness does not recurse by itself', () => { + const r = BulkActionDefSchema.safeParse({ + name: 'reassign', + operation: 'update', + params: [{ name: 'owner', type: 'lookup', object: 'sys_user', dependsOn: [{ local: 'account' }] }], + }); + expect(r.success).toBe(false); + if (r.success) return; + const message = formatZodError(r.error as Parameters[0]); + expect(message).toContain('`local` → `field`'); + expect(message).toContain('this dependsOn entry'); + // The STRING arm's kind mismatch is not a prescription — it must not be + // what the author is shown. + expect(message).not.toContain('expected string, received object'); + }); + + // ⭐ The parity pin. The ruling said "the same shape and `describe` as the + // single-record twin", and the twin for THIS key is `FieldSchema.dependsOn` + // — `ActionParamSchema` declares no `dependsOn` at all, because the + // single-record dialog reaches it through the field-backed route. Pinning + // acceptance parity is what stops the two doors drifting into dialects. + it('accepts exactly what the FieldSchema twin accepts, and refuses exactly what it refuses', () => { + const asField = (dependsOn: unknown) => + FieldSchema.safeParse({ name: 'owner', type: 'lookup', reference: 'account', dependsOn }).success; + const asBulkParam = (dependsOn: unknown) => + BulkActionParamSchema.safeParse({ name: 'owner', type: 'lookup', object: 'account', dependsOn }).success; + + const cases: unknown[] = [ + ['account'], + [{ field: 'account' }], + [{ field: 'account', param: 'account_id' }], + ['account', { field: 'region', param: 'region_id' }], + [{ local: 'account' }], + [{ param: 'account_id' }], + 'account', + [42], + ]; + const twin = cases.map(asField); + const bulk = cases.map(asBulkParam); + expect(bulk).toEqual(twin); + // The comparison is only worth anything if both verdicts really occur. + expect(new Set(twin)).toEqual(new Set([true, false])); + }); + }); + + describe('— the close carries the author across, it does not just say no (#18177)', () => { + const paramIssues = (param: Record): string => + reject({ name: 'd', operation: 'update', params: [{ name: 'p', type: 'text', ...param }] }).join('\n'); + + it('renames the ACTION-param spellings of the known divergence onto this surface words', () => { + expect(paramIssues({ helpText: 'x' })).toContain('`helpText` → `help`'); + expect(paramIssues({ defaultValue: 'x' })).toContain('`defaultValue` → `default`'); + expect(paramIssues({ reference: 'sys_user' })).toContain('`reference` → `object`'); + expect(paramIssues({ displayField: 'name' })).toContain('`displayField` → `labelField`'); + }); + + it('answers `field` with the route that does not exist rather than a spelling hint', () => { + const issues = paramIssues({ field: 'owner' }); + expect(issues).toContain('FIELD-BACKED'); + expect(issues).toContain('Declare the param inline'); + }); + + it('sends a param-level `visible` to the DEF, where the predicate is really read', () => { + expect(paramIssues({ visible: 'true' })).toContain('bulkActionDefs[].visible'); + }); + + it('answers the whole widget-config family with ONE prescription, not one per key', () => { + const issues = paramIssues({ min: 1, max: 9, step: 1 }); + const occurrences = issues.split('widget-config keys like').length - 1; + expect(occurrences).toBe(1); + }); + }); }); diff --git a/packages/spec/src/ui/bulk-action.zod.ts b/packages/spec/src/ui/bulk-action.zod.ts index 93d64f34045..dfffcbf8101 100644 --- a/packages/spec/src/ui/bulk-action.zod.ts +++ b/packages/spec/src/ui/bulk-action.zod.ts @@ -48,13 +48,28 @@ import { FieldType } from '../data/field.zod'; // error for a blank screen. Localizing means declaring a real action and // naming it in `bulkActions`: THAT path runs through the i18n resolver // (`toBulkActionDef`'s `localize`). -// - `params[]` is `.passthrough()`. objectui's `BulkActionParam` declares an -// explicit `[key: string]: unknown` catch-all — widget config forwarded to -// the field renderer as-is (min/max/step/format). Locking it down would -// reject valid config, so declared keys are typed and the rest rides -// through, the same call `dashboard.zod.ts` makes for a widget's `config`. -// - `params[].options[]` is `.passthrough()` TOO — measured, not inherited -// from its parent by symmetry. objectui's option TYPE is closed +// - `params[]` is STRICT (#18177, maintainer ruling batch #146 item 4, +// letter A). It used to be `.passthrough()`, mirroring the +// `[key: string]: unknown` catch-all on objectui's `BulkActionParam` — and +// that made its accept a NULL READING: measured against installed spec +// 17.4.0 it took `zzz_nonsense_key_that_no_producer_emits_8755` in the same +// run that it took `dependsOn`, while `ActionParamSchema` one surface over +// refused both with `unrecognized_keys`. A shape that examines nothing can +// license nothing, so the twins now carry the same strictness. +// ⚠️ What that CHANGED, stated rather than buried: the widget-config keys +// the catch-all forwarded (`min`/`max`/`step`/…) are keys of a FIELD, not +// of a bulk param, and this shape declares none of them — they are refused +// now, with the `BULK_PARAM_WIDGET_CONFIG_KEYS` prescription below naming +// where the vocabulary IS real. A census of authored bulk params over the +// two repos reachable from the landing session found ZERO carrying an +// undeclared key, so no in-corpus configuration stops working; hotcrm was +// NOT REACHABLE for that census and is recorded unmeasured, never clean. +// The one key measured LIVE on this surface is DECLARED rather than +// refused — `dependsOn`, below. Retiring it would delete a capability that +// ships. +// - `params[].options[]` is `.passthrough()` — and since the parent closed it +// is the ONE open level left here. Still measured, never inherited from a +// neighbour by symmetry. objectui's option TYPE is closed // (`Array<{ label; value }>`, `packages/types/src/objectql.ts:271`), but the // type is not what an authored option meets: `bulkParamToField` SPREADS each // entry — `options?.map(o => ({ ...o, value: String(o.value) }))`, @@ -73,7 +88,11 @@ import { FieldType } from '../data/field.zod'; // KNOWN DIVERGENCE, DELIBERATELY NOT FIXED HERE. A bulk param and an action // param are the same idea under different spellings (`help`/`helpText`, // `default`/`defaultValue`, `object`/`reference`, plus `labelField`, which -// `ActionParamSchema` has no counterpart for). objectui already owns a converter +// `ActionParamSchema` has no counterpart for — `displayField` is the FIELD +// spelling of the same idea). Closing this shape turned those from keys that +// rode through and did nothing into RENAMES: each is an `aliases` row below, so +// an author who reaches for the neighbouring surface's word is told the one +// this surface takes. objectui already owns a converter // for the PROMOTED direction (`toBulkParam` in `resolveBulkActions.ts`); // converging the AUTHORED direction means teaching the renderer to run authored // params through it and giving `ActionParamSchema` a `labelField` — a cross-repo @@ -104,18 +123,125 @@ export type BulkActionOperation = z.input; export const BulkActionExecutionSchema = z.enum(['perRecord', 'aggregate']); export type BulkActionExecution = z.input; +/** + * Keys a widget on this path really reads off the field bag — and which this + * shape nevertheless does NOT declare, so their rejection has to carry the + * reason rather than a bare "unknown key". + * + * Measured, not listed from memory: `bulkParamToField` destructures the eleven + * declared keys out and spreads the REST onto the field metadata it hands + * `getLazyFieldWidget`, so any of these reaches a widget that reads it — + * `min`/`max`/`step` (NumberField / SliderField / CurrencyField / PercentField + * / RatingField), `accept`/`maxSize`/`crop`/`capture` (FileField / ImageField), + * `rows` (TextAreaField / RichTextField), `precision`/`scale`, `dimensions` + * (VectorField), `defaultName` (AvatarField), and the picker family + * `descriptionField` / `idField` / `allowCreate` / `lookupColumns` / + * `lookupPageSize` / `lookupFilters` / `picker` / `subtitle` / `avatarField` + * (LookupField, and UserField through it). + * + * ⛔ Reading that as "so declare them" is the move this file does not make. A + * declared key is published contract whose removal costs a full retirement, and + * the census that accompanied the close found NO authored bulk param writing + * any of them — the evidence licenses a loud rejection, not twenty new members. + * `format` earns its absence from the list the same way: the module header used + * to name it beside min/max/step, and the sweep found no FORM widget reading it + * at all. + * + * ⚠️ The prescription deliberately refuses the obvious-sounding remedy. There + * is no field-backed param route on the bulk surface — `toBulkParam` never + * consults the object's field definitions — so "declare it on the field and let + * the dialog inherit" would be a confidently wrong answer, the shape this + * campaign has already shipped more than once. + */ +const BULK_PARAM_WIDGET_CONFIG_KEYS = [ + 'min', 'max', 'step', 'precision', 'scale', 'rows', + 'accept', 'maxSize', 'crop', 'capture', 'dimensions', 'defaultName', + 'descriptionField', 'idField', 'allowCreate', + 'lookupColumns', 'lookupPageSize', 'lookupFilters', 'picker', 'subtitle', 'avatarField', +] as const; + /** * One input collected ONCE by the bulk dialog before the run (never re-prompted * per record). For `operation: 'update'` the collected values ARE the patch * (merged over the def's static `patch`); for an aggregate `custom` def they * ride along as the action's params. * - * `.passthrough()` — see the module header: the renderer's own type declares a - * catch-all for widget config, so the declared keys are typed and extras are - * forwarded. That means a typo'd key here still ships silently; the def LEVEL - * is where strictness buys something, and this level is where it would lie. + * STRICT since #18177 — see the module header. An unknown key is refused by + * name, carrying either the rename or the prescription that fixes it, exactly + * as on `ActionParamSchema`. The sentence this replaced said strictness "would + * lie" at this level; the measurement said the opposite — the OPEN shape was + * the lie, because it accepted a nonsense key and `dependsOn` in one breath and + * could therefore license neither. */ -export const BulkActionParamSchema = lazySchema(() => z.object({ +export const BulkActionParamSchema = lazySchema(() => strictObject( + { + surface: 'this bulk action param', + aliases: { + // The KNOWN DIVERGENCE pairs from the module header, in the direction an + // author actually slips: they are writing an ACTION param (or a FIELD) + // and reaching for its word. `toBulkParam` maps the same three when it + // promotes an action param, so the two directions now agree. + helpText: 'help', + description: 'help', + defaultValue: 'default', + reference: 'object', + referenceTo: 'object', + // `labelField` is this surface's name for the picker's option label; + // `FieldSchema` and objectui's picker both spell it `displayField`. + displayField: 'labelField', + title: 'label', + }, + guidance: { + field: + '`field` declares a FIELD-BACKED param, and the bulk surface has no such route: ' + + '`resolveActionParams` consults the object\'s field definitions for the single-record ' + + 'dialog, `resolveBulkActions`\'s `toBulkParam` never does. Declare the param inline ' + + 'instead — `name` + `type`, plus `object` (and optionally `labelField`) for a picker.', + objectOverride: + '`objectOverride` belongs to a field-backed ACTION param, which names the object owning ' + + 'the referenced field. A bulk param is always inline; the object a picker searches is ' + + '`object`.', + visible: + '`visible` on a bulk param has no reader — the dialog renders every param it is given. ' + + 'The per-record eligibility predicate belongs on the DEF (`bulkActionDefs[].visible`), ' + + 'where it gates the button and narrows the run; a per-OPTION rule goes on ' + + '`options[].visibleWhen`.', + visibleWhen: + '`visibleWhen` is a per-OPTION key, not a param one: write it inside `options[]`, where ' + + 'the select/multiselect/radio/checkbox widgets narrow the offered set against the ' + + 'dialog\'s own in-progress values. To gate the whole button, use the def\'s `visible`.', + carryOver: + '`carryOver` is an ACTION-param contract (seed from the current row, render read-only, ' + + 'submit verbatim). A bulk dialog runs over a SELECTION and holds no single row to seed ' + + 'from, so there is nothing for it to carry over. Put a fixed value in the def\'s ' + + '`patch` instead, which is merged under the collected params.', + defaultFromRow: + '`defaultFromRow` prefills an ACTION param from the current row. A bulk dialog has a ' + + 'selection, not a row — use `default` for a fixed prefill, or the def\'s `patch` for a ' + + 'value the user should not see.', + requiresFeature: + '`requiresFeature` is the ACTION param\'s capability sugar, lowered into `visible` at ' + + 'parse time. This shape has no `visible` to lower into; gate the whole button with the ' + + 'def\'s `visible` (`features.x`) or its `requiredPermissions`.', + }, + guidanceSets: [{ + name: 'BULK_PARAM_WIDGET_CONFIG_KEYS', + keys: BULK_PARAM_WIDGET_CONFIG_KEYS, + prescription: + 'widget-config keys like `min` / `max` / `step` / `accept` / `lookupFilters` are keys of a ' + + 'FIELD (`FieldSchema`, `data/field.zod.ts`), not of a bulk action param — this shape ' + + 'declares none of them. Until it was closed they rode through onto the renderer\'s field ' + + 'bag and whichever widget read one honoured it; that door is shut, so the value is ' + + 'refused rather than silently forwarded. ⛔ Declaring the key on the object\'s FIELD does ' + + 'not reach this dialog either: the bulk surface has no field-backed param route. Remove ' + + 'the key, and open an issue if a bulk param genuinely needs it declared here.', + }], + history: + 'Until this shape was closed, `params[]` was `.passthrough()` — every unknown key rode through ' + + 'onto the renderer\'s field bag, so a mis-spelled widget config shipped as a control that ' + + 'quietly ignored it, and a nonsense key parsed exactly as cleanly as a real one.', + }, + { name: z.string().min(1).describe('Param key — becomes params[name] in the patch / action params bag.'), label: z.string().optional().describe('Field label in the dialog. Plain string: an authored def is not i18n-resolved (see module header).'), help: z.string().optional().describe('Help text under the field. (An ActionParam spells this `helpText` — known divergence, module header.)'), @@ -130,7 +256,45 @@ export const BulkActionParamSchema = lazySchema(() => z.object({ labelField: z.string().optional().describe('Related-object field used as the option label for a `lookup` widget (defaults to name/full_name/email/id).'), multiple: z.boolean().optional().describe('Allow picking multiple values — the param value becomes an array and is written to the patch as-is.'), placeholder: z.string().optional().describe('Placeholder text.'), -}).passthrough()); + + /** + * Cascade binding — the ONE key this close DECLARES rather than refuses + * (#18177, ruling batch #146 item 4 letter A). + * + * Shape and description mirror the single-record twin. ⚠️ That twin is + * `FieldSchema.dependsOn` (`data/field.zod.ts`), NOT `ActionParamSchema`, + * which declares no `dependsOn` at all: the single-record dialog reaches the + * key through the FIELD-BACKED route (`resolveActionParams` resolves the + * object's field definitions), which is the very route the bulk surface does + * not have. So one vocabulary, two doors — and on this door the key has to be + * written on the param itself. + * + * Live on BOTH widget families reachable from the bulk dialog, measured on + * the renderer rather than inferred from this schema: + * - the OPTION family reads `field?.dependsOn` and gates/refreshes the + * offered set through `useCascadingOptions` (`SelectField`, + * `MultiSelectField`, `RadioField`, `CheckboxesField`); + * - the reference-bearing PICKER family reads the same key off the same bag + * as `cascadeMeta?.dependsOn` and lowers it into a hard candidate filter + * (`LookupField`, and `UserField` through it). + * + * It reaches them because `bulkParamToField` does not destructure it out — it + * rides the `...extra` spread onto the field metadata. That was already true + * while this shape was open, which is why the accept could not be read as a + * licence and the key could not be retired either: an ablation removing it + * from the spread reddened 7 of 12 cases in objectui. + */ + dependsOn: z.array(z.union([z.string(), strictObject({ + surface: 'this dependsOn entry', + history: + 'Until this shape was closed these were dropped silently — the entry still parsed, so a ' + + 'mis-spelled binding left the picker ungated and unscoped.', + aliases: { name: 'field', fieldName: 'field', local: 'field', remote: 'param', remoteField: 'param', key: 'param' }, + }, { + field: z.string(), + param: z.string().optional(), + })])).optional().describe("Declares that this param's available values depend on the value of other field(s) on the same record — the form gates the field until they are set and re-evaluates as they change. For `lookup`/`master_detail` it scopes the candidate query (string = same local/remote key; {field,param} when the remote filter key differs — the {field,param} form is lookup-only). For `select`/`multiselect`/`radio` the actual per-option rule lives in each option's `visibleWhen`; list the referenced fields here (string form) so the option list gates and refreshes with the parent. On a BULK param the record is the dialog's own in-progress param values — a bulk run holds a selection, not a row — so a binding names a SIBLING PARAM of the same def."), +})); export type BulkActionParam = z.input; /**