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
16 changes: 16 additions & 0 deletions .changeset/10946-expression-wire-slots-by-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,19 @@ reference identity of the spec arm) still holds on `object-kanban`, and
`spec-expression-wire-slots-10946.test.ts` still pins it there, now reading
`condition` straight off the rule's shape. The rest of this entry is kept as
the reading of this change.

⚠️ **Dated note, 2026-10-03 — the list view's (and the grid's) rule is no longer a union either — objectui#11533.**
At this change the list view's rule, which `ObjectGridSchema` shares, was also a
union of two dialects (the native `{ field, operator, value, … }` comparison and
`{ condition, style }`), so the sentence above about "the list view's and the
kanban board's rule unions" described two unions. Now neither is one: the grid's
and the list view's rule is ONE object, the spec list view's `{ condition, style }`
rule by reference, with the native rule, its `expression` and a top-level colour
refused by name. It reads the same `condition` schema, so everything this entry says
about the condition (the `z.string()` first arm, the envelope, the `''` control,
the reference identity of the spec arm) still holds on `list-view` and
`object-grid`, and `spec-expression-wire-slots-10946.test.ts` now reads
`condition` straight off the list view's rule as well. `ConditionalFormattingRule`
is still built on `SpecConditionalFormattingRule`, so the widening this entry
describes still reaches it. The rest of this entry is kept as the reading of this
change.
15 changes: 15 additions & 0 deletions .changeset/11522-kanban-rule-dialect-retired.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,18 @@ Each refusal message names the key, objectui#11522 and the `{ condition, style }
**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.

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.

⚠️ **Dated note, 2026-10-03 — the grid and the list view retired the same dialect — objectui#11533.**
At this change the grid's and the list view's rule was still a union that
declared the native `{ field, operator, value }` comparison and the top-level
colour keys, and the "Not changed" paragraph above gives that as the reason the
shared evaluator keeps every arm. Now that rule retires them too: `object-grid`
and `list-view` take the spec list view's `{ condition, style }` rule only, and
the native rule, its `expression` and a top-level colour are refused by name.
`resolveConditionalFormatting` still keeps every arm, for a different reason: it
is a compatibility read for rules already STORED in the native dialect, so a
board, grid or list view a relay or a stored view hands such a rule still paints
it. The `condition` the kanban rule shares with the grid's and the list view's
rule (called their "`{ condition, style }` arm" above) is still the same schema;
it is now their whole rule rather than one arm of it. The rest of this entry is
kept as the reading of this change.
33 changes: 33 additions & 0 deletions .changeset/11533-grid-listview-rule-dialect-retired.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
'@object-ui/types': minor
'@object-ui/plugin-grid': minor
---

**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`.

| Rule on `ObjectGridSchema`, an `object-view` `table` or a `list-view` | Before | Now |
|---|---|---|
| `{ condition, style }` | accepted | accepted, unchanged |
| 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 |
| `{ expression, backgroundColor }` (no native triple) | refused as a bare union failure | refused at `expression` and the colour key, naming the retirement |
| flat CEL `{ condition, backgroundColor }` (a colour beside `condition`, no `style`) | refused as a bare union failure | refused at the colour key, naming the retirement |
| `{ 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 |
| `{ condition, style, label }` (any other undeclared key) | accepted by `safeValidateSchema` | refused as an unrecognized key, with the spec rule's own message |

Each refusal message names the key, objectui#11533 and the `{ condition, style }` respelling, on `ObjectGridSchema`, `ListViewSchema`, `safeValidateSchema` and `StrictAnyComponentSchema` alike.

**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`.

**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.

**`@object-ui/types`.**

- `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`.
- `ObjectUIConditionalFormattingRule` is removed from `@object-ui/types`. Importing it is a compile error (TS2305).
- 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.

**`@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.

**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.

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.
Original file line number Diff line number Diff line change
Expand Up @@ -2810,7 +2810,7 @@ const MEMBER_PINS: Record<string, MemberPin> = {
},
'object-grid.conditionalFormatting': {
file: 'packages/plugin-grid/src/__tests__/gridRowDecorationMembers-8071.test.tsx',
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).',
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).',
},
'object-grid.operations': {
file: 'packages/plugin-grid/src/__tests__/gridOperationsMembers-8071.test.tsx',
Expand Down
1 change: 1 addition & 0 deletions content/docs/api/schema-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,7 @@ its row below).
| `editable` | `boolean` | Enable inline cell editing. |
| `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`. |
| `frozenColumns` | `number` | Number of columns frozen on scroll. |
| `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. |
| `navigation` | `ViewNavigationConfig` | SPA navigation configuration. |
| `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. |

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

