diff --git a/.changeset/5293-view-sort-order-spelling.md b/.changeset/5293-view-sort-order-spelling.md new file mode 100644 index 0000000000..47728f24b6 --- /dev/null +++ b/.changeset/5293-view-sort-order-spelling.md @@ -0,0 +1,53 @@ +--- +'@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 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}` `` +— 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. + +**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 +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..f0a36243f4 100644 --- a/packages/plugin-view/src/ObjectView.tsx +++ b/packages/plugin-view/src/ObjectView.tsx @@ -242,13 +242,35 @@ 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 + * 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 + * 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(