Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/30259-action-param-widget.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@objectstack/spec': minor
'@objectstack/platform-objects': minor
---

feat(spec): an action param can name the widget that draws it (`widget`, same key as on a field), so a param can ask for the permission-set picker (ADR-0131 D4)

Clause-②: yes (widening)
Metadata-protocol: added

- **`@objectstack/spec`: `ActionParamSchema.widget`**, an optional string with the same key, vocabulary and meaning as `FieldSchema.widget`. It names a registered field component, looked up as `field:` plus the name, to draw the param in the action dialog instead of the `type` default. `type` stays the value's type. The case it exists for is `widget: 'permission-ref'` on a `text` param: it lists the permission sets of the security catalog and submits the chosen machine name, the picker the grant column `sys_user_permission_set.permission_set` already uses. Like the field key it is an open string: an unregistered name is kept, and the dialog draws the `type` renderer instead. A non-string is refused. A param that does not declare the key parses exactly as before. Until now a param naming `widget` was refused as an unknown key.
- **Not read yet.** The console's param resolver does not carry the key to the dialog's widget until objectstack#30260 lands. Until then a param that declares it is drawn by its `type` renderer, and its value is submitted as before. The key's description says so, and the liveness ledger grades it `planned` with an author warning: `os validate` and `os lint` warn an author who writes it.
- **`@objectstack/platform-objects`: the action form's Params repeater offers the key** as a `Widget` column, labelled in en, zh-CN, ja-JP and es-ES.
3 changes: 3 additions & 0 deletions content/docs/references/ui/action.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ L2 sandboxed JS body — runs inside an isolated VM with declared capabilities
| **objectOverride** | `string` | optional | Snake case identifier (lowercase with underscores only) |
| **label** | `string \| Record<string, string>` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time |
| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>` | optional | |
| **widget** | `string` | optional | Widget override for this param — names a registered field component (resolved as `field:<widget>`) to draw it instead of the `type` default; same key and vocabulary as FieldSchema.widget, and `type` stays the value's type. e.g. "permission-ref" on a text param picks a permission set by name (ADR-0131 D4). Degrades to the `type` renderer when unregistered. Not read yet: until the console's action dialog passes it through, the dialog draws every param with its `type` renderer whatever this names. |
| **required** | `boolean` | optional (default: `false`) | |
| **options** | `{ label: string \| Record<string, string>; value: string; visibleWhen?: string \| object }[]` | optional | |
| **placeholder** | `string` | optional | |
Expand Down Expand Up @@ -234,6 +235,7 @@ L2 sandboxed JS body — runs inside an isolated VM with declared capabilities
| **objectOverride** | `string` | optional | Snake case identifier (lowercase with underscores only) |
| **label** | `string \| Record<string, string>` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time |
| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| 'markdown' \| 'html' \| 'richtext' \| 'number' \| 'currency' \| 'percent' \| 'date' \| … +35 more>` | optional | |
| **widget** | `string` | optional | Widget override for this param — names a registered field component (resolved as `field:<widget>`) to draw it instead of the `type` default; same key and vocabulary as FieldSchema.widget, and `type` stays the value's type. e.g. "permission-ref" on a text param picks a permission set by name (ADR-0131 D4). Degrades to the `type` renderer when unregistered. Not read yet: until the console's action dialog passes it through, the dialog draws every param with its `type` renderer whatever this names. |
| **required** | `boolean` | optional (default: `false`) | |
| **options** | `{ label: string \| Record<string, string>; value: string; visibleWhen?: string \| object }[]` | optional | |
| **placeholder** | `string` | optional | |
Expand Down Expand Up @@ -370,6 +372,7 @@ L2 sandboxed JS body — runs inside an isolated VM with declared capabilities
| **objectOverride** | `string` | optional | Snake case identifier (lowercase with underscores only) |
| **label** | `string \| Record<string, string>` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time |
| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>` | optional | |
| **widget** | `string` | optional | Widget override for this param — names a registered field component (resolved as `field:<widget>`) to draw it instead of the `type` default; same key and vocabulary as FieldSchema.widget, and `type` stays the value's type. e.g. "permission-ref" on a text param picks a permission set by name (ADR-0131 D4). Degrades to the `type` renderer when unregistered. Not read yet: until the console's action dialog passes it through, the dialog draws every param with its `type` renderer whatever this names. |
| **required** | `boolean` | optional (default: `false`) | |
| **options** | `{ label: string \| Record<string, string>; value: string; visibleWhen?: string \| object }[]` | optional | |
| **placeholder** | `string` | optional | |
Expand Down
28 changes: 28 additions & 0 deletions packages/lint/src/lint-liveness-properties.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,34 @@ describe('lintLivenessProperties', () => {
}).filter((f) => f.message.includes('objectName'))).toEqual([]);
});

// #30259 — `action.params[].widget` is declared before the console's dialog
// reads it (#30260 adds the pass-through), so the drilled row is `planned` +
// `authorWarn`. Pinned through the SHIPPED ledger and an array of params, so
// the drill (`params.widget`) and the array fan-out both have to hold.
it('warns on an action param that names a widget — planned, nothing reads it yet', () => {
const findings = lintLivenessProperties({
actions: [{
name: 'create_user',
params: [
{ name: 'email', type: 'email', required: true },
{ name: 'permissionSet', type: 'text', widget: 'permission-ref' },
],
}],
});
const warned = findings.filter((f) => f.message.includes('sets `params.widget`'));
expect(warned).toHaveLength(1);
expect(warned[0].rule).toBe('liveness-planned-property');
expect(warned[0].hint).toContain('nothing reads it yet');
// The live siblings the drill graded stay silent.
for (const key of ['name', 'type', 'required', 'options', 'helpText', 'carryOver']) {
expect(ruleOf(findings, `params.${key}`), key).toBeUndefined();
}
// And an action whose params name no widget draws nothing about it.
expect(lintLivenessProperties({
actions: [{ name: 'a2', params: [{ name: 'note', type: 'text' }] }],
}).filter((f) => f.message.includes('widget'))).toEqual([]);
});

it('warns on an experimental prop with no authorWarn of its own (tool.outputSchema)', () => {
// `experimental` warns implicitly — shouldWarn() treats a declared-but-
// unenforced guarantee like an opted-in dead prop. Repointed from
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -637,8 +637,9 @@ describe('#19403 round 5 — the blind spot, FIFTH shape: one schema, two forms,
// `repeater-row-properties.test.ts` derives a row property for every child
// of a `type: 'repeater'` field. On `actionForm` that is exactly ONE
// repeater — `params`, with eighteen children — and every one of those
// carried its own text in all three locales before this round.
expect(ACTION_ENUMERATED.covered.length).toBe(18);
// carried its own text in all three locales before this round. Nineteen
// since #30259 added `params.widget`, authored in all three locales too.
expect(ACTION_ENUMERATED.covered.length).toBe(19);
expect(ACTION_ENUMERATED.covered).toContain('params.carryOver');
expect(ACTION_ENUMERATED.covered).toContain('params.requiresFeature');
for (const [locale, forms] of TRANSLATED_LOCALES) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1756,6 +1756,9 @@ export const enMetadataForms: NonNullable<TranslationData['metadataForms']> = {
"params.type": {
label: "Type"
},
"params.widget": {
label: "Widget"
},
"params.required": {
label: "Required"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1756,6 +1756,9 @@ export const esESMetadataForms: NonNullable<TranslationData['metadataForms']> =
"params.type": {
label: "Tipo"
},
"params.widget": {
label: "Control del campo"
},
"params.required": {
label: "Obligatorio"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1756,6 +1756,9 @@ export const jaJPMetadataForms: NonNullable<TranslationData['metadataForms']> =
"params.type": {
label: "型"
},
"params.widget": {
label: "ウィジェット指定"
},
"params.required": {
label: "必須"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1259,7 +1259,10 @@ describe('#19403 round 10 — the verdicts, on the live bundles', () => {
// 701 since #29442: the flow form's variables repeater offers
// `objectName` (a start input declared a record id) — one row label,
// authored in all three locales.
expect(translated.length, `${locale} positive control`).toBe(701);
// 702 since #30259: the action form's params repeater offers `widget`
// (the widget that draws the param, the same key as a field's) — one
// row label, authored in all three locales.
expect(translated.length, `${locale} positive control`).toBe(702);
}
// ⭐ DARK — the blindness, executable. On a synthetic two-locale catalog the
// all-three predicate returns 0 while the per-locale one returns 1, so the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1756,6 +1756,9 @@ export const zhCNMetadataForms: NonNullable<TranslationData['metadataForms']> =
"params.type": {
label: "类型"
},
"params.widget": {
label: "控件覆盖"
},
"params.required": {
label: "必填"
},
Expand Down
1 change: 1 addition & 0 deletions packages/spec/authorable-surface/ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@
"ui/ActionParam:requiresFeature",
"ui/ActionParam:type",
"ui/ActionParam:visible",
"ui/ActionParam:widget",
"ui/ActionSession:organizationId",
"ui/ActionSession:positions",
"ui/ActionSession:roles",
Expand Down
Loading
Loading