Uh oh!
There was an error while loading. Please reload this page.
refactor(plugin-list,plugin-detail): converge the last sort-axis consumers on the served sortability signal - #6454
Merged
Conversation
…umers on the served signal ListView's toolbar sort picker and both of RelatedList's sort entry points decided "can the platform order by this?" by re-deriving it from the field's type — `UNMATERIALIZED_FIELD_TYPES` in the picker, `isUnmaterializedFieldType` in the related list. objectstack#10235 ruling A serves that verdict per column on the metadata envelope, and objectui#5729 landed its consumer spelling in `@object-ui/core`; the grid header already reads it. These three now do too. The re-derivation agreed with the projection about `formula`, which is why the drift went unnoticed. It disagrees on everything the projection encodes as absence (an unknown name, a dotted path, an unprovisioned audit column), on any refusal carrying no `reason: virtual-type`, and in the other direction on a field the platform has since started ordering by. Kept deliberately separate: the relational carve-out (the projection says a `lookup` IS sortable; the UI withholds for a different reason), and the picker's in-use exception, which is the only way to remove a sort the server refuses. A deployment that served no projection keeps the type read as a compatibility floor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
…drift family The two combined cells named nothing when they failed: "expected true to be false" over six assertions. Each family the served projection and the deleted type read disagree on — an absent name, a dotted path, a refusal carrying no `virtual-type` reason, and a formula the platform DOES order by — now has its own test on each of the two surfaces, so a regression says which one moved. Measured against the re-derivation put back: 8 of 13 red, and the 5 that stay green are exactly the agreement and floor cells (the relational carve-out on both surfaces, the no-projection fallback on both, and client mode, which never consults the signal). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-support-ai
marked this pull request as ready for review
August 26, 2026 01:36
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#6108
The last three sort-axis consumers now read the platform's per-column sortability
signal instead of re-deriving it from the field's type. Inherits objectstack#10235
maintainer ruling A through #5729's landed contract spelling —
isPlatformSortableFieldin
@object-ui/core, whichObjectGrid's header already reads and which this PR doesnot touch.
The census, before anything was written
The card named two files and two line numbers. Both were re-derived against
origin/mainfdefd4b01rather than trusted, and one of them had drifted:plugin-list/src/ListView.tsx:2409:2464plugin-detail/src/RelatedList.tsx:1146,1391RelatedListdoes not mentionUNMATERIALIZED_FIELD_TYPESanywhere — a grep for thatsymbol surfaces only
ListViewandcore. It re-derives throughisUnmaterializedFieldType(fieldDef), which is the same judgement wearing another name.Censusing the shape ("decides sortability from the field's type") rather than the
symbol is what found it; the card's claim was correct and this card covers three surfaces,
not one.
What changed
ListView.tsx— toolbar sort picker!UNMATERIALIZED_FIELD_TYPES.has(field.type)isPlatformSortableField(projection, field.value)RelatedList.tsx— embedded table headersisUnmaterializedFieldType(fieldDef)withheldFromServerSortRelatedList.tsx—data-listsort-button rowisUnmaterializedFieldType(fieldDef)The two related-list entry points never shared a derivation, which is how the same refused
sort stayed reachable through whichever control the other one did not cover; they now go
through one
React.useCallback.Three things deliberately preserved
sortable: truefor alookup— the platform's question is whether it can order by thestored foreign key, which it can. Ours is whether that order means anything beside a
column of names, and it does not (objectstack#4256). Folding this into the signal would
hand every relational control its sort back. This is the same split
ObjectGridmakes.stays listed — the only way to remove a sort the server refuses outright, and the reason
a saved-before-the-signal view neither renders a blank row nor re-emits the refused entry
into the next
persistViewPatch({ sort }). It now covers platform-refused fields, notjust formulas.
readObjectSortabilityreturningundefinedmeans "no signalwas served" — an older backend, an inline/mock data source, a schema fetch not yet landed
— not "nothing is sortable". That branch keeps the type read, so behaviour on a
pre-objectstack#10235 deployment is unchanged.
The pins, and why they are not vacuous
isPlatformSortableFieldand the deleted re-derivation agree on most inputs — theplatform computes its projection from the same
@objectstack/specstorage fact, which isexactly why the drift went unnoticed across two cards. A pin over
formulawould passagainst the re-derivation too and prove nothing. Every load-bearing cell is an input where
the two disagree:
audited_at)formula)account.name)reason: virtual-type(remote_status)text)formulait DOES order by (rolled_total)The last row is the direction no type read can ever follow. Controls sit in the same render
throughout — a stored column that stays sortable (which also proves the surface rendered at
all), a
formulaboth readings withhold, and the relational carve-out.Everything that is not a drift cell is generated by the platform's own
resolveObjectSortability(@objectstack/spec/api), the resolver the REST layer serves theprojection from — agreement over a copied table.
Reverse verification
Each surface's pins were run with the re-derivation put back. Predicted direction, stated
before each run: RED on the drift cells, GREEN on the agreement/floor cells. Mutations were
proven on disk by grepping the injected and the deleted text (never a diffstat, never an
editor's exit code); restores were proven by comparing the worktree blob hash to the
HEADblob hash plus an empty
git diff HEAD, with anEXIT/INT/TERMtrap holding absolutepaths.
ListViewpredicate revertedRelatedListpredicate revertedNo
distis in the path: the tests import../ListView/../RelatedListdirectly, and theroot vitest config aliases
@object-ui/coretopackages/core/src, so the ablation reads themutated source. The dependency closure (
pnpm --filter '<pkg>^...' build) was built firstregardless.
Verification
All runs below are at
27f9d10f6(the final commit) unless noted.pnpm exec vitest run packages/plugin-list packages/plugin-detail— 155 files, 1710tests, all passing (
Test Files 48 passed (48)/Tests 690 passed (690)andTest Files 107 passed (107)/Tests 1020 passed (1020)).pnpm --filter @object-ui/plugin-list --filter @object-ui/plugin-detail run type-check—Donefor both; each runstsc --noEmit && tsc -p tsconfig.test.json, so the new testfiles are inside the checked set rather than excluded from it.
check:control-bytes(
✅ OK (scanned 5327 tracked text file(s))),check:vi-mock-specifiers(✅ OK),check:spec-symbols(✅ spec symbol derivation),check:phantom-deps(
✅ Every in-scope import is declared by the package that publishes it),check:i18n-keys(
✅).eslint .was run complete for both affectedpackages — 219 files (59 + 160, counted from
--format json, so the population comesfrom eslint's own config resolution and not from a guess about which files count),
0 errors, 1268 pre-existing warnings. The other 45 workspace projects were excluded, and
that exclusion is provable rather than assumed:
eslint.config.jsextendstseslint.configs.recommendedwithlanguageOptions: { ecmaVersion, globals }and declaresno
parserOptions.project/projectService, so type-aware linting is not enabled andthis diff cannot move the verdict on any file it does not touch. CI runs the full farm
regardless.
Out of scope
packages/plugin-grid/**is untouched —ObjectGrid.tsxis this card's precedent consumer(read, never edited) and #6004 is in flight holding it. The convergence needed nothing there.
Generated by Claude Code
Generated by Claude Code