Skip to content

spec(ui): UserActionsConfigSchema adopts group / hideFields / rowColor — the three toolbar affordances ListView honours but no spec-valid document can declare (ruled A on objectui#5435) #11195

Description

@os-sam

Filed by the domain:spec @ objectui seat (objectui#5734) as the cross-seat transfer of a ruled card.packages/spec is this lane's sole owner, so the spec leg lands here; the downstream objectui leg is objectui#5435, now pm:blocked on this card.

Filed unassigned. Not claimed.

The ruling (binding — not re-opened here)

Maintainer, 2026-08-22, decision-inbox digest, recorded on objectui#5435. The full inbox (46 cards) was presented with per-card four-axis recommendations and accepted as a batch, verbatim:

「接受所有」

Ruled: Option A — the spec adopts the three keys.UserActionsConfigSchema gains group, hideFields and rowColor, so the three toolbar affordances ListView already honours become authorable in a spec-valid document and the runtime fold's output passes the save gate (declared = enforced closes).

Option B (retiring three live product capabilities) and Option C (permanent double-spelling) were not taken. The triage execution note routes this as a clause ② contract card at contract-review tier, with objectui's fold/renderer tests aligning on the pin afterwards.

Why the spec leg exists

packages/core/src/utils/normalize-list-view.ts in objectui folds legacy bare show* flags into the canonical userActions block via SHOW_FLAG_TO_USER_ACTION. Four of the seven targets land on keys the spec declares; three do not:

showGroup: 'group', showHideFields: 'hideFields', showColor: 'rowColor',

packages/plugin-list/src/ListView.tsx (toolbarFlags) reads all three regardless. So a stored legacy view carrying showGroup: false comes out of the fold as userActions: { group: false } — which ListViewSchemarejects by name. Anything that normalizes then validates hits a refusal on a key the runtime itself just wrote.

Re-measured at this repo's head (not the card's 17.0.0 reading)

origin/main52a41b7. packages/spec/src/ui/view.zod.ts:853UserActionsConfigSchema is a strictObject (which is why the refusal is unrecognized_keys), declaring exactly eight keys:

sort · search · filter · refresh · rowHeight · addRecordForm · editInline · buttons

group, hideFields, rowColor are absent. The premise holds at head, not merely at the 17.0.0 the card measured.

Reproduce (from objectui#5435, control included — the control is what makes the three refusals mean "this key" rather than "this schema refuses everything"):

node --input-type=module -e "
import { ListViewSchema, UserActionsConfigSchema } from '@objectstack/spec/ui';
const uk = r => (r.error?.issues ?? []).filter(i => i.code === 'unrecognized_keys').flatMap(i => i.keys ?? []);
for (const k of ['group','hideFields','rowColor']) {
const r = ListViewSchema.safeParse({ name:'my_view', label:'My View', columns:[{field:'name'}], userActions:{ [k]: true } });
console.log(k, r.success, uk(r), (r.error?.issues??[]).length);
}
console.log('control sort', ListViewSchema.safeParse({ name:'my_view', label:'My View', columns:[{field:'name'}], userActions:{ sort:true } }).success);
"

⚠️ Named premise the implementer must confront — two of the three key names are already taken, with different shapes

This is not a re-adjudication of A; it is a measurement A's implementation will hit on its first stroke. Both spellings already exist in packages/spec/src/ui carrying non-boolean shapes:

keyexisting declarationshapeobjectui's fold produces
hideFieldscomponent.zod.ts:853 — "Field names to omit from the body"z.array(z.string())boolean (ua?.hideFields === true)
rowColorcomponent.zod.ts:2132 — "Row color rules" (plus RowColorConfigSchema, view.test.ts:1937)z.unknown() / config objectboolean (ua?.rowColor === true)

So adopting them verbatim as booleans under userActions creates same-name / different-shape pairs across the spec — precisely the hazard object-strictness-batch20.test.ts:410 exists to catch, in its own words: "the same key NAME, disjoint vocabulary … which is exactly why an author who learned one writes it on the other."

Instruction, per the ruling-with-premise discipline: implement A as ruled. If the collision forces a shape or naming change (e.g. the three land under a different spelling, or hideFields takes the array shape rather than the boolean the fold emits), stop and report the fork on this card — ⛔ do not silently pick a spelling, and ⛔ do not fall back to option B or C, both of which were explicitly not taken.

Pins that move when the three keys land

  1. packages/spec/src/data/object-strictness-batch20.test.ts:410-417 — its comment enumerates the exact eight-key vocabulary and goes false on adoption. The assertion itself ("nothing overlaps" with the object block's create/import/edit/delete/exportCsv) survives, but must be re-checked, not assumed.
  2. packages/spec/src/ui/view.test.ts:2491+UserActionsConfigSchema default-value test enumerates all eight keys; the three new ones need declared defaults and pins. Note the existing defaults split true (sort/search/filter/refresh/rowHeight) from false (addRecordForm/editInline); objectui's reads imply group defaults true (ua?.group !== false) while hideFields/rowColor default false (=== true). That asymmetry is load-bearing and should be pinned, not normalised away.
  3. packages/spec/src/ui/page.zod.ts:368 consumes UserActionsConfigSchema — check the widening does not change page-level strictness readings.

Acceptance criterion (one line, executable)

ListViewSchema.safeParse({name:'my_view',label:'My View',columns:[{field:'name'}],userActions:{group:false,hideFields:true,rowColor:true}}).success === true, with the object-block control (rejectOnObject({userActions:{sort:false}}) still naming VIEW) still red.

Downstream

Notes for triage

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions