Skip to content

Commit 6f5719e

Browse files
feat(types,plugin-grid)!: object-grid and list-view conditionalFormatting take the spec list view's { condition, style } rule only; the native dialect is refused by name (objectui#11533) (#11540)
Fixes #11533 Clause-②: no (narrowing) `conditionalFormatting` on `object-grid` (`ObjectGridSchema`, and so the `object-view` `table` slot built from it) and on `list-view` takes ONE rule dialect, the spec list view's `{ condition, style }`. The native `{ field, operator, value }` rule, its `expression` template predicate and a colour written at the top level of a rule (`backgroundColor`, `borderColor`, `textColor`) are retired with no alias window and refused by name. This executes triage's ruling `5965301211` (retire, as objectui#11522 ruled for the kanban board in `5963861071`; never widen the spec's list-view member). Stored stock is not authoring: `resolveConditionalFormatting` keeps every arm, so a grid or list view STORED with a native rule still paints, byte for byte as before. ## What changes | Rule on `ObjectGridSchema`, an `object-view` `table` or a `list-view` | Before (BASE `8bfc0012`) | After | |---|---|---| | `{ condition, style }` (string, envelope or `''` condition) | accepted on every face | accepted, unchanged | | native `{ field, operator, value, backgroundColor }` | accepted on every face, tolerant and strict | refused at `field`, `operator`, `value` and `backgroundColor`, each naming the retirement | | `{ expression, backgroundColor }` | refused as a bare `invalid_union` at the rule | refused at `expression` and `backgroundColor` by name | | flat CEL `{ condition, backgroundColor }` | refused as a bare `invalid_union` at the rule | refused at `backgroundColor` by name, and `style` (required) | | `{ condition, style, backgroundColor }` | tolerant faces accepted it (the key was stripped, then painted by the shared resolver anyway); strict face refused it as `invalid_union` | refused at `backgroundColor` on every face | | `{ condition, style, label }` | tolerant faces accepted it, strict face refused it | refused on every face, `unrecognized_keys` with the spec rule's own message | The faces read: the `ObjectGridSchema` and `ListViewSchema` mirrors, and `safeValidateSchema` (tolerant) and `StrictAnyComponentSchema` (strict), each on a `list-view` node and on an `object-view` `table` slot. Both columns were read with a throwaway probe on each tree; it was deleted and never committed. **Not judged here, and unchanged: the authored `object-grid` node's `properties` bag.** Its members are `@objectstack/spec`'s `ComponentPropsMap['object-grid']` row by reference (objectui#11276). The installed 17.5.0 row types `conditionalFormatting` as `unknown`, so the bag accepted all six rules before and after. It carries no objectui copy of the rule, and objectui does not narrow a spec row (see Acceptance notes). - **zod (`@object-ui/types`, `src/zod/objectql.zod.ts`).** The grid's and the list view's shared rule `ConditionalFormattingRuleSchema` (module-private, one declaration for both mirrors) is no longer a union. It is the spec `ListViewSchema.conditionalFormatting` element (`SpecListViewRuleSchema`, now declared once above the grid mirror and `.extend()`-ed by both this rule and the kanban rule), so it inherits the spec rule's strictness and its `style` map. Two things are layered on top: `condition` stays `SpecRuleConditionSchema` (objectui's `z.string()` arm before the spec slot, so a string is not canonicalized and `''` is still accepted, objectui#10946), and seven retirement tombstones: `field`, `operator`, `value`, `expression`, `backgroundColor`, `borderColor`, `textColor`, through a `gridRuleKeyRetired` message builder. The grid member's `.describe()` and the "intentionally broader than spec (migration deferred)" docblock lines go; `conditionalFormatting` leaves that list. - **TS (`objectql.ts`).** `ConditionalFormattingRule` is an interface that extends `SpecConditionalFormattingRule` and declares the same seven keys `?: never`. `ObjectUIConditionalFormattingRule` is deleted and dropped from the root barrel (TS2305 for an importer). tsc reports each retired key at the key (TS2322). - **`@object-ui/plugin-grid`.** The `object-grid` registration's `conditionalFormatting` input description stops advertising both forms: it describes the one rule and names the retired spellings. No render path changes. - **Docs.** A `conditionalFormatting` row in `content/docs/api/schema-reference.md`'s ObjectGridSchema table; `packages/types/src/zod/README.md` drops the member from the broader-than-spec list. - **One changeset** (`11533-grid-listview-rule-dialect-retired.md`): `@object-ui/types` and `@object-ui/plugin-grid` `minor`, BREAKING in the first line, before/after table, respelling, the bag and the paint stated as unchanged. ## Mechanism hypotheses, measured **H1, the faces and the shared union (a type-checker census, not grep).** A TypeScript LanguageService over all 5,034 tracked TS sources, under the root tsconfig's paths (which resolve `@object-ui/types` to source): `findReferences` on each declaration, plus every `conditionalFormatting` identifier resolved to the declaration it binds. | Declaration | Consumers | How each moved | |---|---|---| | zod `ConditionalFormattingRuleSchema` | `ObjectGridSchema.conditionalFormatting` and the `ListViewSchema` override (2 refs); through the grid mirror's `.shape` it also builds the `object-view` `table` slot | all narrow with it. No other zod schema embeds either member: `ObjectGridBlockSchema` takes six other mirror members by reference, not this one | | TS `ConditionalFormattingRule` | `ObjectGridSchema.conditionalFormatting`, `NamedListView.conditionalFormatting`, the root barrel, the 10946 relay pin, a docblock link in the kanban rule | narrowed in place (same name, now the one-dialect interface) | | TS `ObjectUIConditionalFormattingRule` | the old union and the root barrel only | deleted | | `ObjectGridSchema.conditionalFormatting` member (12 refs) | `ObjectGrid.tsx` (5 reads, each handed to the resolver or the harvest as an unknown array), plugin-view's route-2 relay, tests | follows the type; plugin-grid and plugin-view type-check green | | zod `ListViewSchema` member (10 refs) | `ListView.tsx` (`evaluateConditionalFormatting`'s rules parameter, the harvest, the relay into its grid node), app-shell `ObjectView.tsx`'s relay (its other operand is `any`), tests | follows the type; plugin-list type-check green | | `resolveConditionalFormatting` | types its own input (`ConditionalFormattingRuleLike` in `@object-ui/core`) | does NOT narrow, by construction | | `listViewPredicates` (the predicate-field harvest) | reads `readonly unknown[]` | does NOT narrow: it still projects a stored native rule's field | Can the kanban rule and the grid/list rule become one schema? Measured: same base element (now one declaration), same `condition` arms (pinned arm by arm), same `style` (identity, pinned). They differ in exactly two things: the carrier and card each message names, and `expression`, which the grid and list rule carried and the kanban rule never did (kanban refuses it through the spec's own unrecognized-keys message with its "Did you mean `expression` → `condition`?"). Folding them would change `object-kanban`'s landed refusal text and its `expression` path, a surface objectui#11522's contract record judged on its landing head. Not done here; the report carries the option. **H2, the census.** Two instruments. (1) An AST pass (TypeScript parser) over the 59 TS files that mention `conditionalFormatting` or `ConditionalFormattingRule`: every object literal with a `field` + `operator` pair, a top-level colour beside a predicate key, or an `expression` beside a style key: 50 hits, each triaged by hand. The filter literals it also caught (`UserFilterTabRuleSchema`, `ReportFilterSchema`, filter fixtures) are its live control. (2) A text pass over every other tracked file that mentions the member: the 500 characters after each token, for a retired key in any syntax: 1 window, the kanban row of the schema reference, which teaches the forms as refused. | Where | Writers at BASE | Disposition | |---|---|---| | `unmirrored-declared-round6-6152.test.ts` | 1 native (the `valid` row) and 1 native with a wrong operator (the `invalid` row) | `valid` respelled `{ condition: "record.status == 'open'", style: { backgroundColor: '#fee2e2' } }`; `invalid` replaced by `{ condition: "record.status == 'late'", style: 'color: red' }`, because its wrong value (an operator outside the native enum) has no member left to be wrong in | | `export-options-spec-parity.test.ts` | 1 native (the neighbour-member control) | the native line removed; the control keeps the `{ condition, style }` line and the non-array refusal | | `objectViewHostSurface.test.tsx` (plugin-view) | 1 forwarding sentinel `{ field, operator: 'eq', value, color }` | respelled `{ condition: "record.stage == 'won'", style: { color: '#ff0000' } }` | | `gridRowDecorationMembers-8071.test.tsx` (plugin-grid) | 11 rules carrying retired members (the predicate precedence, the colour members) | kept and re-framed as STORED-rule reads; a new block pins refused-at-authoring beside painted-at-render | | `spec-expression-wire-slots-10946.test.ts` | none; it indexed the union's second arm | reads `condition` straight off the rule's shape | | `listConditional.test.ts`, `predicate-fields.test.ts` (core) | 10 and 1 | untouched: the resolver's arms and the harvest stay | | `ConditionalFormattingEditor.test.tsx` (app-shell) | 4 | untouched: Studio's upgrade-on-read of stored rules | | `kanban-conditional-formatting.test.ts`, `unmirrored-declared-round3-6152.test.ts` | 4 and 3 | untouched: the kanban refusals, and the report component's own rule (a different carrier) | | plugin-grid registration description | taught both | rewritten | | objectui `examples/`, `apps/console` non-test code, `skills/` | zero; controls fire (`object-grid` / `list-view` in 5 `examples/` files, `object-grid` in 1 console file and 3 `skills/` files) | none | | objectstack `examples/` at `4916168` (my own sparse clone) | zero. The one `conditionalFormatting` (app-showcase `field-zoo.view.ts`) is `{ condition, style }`; controls fire (`object-grid` / `list-view` view files, `{ field, operator, value }` filter literals) | none | | `objectstack-ai/hotcrm` | NOT MEASURED: not in this container. Triage measured zero | none | **H3, stored rules still paint.** Before and after, through the real `ObjectGrid` and through the real `ListView` handing its node to the real grid, for all six rules of the table: the readings are byte-identical, for example `native: Alpha=[background-color: rgb(1, 2, 3);] Beta=[null]` on both trees and both renderers. Read paths that could validate a stored view: none on the render path (`SchemaRenderer` runs only the dev-only structural `validateSchema`; nothing parses a grid or list-view node). Outside `packages/types`, the only non-test callers of objectui's zod faces are the CLI's `objectui validate` / `objectui check` (authoring doors) and three unrelated schema imports (a dashboard widget, a dashboard designer door, a map config). Studio's `clientValidation` judges a view draft against `@objectstack/spec/ui`'s `ViewSchema`, not objectui's face, so this change does not reach it; the installed spec's list-view rule already refuses the native rule there (`unrecognized_keys` naming `field`, `operator`, `value`, `backgroundColor`), at BASE as now. Premise held; nothing for the seat. **H4, the refusals.** One arm, so each refusal sits at the retired key's own path; the messages are byte-identical on the tolerant and strict faces and on both carriers. Quoted, with backticks as in the source: - at `conditionalFormatting.N.field` (`operator` and `value` the same, key swapped): > `field` belongs to the native rule dialect `{ field, operator, value }`, which `conditionalFormatting` on `object-grid` and `list-view` no longer accepts: RETIRED (objectui#11533), with no alias window. A rule is `{ condition, style }` — a CEL `condition` over `record.*` and a CSS `style` map, the rule `@objectstack/spec`'s `ListViewSchema.conditionalFormatting` declares. Respell `{ field: 'priority', operator: 'equals', value: 'high', backgroundColor: '#fee2e2' }` as `{ condition: "record.priority == 'high'", style: { backgroundColor: '#fee2e2' } }` (`not_equals` is `!=`, `greater_than` is `>`, `less_than` is `<`, `contains` is `.contains(…)`, `in` is `record.f in [ … ]`). - at `.expression`: > `expression` is the template predicate of the native rule dialect, which `conditionalFormatting` on `object-grid` and `list-view` no longer accepts: RETIRED (objectui#11533), with no alias window. A rule is `{ condition, style }` — […same sentence…]. Write the predicate as the rule's `condition`, in CEL over `record.*` with no `${…}` wrapper: `{ expression: '${record.amount > 1000}', backgroundColor: '#fee2e2' }` is `{ condition: 'record.amount > 1000', style: { backgroundColor: '#fee2e2' } }`. - at `.backgroundColor` (`borderColor` the same; `textColor` ends `style: { color }`): > `backgroundColor` is a colour written at the top level of the rule, which `conditionalFormatting` on `object-grid` and `list-view` no longer accepts: RETIRED (objectui#11533), with no alias window. A rule is `{ condition, style }` — […same sentence…]. Move the colour into the rule's CSS map: `style: { backgroundColor }`. - Beside these, a rule with no `condition` also draws `invalid_union` at `.condition` and `invalid_type` at `.style` (both required); an undeclared key draws the spec rule's own message, "Unrecognized key(s) on this conditional formatting rule: `label`. …". ## The pins - `packages/types/src/__tests__/grid-list-view-conditional-formatting-11533.test.ts` (new, 44 tests): on each of the six faces, the `{ condition, style }` control (string and envelope); the native rule refused at each retired key with the message starting with the key's name and containing `RETIRED (objectui#11533)`, `{ condition, style }` and the respelling, while the accepted rule at index 0 of the same document draws no issue; `expression` refused with its respelling; the flat CEL rule refused at the colour key with no `invalid_union` at the rule; all three colour keys refused beside a `style`; the spec's unrecognized-keys message. The reference: the two mirrors share one rule object, `style` is the spec element's own member (identity), `condition`'s second arm is the spec slot and both arms are the kanban rule's, with the control that `condition` is not the bare slot. The TS face: `@ts-expect-error` on each retired key, on a native rule in an `ObjectGridSchema` and a `ListViewSchema` literal, `keyof` equality with the zod input, and the three members (`ObjectGridSchema`, `ListViewSchema`, `NamedListView`) carrying the one rule. - `gridRowDecorationMembers-8071.test.tsx` (plugin-grid): the stored native rule refused by `ObjectGridSchema` at `field` with the retirement, AND painted by the real grid on the matching row only; the respelling accepted and painting the same row the same colour as the control. - `ListView.storedRuleDialect-11533.test.tsx` (plugin-list, new): a stored list view's native rules refused by `ListViewSchema` at `field`, AND handed to its `object-grid` node untouched; the respelling as the control. - `apps/console/.../registry-inputs-spec-parity.test.ts`: the `object-grid.conditionalFormatting` member-pin prose says the retired members are stored-rule reads now (prose only, no logic). ## Pending changesets (read, not recalled) Read as text: every pending entry naming `conditionalFormatting`, `ConditionalFormattingRule`, the resolver, a rule union, "broader than spec", or a file this diff touches (41 by symbol and prose, and the 51 that `check:changeset-claims` lists by file name, each grepped for the symbols this diff moves). - **Dated note appended:** `10946-expression-wire-slots-by-reference.md`. Its "the list view's and the kanban board's rule unions share one `condition` schema" now describes no union at all; the note keeps every condition claim, which still holds and is still pinned. (+16/−0) - **Dated note appended:** `11522-kanban-rule-dialect-retired.md`. Its "Not changed" paragraph gives "because the grid's and the list view's rule union still declares them" as the reason the evaluator keeps every arm; the reason is now the stored-rule compatibility read. (+15/−0) - **Left alone, still true:** `7664-kanban-arm-plugin-dialect.md` (kanban only, already noted by objectui#11522); `6152-objectgrid-round6-mirrored.md` (says the mirror declares the key typed as the twin types it, still true, and makes no dialect claim); `6152-unmirrored-round3.md` (the report's rule); `11276-object-grid-properties-bag.md` (the bag, which did not move); `10976`, `10885`, `10758`, `7928` (relays and named views, unchanged); `10689` ×2, `7216`, `7727`, `8167`, `8972`, `9318`, `row-predicate-phase2-record-only-5741` (harvest and condition scope, unchanged); `member-pins-object-grid-row-layer-8071` (names the key only); `8313`, `8801`, `9242`, `8932-retire-kanban-enhanced`, `7742`, `7322`, `4730`, `10275`, `8818` (kanban, i18n or FLS readings that name the key only); `4535` and `6956` (name `export-options-spec-parity.test.ts` for its export-options rows, not the neighbour control); the entries naming `KanbanImpl.tsx` (card, lane and click behaviour, none about the evaluator comment); `10719` (a different "rule union"). - `check:changeset-overwrite` reports the two modified entries with their declarations unchanged, which is the appended-note case. ## Verification - Final head `ebb37b7b`. It differs from `aab1cc53` (where the suites below ran) by one test file, `ListView.storedRuleDialect-11533.test.tsx` (`as any` to `as never`), re-run at `ebb37b7b` with plugin-list's type-check. - `pnpm exec vitest run --maxWorkers=2 packages/types/ packages/plugin-grid/` at `aab1cc53`: 527 files, 10,923 tests passed (`VERDICT command-exit 0`). - 41 targeted files at `aab1cc53` (the plugin-list, plugin-view, plugin-kanban, app-shell, core, components, react, i18n, console and schema-catalog files that read the member or the changed files): 968 tests passed. 19 `scripts/__tests__` suites that read the changed files (one-authority, doc-links, doc-snippet, readme-exports, changeset gates and others): 1,093 tests passed. - `type-check` (each script name echoed, each exit read separately), after `turbo run build` of the dependency closure: `@object-ui/types` (src, examples and test configs; `--listFilesOnly` confirms the new pin file is compiled), `plugin-grid`, `plugin-list` (again at `ebb37b7b`), `plugin-view`, `plugin-kanban`: exit 0. - Reverse leg, committed first, restored from `HEAD`: a native rule appended to plugin-grid's `specExpressionWire-10946.test.tsx` as an `ObjectGridSchema` literal turned plugin-grid's test type-check red (exit 2, TS2322 at `field`, `operator`, `value`), proving it reads the rebuilt declaration; after restore the blob equals `HEAD`'s and `git diff HEAD` is empty. - After the scoped build (35/35 tasks): `check:doc-snippets` (777 of 777 blocks, 0 failed), `check:doc-examples` (exit 0, ledger-exact), `check:doc-types` (exit 0). With the console built: `check:sdui-registration-pins` exit 0, `check:component-surface-parity` (report-only) exit 0 with no `object-grid` row. - At `ebb37b7b`, all exit 0: `check:new-line-citations` (0 new), `check:control-bytes`, `check-changeset-no-major`, `check-changeset-presence`, `check-changeset-fixed`, `check:changeset-claims`, `check:pending-changeset-literals`, `check:test-path-roots`, `check-type-check-coverage`, `check-doc-links`, `check:spec-symbols`, `check:doc-fences`, `check-doc-expression-carriage`, `check:readme-exports`, `check-changeset-overwrite`; `check-governed-queue-guard --test` over the 18 paths: NOT GOVERNED. - NOT MEASURED: the repo-wide lint and the full test matrix (CI's). A narrowed eslint pass over the 13 changed TS files found no error this diff introduces. ## Acceptance notes - **The authored `object-grid` bag.** At the installed spec the bag still accepts any rule, because the spec row's member is `unknown`; objectstack `main` (`4916168`) types it as the list view's own member by reference, so the bag narrows with the `@objectstack/*` bump that carries objectstack PR #21463. Carrier: objectui#11438 (the 17.6.0 trunk) or the bump after it. - **The skills guide.** `skills/objectui/guides/schema-expressions.md`'s "Legacy shapes are translated" bullet says the native and `expression` forms "still work" under a heading naming list, grid and kanban: true of the evaluator, now false of authoring on all three carriers. `skills/**` is governed and this PR stays ungoverned, so it is left; it is the same item as objectui#11522's contract record escalation (c). - **The evaluator's native arms** now have no authored carrier on any objectui face; they serve stored rules only. Their removal is the separate act after a stored-stock census, as triage ruled. - **The report component's own rule** (`ReportComponentSchema.conditionalFormatting`, a `{ field, operator, value, backgroundColor?, textColor? }` shape `ReportViewer` reads per cell) is a separate declaration, and neither spec line declares a report rule member. Untouched. Carrier: none. - `ROADMAP.md` records the dual-format union as delivered; it is a dated log and is left. - Files outside the claim's list: `packages/plugin-kanban/src/KanbanImpl.tsx` (comment only: `getCardStyles`' stated reason became false), `ListView.storedRuleDialect-11533.test.tsx` (the H3 pin), `objectViewHostSurface.test.tsx` (sentinel respelled), the console pin prose. Session: `https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC` --- _Generated by [Claude Code](https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 160c6c6 commit 6f5719e

