Repository navigation
Commit c2d353d
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>1 parent 8c1bf5b commit c2d353d
0 file changed
0 commit comments