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
48 changes: 48 additions & 0 deletions .changeset/object-grid-default-sort-retired.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
---
"@objectstack/spec": minor
---

feat(spec): retire `object-grid`'s legacy `defaultSort` fallback (#11805, ADR-0049)

<!-- adr-0087: registered object-grid-default-sort-removed -->

**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep
launch-window convention ships it as `minor`, per the maintainer's #11805
ruling — 「不需要major」; the migration prescription is registered under
protocol major 18, where `os migrate meta` users will look).

`ObjectGridPropsSchema.defaultSort` was the legacy second spelling of `sort`:
a single `{ field, order }` pair the renderer read only when `sort` was absent
— measured at the `.objectui-sha` pin (`190fbd01d`),
`plugin-grid/src/ObjectGrid.tsx:1244-1246` (the `$orderby` fetch fallback) and
`:2847`, where the header-arrow path wraps it `[schema.defaultSort]`, the
exact array shape `sort` carries. One intent, two spellings; objectui's mirror
schema is parity-test-only and parses nothing at runtime, so only this
strictObject can refuse the legacy spelling (objectui#5861 retires the
renderer's reads as the consumer half, on its own schedule).

FROM → TO:

- `defaultSort: { field, order }` (no `sort` beside it) →
`sort: [{ field, order }]` — the same pair, wrapped in the array shape every
read path honours.
- `defaultSort` beside an authored `sort` → *(removed)*. The renderer's own
precedence made the fallback unread there, so the deletion is lossless.

One-line fix: rename the key to `sort` and wrap the value in an array;
`os migrate meta --from 17` lists the mechanical edits for existing sources.

The retirement kit:

- `retiredKey()` tombstone in `ObjectGridPropsSchema` — authoring the key is a
tsc error (`never`) and a parse error carrying the wrap-and-rename
prescription (the surface baseline line carries `[RETIRED]`)
- ADR-0087 registration: `ui/ObjectGridProps:defaultSort` in
`RETIRED_KEYS_BY_MAJOR[18]`, and the D2 conversion
`object-grid-default-sort-removed` (protocol 18) wired into the step-18
chain — wrap-and-rename when `sort` is absent, a pure strip when `sort` is
present
- pin tests (`component.test.ts`): a refusal pin asserting the prescription, a
no-materialize pin, and a surviving-surface pin on `sort`
- zero authored occurrences in either repo's corpora (the card's measurement,
re-run at dispatch), so no in-repo source changes ride along
2 changes: 1 addition & 1 deletion content/docs/references/ui/component.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -275,7 +275,7 @@ const result = AIChatWindowProps.parse(data);
| **filter** | `any` | optional | Base query filter (ObjectQL filter array/AST) — lowered to the wire `$filter`. THE key #7750 misspelled as plural |
| **defaultFilters** | `any` | optional | Legacy base-filter fallback, read only when `filter` is absent. Prefer `filter` |
| **sort** | `any` | optional | Initial sort (array of `{ field, order }`) |
| **defaultSort** | `any` | optional | Legacy single-sort fallback (`{ field, order }`), read only when `sort` is absent. Prefer `sort` |
| **defaultSort** | `never` | optional | [REMOVED] `object-grid` property `defaultSort` was removed in @objectstack/spec 17 (#11805, ADR-0049) — it was the legacy second spelling of `sort`: a single `{ field, order }` pair read only when `sort` was absent, so one intent had two spellings and a grid authoring both silently ignored this one. Rename the key to `sort` and wrap the value in an array (`defaultSort: { field, order }` becomes `sort: [{ field, order }]`); the pair itself is unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |
| **pagination** | `any` | optional | Pagination config (`{ pageSize, pageSizeOptions, … }`); its presence enables paging |
| **pageSize** | `number` | optional | Flat page-size shorthand; `pagination.pageSize` wins when both are set |
| **showPagination** | `boolean` | optional | Show the pager (read only when `pagination` is absent) |
Expand Down
2 changes: 1 addition & 1 deletion packages/spec/authorable-surface/ui.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -719,7 +719,7 @@
"ui/ObjectGridProps:conditionalFormatting",
"ui/ObjectGridProps:data",
"ui/ObjectGridProps:defaultFilters",
"ui/ObjectGridProps:defaultSort",
"ui/ObjectGridProps:defaultSort [RETIRED]",
"ui/ObjectGridProps:editable",
"ui/ObjectGridProps:exportOptions",
"ui/ObjectGridProps:fields",
Expand Down
193 changes: 193 additions & 0 deletions packages/spec/src/conversions/registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7901,6 +7901,198 @@ const pageComponentResponsiveRemoved: MetadataConversion = {
},
};