18 files changed

Lines changed: 746 additions & 93 deletions

‎.changeset/10946-expression-wire-slots-by-reference.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,19 @@ reference identity of the spec arm) still holds on `object-kanban`, and
4646
`spec-expression-wire-slots-10946.test.ts` still pins it there, now reading
4747
`condition` straight off the rule's shape. The rest of this entry is kept as
4848
the reading of this change.
49+
50+
⚠️ **Dated note, 2026-10-03 — the list view's (and the grid's) rule is no longer a union either — objectui#11533.**
51+
At this change the list view's rule, which `ObjectGridSchema` shares, was also a
52+
union of two dialects (the native `{ field, operator, value, … }` comparison and
53+
`{ condition, style }`), so the sentence above about "the list view's and the
54+
kanban board's rule unions" described two unions. Now neither is one: the grid's
55+
and the list view's rule is ONE object, the spec list view's `{ condition, style }`
56+
rule by reference, with the native rule, its `expression` and a top-level colour
57+
refused by name. It reads the same `condition` schema, so everything this entry says
58+
about the condition (the `z.string()` first arm, the envelope, the `''` control,
59+
the reference identity of the spec arm) still holds on `list-view` and
60+
`object-grid`, and `spec-expression-wire-slots-10946.test.ts` now reads
61+
`condition` straight off the list view's rule as well. `ConditionalFormattingRule`
62+
is still built on `SpecConditionalFormattingRule`, so the widening this entry
63+
describes still reaches it. The rest of this entry is kept as the reading of this
64+
change.

