Update dashboard components (Except for major 4)#1205
Update dashboard components (Except for major 4)#1205Developing-Gamer wants to merge 100 commits intodevfrom
Conversation
…hosphor icons successful
…email logs. Updated sender email formatting to exclude non-alphanumeric characters. Added tests for email template deletion scenarios, including handling of shared email configurations.
…and user feedback. Added state management for delete errors and updated the confirmation dialog to display error messages when deletion fails.
…g. Enhance mobile layout in VibeCodeLayout with responsive design and chat functionality.
… effects and transitions. Added accent hover tint for better visual feedback.
…Removed redundant try-catch block and streamlined message validation process for improved readability and maintainability.
…lobe component to fix flickering bug, improved culling.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Too many files changed for review. ( |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a large client-side Playground page and migrates many dashboard pages from legacy UI primitives to new Design-prefixed components; introduces DesignDataTable, updates SmartForm/SmartFormDialog defaults, and adds PageLayout.allowContentOverflow prop. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 3❌ Failed checks (2 warnings, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). 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 |
There was a problem hiding this comment.
Pull request overview
This PR updates dashboard UI components with a new design system, excluding four major components. It introduces a comprehensive set of design components with glassmorphic styling, improved visual consistency, and enhanced user experience across the dashboard.
Changes:
- Introduces new design component library (
design-components) with glassmorphic styling and consistent visual patterns - Updates email editing interface with WYSIWYG capabilities and improved code editor theming
- Enhances theme toggle with animated view transitions and cursor blast effects
- Refactors numerous page layouts and data tables to use new design system components
Reviewed changes
Copilot reviewed 106 out of 123 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| apps/e2e/tests/backend/endpoints/api/v1/internal/email-templates-create.test.ts | Adds tests for email template creation validation with shared/custom email configs |
| apps/e2e/tests/backend/endpoints/api/v1/internal/email-drafts.test.ts | Adds tests for email draft deletion functionality |
| apps/e2e/tests/backend/endpoints/api/v1/emails/email-queue.test.ts | Updates email HTML snapshots with new theme styling including margin and shadow properties |
| apps/e2e/tests/backend/endpoints/api/v1/email-themes.test.ts | Updates default email theme to use Section component and improved responsive layout |
| apps/dashboard/src/lib/prefetch/url-prefetcher.tsx | Fixes incorrect array transformation that was returning nested arrays |
| apps/dashboard/src/components/vibe-coding/index.ts | Exports additional types for viewport mode and WYSIWYG debug info |
| apps/dashboard/src/components/vibe-coding/code-editor.tsx | Enhances code editor with custom themes, improved font settings, and better scrollbar styling |
| apps/dashboard/src/components/vibe-coding/chat-adapters.ts | Improves message formatting by separating tool calls from content |
| apps/dashboard/src/components/vibe-coding/assistant-chat.tsx | Restructures chat layout with border and off-white light mode support |
| apps/dashboard/src/components/ui/simple-tooltip.tsx | Adds pointer events to tooltip content and adjusts delay duration |
| apps/dashboard/src/components/ui/select.tsx | Updates select components with new styling including rounded borders and glassmorphic effects |
| apps/dashboard/src/components/ui/input.tsx | Enhances input component with size variants, leading icon support, and glassmorphic styling |
| apps/dashboard/src/components/ui/dropdown-menu.tsx | Refactors dropdown menu with improved keyboard navigation and item activation logic |
| apps/dashboard/src/components/ui/data-table/* | Updates data table components with new styling and improved filter UI |
| apps/dashboard/src/components/ui/button.tsx | Updates button outline variant with glassmorphic background |
| apps/dashboard/src/components/ui/alert.tsx | Updates alert styling with increased border radius |
| apps/dashboard/src/components/theme-toggle.tsx | Implements animated theme transitions using View Transition API |
| apps/dashboard/src/components/theme-provider.tsx | Reformats theme provider component |
| apps/dashboard/src/components/stack-companion.tsx | Updates companion UI with improved color scheme for light/dark modes |
| apps/dashboard/src/components/search-bar.tsx | Changes search bar to use Input component props |
| apps/dashboard/src/components/router.tsx | Replaces window.confirm with custom navigation dialog for unsaved changes |
| apps/dashboard/src/components/resizable-container.tsx | Improves resize handles with rounded styling and better visual feedback |
| apps/dashboard/src/components/navbar.tsx | Updates navbar with improved border styling and integrated theme toggle |
| apps/dashboard/src/components/inline-save-discard.tsx | Wraps save handler with error alert utility |
| apps/dashboard/src/components/email-theme-selector.tsx | Adds responsive width styling to theme selector |
| apps/dashboard/src/components/editable-input.tsx | Refactors to use design components with improved button styling |
| apps/dashboard/src/components/design-components/* | Introduces comprehensive design component library with tabs, tables, inputs, cards, badges, alerts, lists, menus, and more |
| apps/dashboard/src/components/data-table/* | Updates data tables to use new design system components |
| apps/dashboard/src/app/globals.css | Adds ambient background gradients, view transition styles, and updated color tokens |
| apps/dashboard/src/app/(main)/handler/[...stack]/page.tsx | Wraps Stack handler in container with data attribute for styling |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/* | Migrates multiple project pages to use new design components |
| apps/backend/src/lib/email-rendering.tsx | Adds WYSIWYG editing support with JSX transpilation and editable region markers |
| apps/backend/src/lib/email-rendering.test.tsx | Adds tests for editable markers functionality |
| apps/backend/src/lib/ai-chat/* | Improves AI adapter system prompts with detailed design principles |
| apps/backend/src/app/api/latest/internal/wysiwyg-edit/route.tsx | Implements WYSIWYG text editing endpoint using AI with mock mode fallback |
| apps/backend/src/app/api/latest/internal/email-themes/cud.tsx | Adds guard to prevent deletion of active email theme |
| apps/backend/src/app/api/latest/internal/email-templates/route.tsx | Adds validation for shared email server and uses default template source |
| apps/backend/src/app/api/latest/internal/email-templates/[templateId]/route.tsx | Adds DELETE endpoint and improves PATCH handling |
| apps/backend/src/app/api/latest/internal/email-drafts/[id]/route.tsx | Adds DELETE endpoint and improves error handling |
| apps/backend/src/app/api/latest/internal/ai-chat/[threadId]/route.tsx | Adds mock mode, timeout handling, and switches to OpenRouter |
| apps/backend/src/app/api/latest/emails/render-email/route.tsx | Adds support for editable markers in email rendering |
| apps/backend/.env.development | Adds mock OpenRouter API key configuration |
| AGENTS.md | Adds guideline to update Playground page when modifying design components |
| .vscode/settings.json | Adds "glassmorphic" to spell checker dictionary |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 17
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (16)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx (1)
197-217: 🛠️ Refactor suggestion | 🟠 Major
hexSelectedCountrystate is dead code.After this refactor,
hexSelectedCountryis written (lines 197, 213, 215, 422) but never read. The tooltip is driven bypolygonSelectedCountry(line 200), and the hex color/altitude accessors readselectedCountryRef.current, which points topolygonSelectedCountry(line 208). The entireuseEffect(lines 211–217) and thehexSelectedCountrystate can be removed.♻️ Suggested cleanup
- const [hexSelectedCountry, setHexSelectedCountry] = useState<{ code: string, name: string } | null>(null); const [polygonSelectedCountry, setPolygonSelectedCountry] = useState<{ code: string, name: string } | null>(null); // Use polygon for tooltip (no gaps), hex just for visual highlighting const selectedCountry = polygonSelectedCountry;- // Sync hex highlighting with polygon hover (for visual consistency) - useEffect(() => { - if (polygonSelectedCountry) { - setHexSelectedCountry(polygonSelectedCountry); - } else { - setHexSelectedCountry(null); - } - }, [polygonSelectedCountry]);And in
onMouseLeave:onMouseLeave={() => { // Only clear when leaving the entire globe area - setHexSelectedCountry(null); setPolygonSelectedCountry(null);#!/bin/bash # Verify hexSelectedCountry is never read (only written via set*) echo "=== All references to hexSelectedCountry ===" rg -n 'hexSelectedCountry' apps/dashboard/src/app/\(main\)/\(protected\)/projects/\[projectId\]/\(overview\)/globe.tsx echo "" echo "=== Read usages (excluding setState calls and declarations) ===" rg -n 'hexSelectedCountry' apps/dashboard/src/app/\(main\)/\(protected\)/projects/\[projectId\]/\(overview\)/globe.tsx | grep -v 'setHexSelectedCountry' | grep -v 'useState'🤖 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]/(overview)/globe.tsx around lines 197 - 217, The hexSelectedCountry state is dead code: remove the declaration const [hexSelectedCountry, setHexSelectedCountry] = useState... and the useEffect that syncs it to polygonSelectedCountry, and delete any setHexSelectedCountry(...) calls (e.g., in onMouseLeave or other handlers); ensure color/altitude accessors and tooltip logic continue to use selectedCountryRef / polygonSelectedCountry so behavior remains unchanged (check functions that currently read selectedCountryRef.current to confirm no further changes are needed).apps/dashboard/src/components/ui/dropdown-menu.tsx (1)
18-42:⚠️ Potential issue | 🟡 MinorContext state ignores
defaultOpen, causing desync.Line 22 initializes internal state with
!!props.open, but if a consumer passesdefaultOpen={true}(forwarded to Radix via...props), Radix will open the menu whileDropdownMenuContextreportsopen: false. This would break any context consumer (e.g.,DropdownMenuItem'ssetOpen).Proposed fix
- const [open, setOpen] = React.useState(!!props.open); + const [open, setOpen] = React.useState(props.open ?? props.defaultOpen ?? false);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/ui/dropdown-menu.tsx` around lines 18 - 42, The internal open state is initialized from props.open only, ignoring props.defaultOpen and causing desync when Radix is uncontrolled; update the DropdownMenu component to initialize state with the uncontrolled initial value by using React.useState(() => props.open ?? props.defaultOpen ?? false) (or Boolean(props.open ?? props.defaultOpen)) so DropdownMenuContext (open and setOpen) reflects Radix's defaultOpen on first render; keep the existing onOpenChange handlers and the prop forwarding to DropdownMenuPrimitive.Root.apps/backend/src/lib/ai-chat/email-template-adapter.ts (1)
41-85:⚠️ Potential issue | 🔴 Critical
currentEmailTemplatecan beundefined— unguarded property access on line 83.
context.tenancy.config.emails.templates[context.threadId]will beundefinedifthreadIddoesn't match any template key. Accessing.tsxSourceonundefinedwill throw aTypeErrorat runtime.Proposed fix
const CREATE_EMAIL_TEMPLATE_TOOL_DESCRIPTION = (context: ChatAdapterContext) => { const currentEmailTemplate = context.tenancy.config.emails.templates[context.threadId]; + if (!currentEmailTemplate) { + throw new Error(`No email template found for thread ID: ${context.threadId}`); + } return `As per coding guidelines, "Code defensively. Prefer
?? throwErr(...)over non-null assertions, with good error messages explicitly stating the assumption."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backend/src/lib/ai-chat/email-template-adapter.ts` around lines 41 - 85, The template builder accesses context.tenancy.config.emails.templates[context.threadId] without guarding for undefined, so CREATE_EMAIL_TEMPLATE_TOOL_DESCRIPTION may dereference currentEmailTemplate.tsxSource and throw; update the code to retrieve the template into currentEmailTemplate and replace the unguarded access with a defensive check using the project's throwErr pattern (e.g. const currentEmailTemplate = context.tenancy.config.emails.templates[context.threadId] ?? throwErr("...")) or an explicit if/throw, and use that variable when interpolating ${currentEmailTemplate.tsxSource} so a clear, descriptive error is thrown when threadId has no matching template.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/apps/page-client.tsx (1)
29-29: 🛠️ Refactor suggestion | 🟠 MajorPrefer
?? throwErr(...)over non-null assertion.
useAdminApp()!uses a non-null assertion. Per coding guidelines, use?? throwErr("...")with a descriptive message instead.Fix
- const adminApp = useAdminApp()!; + const adminApp = useAdminApp() ?? throwErr("useAdminApp returned null — expected to be inside AdminInterfaceProvider");As per coding guidelines: "Code defensively. Prefer
?? throwErr(...)over non-null assertions, with good error messages explicitly stating the assumption".🤖 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]/apps/page-client.tsx at line 29, Replace the non-null assertion used when assigning adminApp (const adminApp = useAdminApp()!) with a defensive null-check using the nullish coalescing operator: const adminApp = useAdminApp() ?? throwErr("Expected admin app from useAdminApp but received null/undefined"); ensure throwErr is imported (or implemented) in the module and provide a clear, descriptive message referencing the assumption that an admin app must be present; update the assignment site (adminApp) and any related imports accordingly.apps/dashboard/src/components/search-bar.tsx (1)
7-15:⚠️ Potential issue | 🟡 MinorPotential double-icon overlap when
leadingIconis passed through.Since
React.ComponentProps<typeof Input>now includesleadingIcon, callers can pass it toSearchBar, where it spreads into<Input>. This would render both the SearchBar's ownMagnifyingGlassIcon(Line 13) and the Input'sleadingIconlayout, causing overlapping icons and doubledpl-8padding.Consider either:
- Excluding
leadingIconfrom the forwarded props, or- Refactoring SearchBar to use Input's
leadingIconprop directly instead of manual icon placement.Option 2: Use Input's leadingIcon prop (preferred)
export const SearchBar = forwardRefIfNeeded< HTMLInputElement, React.ComponentProps<typeof Input> ->((props, ref) => ( - <div className="relative"> - <Input ref={ref} className="pl-8" {...props} /> - <MagnifyingGlassIcon className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" /> - </div> +>((props, ref) => ( + <Input + ref={ref} + leadingIcon={<MagnifyingGlassIcon className="h-4 w-4" />} + {...props} + /> ));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/search-bar.tsx` around lines 7 - 15, The SearchBar currently always renders MagnifyingGlassIcon while also forwarding React.ComponentProps<typeof Input> (including leadingIcon), which can cause duplicate/overlapping icons; update SearchBar (the forwardRefIfNeeded wrapper) to stop manually placing MagnifyingGlassIcon and instead pass a leadingIcon prop into <Input> (or alternatively strip leadingIcon from the spread props) so only one icon/layout path is used; specifically modify the component around SearchBar, Input, leadingIcon and MagnifyingGlassIcon to either remove leadingIcon from props before {...props} or set leadingIcon={<MagnifyingGlassIcon .../>} on <Input> and remove the separate absolute icon element.apps/dashboard/src/components/ui/input.tsx (1)
28-41:⚠️ Potential issue | 🟡 Minor
prefixItemborder radius mismatch with newrounded-xlbase.The prefix container uses
rounded-l-md(Line 31) while the new base classes userounded-xl. The input in this branch also appliesrounded-l-none, so the left side of the composite input will have amdradius while the right side hasxl. Consider updating the prefix container torounded-l-xlto stay visually consistent.🔧 Suggested fix
- <div className={'flex self-stretch justify-center items-center text-muted-foreground pl-3 select-none bg-muted/70 pr-3 border-r border-input rounded-l-md'}> + <div className={'flex self-stretch justify-center items-center text-muted-foreground pl-3 select-none bg-muted/70 pr-3 border-r border-input rounded-l-xl'}>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/ui/input.tsx` around lines 28 - 41, The prefixItem branch renders a left-side container with class 'rounded-l-md' which conflicts with the new base input radius ('rounded-xl') and causes inconsistent corners; update the prefix container's border-radius class from 'rounded-l-md' to 'rounded-l-xl' (the JSX block that renders prefixItem containing the div with classes including 'rounded-l-md') so it matches the input which uses baseClasses and 'rounded-l-none' for consistent visual rounding.apps/dashboard/src/components/editable-input.tsx (1)
138-150:⚠️ Potential issue | 🟠 MajorDouble-wrapping with
runAsynchronouslyWithAlertbreaks DesignButton's loading state.
DesignButtonalready wrapsonClickwithuseAsyncCallback+runAsynchronouslyWithAlertinternally (seebutton.tsxLines 68-79). Wrapping the handler here in anotherrunAsynchronouslyWithAlertmakes the call fire-and-forget from DesignButton's perspective —await onClick?.(e)resolves immediately, soisLoadingnever reflects the actual async work, and the button won't stay disabled during the update.Return the Promise directly instead:
Suggested fix
- onClick={() => runAsynchronouslyWithAlert(async () => { + onClick={async () => { try { forceAllowBlur.current = true; inputRef.current?.blur(); if (action === "accept") { await handleUpdate(editValue ?? throwErr("No value to update")); } setEditValue(null); setHasChanged(false); } finally { forceAllowBlur.current = false; } - })} + }}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/editable-input.tsx` around lines 138 - 150, The click handler is double-wrapped with runAsynchronouslyWithAlert which prevents DesignButton's internal useAsyncCallback from seeing the real Promise; remove the outer runAsynchronouslyWithAlert wrapper and return the async operation's Promise directly so DesignButton can await it. Specifically, in the onClick handler that references forceAllowBlur.current, inputRef.current?.blur(), handleUpdate(editValue ?? throwErr("No value to update")), setEditValue(null) and setHasChanged(false), make the function itself async (or return the async function) instead of calling runAsynchronouslyWithAlert around it so DesignButton's internal useAsyncCallback/runAsynchronouslyWithAlert can manage isLoading correctly.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsx (1)
366-366: 🛠️ Refactor suggestion | 🟠 MajorURL built with plain string interpolation — use
urlStringorencodeURIComponent.If
projectIdoruser.idcontain special characters, this could produce a malformed URL.- onClick={() => router.push(`/projects/${projectId}/users/${user.id}`)} + onClick={() => router.push(urlString`/projects/${projectId}/users/${user.id}`)}As per coding guidelines: "Use urlString`` or encodeURIComponent() instead of normal string interpolation for URLs, for consistency".
🤖 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]/(overview)/line-chart.tsx at line 366, The onClick handler builds a URL with plain string interpolation which can break if projectId or user.id contain special characters; update the router.push call in the onClick handler (where router.push is invoked) to construct the path using the project's urlString`` tagged template or by encoding components with encodeURIComponent(projectId) and encodeURIComponent(user.id) so the final path is safe and consistent with coding guidelines.apps/dashboard/src/components/assistant-ui/thread.tsx (1)
182-196: 🛠️ Refactor suggestion | 🟠 MajorHover-enter transition on action bars violates coding guidelines.
Lines 187 and 276 use
transition-opacity duration-150which animates the fade-in on hover-enter. Per guidelines, hover-enter should be instant and only the exit should be transitioned. Apply the established pattern:- className="flex items-center gap-0.5 self-center opacity-0 group-hover:opacity-100 transition-opacity duration-150" + className="flex items-center gap-0.5 self-center opacity-0 group-hover:opacity-100 transition-opacity duration-150 group-hover:transition-none"The same applies to
AssistantActionBarat Line 276.As per coding guidelines: "WHENEVER you create hover transitions, avoid hover-enter transitions, and just use hover-exit transitions (e.g.,
transition-colors hover:transition-none)".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/assistant-ui/thread.tsx` around lines 182 - 196, UserActionBar and AssistantActionBar currently apply a transition on hover-enter via "transition-opacity duration-150" which violates the guideline; update the className on both components to disable transitions while hovered so only hover-exit is animated—e.g., keep "transition-opacity duration-150" but add "group-hover:transition-none" (or "hover:transition-none" if the element itself is hovered) to the className string so the opacity change on hover-enter is instant and only the exit uses the transition; locate the className props on the UserActionBar and AssistantActionBar JSX and insert the group-hover/hover transition-none modifier accordingly.apps/dashboard/src/components/editable-grid.tsx (1)
159-181:⚠️ Potential issue | 🟠 MajorWrap the async function in a lambda so the helper owns the async flow
Lines 180 and 242 call
runAsynchronouslyWithAlert(handleChange(v)), which executes the promise before the utility can establish proper error handling. Instead, pass a function so the utility controls the entire async lifecycle:Suggested change
- onValueChange={(v) => runAsynchronouslyWithAlert(handleChange(v))} + onValueChange={(v) => runAsynchronouslyWithAlert(() => handleChange(v))}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/editable-grid.tsx` around lines 159 - 181, handleChange is being invoked before runAsynchronouslyWithAlert can manage its lifecycle; update the Select onValueChange (and any other similar calls) to pass a function reference that calls handleChange instead of calling it directly. For example, replace runAsynchronouslyWithAlert(handleChange(v)) with runAsynchronouslyWithAlert(() => handleChange(v)) so runAsynchronouslyWithAlert controls the async execution and error handling for handleChange.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/data-vault/stores/page-client.tsx (1)
61-63:⚠️ Potential issue | 🟡 MinorURL constructed via template literal instead of
urlString.
storeIdcould contain characters that need encoding. UseurlStringfor consistency and safety.♻️ Suggested fix
Add the import:
import { urlString } from "@stackframe/stack-shared/dist/utils/urls";const handleStoreClick = (storeId: string) => { - router.push(`/projects/${project.id}/data-vault/stores/${storeId}`); + router.push(urlString`/projects/${project.id}/data-vault/stores/${storeId}`); };As per coding guidelines, "Use urlString`` or encodeURIComponent() instead of normal string interpolation for URLs, for consistency".
🤖 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]/data-vault/stores/page-client.tsx around lines 61 - 63, The handleStoreClick function constructs a URL with a template literal that can be unsafe for special characters; update handleStoreClick to build the path using urlString (imported from "@stackframe/stack-shared/dist/utils/urls") instead of direct string interpolation and pass the result to router.push, ensuring you reference project.id and the storeId parameter via urlString so storeId is properly encoded.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx (1)
854-862:⚠️ Potential issue | 🟠 MajorError notification uses
toastinstead of an alert."No recipients selected" at line 856 is a blocking user error that prevents form submission. Per the coding guidelines, this should use an alert component rather than a toast, which is easily missed.
As per coding guidelines: "For blocking alerts and errors, never use
toast, as they are easily missed by the user. Instead, use alerts."🤖 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]/emails/page-client.tsx around lines 854 - 862, The current handleNext function uses toast for the blocking "No recipients selected" error; replace that toast call with the alert component used elsewhere in the app to surface blocking errors (show the same title "No recipients selected" and description "Please select at least one recipient to send the email." and use the destructive/negative styling variant). Update the logic inside handleNext so the alert is rendered/shown when selectedUsers.length === 0 and still returns "prevent-close" to stop submission; reference the handleNext function to locate and change the toast invocation to an alert-based pattern consistent with other alert usages in the codebase.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx (1)
411-414: 🛠️ Refactor suggestion | 🟠 Major
as anycast without explanation.Line 412 casts
appFrontendtoanyfor a truthiness check. This bypasses the type system. If the intent is to guard against undefined, use a proper== nullcheck on the original type, or add a comment explaining why the cast is needed.Suggested fix
- if (!(appFrontend as any)) { + if (appFrontend == null) {As per coding guidelines: "Do NOT use
as/any/type casts or anything else like that to bypass the type system unless you specifically asked the user about it."🤖 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]/sidebar-layout.tsx around lines 411 - 414, The code uses an unsafe cast "(appFrontend as any)" to perform a truthiness check; remove the cast and perform a proper null/undefined guard on the real value returned from ALL_APPS_FRONTEND[enabledApp] (the appFrontend variable) — e.g. replace the check with "if (appFrontend == null) continue;" or "if (typeof appFrontend === 'undefined') continue;" (or add a short comment if a more specific type-narrowing reason exists) so the type system is not bypassed while keeping the same control flow.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx (1)
126-144:⚠️ Potential issue | 🔴 CriticalUse
runAsynchronouslyWithAlertto handle errors from async operations instead of try/finally without catch.
handleTransferwrapstransferProjectintry/finallybut lacks acatchblock. If the operation throws, the error propagates unhandled throughActionDialog'sonClickhandler (which is a bare async function with no error handling), and the dialog closes without user feedback. Per the codebase guidelines, userunAsynchronouslyWithAlertfor async button handlers to automatically show errors and handle promise rejections properly.🤖 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]/project-settings/page-client.tsx around lines 126 - 144, Replace the current try/finally in handleTransfer with runAsynchronouslyWithAlert so promise rejections show an alert instead of bubbling unhandled; specifically, inside handleTransfer call runAsynchronouslyWithAlert(async () => { setIsTransferring(true); await user.transferProject(project.id, selectedTeamId); toast({ title: 'Project transferred successfully', variant: 'success' }); window.location.reload(); } , /* optional message */) and ensure setIsTransferring(false) is executed in a finally block inside that async callback (use setIsTransferring and isTransferring as before); remove the outer try/finally so errors are routed through runAsynchronouslyWithAlert.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/vercel/page-client.tsx (2)
267-276:⚠️ Potential issue | 🟡 MinorUse
performance.now()instead ofDate.now()for elapsed-time measurement.The confetti animation loop compares
Date.now()against a deadline to measure elapsed time.Date.now()can drift with system clock adjustments;performance.now()is the correct choice here.🛡️ Proposed fix
- const animationEnd = Date.now() + duration; + const animationEnd = performance.now() + duration; const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 9999 }; function randomInRange(min: number, max: number) { return Math.random() * (max - min) + min; } const interval = setInterval(() => { - const timeLeft = animationEnd - Date.now(); + const timeLeft = animationEnd - performance.now();As per coding guidelines, "Don't use Date.now() for measuring elapsed (real) time, instead use
performance.now()".🤖 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]/vercel/page-client.tsx around lines 267 - 276, The timing uses Date.now() for the confetti loop (variables duration, animationEnd and the interval callback) which can drift; change to performance.now() by computing animationEnd = performance.now() + duration and replace Date.now() with performance.now() inside the interval callback (and anywhere else using Date.now() to measure elapsed time, e.g. in the interval computation of timeLeft) so the elapsed-time measurement uses the high-resolution, monotonic timer.
86-87:⚠️ Potential issue | 🟡 MinorUse
?? throwErr(...)instead of non-null assertions.Both
publishableClientKeyandsecretServerKeycan benullat runtime. Replace!with a defensive fallback that surfaces a clear error.🛡️ Proposed fix
- publishableClientKey: newKey.publishableClientKey!, - secretServerKey: newKey.secretServerKey!, + publishableClientKey: newKey.publishableClientKey ?? throwErr("Expected publishableClientKey to be defined after creating an API key with hasPublishableClientKey: true"), + secretServerKey: newKey.secretServerKey ?? throwErr("Expected secretServerKey to be defined after creating an API key with hasSecretServerKey: true"),As per coding guidelines, "Code defensively. Prefer
?? throwErr(...)over non-null assertions, with good error messages explicitly stating the assumption".🤖 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]/vercel/page-client.tsx around lines 86 - 87, Replace the non-null assertions on newKey.publishableClientKey and newKey.secretServerKey with defensive fallbacks using the project's throwErr helper so a clear error is thrown if either is null; specifically, change the assignments for publishableClientKey and secretServerKey in page-client.tsx from using "newKey.publishableClientKey!" / "newKey.secretServerKey!" to "newKey.publishableClientKey ?? throwErr('Expected publishableClientKey from createNewKey')" and "newKey.secretServerKey ?? throwErr('Expected secretServerKey from createNewKey')" (or similar descriptive messages) to surface a clear runtime error instead of using non-null assertions.
🟡 Minor comments (17)
apps/dashboard/src/components/resizable-container.tsx-210-215 (1)
210-215:⚠️ Potential issue | 🟡 MinorHardcoded
bg-whitewon't adapt to dark mode.Line 212 uses
bg-whitefor the container background. Given that the ring already has dark-mode support (dark:ring-white/[0.08]), the background should likely also respect dark mode.♻️ Suggested fix
- "bg-white shadow-2xl", + "bg-white dark:bg-zinc-900 shadow-2xl",Adjust the dark color to match your design system's surface token.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/resizable-container.tsx` around lines 210 - 215, The container currently hardcodes "bg-white" inside the className passed to cn (the JSX in ResizableContainer / resizable-container.tsx); update that class string to use your design-system surface token and add a dark-mode variant (for example replace "bg-white" with the surface token and "dark:..." counterpart such as "bg-surface dark:bg-surface-<dark-token>" or whatever token names your design system uses) so the background follows dark mode while keeping the existing ring classes and other utilities untouched.apps/backend/src/app/api/latest/internal/wysiwyg-edit/route.tsx-151-151 (1)
151-151:⚠️ Potential issue | 🟡 MinorMock-mode
//comment may break non-JS source types.The
source_typefield accepts"template","theme", and"draft". If"theme"represents CSS or a non-JS format, prepending a// NOTE: ...comment produces invalid syntax. Consider using a format-appropriate comment or skipping the note for non-JS types.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backend/src/app/api/latest/internal/wysiwyg-edit/route.tsx` at line 151, The current construction of updatedSource unconditionally prepends a // comment which can invalidate non-JS sources; update the logic in route.tsx around the updatedSource creation (the updatedSource variable and where source_type is read) to choose a source-type-appropriate comment or omit the note: implement a small helper (e.g., getCommentPrefixForSourceType(source_type)) that returns '//' for JS-like types, '/* ... */' or '/*...*/\n' for CSS/theme, '<!-- ... -->' for HTML templates, or an empty string for unknown binary formats, then prepend that prefix + note only when non-empty before ${replacedSource}; ensure the helper covers "template", "theme", and "draft" cases and falls back to omitting the note.apps/backend/src/app/api/latest/internal/wysiwyg-edit/route.tsx-27-27 (1)
27-27:⚠️ Potential issue | 🟡 MinorTypo in system prompt.
Line 31: "without massively the rest of the source code" → should be "without massively changing the rest of the source code".
Fix
-8. Context: The user is editing the text in a WYSIWYG editor. They expect that the change they made will be reflected as-is, without massively the rest of the source code. However, in most cases, the user don't actually care about the rest of the source code, so in the rare cases where things are complex and you would have to change a bit more than just the text node, you should make the changes that sound reasonable from a UX perspective. +8. Context: The user is editing the text in a WYSIWYG editor. They expect that the change they made will be reflected as-is, without massively changing the rest of the source code. However, in most cases, the user doesn't actually care about the rest of the source code, so in the rare cases where things are complex and you would have to change a bit more than just the text node, you should make the changes that sound reasonable from a UX perspective.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backend/src/app/api/latest/internal/wysiwyg-edit/route.tsx` at line 27, There's a typo in the system prompt text in route.tsx: replace the phrase "without massively the rest of the source code" with "without massively changing the rest of the source code" so the prompt reads correctly; locate the system prompt string (used in the WYSIWYG edit route handler in apps/backend/src/app/api/latest/internal/wysiwyg-edit/route.tsx) and update that literal accordingly.apps/dashboard/src/components/design-components/button.tsx-58-58 (1)
58-58:⚠️ Potential issue | 🟡 MinorDuplicate
displayName— both inner and outer button are"DesignButton".
DesignOriginalButtonhasdisplayName = "DesignButton"(Line 58) andDesignButtonalso hasdisplayName = "DesignButton"(Line 91). This makes the two components indistinguishable in React DevTools.Suggested fix
-DesignOriginalButton.displayName = "DesignButton"; +DesignOriginalButton.displayName = "DesignOriginalButton";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/design-components/button.tsx` at line 58, Both components share the same displayName; change DesignOriginalButton.displayName to a unique name so React DevTools can distinguish them. Locate the displayName assignments for DesignOriginalButton and DesignButton and set them to distinct identifiers (e.g., DesignOriginalButton.displayName = "DesignOriginalButton" and keep DesignButton.displayName = "DesignButton") so each component has a unique displayName.apps/dashboard/src/components/design-components/tabs.tsx-96-102 (1)
96-102:⚠️ Potential issue | 🟡 Minor
!valueingetMapValueOrThrowfails for falsy values like0,"", orfalse.
Map.getreturnsundefinedfor missing keys, so the check should bevalue === undefinedto avoid incorrectly throwing for legitimately stored falsy values. While current callers use object values, the helper is generic.Suggested fix
function getMapValueOrThrow<TKey, TValue>(map: Map<TKey, TValue>, key: TKey, mapName: string) { - const value = map.get(key); - if (!value) { + if (!map.has(key)) { throw new Error(`Missing ${mapName} entry for key "${String(key)}"`); } - return value; + return map.get(key) as TValue; }Or keep the
get+ explicitundefinedcheck:const value = map.get(key); - if (!value) { + if (value === undefined) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/design-components/tabs.tsx` around lines 96 - 102, The check in getMapValueOrThrow incorrectly treats falsy stored values (0, "", false) as missing; update the existence test to only treat undefined as missing by checking value === undefined (or value === undefined || map.has(key) === false pattern), so replace the current if (!value) throw with an explicit undefined check while keeping the generic signature of getMapValueOrThrow and using map.get/key/mapName to build the error message.apps/dashboard/src/components/email-preview.tsx-623-635 (1)
623-635:⚠️ Potential issue | 🟡 MinorFix
editableRegionstype at the source instead of casting at usageLine 631 casts
editableRegions[id]toEditableMetadatato work around the hook returningRecord<string, unknown>. UpdateuseEmailPreviewWithEditableMarkersinpackages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts(line 682) to returnRecord<string, EditableMetadata>instead ofRecord<string, unknown>, eliminating the need for the cast.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/email-preview.tsx` around lines 623 - 635, The hook useEmailPreviewWithEditableMarkers currently returns Record<string, unknown>, forcing callers to cast editableRegions[id] to EditableMetadata; update the hook implementation (useEmailPreviewWithEditableMarkers) to return Record<string, EditableMetadata> (or a typed interface) so editableRegions is correctly typed at the source, adjust any internal logic/returns inside that function to construct and return EditableMetadata values, and update its signature/export so callers like the component using editableRegions no longer need to cast.apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/playground/page-client.tsx-327-341 (1)
327-341:⚠️ Potential issue | 🟡 MinorReplace
row.getValue()withrow.originalto eliminate unnecessary type castsLines 331 and 339 use
astype assertions onrow.getValue(...). Since the column definition is typed asColumnDef<DemoProduct>[],row.originalis already properly typed asDemoProductand eliminates the need for casts.Suggested change
- cell: ({ row }) => ( - <span className="text-sm text-muted-foreground"> - ${(row.getValue("price") as number).toFixed(2)} - </span> - ), + cell: ({ row }) => ( + <span className="text-sm text-muted-foreground"> + ${row.original.price.toFixed(2)} + </span> + ), ... - const s = row.getValue("status") as DemoProduct["status"]; - return <DesignBadge label={STATUS_BADGE[s].label} color={STATUS_BADGE[s].color} size="sm" />; + const status = row.original.status; + return <DesignBadge label={STATUS_BADGE[status].label} color={STATUS_BADGE[status].color} size="sm" />;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/app/`(main)/(protected)/(outside-dashboard)/playground/page-client.tsx around lines 327 - 341, Replace unnecessary type assertions by using the typed row.original property: in the price column cell (currently using (row.getValue("price") as number).toFixed(2)) use the typed value from row.original.price, and in the status column cell (currently using row.getValue("status") as DemoProduct["status"]) use row.original.status to feed STATUS_BADGE and DesignBadge; this leverages the ColumnDef<DemoProduct>[] typing and removes the need for casts on row.getValue.apps/dashboard/src/components/editable-grid.tsx-438-442 (1)
438-442:⚠️ Potential issue | 🟡 MinorUse a stable key for grid items
Line 440 uses
key={index}; if items reorder, React can reuse state incorrectly. SinceitemKeyis available in all item types (viaBaseItemProps), use that with a fallback for stability.Suggested change
- key={index} + key={item.itemKey ?? index}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/editable-grid.tsx` around lines 438 - 442, The map currently uses an unstable index key (key={index}) for GridItemContent which can cause incorrect state reuse when items reorder; change it to use the item's stable identifier (item.itemKey) as the key and fall back to a stable derived value only if item.itemKey is missing so keys remain consistent; update the items.map call that renders <GridItemContent ...> to use item.itemKey (with a deterministic fallback) and ensure any code referencing externalModifiedKeys still uses item.itemKey to check modification status.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-templates/[templateId]/page-client.tsx-98-105 (1)
98-105:⚠️ Potential issue | 🟡 MinorUse explicit check instead of
!currentCodefor string state.Line 100 uses
!currentCodewhich is a boolean check on a string. An empty template source ("") would be falsy, which means the sync would trigger even if the user had cleared the editor. Consider an explicit check that better represents the intent, e.g. tracking whether the template has been initially loaded.As per coding guidelines, "prefer explicit null/undefinedness checks over boolean checks, eg.
foo == nullinstead of!foo".🤖 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]/email-templates/[templateId]/page-client.tsx around lines 98 - 105, The useEffect currently checks `!currentCode`, which treats an empty string as unset; change that to an explicit null/undefined check (e.g., `currentCode == null`) so only truly uninitialized state triggers the sync; update the conditional in the useEffect that references `templateFromHook`, `currentCode`, `hasSyncedTemplateFromHook`, and the calls to `setCurrentCode`/`setSelectedThemeId` to use the explicit null check for `currentCode` while keeping the existing `hasSyncedTemplateFromHook` guard to ensure initial-load-only behavior.apps/backend/src/app/api/latest/emails/render-email/route.tsx-82-83 (1)
82-83:⚠️ Potential issue | 🟡 MinorAvoid the
astype cast — use a fallback or explicit check instead.Line 83 uses
as 'template' | 'theme' | 'both'to bypass the type system. Since yup validates the value via.oneOf(...), the runtime value is guaranteed, but the cast hides this from TypeScript. Consider a safer pattern:Proposed fix
- const editableSource = ('editable_source' in body ? body.editable_source : 'template') as 'template' | 'theme' | 'both'; + const rawEditableSource = 'editable_source' in body ? body.editable_source : undefined; + const editableSource: 'template' | 'theme' | 'both' = + rawEditableSource === 'template' || rawEditableSource === 'theme' || rawEditableSource === 'both' + ? rawEditableSource + : 'template';As per coding guidelines, "Do NOT use
as/any/type casts or anything else like that to bypass the type system unless you specifically asked the user about it".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backend/src/app/api/latest/emails/render-email/route.tsx` around lines 82 - 83, The code uses a type cast on editableSource to silence TypeScript; instead add a runtime check or type guard so TypeScript can infer the union without "as". Replace the cast on editableSource by validating body.editable_source with a small predicate or includes check (e.g. an isEditableSource(value): value is 'template'|'theme'|'both' or ['template','theme','both'].includes(value)) and then set const editableSource = isEditableSource(body.editable_source) ? body.editable_source : 'template'; ensure you reference the existing editableSource variable and body.editable_source so the type is narrowed safely without using as.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-drafts/page-client.tsx-350-355 (1)
350-355:⚠️ Potential issue | 🟡 MinorYup schema allows empty string for draft name.
yup.string().defined()permits empty strings. While therequiredHTML attribute on theInputField(line 362) provides browser-level validation, the schema itself would pass"". Consider adding.required()to the yup schema for robust validation.♻️ Suggested fix
formSchema={yup.object({ - name: yup.string().defined().label("Draft name"), + name: yup.string().required().label("Draft name"), })}🤖 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]/email-drafts/page-client.tsx around lines 350 - 355, The yup schema for the form (the formSchema passed to the component) currently uses yup.string().defined() for the "name" field which allows empty strings; update the schema for the name field in the formSchema (the yup.object used alongside handleCreateNewDraft) to enforce non-empty values by chaining .required() (optionally also .trim().min(1) to reject whitespace-only names) so the schema itself will reject empty/blank draft names instead of relying only on the InputField HTML required attribute.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/webhooks/page-client.tsx-395-438 (1)
395-438:⚠️ Potential issue | 🟡 MinorColumn definitions are re-created on every render; also, status is always hardcoded to "Active".
Two observations:
columnsis defined inside the else branch withoutuseMemo, so it's re-created on every render. This can cause unnecessary table re-renders.The "Status" column (lines 414-423) always renders a green "Active" badge regardless of the actual endpoint state. If Svix endpoints can be disabled or paused, this misrepresents their status.
♻️ Consider memoizing the columns
Move the column definitions into a
useMemoor extract them outside the component (since they depend onprops).🤖 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]/webhooks/page-client.tsx around lines 395 - 438, The columns array (columns) is being re-created on every render and the "Status" column always shows a hardcoded "Active" badge; wrap the column definitions in a useMemo so they are memoized based on dependencies (e.g., props.updateFn, props.onTestRequested) and update the status column cell to read the endpoint state from row.original (e.g., row.original.status or row.original.enabled) and render DesignBadge with the appropriate label and color instead of the hardcoded "Active"; ensure ActionMenu still receives endpoint={row.original} and the memo includes any props it uses.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/team-settings/page-client.tsx-37-42 (1)
37-42:⚠️ Potential issue | 🟡 Minor
useEffectresets selection on everydefaultPermissionsreference change while dialog is open.
defaultPermissionsis an object derived from config, so its reference may change on every render (or whenever config re-fetches). If this happens while the dialog is open, the user's in-progress checkbox changes get silently discarded. Consider memoizingdefaultPermissionsor comparing by value (e.g.,JSON.stringify) to avoid spurious resets.♻️ Suggested approach
+ const defaultPermissionsKey = JSON.stringify(defaultPermissions); + // Reset selection when dialog opens useEffect(() => { if (open) { setSelectedIds(Object.keys(defaultPermissions).filter(id => defaultPermissions[id])); } - }, [open, defaultPermissions]); + // eslint-disable-next-line react-hooks/exhaustive-deps -- compare by value, not reference + }, [open, defaultPermissionsKey]);🤖 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]/team-settings/page-client.tsx around lines 37 - 42, The useEffect that resets selection when the dialog opens is using the mutable reference defaultPermissions, causing selection to be reset whenever defaultPermissions' reference changes while open; update the code so the effect only depends on stable values—either memoize defaultPermissions where it's created (so the reference is stable) or change the effect to compare values (e.g., derive a stringifiedKey = JSON.stringify(defaultPermissions) and depend on that) and keep the current conditions (open) and update via setSelectedIds(Object.keys(defaultPermissions).filter(id => defaultPermissions[id])) only when the actual permissions value changes rather than on reference equality; ensure you adjust dependencies to use the memoized variable or the stringified value in the useEffect signature.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx-709-719 (1)
709-719:⚠️ Potential issue | 🟡 MinorBoolean checks for missing fields: port
0would be falsely flagged as missing.
!emailServerConfig.portis falsy when the port is0, which is a valid (though uncommon) port number and is explicitly allowed by the schema (yup.number().min(0, ...)). Use explicit null/undefined checks instead.Suggested fix
- if (!emailServerConfig.host) missingFields.push("host"); - if (!emailServerConfig.port) missingFields.push("port"); - if (!emailServerConfig.username) missingFields.push("username"); - if (!emailServerConfig.password) missingFields.push("password"); - if (!emailServerConfig.senderName) missingFields.push("sender name"); - if (!emailServerConfig.senderEmail) missingFields.push("sender email"); + if (emailServerConfig.host == null) missingFields.push("host"); + if (emailServerConfig.port == null) missingFields.push("port"); + if (emailServerConfig.username == null) missingFields.push("username"); + if (emailServerConfig.password == null) missingFields.push("password"); + if (emailServerConfig.senderName == null) missingFields.push("sender name"); + if (emailServerConfig.senderEmail == null) missingFields.push("sender email");As per coding guidelines: "Unless very clearly equivalent from types, prefer explicit null/undefinedness checks over boolean checks, eg.
foo == nullinstead of!foo."🤖 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]/emails/page-client.tsx around lines 709 - 719, The null/undefined check logic currently uses boolean falsiness (e.g., !emailServerConfig.port) which incorrectly treats valid falsy values like 0 as missing; update the checks to use explicit null/undefined checks—for example replace the port check with emailServerConfig.port == null (or === null || === undefined) before pushing "port" to missingFields; apply the same explicit null/undefined style for any other fields where falsy but valid values may exist, then keep the setError and return "prevent-close" behavior unchanged.apps/dashboard/src/components/design-components/list.tsx-197-206 (1)
197-206:⚠️ Potential issue | 🟡 MinorEmpty name produces an invisible avatar initial.
If
nameis an empty string,name.charAt(0)returns"", leaving the avatar circle blank with no visual fallback.Suggested fix
- {name.charAt(0)} + {name.charAt(0) || "?"}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/design-components/list.tsx` around lines 197 - 206, UserAvatar currently renders name.charAt(0) which produces an empty string for an empty or whitespace-only name and leaves the avatar blank; update the rendering in the UserAvatar component to compute a safe initial (for example: take name?.trim().charAt(0)?.toUpperCase() and fall back to a default like "?" when that result is falsy) so the avatar always shows a visible character; reference the UserAvatar function and its name prop to locate where to replace name.charAt(0) with this guarded fallback.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-drafts/[draftId]/page-client.tsx-279-287 (1)
279-287:⚠️ Potential issue | 🟡 Minor
getErrorMessageis defined but never used.This function at the bottom of the file is unreferenced — it appears to be dead code left over from refactoring.
🤖 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]/email-drafts/[draftId]/page-client.tsx around lines 279 - 287, The function getErrorMessage is dead code and should be removed; delete the getErrorMessage function (and its dependency on KnownErrors.EmailRenderingError if that import is only used by it) from page-client.tsx to clean up unused code, or if its logic is still needed, replace inline error handling in the surrounding components/methods with calls to getErrorMessage (e.g., where errors are rendered) and ensure KnownErrors.EmailRenderingError remains imported; locate the symbol getErrorMessage to remove or replace and adjust imports accordingly.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-themes/page-client.tsx-275-282 (1)
275-282:⚠️ Potential issue | 🟡 MinorPreview
senderEmailderivation could produce an odd address for edge-case project names.If
project.displayNameis empty or all-special-characters, the email becomesnoreply@.com. This is only for a preview so the impact is cosmetic, but a fallback would be cleaner:- senderEmail={`noreply@${project.displayName.toLowerCase().replace(/[^a-z0-9]/g, '')}.com`} + senderEmail={`noreply@${project.displayName.toLowerCase().replace(/[^a-z0-9]/g, '') || 'example'}.com`}🤖 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]/email-themes/page-client.tsx around lines 275 - 282, The preview senderEmail can become "noreply@.com" when project.displayName is empty or fully sanitized away; update the code that supplies the senderEmail prop for EmailPreview to compute a sanitized local-part (e.g., lowercased project.displayName with non-alphanumerics removed) and if that result is empty use a safe fallback (like project.id, "example", or "default") so senderEmail is always a valid-looking address (e.g., `noreply@{sanitizedOrFallback}.com`); adjust where senderEmail is constructed near the EmailPreview usage that references selectedThemeData and project.displayName.
apps/backend/src/app/api/latest/internal/email-templates/[templateId]/route.tsx
Show resolved
Hide resolved
apps/backend/src/app/api/latest/internal/email-templates/[templateId]/route.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx
Show resolved
Hide resolved
.../src/app/(main)/(protected)/projects/[projectId]/data-vault/stores/[storeId]/page-client.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx
Show resolved
Hide resolved
- Added dynamic string handling for email template configuration overrides. - Updated line-chart component to use urlString for routing. - Refactored data vault store configuration to directly set displayName. - Improved error handling in email template deletion process. - Enhanced launch checklist with new asynchronous alert functionality. - Updated Vercel page to ensure proper key assignment with error handling. - Modified dropdown menu state initialization for better default handling. - Improved data table component to prevent multiple calls on table readiness.
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-templates/page-client.tsx (1)
150-156:⚠️ Potential issue | 🔴 CriticalBug:
"prevent-close"return value fromhandleDeleteis silently discarded.The
onClickhandlerawaitshandleDeletebut neverreturns its result, soActionDialogalways seesundefinedand closes the dialog — even when the delete fails. This means the error alert on lines 163-170 is still never visible, despite thereturn "prevent-close"added on line 36.Proposed fix
okButton={{ label: "Delete", onClick: async () => { if (deleteDialogOpen) { - await handleDelete(deleteDialogOpen); + return await handleDelete(deleteDialogOpen); } },🤖 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]/email-templates/page-client.tsx around lines 150 - 156, The okButton onClick awaits handleDelete(deleteDialogOpen) but does not return its result, so the "prevent-close" sentinel is discarded and ActionDialog always closes; update the okButton.onClick handler in the component (the okButton block where deleteDialogOpen is checked) to return the awaited result of handleDelete(deleteDialogOpen) (i.e., use "return await handleDelete(deleteDialogOpen);") so ActionDialog receives the "prevent-close" value and can keep the dialog open on failure.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/launch-checklist/page-client.tsx (1)
436-457:⚠️ Potential issue | 🔴 CriticalBug: error list never renders — condition is always true.
productionChecksPassingis defined asproductionModeErrors.length === 0(line 418), so the guard on line 437 (productionChecksPassing || productionModeErrors.length === 0) is tautologically true. Theelsebranch (lines 441–456) showing the list of production-mode errors is dead code — users will never see what they need to fix.The intent was likely
!productionChecksPassingas the first operand (or simply checkingproductionModeErrors.length === 0alone):Proposed fix
- productionChecksPassing || productionModeErrors.length === 0 ? ( + productionModeErrors.length === 0 ? (🤖 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]/launch-checklist/page-client.tsx around lines 436 - 457, The conditional always evaluates true because productionChecksPassing is defined as productionModeErrors.length === 0; update the JSX guard so the error list renders when checks fail by using the negation (e.g., !productionChecksPassing) or directly checking productionModeErrors.length > 0 in the ternary condition where productionChecksPassing || productionModeErrors.length === 0 is currently used; modify the conditional around productionChecksPassing/productionModeErrors in the JSX block that renders the "All checks are passing." vs the error list so the else branch with productionModeErrors.map(...) can execute when there are errors.apps/dashboard/src/components/ui/dropdown-menu.tsx (1)
119-152:⚠️ Potential issue | 🔴 Critical
onClickleaks through the{...props}spread, still causing double execution on mouse clicks.
onClickis not destructured out of...propson line 119, so{...props}on line 150 passes it directly toDropdownMenuPrimitive.Item. On a mouse click, the nativeonClickfiresprops.onClickdirectly andonSelectfireshandleItemActionwhich callsprops.onClickagain — the same double-execution bug from the previous review, just via a different path.Destructure
onClickout of the rest props so it doesn't leak to the primitive:Proposed fix
->(({ className, inset, icon, ...props }, ref) => { +>(({ className, inset, icon, onClick, ...props }, ref) => { const { setOpen } = React.useContext(DropdownMenuContext) ?? throwErr("No DropdownMenuContext found"); const [isLoading, setIsLoading] = React.useState(false); // Share activation logic so keyboard onSelect matches mouse clicks. const handleItemAction = (event: { preventDefault: () => void, stopPropagation: () => void }) => { event.preventDefault(); event.stopPropagation(); - const result = props.onClick?.(event as unknown as React.MouseEvent<HTMLDivElement, MouseEvent>); + const result = onClick?.(event as unknown as React.MouseEvent<HTMLDivElement, MouseEvent>); if (result instanceof Promise) { setIsLoading(true); runAsynchronouslyWithAlert( result.finally(() => { setIsLoading(false); setOpen(false); }) ); } else { setOpen(false); } }; ... {...props} disabled={isLoading || props.disabled} - onSelect={props.onClick ? handleItemAction : undefined} + onSelect={onClick ? handleItemAction : undefined} >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/ui/dropdown-menu.tsx` around lines 119 - 152, The onClick handler is leaking through the {...props} spread and causing double execution (native onClick plus onSelect -> handleItemAction -> props.onClick); fix by destructuring onClick from the component args (e.g., change ({ className, inset, icon, ...props }, ref) to extract onClick: const { onClick, ...rest } or destructure directly) and use rest in the JSX spread so DropdownMenuPrimitive.Item does not receive onClick, then reference the extracted onClick inside handleItemAction and set onSelect={onClick ? handleItemAction : undefined} and disabled based on isLoading || rest.disabled. Ensure you update all uses of props to rest and props.onClick to the extracted onClick (handleItemAction should call the extracted onClick).apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/vercel/page-client.tsx (1)
63-63:⚠️ Potential issue | 🟡 MinorDead code:
errorstate is declared but never set.
setError(line 63) is never called anywhere — the oldcatchblock that presumably calledsetError(...)appears to have been removed whenrunAsynchronouslyWithAlertwas adopted. As a result,props.errorinStepCardis alwaysnulland theDesignAlerton lines 591-595 will never render. Either remove the deaderrorstate and related UI, or restore error-setting logic if per-field error display is still desired alongside the global alert.Option A: Remove dead code
- const [error, setError] = useState<string | null>(null);And remove the
errorprop fromStepCardusage and its rendering logic.Also applies to: 590-595
🤖 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]/vercel/page-client.tsx at line 63, The local state error declared as `const [error, setError] = useState<string | null>(null)` is dead because `setError` is never called after switching to `runAsynchronouslyWithAlert`; either remove this state and all uses (remove the `error` prop passed into `StepCard` and the `DesignAlert` rendering around lines where `props.error` is referenced), or restore error-setting where failures are handled by calling `setError(...)` inside the async handlers invoked by `runAsynchronouslyWithAlert` so `StepCard` and `DesignAlert` receive and display per-field errors; update the `StepCard` prop usage and any conditional rendering accordingly (refer to `error`, `setError`, `runAsynchronouslyWithAlert`, `StepCard`, and `DesignAlert` to locate the changes).
🤖 Fix all issues with AI agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsx:
- Around line 186-218: Hoist the static mapping const designCardGradients out of
the component and into module scope so it isn't recreated on every render: move
the existing designCardGradients declaration to the top-level of the file (above
the component) and leave all references (designCardGradients[gradientColor])
inside the component unchanged; ensure the identifier name remains the same and
adjust any imports/exports only if you intend to reuse it across files.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/data-vault/stores/[storeId]/page-client.tsx:
- Line 47: Replace the plain template-literal URL construction used in the
DesignButton onClick (router.push(`/projects/${project.id}/data-vault/stores`))
and the other router.push that uses
`/projects/${project.id}/data-vault/stores/${storeId}` with the urlString
template tag (e.g., urlString`/projects/${project.id}/data-vault/stores` and
urlString`/projects/${project.id}/data-vault/stores/${storeId}`) and add the
missing import for urlString at the top of the file so special characters in
project.id or storeId are properly encoded; update the two router.push call
sites (the DesignButton onClick and the other redirect using storeId) to use
urlString instead of plain interpolation.
- Around line 110-117: The onClick currently calls copyToClipboard(storeId) and
drops the returned promise; wrap that async call with the project's async error
handler so clipboard failures aren't swallowed—replace the arrow handler on the
DesignButton with a call to runAsynchronously or runAsynchronouslyWithAlert
(import it if missing) and pass a callback that invokes
copyToClipboard(storeId); keep DesignButton, copyToClipboard, and the chosen
runAsynchronously/runAsynchronouslyWithAlert identifiers to locate and update
the handler.
- Around line 36-54: The code reads `store = config.dataVault.stores[storeId]`
before checking existence and declares `modifiedKeys` with `useMemo`, which can
leave `store` typed as possibly undefined later; fix by either moving the
existence guard (the `if (!(storeId in config.dataVault.stores))` block) above
the `store` and `useMemo` declarations so hooks remain in the same order across
returns, or keep the current order and add an explicit defensive narrowing
immediately after the guard (e.g., throw via a utility if `store` is undefined)
so usages later (references to `store`, `modifiedKeys`, `useMemo`) see a
non-undefined `store`; ensure you do not introduce conditional hooks when
reordering.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/email-templates/page-client.tsx:
- Around line 150-156: The okButton onClick awaits
handleDelete(deleteDialogOpen) but does not return its result, so the
"prevent-close" sentinel is discarded and ActionDialog always closes; update the
okButton.onClick handler in the component (the okButton block where
deleteDialogOpen is checked) to return the awaited result of
handleDelete(deleteDialogOpen) (i.e., use "return await
handleDelete(deleteDialogOpen);") so ActionDialog receives the "prevent-close"
value and can keep the dialog open on failure.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/launch-checklist/page-client.tsx:
- Around line 539-548: The confetti timer uses Date.now() for elapsed-time
calculations; update the logic to use performance.now() instead by setting
animationEnd based on performance.now() + duration (replace the current
animationEnd = Date.now() + duration) and compute timeLeft as animationEnd -
performance.now() inside the setInterval callback (keep the rest of the confetti
logic including defaults, randomInRange, and interval unchanged). Ensure all
references to Date.now() in this block are replaced so elapsed-time measurement
consistently uses performance.now().
- Around line 149-158: The cardClass values for the checklist states (keys done,
action, blocked) use hover-enter classes like "transition-all duration-300
hover:shadow-lg"; update each cardClass in page-client.tsx to follow the
dashboard hover-exit pattern by moving the transition to the base state and
removing hover-enter effects—specifically, keep a base transition (e.g.,
transition-[property] and duration) and replace hover:shadow-lg with
hover:transition-none (and remove or neutralize any shadow-on-hover) so the
visual transition occurs on hover-exit rather than on hover-enter; update the
cardClass strings for done, action, and blocked accordingly.
- Around line 142-161: STATUS_META currently defines identical cardClass strings
for all LaunchTaskStatus keys (done, action, blocked); either deduplicate by
extracting a shared constant and reusing it in STATUS_META or assign distinct
cardClass values per status if visual differentiation is intended. Update the
STATUS_META declaration (and the LaunchTaskStatus usages) to reference a single
constant like DEFAULT_CARD_CLASS for the common string, or replace the three
identical cardClass values with unique style strings for done/action/blocked so
the per-status lookup becomes meaningful.
- Around line 436-457: The conditional always evaluates true because
productionChecksPassing is defined as productionModeErrors.length === 0; update
the JSX guard so the error list renders when checks fail by using the negation
(e.g., !productionChecksPassing) or directly checking
productionModeErrors.length > 0 in the ternary condition where
productionChecksPassing || productionModeErrors.length === 0 is currently used;
modify the conditional around productionChecksPassing/productionModeErrors in
the JSX block that renders the "All checks are passing." vs the error list so
the else branch with productionModeErrors.map(...) can execute when there are
errors.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/vercel/page-client.tsx:
- Around line 403-414: The subtitle prop on the DesignCard is using the HTML
entity ' which renders literally in JSX attributes; update the subtitle
value (on the DesignCard component instance) to use a real apostrophe or a JS
string expression instead (e.g. subtitle="See Vercel's documentation on
environment variables for more details." or subtitle={'See Vercel\\'s
documentation on environment variables for more details.'}) so the apostrophe
displays correctly.
- Around line 492-496: The DesignButton/StyledLink combination currently uses
"transition-all duration-150" which triggers hover-enter transitions; replace
that with a hover-exit pattern such as "transition-colors duration-150
hover:transition-none" (keeping other classes like "font-medium border
border-border shadow-sm active:scale-95 dark:..." intact) for the instance
inside page-client.tsx and the matching button(s) later in the file to follow
the dashboard guideline.
- Line 63: The local state error declared as `const [error, setError] =
useState<string | null>(null)` is dead because `setError` is never called after
switching to `runAsynchronouslyWithAlert`; either remove this state and all uses
(remove the `error` prop passed into `StepCard` and the `DesignAlert` rendering
around lines where `props.error` is referenced), or restore error-setting where
failures are handled by calling `setError(...)` inside the async handlers
invoked by `runAsynchronouslyWithAlert` so `StepCard` and `DesignAlert` receive
and display per-field errors; update the `StepCard` prop usage and any
conditional rendering accordingly (refer to `error`, `setError`,
`runAsynchronouslyWithAlert`, `StepCard`, and `DesignAlert` to locate the
changes).
In `@apps/dashboard/src/components/ui/dropdown-menu.tsx`:
- Around line 119-152: The onClick handler is leaking through the {...props}
spread and causing double execution (native onClick plus onSelect ->
handleItemAction -> props.onClick); fix by destructuring onClick from the
component args (e.g., change ({ className, inset, icon, ...props }, ref) to
extract onClick: const { onClick, ...rest } or destructure directly) and use
rest in the JSX spread so DropdownMenuPrimitive.Item does not receive onClick,
then reference the extracted onClick inside handleItemAction and set
onSelect={onClick ? handleItemAction : undefined} and disabled based on
isLoading || rest.disabled. Ensure you update all uses of props to rest and
props.onClick to the extracted onClick (handleItemAction should call the
extracted onClick).
🧹 Nitpick comments (6)
🤖 Fix all nitpicks with AI agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsx: - Around line 186-218: Hoist the static mapping const designCardGradients out of the component and into module scope so it isn't recreated on every render: move the existing designCardGradients declaration to the top-level of the file (above the component) and leave all references (designCardGradients[gradientColor]) inside the component unchanged; ensure the identifier name remains the same and adjust any imports/exports only if you intend to reuse it across files. In `@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/data-vault/stores/[storeId]/page-client.tsx: - Around line 36-54: The code reads `store = config.dataVault.stores[storeId]` before checking existence and declares `modifiedKeys` with `useMemo`, which can leave `store` typed as possibly undefined later; fix by either moving the existence guard (the `if (!(storeId in config.dataVault.stores))` block) above the `store` and `useMemo` declarations so hooks remain in the same order across returns, or keep the current order and add an explicit defensive narrowing immediately after the guard (e.g., throw via a utility if `store` is undefined) so usages later (references to `store`, `modifiedKeys`, `useMemo`) see a non-undefined `store`; ensure you do not introduce conditional hooks when reordering. In `@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/launch-checklist/page-client.tsx: - Around line 539-548: The confetti timer uses Date.now() for elapsed-time calculations; update the logic to use performance.now() instead by setting animationEnd based on performance.now() + duration (replace the current animationEnd = Date.now() + duration) and compute timeLeft as animationEnd - performance.now() inside the setInterval callback (keep the rest of the confetti logic including defaults, randomInRange, and interval unchanged). Ensure all references to Date.now() in this block are replaced so elapsed-time measurement consistently uses performance.now(). - Around line 149-158: The cardClass values for the checklist states (keys done, action, blocked) use hover-enter classes like "transition-all duration-300 hover:shadow-lg"; update each cardClass in page-client.tsx to follow the dashboard hover-exit pattern by moving the transition to the base state and removing hover-enter effects—specifically, keep a base transition (e.g., transition-[property] and duration) and replace hover:shadow-lg with hover:transition-none (and remove or neutralize any shadow-on-hover) so the visual transition occurs on hover-exit rather than on hover-enter; update the cardClass strings for done, action, and blocked accordingly. - Around line 142-161: STATUS_META currently defines identical cardClass strings for all LaunchTaskStatus keys (done, action, blocked); either deduplicate by extracting a shared constant and reusing it in STATUS_META or assign distinct cardClass values per status if visual differentiation is intended. Update the STATUS_META declaration (and the LaunchTaskStatus usages) to reference a single constant like DEFAULT_CARD_CLASS for the common string, or replace the three identical cardClass values with unique style strings for done/action/blocked so the per-status lookup becomes meaningful. In `@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/vercel/page-client.tsx: - Around line 492-496: The DesignButton/StyledLink combination currently uses "transition-all duration-150" which triggers hover-enter transitions; replace that with a hover-exit pattern such as "transition-colors duration-150 hover:transition-none" (keeping other classes like "font-medium border border-border shadow-sm active:scale-95 dark:..." intact) for the instance inside page-client.tsx and the matching button(s) later in the file to follow the dashboard guideline.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/launch-checklist/page-client.tsx (3)
539-548: Useperformance.now()instead ofDate.now()for elapsed-time measurement.The confetti animation interval computes remaining time via
Date.now(). Per coding guidelines,performance.now()should be used for measuring elapsed (real) time.Proposed fix
- const animationEnd = Date.now() + duration; + const animationEnd = performance.now() + duration; ... - const timeLeft = animationEnd - Date.now(); + const timeLeft = animationEnd - performance.now();As per coding guidelines: "Use
performance.now()instead ofDate.now()for measuring elapsed (real) time."🤖 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]/launch-checklist/page-client.tsx around lines 539 - 548, The confetti timer uses Date.now() for elapsed-time calculations; update the logic to use performance.now() instead by setting animationEnd based on performance.now() + duration (replace the current animationEnd = Date.now() + duration) and compute timeLeft as animationEnd - performance.now() inside the setInterval callback (keep the rest of the confetti logic including defaults, randomInRange, and interval unchanged). Ensure all references to Date.now() in this block are replaced so elapsed-time measurement consistently uses performance.now().
149-158: Consider hover-exit transition pattern per dashboard guidelines.Classes like
transition-all duration-300 hover:shadow-lgare hover-enter transitions. The dashboard guideline recommends the inverse: apply transitions on exit, not entry (e.g.,transition-all hover:transition-none), so the visual effect feels snappier.As per coding guidelines: "Use hover-exit transitions instead of hover-enter transitions. For example,
transition-colors hover:transition-none."🤖 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]/launch-checklist/page-client.tsx around lines 149 - 158, The cardClass values for the checklist states (keys done, action, blocked) use hover-enter classes like "transition-all duration-300 hover:shadow-lg"; update each cardClass in page-client.tsx to follow the dashboard hover-exit pattern by moving the transition to the base state and removing hover-enter effects—specifically, keep a base transition (e.g., transition-[property] and duration) and replace hover:shadow-lg with hover:transition-none (and remove or neutralize any shadow-on-hover) so the visual transition occurs on hover-exit rather than on hover-enter; update the cardClass strings for done, action, and blocked accordingly.
142-161: All threeSTATUS_METAentries share the samecardClassstring.The
done,action, andblockedstatuses have identicalcardClassvalues, which makes the per-status lookup pointless. If differentiation is planned, consider adding distinct styles; otherwise, extract a single constant to avoid the repetition.🤖 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]/launch-checklist/page-client.tsx around lines 142 - 161, STATUS_META currently defines identical cardClass strings for all LaunchTaskStatus keys (done, action, blocked); either deduplicate by extracting a shared constant and reusing it in STATUS_META or assign distinct cardClass values per status if visual differentiation is intended. Update the STATUS_META declaration (and the LaunchTaskStatus usages) to reference a single constant like DEFAULT_CARD_CLASS for the common string, or replace the three identical cardClass values with unique style strings for done/action/blocked so the per-status lookup becomes meaningful.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/vercel/page-client.tsx (1)
492-496: Prefer hover-exit transitions per dashboard guidelines.The buttons use
transition-all duration-150which applies transitions on hover-enter. The coding guideline states: "Use hover-exit transitions instead of hover-enter transitions. For example,transition-colors hover:transition-none."Proposed fix
- className="font-medium border border-border shadow-sm transition-all duration-150 hover:bg-accent active:scale-95 ..." + className="font-medium border border-border shadow-sm transition-all duration-150 hover:transition-none hover:bg-accent active:scale-95 ..."Also applies to: 501-508
🤖 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]/vercel/page-client.tsx around lines 492 - 496, The DesignButton/StyledLink combination currently uses "transition-all duration-150" which triggers hover-enter transitions; replace that with a hover-exit pattern such as "transition-colors duration-150 hover:transition-none" (keeping other classes like "font-medium border border-border shadow-sm active:scale-95 dark:..." intact) for the instance inside page-client.tsx and the matching button(s) later in the file to follow the dashboard guideline.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/data-vault/stores/[storeId]/page-client.tsx (1)
36-54:storeis read before the existence guard — consider reordering or adding a defensive throw.
storeis assigned on line 36 but may beundefinedifstoreIdisn't present. The early return on line 41 prevents further usage at runtime, but TypeScript may not narrow the type, leavingstoretyped as potentiallyundefinedwhen used later (lines 57, 65, 129). A small reorder makes the intent explicit:♻️ Suggested reorder
const config = project.useConfig(); - const store = config.dataVault.stores[storeId]; - const modifiedKeys = useMemo(() => new Set([ - ...(localDisplayName !== undefined ? ["display-name"] : []), - ]), [localDisplayName]); if (!(storeId in config.dataVault.stores)) { return ( ... ); } + const store = config.dataVault.stores[storeId] ?? throwErr(`Store ${storeId} not found despite passing existence check`); + const modifiedKeys = useMemo(() => new Set([ + ...(localDisplayName !== undefined ? ["display-name"] : []), + ]), [localDisplayName]);Note: this requires that no React hooks are called between the early return and the rest of the component (hooks must not be called conditionally). Since
useMemois currently before the guard, moving it after the guard would violate the rules of hooks if the guard triggers a different return path. This needs careful handling — you may instead keep the current structure and just add an explicit narrowing assertion after the guard.As per coding guidelines, 'Code defensively. Prefer
?? throwErr(...)over non-null assertions, with good error messages explicitly stating the assumption that must've been violated'.🤖 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]/data-vault/stores/[storeId]/page-client.tsx around lines 36 - 54, The code reads `store = config.dataVault.stores[storeId]` before checking existence and declares `modifiedKeys` with `useMemo`, which can leave `store` typed as possibly undefined later; fix by either moving the existence guard (the `if (!(storeId in config.dataVault.stores))` block) above the `store` and `useMemo` declarations so hooks remain in the same order across returns, or keep the current order and add an explicit defensive narrowing immediately after the guard (e.g., throw via a utility if `store` is undefined) so usages later (references to `store`, `modifiedKeys`, `useMemo`) see a non-undefined `store`; ensure you do not introduce conditional hooks when reordering.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsx (1)
186-218: Consider hoistingdesignCardGradientsto module scope.This static mapping doesn't depend on props or state, yet it's recreated on every render of
ChartCard. Moving it outside the component is a small clarity and allocation win.♻️ Proposed change
+const designCardGradients: Record<GradientColor, "blue" | "cyan" | "purple" | "green" | "orange" | "default"> = { + blue: "blue", + purple: "purple", + green: "green", + orange: "orange", + slate: "default", + cyan: "cyan", +}; + export function ChartCard({ children, className, gradientColor = "blue" }: { children: React.ReactNode, className?: string, gradientColor?: GradientColor, }) { - const designCardGradients: Record<GradientColor, "blue" | "cyan" | "purple" | "green" | "orange" | "default"> = { - blue: "blue", - purple: "purple", - green: "green", - orange: "orange", - slate: "default", - cyan: "cyan", - };🤖 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]/(overview)/line-chart.tsx around lines 186 - 218, Hoist the static mapping const designCardGradients out of the component and into module scope so it isn't recreated on every render: move the existing designCardGradients declaration to the top-level of the file (above the component) and leave all references (designCardGradients[gradientColor]) inside the component unchanged; ensure the identifier name remains the same and adjust any imports/exports only if you intend to reuse it across files.
.../src/app/(main)/(protected)/projects/[projectId]/data-vault/stores/[storeId]/page-client.tsx
Show resolved
Hide resolved
.../src/app/(main)/(protected)/projects/[projectId]/data-vault/stores/[storeId]/page-client.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/vercel/page-client.tsx
Show resolved
Hide resolved
- Removed default values from the project details dialog in the project settings page. - Added a globe icon to the endpoints design card in the webhooks page for improved UI clarity.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx (1)
88-101: 🛠️ Refactor suggestion | 🟠 MajorURL constructed via plain string interpolation — use
urlStringorencodeURIComponent.
project.idis interpolated directly into the URL without encoding. Per coding guidelines, URLs should useurlString\`orencodeURIComponent()` to prevent issues with special characters.Proposed fix
const jwksUrl = useMemo( - () => `${baseApiUrl}/api/v1/projects/${project.id}/.well-known/jwks.json`, + () => `${baseApiUrl}/api/v1/projects/${encodeURIComponent(project.id)}/.well-known/jwks.json`, [baseApiUrl, project.id] );As per coding guidelines: "Use urlString`` or encodeURIComponent() instead of normal string interpolation for URLs".
🤖 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]/project-settings/page-client.tsx around lines 88 - 101, The URL construction currently interpolates project.id directly in jwksUrl (and then used by restrictedJwksUrl and allJwksUrl); update these to encode the project id or use the urlString template helper: build jwksUrl using either urlString`...${encodeURIComponent(project.id)}...` or urlString`...${project.id}...` (where urlString handles encoding), then derive restrictedJwksUrl and allJwksUrl from that encoded jwksUrl to ensure special characters in project.id are safely encoded.
🤖 Fix all issues with AI agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx:
- Around line 270-278: SmartFormDialog is missing the defaultValues prop so the
form opens blank; pass the current project values as defaultValues (e.g. map
project.displayName and project.description to the schema field names) when
rendering SmartFormDialog, ensuring you use the same field keys used in
projectInformationSchema and update the defaults when the project data changes
(useMemo or derive from project before passing into SmartFormDialog). Keep the
existing props: open={isProjectDetailsDialogOpen},
onOpenChange={setIsProjectDetailsDialogOpen},
formSchema={projectInformationSchema}, and onSubmit={handleProjectDetailsSubmit}
while adding defaultValues to pre-populate the form.
- Line 263: The rendering uses a boolean fallback that treats empty strings as
missing; change the fallback for project.description to an explicit nullish
check (e.g., use the null/undefined check on project.description or the nullish
coalescing behavior) so that an empty string remains displayed while only
null/undefined becomes "-"; update the JSX that references project.description
in page-client.tsx (the <p> that shows project.description) to use an explicit
check like project.description == null ? "-" : project.description or the
equivalent nullish-coalescing approach.
- Line 30: Replace the three uses of `any` with proper types or a short
justification comment: for the `TeamMemberItem` prop `member` use the SDK type
(e.g., `TeamMember`) instead of `any`; for the `team` variable use the SDK's
`Team` type; for the form `values` replace `any` with `yup.InferType<typeof
projectInformationSchema>` (or the explicit inferred type) — if a precise type
truly cannot be expressed, add a one-line comment next to each usage explaining
why `any` is required and reference the limitation (e.g., runtime shape,
third‑party SDK looseness) so the rationale is recorded.
- Around line 303-321: The dark-mode LogoUpload handlers are inline async
lambdas while the light-mode handlers use useCallback (handleLogoChange,
handleFullLogoChange); make the dark-mode handlers consistent by creating
memoized callbacks (e.g., handleLogoDarkChange and handleFullLogoDarkChange)
using useCallback that call project.update({ logoDarkModeUrl }) and
project.update({ logoFullDarkModeUrl }) respectively, and pass those memoized
functions to the LogoUpload onValueChange props so all four handlers are
implemented consistently.
- Around line 88-101: The URL construction currently interpolates project.id
directly in jwksUrl (and then used by restrictedJwksUrl and allJwksUrl); update
these to encode the project id or use the urlString template helper: build
jwksUrl using either urlString`...${encodeURIComponent(project.id)}...` or
urlString`...${project.id}...` (where urlString handles encoding), then derive
restrictedJwksUrl and allJwksUrl from that encoded jwksUrl to ensure special
characters in project.id are safely encoded.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/webhooks/page-client.tsx:
- Around line 415-425: The status column currently hardcodes "Active"; update
the Endpoint type (the type alias used for rows, extending Svix's EndpointOut)
to include a disabled: boolean field, then change the column with id "status"
(the cell renderer) to read the row's disabled value (e.g.,
row.original.disabled) and render DesignBadge with label "Disabled" when true
and "Active" when false, using an appropriate color (e.g., red for disabled,
green for active) and keeping size "sm".
- Around line 396-439: The columns array (ColumnDef<Endpoint> assigned to
columns) is recreated on every render and closes over props.updateFn and
props.onTestRequested causing unnecessary re-renders of DesignDataTable; fix by
moving the columns definition above the early return and wrapping it in useMemo,
e.g. useMemo(() => [ ...columns... ], [props.updateFn, props.onTestRequested])
so ActionMenu receives stable updateFn/onTestEndpoint references and the table
won’t re-render unnecessarily.
- Around line 344-350: The conditional rendering uses a truthy check for the
variable errorMessage; change it to an explicit null/undefined check (e.g.,
errorMessage != null) so the condition only triggers when errorMessage is not
null/undefined; update the JSX around the status, errorMessage, and DesignAlert
usage in the page-client component to use the explicit check.
🧹 Nitpick comments (5)
🤖 Fix all nitpicks with AI agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx: - Line 263: The rendering uses a boolean fallback that treats empty strings as missing; change the fallback for project.description to an explicit nullish check (e.g., use the null/undefined check on project.description or the nullish coalescing behavior) so that an empty string remains displayed while only null/undefined becomes "-"; update the JSX that references project.description in page-client.tsx (the <p> that shows project.description) to use an explicit check like project.description == null ? "-" : project.description or the equivalent nullish-coalescing approach. - Line 30: Replace the three uses of `any` with proper types or a short justification comment: for the `TeamMemberItem` prop `member` use the SDK type (e.g., `TeamMember`) instead of `any`; for the `team` variable use the SDK's `Team` type; for the form `values` replace `any` with `yup.InferType<typeof projectInformationSchema>` (or the explicit inferred type) — if a precise type truly cannot be expressed, add a one-line comment next to each usage explaining why `any` is required and reference the limitation (e.g., runtime shape, third‑party SDK looseness) so the rationale is recorded. - Around line 303-321: The dark-mode LogoUpload handlers are inline async lambdas while the light-mode handlers use useCallback (handleLogoChange, handleFullLogoChange); make the dark-mode handlers consistent by creating memoized callbacks (e.g., handleLogoDarkChange and handleFullLogoDarkChange) using useCallback that call project.update({ logoDarkModeUrl }) and project.update({ logoFullDarkModeUrl }) respectively, and pass those memoized functions to the LogoUpload onValueChange props so all four handlers are implemented consistently. In `@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/webhooks/page-client.tsx: - Around line 396-439: The columns array (ColumnDef<Endpoint> assigned to columns) is recreated on every render and closes over props.updateFn and props.onTestRequested causing unnecessary re-renders of DesignDataTable; fix by moving the columns definition above the early return and wrapping it in useMemo, e.g. useMemo(() => [ ...columns... ], [props.updateFn, props.onTestRequested]) so ActionMenu receives stable updateFn/onTestEndpoint references and the table won’t re-render unnecessarily. - Around line 344-350: The conditional rendering uses a truthy check for the variable errorMessage; change it to an explicit null/undefined check (e.g., errorMessage != null) so the condition only triggers when errorMessage is not null/undefined; update the JSX around the status, errorMessage, and DesignAlert usage in the page-client component to use the explicit check.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx (3)
263-263: Boolean check onproject.descriptionhides empty strings.
project.description || "-"will display"-"for bothnull/undefinedand an empty string"". If an empty description is a valid state distinct from "no description", prefer an explicit nullish check.- <p className="text-sm text-foreground/80">{project.description || "-"}</p> + <p className="text-sm text-foreground/80">{project.description ?? "-"}</p>As per coding guidelines: "Prefer explicit null/undefinedness checks over boolean checks, eg.
foo == nullinstead of!foo".🤖 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]/project-settings/page-client.tsx at line 263, The rendering uses a boolean fallback that treats empty strings as missing; change the fallback for project.description to an explicit nullish check (e.g., use the null/undefined check on project.description or the nullish coalescing behavior) so that an empty string remains displayed while only null/undefined becomes "-"; update the JSX that references project.description in page-client.tsx (the <p> that shows project.description) to use an explicit check like project.description == null ? "-" : project.description or the equivalent nullish-coalescing approach.
30-30:anytypes used without justification comments.Three places use
anywithout explaining why the type system can't express the correct type:
- Line 30:
member: any- Line 162:
team: any- Line 167:
values: anyPer coding guidelines, when
anyis necessary, a comment should explain why. Ideally, type these properly (e.g., use the SDK'sTeamMember/Teamtypes andyup.InferType<typeof projectInformationSchema>for values).As per coding guidelines: "Try to avoid the
anytype. When necessary, leave a comment explaining why you're using it and why the type system fails there".Also applies to: 162-164, 167-169
🤖 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]/project-settings/page-client.tsx at line 30, Replace the three uses of `any` with proper types or a short justification comment: for the `TeamMemberItem` prop `member` use the SDK type (e.g., `TeamMember`) instead of `any`; for the `team` variable use the SDK's `Team` type; for the form `values` replace `any` with `yup.InferType<typeof projectInformationSchema>` (or the explicit inferred type) — if a precise type truly cannot be expressed, add a one-line comment next to each usage explaining why `any` is required and reference the limitation (e.g., runtime shape, third‑party SDK looseness) so the rationale is recorded.
303-321: Dark-mode logo callbacks are inline while light-mode equivalents are memoized.
handleLogoChangeandhandleFullLogoChangeare wrapped inuseCallback(lines 148–154), but the dark-mode variants at lines 306–308 and 316–318 use inline async lambdas. This is an inconsistency — either memoize all four or none.🤖 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]/project-settings/page-client.tsx around lines 303 - 321, The dark-mode LogoUpload handlers are inline async lambdas while the light-mode handlers use useCallback (handleLogoChange, handleFullLogoChange); make the dark-mode handlers consistent by creating memoized callbacks (e.g., handleLogoDarkChange and handleFullLogoDarkChange) using useCallback that call project.update({ logoDarkModeUrl }) and project.update({ logoFullDarkModeUrl }) respectively, and pass those memoized functions to the LogoUpload onValueChange props so all four handlers are implemented consistently.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/webhooks/page-client.tsx (2)
396-439: Column definitions are recreated on every render.
columnsis defined inline in the render path and capturesprops.updateFn/props.onTestRequestedvia closures. Consider wrapping it inuseMemoto avoid unnecessary re-renders of DesignDataTable.Wrap in useMemo
Move the column definition before the early return and memoize it:
function Endpoints(props: { updateFn: () => void, onTestRequested: (endpoint: Endpoint) => void }) { const endpoints = getSvixResult(useEndpoints({ limit: 100 })); + + const columns: ColumnDef<Endpoint>[] = useMemo(() => [ + // ...column defs unchanged... + ], [props.updateFn, props.onTestRequested]); if (!endpoints.loaded) { return endpoints.rendered; - } else { - const columns: ColumnDef<Endpoint>[] = [ - // ... - ]; - return ( ... ); - } + } + + return ( ... ); }🤖 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]/webhooks/page-client.tsx around lines 396 - 439, The columns array (ColumnDef<Endpoint> assigned to columns) is recreated on every render and closes over props.updateFn and props.onTestRequested causing unnecessary re-renders of DesignDataTable; fix by moving the columns definition above the early return and wrapping it in useMemo, e.g. useMemo(() => [ ...columns... ], [props.updateFn, props.onTestRequested]) so ActionMenu receives stable updateFn/onTestEndpoint references and the table won’t re-render unnecessarily.
344-350: Prefer explicit null check forerrorMessage.Line 344 uses a truthy check on
errorMessage(astring | null). Per project guidelines, prefer explicit null/undefinedness checks.- {status === 'error' && errorMessage && ( + {status === 'error' && errorMessage != null && (🤖 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]/webhooks/page-client.tsx around lines 344 - 350, The conditional rendering uses a truthy check for the variable errorMessage; change it to an explicit null/undefined check (e.g., errorMessage != null) so the condition only triggers when errorMessage is not null/undefined; update the JSX around the status, errorMessage, and DesignAlert usage in the page-client component to use the explicit check.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/webhooks/page-client.tsx
Show resolved
Hide resolved
- Introduced default values for project details in the project settings dialog. - Added a 'disabled' state to webhooks endpoints, improving status visibility in the UI. - Updated the SmartForm component to support default values and handle form resets more effectively.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/webhooks/page-client.tsx (2)
289-300:⚠️ Potential issue | 🟠 Major
sendTestEventcan throw unhandled ifsendTestWebhookrejects.If
stackAdminApp.sendTestWebhookthrows a network error or unexpected exception, the status will remain stuck at'sending'and the user will see no feedback. Consider wrapping in try/catch to set the error state on unexpected failures.Suggested fix
const sendTestEvent = async () => { setStatus('sending'); setErrorMessage(null); + try { const result = await stackAdminApp.sendTestWebhook({ endpointId: props.endpoint.id }); if (result.status === 'ok') { setStatus('success'); return; } setStatus('error'); setErrorMessage(result.error.errorMessage); + } catch (e) { + setStatus('error'); + setErrorMessage(e instanceof Error ? e.message : 'An unexpected error occurred'); + } };Note: the outer
ActionDialogonClickmay catch and display the error, but the local state (status) would still be stuck at'sending', leaving the Cancel button hidden (Line 319) and the UI in a broken state.🤖 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]/webhooks/page-client.tsx around lines 289 - 300, sendTestEvent can throw if stackAdminApp.sendTestWebhook rejects; wrap the await call in a try/catch so any thrown error sets local UI state (call setStatus('error') and setErrorMessage with the caught error message or a fallback) and avoid leaving the status stuck at 'sending'. In the catch block reference stackAdminApp.sendTestWebhook failure, call setStatus('error'), setErrorMessage(error?.message ?? 'Unexpected error'), and return (or ensure finally behavior) so the Cancel button and UI are restored.
381-381:⚠️ Potential issue | 🟡 MinorImport and use
urlStringtemplate tag for the route construction.Per coding guidelines, prefer
urlStringorencodeURIComponent()over plain template literals for URL interpolation. TheurlStringutility is available from@stackframe/stack-shared/dist/utils/urlsand is already used throughout the dashboard for similar navigation patterns.Suggested change
+import { urlString } from "@stackframe/stack-shared/dist/utils/urls"; + - { item: "View Details", onClick: () => router.push(`/projects/${project.id}/webhooks/${props.endpoint.id}`) }, + { item: "View Details", onClick: () => router.push(urlString`/projects/${project.id}/webhooks/${props.endpoint.id}`) },🤖 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]/webhooks/page-client.tsx at line 381, The route is built with a plain template literal; import the urlString template tag from `@stackframe/stack-shared/dist/utils/urls` and use it when calling router.push so path segments are properly encoded. Replace router.push(`/projects/${project.id}/webhooks/${props.endpoint.id}`) with router.push(urlString`/projects/${project.id}/webhooks/${props.endpoint.id}`) and add the corresponding import for urlString at the top of the module; reference symbols: router.push, project.id, props.endpoint.id, and urlString.
🧹 Nitpick comments (8)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/webhooks/page-client.tsx (2)
463-468: Inline empty-array literals ([]) create new references each render.
defaultColumnFilters={[]}anddefaultSorting={[]}produce fresh arrays every render. IfDesignDataTableuses these in dependency arrays or passes them touseReactTableoptions, this can trigger unnecessary re-computations. Hoist them to module-level constants.Suggested fix
At the module level (e.g. near the type definition):
const EMPTY_COLUMN_FILTERS: never[] = []; const EMPTY_SORTING: never[] = [];Then reference them:
- defaultColumnFilters={[]} - defaultSorting={[]} + defaultColumnFilters={EMPTY_COLUMN_FILTERS} + defaultSorting={EMPTY_SORTING}🤖 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]/webhooks/page-client.tsx around lines 463 - 468, The inline empty-array literals passed to DesignDataTable as defaultColumnFilters and defaultSorting create new references each render and can cause unnecessary recomputations; fix by hoisting them to module-level constants (e.g., EMPTY_COLUMN_FILTERS and EMPTY_SORTING) and then passing those constants to the DesignDataTable props defaultColumnFilters and defaultSorting instead of [] so the same reference is reused across renders.
398-447: Column definitions and row data are re-created on every render — consider memoizing.
columnsandendpointRowsare both defined inline in theelsebranch withoutuseMemo, so they produce new references on each render. This can causeDesignDataTable(and its internaluseReactTable) to reset or re-render unnecessarily.Additionally, Line 445 (
description: endpoint.description) is redundant after the...endpointspread.Suggested refactor
Move the column/row definitions above the conditional, wrapping them with
useMemo:function Endpoints(props: { updateFn: () => void, onTestRequested: (endpoint: Endpoint) => void }) { const endpoints = getSvixResult(useEndpoints({ limit: 100 })); + const columns: ColumnDef<Endpoint>[] = useMemo(() => [ + { + accessorKey: "url", + header: "Endpoint URL", + cell: ({ row }) => ( + <span className="text-sm font-medium text-foreground">{row.original.url}</span> + ), + }, + { + accessorKey: "description", + header: "Description", + cell: ({ row }) => ( + row.original.description ? ( + <span className="text-sm text-foreground/80">{row.original.description}</span> + ) : ( + <span className="text-sm text-muted-foreground">-</span> + ) + ), + }, + { + id: "status", + header: "Status", + cell: ({ row }) => ( + <DesignBadge + label={row.original.disabled ? "Disabled" : "Active"} + color={row.original.disabled ? "red" : "green"} + size="sm" + /> + ), + }, + { + id: "actions", + header: "", + cell: ({ row }) => ( + <div className="flex justify-end"> + <ActionMenu + endpoint={row.original} + updateFn={props.updateFn} + onTestEndpoint={props.onTestRequested} + /> + </div> + ), + }, + ], [props.updateFn, props.onTestRequested]); + + const endpointRows: Endpoint[] = useMemo(() => + endpoints.loaded + ? endpoints.data.map((endpoint) => ({ + ...endpoint, + disabled: endpoint.disabled ?? false, + })) + : [], + [endpoints.loaded, endpoints.data] + ); + if (!endpoints.loaded) { return endpoints.rendered; - } else { - const columns: ColumnDef<Endpoint>[] = [ ... ]; - const endpointRows: Endpoint[] = endpoints.data.map((endpoint) => ({ - ...endpoint, - description: endpoint.description, - disabled: endpoint.disabled ?? false, - })); - return ( ... ); + } + + return ( + <DesignCard ...> + <DesignDataTable + data={endpointRows} + columns={columns} + defaultColumnFilters={[]} + defaultSorting={[]} + /> + </DesignCard> + ); }🤖 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]/webhooks/page-client.tsx around lines 398 - 447, The columns and endpointRows are recreated on every render which causes unnecessary re-renders of DesignDataTable/useReactTable; wrap the columns array and the endpointRows mapping in React.useMemo to memoize them (e.g., const columns = useMemo(() => [...], [props.updateFn, props.onTestRequested]) and const endpointRows = useMemo(() => endpoints.data.map(e => ({ ...e, disabled: e.disabled ?? false })), [endpoints.data]) ), remove the redundant description: endpoint.description after the spread, and ensure dependencies include any props or values referenced by the memoized callbacks (like props.updateFn, props.onTestRequested and endpoints.data).apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx (4)
171-174:||used instead of explicit nullish check fordescription.
project.description || undefinedtreats empty strings as falsy and converts them toundefined. If this is intentional (empty string = no description), a comment would help clarify the intent. Otherwise,project.description ?? undefinedis the guideline-preferred pattern.As per coding guidelines, "Prefer explicit null/undefinedness checks over boolean checks, eg.
foo == nullinstead of!foo."🤖 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]/project-settings/page-client.tsx around lines 171 - 174, The use of a boolean-falsy fallback incorrectly converts empty strings to undefined in projectDetailsDefaultValues; update the expression to use a nullish check so only null/undefined become undefined (replace project.description || undefined with a nullish pattern such as project.description ?? undefined), or if empty-string-to-undefined is intentional, add a clarifying comment next to projectDetailsDefaultValues explaining that empty strings should be treated as missing descriptions.
309-327: Dark-mode logo callbacks are inline while their light-mode counterparts are memoized.
handleLogoChangeandhandleFullLogoChange(lines 148-154) are extracted asuseCallbackmemos, but the dark-mode variants on lines 312-314 and 322-324 are defined inline. This inconsistency means the dark-modeLogoUploadcomponents receive a new function reference every render. Consider extracting them for consistency.♻️ Extract memoized callbacks
+ const handleLogoDarkModeChange = useCallback(async (logoDarkModeUrl: string | null) => { + await project.update({ logoDarkModeUrl }); + }, [project]); + + const handleLogoFullDarkModeChange = useCallback(async (logoFullDarkModeUrl: string | null) => { + await project.update({ logoFullDarkModeUrl }); + }, [project]);Then use
onValueChange={handleLogoDarkModeChange}andonValueChange={handleLogoFullDarkModeChange}in the JSX.🤖 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]/project-settings/page-client.tsx around lines 309 - 327, The dark-mode LogoUpload callbacks are defined inline causing new function refs each render; extract them as memoized callbacks like the existing handleLogoChange and handleFullLogoChange by creating useCallback hooks (e.g., handleLogoDarkModeChange and handleLogoFullDarkModeChange) that call project.update({ logoDarkModeUrl }) and project.update({ logoFullDarkModeUrl }) respectively, then pass those handlers as onValueChange to the dark-mode LogoUpload components so the props remain stable across renders.
88-101: URL construction uses raw string interpolation — guideline requiresencodeURIComponentorurlString.
project.idis interpolated directly into the URL template. While project IDs are likely safe identifiers, the coding guideline explicitly requires usingurlStringtagged templates orencodeURIComponent()for URL construction.♻️ Suggested fix
const jwksUrl = useMemo( - () => `${baseApiUrl}/api/v1/projects/${project.id}/.well-known/jwks.json`, + () => `${baseApiUrl}/api/v1/projects/${encodeURIComponent(project.id)}/.well-known/jwks.json`, [baseApiUrl, project.id] );Or use
urlStringif it's available in the project.As per coding guidelines, "Use urlString`` or encodeURIComponent() instead of normal string interpolation for URLs".
🤖 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]/project-settings/page-client.tsx around lines 88 - 101, The three URL builders jwksUrl, restrictedJwksUrl and allJwksUrl currently interpolate project.id directly; update them to encode the project id (or use the project's urlString tagged template if available) so unsafe characters are escaped: replace `${project.id}` with an encoded value via encodeURIComponent(project.id) or use urlString`...${project.id}...` and keep the same dependency arrays ([baseApiUrl, project.id] and [jwksUrl]) so the useMemo hooks still update correctly.
30-30: Multipleanytypes without explanatory comments.
member: any(line 30)team: any(line 162)values: any(line 167)Per guidelines, the
anytype should be avoided, and when necessary a comment should explain why. Forvalues,yup.InferType<typeof projectInformationSchema>would be more precise and provide type safety on theproject.update(values)call. Formemberandteam, consider using the actual SDK types.♻️ Example: type-safe submit handler
- const handleProjectDetailsSubmit = useCallback(async (values: any) => { + const handleProjectDetailsSubmit = useCallback(async (values: yup.InferType<typeof projectInformationSchema>) => { await project.update(values); }, [project]);As per coding guidelines, "Try to avoid the
anytype. When necessary, leave a comment explaining why you're using it and why the type system fails there."Also applies to: 162-163, 167-168
🤖 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]/project-settings/page-client.tsx at line 30, The code uses several any types (member in TeamMemberItem, team, and values) which reduces type safety; replace member and team with the correct SDK types from your project/team SDK (e.g., TeamMember or User types) by updating the TeamMemberItem prop signature and the team variable's type, and change values to a precise type using yup.InferType<typeof projectInformationSchema> so the call to project.update(values) is type-safe; if an SDK type is missing, add a short comment explaining why any is used and reference the intended type.apps/dashboard/src/components/smart-form.tsx (2)
39-52: Dependency array includesprops— callback recreated every render.
propsas a whole is a new object reference every render, so including it in theuseCallbackdeps makes the memoization effectively useless. Consider destructuring the specific props used (props.onChangeIsSubmitting,props.onSubmit) and listing them individually.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/smart-form.tsx` around lines 39 - 52, handleSubmit is being recreated each render because the entire props object is in the useCallback dependency array; instead, destructure the specific callbacks you use (onChangeIsSubmitting and onSubmit) from props and reference those individual variables in the dependency list along with form and resolvedDefaultValues. Update the component to pull const { onChangeIsSubmitting, onSubmit } = props (or function params) and replace props.onChangeIsSubmitting/props.onSubmit calls inside handleSubmit, then change the useCallback deps to [onChangeIsSubmitting, onSubmit, form, resolvedDefaultValues] to restore proper memoization.
54-61:resolvedDefaultValuesis a new object reference every render, causing the effect to fire unnecessarily.
resolvedDefaultValues(line 32) is recomputed as a new object each render, so listing it in the dependency array means this effect runs every render. The guard condition (currentOpen && !prevOpen.current) prevents spurious resets, so this is not a correctness bug, but it's wasteful. Memoizing or stabilizing the defaults would make the intent clearer.♻️ Suggested stabilization
Wrap
resolvedDefaultValuesinuseMemoso the effect (andhandleSubmitcallback) only re-run when the values actually change:- const resolvedDefaultValues = props.defaultValues ?? props.formSchema.getDefault(); + const resolvedDefaultValues = useMemo( + () => props.defaultValues ?? props.formSchema.getDefault(), + [props.defaultValues, props.formSchema] + );Note: this still won't deep-stabilize if
props.defaultValuesis a new object with the same content each render (the caller should memoize). But it avoids the unconditional recompute ofgetDefault()and signals intent.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/smart-form.tsx` around lines 54 - 61, The effect depends on resolvedDefaultValues which is recomputed each render; stabilize it by wrapping the resolvedDefaultValues computation (the variable assigned at line 32) in useMemo so it only changes when its true inputs change (e.g., props.defaultValues or getDefault inputs), and then keep that memoized resolvedDefaultValues in the useEffect dependency array that calls form.reset; also ensure the handleSubmit callback (which uses resolvedDefaultValues) uses the memoized value so both the effect and callback only re-run when the actual defaults change.
🤖 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]/webhooks/page-client.tsx:
- Around line 346-351: The conditional rendering for the error alert currently
checks errorMessage truthiness which hides empty-string errors; update the JSX
condition to explicitly check for null/undefined (e.g., errorMessage != null or
errorMessage !== null && errorMessage !== undefined) so that when status ===
'error' and errorMessage is an empty string the DesignAlert (title "Unable to
send event", component DesignAlert) still renders using
result.error.errorMessage; adjust the expression near the existing status ===
'error' && errorMessage to status === 'error' && errorMessage != null.
---
Outside diff comments:
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/webhooks/page-client.tsx:
- Around line 289-300: sendTestEvent can throw if stackAdminApp.sendTestWebhook
rejects; wrap the await call in a try/catch so any thrown error sets local UI
state (call setStatus('error') and setErrorMessage with the caught error message
or a fallback) and avoid leaving the status stuck at 'sending'. In the catch
block reference stackAdminApp.sendTestWebhook failure, call setStatus('error'),
setErrorMessage(error?.message ?? 'Unexpected error'), and return (or ensure
finally behavior) so the Cancel button and UI are restored.
- Line 381: The route is built with a plain template literal; import the
urlString template tag from `@stackframe/stack-shared/dist/utils/urls` and use it
when calling router.push so path segments are properly encoded. Replace
router.push(`/projects/${project.id}/webhooks/${props.endpoint.id}`) with
router.push(urlString`/projects/${project.id}/webhooks/${props.endpoint.id}`)
and add the corresponding import for urlString at the top of the module;
reference symbols: router.push, project.id, props.endpoint.id, and urlString.
---
Duplicate comments:
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx:
- Around line 275-284: The default values are correctly wired:
projectDetailsDefaultValues (memo defined earlier) is being passed into
SmartFormDialog's defaultValues prop and pre-populates the form; no code change
required—approve the change as-is and ensure SmartFormDialog,
projectDetailsDefaultValues, and handleProjectDetailsSubmit remain unchanged.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/webhooks/page-client.tsx:
- Around line 417-427: Status column was updated to remove the hardcoded
"Active" label; ensure the column definition with id "status" uses
row.original.disabled to conditionally set DesignBadge's label ("Disabled" vs
"Active") and color ("red" vs "green"). Locate the column object (id: "status")
and confirm the cell renderer returns <DesignBadge label={row.original.disabled
? "Disabled" : "Active"} color={row.original.disabled ? "red" : "green"}
size="sm" /> so the UI reflects the disabled field correctly.
---
Nitpick comments:
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx:
- Around line 171-174: The use of a boolean-falsy fallback incorrectly converts
empty strings to undefined in projectDetailsDefaultValues; update the expression
to use a nullish check so only null/undefined become undefined (replace
project.description || undefined with a nullish pattern such as
project.description ?? undefined), or if empty-string-to-undefined is
intentional, add a clarifying comment next to projectDetailsDefaultValues
explaining that empty strings should be treated as missing descriptions.
- Around line 309-327: The dark-mode LogoUpload callbacks are defined inline
causing new function refs each render; extract them as memoized callbacks like
the existing handleLogoChange and handleFullLogoChange by creating useCallback
hooks (e.g., handleLogoDarkModeChange and handleLogoFullDarkModeChange) that
call project.update({ logoDarkModeUrl }) and project.update({
logoFullDarkModeUrl }) respectively, then pass those handlers as onValueChange
to the dark-mode LogoUpload components so the props remain stable across
renders.
- Around line 88-101: The three URL builders jwksUrl, restrictedJwksUrl and
allJwksUrl currently interpolate project.id directly; update them to encode the
project id (or use the project's urlString tagged template if available) so
unsafe characters are escaped: replace `${project.id}` with an encoded value via
encodeURIComponent(project.id) or use urlString`...${project.id}...` and keep
the same dependency arrays ([baseApiUrl, project.id] and [jwksUrl]) so the
useMemo hooks still update correctly.
- Line 30: The code uses several any types (member in TeamMemberItem, team, and
values) which reduces type safety; replace member and team with the correct SDK
types from your project/team SDK (e.g., TeamMember or User types) by updating
the TeamMemberItem prop signature and the team variable's type, and change
values to a precise type using yup.InferType<typeof projectInformationSchema> so
the call to project.update(values) is type-safe; if an SDK type is missing, add
a short comment explaining why any is used and reference the intended type.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/webhooks/page-client.tsx:
- Around line 463-468: The inline empty-array literals passed to DesignDataTable
as defaultColumnFilters and defaultSorting create new references each render and
can cause unnecessary recomputations; fix by hoisting them to module-level
constants (e.g., EMPTY_COLUMN_FILTERS and EMPTY_SORTING) and then passing those
constants to the DesignDataTable props defaultColumnFilters and defaultSorting
instead of [] so the same reference is reused across renders.
- Around line 398-447: The columns and endpointRows are recreated on every
render which causes unnecessary re-renders of DesignDataTable/useReactTable;
wrap the columns array and the endpointRows mapping in React.useMemo to memoize
them (e.g., const columns = useMemo(() => [...], [props.updateFn,
props.onTestRequested]) and const endpointRows = useMemo(() =>
endpoints.data.map(e => ({ ...e, disabled: e.disabled ?? false })),
[endpoints.data]) ), remove the redundant description: endpoint.description
after the spread, and ensure dependencies include any props or values referenced
by the memoized callbacks (like props.updateFn, props.onTestRequested and
endpoints.data).
In `@apps/dashboard/src/components/smart-form.tsx`:
- Around line 39-52: handleSubmit is being recreated each render because the
entire props object is in the useCallback dependency array; instead, destructure
the specific callbacks you use (onChangeIsSubmitting and onSubmit) from props
and reference those individual variables in the dependency list along with form
and resolvedDefaultValues. Update the component to pull const {
onChangeIsSubmitting, onSubmit } = props (or function params) and replace
props.onChangeIsSubmitting/props.onSubmit calls inside handleSubmit, then change
the useCallback deps to [onChangeIsSubmitting, onSubmit, form,
resolvedDefaultValues] to restore proper memoization.
- Around line 54-61: The effect depends on resolvedDefaultValues which is
recomputed each render; stabilize it by wrapping the resolvedDefaultValues
computation (the variable assigned at line 32) in useMemo so it only changes
when its true inputs change (e.g., props.defaultValues or getDefault inputs),
and then keep that memoized resolvedDefaultValues in the useEffect dependency
array that calls form.reset; also ensure the handleSubmit callback (which uses
resolvedDefaultValues) uses the memoized value so both the effect and callback
only re-run when the actual defaults change.
Summary by CodeRabbit
New Features
Refactor
Improvements