Skip to content

fix(list): sort picker falls back to all sortable fields, and gains a reset to the view's default sort (#4243) - #4299

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4243-sort-fallback-reset
Aug 11, 2026
Merged

fix(list): sort picker falls back to all sortable fields, and gains a reset to the view's default sort (#4243)#4299
yinlianghui merged 1 commit into
mainfrom
claude/issue-4243-sort-fallback-reset

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#4243

Implements the maintainer ruling recorded on #4243 on 2026-08-11 — the narrow fix, both halves, no new spec vocabulary:

Ruling: the narrow fix. (1) The Sort panel's field list falls back to all non-relation fields instead of reusing filterableFields — ⛔ no new sortableFields spec key (a new vocabulary entry with no measured pull, against startup-focus). (2) Add a "reset to view default sort" affordance so a header click no longer makes the declared multi-level sort unreachable for the session.

That ruling was itself recorded as the PM seat executing the maintainer's direct instruction in chat, quoted verbatim in the same comment: 「接受你的全部建议,请更新 issue 的状态和标签」.

Part 1 — the sort picker stops borrowing the filter whitelist

filterableFields was applied inside the one memo that built the field set both toolbar builders read, so a whitelist authored for filtering silently became the sort whitelist. The card's reported shape reproduces exactly: a view declaring a two-level default sort on plan_start_date then name, with neither field whitelisted, got a sort panel that offered neither and rendered both rows blank — Radix resolves a select's display label from its item list, so a value absent from the list renders as nothing.

The whitelist now narrows the filter builder alone. The sort picker starts from every field the view can name and applies its own sortability rules.

Fix site. Both halves are in packages/plugin-list/src/ListView.tsx. Triage cited packages/plugin-view/src/SortUI.tsx:166 as the second face; re-verified at current main, that attribution does not hold — SortUI is a separately registered sort-ui component whose fields come from its own authored SortUISchema, and it is never fed by ListView's field set. ListView's sort panel renders SortBuilder directly. The card's own claim ("one array feeds both builders") is exactly right; only the file pointer was off, so SortUI.tsx needs no change and is untouched.

The measurement rider: which types the widened set must still exclude

The dispatch asked whether formula (and other computed types) must be excluded from the widened fallback, given #3950. Measured against objectstack, read-only:

  • packages/metadata-protocol/src/protocol.ts:1593UNMATERIALIZED_SORT_TYPES is a set holding formula. A sort naming a formula field is refused there with 400 INVALID_SORT; before objectstack#6994 it degraded silently (200, every row present, asc and desc byte-identical, measured on a real SqlDriver).
  • So yes — and it matters because the base set widened: a formula field previously reached this picker only if someone had whitelisted it, and every formula field on the object would now be offered.

The more important half of the measurement is where the exclusion stops. It is formula alone, deliberately not the spec's COMPUTED_VALUE_TYPES (formula / summary / autonumber). objectstack's own conformance test pins that trap by name — "a summary field still sorts, in both directions — the family is formula, not 'computed'" — and warns that widening the gate to COMPUTED_VALUE_TYPES "would break two types that work": that set is the WRITE contract, and summary / autonumber each get a real maintained column. The renderer's constant mirrors the server's, with that reasoning recorded at the definition.

Both rules keep the existing escape hatch: a field the current sort already uses stays listed. For a formula field that is the only way to remove an offending row, since the sort it names is one the server refuses outright.

⚠️ Deliberately not fixed here: #3950's own surface, the grid's clickable sort header on formula columns (ObjectGrid's sortable flag). Different site, different fix, still open.

Part 2 — reset to the view's declared sort

The sort panel gains a Reset to view default control.

  • It restores the declared array whole — multi-level, in declared order. Clearing the sort would not put the view back, and rebuilding it by hand is what the card reports as impossible.
  • One source of truth: it reads the declared sort through parseSortConfig(schema.sort), the same resolver the initial useState and the view-switch effect already use — not a re-derivation. The comparison is by (field, order) in order, never by id, which parseSortConfig mints fresh from crypto.randomUUID() on every call.
  • It is the sibling of handleHeaderSort, not a special case of it: same currentSort state, same page-1 reset, same onSortChange notification, so a host persists a reset exactly as it persists a header click.
  • When the view declares no sort: hidden. The affordance's contract is "put back what the view declared"; with nothing declared there is no default to return to, and a control under this label that merely cleared the sort would be a second, differently-named way to do what removing the rows already does. Disabled — rather than hidden — while the active sort already equals the declared one, so it stays discoverable and says "you are at the default" instead of vanishing.
  • The header click's own semantics are unchanged: it still replaces the whole array. The ruling adds a way back; it does not change the click. Pinned as a control.

An interaction worth naming

The hint explaining the relational omission used to be gated by the same whitelist. A view whitelisting only status showed a near-empty sort picker and no word about why, because the relational field never entered the loop that records something as withheld. It now reaches the relational rule, is withheld by it, and says so. This surfaced as a reverse-verification surprise: a case labelled a control went red, and it is a pin, not a control — relabelled as such, with the reason recorded in the test.

Tests

New: packages/plugin-list/src/__tests__/ListView.sortFieldFallback.test.tsx (9 cases).

Reverse verification — the fix reverted with git checkout origin/main -- ListView.tsx, direction predicted before running, 6 red / 3 green, as predicted:

CaseReverted
offers a non-filterable field; declared rows render by namered (offered ['Status'] only, rows blank)
withholds a formula fieldred (offered)
relational rule on the widened set + its hintred (hint absent — the interaction above)
reset restores the two-level default after a header clickred
reset disabled while already at the defaultred
hidden when the view declares no sortred
keeps a formula field the current sort usesgreen both sides
CONTROL — filter builder still honours filterableFieldsgreen both sides
CONTROL — header click still replaces the whole arraygreen both sides

The "hidden when the view declares no sort" case asserts both branches — same header click, one fixture declaring a sort and one not. A bare null-check there would have been green on the unfixed code for the empty reason that no such control exists at all; the contrast is what makes it an assertion.

Green-both-sides controls for the pre-existing rules are the untouched ListView.relationalSort.test.tsx and ListView.headerSort.test.tsx — both verified green against the reverted file and against this branch.

Local runs (repo-root vitest, per objectui#3378):

pnpm exec vitest run --maxWorkers=2 packages/plugin-list/ packages/i18n/
Test Files 69 passed (69)
Tests 1144 passed (1144)
pnpm --filter @object-ui/plugin-list --filter @object-ui/i18n type-check
packages/i18n type-check$ tsc --noEmit Done
packages/plugin-list type-check$ tsc --noEmit && tsc -p tsconfig.typetests.json Done
pnpm --filter @object-ui/plugin-list --filter @object-ui/i18n lint 0 errors

Gates: check:i18n-keys, check:i18n-drift, check:control-bytes, changeset presence / no-major / fixed — all OK. The drift gate reports the new key added across all ten packs.

i18n

One new key, list.resetSortToDefault, added to all ten locale packs with native translations. No defaultValue at the call site, matching the sibling sort strings.

Out-of-scope finding

Filed #4294 (unassigned, unlabeled for triage): list.sortRelationalHint tells the user "To sort by that name, add a formula field holding it" — the one type the server refuses to sort by. objectstack's hint for the same situation says the opposite in as many words ("Not a formula field: it is virtual"). Pre-existing and untouched here; re-wording it changes an en value and so drags all nine other packs into the same PR.

Changeset

.changeset/list-sort-field-fallback-and-reset-4243.md — patch on @object-ui/plugin-list and @object-ui/i18n.


Generated by Claude Code

… reset to the view's default sort (#4243)
`filterableFields` was applied to the one field set both toolbar builders
read, so a whitelist authored for filtering silently doubled as the sort
whitelist. A view declaring a two-level default sort on non-filterable
fields got a picker offering neither and rows rendering blank: the
declared sort worked on load and could then be neither reproduced nor
modified.
The whitelist now narrows the filter builder alone. The sort picker
starts from every field the view can name and applies its own
sortability rules: the existing relational exclusion, plus `formula`,
which the server refuses to order by outright (objectstack's
`UNMATERIALIZED_SORT_TYPES`) and which matters here precisely because
the base set widened. The exclusion is `formula` alone, not the spec's
`COMPUTED_VALUE_TYPES` — `summary` and `autonumber` each get a real
maintained column and sort correctly.
Second half: a column-header click replaced the whole sort array, so a
view's multi-level default was unreachable for the session. The sort
panel gains a reset control that restores the declared array whole, read
through the same resolver the initial render uses. Disabled while the
active sort already matches; absent when the view declares no sort. The
header click's own semantics are unchanged.
Per the maintainer ruling recorded on #4243 (2026-08-11): the narrow
fix, no new `sortableFields` spec key.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercelBot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 11, 2026 12:04pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)29.4 KB350 KB
Entry fileindex-CiJxPeJ1.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.88KB3.25KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.27KB3.23KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)488.62KB108.26KB
core (index.js)3.04KB1.15KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)150.04KB39.79KB
fields (index.js)228.45KB56.62KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)16.38KB5.47KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.98KB10.85KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)45.23KB12.45KB
plugin-charts (index.js)61.73KB17.54KB
plugin-chatbot (index.js)180.33KB42.79KB
plugin-dashboard (index.js)118.79KB30.79KB
plugin-designer (index.js)210.91KB42.67KB
plugin-detail (index.js)238.98KB59.76KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)114.58KB27.68KB
plugin-gantt (index.js)164.14KB39.98KB
plugin-grid (index.js)187.97KB49.90KB
plugin-kanban (index.js)48.60KB13.41KB
plugin-list (index.js)109.93KB26.65KB
plugin-map (index.js)17.00KB5.32KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.60KB10.58KB
plugin-timeline (index.js)26.21KB7.52KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)23.71KB7.96KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.23KB0.66KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.05KB1.52KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[console] List sort: filterableFields doubles as the sort whitelist, and a header click discards the view's multi-level default with no way back

2 participants

@yinlianghui@claude