From 604c2036667b12dd982f5070c87221e60b4b866b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 12:23:52 +0000 Subject: [PATCH 1/2] fix(plugin-view): spell the views-prop sort direction key `order`, not `direction` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ObjectViewProps.views[].sort` declared `{ field, direction }` while every consumer of the resolved `activeView.sort` reads `order` — the shared sink `convertSortToQueryParams`, the grid path via `ObjectGridSchema.sort`, and `mergedSort`. A host writing the exact shape the prop declared got a silently ascending list with no failure signal anywhere. Renaming the key to `order` gives the repo one sort spelling and makes the retired one a loud type error. Per the 2026-08-24 maintainer ruling this is deliberately NOT a tolerant dual-read (`direction ?? order`) — that is the tolerance layer objectui#4869 ruled against. Producers corrected: the studio adapter round-trip fixtures, and two `views`-prop fixtures inside plugin-view's own suite that the ruling did not enumerate. `ObjectView.sortSink.test.tsx` held a pin asserting `{ name: 'asc' }` for a `direction: 'desc'` fixture — green only because nothing read the key — which is replaced by an assertion that the direction now survives, plus a `@ts-expect-error` guard that turns red if the old spelling is ever readmitted. NOT touched: `SortUI` and its `sort-ui` `defaultProps` in `index.tsx`, which legitimately own `direction` on their own `SortUISchema`. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L --- .changeset/5293-view-sort-order-spelling.md | 43 ++++++++++++++++++ .../src/views/view-config-adapter.test.ts | 4 +- packages/plugin-view/src/ObjectView.tsx | 17 ++++++- .../__tests__/ObjectView.sortSink.test.tsx | 44 ++++++++++++++----- .../src/__tests__/ObjectView.test.tsx | 2 +- 5 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 .changeset/5293-view-sort-order-spelling.md diff --git a/.changeset/5293-view-sort-order-spelling.md b/.changeset/5293-view-sort-order-spelling.md new file mode 100644 index 0000000000..dd043b1714 --- /dev/null +++ b/.changeset/5293-view-sort-order-spelling.md @@ -0,0 +1,43 @@ +--- +'@object-ui/plugin-view': minor +--- + +**Breaking (shipped as `minor` per AGENTS.md §版本号策略).** `ObjectViewProps.views[].sort` +now spells its direction key **`order`**. The retired spelling is **`direction`** — named +here so that a host still writing it can find this entry by searching the old key +(objectui#5293). + +```diff + +``` + +**Nothing that worked stops working, because `direction` never worked.** No reader in the +repo has ever known the word. All three consumers of the resolved `activeView.sort` read +`order`: the non-grid fetch lowers it through the shared sink `convertSortToQueryParams`, +whose `entry.order === 'desc'` is false for a missing key; the grid path forwards it to +`ObjectGridSchema.sort`, where `ObjectGrid` builds the wire string `` `${s.field} ${s.order}` `` +— literally `"created_at undefined"` — and `parseSchemaSort` reads a missing `order` as +ascending, so the column header even drew an ascending arrow; `mergedSort` hands the same +value to the delegated list view. + +So a host writing the exact shape the prop declared got an **ascending** list with no +failure signal anywhere: the declaration said the value was well-formed, and the direction +was dropped at three independent readers rather than rejected at one. This rename does not +take away a feature — it converts a silent wrong answer into a loud type error at the one +place that can still be fixed cheaply. + +`order` is the spelling every other sort surface already uses (`SortConfig`, +`NamedListView.sort`, `ObjectGridSchema.sort` / `.defaultSort`, and the shared +`QuerySortEntry` sink), so the prop now has one spelling repo-wide and declared equals +enforced. + +⛔ Deliberately **not** a tolerant dual-read (`direction ?? order`): that is the tolerance +layer objectui#4869 ruled against, and admitting the old key as an alias would rebuild the +drift this change removes. `SortUI` is untouched — it legitimately owns `direction` on its +own `SortUISchema` and converts at its boundaries. diff --git a/packages/app-shell/src/views/view-config-adapter.test.ts b/packages/app-shell/src/views/view-config-adapter.test.ts index f28710f91c..a860323898 100644 --- a/packages/app-shell/src/views/view-config-adapter.test.ts +++ b/packages/app-shell/src/views/view-config-adapter.test.ts @@ -15,7 +15,7 @@ describe('view-config-adapter', () => { type: 'grid', columns: ['name', 'status'], filter: [{ field: 'owner', op: 'eq', value: 'me' }], - sort: [{ field: 'name', direction: 'asc' }], + sort: [{ field: 'name', order: 'asc' }], showSearch: true, }; const draft = runtimeViewToInspectorDraft(view, 'crm_lead'); @@ -79,7 +79,7 @@ describe('view-config-adapter', () => { type: 'grid', columns: ['name', 'status', 'owner'], filter: [{ field: 'status', op: 'eq', value: 'open' }], - sort: [{ field: 'created_at', direction: 'desc' }], + sort: [{ field: 'created_at', order: 'desc' }], showSearch: true, showFilters: false, pageSize: 50, diff --git a/packages/plugin-view/src/ObjectView.tsx b/packages/plugin-view/src/ObjectView.tsx index d41c47e1e2..a86914f5d8 100644 --- a/packages/plugin-view/src/ObjectView.tsx +++ b/packages/plugin-view/src/ObjectView.tsx @@ -242,13 +242,28 @@ export interface ObjectViewProps { * Views available for the ViewSwitcher. * Each view defines a type (grid, kanban, calendar, etc.) and display columns/config. * If not provided, uses schema.listViews or falls back to default grid view. + * + * `sort` spells its direction key `order`, like every other sort surface in + * the repo (`SortConfig`, `NamedListView.sort`, `ObjectGridSchema.sort` / + * `.defaultSort`) and like the shared sink `convertSortToQueryParams` reads + * it. It used to be declared as `direction` (objectui#5293), which NO reader + * ever knew: all three consumers of the resolved `activeView.sort` read + * `order`, so a host writing `direction: 'desc'` got a SILENTLY ascending + * list — the sink's `entry.order === 'desc'` is false for a missing key, the + * grid built the wire string `name undefined`, and `parseSchemaSort` drew an + * ascending arrow above it. The rename does not remove a working feature; + * it converts that silent wrong answer into a loud type error. + * + * ⛔ Deliberately NOT a tolerant dual-read (`direction ?? order`) — that is + * the tolerance layer objectui#4869 ruled against, and re-adding it here + * would restore the very spelling drift this declaration now closes. */ views?: Array<{ id: string; label: string; type: ViewType; columns?: string[]; - sort?: Array<{ field: string; direction: 'asc' | 'desc' }>; + sort?: Array<{ field: string; order: 'asc' | 'desc' }>; filter?: any[]; [key: string]: any; }>; diff --git a/packages/plugin-view/src/__tests__/ObjectView.sortSink.test.tsx b/packages/plugin-view/src/__tests__/ObjectView.sortSink.test.tsx index 7f0ec4411a..92db59401b 100644 --- a/packages/plugin-view/src/__tests__/ObjectView.sortSink.test.tsx +++ b/packages/plugin-view/src/__tests__/ObjectView.sortSink.test.tsx @@ -192,24 +192,48 @@ describe('precedence is unchanged by the lowering', () => { }); describe('the census the card asked for: no spelling regressed on the way in', () => { - it('leaves the `views` prop spelling exactly where it already was — objectui#5293', async () => { - // ⚠️ NOT this card's defect and NOT fixed here. `ObjectViewProps.views[]` - // declares `sort?: Array<{ field, direction }>` while every consumer reads - // `order`, so the direction is dropped. Measured BOTH ways: unlowered, the - // adapter's `shorthand(field, undefined)` sent ascending `name`; lowered, - // the sink's `entry.order === 'desc'` is equally false and sends - // `{ name: 'asc' }`. Same answer before and after — the lowering neither - // fixes objectui#5293 nor makes it worse. Pinned so that whoever DOES fix - // that card is told this site exists. + it("carries a `views` prop sort through to the sink, DESCENDING — objectui#5293", async () => { + // This assertion is the fix. It replaces a pin that asserted + // `{ name: 'asc' }` for a `direction: 'desc'` fixture — green not because + // anything worked but because NOTHING read the key, which is exactly the + // silent wrong answer objectui#5293 was filed about. `ObjectViewProps` + // now declares `sort?: Array<{ field, order }>`, the one spelling every + // consumer and the shared sink already read, so the authored direction + // survives to `$orderby` instead of being dropped on the way in. const ds = mockDataSource(); render( , ); await waitFor(() => expect(ds.find).toHaveBeenCalled()); + expect(ds.find.mock.calls[0][1].$orderby).toEqual({ name: 'desc' }); + }); + + it('the old `direction` spelling is refused by the declaration, not silently dropped', async () => { + // The other half of objectui#5293, and the reason the break is worth + // shipping: a host that still writes the retired spelling must FAIL, and + // fail at the type boundary rather than by rendering an ascending list. + // The `@ts-expect-error` IS the assertion — it turns red if the excess + // property is ever admitted again, which is precisely what a tolerant + // dual-read (`direction ?? order`) would do. ⛔ objectui#4869 ruled that + // tolerance layer out; this line is the guard that keeps it out. + const ds = mockDataSource(); + render( + , + ); + // Runtime behaviour of the retired spelling is unchanged and deliberately + // still asserted: it orders ascending. That is what makes the type error + // the ONLY failure signal a host gets, and why the changeset names the + // old key so the break is searchable. + await waitFor(() => expect(ds.find).toHaveBeenCalled()); expect(ds.find.mock.calls[0][1].$orderby).toEqual({ name: 'asc' }); }); diff --git a/packages/plugin-view/src/__tests__/ObjectView.test.tsx b/packages/plugin-view/src/__tests__/ObjectView.test.tsx index 2483b9fb91..64e432a70b 100644 --- a/packages/plugin-view/src/__tests__/ObjectView.test.tsx +++ b/packages/plugin-view/src/__tests__/ObjectView.test.tsx @@ -487,7 +487,7 @@ describe('ObjectView', () => { // Update with sort config — simulates live preview of sort changes const updatedViews = [ - { id: 'all', label: 'All', type: 'grid' as const, columns: ['name'], sort: [{ field: 'name', direction: 'desc' as const }] }, + { id: 'all', label: 'All', type: 'grid' as const, columns: ['name'], sort: [{ field: 'name', order: 'desc' as const }] }, ]; rerender( From 54ee7f05b00baf33f18f60b4fa3d0b19c482ac33 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 12:48:47 +0000 Subject: [PATCH 2/2] docs(plugin-view): scope the retired-`direction` claim to the `views` prop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The changeset and the `ObjectViewProps.views` JSDoc both carried an unqualified repo-wide negative — "no reader in the repo has ever known the word" — that one grep in this same package falsifies: `toSortItems` (`src/config/view-config-utils.ts:328`) folds `s.order || s.direction || 'asc'`, and it is a published export (barrel `src/index.tsx:38`, README:91). The scoped claim in the same paragraph — all three consumers of the resolved `activeView.sort` read `order` — is the one that carries the argument and is correct. Both artifacts now make that claim instead, and both name the surviving site so a host migrating `views[].sort` who greps the old key does not read the hit in `toSortItems` as a partial retirement. Retiring that fallback is its own break on its own public export and is tracked separately; it is deliberately untouched here. Prose only: no declaration, no fixture and no runtime line changes. The `views[].sort` declaration, the `@ts-expect-error` guard and the whole code half of this branch are byte-identical to the previous commit. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L --- .changeset/5293-view-sort-order-spelling.md | 14 ++++++++++++-- packages/plugin-view/src/ObjectView.tsx | 21 ++++++++++++++------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/.changeset/5293-view-sort-order-spelling.md b/.changeset/5293-view-sort-order-spelling.md index dd043b1714..47728f24b6 100644 --- a/.changeset/5293-view-sort-order-spelling.md +++ b/.changeset/5293-view-sort-order-spelling.md @@ -17,8 +17,8 @@ here so that a host still writing it can find this entry by searching the old ke /> ``` -**Nothing that worked stops working, because `direction` never worked.** No reader in the -repo has ever known the word. All three consumers of the resolved `activeView.sort` read +**Nothing that worked stops working on this surface, because on the `views` prop +`direction` never worked.** All three consumers of the resolved `activeView.sort` read `order`: the non-grid fetch lowers it through the shared sink `convertSortToQueryParams`, whose `entry.order === 'desc'` is false for a missing key; the grid path forwards it to `ObjectGridSchema.sort`, where `ObjectGrid` builds the wire string `` `${s.field} ${s.order}` `` @@ -32,6 +32,16 @@ was dropped at three independent readers rather than rejected at one. This renam take away a feature — it converts a silent wrong answer into a loud type error at the one place that can still be fixed cheaply. +**Scope — one published export still accepts `direction`, and this release does not retire +it.** `toSortItems` (`packages/plugin-view/src/config/view-config-utils.ts`, re-exported +from the package root and listed in the README) folds `s.order || s.direction || 'asc'`. +It serves a different surface — the studio inspector-draft that feeds `SortBuilder` — and +it is not reachable from the `views` prop, so it neither affects nor is affected by this +rename. If you migrate by searching for the old key, that is the other hit you will find: +it is dormant (nothing in this repo calls it outside a test), and removing it would be a +separate break on a separate public export, tracked as objectui#6011. It is not a partial +retirement of this one. + `order` is the spelling every other sort surface already uses (`SortConfig`, `NamedListView.sort`, `ObjectGridSchema.sort` / `.defaultSort`, and the shared `QuerySortEntry` sink), so the prop now has one spelling repo-wide and declared equals diff --git a/packages/plugin-view/src/ObjectView.tsx b/packages/plugin-view/src/ObjectView.tsx index a86914f5d8..f0a36243f4 100644 --- a/packages/plugin-view/src/ObjectView.tsx +++ b/packages/plugin-view/src/ObjectView.tsx @@ -246,13 +246,20 @@ export interface ObjectViewProps { * `sort` spells its direction key `order`, like every other sort surface in * the repo (`SortConfig`, `NamedListView.sort`, `ObjectGridSchema.sort` / * `.defaultSort`) and like the shared sink `convertSortToQueryParams` reads - * it. It used to be declared as `direction` (objectui#5293), which NO reader - * ever knew: all three consumers of the resolved `activeView.sort` read - * `order`, so a host writing `direction: 'desc'` got a SILENTLY ascending - * list — the sink's `entry.order === 'desc'` is false for a missing key, the - * grid built the wire string `name undefined`, and `parseSchemaSort` drew an - * ascending arrow above it. The rename does not remove a working feature; - * it converts that silent wrong answer into a loud type error. + * it. It used to be declared as `direction` (objectui#5293), which NO + * consumer of THIS prop ever read: all three consumers of the resolved + * `activeView.sort` read `order`, so a host writing `direction: 'desc'` got a + * SILENTLY ascending list — the sink's `entry.order === 'desc'` is false for + * a missing key, the grid built the wire string `name undefined`, and + * `parseSchemaSort` drew an ascending arrow above it. The rename does not + * remove a working feature; it converts that silent wrong answer into a loud + * type error. + * + * The claim is scoped to those three consumers on purpose. Elsewhere in this + * package the published `toSortItems` export still folds + * `s.order || s.direction` for the studio inspector-draft — a different + * surface, unreachable from this prop, and deliberately not retired here + * (objectui#6011). * * ⛔ Deliberately NOT a tolerant dual-read (`direction ?? order`) — that is * the tolerance layer objectui#4869 ruled against, and re-adding it here