import { ObjectGridSchema } from '@object-ui/types/zod';

import { ObjectGrid } from '../ObjectGrid';
import { registerAllFields } from '@object-ui/fields';
import { ActionProvider } from '@object-ui/react';
Expand Down Expand Up @@ -176,7 +192,7 @@ describe('object-grid `rowColor` — the two members the row-className resolver
});
});

describe('object-grid `conditionalFormatting` — the members read inside ONE rule', () => {
describe('object-grid `conditionalFormatting` — the members read inside ONE rule (retired ones as STORED-rule reads, objectui#11533)', () => {
it('reads `condition` in preference to `expression` and to the native field/operator/value triple', () => {
renderGrid(
[
Expand Down Expand Up @@ -354,3 +370,35 @@ describe('object-grid — the two decoration keys reach the SAME row and neither
expect(rowOf('StyleOnly').style.backgroundColor).toBe('rgb(14, 14, 14)');
});
});

describe('object-grid — a rule STORED in a retired dialect is refused at authoring and still paints (objectui#11533)', () => {
/** The native rule, exactly as a grid stored before objectui#11533 carries it. */
const STORED = { field: 'name', operator: 'equals', value: 'Gamma', backgroundColor: 'rgb(7, 8, 9)' };
/** Its `{ condition, style }` respelling, the one dialect the authoring faces take. */
const RESPELLED = { condition: "record.name == 'Gamma'", style: { backgroundColor: 'rgb(7, 8, 9)' } };
const ROWS = [
{ id: '1', name: 'Beta' },
{ id: '2', name: 'Gamma' },
];
const doc = (rule: unknown) => ({ type: 'object-grid', objectName: 'test_object', conditionalFormatting: [rule] });

it('the authoring face refuses the stored native rule BY NAME, and the real grid still paints its row', () => {
const parsed = ObjectGridSchema.safeParse(doc(STORED));
expect(parsed.success).toBe(false);
const atField = parsed.error!.issues.find((i) => i.path.join('.') === 'conditionalFormatting.0.field');
expect(atField?.message).toContain('RETIRED (objectui#11533)');

// Render is not a validation door: the stored rule reaches the resolver,
// which keeps the native arm as a compatibility read.
renderGrid(ROWS, { conditionalFormatting: [STORED] });
expect(rowOf('Gamma').style.backgroundColor).toBe('rgb(7, 8, 9)');
expect(rowOf('Beta').style.backgroundColor).toBe('');
});

it('CONTROL — the respelling is accepted at authoring and paints the SAME row with the same colour', () => {
expect(ObjectGridSchema.safeParse(doc(RESPELLED)).success).toBe(true);
renderGrid(ROWS, { conditionalFormatting: [RESPELLED] });
expect(rowOf('Gamma').style.backgroundColor).toBe('rgb(7, 8, 9)');
expect(rowOf('Beta').style.backgroundColor).toBe('');
});
});
2 changes: 1 addition & 1 deletion packages/plugin-grid/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ const GRID_QUERY_INPUTS: ComponentInput[] = [
{ name: 'reorderableColumns', type: 'boolean', description: 'Let users drag columns into a different order.' },
{ 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.' },
{ name: 'rowColor', type: 'object', description: 'Rules that colour whole rows from a field value.' },
{ name: 'conditionalFormatting', type: 'array', description: 'Row/cell styling rules. Accepts both the ObjectUI `{ field, operator, value }` form and the spec expression form `{ condition, style }`.' },
{ 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).' },
// ── grouping and roll-ups ─────────────────────────────────────────────────
{ name: 'grouping', type: 'object', description: 'Group rows by one or more fields into collapsible sections.' },
{ 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.' },
Expand Down
Loading
Loading