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
Found while implementing #6108 (converging ListView's sort picker onto isPlatformSortableField). Filed unassigned; deliberately not fixed there, because #6108's dispatch fenced the picker's keep-current-sort behaviour and its own "Scope when picked up" paragraph names only the predicate swap.
What is measured
@object-ui/core ships two halves of objectstack#10235 ruling A, and only one of them has more than a single consumer:
filterPlatformSortableSort — the RESTORE/EMIT leg, "drop from a sort list every entry the platform will not order by". Its docblock states the reason plainly: withholding the control stops a fresh unsortable sort from being created, but a sort persisted BEFORE the signal existed is already in stored view state, and replaying it keeps a refused $orderby alive and re-persists it on the next unrelated edit.
grep -rn filterPlatformSortableSort packages/*/src at 27f9d10f6 returns exactly one non-test consumer: packages/plugin-grid/src/ObjectGrid.tsx. ListView's picker is not behind it.
Why that is a leak and not merely an asymmetry
The picker keeps a platform-refused field listed when the CURRENT sort already names it. That exception is correct and deliberate — it is the only way a user can REMOVE a sort the server refuses outright, and without it the row renders blank and the sort is silently dropped on the next edit.
But the picker renders and emits from the same array. Editing anything else in that popover — adding a second sort key, flipping a direction — re-emits the whole sort array with the refused entry still in it. ObjectView's onSortChange turns that into persistViewPatch({ sort }): a personalization PUT that stores a column the platform answers 400 INVALID_SORT for, written by a user who never touched that row.
#5729 closed exactly this at the grid seam by filtering both what it renders and what it emits. The picker is a second door onto the same stored view state.
The design question this needs answered, not just a patch
The in-use exception and the emit filter pull in opposite directions on one array. Closing the leak means separating "what the picker LISTS" from "what the picker PERSISTS" — the refused entry stays visible so it can be removed, while never surviving a write. That is a real seam change in ListView (and possibly in ObjectView's patch path), not a one-line filter call, which is why this is a card rather than a rider.
Worth deciding at the same time: whether RelatedList needs the same treatment. Its sort is component state rather than persisted view state, so it may be out of scope by construction — that should be confirmed rather than assumed.
Found while implementing #6108 (converging ListView's sort picker onto
isPlatformSortableField). Filed unassigned; deliberately not fixed there, because #6108's dispatch fenced the picker's keep-current-sort behaviour and its own "Scope when picked up" paragraph names only the predicate swap.What is measured
@object-ui/coreships two halves of objectstack#10235 ruling A, and only one of them has more than a single consumer:isPlatformSortableField— the RENDER leg, "should this control be offered?". After The other two sort-axis consumers still re-derive sortability from field type — ListView's sort picker and RelatedList #6108 it has four consumers:ObjectGrid's headers,ListView's toolbar sort picker, and both ofRelatedList's sort entry points.filterPlatformSortableSort— the RESTORE/EMIT leg, "drop from a sort list every entry the platform will not order by". Its docblock states the reason plainly: withholding the control stops a fresh unsortable sort from being created, but a sort persisted BEFORE the signal existed is already in stored view state, and replaying it keeps a refused$orderbyalive and re-persists it on the next unrelated edit.grep -rn filterPlatformSortableSort packages/*/srcat27f9d10f6returns exactly one non-test consumer:packages/plugin-grid/src/ObjectGrid.tsx.ListView's picker is not behind it.Why that is a leak and not merely an asymmetry
The picker keeps a platform-refused field listed when the CURRENT sort already names it. That exception is correct and deliberate — it is the only way a user can REMOVE a sort the server refuses outright, and without it the row renders blank and the sort is silently dropped on the next edit.
But the picker renders and emits from the same array. Editing anything else in that popover — adding a second sort key, flipping a direction — re-emits the whole sort array with the refused entry still in it.
ObjectView'sonSortChangeturns that intopersistViewPatch({ sort }): a personalization PUT that stores a column the platform answers400 INVALID_SORTfor, written by a user who never touched that row.#5729 closed exactly this at the grid seam by filtering both what it renders and what it emits. The picker is a second door onto the same stored view state.
The design question this needs answered, not just a patch
The in-use exception and the emit filter pull in opposite directions on one array. Closing the leak means separating "what the picker LISTS" from "what the picker PERSISTS" — the refused entry stays visible so it can be removed, while never surviving a write. That is a real seam change in
ListView(and possibly inObjectView's patch path), not a one-line filter call, which is why this is a card rather than a rider.Worth deciding at the same time: whether
RelatedListneeds the same treatment. Its sort is component state rather than persisted view state, so it may be out of scope by construction — that should be confirmed rather than assumed.Refs: #6108 · #5729 · objectstack-ai/objectstack#10235
Generated by Claude Code