diff --git a/.changeset/action-param-widget-config.md b/.changeset/action-param-widget-config.md new file mode 100644 index 0000000000..d19bee7b68 --- /dev/null +++ b/.changeset/action-param-widget-config.md @@ -0,0 +1,14 @@ +--- +"@objectstack/spec": minor +--- + +feat(spec): `ActionParamSchema` gains optional widget config — `multiple`, `accept`, `maxSize` + +The console now renders action params through the same field-widget renderer +the record form uses (objectui#2700, objectui ADR-0059), so inline params can +declare the widget config the form widgets consume: `multiple` (array value +shape, mirrors `FieldSchema.multiple`), and the upload constraints `accept` +(MIME types / extensions) and `maxSize` (bytes) for `file`/`image` params. +Field-backed params (`{ field }`) keep inheriting these from the referenced +field at runtime; inline values override. Purely additive — no existing +schema changes shape. diff --git a/.changeset/console-fd45313b4d00.md b/.changeset/console-fd45313b4d00.md index 9c2bb2e655..41b163863d 100644 --- a/.changeset/console-fd45313b4d00.md +++ b/.changeset/console-fd45313b4d00.md @@ -1,5 +1,5 @@ --- -"@objectstack/console": major +"@objectstack/console": minor --- Console (objectui) refreshed to `fd45313b4d00`. Frontend changes in this range: diff --git a/content/docs/references/ui/action.mdx b/content/docs/references/ui/action.mdx index c9de25bf1b..8b0d15621b 100644 --- a/content/docs/references/ui/action.mdx +++ b/content/docs/references/ui/action.mdx @@ -150,6 +150,9 @@ const result = Action.parse(data); | **placeholder** | `string` | optional | | | **helpText** | `string` | optional | | | **defaultValue** | `any` | optional | | +| **multiple** | `boolean` | optional | Allow multiple values (array value shape); mirrors FieldSchema.multiple. | +| **accept** | `string[]` | optional | Accepted upload types (MIME types / extensions) for file/image params. | +| **maxSize** | `integer` | optional | Max upload size in bytes for file/image params. | | **defaultFromRow** | `boolean` | optional | | | **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Param visibility predicate (CEL); omits the param when false. | | **requiresFeature** | `Enum<'twoFactor' \| 'passkeys' \| 'magicLink' \| 'organization' \| 'multiOrgEnabled' \| 'degradedTenancy' \| 'oidcProvider' \| 'sso' \| 'ssoEnforced' \| 'deviceAuthorization' \| 'admin' \| 'phoneNumber' \| 'phoneNumberOtp'>` | optional | Public auth feature flag gating this param; lowered into `visible` at parse time. | diff --git a/content/docs/ui/actions.mdx b/content/docs/ui/actions.mdx index b65343a727..3c3c918a73 100644 --- a/content/docs/ui/actions.mdx +++ b/content/docs/ui/actions.mdx @@ -168,9 +168,13 @@ engine still requires the action to declare the matching location (that's why - **`params`** — prompt the user for input before execution. Prefer field-backed params (`{ field: 'due_date' }`) which inherit the object - field's label, type, and validation; inline params + field's label, type, validation, and widget config; inline params (`{ name, label, type, required, options }`) cover the rest. - `defaultFromRow` prefills from the current record. + `defaultFromRow` prefills from the current record. The console renders each + param through the **same field widgets as the record form** (objectui + ADR-0059), so any `FieldType` works — a `file` param shows a real upload + control (`multiple` / `accept` / `maxSize` honored), `lookup` a record + picker, `date` a date picker, `richtext` the rich-text editor, and so on. - **`confirmText`** — confirmation dialog before running. - **`successMessage` / `errorMessage` / `refreshAfter`** — post-run feedback and an automatic data refresh. diff --git a/packages/spec/src/ui/action.test.ts b/packages/spec/src/ui/action.test.ts index 9a911102b3..990707101a 100644 --- a/packages/spec/src/ui/action.test.ts +++ b/packages/spec/src/ui/action.test.ts @@ -38,6 +38,42 @@ describe('ActionParamSchema', () => { expect(() => ActionParamSchema.parse(param)).not.toThrow(); }); + + // objectui ADR-0059 — params render through the shared form field-widget + // renderer, so inline params can carry the widget config the form widgets + // consume (field-backed params inherit these from the field at runtime). + it('accepts a file param with upload widget config (multiple/accept/maxSize)', () => { + const result = ActionParamSchema.parse({ + name: 'attachments', + label: 'Attachments', + type: 'file' as const, + multiple: true, + accept: ['application/pdf', 'image/*'], + maxSize: 5 * 1024 * 1024, + }); + expect(result.multiple).toBe(true); + expect(result.accept).toEqual(['application/pdf', 'image/*']); + expect(result.maxSize).toBe(5 * 1024 * 1024); + }); + + it('leaves widget config undefined when not declared (runtime inherits from the field)', () => { + const result = ActionParamSchema.parse({ field: 'avatar_file' }); + expect(result.multiple).toBeUndefined(); + expect(result.accept).toBeUndefined(); + expect(result.maxSize).toBeUndefined(); + }); + + it('rejects a non-positive or fractional maxSize', () => { + expect(() => ActionParamSchema.parse({ name: 'f', type: 'file', maxSize: 0 })).toThrow(); + expect(() => ActionParamSchema.parse({ name: 'f', type: 'file', maxSize: -1 })).toThrow(); + expect(() => ActionParamSchema.parse({ name: 'f', type: 'file', maxSize: 1.5 })).toThrow(); + }); + + it('accepts rich field types for params (rendered by the shared widget renderer)', () => { + for (const type of ['file', 'image', 'richtext', 'color', 'date', 'address', 'code'] as const) { + expect(() => ActionParamSchema.parse({ name: 'p', label: 'P', type })).not.toThrow(); + } + }); }); // #2874 P1 — declarative `requiresFeature` sugar, lowered at parse time into diff --git a/packages/spec/src/ui/action.zod.ts b/packages/spec/src/ui/action.zod.ts index ea5a501683..1774464d32 100644 --- a/packages/spec/src/ui/action.zod.ts +++ b/packages/spec/src/ui/action.zod.ts @@ -63,6 +63,19 @@ export const ActionParamSchema = lazySchema(() => z.object({ helpText: z.string().optional(), /** Default value for the dialog input. */ defaultValue: z.unknown().optional(), + /** + * Widget config for inline params (field-backed params inherit these from + * the referenced field at runtime; inline values override). The param + * dialog renders every param through the same field-widget renderer the + * object form uses (objectui ADR-0059), so these mirror the corresponding + * `FieldSchema` knobs. + */ + /** Allow multiple values (file/image/lookup/user params → array value). */ + multiple: z.boolean().optional().describe('Allow multiple values (array value shape); mirrors FieldSchema.multiple.'), + /** Accepted upload types (MIME types / extensions) for `file`/`image` params. */ + accept: z.array(z.string()).optional().describe('Accepted upload types (MIME types / extensions) for file/image params.'), + /** Max upload size in bytes for `file`/`image` params. */ + maxSize: z.number().int().positive().optional().describe('Max upload size in bytes for file/image params.'), /** * When true, the param's default value is pulled from the current row record * (key = the resolved field name) when the action runs from a list_item