Skip to content

[Dashboard] Redefine the user page with tabs and updated UI#1351

Open
madster456 wants to merge 6 commits intodevfrom
dashboard/updated_user_page
Open

[Dashboard] Redefine the user page with tabs and updated UI#1351
madster456 wants to merge 6 commits intodevfrom
dashboard/updated_user_page

Conversation

@madster456
Copy link
Copy Markdown
Collaborator

@madster456 madster456 commented Apr 19, 2026

Summary by CodeRabbit

  • New Features

    • Added tabbed navigation to user profile page featuring profile and activity sections.
  • UI/UX Improvements

    • Redesigned user action menu with streamlined options.
    • Updated data tables for contact channels, teams, and provider information with consistent styling.
    • Enhanced layout spacing and sizing for improved usability.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 19, 2026

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

Project Deployment Actions Updated (UTC)
stack-auth-hosted-components Ready Ready Preview, Comment Apr 19, 2026 6:13pm
stack-backend Ready Ready Preview, Comment Apr 19, 2026 6:13pm
stack-dashboard Ready Ready Preview, Comment Apr 19, 2026 6:13pm
stack-demo Ready Ready Preview, Comment Apr 19, 2026 6:13pm
stack-docs Ready Ready Preview, Comment Apr 19, 2026 6:13pm
stack-preview-backend Ready Ready Preview, Comment Apr 19, 2026 6:13pm
stack-preview-dashboard Ready Ready Preview, Comment Apr 19, 2026 6:13pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 19, 2026

📝 Walkthrough

Walkthrough

The pull request refactors a user detail page by replacing inline UI components and table primitives with design system components (DesignDataTable, DesignEditableGrid, DesignMenu, DesignCard). It introduces a new tabbed layout with placeholder sections for activity, while maintaining the existing profile content structure with updated styling and imports.

Changes

Cohort / File(s) Summary
User Details Page Refactoring
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
Replaced inline UserInfo wrapper and dropdown primitives with DesignEditableGrid and DesignMenu components. Refactored three table sections (ContactChannelsSection, UserTeamsSection, OAuthProvidersSection) to use DesignDataTable with memoized ColumnDef configurations. Added new tabbed layout with selectedTab state, ActivityPlaceholder/TabPlaceholder components, and conditional rendering. Updated React imports and flex layout styling.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • N2D4

Poem

🐰 Tables transformed with grace divine,
Design components now align,
Tabs appear, placeholders bloom,
Where user data finds its room!
A dashboard reborn, crisp and clean—
The finest refactoring I've seen! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description lacks substantive content, containing only a template reminder with no explanation of changes, rationale, or impact. Add a detailed description explaining the UI changes (tabs, design system components), the rationale, testing performed, and any breaking changes or migration notes.
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: redesigning the user page with a tabbed layout and updated UI components.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dashboard/updated_user_page

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 19, 2026

Greptile Summary

This PR refactors the user detail page in the dashboard to use the shared DesignCard, DesignDataTable, DesignEditableGrid, DesignMenu, and DesignCategoryTabs design-system components, and introduces a tab-based layout (Profile / Analytics / Payments / Fraud Protection) with placeholder content for the non-profile tabs. The contact channels, teams, and OAuth providers sections are migrated from raw table markup to DesignDataTable column definitions.

  • P1: handleRemoveRestriction has no catch block — any failure is a silent unhandled rejection with no user feedback.
  • P1: handleSaveAndRestrict catches errors only with captureError and never surfaces them to the user, leaving the dialog in a stuck-but-silent state on failure.

Confidence Score: 4/5

Safe to merge after fixing silent error handling in RestrictionDialog; the two P1s leave users with no feedback when restriction operations fail.

Two P1 findings exist: unhandled rejections in handleRemoveRestriction and silent error swallowing in handleSaveAndRestrict. Both affect the restriction management flow. The remaining findings (oauthColumns useMemo, runAsynchronouslyWithAlert on menu items) are P2. Score is 4 pending the P1 fixes.

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx — specifically the RestrictionDialog handlers and OAuthProvidersSection

Important Files Changed

Filename Overview
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx Large UI refactor migrating tables to DesignDataTable and adding tab layout; two P1 issues: silent error handling in RestrictionDialog and missing useMemo on oauthColumns

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[UserPage] --> B[RestrictionBanner]
    A --> C[UserHeader\nDesignMenu actions]
    A --> D[DesignCategoryTabs]
    D --> E{selectedTab}
    E -->|profile| F[UserDetails\nDesignEditableGrid]
    E -->|profile| G[ContactChannelsSection\nDesignDataTable]
    E -->|profile| H[UserTeamsSection\nDesignDataTable]
    E -->|profile| I[OAuthProvidersSection\nDesignDataTable]
    E -->|profile| J[MetadataSection]
    E -->|analytics| K[TabPlaceholder]
    E -->|payments| L[TabPlaceholder]
    E -->|fraud-protection| M[TabPlaceholder]
    C -->|impersonate / remove-2fa| N[async onClick\n⚠ no runAsynchronouslyWithAlert]
    F --> O[RestrictionDialog]
    O -->|Save & restrict| P[handleSaveAndRestrict\n⚠ silent catch]
    O -->|Remove restriction| Q[handleRemoveRestriction\n⚠ no catch block]
