From 6639eb4a52d403b6c286c7590862466245d16186 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 07:38:59 +0000 Subject: [PATCH] =?UTF-8?q?fix(platform-objects):=20Setup=20=E2=86=92=20Us?= =?UTF-8?q?ers=20opens=20on=20the=20All=20Users=20list=20view?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .changeset/21960-setup-users-all-users.md | 11 ++ .../src/apps/setup-nav.contributions.ts | 8 +- .../src/apps/setup-users-nav-view.test.ts | 128 ++++++++++++++++++ .../src/identity/sys-user.object.ts | 13 +- 4 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 .changeset/21960-setup-users-all-users.md create mode 100644 packages/platform-objects/src/apps/setup-users-nav-view.test.ts diff --git a/.changeset/21960-setup-users-all-users.md b/.changeset/21960-setup-users-all-users.md new file mode 100644 index 00000000000..bd0e6664246 --- /dev/null +++ b/.changeset/21960-setup-users-all-users.md @@ -0,0 +1,11 @@ +--- +"@objectstack/platform-objects": patch +--- + +Setup → Users now opens on the "All Users" list. Before this, the console opened `sys_user`'s first declared list view, "My Profile". That view is filtered to the caller with a page size of 1, so an administrator saw one row, themselves, and nothing said the rest of the organization was one tab away. + +Clause-②: no + +- The Setup app's `nav_users` entry now sets `viewName: 'all_users'`. The key is the one the spec already declares on an object navigation item, and the console honours it. No new key, no `listViews` reorder, and no view is removed. +- "My Profile" (`me`) is still a tab on the Users page. The Account app's profile entry is unchanged: it is the `account:profile_card` component, which reads the signed-in user from the session, not this list view. The `me` view's code comment no longer says the Account app surfaces it. +- ⛔ No schema, parse, export or accept-set change. diff --git a/packages/platform-objects/src/apps/setup-nav.contributions.ts b/packages/platform-objects/src/apps/setup-nav.contributions.ts index 1428168fd4d..72a3adcc106 100644 --- a/packages/platform-objects/src/apps/setup-nav.contributions.ts +++ b/packages/platform-objects/src/apps/setup-nav.contributions.ts @@ -68,7 +68,13 @@ export const SETUP_NAV_CONTRIBUTIONS: NavigationContribution[] = [ group: 'group_people_org', priority: BASE_PRIORITY, items: [ - { id: 'nav_users', type: 'object', label: 'Users', objectName: 'sys_user', icon: 'user' }, + // `viewName` names the tenant-wide list (#21960). With no view named, + // the console opens the object's FIRST declared list view, and + // `sys_user` declares `me` first — a one-row view of the caller — so an + // administrator landed on themselves and read "this organization has + // one user". `me` stays a tab in the view switcher; the Account app's + // profile entry is the `account:profile_card` component, not that view. + { id: 'nav_users', type: 'object', label: 'Users', objectName: 'sys_user', viewName: 'all_users', icon: 'user' }, // The ACTIVE organization's record page (Members / Invitations / Teams // tabs with the better-auth row actions), rendered inside the app shell // (cloud ADR-0081 D3). `{current_org_id}` resolves from the session's active diff --git a/packages/platform-objects/src/apps/setup-users-nav-view.test.ts b/packages/platform-objects/src/apps/setup-users-nav-view.test.ts new file mode 100644 index 00000000000..0ce065ebf1e --- /dev/null +++ b/packages/platform-objects/src/apps/setup-users-nav-view.test.ts @@ -0,0 +1,128 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// Setup → Users opens on the tenant-wide user list, and the Account app's +// profile entry is not that list (#21960). +// +// The defect: `nav_users` named no view, and the console opens the FIRST +// declared list view of an object when the route names none (objectui +// `ObjectView`: URL view id, then `?view=`, then a view marked `isDefault`, +// then `views[0]`). `sys_user` declares `me` first — `id = {current_user_id}`, +// page size 1 — so an administrator's Users page showed one row, themselves. +// +// The repair uses the key the spec already declares on an object navigation +// item, `viewName` ("Default list view to open"). The console honours it: its +// `NavigationRenderer.resolveHref` emits `/view/`, and +// `resolveViewId` matches the short name against the qualified view ids. Read +// at the objectui commit `.objectui-sha` pins +// (0abd4f9f8769fc4c19ad2f96707684876f74c09f). The same function gives +// `recordId` and then `filters` precedence over `viewName`, which is why the +// entry must carry neither. +// +// The profile half: the Account app's profile entry is the +// `account:profile_card` component, which reads the signed-in user from the +// session (objectui `ProfilePage`, `useAuth()`), not a `sys_user` list view. +// No Account entry routes to `sys_user`, so the view order on `sys_user` is not +// what puts a user on their own profile. +import { describe, it, expect } from 'vitest'; +import { NavigationContributionSchema } from '@objectstack/spec/ui'; + +import { SETUP_NAV_CONTRIBUTIONS } from './setup-nav.contributions.js'; +import { ACCOUNT_APP } from './account.app.js'; +import { SysUser } from '../identity/sys-user.object.js'; + +type NavItem = { + id?: string; + type?: string; + objectName?: string; + viewName?: string; + recordId?: string; + filters?: Record; + componentRef?: string; + children?: NavItem[]; +}; + +type ListView = { + name?: string; + filter?: unknown; + pagination?: { pageSize?: number }; +}; + +const usersContribution = () => { + const found = SETUP_NAV_CONTRIBUTIONS.find((c) => + (c.items ?? []).some((i) => (i as NavItem).id === 'nav_users'), + ); + expect(found, 'Setup lost its nav_users entry').toBeDefined(); + return found!; +}; + +const usersEntry = (): NavItem => + (usersContribution().items ?? []).find((i) => (i as NavItem).id === 'nav_users') as NavItem; + +const userListViews = (): Record => + ((SysUser as { listViews?: Record }).listViews ?? {}); + +/** Every Account nav item, depth-first. */ +const accountItems = (): NavItem[] => { + const out: NavItem[] = []; + const walk = (items: NavItem[] = []) => { + for (const item of items) { + if (!item) continue; + out.push(item); + if (Array.isArray(item.children)) walk(item.children); + } + }; + walk((ACCOUNT_APP.navigation ?? []) as NavItem[]); + return out; +}; + +describe('Setup → Users opens on the tenant-wide user list (#21960)', () => { + it('names the `all_users` list view of `sys_user`', () => { + expect(usersContribution().group).toBe('group_people_org'); + expect(usersEntry()).toMatchObject({ + type: 'object', + objectName: 'sys_user', + viewName: 'all_users', + }); + }); + + it('carries neither `recordId` nor `filters`, which the console honours ahead of `viewName`', () => { + expect(usersEntry().recordId).toBeUndefined(); + expect(usersEntry().filters).toBeUndefined(); + }); + + it('names a view `sys_user` declares, under that same name', () => { + const view = userListViews()[usersEntry().viewName ?? '']; + expect(view, `sys_user declares no list view "${usersEntry().viewName}"`).toBeDefined(); + expect(view.name).toBe(usersEntry().viewName); + }); + + it('names a view that is not scoped to the caller', () => { + const view = userListViews()[usersEntry().viewName ?? '']; + expect(view.filter).toBeUndefined(); + expect(JSON.stringify(view)).not.toContain('{current_user_id}'); + expect(view.pagination?.pageSize ?? 0).toBeGreaterThan(1); + }); + + it('parses as a NavigationContribution, so the key reaches the served app', () => { + const parsed = NavigationContributionSchema.safeParse(usersContribution()); + expect(parsed.success, JSON.stringify(parsed.error?.issues)).toBe(true); + const item = (parsed.data?.items ?? []).find((i) => (i as NavItem).id === 'nav_users') as NavItem; + expect(item.viewName).toBe('all_users'); + }); +}); + +describe('the Account app profile entry is not a `sys_user` list view (#21960)', () => { + it('opens on the `account:profile_card` component, as its first entry', () => { + const first = ((ACCOUNT_APP.navigation ?? []) as NavItem[])[0]; + expect(first).toMatchObject({ + id: 'nav_account_profile', + type: 'component', + componentRef: 'account:profile_card', + }); + }); + + it('routes no entry to `sys_user`', () => { + const toUser = accountItems().filter((i) => i.objectName === 'sys_user').map((i) => i.id); + expect(toUser).toEqual([]); + }); +}); diff --git a/packages/platform-objects/src/identity/sys-user.object.ts b/packages/platform-objects/src/identity/sys-user.object.ts index c5bcf3e7f31..6a3362a2f05 100644 --- a/packages/platform-objects/src/identity/sys-user.object.ts +++ b/packages/platform-objects/src/identity/sys-user.object.ts @@ -601,11 +601,14 @@ export const SysUser = ObjectSchema.create({ ], listViews: { - // Self-service profile entry — surfaced by the Account App so every - // authenticated user can view / edit their own basic profile (name, - // email, avatar). Filtered to a single row (the caller) via the - // `{current_user_id}` template variable; RLS additionally enforces - // that non-admins cannot read other users' rows. + // The caller's own row, as a tab of this object's view switcher. + // Filtered to a single row via the `{current_user_id}` template + // variable; RLS additionally enforces that non-admins cannot read other + // users' rows. No navigation entry names this view: the Account App's + // profile entry is the `account:profile_card` component (`account.app.ts`), + // which reads the signed-in user from the session, not from this list. + // Declared first, it is the tab the console opens when a route names no + // view — so Setup's Users entry names `all_users` explicitly (#21960). me: { type: 'grid', name: 'me',