docs/superpowers/plans/2026-04-03-users-page-split.md
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Split the 3,489-line UsersPage.tsx monolith into three independent pages (Users, Members, Groups) with separate routes and sidebar entries.
Architecture: Extract shared utilities (usePagedData, PagedTableFooter, AADSyncDrawer) into a shared/ directory, then create MembersPage.tsx and GroupsPage.tsx as standalone pages. Update routes and sidebar to expose all three as independent entries. Members becomes visible in both SaaS and non-SaaS modes.
Tech Stack: React, TypeScript, Vue Router (routes/sidebar are still Vue), Pinia stores via useVueState
Spec: docs/superpowers/specs/2026-04-03-users-page-split-design.md
Files:
Create: frontend/src/react/pages/settings/shared/usePagedData.ts
Create: frontend/src/react/pages/settings/shared/AADSyncDrawer.tsx
Modify: frontend/src/react/pages/settings/UsersPage.tsx
Step 1: Create shared/usePagedData.ts
Extract usePagedData hook (lines 118-241), PagedTableFooter component (lines 247-296), and their imports from UsersPage.tsx. The file should export both usePagedData and PagedTableFooter.
Required imports to include:
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/react/components/ui/button";
import { getPageSizeOptions, useSessionPageSize } from "@/react/hooks/useSessionPageSize";
import { cn } from "@/react/lib/utils";
Also export the PagedDataResult type (the return type of usePagedData) so consumers can reference it.
shared/AADSyncDrawer.tsxExtract AADSyncDrawer (lines 2098-2284) and its imports. This drawer is used by both Users and Groups pages.
Key imports it needs:
import { useTranslation } from "react-i18next";
import { Button } from "@/react/components/ui/button";
import { Input } from "@/react/components/ui/input";
import { useVueState } from "@/react/hooks/useVueState";
import { useActuatorV1Store, useSettingV1Store } from "@/store";
Read lines 2098-2289 carefully for the full set of imports needed.
UsersPage.tsx to import from sharedReplace the inline usePagedData, PagedTableFooter, and AADSyncDrawer definitions with imports:
import { usePagedData, PagedTableFooter } from "./shared/usePagedData";
import { AADSyncDrawer } from "./shared/AADSyncDrawer";
Remove the extracted code blocks (lines 114-296 and 2094-2284) and their now-unused imports.
Run:
pnpm --dir frontend type-check
Expected: No type errors.
git add frontend/src/react/pages/settings/shared/
git add frontend/src/react/pages/settings/UsersPage.tsx
git commit -m "refactor(frontend): extract shared usePagedData and AADSyncDrawer from UsersPage"
MembersPage.tsxFiles:
Create: frontend/src/react/pages/settings/MembersPage.tsx
Modify: frontend/src/react/pages/settings/UsersPage.tsx (remove members code)
Step 1: Create MembersPage.tsx
Extract these components from UsersPage.tsx into a new standalone page:
MemberTable (lines 2304-2472)MemberTableByRole (lines 2478-2627)EditMemberRoleDrawer (lines 2633-2815)Important: The exported function name must be MembersPage (matching the filename) for mount.ts auto-discovery via import.meta.glob("./pages/settings/*.tsx").
Create a new MembersPage export function containing the members-specific state and UI from the main UsersPage function. This includes:
memberSearchText, memberViewTab, selectedMembers, showEditMemberDrawer, editingMember statememberBindings computed value (lines 2868-2879)canSetIamPolicy permission check (lines 2881-2883)handleRevokeSelected (lines 2885-2913)handleMemberUpdateBinding (lines 2915-2918)handleMemberRevokeBinding (lines 2920-2933)The page should NOT have tabs — it directly renders the member view with sub-tabs ("View by Members" / "View by Roles").
Required imports — pull from the UsersPage imports list:
import type { MemberBinding } from "@/components/Member/types";
import { getMemberBindings } from "@/components/Member/utils";
import { ComponentPermissionGuard } from "@/react/components/ComponentPermissionGuard";
import { Button } from "@/react/components/ui/button";
import { Input } from "@/react/components/ui/input";
import { Tabs, TabsList, TabsPanel, TabsTrigger } from "@/react/components/ui/tabs";
import { useVueState } from "@/react/hooks/useVueState";
import { pushNotification, useCurrentUserV1, useRoleStore, useWorkspaceV1Store } from "@/store";
import { AccountType, ALL_USERS_USER_EMAIL, getAccountTypeByEmail, getUserEmailInBinding, userBindingPrefix } from "@/types";
import { PRESET_WORKSPACE_ROLES, PresetRoleType } from "@/types/iam";
import { displayRoleDescription, displayRoleTitle, hasWorkspacePermissionV2, sortRoles } from "@/utils";
Read the extracted components carefully for any additional imports (lucide icons, etc.).
UsersPage.tsxRemove from UsersPage.tsx:
MemberTable, MemberTableByRole, EditMemberRoleDrawer function definitions
Members-related state variables from the main UsersPage function (memberSearchText, memberViewTab, selectedMembers, showEditMemberDrawer, editingMember)
memberBindings, canSetIamPolicy, handleRevokeSelected, handleMemberUpdateBinding, handleMemberRevokeBinding
The MEMBERS tab trigger (lines 3111-3114)
The MEMBERS tab action bar (lines 3229-3260)
The MEMBERS tab panel (lines 3363-3404)
The EditMemberRoleDrawer render at bottom of UsersPage
The TabValue type — change to just "USERS" | "GROUPS" (or remove tabs entirely if only USERS remains after Task 3)
Remove isSaaSMode conditional around the USERS tab trigger (it's always shown now since Members is separate)
Remove unused imports (getMemberBindings, MemberBinding, useWorkspaceV1Store, etc.)
Step 3: Verify type checking passes
Run:
pnpm --dir frontend type-check
Expected: No type errors.
git add frontend/src/react/pages/settings/MembersPage.tsx
git add frontend/src/react/pages/settings/UsersPage.tsx
git commit -m "refactor(frontend): extract MembersPage from UsersPage"
GroupsPage.tsxFiles:
Create: frontend/src/react/pages/settings/GroupsPage.tsx
Modify: frontend/src/react/pages/settings/UsersPage.tsx (remove groups code)
Step 1: Create GroupsPage.tsx
Extract these components from UsersPage.tsx:
GroupTable (lines 673-845)GroupRow (lines 847-984)RoleMultiSelect (lines 990-1186)extractUserTitle (lines 1187-1190)normalizeMemberIdentifier (lines 1600-1605)deduplicateMembers (lines 1607-1622)CreateGroupDrawer (lines 1624-2092)Important: The exported function name must be GroupsPage (matching the filename) for mount.ts auto-discovery.
Create a new GroupsPage export function containing the groups-specific state and UI:
groupSearchText stateshowCreateGroupDrawer, editingGroup stateshowAadSyncDrawer statehasUserGroupFeature, workspaceDomains, hasDirectorySyncFeature, canAccessSettings computed valuesgroupPaged data (using imported usePagedData)?name=groups/... query param (lines 3002-3024)Import shared utilities:
import { usePagedData, PagedTableFooter } from "./shared/usePagedData";
import { AADSyncDrawer } from "./shared/AADSyncDrawer";
The page should NOT have tabs — it directly renders the groups table with action bar.
UsersPage.tsxRemove from UsersPage.tsx:
GroupTable, GroupRow, RoleMultiSelect, extractUserTitle, normalizeMemberIdentifier, deduplicateMembers, CreateGroupDrawer function definitionsgroupSearchText, showCreateGroupDrawer, editingGroup)hasUserGroupFeature, workspaceDomains computed values (if only used by groups)groupPaged data?name=groups/... effect (lines 3002-3024)CreateGroupDrawer render at bottomTabValue type, getInitialTab function, and handleTabChange — no more tabswindow.location.hash = tab and hashchange listener<Tabs>, <TabsList>, <TabsTrigger>, <TabsPanel>)Tabs, TabsList, TabsPanel, TabsTrigger, etc.)Keep in UsersPage:
showAadSyncDrawer state and <AADSyncDrawer> render (imported from shared) — the Directory Sync button on the Users page still needs ithasDirectorySyncFeature, canAccessSettings — still used by the Directory Sync buttonUpdate cross-page navigation:
UserTable onGroupSelected callback (currently calls setTab("GROUPS") + opens drawer) must instead navigate to the Groups page: router.push({ name: WORKSPACE_ROUTE_GROUPS, query: { name: group.name } }). Import WORKSPACE_ROUTE_GROUPS from workspaceRoutes.After this, UsersPage.tsx should contain only:
Imports (from shared + own deps)
UserTable (lines 302-667) and its helpers (UserGroupsCell, HighlightText)
CreateUserDrawer (lines 1192-1592)
The main UsersPage function with only user-related state and UI
No tabs — just the user table, search, add user button, directory sync button, inactive users section
Step 3: Verify type checking passes
Run:
pnpm --dir frontend type-check
Expected: No type errors.
git add frontend/src/react/pages/settings/GroupsPage.tsx
git add frontend/src/react/pages/settings/UsersPage.tsx
git commit -m "refactor(frontend): extract GroupsPage from UsersPage"
Files:
Modify: frontend/src/router/dashboard/workspaceRoutes.ts:28
Modify: frontend/src/router/dashboard/workspace.ts:310-354
Step 1: Add WORKSPACE_ROUTE_GROUPS constant
In frontend/src/router/dashboard/workspaceRoutes.ts, add after line 28 (WORKSPACE_ROUTE_MEMBERS):
export const WORKSPACE_ROUTE_GROUPS = "workspace.groups";
In frontend/src/router/dashboard/workspace.ts, update the Users route (lines 310-318) to add permission and title:
{
path: "users",
name: WORKSPACE_ROUTE_USERS,
meta: {
title: () => t("common.users"),
requiredPermissionList: () => ["bb.users.list"],
},
component: () => import("@/react/ReactPageMount.vue"),
props: () => ({ page: "UsersPage" }),
},
Update the Members route (lines 342-354) to use React instead of Vue:
{
path: "members",
name: WORKSPACE_ROUTE_MEMBERS,
meta: {
title: () => t("settings.sidebar.members"),
requiredPermissionList: () => ["bb.workspaces.getIamPolicy"],
},
component: () => import("@/react/ReactPageMount.vue"),
props: () => ({ page: "MembersPage" }),
},
Add a new route after the Members route:
{
path: "groups",
name: WORKSPACE_ROUTE_GROUPS,
meta: {
title: () => t("settings.members.groups.self"),
requiredPermissionList: () => ["bb.groups.list"],
},
component: () => import("@/react/ReactPageMount.vue"),
props: () => ({ page: "GroupsPage" }),
},
Import WORKSPACE_ROUTE_GROUPS at the top of workspace.ts alongside the other route constants.
Run:
pnpm --dir frontend type-check
git add frontend/src/router/dashboard/workspaceRoutes.ts
git add frontend/src/router/dashboard/workspace.ts
git commit -m "feat(frontend): add routes for separate Members and Groups pages"
Files:
Modify: frontend/src/utils/useDashboardSidebar.ts:128-152
Step 1: Update sidebar entries
In frontend/src/utils/useDashboardSidebar.ts, replace lines 128-152 (the Users and Members entries in the IAM and Admin section):
Users entry (line 128-134) — add hide: isSaaSMode and fix title:
{
title: t("common.users"),
name: WORKSPACE_ROUTE_USERS,
type: "route",
hide: actuatorStore.isSaaSMode,
},
Members entry (lines 147-152) — remove hide:
{
title: t("settings.sidebar.members"),
name: WORKSPACE_ROUTE_MEMBERS,
type: "route",
},
Groups entry — add new entry after Members:
{
title: t("settings.members.groups.self"),
name: WORKSPACE_ROUTE_GROUPS,
type: "route",
},
Import WORKSPACE_ROUTE_GROUPS at the top of the file alongside the other route constants.
Run:
pnpm --dir frontend type-check
git add frontend/src/utils/useDashboardSidebar.ts
git commit -m "feat(frontend): update sidebar with separate Users, Members, Groups entries"
Files:
Delete: frontend/src/views/SettingWorkspaceMembers.vue
Step 1: Verify no other imports of SettingWorkspaceMembers.vue
Search for imports of SettingWorkspaceMembers across the codebase. The only reference should be the old route in workspace.ts which was already updated in Task 4.
rg "SettingWorkspaceMembers" frontend/src/
If there are other references, update them before deleting.
rm frontend/src/views/SettingWorkspaceMembers.vue
WorkspaceMembers.vue can be deletedSearch for imports of WorkspaceMembers from @/components/Member/Views/:
rg "WorkspaceMembers" frontend/src/
If SettingWorkspaceMembers.vue was its only consumer, delete frontend/src/components/Member/Views/WorkspaceMembers.vue too.
Run:
pnpm --dir frontend type-check
git add -u
git commit -m "chore(frontend): remove dead Vue members components"
Files: All modified/created files
pnpm --dir frontend fix
Fix any reported issues.
pnpm --dir frontend check
Expected: No errors.
pnpm --dir frontend type-check
Expected: No errors.
pnpm --dir frontend test
Expected: All tests pass.
git add -u
git commit -m "fix(frontend): lint and format fixes for users page split"
No automated tests exist for this page. Verify manually:
Non-SaaS mode:
/settings/users — shows user table with add/search/inactive users/settings/members — shows member list with grant/revoke, view by members/roles sub-tabs/settings/groups — shows groups table with add/search, expandable member listsSaaS mode:
/settings/members — works same as non-SaaS/settings/groups — works same as non-SaaSCross-page navigation:
/settings/groups and opens the group