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
53 changes: 53 additions & 0 deletions .changeset/5293-view-sort-order-spelling.md
Original file line numberDiff line numberDiff line change
@@ -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
<ObjectView
views={[{
id: 'recent', label: 'Recent', type: 'grid',
- sort: [{ field: 'created_at', direction: 'desc' }],
+ sort: [{ field: 'created_at', order: 'desc' }],
}]}
/>
```

**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.
4 changes: 2 additions & 2 deletions packages/app-shell/src/views/view-config-adapter.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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');
Expand DownExpand Up@@ -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,
Expand Down
24 changes: 23 additions & 1 deletion packages/plugin-view/src/ObjectView.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
}>;
Expand Down
44 changes: 34 additions & 10 deletions packages/plugin-view/src/__tests__/ObjectView.sortSink.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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(
<ObjectView
schema={{ type: 'object-view', objectName: 'task' } as ObjectViewSchema}
views={[{ id: 'v1', label: 'V1', type: 'calendar', sort: [{ field: 'name', direction: 'desc' }] }] as any}
views={[{ id: 'v1', label: 'V1', type: 'calendar', sort: [{ field: 'name', order: 'desc' }] }]}
dataSource={ds as any}
/>,
);
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(
<ObjectView
schema={{ type: 'object-view', objectName: 'task' } as ObjectViewSchema}
// @ts-expect-error — `direction` is not a key of the sort entry (objectui#5293)
views={[{ id: 'v1', label: 'V1', type: 'calendar', sort: [{ field: 'name', direction: 'desc' }] }]}
dataSource={ds as any}
/>,
);
// 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' });
});

Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-view/src/__tests__/ObjectView.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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(
Expand Down
Loading