Repository navigation
fix(platform-objects): Setup → Users opens on the All Users list view - #21971
Conversation
The Setup app's nav_users entry named no view, so the console opened sys_user's first declared list view, `me` (the caller's own row, page size 1). nav_users now names `all_users` through the object navigation item's declared `viewName` key. The `me` view's comment no longer says the Account app surfaces it: the profile entry is the `account:profile_card` component. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…tup-users-all-users
📓 Docs Drift CheckThis PR changes 1 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 070347c82aa320e21b4e1d96c18ba68805c525a6 && git checkout 070347c82aa320e21b4e1d96c18ba68805c525a6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 787104baa9ecca77ff13a8e83a4b7349728feadc eb9b315b285665aeb9c950758c74220e806c9d50 && git checkout -B drift-repro 787104baa9ecca77ff13a8e83a4b7349728feadc && git merge --no-ff eb9b315b285665aeb9c950758c74220e806c9d50
node scripts/docs-audit/affected-docs.mjs --json 787104baa9ecca77ff13a8e83a4b7349728feadc
|
ACCEPT (seat review) — PR #21971 at head
|
…ist; a caller-scoped list view is never first (objectstack-ai#21983) Fixes objectstack-ai#21972 Clause-②: no ## What changes An administrator who opens Setup → API Keys, Sessions, OAuth Applications, Identity Links, User Preferences or Record Shares now lands on the tenant-wide "All" list. Before this, each of those entries named no view. The console then opened the object's first declared list view, and on every one of them that view was filtered to the caller (`user_id = {current_user_id}`, or `recipient_id` for record shares). The administrator saw only their own rows. This applies triage's rule for the family (6012877503): a caller-scoped list view is never an object's first, and every entry that wants one names it. - **Seven objects are reordered, and nothing else in them changes.** In each, the unscoped "All" view moves to first place and the caller-scoped view follows it. The other views keep their relative order. No view is added, removed or edited. - **Six Setup entries name their unscoped view with `viewName`,** as `nav_users` already does. That is the key the spec already declares on an object navigation item, so there is no new key. - **The Account app's Linked Accounts entry (`nav_account_linked`) now names `mine`.** It was the one Account object entry that relied on the declared order. With `mine` no longer first, it would otherwise have opened `all_links`. - **Two comments are corrected.** - `sys_user`'s `me` comment said RLS stops non-admins reading other users' rows. `member_default` admits the caller's organization's users through `sys_user_org_members`, and the comment now says so. - `nav_users`' comment now says `me` *used to be* first. - **The generated translation bundles follow the new order.** Eight files changed (four in each package), regenerated with `node scripts/check-i18n-bundles.mjs --write`. These are pure reorders of the `_views` keys: no translated text changed (+84 / −84). - **New pins:** `packages/platform-objects/src/apps/caller-scoped-first-list-view.test.ts`, with 29 cases (described below). - **Two changesets, both `patch`:** `@objectstack/platform-objects` and `@objectstack/plugin-sharing`. Each carries the `Clause-②: no` line. ⛔ No new key, no objectui change, no `packages/spec` edit, and no governed surface. In `plugin-sharing`, only the two declared files change (`sys-record-share.object.ts` and `sharing-plugin.ts`), plus their four regenerated bundles. Implemented by the os-dev run of session `session_017ErfyP2Rx7XWHJA27QjyUi` on branch `claude/issue-21972-caller-scoped-views-not-first`. | object | listViews at | old first | new order | Setup entry · `viewName` | Account entry · `viewName` | |:--|:--|:--|:--|:--|:--| | `sys_user` | `sys-user.object.ts:603` | `me` | `all_users`, `me`, `unverified`, `two_factor`, `banned` | `nav_users` · `all_users` (already set by objectstack-ai#21971) | none routes to `sys_user` | | `sys_api_key` | `sys-api-key.object.ts:121` | `mine` | `all_keys`, `mine`, `active`, `revoked` | `nav_api_keys` · `all_keys` | `nav_account_api_keys` · `mine` (unchanged) | | `sys_session` | `sys-session.object.ts:113` | `mine` | `all_sessions`, `mine`, `revoked` | `nav_sessions` · `all_sessions` | `nav_account_sessions` · `mine` (unchanged) | | `sys_oauth_application` | `sys-oauth-application.object.ts:210` | `mine` | `all_apps`, `mine`, `active`, `disabled_apps` | `nav_oauth_apps` · `all_apps` | `nav_account_oauth_apps` · `mine` (unchanged) | | `sys_account` | `sys-account.object.ts:80` | `mine` | `all_links`, `mine`, `by_provider` | `nav_accounts` · `all_links` | `nav_account_linked` · `mine` (**new**) | | `sys_user_preference` | `sys-user-preference.object.ts:36` | `mine` | `all_preferences`, `mine`, `by_user` | `nav_user_preferences` · `all_preferences` | none | | `sys_record_share` | `plugin-sharing/src/objects/sys-record-share.object.ts:46` | `granted_to_me` | `all_shares`, `granted_to_me`, `granted_by_me`, `by_object`, `manual_grants`, `rule_grants` | `nav_record_shares` (`sharing-plugin.ts:591`) · `all_shares` | none | ## The dispatch's hypotheses, measured - **H1 holds: each of the seven objects declares an unscoped list view.** Each one's "All" view carries no filter, except `all_sessions`, whose only filter is `revoked_at is_null`. That view is the one now first (table above). Stop condition 2 does not fire. - **H2 holds: the order changes which view opens, not which rows a caller may read.** This was read from `member_default`'s row-level security in `packages/plugins/plugin-security/src/objects/default-permission-sets.ts`. No real-door read was run. - **`sys_api_key`, `sys_session`, `sys_oauth_application` and `sys_user_preference`** carry `_self` policies with `user_id == current_user.id`, operation `all` (`:921`, `:891`, `:955`, `:915`). **`sys_account`** carries one for `select` (`:897`). Each is the same predicate as `mine`, so a member's "All" view returns exactly the rows `mine` returns. - **`sys_user`** carries `sys_user_self` (`:871`) and `sys_user_org_members`, `id in current_user.org_user_ids` (`:885`). So a member's "All Users" view returns their organization's users, and `me` did not. That is the declared staff-directory policy, and the header of the same file names it as intended. It is not an access defect: the member already had those rows through the existing "All Users" tab and through `GET /data/sys_user`, and this diff changes neither. I read stop condition 1 as not met. The reasoning is stated here so the seat can disagree. - **`sys_record_share`:** `member_default` has no wildcard object grant and names no `sys_record_share` permission, so a member reads no rows through either view. The Setup entry also requires `manage_platform_settings`. - **H3 holds: these are the readers of the declared order in this repository.** - **Account entries:** `nav_account_linked` was the only object entry without a view. All six Account object entries now name `mine`. - **Setup entries:** after this change, no Setup entry in `platform-objects` names an object whose first view is caller-scoped. The pin's (a) cases enumerate all of them. - **Code:** `packages/cli/src/commands/lint.ts:168` (`firstListViewKey`) reads the first key only to place a label diagnostic when no list view has a label. Every view here has one. - **Pages:** `sys-user.page.ts` related lists name these objects with `showViewAll: true` and no view. How objectui's "View all" picks a view was not read (NOT MEASURED). - **Dashboards:** `system.datasets.ts` reads `sys_user` and `sys_session` by object, not by view. - **Tests:** none assert a view index. `setup-users-nav-view.test.ts` and `i18n-resolver.object-list-views.test.ts` read views by name. - **objectui (out of scope):** the `views[0]` fallback, the breadcrumb and the object switcher are covered by the reorder itself. - **H4 holds: the reorder moved eight generated files.** These are `packages/platform-objects/src/apps/translations/{en,es-ES,ja-JP,zh-CN}.objects.generated.ts` and `packages/plugins/plugin-sharing/src/translations/{en,es-ES,ja-JP,zh-CN}.objects.generated.ts`. `pnpm check:i18n` first read "platform-objects DRIFTED (4)" and "plugins/plugin-sharing DRIFTED (4)". After `--write` it exits 0. The other seven packages regenerated with no diff. No count or order pin moved. ## Pins `caller-scoped-first-list-view.test.ts`, 29 cases. The population is derived from this package's navigation, not hand-listed. It is every `type: 'object'` entry of `SETUP_NAV_CONTRIBUTIONS` and `ACCOUNT_APP` (18 entries, 13 objects), looked up in this package's own exports. - **(a), 11 cases: a named object's first declared list view carries no `{current_user_id}`.** That is checked anywhere in the view, so the `${current_user_id}` spelling is included. - **(b), 12 cases:** every entry whose object declares a caller-scoped view names a `viewName`. The object must declare that view under that same name, and on a Setup entry the named view must not be caller-scoped. - **Population and non-vacuity, 4 cases:** - both apps contribute object entries; - the six objects this card reordered in this package are judged by both (a) and (b); - the named objects this package cannot read are exactly `sys_inbox_message`, owned by `service-messaging`. (b) therefore requires its entry to name a view, and it names `mine`; - the named objects that declare only caller-scoped views are exactly `sys_member`. Only the Account app names it, with `mine`. - **Parse, 2 cases:** every Setup contribution parses through `NavigationContributionSchema` and the Account app through `AppSchema`, and each object entry keeps its `viewName`. - **objectstack-ai#21960's `setup-users-nav-view.test.ts`** stays green (7 of 7). **Reach of the pins.** - **Plugin-contributed entries are outside them.** Setup entries contributed by plugins at runtime (`nav_record_shares`, `nav_approval_requests`, …) are not visible from `platform-objects`, which cannot import the plugins because they depend on it. `plugin-sharing` gets no test file here: the dispatch declares only its two files. - **The sharing half was measured once instead.** Each built plugin was booted with a fake manifest context, the way `check-app-nav-i18n` boots them, at `a4d4688cfd`. That read gives `nav_record_shares → sys_record_share | first=all_shares (unscoped) | viewName=all_shares (unscoped)`. ## Reverse verification (on committed `f757947e6c`, through `scripts/ablation-replace.mjs`) The test imports its subjects by relative path from `src/`, so no `dist/` sits between the mutation and the run. Directions were predicted before each run: one red case each. - **(a): `sys_session` restored to its old order** (`mine`, `all_sessions`, `revoked`), by one literal swap of the two adjacent view blocks. - **Mutation landed:** anchor 1 → 0, replacement 0 → 1, blob `4e46fda0773c` → `884ccaa2b537`. - **Result: 1 failed | 28 passed (29).** The failing case is `(a) … › sys_session`: "sys_session declares the caller-scoped list view "mine" first". - **Restored:** blob `4e46fda0773c` equals HEAD, `git diff HEAD` is empty, and `git status --porcelain` is empty. - **(b): `viewName: 'all_sessions', ` deleted from `nav_sessions`.** - **Mutation landed:** anchor 1 → 0, blob `17f1725c3cad` → `2d7a16c6a894`. - **Result: 1 failed | 28 passed (29).** The failing case is `(b) … › setup nav_sessions → sys_session`: "nav_sessions names no view". - **Restored:** blob `17f1725c3cad` equals HEAD, `git diff HEAD` is empty, and `git status --porcelain` is empty. ## Tests and gates, at `a4d4688cfd` That commit is the merge of `origin/main` at `dcf3eb494a`, which brought only `packages/spec` test files. The whole workspace was built before it (`turbo run build --concurrency=2`, 72 tasks). - **`pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2`: Test Files 62 passed (62), Tests 996 passed (996).** - **`pnpm --filter @objectstack/plugin-sharing exec vitest run --maxWorkers=2`: Test Files 40 passed (40), Tests 980 passed (980).** - **`pnpm --filter @objectstack/platform-objects typecheck` and `pnpm --filter @objectstack/plugin-sharing typecheck`: both exit 0, with `check:test-typecheck: OK`.** The new test file is in the `tsconfig.test.json` program, counted with `--listFiles`. - **`node scripts/pm/dispatch-gates.mjs --commands` derived 66 commands, and all 66 exited 0.** `--ran` reads "66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN". That zero is derived: every line carried its exit code. - **14 are new against the dispatch-time list:** `check-adr-0087-registration` (base and self-test), `check-empty-changeset` (base and self-test), `release-rehearsal-clone --self-test`, `release-pending-publish --self-test`, `check:engine-double-contract`, `check:objectql-double-limit`, `check:objectui-changeset`, `check:pm-changeset-deadline-census`, `check:query-options-erasure`, `check:type-check-coverage`, `check:type-check-debt` and `check:where-matcher`. - **`check:type-check-debt` ran twice.** The first run was killed by my own batch timeout. The rerun exited 0 in 221 s: "1 ledger entr(ies) re-measured … none above its recorded number". - **Artifact-roster block: 55 families, all run. 52 exited 0.** - `check-closing-target-claim.mjs`, `check-partof-closing-keyword.mjs` and `check-single-claim-paths.mjs` answer NOT WIRED without a PR context. CI runs them with one. - **Symbol-anchor sweeps:** `check:adr-symbol-anchors`, `check:scripts-symbol-anchors`, `check:spec-docblock-symbol-anchors` and `check:adr-anchors` all exited 0. - **ESLint, narrowed to the 19 touched TypeScript files:** 19 files, 0 errors, 0 warnings, none reported as ignored. - The population is read from `eslint.config.mjs`: its `packages/**/*.{ts,tsx,mts,cts}` blocks match all 19. - The count comes from `--format json`. - The config enables no type-aware linting (no `parserOptions.project`), so this diff cannot move a verdict on an untouched file. The repo-wide `pnpm lint` is CI's. ## Acceptance notes - **One more member of the family sits outside this card's lane.** - **The defect:** Setup → Approvals → Requests (`nav_approval_requests`, `packages/plugins/plugin-approvals/src/approvals-plugin.ts:147`) names no view. `sys_approval_request` declares `my_pending` first, filtered on `pending_approvers contains {current_user_id}`, so an administrator sees only the requests pending on them. - **How it was measured:** by the same built-plugin read as above. - **Why it is not here:** it is in `domain:services`, and the dispatch allows nothing in that lane beyond the two `plugin-sharing` files. It is reported to the seat on the card. - **Two named objects declare only caller-scoped list views:** `sys_member` (`mine`) and `sys_inbox_message` (`mine`). Only Account entries name them, and those entries name `mine`. No unscoped view is added here; that is triage's decision. Both are pinned as exact sets, so a third object cannot join unnoticed. - **The bare-object doors now open "All" for members too.** RLS scopes the rows (H2). On `sys_user`, a member's object breadcrumb now opens their organization's user list rather than My Profile. - **Sidebar highlight.** Each entry that now names a view lights up only on that view, as `nav_users`, `nav_notifications` and the Account `mine` entries already do. This was not measured in a browser. --- _Generated by [Claude Code](https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #21960
Clause-②: no
What changes
Setup → Users now opens on the All Users list view (
all_users). Before this, it opened My Profile (me): one row, the administrator, with a page size of 1.packages/platform-objects/src/apps/setup-nav.contributions.ts:nav_usersgainsviewName: 'all_users'. That key is the oneObjectNavItemSchemaalready declares on an object navigation item ("Default list view to open"). No new key.packages/platform-objects/src/identity/sys-user.object.ts: a comment only. Themeview's comment said the Account App surfaces it. Atmainit does not (see the readers below), and the comment now names what does reach it.packages/platform-objects/src/apps/setup-users-nav-view.test.ts: the pins..changeset/21960-setup-users-all-users.md:@objectstack/platform-objectspatch.⛔ No
listViewsreorder, no view removed, nopackages/specedit, no governed surface.Implemented by the os-dev run of session
session_017ErfyP2Rx7XWHJA27QjyUion branchclaude/issue-21960-setup-users-all-users.The console honours
viewName(measured, H1)Read in objectui at the commit
.objectui-shapins,0abd4f9f8769fc4c19ad2f96707684876f74c09f:packages/layout/src/NavigationRenderer.tsx,resolveHref, caseobject. The precedence isrecordId, thenfilters, thenviewName. WithviewNamethe href isOBJECT/view/VIEWNAME; without it, the bareOBJECTpath.packages/app-shell/src/views/ObjectView.tsx. The active view is the URL view id, then?view=, then a view markedisDefault, thenviews[0]. Only the object's primarylistis markedisDefault(buildViewTabs), andsys_userdeclares none, soviews[0]is the first declared list view:me. That is what the card observed.packages/core/src/utils/resolve-view-id.tsmatches the short nameall_usersagainst the qualified idsys_user.all_users.viewNamedescribe does not hold for an object that declares list views.allis only the id of the fallback tab the console builds for an object with no list views. This is reported on the card; it is not edited here, because it is spec text.The served path keeps the key as authored.
applyNavContributions(packages/objectql/src/registry.ts) pushes contributed items into a structured clone of the app, andfilterNav(packages/rest/src/meta-item-read-gate.ts) pushes a passing leaf item unchanged.nav_notificationsalready shipsviewName: 'recent'through the same contribution path.Who reads
me(measured, H2)nav_account_profile:type: 'component',componentRef: 'account:profile_card'. No Account entry routes tosys_user.account:profile_cardis registered inapps/console/src/registerAccountComponents.tsxand rendersProfilePage, which reads the signed-in user fromuseAuth()and writes throughupdateUser.pages/sys-user.page.tsnames no list view.ObjectNavItemSchemadocblock citessys_user.meas an example of the{current_user_id}filter vocabulary.So
meis reached only as a tab, and as the default tab when a route names no view. Triage's second pin ("the Account app's My Profile still opens on the caller's own row") is held by the profile component, and it is pinned there.Pins
setup-users-nav-view.test.ts, 7 cases:nav_userssits ingroup_people_orgwithtype: 'object',objectName: 'sys_user'andviewName: 'all_users';recordIdnorfilters, both of whichresolveHrefhonours ahead ofviewName;sys_user.listViews.all_usersexists under that same name;filter, no{current_user_id}anywhere in it, and a page size above 1;NavigationContributionSchema.safeParse, and the parsed item keepsviewName: 'all_users';nav_account_profile, theaccount:profile_cardcomponent;sys_user.No dogfood door pin. The framework half of the door is the served Setup nav, and both sites on that path pass the item through as authored (above). The data half is unchanged by this diff:
all_userscarries no filter. A door case would assert what the metadata pin already holds.Reverse verification
On committed
6639eb4a52:72a3adcc1066→7c70ade946a9.nav_users.viewNamewent red. TherecordId/filterscase and both Account cases stayed green, as expected, because none of them readsviewName.72a3adcc1066, equals the HEAD blob, andgit diff HEADis empty.git status --porcelainwas empty afterwards.src/, not through the packageexports, so nodist/sits between the mutation and the run.Tests and gates, at
eb9b315b28That is the merge of
origin/mainat2a22177ae7.packages/specwas rebuilt after the merge, then the whole workspace (turbo run build --concurrency=2, 72 tasks).pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2: Test Files 61 passed (61), Tests 967 passed (967).pnpm --filter @objectstack/platform-objects typecheck: exit 0, withcheck:test-typecheck: OK.tsconfig.test.jsonprogram, counted with--listFiles.feature-gate-guard.test.tsdebt.node scripts/pm/dispatch-gates.mjs --commandsderived 63 commands, and all 63 exited 0.--ranreconciliation reads63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN. That is a derived zero: every line carried its exit code.check-adr-0087-registration(base and self-test),check-empty-changeset(base and self-test),release-rehearsal-clone --self-test,release-pending-publish --self-test,check:engine-double-contract,check:objectql-double-limit,check:objectui-changeset,check:pm-changeset-deadline-census,check:query-options-erasure,check:type-check-coverage,check:type-check-debtandcheck:where-matcher.check-closing-target-claim.mjs,check-partof-closing-keyword.mjsandcheck-single-claim-paths.mjsanswer NOT WIRED without a PR context. CI runs them with one.check-partof-closing-keyword.mjsalso exits 0 on this body whenPR_BODYis set locally. The other two need the PR number.check:adr-symbol-anchors,check:scripts-symbol-anchors,check:spec-docblock-symbol-anchorsandcheck:adr-anchorsall exited 0.check:dual-build-cjs-loads,check:i18n,check:lean-entry-closureandcheck:published-readme-exportsfirst refused with exit 3 (nodist/). After the full build all four exited 0, andcheck:i18nreads "all bundles in sync".check:type-check-debtfirst hit the runner's 420 s cap while it refreshed its closure. The rerun exited 0, with "1 ledger entr(ies) re-measured ... none above its recorded number".eslint.config.mjs: itspackages/**/*.{ts,tsx,mts,cts}blocks match all three, and none was reported as ignored.--format json.parserOptions.project), so this diff cannot move a verdict on an untouched file.pnpm lintis CI's.Acceptance notes
me. objectui'sAppHeaderlinks the object breadcrumb segment and the object switcher to the bare object path, and a bare path opensviews[0]. So from a user's record page, the "Users" breadcrumb still lands on My Profile. This PR closes the Setup nav door, as dispatched. Changing which viewsys_useropens by default is the declaration-order route, which the dispatch reserved. It is reported on the card.resolveActiveNavItemscores aviewNameitem only on its own view. Sonav_usersno longer lights up on a user's record page or on the other tabs, where the bare item used to claim everysys_usersub-route. EveryviewNameentry behaves this way today (nav_notifications, and the Accountmineentries). This was read from source at the pin, not measured in a browser.nav_api_keys,nav_sessions,nav_oauth_apps,nav_accountsandnav_user_preferenceseach open amineview first, and plugin-sharing'snav_record_sharesopensgranted_to_mefirst. Their page sizes are 50 to 100, so these are not one-row views. They are reported on the card, not fixed here.Generated by Claude Code