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
60 changes: 60 additions & 0 deletions .changeset/8365-stray-kanban-groupby-refused.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
"@object-ui/types": minor
"@object-ui/plugin-list": minor
---

**A stray `groupBy` in a view's kanban config no longer overrides the lane, and is now refused by name.**

`ListView`'s kanban branch destructured
`columns`/`groupByField`/`groupField`/`cardFields`/`titleField` out of the merged
kanban config and spread the **rest** *after* its own `groupBy: laneField`. A
`groupBy` surviving in that bag therefore **overrode the lane the branch had just
resolved**. Measured on a distinguishing fixture, not reasoned: with
`options.kanban = { groupBy: 'LANE_FROM_STRAY_GROUPBY' }` against
`kanban = { groupByField: 'LANE_FROM_CANONICAL' }`, the generated `object-kanban`
node carried `groupBy: 'LANE_FROM_STRAY_GROUPBY'`. It read as latent only because
the one producer that fed it wrote both spellings with the same value by
construction; that producer was retired separately, and this closes the override
itself.

Two halves, per the maintainer's ruling (option B — a silent re-grouping was the
fallback and was **not** taken):

1. **The canonical lane wins.** `groupBy` joins the destructure, so the stray key
can no longer reach the passthrough spread. A view that authored it now groups
by whatever `groupByField` / `groupField` / the declared lifecycle field
resolves.
2. **The stray key is refused loudly, at the read door of the view.** This repo's
`.passthrough()` `KanbanConfig` mirror (`@object-ui/types`) declares `groupBy`
as a named alias refusal pointing at `groupByField`, in the same sentence
shape `@objectstack/spec` already answers the sibling alias with — "Unrecognized
key(s) on this kanban configuration: `groupBy`. Did you mean `groupBy` →
`groupByField`?". The refusal lands wherever a view's metadata is validated:
the CLI's `os check` / `os validate`, the VS Code extension, and `tsc` at the
authoring site (the inferred authoring face now carries `groupBy?: never`).
The legacy `options.kanban` nesting — where the retired producer wrote, and so
where stored views carry the key — takes the identical message through a check
on that untyped bag.

**Breaking, in the sense worth stating explicitly** (shipped `minor`: this repo
never declares `major`, and `.changeset/config.json` puts every package in one
`fixed` group, so levels cannot be split). Two behaviours change for **stored
data**, which is why this is not a patch:

- a stored view authoring `kanban.groupBy` (either nesting) **re-points its lane**
— it used to group by the stray key and now groups by the canonical binding, so
its board may show different columns;
- the same document now **fails validation** where it used to pass: any pipeline
running `safeValidateSchema` over it (`os check`, `os validate`, the extension)
reports one issue naming the key and the replacement.

Honouring `groupBy` as a declared alias was never an option here:
`@objectstack/spec`'s `KanbanConfigSchema` is a strict object of
`columns` / `groupByField` / `summarizeField` and refuses it by name — re-measured
on the pinned 17.4.0 with both controls firing — so legalising it would be a spec
change, not a renderer widening (AGENTS.md #0.1).

`groupBy` on the generated `object-kanban` **node** is untouched: that is the live,
canonical lane key `ObjectKanban` reads. Only the **view-level** kanban config
spelling is refused. The `.passthrough()` itself is kept — an undeclared sibling
key still rides through, which is pinned as a control.
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,19 @@
* `groupBy`.
*
* ⚠️ NO ARM HERE ASSERTS THE `...restKanban` OVERRIDE, deliberately. `ListView`
* spreads the rest of the merged kanban config AFTER its own `groupBy:
* laneField`, so a surviving `groupBy` wins over the lane it just resolved.
* That override is real, but it cannot be pinned FROM THIS PRODUCER: every bag
* this producer can build has `groupBy` and `groupByField` holding the same
* spread the rest of the merged kanban config AFTER its own `groupBy:
* laneField`, so a surviving `groupBy` won over the lane it had just resolved.
* That override was real, but it could not be pinned FROM THIS PRODUCER: every
* bag this producer can build has `groupBy` and `groupByField` holding the same
* value by construction, so an override row driven by it passes with or without
* the fix — a test that cannot fail. Distinguishing the two spellings needs a
* the fix — a test that cannot fail. Distinguishing the two spellings needed a
* hand-built `plugin-list` fixture (`options.kanban.groupBy: 'a'` against
* `kanban.groupByField: 'b'`), which is a `plugin-list` change on its own card.
* What this deletion does is remove the only producer in this repo that fed the
* override; the override itself is untouched.
* `kanban.groupByField: 'b'`), which was a `plugin-list` change on its own card.
* ⭐ THAT FIXTURE NOW EXISTS and the override is closed — objectui#8365,
* `plugin-list/src/__tests__/ListView.strayGroupByRefused-8365.test.tsx`. What
* THIS deletion did remains what it always was: it removed the only producer in
* this repo that fed the override. The arms below are unchanged by that card,
* because they measure the producer, not the render branch.
*
* REVERSE VERIFICATION — direction predicted before running, then observed:
* restore `{ groupBy: lane, groupByField: lane }` in `kanbanViewOptions` and
Expand Down
22 changes: 14 additions & 8 deletions packages/app-shell/src/views/ObjectView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -412,14 +412,20 @@ export function galleryViewOptions(viewDef: any): Record<string, unknown> {
* nestings), and neither does the render branch (`groupByField || groupField ||
* detectStatusField(...)`).
*
* ⚠️ WHAT THIS CLOSES AND WHAT IT DOES NOT. `ListView`'s kanban branch
* destructures `columns`/`groupByField`/`groupField`/`cardFields`/`titleField`
* out of the merged config and spreads the REST *after* its own
* `groupBy: laneField`, so a surviving `groupBy` overrides the lane it just
* resolved. This deletion removes the only producer in this repo that fed that
* override — it does NOT remove the override, which stays reachable from
* author-written `kanban.groupBy` riding this repo's `.passthrough()` mirror
* and is a `plugin-list` change on its own card.
* ⚠️ WHAT THIS CLOSED AND WHAT IT DID NOT — and what has since closed the rest.
* `ListView`'s kanban branch destructured
* `columns`/`groupByField`/`groupField`/`cardFields`/`titleField` out of the
* merged config and spread the REST *after* its own `groupBy: laneField`, so a
* surviving `groupBy` overrode the lane it had just resolved. This deletion
* removed the only producer in this repo that fed that override; the override
* itself stayed reachable from an author-written `kanban.groupBy` riding this
* repo's `.passthrough()` mirror, and was carried on its own card.
* ⭐ THAT CARD HAS LANDED (objectui#8365, maintainer ruling of 2026-09-12 —
* decision batch #117 item 5, option B): `groupBy` is now stripped in
* `ListView`'s destructure, so the canonical lane wins, AND the view-level
* `KanbanConfig` mirror (`@object-ui/types`, `zod/objectql.zod.ts`) declares it
* as an alias refusal naming `groupByField`, so the key is refused BY NAME at
* the read door instead of riding the passthrough. ⛔ Do not re-file it.
*
* ⚠️ `titleField` AND `cardFields` BELOW ARE ALSO OUTSIDE `KanbanConfigSchema`,
* and are deliberately NOT swept up here. `cardFields` is a DECLARED deprecated
Expand Down
20 changes: 19 additions & 1 deletion packages/plugin-list/src/ListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2566,7 +2566,25 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
// own `columns` prop is its LANES, so passing this through verbatim built
// lanes with undefined id/title. Map it to `cardFields` and strip the
// vocabulary keys from the passthrough (mirrors plugin-view's adapter).
const { columns: kanbanCardColumns, groupByField, groupField, cardFields, titleField, ...restKanban } = kanbanCfg as Record<string, any>;
// ⭐ `groupBy` IS STRIPPED HERE (objectui#8365, maintainer ruling of
// 2026-09-12 — decision batch #117 item 5, option B). It is a THIRD
// spelling of the lane, and because it was NOT in this destructure it
// survived into `restKanban`, which the return below spreads AFTER its
// own `groupBy: laneField` — so an authored `kanban.groupBy` OVERRODE
// the lane this branch had just resolved. Measured on the card's
// distinguishing fixture (`options.kanban = { groupBy:
// 'LANE_FROM_STRAY_GROUPBY' }` against `kanban = { groupByField:
// 'LANE_FROM_CANONICAL' }`): the generated node carried
// `groupBy: 'LANE_FROM_STRAY_GROUPBY'`. Stripping it is the half that
// makes the CANONICAL lane win; the loud half is the read door, where
// the view-level `KanbanConfig` mirror (`@object-ui/types`,
// `zod/objectql.zod.ts`) now declares `groupBy` as an alias refusal and
// names `groupByField`, so the key is no longer silently accepted by
// that object's `.passthrough()`.
// ⛔ Deliberately NOT folded onto `laneField`: this branch's own read is
// already canonical-first (`groupByField || groupField || detect…`), so
// a fold would re-create the override it just closed.
const { columns: kanbanCardColumns, groupByField, groupField, cardFields, titleField, groupBy: _strayGroupBy, ...restKanban } = kanbanCfg as Record<string, any>;
const laneField = groupByField || groupField || detectStatusField(objectDef) || undefined;
// `groupBy` is the lane key and the ONLY one written here. This node
// used to carry `groupField: laneField` alongside it — a duplicate the
Expand Down
Loading
Loading