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(