/**
* `object-grid`'s legacy single-sort fallback leaves the contract (protocol 18,
* #11805, ADR-0049 enforce-or-remove; maintainer ruling 2026-08-25,
* decision-inbox batch 4 — the producer half of objectui#5861, under the
* objectui#4869 「接受所有」 direction).
*
* `defaultSort` was the second spelling of `sort`: a SINGLE `{ field, order }`
* pair the renderer read only when `sort` was absent — measured at the
* `.objectui-sha` pin (`190fbd01d`), `plugin-grid/src/ObjectGrid.tsx:1244-1246`
* (the `$orderby` fetch fallback) and `:2847`, where the header-arrow path
* wraps it `[schema.defaultSort]`, the exact array shape `sort` carries. One
* intent, two spellings, and only the spec's strictObject can refuse the
* legacy one: objectui's mirror schema exists for parity tests and parses
* nothing at runtime, so stopping the reads without this producer-side
* retirement would turn a declared contract into a silent no-op.
*
* NOT a pure lossless delete, unlike {@link pageStructureInertKeysRemoved}:
* when `sort` is absent the fallback WAS the grid's sort, so the conversion
* carries the pair over — renamed to `sort` and wrapped in the array shape the
* renderer itself wraps it into at `:2847`. When `sort` is present the
* fallback was never read (the renderer's own precedence), and the key strips
* as a lossless delete. Zero authored occurrences in either repo's corpora
* (the card's measurement, re-run at dispatch), so this entry exists for
* stored `sys_metadata` rows and for authors outside the repo.
*
* objectui#5861 (retire the renderer's two reads) is the consumer half and
* proceeds on its own schedule after a released spec version reaches the pin.
*/
const objectGridDefaultSortRemoved: MetadataConversion = {
id: 'object-grid-default-sort-removed',
toMajor: 18,
retiredFromLoadPath: true,
surface: 'page.component.object-grid.defaultSort',
summary:
"object-grid component prop 'defaultSort' removed (#11805 — the legacy single-sort second "
+ "spelling of 'sort', read only when 'sort' was absent; the pair moves to sort: [{ field, "
+ 'order }], the array shape every read path honours)',
apply(stack, emit) {
return mapPageComponents(stack, (component, path) => {
if (component.type !== 'object-grid') return component;
const properties = component.properties;
if (!isDict(properties) || !('defaultSort' in properties)) return component;
if ('sort' in properties) {
// `sort` present: the fallback was never read — a pure lossless delete.
const stripped = stripKeys(properties, ['defaultSort'], emit, `${path}.properties`);
return { ...component, properties: stripped };
}
// `sort` absent: the fallback WAS the sort. Carry the pair, wrapped in
// the array shape `sort` reads (the renderer's own `[schema.defaultSort]`
// equivalence); an already-array value moves as-is — an array never
// worked in `defaultSort` (the fetch path read `.field` off it), and
// `sort` is where an array is legal.
const { defaultSort, ...rest } = properties;
emit({ from: 'defaultSort', to: 'sort', path: `${path}.properties.sort` });
return {
...component,
properties: { ...rest, sort: Array.isArray(defaultSort) ? defaultSort : [defaultSort] },
};
});
},
fixture: {
before: {
pages: [
{
name: 'work_queue',
regions: [
{
name: 'main',
components: [
// The fallback IS the sort: no `sort` beside it, so the pair
// moves — renamed and wrapped.
{
type: 'object-grid',
id: 'g1',
properties: { objectName: 'crm_task', defaultSort: { field: 'due_date', order: 'asc' } },
},
// Both spellings authored: `sort` wins (the renderer's own
// precedence), so the fallback strips as a lossless delete.
{
type: 'object-grid',
id: 'g2',
properties: {
objectName: 'crm_task',
sort: [{ field: 'priority', order: 'desc' }],
defaultSort: { field: 'due_date', order: 'asc' },
},
},
// `defaultSort` on a component that is not an object-grid —
// not this entry's key (the strip is scoped by component type,
// never by key name).
{
type: 'object-kanban',
id: 'k1',
properties: { objectName: 'crm_task', defaultSort: { field: 'due_date', order: 'asc' } },
},
// The nested position (#6775's lesson): a grid inside a card's
// `children` is still a component.
{
type: 'page:card',
id: 'c1',
properties: {
children: [
{
type: 'object-grid',
id: 'g3',
properties: { objectName: 'crm_lead', defaultSort: { field: 'created_at', order: 'desc' } },
},
],
},
},
],
},
],
},
// The named-slot shape (#6776): a grid authored into a slotted page.
{
name: 'work_queue_detail',
kind: 'slotted',
regions: [],
slots: {
details: {
type: 'object-grid',
id: 'g4',
properties: { objectName: 'crm_task', defaultSort: { field: 'updated_at', order: 'desc' } },
},
},
},
],
},
after: {
pages: [
{
name: 'work_queue',
regions: [
{
name: 'main',
components: [
{
type: 'object-grid',
id: 'g1',
properties: { objectName: 'crm_task', sort: [{ field: 'due_date', order: 'asc' }] },
},
{
type: 'object-grid',
id: 'g2',
properties: {
objectName: 'crm_task',
sort: [{ field: 'priority', order: 'desc' }],
},
},
{
type: 'object-kanban',
id: 'k1',
properties: { objectName: 'crm_task', defaultSort: { field: 'due_date', order: 'asc' } },
},
{
type: 'page:card',
id: 'c1',
properties: {
children: [
{
type: 'object-grid',
id: 'g3',
properties: { objectName: 'crm_lead', sort: [{ field: 'created_at', order: 'desc' }] },
},
],
},
},
],
},
],
},
{
name: 'work_queue_detail',
kind: 'slotted',
regions: [],
slots: {
details: {
type: 'object-grid',
id: 'g4',
properties: { objectName: 'crm_task', sort: [{ field: 'updated_at', order: 'desc' }] },
},
},
},
],
},
// Four notices: three wrap-and-renames (g1, the nested g3, the slotted g4)
// and one strip (g2, where `sort` already won).
expectedNotices: 4,
},
};

export const CONVERSIONS_BY_MAJOR: Readonly<Record<number, readonly MetadataConversion[]>> = {
11: [flowNodeHttpRename, pageKindJsxToHtml, flowNodeFilterAlias, objectCompactLayoutRename],
13: [stackRolesToPositions, owdLegacyReadAliases, sharingRecipientRoleToPosition],
Expand DownExpand Up@@ -7985,6 +8177,7 @@ export const CONVERSIONS_BY_MAJOR: Readonly<Record<number, readonly MetadataConv
mappingLookupParamsRemoved,
translationComponentSubmitLabelRemoved,
pageComponentResponsiveRemoved,
objectGridDefaultSortRemoved,
],
};

Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

// #11805 — ADR-0049 enforce-or-remove (maintainer ruling 2026-08-25,
// decision-inbox batch 4: 「#11805 退役 defaultSort,不需要major」; the producer
// half of objectui#5861, under the objectui#4869 「接受所有」 direction).
// `defaultSort` was the legacy second spelling of `object-grid`'s `sort`: a
// single `{ field, order }` pair the renderer read only when `sort` was absent
// (measured at the `.objectui-sha` pin `190fbd01d`,
// `plugin-grid/src/ObjectGrid.tsx:1244-1246` fetch fallback and `:2847`, which
// wraps it `[schema.defaultSort]` — the exact array shape `sort` carries). One
// intent, two spellings; objectui's mirror schema is parity-test-only and
// parses nothing at runtime, so only this repo's strictObject can refuse the
// key. Zero authored occurrences in either repo's corpora (the card's
// measurement, re-run here at dispatch).
//
// Registered under 18, not 17: v17.0.0 was cut before this landed, so the
// removal ships on the 17.x line (launch-window convention: accept-set
// narrowings ride minor releases) and the prescription lives at the major
// boundary where `migrate meta` users look (the #8495 / PR #8666 precedent,
// as `data/Metric:filters` before it). Tombstoned with `retiredKey()` in
// `ObjectGridPropsSchema` (the surface baseline line carries `[RETIRED]`);
// sources are rewritten by the D2 conversion `object-grid-default-sort-removed`
// (wrap-and-rename to `sort: [pair]` when `sort` is absent; a pure lossless
// delete when `sort` is present, since the fallback was never read then).
export const entry = 'ui/ObjectGridProps:defaultSort';
38 changes: 37 additions & 1 deletion packages/spec/src/migrations/registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5195,7 +5195,19 @@ const step18: MigrationStep = {
'never consulted (zero value reads; the only non-test occurrences were the ' +
'schema declaration and two type positions). The `kind` bucket itself and its ' +
'`id` are untouched; file-type discovery stays single-channel on `filePatterns`. ' +
'D3 semantic `plugin-manifest-kind-globs-retired`, same no-seam reasoning.',
'D3 semantic `plugin-manifest-kind-globs-retired`, same no-seam reasoning. ' +
'Finally, it retires `object-grid`\'s `defaultSort` (#11805, ADR-0049 enforce-or-remove; ' +
'maintainer ruling 2026-08-25, decision-inbox batch 4 — the producer half of ' +
'objectui#5861, under the objectui#4869 「接受所有」 direction): the legacy second ' +
'spelling of `sort`, a single `{ field, order }` pair the renderer read only when ' +
'`sort` was absent (measured at the `.objectui-sha` pin `190fbd01d`, ' +
'`plugin-grid/src/ObjectGrid.tsx:1244-1246` and `:2847`, which wraps it ' +
'`[schema.defaultSort]` — the exact array shape `sort` carries). One intent, two ' +
'spellings; objectui\'s mirror schema is parity-test-only and parses nothing at ' +
'runtime, so only the spec strictObject can refuse the key. The mechanical ' +
'conversion carries the pair over — renamed to `sort` and wrapped in the array ' +
'shape — when `sort` is absent, and strips it as a pure lossless delete when ' +
'`sort` is present (the renderer\'s own precedence made it unread then).',
conversionIds: [
'field-malformed-scale-precision-removed',
'record-chatter-position-vocabulary',
Expand All@@ -5208,6 +5220,7 @@ const step18: MigrationStep = {
'mapping-lookup-params-removed',
'translation-component-submit-label-removed',
'page-component-responsive-removed',
'object-grid-default-sort-removed',
],
semantic: [
// One file per entry under `entries/semantic/`, concatenated here sorted by
Expand DownExpand Up@@ -7447,6 +7460,29 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly<Record<number, readonly string[]>>
// `element-input-target-variable-removed` (a page component IS a stack
// collection member, unlike the `kernel/Manifest:loading` family).
'ui/ElementTextInputProps:targetVariable',
// #11805 — ADR-0049 enforce-or-remove (maintainer ruling 2026-08-25,
// decision-inbox batch 4: 「#11805 退役 defaultSort,不需要major」; the producer
// half of objectui#5861, under the objectui#4869 「接受所有」 direction).
// `defaultSort` was the legacy second spelling of `object-grid`'s `sort`: a
// single `{ field, order }` pair the renderer read only when `sort` was absent
// (measured at the `.objectui-sha` pin `190fbd01d`,
// `plugin-grid/src/ObjectGrid.tsx:1244-1246` fetch fallback and `:2847`, which
// wraps it `[schema.defaultSort]` — the exact array shape `sort` carries). One
// intent, two spellings; objectui's mirror schema is parity-test-only and
// parses nothing at runtime, so only this repo's strictObject can refuse the
// key. Zero authored occurrences in either repo's corpora (the card's
// measurement, re-run here at dispatch).
//
// Registered under 18, not 17: v17.0.0 was cut before this landed, so the
// removal ships on the 17.x line (launch-window convention: accept-set
// narrowings ride minor releases) and the prescription lives at the major
// boundary where `migrate meta` users look (the #8495 / PR #8666 precedent,
// as `data/Metric:filters` before it). Tombstoned with `retiredKey()` in
// `ObjectGridPropsSchema` (the surface baseline line carries `[RETIRED]`);
// sources are rewritten by the D2 conversion `object-grid-default-sort-removed`
// (wrap-and-rename to `sort: [pair]` when `sort` is absent; a pure lossless
// delete when `sort` is present, since the fallback was never read then).
'ui/ObjectGridProps:defaultSort',
// #11027 — ADR-0049 enforce-or-remove (maintainer ruling 2026-08-22, ruled B:
// retire + repair the redirect texts in the same change). The LAST carrier of
// the `ResponsiveConfig` layout block, and the destination the
Expand Down
Loading
Loading