[Dashboard] Redefine the user page with tabs and updated UI#1351
[Dashboard] Redefine the user page with tabs and updated UI#1351madster456 wants to merge 6 commits intodevfrom
Conversation
Made-with: Cursor # Conflicts: # apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThe pull request refactors a user detail page by replacing inline UI components and table primitives with design system components ( Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Greptile SummaryThis PR refactors the user detail page in the dashboard to use the shared
Confidence Score: 4/5Safe 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
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]
|
| 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> | ||
| ); | ||
| }, | ||
| }, | ||
| ]; |
There was a problem hiding this 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.
| 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.| 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[] : [], |
There was a problem hiding this 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.
| 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.There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx (3)
125-131:satisfiesonly 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 againstDesignMenuActionItem[]. If you want the assertion to protect the entire list, place it on the outeritemsarray (or type the intermediate conditional asDesignMenuActionItem[]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.
selectedTablives 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) viauseSearchParams+router.replace, or storing insessionStorageas 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:oauthColumnsshould be memoized like the other column definitions.
contactChannelColumns(line 740) andteamColumns(line 906) are wrapped inuseMemo, butoauthColumnsis recreated on every render. This produces a newcolumnsreference forDesignDataTableon 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
handleProviderUpdateitself is recreated each render; if you want full stability, wrap it inuseCallbackor 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
📒 Files selected for processing (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
| item: "View Team", | ||
| onClick: () => { | ||
| window.open(`/projects/${stackAdminApp.projectId}/teams/${row.original.id}`, '_blank', 'noopener'); | ||
| }, | ||
| }, |
There was a problem hiding this comment.
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.
| 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.
| 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> | ||
| ); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| <DesignCategoryTabs | ||
| categories={[...USER_PAGE_TABS]} | ||
| selectedCategory={selectedTab} | ||
| onSelect={(id) => setSelectedTab(id as UserPageTab)} | ||
| showBadge={false} | ||
| size="sm" | ||
| glassmorphic={false} | ||
| /> |
There was a problem hiding this comment.
🧩 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 || trueRepository: 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 -nRepository: 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 -50Repository: 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 usersRepository: 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 1Repository: 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 -30Repository: 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.
| <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.
Summary by CodeRabbit
New Features
UI/UX Improvements