You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 8c1bf5b
Browse filesBrowse the repository at this point in the historyBrowse files
fix(plugin-list): a stored view sort the platform refuses is not sent, so the list loads; an explicit column honours its field's sortable: false under client-side sorting (#30032)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBNVuXH5MLbuSJQfjBFcLvKn)_Fixes#30024
Clause-②: no
Metadata-protocol: none
**Landing order with #30021:** #30021 merged first (`140d8ce8`,
2026-11-04T08:35Z) and this branch carries it (merge of `origin/main`
`c618e890`). Land this PR before the next release cut so both ship in
the same release: a release with #30021 and without this PR fails to
load any existing view whose `sort` names a `sortable: false` field.
This PR drops only entries the served projection refuses, so it needs no
change of its own when the projection gains a verdict.
## 维护者速读
-
改了什么:列表视图发给服务器的排序,先按平台随对象元数据下发的「可排序投影」去掉平台拒收的那几项(取数、服务端导出、看板/日历/时间线/甘特各自取数都一样),剩下的按原顺序照发;表格里显式列出的列,字段写了
`sortable: false` 时,浏览器内排序也不再给排序点击。
- 为什么改:已存视图的 `sort` 若指向平台拒收的字段(今天是公式字段,#30021 落地后还有 `sortable: false`
字段),列表首次取数就把它发出去,接口答 400 `INVALID_SORT`,整张列表显示加载失败而不是数据。
-
风险与回滚:只少发平台本来就拒收的排序项,其余请求字节不变;没有投影时(旧平台、内联数据)行为不变;被拒那一项仍留在排序面板里「仅可移除」,下次保存时被丢弃(原有行为)。回滚即
revert 本 PR。
- 先看:`ui/packages/plugin-list/src/ListView.tsx` 的 `serverSort`(新的请求侧排序,及
`platformSortability` 上移到 `objectDef`
旁)、`ui/packages/plugin-grid/src/objectGridGenerateColumns.tsx` 第一条路径的
`sortable`
行、`ui/packages/plugin-list/src/__tests__/ListView.refusedSortNotSent-30024.test.tsx`。
- 两分钟验证:`cd ui && pnpm exec vitest run
packages/plugin-list/src/__tests__/ListView.refusedSortNotSent-30024.test.tsx
packages/plugin-grid/src/__tests__/clientSortAuthorUnsortable-30024.test.tsx`,预期
8 个用例全过。
- 需要你做的:无,席位落地。
## What changes
- **Fetch side (`ListView.tsx`).** `serverSort` is `currentSort` passed
through `filterPlatformSortableSort(currentSort, platformSortability)`,
the same reader the header display (`ObjectGrid` `manualSort`) and the
persist path (`emitSortChange`) already use. The data fetch's
`$orderby`, the server export's `sort`, and the `sort` handed to child
views read it. The kanban, calendar, timeline and gantt fetch their own
rows from that `sort`, so a refused entry reaches the server from none
of them. With no projection served, `serverSort` is `currentSort`
unchanged.
- **One projection read.** `platformSortability` moved up beside
`objectDef`, because the fetch effect's dependency list is evaluated
where the effect is declared and the old declaration sat about 2,000
lines below it. The sort picker and `emitSortChange` read the same memo
as before. No second reader and no spec key.
- **What stays.** `currentSort` keeps the refused entry, so the sort
picker still lists it removable only (#6455, objectui#11943), and the
picker's value and the toolbar's sort count read it.
`pageResetSignature` still reads `currentSort` by value.
- **Dropped-sort notice:** the list has no notice for a refused or
dropped entry today. The only existing affordance is the picker's
removable-only row. No new UI was added, as the order asked.
- **#6455 comments revised.** The prop docblock and the `emitSortChange`
docblock said the refused entry stays "the order this list asks the
server for". They now say that no request carries it either.
- **Header under client-side sorting (`objectGridGenerateColumns.tsx`,
path A).** Seat ruling B: an explicit column now reads its field's
`sortable: false` (`objectDefField?.sortable !== false`) beside the
column's own flag, as paths B-D (`fieldDef?.sortable !== false`) already
did. Where the sort stays in the browser, the served projection still
does not judge the header. That keeps the #3950 doctrine stated in
`ObjectGrid.tsx` `withSortability` ("Only under manual sorting. A
client-side sort keys off the value the cell renders ... which is
honest, so those headers stay"), pinned by
`ObjectGrid.unmaterializedSort.test.tsx` "keeps the formula header live
where the sort never leaves the browser". Where the sort leaves the
browser, `withSortability` still applies the projection, unchanged.
## Tests
- `ListView.refusedSortNotSent-30024.test.tsx` (6 cases). The data
source double answers `400 INVALID_SORT` (`status` 400, `code`
`INVALID_SORT`) for any `$orderby` naming a refused field, as the Data
API does. The projection is the platform's own
`resolveObjectSortability` over an object with a `formula` field
(refused as `virtual-type`) and a `sortable: false` text field (refused
as `author-declared` since #30021). No case hand-writes a verdict.
- A stored sort naming only the refused field: every request carries no
`$orderby`, and no load-error panel is drawn.
- CONTROL: a sortable field's sort is sent unchanged (`amount desc`).
- A mixed sort `[name asc, expected_revenue desc, amount desc]` sends
`[name asc, amount desc]`.
- A stored sort `[internal_code asc, amount desc]` on a `text` field
marked `sortable: false` (projection `{ sortable: false, reason:
'author-declared' }` since #30021) sends `[amount desc]`, and the list
loads.
- CONTROL: with no projection served, the refused entry is sent as
before and the double's 400 shows the load-error panel. This proves the
harness can show the failure that the fixed cells do not.
- `clientSortAuthorUnsortable-30024.test.tsx` (2 cases, inline rows so
the sort stays in the browser). A column on a `sortable: false` field
offers no sort click, while `Amount` and `Name` on the same table still
do, and no request is made. The `formula` header stays live with a
refusing projection served (#3950 doctrine).
## Acceptance notes
- **Live check on a dev boot: in progress at PR open.** #30021 is now on
this branch, so a view whose `sort` names a `sortable: false` field can
be loaded against the real door; the reading goes into the os-dev-report
on #30024.
- First fetch: the data fetch waits on `objectDefLoaded`, and
`setObjectDef(def)` and `setObjectDefLoaded(true)` are set in the same
async continuation (batched), so the first request already sees the
projection. When `getObjectSchema` fails or is absent, there is no
projection and the sort is sent unchanged, as before.
- `ObjectGrid`'s `manualSort` still filters the header arrows for
display. ListView's grid hand-off (`sort: currentSort` in the
manual-sort block) is unchanged, because `ObjectGrid` already filters it
there and the grid on that path does not fetch.
- Serial constraints, re-checked at PR time: no other in-flight
`domain:ui` claim touches `plugin-list` or `plugin-grid` (seat read
2026-11-04T08:31Z, in the claim).
## Verification
Local runs at head `a3a33266` (merge of `origin/main` `c618e890` + this
PR's two commits):
<details><summary>Suites, type-check, lint</summary>
- `cd ui && pnpm exec vitest run packages/plugin-grid/` (under
`os-verify-lock`): Test Files 277 passed (277), Tests 2376 passed
(2376), exit 0.
- `cd ui && pnpm exec vitest run packages/plugin-list/`: Test Files 189
passed (189), Tests 1796 passed (1796), exit 0.
- `pnpm --filter @object-ui/plugin-list --filter @object-ui/plugin-grid
run type-check` (`tsc --noEmit && tsc -p tsconfig.test.json`, both
packages): exit 0.
- `npx eslint` on the four changed `src` files from each package dir
(CI's ui lint form): 0 errors.
- Prerequisite: `npx turbo run build
--filter='@object-ui/plugin-list...'
--filter='@object-ui/plugin-grid...' --concurrency=2` after the main
merge: 54 tasks, exit 0.
</details>
<details><summary>Ablation (fix committed first, mutation through
scripts/ablation-replace.mjs, restored to the HEAD blob)</summary>
- Fetch-side filter removed (`serverSort` → `currentSort` at the
`$orderby` build), post-merge: 3 of 6 cases red (the `sortable: false`
case, the formula-only case, the mixed case); both CONTROLs and the
sanity case stay green. Restore: blob == HEAD, `git diff HEAD` empty,
marker count 0. The same mutation before the merge: 2 of 5 red.
- Header change removed (`objectDefField?.sortable !== false` dropped
from path A): both grid cases red (the `#3950` case waits on the
`sortable: false` header first). Restore verified the same way.
- Both test files import the code under test from `src` (relative import
/ ui's `@object-ui/*` source alias), so no dist rebuild sits between the
mutation and the run.
</details>
<details><summary>dispatch-gates --commands (35 derived, serial,
unlocked except where a gate takes the lock itself)</summary>
At PR open, 24 of 35 had run, every one `exit 0`: the ADR-0087 pair,
`check-changeset-no-major`, closing-keyword parity pair, comment-mask
corpus, empty-changeset pair, `check-sdui-manifest` pair,
release-rehearsal and release-pending-publish self-tests, `pnpm --dir ui
exec vitest run scripts/`, spec `check:duration-unit-keys`,
`check:changeset-gate-self-tests`, `check:closed-ceremony-text`,
`check:cross-package-test-inputs`, `check:dispatcher-error-vocabulary`,
`check:driver-memory-census`, `check:engine-double-contract`,
`check:gitlink-declared`, `check:nul-bytes`, `check:published-files`,
`check:query-options-erasure`. The remaining 11 (ending with
`check:type-check-debt`) were still running; the full `--ran`
reconciliation goes into the os-dev-report on #30024.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01WGufzi9idgN8uB7bkz9nTg)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(plugin-list): a stored view sort the platform refuses is no longer sent, so the list loads instead of failing; a column on a `sortable: false` field offers no sort when the grid sorts in the browser
7
+
8
+
Clause-②: no
9
+
Metadata-protocol: none
10
+
11
+
**Visible behaviour change on a released page.** A list view whose stored `sort` (or a restored user sort) names a field the platform will not order by used to send that sort with its data request. The Data API answers such a sort `400 INVALID_SORT`, so the list showed its load error instead of its rows. Fields the platform refuses are the ones its sortability projection, served with the object's metadata, marks unsortable: a `formula` field today, and a field marked `sortable: false` once the platform honours that flag.
12
+
13
+
-**`@object-ui/plugin-list`:** the list's data request, its server export and the kanban, calendar, timeline and gantt views (which fetch their own rows) now send the sort without the refused entries. The entries left keep their original order, and a sort with nothing refused in it is sent unchanged. The refused entry is still listed in the sort picker as removable only, as before, and the next sort the list saves leaves it out. With no projection served (an older platform, inline rows), the sort is sent as before.
14
+
-**`@object-ui/plugin-grid`:** a column listed in a view's `columns` now reads its field's `sortable: false`, as columns derived from the object's fields already did. Where the grid sorts the rows it holds (inline rows), such a column no longer offers a sort click. A `formula` column keeps its sort click there, since the browser orders by the value the cell shows; where the sort is sent to the server, the platform's projection still decides, as before.
},[schema.objectName,schema.data,dataSource,authoredFilter,effectivePageSize,currentSort,appliedFilters,appliedUserFilterConditions,appliedQueueSittingConditions,refreshKey,searchTerm,schema.searchableFields,schema.columns,schema.kanban,schema.calendar,schema.gallery,schema.timeline,schema.gantt,schema.map,objectDef?.fields,objectDefLoaded,schema.refreshTrigger,perms,fetchSkip,groupingConfig,predicateProjectionKey,invalidationNonce,groupingNeedsHeaderQuery,calendarWindowKey,rowsCarryConcurrencyToken]);// Re-fetch on filter/sort/search/refreshTrigger/perms/window change; `rowsCarryConcurrencyToken` (objectui#12292) re-reads the rows with their token when inline edit turns on
3879
+
},[schema.objectName,schema.data,dataSource,authoredFilter,effectivePageSize,serverSort,appliedFilters,appliedUserFilterConditions,appliedQueueSittingConditions,refreshKey,searchTerm,schema.searchableFields,schema.columns,schema.kanban,schema.calendar,schema.gallery,schema.timeline,schema.gantt,schema.map,objectDef?.fields,objectDefLoaded,schema.refreshTrigger,perms,fetchSkip,groupingConfig,predicateProjectionKey,invalidationNonce,groupingNeedsHeaderQuery,calendarWindowKey,rowsCarryConcurrencyToken]);// Re-fetch on filter/sort/search/refreshTrigger/perms/window change; `rowsCarryConcurrencyToken` (objectui#12292) re-reads the rows with their token when inline edit turns on
3831
3880
3832
3881
// Any change to the result-defining inputs (object, filters, sort, search,
3833
3882
// grouping, page size) invalidates the current page number — snap back to
0 commit comments