‎.changeset/11522-kanban-rule-dialect-retired.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,18 @@ Each refusal message names the key, objectui#11522 and the `{ condition, style }
2828
**Not changed: what the board paints.** The shared evaluator, `resolveConditionalFormatting` in `@object-ui/core`, keeps every arm, because the grid's and the list view's rule union still declares them. A `{ condition, style }` rule styles a card exactly as before. A rule that a relay hands the board, for example a list view's, is painted as before too. Only the authored `object-kanban` member narrowed.
2929

3030
Pins: `packages/types/src/__tests__/kanban-conditional-formatting.test.ts` (turned around) pins both refusals on all three zod faces, the spec rule's identity and strictness, and the TS face. `ObjectKanban.structuredMembersReachTheirSinks-8313.test.tsx` and `objectFieldsIsAPropNotASchemaKey-7742.test.tsx` in `@object-ui/plugin-kanban` draw the respelled rules through the real board and assert the same cards are painted.
31+
32+
⚠️ **Dated note, 2026-10-03 — the grid and the list view retired the same dialect — objectui#11533.**
33+
At this change the grid's and the list view's rule was still a union that
34+
declared the native `{ field, operator, value }` comparison and the top-level
35+
colour keys, and the "Not changed" paragraph above gives that as the reason the
36+
shared evaluator keeps every arm. Now that rule retires them too: `object-grid`
37+
and `list-view` take the spec list view's `{ condition, style }` rule only, and
38+
the native rule, its `expression` and a top-level colour are refused by name.
39+
`resolveConditionalFormatting` still keeps every arm, for a different reason: it
40+
is a compatibility read for rules already STORED in the native dialect, so a
41+
board, grid or list view a relay or a stored view hands such a rule still paints
42+
it. The `condition` the kanban rule shares with the grid's and the list view's
43+
rule (called their "`{ condition, style }` arm" above) is still the same schema;
44+
it is now their whole rule rather than one arm of it. The rest of this entry is
45+
kept as the reading of this change.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
'@object-ui/types': minor
3+
'@object-ui/plugin-grid': minor
4+
---
5+
6+
**BREAKING for authors of `object-grid` and `list-view` row rules, released as `minor`.** `conditionalFormatting` on `ObjectGridSchema` (and so on the `object-view` `table` slot built from it) and on `list-view` takes ONE rule dialect, the spec list view's `{ condition, style }`: a CEL `condition` over the row's `record.*` and a CSS `style` map. The native dialect it used to take beside it is retired with no alias window and refused by name (objectui#11533), as objectui#11522 did for `object-kanban`.
7+
8+
| Rule on `ObjectGridSchema`, an `object-view` `table` or a `list-view` | Before | Now |
9+
|---|---|---|
10+
| `{ condition, style }` | accepted | accepted, unchanged |
11+
| native `{ field, operator, value, backgroundColor?, textColor?, borderColor?, expression? }` | accepted on every face | refused at `field`, `operator`, `value` and at each colour key and `expression` written |
12+
| `{ expression, backgroundColor }` (no native triple) | refused as a bare union failure | refused at `expression` and the colour key, naming the retirement |
13+
| flat CEL `{ condition, backgroundColor }` (a colour beside `condition`, no `style`) | refused as a bare union failure | refused at the colour key, naming the retirement |
14+
| `{ condition, style, backgroundColor }` | accepted by `safeValidateSchema` (the colour key was stripped from the parse, then painted by the grid anyway) | refused at the colour key |
15+
| `{ condition, style, label }` (any other undeclared key) | accepted by `safeValidateSchema` | refused as an unrecognized key, with the spec rule's own message |
16+
17+
Each refusal message names the key, objectui#11533 and the `{ condition, style }` respelling, on `ObjectGridSchema`, `ListViewSchema`, `safeValidateSchema` and `StrictAnyComponentSchema` alike.
18+
19+
**Respelling.** `{ field: 'priority', operator: 'equals', value: 'high', backgroundColor: '#fee2e2' }` is `{ condition: "record.priority == 'high'", style: { backgroundColor: '#fee2e2' } }`. `not_equals` is `!=`, `greater_than` is `>`, `less_than` is `<`, `contains` is `record.f.contains(…)` and `in` is `record.f in [ … ]`. An `expression` predicate becomes the `condition`, written as CEL over `record.*` with no `${…}` wrapper. A top-level colour moves into `style`; `textColor` is `style.color`.
20+
21+
**Not judged here: an authored `object-grid` node's `properties` bag.** Its members are `@objectstack/spec`'s `ComponentPropsMap['object-grid']` row by reference (objectui#11276), and the installed spec types that row's `conditionalFormatting` as `unknown`, so the bag judges the member as the installed spec does. objectui does not narrow the spec's row.
22+
23+
**`@object-ui/types`.**
24+
25+
- `ConditionalFormattingRule` (exported from `@object-ui/types`) is now an interface that extends `SpecConditionalFormattingRule`, with `field`, `operator`, `value`, `expression`, `backgroundColor`, `borderColor` and `textColor` declared `?: never`. It used to be the union of `ObjectUIConditionalFormattingRule` and `SpecConditionalFormattingRule`. It types the `conditionalFormatting` members of `ObjectGridSchema` and `NamedListView`; the `ListViewSchema` type's member is the zod rule's input, with the same keys. Every published type built on those members follows, for example the rules parameter of `@object-ui/plugin-list`'s `evaluateConditionalFormatting`.
26+
- `ObjectUIConditionalFormattingRule` is removed from `@object-ui/types`. Importing it is a compile error (TS2305).
27+
- The zod rule the `ObjectGridSchema` and `ListViewSchema` mirrors (both on the `@object-ui/types/zod` barrel) share is module-private. It is the spec `ListViewSchema.conditionalFormatting` rule taken by reference and extended, not a union. It keeps the spec rule's strictness and its `style` map. Its `condition` is the same schema as before, so a string condition is still not canonicalized into an envelope and `''` is still accepted. The seven retired keys are retirement tombstones.
28+
29+
**`@object-ui/plugin-grid`.** The `object-grid` registration's `conditionalFormatting` input description now describes the one rule and names the retired spellings. `ObjectGridProps.schema.conditionalFormatting` follows the narrowed type.
30+
31+
**Not changed: what a grid or a list view paints.** The shared evaluator, `resolveConditionalFormatting` in `@object-ui/core`, keeps every arm as a compatibility read for rules already STORED in the native dialect, and nothing on the render path parses a stored view, so a grid or list view saved with a native rule still paints exactly as before. A `{ condition, style }` rule paints exactly as before too. Only the authoring faces narrowed.
32+
33+
Pins: `packages/types/src/__tests__/grid-list-view-conditional-formatting-11533.test.ts` pins every refusal on the six zod faces and carriers, the spec rule's identity and strictness, and the TS face. `gridRowDecorationMembers-8071.test.tsx` in `@object-ui/plugin-grid` pins a stored native rule refused at authoring and painted by the real grid, with its respelling as the control, and `ListView.storedRuleDialect-11533.test.tsx` in `@object-ui/plugin-list` pins a stored list view handing its native rules to its grid untouched.

‎apps/console/src/__tests__/registry-inputs-spec-parity.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2810,7 +2810,7 @@ const MEMBER_PINS: Record<string, MemberPin> = {
28102810
},
28112811
'object-grid.conditionalFormatting': {
28122812
file: 'packages/plugin-grid/src/__tests__/gridRowDecorationMembers-8071.test.tsx',
2813-
pins: 'The members read inside ONE formatting rule, asserted on the `style` attribute of the `<tr>` the grid paints. THREE alternative predicate members with a PRECEDENCE between them — one rule carrying `condition`, `expression` and the native `field`/`operator`/`value` triple, each naming a DIFFERENT row, is decided by `condition`; drop it and `expression` decides; drop that and the triple does — so a renderer reading any two of them as equivalent is red in a specific direction rather than merely unpinned. FOUR style members, and the sharp one is a RENAME: `backgroundColor` and `borderColor` keep their authored names on the way to the DOM while `textColor` is read as the CSS `color`, so keeping the authored spelling hands React a key it silently drops — same rules, same background, the text colour gone with nothing thrown. `style` is pinned as the BASE the three colour members override rather than replace (a non-colour member of it survives alongside the override). Two rule-list facts complete it: FIRST-MATCH-WINS with no merge (the second matching rule\'s own `textColor` never lands) and a rule carrying NO predicate member is SKIPPED rather than read as always-true, with the rule after it still deciding. The registration is an `array` arm and the spec row constrains nothing inside a member, so the read site is the whole member contract (objectui#8071 slice 12).',
2813+
pins: 'The members read inside ONE formatting rule, asserted on the `style` attribute of the `<tr>` the grid paints. THREE alternative predicate members with a PRECEDENCE between them — one rule carrying `condition`, `expression` and the native `field`/`operator`/`value` triple, each naming a DIFFERENT row, is decided by `condition`; drop it and `expression` decides; drop that and the triple does — so a renderer reading any two of them as equivalent is red in a specific direction rather than merely unpinned. FOUR style members, and the sharp one is a RENAME: `backgroundColor` and `borderColor` keep their authored names on the way to the DOM while `textColor` is read as the CSS `color`, so keeping the authored spelling hands React a key it silently drops — same rules, same background, the text colour gone with nothing thrown. `style` is pinned as the BASE the three colour members override rather than replace (a non-colour member of it survives alongside the override). Two rule-list facts complete it: FIRST-MATCH-WINS with no merge (the second matching rule\'s own `textColor` never lands) and a rule carrying NO predicate member is SKIPPED rather than read as always-true, with the rule after it still deciding. ⚠️ Since objectui#11533 the AUTHORED member is ONE dialect, `{ condition, style }`: `expression`, the native triple and the three top-level colour members are refused by name on `ObjectGridSchema` and the list view (`packages/types/src/__tests__/grid-list-view-conditional-formatting-11533.test.ts`), so the rows that write them pin STORED-rule reads, which the resolver keeps as compatibility arms; a last block pins the two facts side by side — the stored native rule refused at authoring and painted at render, with its respelling painting the same row. The registration is an `array` arm and the installed spec row constrains nothing inside a member, so the read site is the whole member contract (objectui#8071 slice 12).',
28142814
},
28152815
'object-grid.operations': {
28162816
file: 'packages/plugin-grid/src/__tests__/gridOperationsMembers-8071.test.tsx',

‎content/docs/api/schema-reference.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,7 @@ its row below).
810810
| `editable` | `boolean` | Enable inline cell editing. |
811811
| `grouping` | `GroupingConfig` | Row grouping configuration. **Server-side**: the set of groups, every group count and every per-group aggregation come from the group header query (`dataSource.queryGroupHeaders`), and each group's rows are paged by the server. Rows handed in whole are grouped in the browser (exact); over a data source with no header query, a grid that fetches its own rows refuses grouping with an error naming `queryGroupHeaders`. |
812812
| `frozenColumns` | `number` | Number of columns frozen on scroll. |
813+
| `conditionalFormatting` | `ConditionalFormattingRule[]` | Row styling rules, each `{ condition, style }` — a CEL `condition` over the row's `record.*` and a CSS `style` map, the rule a list view declares; the first matching rule styles the row. The native `{ field, operator, value }` rule, its `expression`, and a colour written beside `condition` (rather than inside `style`) are retired (objectui#11533): `@object-ui/types` refuses them by name on `ObjectGridSchema`, the `object-view` `table` slot and `list-view`. `{ field: 'priority', operator: 'equals', value: 'high', backgroundColor: '#fee2e2' }` is `{ condition: "record.priority == 'high'", style: { backgroundColor: '#fee2e2' } }`. In an authored node's `properties` bag the member is `@objectstack/spec`'s row member, judged by the installed spec. A grid stored with a retired rule still paints it. |
813814
| `navigation` | `ViewNavigationConfig` | SPA navigation configuration. |
814815
| `emptyState` | `{ title?, message?, icon? }` | Drawn in place of an empty table: a Lucide `icon`, a `title` (default: the table's "No results found") and a `message` (default: none). Not drawn when a term in the grid's own server-side search box emptied it — the table and its search box stay (objectui#11068). **Not authorable in a document today:** the spec's `object-grid` row does not declare it, so `objectui validate` refuses it in the `properties` bag, and the strict face refuses it on the node; a host mounting `<ObjectGrid schema={…}>` or composing the node in code can set it. |
815816

‎packages/plugin-grid/src/__tests__/gridRowDecorationMembers-8071.test.tsx‎

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,20 @@
3030
* one of which is RENAMED on the way to the DOM. That is the objectui#8068
3131
* criterion: constrain the shape the RENDERER READS, not the registration.
3232
*
33+
* ## Most of a rule's members are STORED-rule reads now (objectui#11533)
34+
*
35+
* Since objectui#11533 the grid's authoring faces take ONE rule dialect, the
36+
* spec list view's `{ condition, style }`: `expression`, the native
37+
* `field`/`operator`/`value` triple and the three top-level colour members
38+
* (`backgroundColor`, `borderColor`, `textColor`) are refused by name on
39+
* `ObjectGridSchema` and on the list view. The RENDERER still reads every one
40+
* of them: `resolveConditionalFormatting` keeps each arm as a compatibility
41+
* read for a grid or list view STORED in the retired dialect, and nothing on
42+
* the render path parses a stored node. So the rows below that write those
43+
* members are not authoring examples — they pin what a stored rule still
44+
* paints, and the last `describe` pins the two facts side by side: refused
45+
* at authoring, painted at render.
46+
*
3347
* ## Read through the real renderer, asserted on the `<tr>`
3448
*
3549
* Every case below renders the real `ObjectGrid` over inline data and reads the
@@ -44,6 +58,8 @@ import { render, screen, cleanup } from '@testing-library/react';
4458
import '@testing-library/jest-dom';
4559
import React from 'react';
4660

61+
import { ObjectGridSchema } from '@object-ui/types/zod';
62+
4763
import { ObjectGrid } from '../ObjectGrid';
4864
import { registerAllFields } from '@object-ui/fields';
4965
import { ActionProvider } from '@object-ui/react';
@@ -176,7 +192,7 @@ describe('object-grid `rowColor` — the two members the row-className resolver
176192
});
177193
});
178194

179-
describe('object-grid `conditionalFormatting` — the members read inside ONE rule', () => {
195+
describe('object-grid `conditionalFormatting` — the members read inside ONE rule (retired ones as STORED-rule reads, objectui#11533)', () => {
180196
it('reads `condition` in preference to `expression` and to the native field/operator/value triple', () => {
181197
renderGrid(
182198
[
@@ -354,3 +370,35 @@ describe('object-grid — the two decoration keys reach the SAME row and neither
354370
expect(rowOf('StyleOnly').style.backgroundColor).toBe('rgb(14, 14, 14)');
355371
});
356372
});
373+
374+
describe('object-grid — a rule STORED in a retired dialect is refused at authoring and still paints (objectui#11533)', () => {
375+
/** The native rule, exactly as a grid stored before objectui#11533 carries it. */
376+
const STORED = { field: 'name', operator: 'equals', value: 'Gamma', backgroundColor: 'rgb(7, 8, 9)' };
377+
/** Its `{ condition, style }` respelling, the one dialect the authoring faces take. */
378+
const RESPELLED = { condition: "record.name == 'Gamma'", style: { backgroundColor: 'rgb(7, 8, 9)' } };
379+
const ROWS = [
380+
{ id: '1', name: 'Beta' },
381+
{ id: '2', name: 'Gamma' },
382+
];
383+
const doc = (rule: unknown) => ({ type: 'object-grid', objectName: 'test_object', conditionalFormatting: [rule] });
384+
385+
it('the authoring face refuses the stored native rule BY NAME, and the real grid still paints its row', () => {
386+
const parsed = ObjectGridSchema.safeParse(doc(STORED));
387+
expect(parsed.success).toBe(false);
388+
const atField = parsed.error!.issues.find((i) => i.path.join('.') === 'conditionalFormatting.0.field');
389+
expect(atField?.message).toContain('RETIRED (objectui#11533)');
390+
391+
// Render is not a validation door: the stored rule reaches the resolver,
392+
// which keeps the native arm as a compatibility read.
393+
renderGrid(ROWS, { conditionalFormatting: [STORED] });
394+
expect(rowOf('Gamma').style.backgroundColor).toBe('rgb(7, 8, 9)');
395+
expect(rowOf('Beta').style.backgroundColor).toBe('');
396+
});
397+
398+
it('CONTROL — the respelling is accepted at authoring and paints the SAME row with the same colour', () => {
399+
expect(ObjectGridSchema.safeParse(doc(RESPELLED)).success).toBe(true);
400+
renderGrid(ROWS, { conditionalFormatting: [RESPELLED] });
401+
expect(rowOf('Gamma').style.backgroundColor).toBe('rgb(7, 8, 9)');
402+
expect(rowOf('Beta').style.backgroundColor).toBe('');
403+
});
404+
});

‎packages/plugin-grid/src/index.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ const GRID_QUERY_INPUTS: ComponentInput[] = [
265265
{ name: 'reorderableColumns', type: 'boolean', description: 'Let users drag columns into a different order.' },
266266
{ name: 'showColumnTypeIcons', type: 'boolean', description: 'Show a field-type icon in each column header. Off by default — the type is usually obvious from the cell content, and the icons compete with the column labels.' },
267267
{ name: 'rowColor', type: 'object', description: 'Rules that colour whole rows from a field value.' },
268-
{ name: 'conditionalFormatting', type: 'array', description: 'Row/cell styling rules. Accepts both the ObjectUI `{ field, operator, value }` form and the spec expression form `{ condition, style }`.' },
268+
{ name: 'conditionalFormatting', type: 'array', description: 'Row style rules, each `{ condition, style }` — a CEL `condition` over the row’s own `record.*` and a CSS `style` map, the rule a list view declares. The first matching rule styles that row. The native `{ field, operator, value }` rule, its `expression`, and a colour written beside `condition` instead of inside `style` are retired (objectui#11533).' },
269269
// ── grouping and roll-ups ─────────────────────────────────────────────────
270270
{ name: 'grouping', type: 'object', description: 'Group rows by one or more fields into collapsible sections.' },
271271
{ name: 'aggregations', type: 'array', description: 'Per-group roll-ups shown in group headers, `[{ field, type: "sum" | "count" | "avg" | "min" | "max" | "count_distinct" }]`. Needs `grouping` to have anything to roll up.' },

0 commit comments

Comments
 (0)