diff --git a/.changeset/28522-view-or-group-live.md b/.changeset/28522-view-or-group-live.md new file mode 100644 index 00000000000..1fed473326a --- /dev/null +++ b/.changeset/28522-view-or-group-live.md @@ -0,0 +1,15 @@ +--- +'@objectstack/spec': patch +--- + +docs(spec): the view filter any-of group is described as live, not planned + +Clause-②: no +Metadata-protocol: none + +The console now applies a view filter's any-of group `{ or: [rule, ...] }`: the list query lowers it to one `or` node ANDed with the filter's other elements, and the filter builder edits a filter that is exactly one group. Two author-facing texts still said otherwise and are corrected: + +- `ViewFilterOrGroupSchema`'s description (what `/meta/types` and the generated reference show) no longer starts with `[PLANNED — not yet rendered]` and no longer says a view that stores a group is refused when it loads. +- The refusal for a MongoDB-style `$or` on a rule-array `filter` still prescribes the any-of group element, and no longer adds that no console applies it. + +Nothing an author can write changes: the schema accepts and refuses exactly what it did. diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index b48ef169ca1..d97480f888b 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -518,7 +518,7 @@ View filter rule ### Nested Shape: `ObjectCalendarProps.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | @@ -741,7 +741,7 @@ View filter rule ### Nested Shape: `ObjectGanttProps.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | @@ -899,7 +899,7 @@ View filter rule ### Nested Shape: `ObjectGridProps.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | @@ -1066,7 +1066,7 @@ View filter rule ### Nested Shape: `ObjectKanbanProps.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | @@ -1149,7 +1149,7 @@ View filter rule ### Nested Shape: `ObjectMapProps.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | @@ -1293,7 +1293,7 @@ View filter rule ### Nested Shape: `ObjectMetricProps.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | @@ -1372,7 +1372,7 @@ View filter rule ### Nested Shape: `ObjectTimelineProps.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | @@ -1471,7 +1471,7 @@ View filter rule ### Nested Shape: `ObjectTreeProps.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | @@ -1938,7 +1938,7 @@ View filter rule ### Nested Shape: `RecordLineItemsProps.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | @@ -2099,7 +2099,7 @@ View filter rule ### Nested Shape: `RecordRelatedListProps.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | diff --git a/content/docs/references/ui/page.mdx b/content/docs/references/ui/page.mdx index 985d5c0b0d6..9a513ffc7d9 100644 --- a/content/docs/references/ui/page.mdx +++ b/content/docs/references/ui/page.mdx @@ -68,7 +68,7 @@ View filter rule ### Nested Shape: `ElementDataSource.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | @@ -150,7 +150,7 @@ View filter rule ### Nested Shape: `InterfacePageConfig.filterBy[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | @@ -487,7 +487,7 @@ View filter rule ### Nested Shape: `PageRecordBinding.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index 677578ef027..5bf556f851c 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -875,7 +875,7 @@ View filter rule ### Nested Shape: `ListView.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | @@ -1264,7 +1264,7 @@ View filter rule ### Nested Shape: `ObjectListView.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | @@ -2001,7 +2001,7 @@ View filter rule #### Option 2 -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. ### Properties @@ -2026,7 +2026,7 @@ View filter rule ## ViewFilterOrGroup -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. ### Properties @@ -2534,7 +2534,7 @@ View filter rule ### Nested Shape: `ViewTab.filter[number][option 2]` -[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). +An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its sibling elements, and its filter builder edits a filter that is exactly one group. | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | diff --git a/packages/spec/liveness/view.json b/packages/spec/liveness/view.json index 3a1bdb3aa6c..36f6e763660 100644 --- a/packages/spec/liveness/view.json +++ b/packages/spec/liveness/view.json @@ -49,7 +49,7 @@ }, "filter": { "status": "live", - "note": "objectui: ListView.tsx client-side filter shaping (audit L15); no list-render prop reaches the server query (audit data-flow note). The console's filter rows carry a React list key that the wire door removes before the parse (packages/spec/src/ui/view.zod.ts, the VIEW_CONSOLE_ROW_DECORATIONS header and the ViewFilterRule docblock; moved out of spec source, #23434): `id: crypto.randomUUID()` is stamped on each new row by objectui @b9814b334 packages/components/src/custom/filter-builder.tsx#FilterBuilder (its `addCondition`), and again on a stored filter read back into the builder by objectui @b9814b334 packages/plugin-view/src/config/view-config-utils.ts#parseTriplet and objectui @b9814b334 packages/plugin-view/src/config/view-config-utils.ts#parseSingleOrNested (both reached from that file's parseSpecFilter); re-read 2026-10-15, first cited by line, sha-less. One element kind rides this live verdict at PLANNED strength (#28020, 2026-10-31): the any-of group `{ or: [rule, ...] }` (ViewFilterOrGroupSchema, its describe marked `[PLANNED — not yet rendered]`) has no console reader yet — the list query build (ui/packages/plugin-list/src/ListView.tsx, buildEffectiveFilter; ui/packages/core/src/utils/filter-converter.ts, convertFiltersToAST) and the builder save fold (ui/packages/app-shell/src/views/viewFilterFold.ts, which refuses an OR group) read flat rules only, read on this tree at 22ce3d13fe. The console half is #28370; amend this note, citing the group read, when it lands." + "note": "objectui: ListView.tsx client-side filter shaping (audit L15); no list-render prop reaches the server query (audit data-flow note). The console's filter rows carry a React list key that the wire door removes before the parse (packages/spec/src/ui/view.zod.ts, the VIEW_CONSOLE_ROW_DECORATIONS header and the ViewFilterRule docblock; moved out of spec source, #23434): `id: crypto.randomUUID()` is stamped on each new row by objectui @b9814b334 packages/components/src/custom/filter-builder.tsx#FilterBuilder (its `addCondition`), and again on a stored filter read back into the builder by objectui @b9814b334 packages/plugin-view/src/config/view-config-utils.ts#parseTriplet and objectui @b9814b334 packages/plugin-view/src/config/view-config-utils.ts#parseSingleOrNested (both reached from that file's parseSpecFilter); re-read 2026-10-15, first cited by line, sha-less. The any-of group element `{ or: [rule, ...] }` (ViewFilterOrGroupSchema, #28020, 2026-10-31) rides this live verdict at full strength since its console half landed (#28370, PR #28475): the list query build lowers it to one `or` node inside the list's AND (ui/packages/core/src/utils/filter-converter.ts, toFilterNode via viewFilterOrGroupToNode, reached from ui/packages/plugin-list/src/ListView.tsx buildEffectiveFilter through mergeFilterNodes and ui/packages/plugin-view/src/ObjectView.tsx), the builder save fold writes an OR group over two or more rules as one group (ui/packages/app-shell/src/views/viewFilterFold.ts) and builderRulesOfSpecFilter reads exactly-one-group back as the builder's OR logic; read on this tree at 94d3e254, 2026-11-02. Its describe lost the `[PLANNED — not yet rendered]` marker in the same change (#28522)." }, "sort": { "status": "live", diff --git a/packages/spec/src/conversions/registry.ts b/packages/spec/src/conversions/registry.ts index 8ba55456e27..a12912c7d80 100644 --- a/packages/spec/src/conversions/registry.ts +++ b/packages/spec/src/conversions/registry.ts @@ -13927,8 +13927,8 @@ function dollarKeysReason(keys: readonly string[]): string { : ` (its top-level ${quoteKeys(others)} ${others.length === 1 ? 'is' : 'are'} not a field either)`; return `carries the combinator${combinators.length === 1 ? '' : 's'} ${quoteKeys(combinators)}` + `${alsoNotFields}: a rule array's elements AND, so \`$and\` is only the separate rules it ` - + 'joins, `$or` over flat rules only an any-of group element `{ or: [rule, ...] }` (declared, ' - + 'not yet applied by any console) that this conversion does not write, and `$not` has no rule ' + + 'joins, `$or` over flat rules only an any-of group element `{ or: [rule, ...] }` that this ' + + 'conversion does not write, and `$not` has no rule ' + 'spelling; a combinator is never flattened — ' + 'that would change which rows the filter selects. Decide which rows it should select, and ' + 'write the rules that select exactly those'; diff --git a/packages/spec/src/ui/filter-rule-array-guidance.test.ts b/packages/spec/src/ui/filter-rule-array-guidance.test.ts index 5abdbe2c47a..59d93e6b2f3 100644 --- a/packages/spec/src/ui/filter-rule-array-guidance.test.ts +++ b/packages/spec/src/ui/filter-rule-array-guidance.test.ts @@ -165,10 +165,8 @@ describe('§1 the record form is refused WITH the new spelling', () => { expect(issue.code).toBe('invalid_type'); expect(issue.message).not.toContain("field: '$or'"); expect(issue.message).toContain('`$or` is a combinator, not a field, and the rule array spells a combinator only as its own elements'); - // [#28020] The one combinator the rule array can spell is named, with its - // status: the any-of group is declared but no console applies it yet. + // [#28020] The one combinator the rule array can spell is named. expect(issue.message).toContain('`$or` over flat rules is one any-of group element'); - expect(issue.message).toContain('no console applies it yet'); expect(issue.message).toContain('`$not` cannot be written at all'); expect(issue.message).toContain('flattening one changes which rows the filter selects'); expect(issue.message).toContain('decide which rows this filter should select'); diff --git a/packages/spec/src/ui/filter-rule-array.ts b/packages/spec/src/ui/filter-rule-array.ts index d7efe87769c..2c01c792641 100644 --- a/packages/spec/src/ui/filter-rule-array.ts +++ b/packages/spec/src/ui/filter-rule-array.ts @@ -188,16 +188,14 @@ export function ruleArrayFilterError(options: RuleArrayFilterErrorOptions): z.co } const named = combinators.map((key) => `\`${key}\``).join(' and '); // [#28020] The rule array gained ONE combinator spelling, the any-of group - // element, and this sentence names it — with its status, because no console - // applies it yet: prescribing it bare would send an author to a filter that - // does not narrow the rows (`ViewFilterOrGroupSchema`'s PLANNED note). + // element, and this sentence names it as the shape a `$or` over flat rules + // is written in (`ViewFilterOrGroupSchema`). const combinatorSentence = combinators.length === 0 ? '' : ` ${named} ${combinators.length === 1 ? 'is a combinator, not a field' : 'are combinators, not fields'}, ` + `and the rule array spells a combinator only as its own elements: they AND, so \`$and\` is ` + `the separate rules it joins, \`$or\` over flat rules is one any-of group element ` - + `\`{ or: [{ field, operator, value }, ...] }\` (declared, but no console applies it yet, so it ` - + `does not narrow the rows today), and \`$not\` cannot be written at all. Nothing rewrites a ` + + `\`{ or: [{ field, operator, value }, ...] }\`, and \`$not\` cannot be written at all. Nothing rewrites a ` + `combinator for you — flattening one changes which rows the filter selects — so decide which ` + `rows this filter should select, and write the rules that select exactly those.`; diff --git a/packages/spec/src/ui/view-filter-or-group.test.ts b/packages/spec/src/ui/view-filter-or-group.test.ts index 61d1aef5b2c..65f2c9007a2 100644 --- a/packages/spec/src/ui/view-filter-or-group.test.ts +++ b/packages/spec/src/ui/view-filter-or-group.test.ts @@ -233,7 +233,7 @@ describe('the wire door and the published shape', () => { expect(issues[0]!.message).toContain('`id` is a console row key'); }); - it('the element publishes both kinds, and the group says it is not yet rendered', () => { + it('the element publishes both kinds, and the group carries no PLANNED marker', () => { const json = z.toJSONSchema(ViewFilterElementSchema, { io: 'input', unrepresentable: 'any' }) as { anyOf?: Array<{ properties?: Record }>; }; @@ -241,7 +241,8 @@ describe('the wire door and the published shape', () => { ['field', 'operator', 'value'], ['or'], ]); - expect(ViewFilterOrGroupSchema.description?.startsWith('[PLANNED — not yet rendered]')).toBe(true); + expect(ViewFilterOrGroupSchema.description?.startsWith('An any-of group:')).toBe(true); + expect(ViewFilterOrGroupSchema.description).not.toContain('[PLANNED'); }); it('`isViewFilterOrGroup` tells the element kinds apart by the own `or` key', () => { diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index e515a325104..3dba7823944 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -1163,16 +1163,16 @@ export function isViewFilterOrGroup(value: unknown): value is { [VIEW_FILTER_OR_ * rule never carries `or`, so it is routed to {@link ViewFilterRuleSchema} * exactly as before. * - * ## [PLANNED — not yet rendered] + * ## Applied by the console * - * No reader applies the group yet. The query a list view runs is built by the - * console (`ui/packages/core/src/utils/filter-converter.ts#toFilterNode`), which - * lowers flat rules only and passes a group through untouched, so the server - * refuses the query as a malformed filter (400 `INVALID_FILTER`) and the list - * shows that error and no rows. It fails closed: a stored group never widens - * what a view, a related list or a picker shows. The describe below says so, - * so no author is told it works. - * The console half is #28370; the marker leaves when it lands. + * The query a list view runs is built by the console + * (`ui/packages/core/src/utils/filter-converter.ts#toFilterNode`), which lowers + * a group to one `or` node of its rules, a child of the list's AND like any + * rule, so a group is ANDed with its sibling elements. The console's filter + * builder edits a filter that is exactly one group as its OR logic, and saves + * its OR logic over two or more rules as one group; a filter holding a group + * beside other elements is applied, and shown read-only in the builder rather + * than flattened on save (`builderRulesOfSpecFilter`). * * @example * ```ts @@ -1214,13 +1214,12 @@ export const ViewFilterOrGroupSchema = lazySchema(() => strictObject({ + 'each judged exactly as a rule at the top level of the filter. A group cannot hold a group.', ), }).describe( - '[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its ' + 'An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its ' + 'rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, ' + 'so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", ' + 'operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the ' - + 'last 7 days. One level only: its members are flat rules. No console reads it yet: until the list ' - + 'query build reads the group, a view that stores one is refused when it loads (the list reports a ' - + 'malformed filter and shows no rows).', + + 'last 7 days. One level only: its members are flat rules. The console applies it, ANDed with its ' + + 'sibling elements, and its filter builder edits a filter that is exactly one group.', )); export type ViewFilterOrGroup = z.input;