Skip to content

fix(plugin-list): the Sort picker's field list asks the column read check (objectui#11943) - #11962

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-11943-sort-field-read
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-11943-sort-field-read

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #11943
Clause-②: no

The Sort picker no longer offers a field the caller may not read. Choosing such a field sent a sort the server refuses with 403, and the list went blank. Left for the follow-up: an unreadable field that the current sort already names stays listed exactly as before, unmarked and still choosable in the picker's other rows, because listing it as removable only needs a new SortBuilder prop (objectui#11943's ruling B; the seat returns that half to triage).

What changes

  • One predicate. objectui#11925's read moves out of the filterFields memo into a module-level function in ListView.tsx, canReadField(perms, objectName, field). It is perms.checkField(objectName, field, 'read') behind the same isLoaded gate as the column list. The Filter panel's list and the Sort picker's list both call it. The filter list behaves exactly as before: its seven objectui#11925 pins run unchanged and stay green.
  • sortFields asks it first. A field the user may not read is dropped unless the current sort names it. Before the permission answer loads nothing is filtered, matching effectiveFields. A dropped lookup no longer raises the relational hint, which explains a missing relation the user could read.
  • The effectiveFields comment states what is true. The old one said unreadable columns also disappear from the hide-fields popover, the filter and sort builders and $select because they are filtered there. None of those lists is built from effectiveFields. The new comment names which lists are built from it, which ask the read themselves, and the Sort picker's in-use exception with its follow-up.

Route note. The seat asked for the predicate at component scope inside ListView. It lives at module scope in the same file instead. A component-scope function would be either a useCallback identity in two useMemo dependency lists, which AGENTS.md #10 forbids, or a per-render closure that react-hooks/exhaustive-deps flags in both memos. As a plain function of perms and schema.objectName, both of which the memos already list, it needs neither.

Pins

packages/plugin-list/src/__tests__/ListView.sortFieldRead-11943.test.tsx reads the real SortBuilder dropdown through MePermissionsProvider:

  1. an unreadable field not in the sort is not offered;
  2. control: full read, and no provider at all, both list today's fields in today's order;
  3. before isLoaded: a provider refetching with the restricted answer still held (checkField would deny, isLoaded is false) lists everything, as the columns do;
  4. an unreadable field already in the sort stays listed, with its row named rather than blank. This pins the known half-state, and the follow-up will change it;
  5. an unreadable lookup does not raise the relational hint, and a readable one still does.

Reverse check, on the committed fix 985ec3c, each mutation landed and restored through scripts/ablation-replace.mjs (anchor 1 to 0, blob 0edf2348fdde changed, restore blob equal to HEAD and git diff HEAD empty), running the new file plus the objectui#11925 file:

Mutation Predicted red Observed
M1: delete the sort read line (the fix reverted) pins 1, 3, 5 Tests 3 failed / 9 passed (12): pins 1, 3, 5
M2: drop the isLoaded leg of canReadField pin 3 Tests 1 failed / 11 passed (12): pin 3
M3: drop the in-use exception from the sort read pin 4 Tests 1 failed / 11 passed (12): pin 4

The seven objectui#11925 pins stayed green under all three mutations.

Gates