Loading

Comments Outside Diff (2)

  1. apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx, line 212-224 (link)

    P1 Unhandled rejection in handleRemoveRestriction

    There is no catch block, so if user.update() throws the error propagates as an unhandled rejection — the dialog freezes or closes silently with no user-facing feedback. Per the codebase convention, async button handlers should use runAsynchronouslyWithAlert instead of manual try/finally.

    Rule Used: Use runAsynchronouslyWithAlert from `@stackframe... (source)

    Learned From
    stack-auth/stack-auth#943

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
    Line: 212-224
    
    Comment:
    **Unhandled rejection in `handleRemoveRestriction`**
    
    There is no `catch` block, so if `user.update()` throws the error propagates as an unhandled rejection — the dialog freezes or closes silently with no user-facing feedback. Per the codebase convention, async button handlers should use `runAsynchronouslyWithAlert` instead of manual try/finally.
    
    
    
    **Rule Used:** Use `runAsynchronouslyWithAlert` from `@stackframe... ([source](https://app.greptile.com/review/custom-context?memory=5e671275-7493-402a-93a8-969537ec4d63))
    
    **Learned From**
    [stack-auth/stack-auth#943](https://github.com/stack-auth/stack-auth/pull/943)
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx, line 195-210 (link)

    P1 Silent error in handleSaveAndRestrict — user receives no feedback on failure

    The catch block calls captureError but never shows anything in the UI. When user.update() fails, the dialog stays open with isSaving reset to false but with no toast, alert, or message — the user has no idea what went wrong. The native alert() used for the validation check is also inconsistent with the rest of the codebase (which uses toasts).

    Consider using runAsynchronouslyWithAlert (or at minimum adding a toast in the catch block) so failures are surfaced to the user.

    Rule Used: Use runAsynchronouslyWithAlert from `@stackframe... (source)

    Learned From
    stack-auth/stack-auth#943

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
    Line: 195-210
    
    Comment:
    **Silent error in `handleSaveAndRestrict` — user receives no feedback on failure**
    
    The `catch` block calls `captureError` but never shows anything in the UI. When `user.update()` fails, the dialog stays open with `isSaving` reset to `false` but with no toast, alert, or message — the user has no idea what went wrong. The native `alert()` used for the validation check is also inconsistent with the rest of the codebase (which uses toasts).
    
    Consider using `runAsynchronouslyWithAlert` (or at minimum adding a toast in the catch block) so failures are surfaced to the user.
    
    **Rule Used:** Use `runAsynchronouslyWithAlert` from `@stackframe... ([source](https://app.greptile.com/review/custom-context?memory=5e671275-7493-402a-93a8-969537ec4d63))
    
    **Learned From**
    [stack-auth/stack-auth#943](https://github.com/stack-auth/stack-auth/pull/943)
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
Line: 212-224

Comment:
**Unhandled rejection in `handleRemoveRestriction`**

There is no `catch` block, so if `user.update()` throws the error propagates as an unhandled rejection — the dialog freezes or closes silently with no user-facing feedback. Per the codebase convention, async button handlers should use `runAsynchronouslyWithAlert` instead of manual try/finally.

```suggestion
  const handleRemoveRestriction = () => {
    runAsynchronouslyWithAlert(async () => {
      setIsSaving(true);
      try {
        await user.update({
          restrictedByAdmin: false,
          restrictedByAdminReason: null,
          restrictedByAdminPrivateDetails: null,
        } as any);
        onOpenChange(false);
      } finally {
        setIsSaving(false);
      }
    });
  };
```

**Rule Used:** Use `runAsynchronouslyWithAlert` from `@stackframe... ([source](https://app.greptile.com/review/custom-context?memory=5e671275-7493-402a-93a8-969537ec4d63))

**Learned From**
[stack-auth/stack-auth#943](https://github.com/stack-auth/stack-auth/pull/943)

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
Line: 195-210

Comment:
**Silent error in `handleSaveAndRestrict` — user receives no feedback on failure**

The `catch` block calls `captureError` but never shows anything in the UI. When `user.update()` fails, the dialog stays open with `isSaving` reset to `false` but with no toast, alert, or message — the user has no idea what went wrong. The native `alert()` used for the validation check is also inconsistent with the rest of the codebase (which uses toasts).

Consider using `runAsynchronouslyWithAlert` (or at minimum adding a toast in the catch block) so failures are surfaced to the user.

**Rule Used:** Use `runAsynchronouslyWithAlert` from `@stackframe... ([source](https://app.greptile.com/review/custom-context?memory=5e671275-7493-402a-93a8-969537ec4d63))

**Learned From**
[stack-auth/stack-auth#943](https://github.com/stack-auth/stack-auth/pull/943)

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
Line: 1171-1243

Comment:
**`oauthColumns` not wrapped in `useMemo`**

Both `teamColumns` (line 906) and `contactChannelColumns` (line 740) are wrapped in `useMemo`, but `oauthColumns` is a plain `const` inside the component body, so it is recreated on every render. Wrapping it in `useMemo` keeps the pattern consistent and avoids unnecessary downstream re-renders in `DesignDataTable`.

```suggestion
  const oauthColumns = useMemo<ColumnDef<ServerOAuthProvider>[]>(() => [
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
Line: 113-131

Comment:
**Async `DesignMenu` `onClick` handlers not wrapped in `runAsynchronouslyWithAlert`**

`DesignMenu` passes `onClick` directly to the underlying `DropdownMenuItem` without any error boundary, so the "Impersonate" and "Remove 2FA" async handlers can throw unhandled rejections with no user-visible feedback. The codebase convention is to wrap async button handlers in `runAsynchronouslyWithAlert`.

```suggestion
            onClick: () => runAsynchronouslyWithAlert(async () => {
              const expiresInMillis = 1000 * 60 * 60 * 2;
              const expiresAtDate = new Date(Date.now() + expiresInMillis);
              const session = await user.createSession({ expiresInMillis });
              const tokens = await session.getTokens();
              setImpersonateSnippet(deindent`
                document.cookie = 'stack-refresh-${stackAdminApp.projectId}=${tokens.refreshToken}; expires=${expiresAtDate.toUTCString()}; path=/'; 
                window.location.reload();
              `);
            }),
```

And similarly for the "Remove 2FA" `onClick`.

**Rule Used:** Use `runAsynchronouslyWithAlert` from `@stackframe... ([source](https://app.greptile.com/review/custom-context?memory=5e671275-7493-402a-93a8-969537ec4d63))

**Learned From**
[stack-auth/stack-auth#943](https://github.com/stack-auth/stack-auth/pull/943)

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix padding" | Re-trigger Greptile

Comment on lines +1171 to +1243
const oauthColumns: ColumnDef<ServerOAuthProvider>[] = [
{
accessorKey: "type",
header: "Provider",
cell: ({ row }) => (
<span className="capitalize font-medium">{row.original.type}</span>
),
},
{
accessorKey: "email",
header: "Email",
},
{
accessorKey: "accountId",
header: "Account ID",
cell: ({ row }) => (
<span className="font-mono text-xs truncate block max-w-[160px]">{row.original.accountId}</span>
),
},
{
id: "allowSignIn",
header: () => <span className="whitespace-nowrap">Used for sign-in</span>,
cell: ({ row }) => (
<div className="text-center">
{row.original.allowSignIn
? <CheckIcon className="mx-auto h-4 w-4 text-green-500" />
: <XIcon className="mx-auto h-4 w-4 text-muted-foreground" />}
</div>
<Button
variant="outline"
size="sm"
onClick={() => setIsAddProviderDialogOpen(true)}
>
Add Provider
</Button>
</div>
),
},
{
id: "allowConnectedAccounts",
header: () => <span className="whitespace-nowrap">Used for connected accounts</span>,
cell: ({ row }) => (
<div className="text-center">
{row.original.allowConnectedAccounts
? <CheckIcon className="mx-auto h-4 w-4 text-green-500" />
: <XIcon className="mx-auto h-4 w-4 text-muted-foreground" />}
</div>
),
},
{
id: "actions",
cell: ({ row }) => {
const provider = row.original;
return (
<div className="flex justify-end">
<ActionCell
items={[
{
item: "Edit",
onClick: () => setEditingProvider(provider),
},
{
item: provider.allowSignIn ? "Disable sign-in" : "Enable sign-in",
onClick: async () => { await handleProviderUpdate(provider, { allowSignIn: !provider.allowSignIn }); },
},
{
item: provider.allowConnectedAccounts ? "Disable connected accounts" : "Enable connected accounts",
onClick: async () => { await handleProviderUpdate(provider, { allowConnectedAccounts: !provider.allowConnectedAccounts }); },
},
{
item: "Delete",
danger: true,
onClick: async () => { await provider.delete(); },
},
]}
/>
</div>
);
},
},
];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 oauthColumns not wrapped in useMemo

Both teamColumns (line 906) and contactChannelColumns (line 740) are wrapped in useMemo, but oauthColumns is a plain const inside the component body, so it is recreated on every render. Wrapping it in useMemo keeps the pattern consistent and avoids unnecessary downstream re-renders in DesignDataTable.

Suggested change
const oauthColumns: ColumnDef<ServerOAuthProvider>[] = [
{
accessorKey: "type",
header: "Provider",
cell: ({ row }) => (
<span className="capitalize font-medium">{row.original.type}</span>
),
},
{
accessorKey: "email",
header: "Email",
},
{
accessorKey: "accountId",
header: "Account ID",
cell: ({ row }) => (
<span className="font-mono text-xs truncate block max-w-[160px]">{row.original.accountId}</span>
),
},
{
id: "allowSignIn",
header: () => <span className="whitespace-nowrap">Used for sign-in</span>,
cell: ({ row }) => (
<div className="text-center">
{row.original.allowSignIn
? <CheckIcon className="mx-auto h-4 w-4 text-green-500" />
: <XIcon className="mx-auto h-4 w-4 text-muted-foreground" />}
</div>
<Button
variant="outline"
size="sm"
onClick={() => setIsAddProviderDialogOpen(true)}
>
Add Provider
</Button>
</div>
),
},
{
id: "allowConnectedAccounts",
header: () => <span className="whitespace-nowrap">Used for connected accounts</span>,
cell: ({ row }) => (
<div className="text-center">
{row.original.allowConnectedAccounts
? <CheckIcon className="mx-auto h-4 w-4 text-green-500" />
: <XIcon className="mx-auto h-4 w-4 text-muted-foreground" />}
</div>
),
},
{
id: "actions",
cell: ({ row }) => {
const provider = row.original;
return (
<div className="flex justify-end">
<ActionCell
items={[
{
item: "Edit",
onClick: () => setEditingProvider(provider),
},
{
item: provider.allowSignIn ? "Disable sign-in" : "Enable sign-in",
onClick: async () => { await handleProviderUpdate(provider, { allowSignIn: !provider.allowSignIn }); },
},
{
item: provider.allowConnectedAccounts ? "Disable connected accounts" : "Enable connected accounts",
onClick: async () => { await handleProviderUpdate(provider, { allowConnectedAccounts: !provider.allowConnectedAccounts }); },
},
{
item: "Delete",
danger: true,
onClick: async () => { await provider.delete(); },
},
]}
/>
</div>
);
},
},
];
const oauthColumns = useMemo<ColumnDef<ServerOAuthProvider>[]>(() => [
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
Line: 1171-1243

Comment:
**`oauthColumns` not wrapped in `useMemo`**

Both `teamColumns` (line 906) and `contactChannelColumns` (line 740) are wrapped in `useMemo`, but `oauthColumns` is a plain `const` inside the component body, so it is recreated on every render. Wrapping it in `useMemo` keeps the pattern consistent and avoids unnecessary downstream re-renders in `DesignDataTable`.

```suggestion
  const oauthColumns = useMemo<ColumnDef<ServerOAuthProvider>[]>(() => [
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +113 to +131
label: "Impersonate",
onClick: async () => {
const expiresInMillis = 1000 * 60 * 60 * 2;
const expiresAtDate = new Date(Date.now() + expiresInMillis);
const session = await user.createSession({ expiresInMillis });
const tokens = await session.getTokens();
setImpersonateSnippet(deindent`
document.cookie = 'stack-refresh-${stackAdminApp.projectId}=${tokens.refreshToken}; expires=${expiresAtDate.toUTCString()}; path=/';
window.location.reload();
`);
},
},
...user.isMultiFactorRequired ? [{
id: "remove-2fa",
label: "Remove 2FA",
onClick: async () => {
await user.update({ totpMultiFactorSecret: null });
}}>
<span>Remove 2FA</span>
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setIsDeleteModalOpen(true)}>
<Typography className="text-destructive">Delete</Typography>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
},
}] satisfies DesignMenuActionItem[] : [],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Async DesignMenu onClick handlers not wrapped in runAsynchronouslyWithAlert

DesignMenu passes onClick directly to the underlying DropdownMenuItem without any error boundary, so the "Impersonate" and "Remove 2FA" async handlers can throw unhandled rejections with no user-visible feedback. The codebase convention is to wrap async button handlers in runAsynchronouslyWithAlert.

Suggested change
label: "Impersonate",
onClick: async () => {
const expiresInMillis = 1000 * 60 * 60 * 2;
const expiresAtDate = new Date(Date.now() + expiresInMillis);
const session = await user.createSession({ expiresInMillis });
const tokens = await session.getTokens();
setImpersonateSnippet(deindent`
document.cookie = 'stack-refresh-${stackAdminApp.projectId}=${tokens.refreshToken}; expires=${expiresAtDate.toUTCString()}; path=/';
window.location.reload();
`);
},
},
...user.isMultiFactorRequired ? [{
id: "remove-2fa",
label: "Remove 2FA",
onClick: async () => {
await user.update({ totpMultiFactorSecret: null });
}}>
<span>Remove 2FA</span>
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setIsDeleteModalOpen(true)}>
<Typography className="text-destructive">Delete</Typography>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
},
}] satisfies DesignMenuActionItem[] : [],
onClick: () => runAsynchronouslyWithAlert(async () => {
const expiresInMillis = 1000 * 60 * 60 * 2;
const expiresAtDate = new Date(Date.now() + expiresInMillis);
const session = await user.createSession({ expiresInMillis });
const tokens = await session.getTokens();
setImpersonateSnippet(deindent`
document.cookie = 'stack-refresh-${stackAdminApp.projectId}=${tokens.refreshToken}; expires=${expiresAtDate.toUTCString()}; path=/';
window.location.reload();
`);
}),

And similarly for the "Remove 2FA" onClick.

Rule Used: Use runAsynchronouslyWithAlert from `@stackframe... (source)

Learned From
stack-auth/stack-auth#943

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
Line: 113-131

Comment:
**Async `DesignMenu` `onClick` handlers not wrapped in `runAsynchronouslyWithAlert`**

`DesignMenu` passes `onClick` directly to the underlying `DropdownMenuItem` without any error boundary, so the "Impersonate" and "Remove 2FA" async handlers can throw unhandled rejections with no user-visible feedback. The codebase convention is to wrap async button handlers in `runAsynchronouslyWithAlert`.

```suggestion
            onClick: () => runAsynchronouslyWithAlert(async () => {
              const expiresInMillis = 1000 * 60 * 60 * 2;
              const expiresAtDate = new Date(Date.now() + expiresInMillis);
              const session = await user.createSession({ expiresInMillis });
              const tokens = await session.getTokens();
              setImpersonateSnippet(deindent`
                document.cookie = 'stack-refresh-${stackAdminApp.projectId}=${tokens.refreshToken}; expires=${expiresAtDate.toUTCString()}; path=/'; 
                window.location.reload();
              `);
            }),
```

And similarly for the "Remove 2FA" `onClick`.

**Rule Used:** Use `runAsynchronouslyWithAlert` from `@stackframe... ([source](https://app.greptile.com/review/custom-context?memory=5e671275-7493-402a-93a8-969537ec4d63))

**Learned From**
[stack-auth/stack-auth#943](https://github.com/stack-auth/stack-auth/pull/943)

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx (3)

125-131: satisfies only covers the truthy branch; move it to assert the whole items array.

satisfies DesignMenuActionItem[] is applied inside the conditional spread, so neither the static items nor the empty-array branch are type-checked against DesignMenuActionItem[]. If you want the assertion to protect the entire list, place it on the outer items array (or type the intermediate conditional as DesignMenuActionItem[] on both branches).

♻️ Proposed shape
-          items={[
+          items={([
             { id: "impersonate", /* ... */ },
-            ...user.isMultiFactorRequired ? [{
-              id: "remove-2fa",
-              /* ... */
-            }] satisfies DesignMenuActionItem[] : [],
+            ...(user.isMultiFactorRequired ? [{
+              id: "remove-2fa",
+              /* ... */
+            }] : []),
             { id: "delete", /* ... */ },
-          ]}
+          ] satisfies DesignMenuActionItem[])}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
around lines 125 - 131, The conditional currently applies "satisfies
DesignMenuActionItem[]" only to the truthy spread branch (the array with id
"remove-2fa"), so the overall items list (including the [] branch) is not
type-checked; fix this by moving the satisfies assertion to the outer items
array (or explicitly type both branches as DesignMenuActionItem[]), e.g. ensure
the final items array that includes "...user.isMultiFactorRequired ? [{ id:
'remove-2fa', label: 'Remove 2FA', onClick: async () => { await user.update({
totpMultiFactorSecret: null }); }, }] : []" is asserted with "satisfies
DesignMenuActionItem[]" so the whole list (not just the truthy branch) is
validated against DesignMenuActionItem.

1355-1402: Consider persisting the selected tab in the URL.

selectedTab lives only in component state, so refreshing or sharing a link always lands on the Profile tab. For a multi-tab user detail page this is usually undesirable (especially once Analytics/Payments/Fraud-Protection become real content). Consider syncing with a query param (e.g. ?tab=analytics) via useSearchParams + router.replace, or storing in sessionStorage as a minimum.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
around lines 1355 - 1402, Persist the UserPage selectedTab state to the URL so
tab survives refresh/share: update the UserPage component to initialize
selectedTab from a query param (e.g., ?tab=...) and keep it in sync on changes
by calling setSelectedTab when the query param changes; on tab clicks call
router.replace (or update useSearchParams) to set the tab param instead of only
setSelectedTab, and fall back to USER_PAGE_TABS[0] if the param is invalid;
alternatively, if URL sync is undesirable, persist selectedTab to sessionStorage
on change and restore it on mount; touch symbols: UserPage, selectedTab,
setSelectedTab, USER_PAGE_TABS, useSearchParams/router.replace or sessionStorage
to implement this.

1171-1243: oauthColumns should be memoized like the other column definitions.

contactChannelColumns (line 740) and teamColumns (line 906) are wrapped in useMemo, but oauthColumns is recreated on every render. This produces a new columns reference for DesignDataTable on each render, which typically defeats TanStack Table's memoized row models and can cause visible churn. Please make this consistent.

♻️ Proposed fix
-  const oauthColumns: ColumnDef<ServerOAuthProvider>[] = [
+  const oauthColumns = useMemo<ColumnDef<ServerOAuthProvider>[]>(() => [
     ...
-  ];
+  ], [handleProviderUpdate]);

Note that handleProviderUpdate itself is recreated each render; if you want full stability, wrap it in useCallback or inline it into the memo.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
around lines 1171 - 1243, The oauthColumns definition is not memoized which
causes a new columns reference each render; wrap the oauthColumns array in
useMemo (like contactChannelColumns and teamColumns) to return a stable
reference for DesignDataTable, e.g. const oauthColumns = useMemo(() => [...],
[/* dependencies */]); and include handleProviderUpdate and any props/state used
inside the column cells in the dependency array (or stabilize
handleProviderUpdate with useCallback) so the memo remains correct and avoids
unnecessary re-renders.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx:
- Around line 939-943: The onClick handler currently builds the URL with string
interpolation using stackAdminApp.projectId and row.original.id; update it to
construct the URL with urlString`...` or by wrapping dynamic parts with
encodeURIComponent() (e.g., encodeURIComponent(stackAdminApp.projectId) and
encodeURIComponent(row.original.id)) before passing to window.open in the
onClick for the "View Team" item so the URL follows the repository guideline for
safe/consistent URL construction.
- Around line 1298-1335: ActivityPlaceholder currently seeds random values
during render inside useMemo using Math.random (cells), causing server/client
hydration mismatches; change it to generate the random cells only after mount by
replacing the useMemo/Math.random usage with a client-only initialization (e.g.,
useState + useEffect or a mounted flag) so cells are populated in an effect
after the component mounts (still referencing ACTIVITY_GRID_WEEKS and
ACTIVITY_GRID_DAYS and keeping the same className grid rendering), ensuring the
initial server HTML matches and preventing React hydration warnings.
- Around line 1366-1373: The code is unsafely casting the onSelect value to
UserPageTab; update the onSelect handler for DesignCategoryTabs to
validate/narrow the incoming id against the known USER_PAGE_TABS array instead
of using "as UserPageTab". Implement a runtime check that confirms id is one of
the entries in USER_PAGE_TABS (e.g., find/includes) and only call
setSelectedTab(id) when it matches; otherwise ignore or handle the unexpected
value (fallback to a default or no-op). Ensure selectedTab and setSelectedTab
continue to use the UserPageTab type so the value stored is type-safe.

---

Nitpick comments:
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx:
- Around line 125-131: The conditional currently applies "satisfies
DesignMenuActionItem[]" only to the truthy spread branch (the array with id
"remove-2fa"), so the overall items list (including the [] branch) is not
type-checked; fix this by moving the satisfies assertion to the outer items
array (or explicitly type both branches as DesignMenuActionItem[]), e.g. ensure
the final items array that includes "...user.isMultiFactorRequired ? [{ id:
'remove-2fa', label: 'Remove 2FA', onClick: async () => { await user.update({
totpMultiFactorSecret: null }); }, }] : []" is asserted with "satisfies
DesignMenuActionItem[]" so the whole list (not just the truthy branch) is
validated against DesignMenuActionItem.
- Around line 1355-1402: Persist the UserPage selectedTab state to the URL so
tab survives refresh/share: update the UserPage component to initialize
selectedTab from a query param (e.g., ?tab=...) and keep it in sync on changes
by calling setSelectedTab when the query param changes; on tab clicks call
router.replace (or update useSearchParams) to set the tab param instead of only
setSelectedTab, and fall back to USER_PAGE_TABS[0] if the param is invalid;
alternatively, if URL sync is undesirable, persist selectedTab to sessionStorage
on change and restore it on mount; touch symbols: UserPage, selectedTab,
setSelectedTab, USER_PAGE_TABS, useSearchParams/router.replace or sessionStorage
to implement this.
- Around line 1171-1243: The oauthColumns definition is not memoized which
causes a new columns reference each render; wrap the oauthColumns array in
useMemo (like contactChannelColumns and teamColumns) to return a stable
reference for DesignDataTable, e.g. const oauthColumns = useMemo(() => [...],
[/* dependencies */]); and include handleProviderUpdate and any props/state used
inside the column cells in the dependency array (or stabilize
handleProviderUpdate with useCallback) so the memo remains correct and avoids
unnecessary re-renders.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b73d132f-3aca-436a-9f0d-e0535555ee6f

📥 Commits

Reviewing files that changed from the base of the PR and between be6970c and 00d8bc4.

📒 Files selected for processing (1)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx

Comment on lines +939 to +943
item: "View Team",
onClick: () => {
window.open(`/projects/${stackAdminApp.projectId}/teams/${row.original.id}`, '_blank', 'noopener');
},
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use urlString/encodeURIComponent for URL construction.

stackAdminApp.projectId and row.original.id are interpolated directly into the URL passed to window.open. As per coding guidelines: "Use urlString`` or encodeURIComponent() instead of normal string interpolation for URLs, for consistency even if it's not strictly necessary".

🔧 Proposed fix
-          window.open(`/projects/${stackAdminApp.projectId}/teams/${row.original.id}`, '_blank', 'noopener');
+          window.open(
+            `/projects/${encodeURIComponent(stackAdminApp.projectId)}/teams/${encodeURIComponent(row.original.id)}`,
+            '_blank',
+            'noopener',
+          );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
item: "View Team",
onClick: () => {
window.open(`/projects/${stackAdminApp.projectId}/teams/${row.original.id}`, '_blank', 'noopener');
},
},
item: "View Team",
onClick: () => {
window.open(
`/projects/${encodeURIComponent(stackAdminApp.projectId)}/teams/${encodeURIComponent(row.original.id)}`,
'_blank',
'noopener',
);
},
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
around lines 939 - 943, The onClick handler currently builds the URL with string
interpolation using stackAdminApp.projectId and row.original.id; update it to
construct the URL with urlString`...` or by wrapping dynamic parts with
encodeURIComponent() (e.g., encodeURIComponent(stackAdminApp.projectId) and
encodeURIComponent(row.original.id)) before passing to window.open in the
onClick for the "View Team" item so the URL follows the repository guideline for
safe/consistent URL construction.

Comment on lines +1298 to +1335
function ActivityPlaceholder() {
const cells = useMemo(() => {
const result: number[] = [];
for (let i = 0; i < ACTIVITY_GRID_WEEKS * ACTIVITY_GRID_DAYS; i++) {
result.push(Math.random());
}
return result;
}, []);

return (
<div className="hidden xl:flex flex-col items-end gap-1.5 opacity-30 select-none shrink-0 pt-1" aria-hidden>
<span className="text-[11px] font-medium text-muted-foreground tracking-wide uppercase">Activity</span>
<div
className="grid gap-[3px]"
style={{
gridTemplateColumns: `repeat(${ACTIVITY_GRID_WEEKS}, 1fr)`,
gridTemplateRows: `repeat(${ACTIVITY_GRID_DAYS}, 1fr)`,
}}
>
{cells.map((rand, i) => (
<div
key={i}
className={cn(
"w-[9px] h-[9px] rounded-[2px]",
rand < 0.55
? "bg-foreground/[0.06]"
: rand < 0.75
? "bg-foreground/[0.12]"
: rand < 0.9
? "bg-foreground/[0.22]"
: "bg-foreground/[0.35]",
)}
/>
))}
</div>
</div>
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Hydration mismatch: Math.random() during render in a server-rendered client component.

Even though this file is "use client", Next.js still renders it on the server for the initial HTML. Math.random() inside the render-phase useMemo will produce different values on the server and client, causing React hydration warnings and a visible flicker when the grid repaints on the client. aria-hidden does not suppress hydration checks.

Either generate the cells in an effect (so they only exist after mount), or gate the placeholder behind a mounted flag / dynamic import with ssr: false.

🔧 Proposed fix using a mount effect
-function ActivityPlaceholder() {
-  const cells = useMemo(() => {
-    const result: number[] = [];
-    for (let i = 0; i < ACTIVITY_GRID_WEEKS * ACTIVITY_GRID_DAYS; i++) {
-      result.push(Math.random());
-    }
-    return result;
-  }, []);
+function ActivityPlaceholder() {
+  const [cells, setCells] = useState<number[] | null>(null);
+  useEffect(() => {
+    const result: number[] = [];
+    for (let i = 0; i < ACTIVITY_GRID_WEEKS * ACTIVITY_GRID_DAYS; i++) {
+      result.push(Math.random());
+    }
+    setCells(result);
+  }, []);
+  if (cells === null) return null;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function ActivityPlaceholder() {
const cells = useMemo(() => {
const result: number[] = [];
for (let i = 0; i < ACTIVITY_GRID_WEEKS * ACTIVITY_GRID_DAYS; i++) {
result.push(Math.random());
}
return result;
}, []);
return (
<div className="hidden xl:flex flex-col items-end gap-1.5 opacity-30 select-none shrink-0 pt-1" aria-hidden>
<span className="text-[11px] font-medium text-muted-foreground tracking-wide uppercase">Activity</span>
<div
className="grid gap-[3px]"
style={{
gridTemplateColumns: `repeat(${ACTIVITY_GRID_WEEKS}, 1fr)`,
gridTemplateRows: `repeat(${ACTIVITY_GRID_DAYS}, 1fr)`,
}}
>
{cells.map((rand, i) => (
<div
key={i}
className={cn(
"w-[9px] h-[9px] rounded-[2px]",
rand < 0.55
? "bg-foreground/[0.06]"
: rand < 0.75
? "bg-foreground/[0.12]"
: rand < 0.9
? "bg-foreground/[0.22]"
: "bg-foreground/[0.35]",
)}
/>
))}
</div>
</div>
);
}
function ActivityPlaceholder() {
const [cells, setCells] = useState<number[] | null>(null);
useEffect(() => {
const result: number[] = [];
for (let i = 0; i < ACTIVITY_GRID_WEEKS * ACTIVITY_GRID_DAYS; i++) {
result.push(Math.random());
}
setCells(result);
}, []);
if (cells === null) return null;
return (
<div className="hidden xl:flex flex-col items-end gap-1.5 opacity-30 select-none shrink-0 pt-1" aria-hidden>
<span className="text-[11px] font-medium text-muted-foreground tracking-wide uppercase">Activity</span>
<div
className="grid gap-[3px]"
style={{
gridTemplateColumns: `repeat(${ACTIVITY_GRID_WEEKS}, 1fr)`,
gridTemplateRows: `repeat(${ACTIVITY_GRID_DAYS}, 1fr)`,
}}
>
{cells.map((rand, i) => (
<div
key={i}
className={cn(
"w-[9px] h-[9px] rounded-[2px]",
rand < 0.55
? "bg-foreground/[0.06]"
: rand < 0.75
? "bg-foreground/[0.12]"
: rand < 0.9
? "bg-foreground/[0.22]"
: "bg-foreground/[0.35]",
)}
/>
))}
</div>
</div>
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
around lines 1298 - 1335, ActivityPlaceholder currently seeds random values
during render inside useMemo using Math.random (cells), causing server/client
hydration mismatches; change it to generate the random cells only after mount by
replacing the useMemo/Math.random usage with a client-only initialization (e.g.,
useState + useEffect or a mounted flag) so cells are populated in an effect
after the component mounts (still referencing ACTIVITY_GRID_WEEKS and
ACTIVITY_GRID_DAYS and keeping the same className grid rendering), ensuring the
initial server HTML matches and preventing React hydration warnings.

Comment on lines +1366 to 1373
<DesignCategoryTabs
categories={[...USER_PAGE_TABS]}
selectedCategory={selectedTab}
onSelect={(id) => setSelectedTab(id as UserPageTab)}
showBadge={false}
size="sm"
glassmorphic={false}
/>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect DesignCategoryTabs' onSelect signature to see whether a cast is truly needed.
fd -t f 'tabs.tsx' packages/dashboard-ui-components/src/components
rg -nP -C3 '\bonSelect\b|DesignCategoryTabsProps' packages/dashboard-ui-components/src/components/tabs.tsx 2>/dev/null || true

Repository: stack-auth/stack-auth

Length of output: 1328


🏁 Script executed:

#!/bin/bash
# Find and inspect USER_PAGE_TABS and UserPageTab definitions in the page-client.tsx file
fd -t f 'page-client.tsx' apps/dashboard/src/app/\*/\*/\*/\*/users/ | head -1 | xargs head -100 | cat -n

Repository: stack-auth/stack-auth

Length of output: 189


🏁 Script executed:

#!/bin/bash
# Search for USER_PAGE_TABS definition
rg -n 'USER_PAGE_TABS|UserPageTab' apps/dashboard/src/app/\*/\*/\*/\*/users/\*/page-client.tsx -A 2 -B 2 | head -50

Repository: stack-auth/stack-auth

Length of output: 159


🏁 Script executed:

#!/bin/bash
# Find page-client.tsx in the dashboard
fd -t f 'page-client.tsx' apps/dashboard/src/ | grep -i users

Repository: stack-auth/stack-auth

Length of output: 243


🏁 Script executed:

#!/bin/bash
# Search for USER_PAGE_TABS and UserPageTab definitions
rg -n 'USER_PAGE_TABS|type UserPageTab|enum UserPageTab' apps/dashboard/src/ -A 3 -B 1

Repository: stack-auth/stack-auth

Length of output: 2032


🏁 Script executed:

#!/bin/bash
# Search for setSelectedTab or selectedTab state definition in the file
rg -n 'setSelectedTab|useState.*selectedTab' 'apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx' -B 2 -A 2 | head -30

Repository: stack-auth/stack-auth

Length of output: 451


Avoid the as UserPageTab cast; narrow against USER_PAGE_TABS instead.

The DesignCategoryTabs component passes arbitrary strings to onSelect, but setSelectedTab expects UserPageTab type. Using as UserPageTab bypasses type safety. Instead, defensively narrow the value:

🔧 Proposed fix
-          onSelect={(id) => setSelectedTab(id as UserPageTab)}
+          onSelect={(id) => {
+            const match = USER_PAGE_TABS.find((t) => t.id === id)
+              ?? throwErr(`Unknown user page tab: ${id}`);
+            setSelectedTab(match.id);
+          }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<DesignCategoryTabs
categories={[...USER_PAGE_TABS]}
selectedCategory={selectedTab}
onSelect={(id) => setSelectedTab(id as UserPageTab)}
showBadge={false}
size="sm"
glassmorphic={false}
/>
<DesignCategoryTabs
categories={[...USER_PAGE_TABS]}
selectedCategory={selectedTab}
onSelect={(id) => {
const match = USER_PAGE_TABS.find((t) => t.id === id)
?? throwErr(`Unknown user page tab: ${id}`);
setSelectedTab(match.id);
}}
showBadge={false}
size="sm"
glassmorphic={false}
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
around lines 1366 - 1373, The code is unsafely casting the onSelect value to
UserPageTab; update the onSelect handler for DesignCategoryTabs to
validate/narrow the incoming id against the known USER_PAGE_TABS array instead
of using "as UserPageTab". Implement a runtime check that confirms id is one of
the entries in USER_PAGE_TABS (e.g., find/includes) and only call
setSelectedTab(id) when it matches; otherwise ignore or handle the unexpected
value (fallback to a default or no-op). Ensure selectedTab and setSelectedTab
continue to use the UserPageTab type so the value stored is type-safe.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant