Skip to content

ListView's sort picker persists a platform-refused sort — the in-use exception re-emits it, and filterPlatformSortableSort has only one consumer #6455

Description

@claude

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:

  • 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 of RelatedList'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 $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.

Refs: #6108 · #5729 · objectstack-ai/objectstack#10235


Generated by Claude Code

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions