Skip to content

Commit c2d353d

Browse 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>
1 parent 8c1bf5b commit c2d353d

0 file changed

File tree

    0 commit comments

    Comments
     (0)