On HEAD 985ec3c, through the container's verify lock where heavy:

  • pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-list^...' build: VERDICT command-exit 0 (13 of 47 workspace projects).
  • pnpm --filter @object-ui/plugin-list type-check (tsc --noEmit && tsc -p tsconfig.test.json): exit 0. --listFilesOnly lists the new test file.
  • pnpm exec vitest run --maxWorkers=2 packages/plugin-list/src/__tests__/ListView packages/core/src/utils/__tests__/column-identity.ratchet.test.ts, which covers every ListView* file in plugin-list (the objectui#11925 pins among them), the new file and the ratchet: Test Files 83 passed (83), Tests 789 passed (789).
  • node scripts/check-changeset-presence.mjs: exit 0, "2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)". node scripts/check-changeset-no-major.mjs: exit 0.
  • check:new-line-citations: VERDICT new-cross-file-line-citations: 0 new citation(s). check:control-bytes: OK.
  • Also exit 0: check:changeset-claims, check:pending-changeset-literals, check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:phantom-deps, check:unreferenced-sources, check:i18n-keys, check:shell-escape-residue.
  • Lint, narrowed and measured. eslint --format json over the 2 touched TS files (the config's **/*.{ts,tsx} population) found 2 files and 0 errors. ListView.tsx has 188 warnings, equal to its base blob linted over stdin; the new test has 0. eslint.config.js enables no type-aware linting and no eslint-rules rule reads other files, so untouched files cannot move. Repo-wide lint is CI's.

Acceptance notes

  • Not filed, from source reading only: the hide-fields popover (allFields) lists every declared column with no read check, so a restricted user can see an unreadable column's label there. Toggling it changes nothing, because the column is already gone. The old effectiveFields comment claimed the opposite. The new comment makes no claim about that popover.
  • This branch was fast-forwarded to main d92b2a1 before the change. Changeset: .changeset/11943-sort-field-read.md, a patch for @object-ui/plugin-list.

Session: https://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU


Generated by Claude Code

…heck (objectui#11943)

The Sort picker built its list from the object definition with no
field-level read check, so it offered a field the grid had dropped, and
choosing it sent a sort the server refuses, blanking the list.

- objectui#11925's read predicate moves out of the filterFields memo into
  one module-level function, canReadField, which the Filter panel's list
  and sortFields both call. The filter list's behaviour is unchanged.
- sortFields drops every unreadable field the current sort does not name.
  A field the current sort already names stays listed as before; making it
  removable only needs a SortBuilder change and is the card's follow-up.
- Before the permission answer loads nothing is filtered, as the columns
  defer.
- The effectiveFields comment now states what is true: which lists are
  built from it, and which ask the read themselves.

Claude-Session: https://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 337 chunks) 3307.1 KB 3312.0 KB
Main entry chunk (gzip) 71.3 KB 350 KB
Entry file index-CJGaonAk.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 17.82KB 6.58KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 587.49KB 141.58KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 266.92KB 67.47KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.50KB 11.82KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.39KB 15.52KB
plugin-charts (index.js) 84.71KB 23.25KB
plugin-chatbot (index.js) 199.63KB 47.46KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 248.57KB 65.47KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 176.62KB 45.75KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 248.06KB 68.79KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 117.68KB 29.40KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.10KB 11.81KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.07KB 22.93KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 8, 2026 12:07
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 8, 2026 12:07
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit e44f9bc Oct 8, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11943-sort-field-read branch October 8, 2026 12:25
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 9, 2026
…1) and the Interfaces page-create (objectui#11932) (objectstack-ai#11967)

Part of objectstack-ai#11861
Re-lands objectui#11823 step 3: objectui#11932, the Studio Interfaces
page-create (seat `domain:ui#3`). That card stays open.
Clause-②: no
Held as a draft until objectui#11949 (the +5,120 B allowance, ruling
`6056819248` step 2) lands.
When this PR was opened, objectui#11949 was already on `main` as
`1c51e973b`, and this branch has merged it.

## What this is

A plain `git revert 3c888c6`, followed by merges of `main`. `3c888c6` is
the rollback PR objectui#11956, a single-parent squash, so `-m 1` does
not apply. It brings back:

- objectui#11931: Studio's validation "New" menu opens on common rules,
with the rule types under "Advanced". This is the validation-rule entry
of objectui#11861.
- objectui#11932: objectui#11823 step 3. The Interfaces pillar creates a
page and opens it on its source beside a live preview.

Claim `6057967120` on objectui#11861 · review `6057399892` on
objectui#11937 · ruling `6056819248` on objectui#11942.

## Tree proof

- The revert commit `7eeffd29b` has the tree of `f3a0488` (`011fe521e`),
so `git diff f3a0488 7eeffd2 --stat` prints nothing.
- `.changeset/11937-eager-budget-rollback.md` was added by `3c888c6` and
never existed at `f3a0488`. Its removal therefore shows against
`3c888c6`, not against `f3a0488`.
- Against `main`, the branch changes the 15 paths `3c888c6` changed, and
no other path.
- Twelve of the 15 hold their `f3a0488` blob byte for byte. The other
three were also changed on `main` after `3c888c6`, and they carry both
intents (next section).

## Merges of main, and the conflicts

The branch merged `main` five times: at `1c51e973b`, `d73d98770`,
`70e3d7721`, `ccc2824cf` and `247f50349`. Only two of the PRs these
brought in touch a restored file.

**objectui#11945** (objectui#11923, the *Runs on* row), merged at
`70e3d7721`.
- One conflicting hunk, in the import block of
`ObjectValidationsPanel.tsx`. This side imports `validationPresets.js`;
`main` imports `ScriptValidationSchema` / `ScriptValidationParsed` from
`@objectstack/spec/data`. Both lines are kept.
- Two files merged without conflict:
- `metadata-admin/i18n.ts`: `main` removes the
`engine.studio.rules.event.delete` row, en and zh. It stays removed.
- the `newRuleWaits-11820` pin: `main` asserts there is no Delete box.
That assertion stays.
- Check, per file: the changed lines of the merge against `main` equal
the re-land's own changed lines (`3c888c6..f3a0488`). The changed lines
of the merge against this branch's previous head equal objectui#11945's
own diff. Both comparisons were identical for all three files.

**objectui#11935** (objectui#11894, typed is-empty operators), merged at
`247f50349`.
- Two conflicting hunks, both in `ObjectValidationsPanel.tsx`. Each side
added the same `FieldOpt` member, `type`.
- The interface keeps one `type`, `main`'s `multiple` and the presets'
`system`. One doc comment names both readers of `type`.
- The field mapping keeps one `type` line, `main`'s `multiple` line with
its comment, and the presets' `system` line.
- `i18n.ts` merged without conflict, and both sides' rows are kept.
- The same check, run on this merge, leaves only the shared `type`
member and the merged comments as residual lines.

`main` moved on after `247f50349` (objectui#11961, objectstack-ai#11962, objectstack-ai#11944). None
of those touches any of the 15 paths, so they are not merged here.

## Changesets

- Restored: `.changeset/11861-validation-presets.md` and
`.changeset/11823-page-create.md`, both `@object-ui/app-shell: patch`.
- Removed: `.changeset/11937-eager-budget-rollback.md`
(`@object-ui/app-shell: patch`). This re-land supersedes it.
- `check-changeset-overwrite` is report-only and exits 0. It reports one
pre-existing changeset deleted, with `@object-ui/app-shell` "GONE from
the declaration" of that file.
- The package still bumps, because both restored changesets declare it.
This is the gate's case 3: a changeset superseded by a replacement in
the same change.

## Gates, on head `fb0c91d45` (every merge above included)

Tests use the repo-root `pnpm exec vitest run` and run under the shared
verify lock.

- **Named set:** 98 files and 631 tests, all passed. It covers:
- all eight `ObjectValidationsPanel*` suites, including objectui#11945's
`runsOn-11923` and objectui#11935's `typedEmptyOps-11894`;
- every test file that imports `interfaceCreate` or
`StudioDesignSurface`;
  - `column-identity.ratchet.test.ts`;
- the `main` pins next to the conflicts:
`ConditionBuilder.typedEmptyOps-11894`, `PagePreview.pageKind-11933` and
`SourcePageEditor.pageKind-11933`.
- **Widened set:** 87 files and 1,103 tests, all passed. It covers every
other test that imports `validationPresets`, `ObjectValidationsPanel` or
the designer string table `metadata-admin/i18n.ts`.
- `pnpm --filter @object-ui/app-shell type-check` passes, after a turbo
build of the app-shell dependency closure (28 of 28 tasks).
- Each of these gates exits 0:
  - `check:i18n-keys`, `check:i18n-designer-parity`, `check:i18n-drift`;
- `check-changeset-presence`, `check-changeset-fixed`,
`check-changeset-no-major`, `check:changeset-claims`,
`check:pending-changeset-literals`, `check-changeset-overwrite`
(report-only, see above);
  - `check:unreferenced-sources`.
- eslint on the 11 touched source and test files reports 0 errors and 22
warnings. The repo sets no `--max-warnings`. The warnings are the ones
already in these files at `f3a0488`, and the merge adds none.
- `check-governed-queue-guard --test` reports NOT GOVERNED: none of the
15 paths is on a governed surface.
- Not run: the console build. CI's `Bundle Analysis` measures the
first-load bytes against the ceiling that objectui#11949 raised.
`scripts/check-eager-closure-budget.mjs` is not touched.
- Reference point: on its push run, `f3a0488` (the tree this re-land
restores) had 66 check runs succeed, 6 skipped and 1 fail, `Bundle
Analysis`. That is 73 of 73 runs read.

---

_Generated by [Claude
Code](https://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU)_

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant