Skip to content

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

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-j2fl4e
Nov 4, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-j2fl4e

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

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 若指向平台拒收的字段(今天是公式字段,fix(spec,metadata-protocol)!: honour field.sortable false in the sortability projection and the data door #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 (docs(pm-dispatch): step 0 扫描判据扩为三个析取,兜住两种不可行动的卡片形状 (#6405) #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.
  • docs(pm-dispatch): step 0 扫描判据扩为三个析取,兜住两种不可行动的卡片形状 (#6405) #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 refactor(spec)!: remove the plugin sandboxing / integrity / approval config that never existed (#3896 follow-up) #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 fix(spec,metadata-protocol)!: honour field.sortable false in the sortability projection and the data door #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 fix(spec,metadata-protocol)!: honour field.sortable false in the sortability projection and the data door #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 (refactor(spec)!: remove the plugin sandboxing / integrity / approval config that never existed (#3896 follow-up) #3950 doctrine).

Acceptance notes

Verification

Local runs at head a3a33266 (merge of origin/main c618e890 + this PR's two commits):

Suites, type-check, lint
  • 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.
Ablation (fix committed first, mutation through scripts/ablation-replace.mjs, restored to the HEAD blob)
  • 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.
dispatch-gates --commands (35 derived, serial, unlocked except where a gate takes the lock itself)

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.


Generated by Claude Code

…explicit column honours its field's sortable: false under client-side sorting

ListView seeded currentSort from the stored view sort and mapped it into
the data request unfiltered, so a sort naming a field the served
sortability projection refuses reached the Data API and the list failed
with 400 INVALID_SORT. The fetch, the server export and the child views
that fetch their own rows now read serverSort, currentSort filtered by
filterPlatformSortableSort through the one projection read, now declared
beside objectDef.

The grid's explicit-columns path read only the column's sortable flag.
It now also reads the field's, as the derived paths already do. Under
client-side sorting the projection still does not judge the header
(#3950 doctrine kept).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzi9idgN8uB7bkz9nTg
…t sent

With the projection now refusing an author-declared sortable: false field,
pin that ListView's fetch drops that entry too and the list loads.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzi9idgN8uB7bkz9nTg
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 4, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-support-ai · session_01WGufzjDyeFwq6AuFu3vwTd

维护者速读(席位终稿)

席位核对 · ACCEPT · head a3a33266dc34117c2a8458346fe3867718d99c8a · 2026-11-04T09:46Z

  1. Shape: draft, base main; the head merges c618e890 and carries fix(spec,metadata-protocol)!: honour field.sortable false in the sortability projection and the data door #30021 (140d8ce8). Fixes #30024. One changeset: @object-ui/plugin-list and @object-ui/plugin-grid patch, Clause-②: no / Metadata-protocol: none, with the visible behaviour change stated. 5 files, +424/−21. check-governed-merges --pr 30032: NOT governed. No spec key and no new reader.
  2. Fetch side, read by the seat: serverSort = filterPlatformSortableSort(currentSort, platformSortability), or currentSort unchanged when no projection is served. It feeds the data request, the server export, and the sort handed to the child views that fetch their own rows. platformSortability is still the one memo, moved beside objectDef so that the picker and emitSortChange read the same value. The first fetch waits for objectDefLoaded (if (!objectDefLoaded) return;), so the projection is read before any request goes out. currentSort keeps the refused entry, so the picker still shows it as removable only (docs(pm-dispatch): step 0 扫描判据扩为三个析取,兜住两种不可行动的卡片形状 (#6405) #6455). The docs(pm-dispatch): step 0 扫描判据扩为三个析取,兜住两种不可行动的卡片形状 (#6405) #6455 docblocks were revised. No dropped-sort notice existed, and none was invented.
  3. Header, seat ruling B (amendment in the order): path A now reads col.sortable !== false && objectDefField?.sortable !== false, the same check paths B–D already use. The projection still judges only server sorting, so the refactor(spec)!: remove the plugin sandboxing / integrity / approval config that never existed (#3896 follow-up) #3950 doctrine and its pin are kept. Accepted.
  4. Evidence:
    • Both halves ablated red. After the merge, the fetch ablation turns 3/6 red, including the new sortable: false case.
    • Live check measured on a showcase dev boot: a view sorted [code asc, amount desc], with code marked sortable: false, loads 200 and sends sort=-amount. With the filter ablated, the same page sends sort=code,-amount and gets a 400 and the error panel.
    • Local: plugin-grid 277/277 and plugin-list 189/189 files green; type-check and eslint clean; dispatch-gates 35/35 exit 0 with --ran reconciled.
    • CI on this head: 44 success, 11 skipped.
  5. Landing: ordinary queue, now. This is a release blocker paired with fix(spec,metadata-protocol)!: honour field.sortable false in the sortability projection and the data door #30021 (already on main). Relay pr_ready + automerge_enable, follow it to MERGED, and record the landing on ui(list): a stored view sort the platform's sortability projection refuses is still sent to the server on fetch (ListView L3186), so the list 400s instead of dropping it; client-side sorting still offers a sortable: false header #30024.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 4, 2026 09:48
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 4, 2026
Merged via the queue into main with commit c2d353d Nov 4, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-j2fl4e branch November 4, 2026 10:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant