Skip to content

Commit dc0dc61

Browse files
committed
feat(spec): ActionParamSchema gains widget config — multiple/accept/maxSize (objectui#2700)
The console now renders action params through the same field-widget renderer the record form uses (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) + `maxSize` (bytes, positive integer) for file/image params. Field-backed params keep inheriting these from the referenced field at runtime; inline values override. Purely additive. Docs: regenerated references/ui/action.mdx; ui/actions.mdx notes that any FieldType param renders its real widget (file upload, lookup picker, richtext, ...). Changeset: @objectstack/spec minor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rg7oavKSUdb2KzYEEYrCHX
1 parent 616e839 commit dc0dc61

5 files changed

Lines changed: 72 additions & 2 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): `ActionParamSchema` gains optional widget config — `multiple`, `accept`, `maxSize`
6+
7+
The console now renders action params through the same field-widget renderer
8+
the record form uses (objectui#2700, objectui ADR-0059), so inline params can
9+
declare the widget config the form widgets consume: `multiple` (array value
10+
shape, mirrors `FieldSchema.multiple`), and the upload constraints `accept`
11+
(MIME types / extensions) and `maxSize` (bytes) for `file`/`image` params.
12+
Field-backed params (`{ field }`) keep inheriting these from the referenced
13+
field at runtime; inline values override. Purely additive — no existing
14+
schema changes shape.

content/docs/references/ui/action.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ const result = Action.parse(data);
150150
| **placeholder** | `string` | optional | |
151151
| **helpText** | `string` | optional | |
152152
| **defaultValue** | `any` | optional | |
153+
| **multiple** | `boolean` | optional | Allow multiple values (array value shape); mirrors FieldSchema.multiple. |
154+
| **accept** | `string[]` | optional | Accepted upload types (MIME types / extensions) for file/image params. |
155+
| **maxSize** | `integer` | optional | Max upload size in bytes for file/image params. |
153156
| **defaultFromRow** | `boolean` | optional | |
154157
| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Param visibility predicate (CEL); omits the param when false. |
155158
| **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. |

content/docs/ui/actions.mdx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,13 @@ engine still requires the action to declare the matching location (that's why
168168

169169
- **`params`** — prompt the user for input before execution. Prefer
170170
field-backed params (`{ field: 'due_date' }`) which inherit the object
171-
field's label, type, and validation; inline params
171+
field's label, type, validation, and widget config; inline params
172172
(`{ name, label, type, required, options }`) cover the rest.
173-
`defaultFromRow` prefills from the current record.
173+
`defaultFromRow` prefills from the current record. The console renders each
174+
param through the **same field widgets as the record form** (objectui
175+
ADR-0059), so any `FieldType` works — a `file` param shows a real upload
176+
control (`multiple` / `accept` / `maxSize` honored), `lookup` a record
177+
picker, `date` a date picker, `richtext` the rich-text editor, and so on.
174178
- **`confirmText`** — confirmation dialog before running.
175179
- **`successMessage` / `errorMessage` / `refreshAfter`** — post-run feedback
176180
and an automatic data refresh.

packages/spec/src/ui/action.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,42 @@ describe('ActionParamSchema', () => {
3838

3939
expect(() => ActionParamSchema.parse(param)).not.toThrow();
4040
});
41+
42+
// objectui ADR-0059 — params render through the shared form field-widget
43+
// renderer, so inline params can carry the widget config the form widgets
44+
// consume (field-backed params inherit these from the field at runtime).
45+
it('accepts a file param with upload widget config (multiple/accept/maxSize)', () => {
46+
const result = ActionParamSchema.parse({
47+
name: 'attachments',
48+
label: 'Attachments',
49+
type: 'file' as const,
50+
multiple: true,
51+
accept: ['application/pdf', 'image/*'],
52+
maxSize: 5 * 1024 * 1024,
53+
});
54+
expect(result.multiple).toBe(true);
55+
expect(result.accept).toEqual(['application/pdf', 'image/*']);
56+
expect(result.maxSize).toBe(5 * 1024 * 1024);
57+
});
58+
59+
it('leaves widget config undefined when not declared (runtime inherits from the field)', () => {
60+
const result = ActionParamSchema.parse({ field: 'avatar_file' });
61+
expect(result.multiple).toBeUndefined();
62+
expect(result.accept).toBeUndefined();
63+
expect(result.maxSize).toBeUndefined();
64+
});
65+
66+
it('rejects a non-positive or fractional maxSize', () => {
67+
expect(() => ActionParamSchema.parse({ name: 'f', type: 'file', maxSize: 0 })).toThrow();
68+
expect(() => ActionParamSchema.parse({ name: 'f', type: 'file', maxSize: -1 })).toThrow();
69+
expect(() => ActionParamSchema.parse({ name: 'f', type: 'file', maxSize: 1.5 })).toThrow();
70+
});
71+
72+
it('accepts rich field types for params (rendered by the shared widget renderer)', () => {
73+
for (const type of ['file', 'image', 'richtext', 'color', 'date', 'address', 'code'] as const) {
74+
expect(() => ActionParamSchema.parse({ name: 'p', label: 'P', type })).not.toThrow();
75+
}
76+
});
4177
});
4278

4379
// #2874 P1 — declarative `requiresFeature` sugar, lowered at parse time into

packages/spec/src/ui/action.zod.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,19 @@ export const ActionParamSchema = lazySchema(() => z.object({
6363
helpText: z.string().optional(),
6464
/** Default value for the dialog input. */
6565
defaultValue: z.unknown().optional(),
66+
/**
67+
* Widget config for inline params (field-backed params inherit these from
68+
* the referenced field at runtime; inline values override). The param
69+
* dialog renders every param through the same field-widget renderer the
70+
* object form uses (objectui ADR-0059), so these mirror the corresponding
71+
* `FieldSchema` knobs.
72+
*/
73+
/** Allow multiple values (file/image/lookup/user params → array value). */
74+
multiple: z.boolean().optional().describe('Allow multiple values (array value shape); mirrors FieldSchema.multiple.'),
75+
/** Accepted upload types (MIME types / extensions) for `file`/`image` params. */
76+
accept: z.array(z.string()).optional().describe('Accepted upload types (MIME types / extensions) for file/image params.'),
77+
/** Max upload size in bytes for `file`/`image` params. */
78+
maxSize: z.number().int().positive().optional().describe('Max upload size in bytes for file/image params.'),
6679
/**
6780
* When true, the param's default value is pulled from the current row record
6881
* (key = the resolved field name) when the action runs from a list_item

0 commit comments

Comments
 (0)