Skip to content

Conversation

@aaryan610
Copy link
Member

@aaryan610 aaryan610 commented Jan 9, 2026

Description

This PR improves the profile settings interface-

  1. Introduces a new modal for profile settings instead of the current separate route under workspace slug.
  2. Introduces new redirections-
    • profile/* -> settings/profile/*
    • :workspaceSlug/settings/account/* -> settings/profile/*

Type of Change

  • Improvement (change that would cause existing functionality to not work as expected)

Media

Screen.Recording.2026-01-08.at.18.07.11.mov

Summary by CodeRabbit

  • Refactor

    • Consolidated profile and account settings into a unified, tab-driven settings area and updated navigation/redirects accordingly.
    • Simplified settings layouts and sidebar behavior; many legacy account pages were removed in favor of the new profile-centered structure.
  • New Features

    • Added a profile settings modal, tabbed profile settings pages (general, preferences, notifications, security, activity, API tokens), and a theme/preferences UI with improved controls and timezone/language options.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 9, 2026

📝 Walkthrough

Walkthrough

This PR migrates workspace-scoped account settings into a unified, tab-based profile settings under /settings/profile, removes legacy workspace/account/profile pages and sidebars, adds modal-driven profile settings UI (sidebar + lazy tab content), updates routing/redirects, and introduces related components, types, and store state.

Changes

Cohort / File(s) Summary
Removed Workspace Account Pages
apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/* (e.g., api-tokens/page.tsx, security/page.tsx, preferences/page.tsx, layout.tsx, sidebar.tsx)
Deleted workspace-scoped account settings UI and logic (API tokens, security/password, preferences, layout, sidebar).
Removed Legacy Profile Pages
apps/web/app/(all)/profile/* (e.g., activity/page.tsx, appearance/page.tsx, notifications/page.tsx, page.tsx, sidebar.tsx)
Removed legacy /profile route pages and sidebar components.
New Profile Settings Route
apps/web/app/(all)/settings/profile/[profileTabId]/page.tsx, apps/web/app/(all)/settings/profile/layout.tsx
Adds tab-driven profile settings page and simplified layout for profile settings route.
Profile Settings Components (pages)
apps/web/core/components/settings/profile/content/pages/* (e.g., general/*, preferences/*, notifications/*, security.tsx, activity/*, api-tokens.tsx)
Adds tab-specific components: general form, preferences (theme, language/timezone), notifications form, security password flow, activity list, and API tokens page.
Profile Settings Infrastructure
apps/web/core/components/settings/profile/* (e.g., content/root.tsx, modal.tsx, sidebar/*, sidebar/index.ts)
New modal, content wrapper, sidebar root, header, item categories, and workspace options for modal-driven profile settings.
Lazy page map & content loader
apps/web/core/components/settings/profile/content/pages/index.ts, apps/web/core/components/settings/profile/content/root.tsx
Adds PROFILE_SETTINGS_PAGES_MAP and ProfileSettingsContent to resolve and render lazy tab components.
Global modal integration & layout update
apps/web/ce/components/common/modal/global.tsx, apps/web/app/(all)/[workspaceSlug]/layout.tsx
Adds GlobalModals component and passes workspaceSlug into WorkspaceLayout for modal rendering.
Routing & Redirects
apps/web/app/routes/core.ts, apps/web/app/routes/redirects/core/*.tsx
Removes workspace account route group, adds redirects from legacy workspace/account paths to /settings/profile/*, adapts API tokens redirect to fixed profile path.
Store changes
apps/web/core/store/base-command-palette.store.ts
Adds profileSettingsModal observable and toggleProfileSettingsModal action to manage modal open/close and active tab.
Settings UI primitives & refactors
apps/web/core/components/settings/*, apps/web/core/components/appearance/*, apps/web/core/components/profile/*, apps/web/ce/components/preferences/*
Adds SettingsControlItem, SettingsSidebarItem, refactors headings, replaces PreferencesSection usage, updates theme/timezone/select components, and removes legacy preference components.
User menu & top nav changes
apps/web/core/components/workspace/sidebar/user-menu-root.tsx, apps/web/ce/components/navigations/top-navigation-root.tsx
User menu now triggers profile settings modal (toggleProfileSettingsModal); removed size prop on UserMenuRoot invocation.
Constants & types
packages/constants/src/profile.ts, packages/constants/src/settings.ts, packages/types/src/settings.ts, packages/types/src/index.ts
Adds TProfileSettingsTabs, reworks PROFILE_SETTINGS and PROFILE_SETTINGS_TABS, removes PROFILE_ACTION_LINKS, and re-exports settings types.
Misc. content & link updates
apps/web/core/layouts/auth-layout/workspace-wrapper.tsx, apps/web/core/components/home/widgets/empty-states/no-projects.tsx
Updates CTAs/links to point to new profile settings paths and create-workspace; minor UI link adjustments.

Sequence Diagram

sequenceDiagram
    participant User
    participant TopNav
    participant UserMenu
    participant Store as CommandPaletteStore
    participant GlobalModals
    participant ProfileModal
    participant Sidebar
    participant Content
    participant TabComponent

    User->>TopNav: open user menu
    TopNav->>UserMenu: render menu
    User->>UserMenu: click "Settings" / "Preferences"
    UserMenu->>Store: toggleProfileSettingsModal({ isOpen: true, activeTab: "general" })
    Store->>GlobalModals: state: profileSettingsModal.isOpen = true
    GlobalModals->>ProfileModal: render modal
    ProfileModal->>Sidebar: render with activeTab
    ProfileModal->>Content: render with activeTab
    Content->>TabComponent: lazy load component for activeTab
    TabComponent->>Content: fetch/display data (user/profile)
    User->>Sidebar: click another tab
    Sidebar->>Store: toggleProfileSettingsModal({ activeTab: "preferences" })
    Store->>Content: update activeTab -> Content re-resolves TabComponent
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Poem

🐰 Hopping through tabs both near and far,

Modal pops open — a bright little star.
Old paths retire, new routes take flight,
Profile settings gathered, tucked in tight.
I nudge a token, change theme with a grin,
A rabbit’s small cheer for the cleaner new din!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'improved profile settings interface' is vague and generic; it doesn't clearly convey the main changes (restructuring from workspace routes to a modal-based settings system with redirects). Clarify the title to reflect the core change: e.g., 'Restructure profile settings to use modal and add profile/* redirects' or 'Move profile settings to modal-based interface with new redirects'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description covers the key changes (modal introduction, redirections) but is incomplete; it lacks details on test scenarios and doesn't fully document the scope of the refactoring (file removals, new components).

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

✨ Finishing touches
  • 📝 Generate docstrings

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

❤️ Share

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

@makeplane
Copy link

makeplane bot commented Jan 9, 2026

Linked to Plane Work Item(s)

This comment was auto-generated by Plane

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR improves the profile settings interface by replacing the separate route-based profile settings with a modal-based approach. This change provides a more modern and accessible user experience by allowing users to access their profile settings from anywhere in the application.

Key Changes:

  • Introduced a new profile settings modal accessible via command palette store
  • Migrated all profile settings pages (general, preferences, activity, notifications, security, api-tokens) to modal-compatible components
  • Added redirects from old routes (profile/* and :workspaceSlug/settings/account/*) to new routes (settings/profile/*)

Reviewed changes

Copilot reviewed 70 out of 70 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
packages/types/src/settings.ts Added new TProfileSettingsTabs type definition for profile settings tabs
packages/constants/src/settings.ts Updated profile settings constants with proper typing and removed href/highlight properties
packages/constants/src/profile.ts Refactored profile settings structure, removed navigation-specific properties
apps/web/core/store/base-command-palette.store.ts Added profile settings modal state management
apps/web/core/components/workspace/sidebar/user-menu-root.tsx Updated user menu to trigger profile settings modal instead of navigation
apps/web/core/components/settings/profile/* New modal-based profile settings components (modal, sidebar, content pages)
apps/web/core/layouts/auth-layout/workspace-wrapper.tsx Updated profile link to new settings route
apps/web/app/routes/redirects/core/* Added redirect routes for backward compatibility
apps/web/app/(all)/settings/profile/* New route-based profile settings pages
apps/web/app/(all)/[workspaceSlug]/layout.tsx Added GlobalModals component
Multiple deleted files Removed old profile settings components and routes
Comments suppressed due to low confidence (1)

apps/web/core/components/settings/profile/content/pages/security.tsx:99

  • The error handling logic has been improved to properly check if error is an instance of Error before accessing error_code. However, the errorInfo variable is initialized inside a conditional block, which could lead to accessing undefined errorInfo if the error is not an Error instance. Consider initializing errorInfo with a default value or ensuring the toast always has a fallback message.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +34 to +63
const { profileTabId } = useParams();
// translation
const { t } = useTranslation();

return (
<div className="mt-4 flex flex-col gap-y-4">
{PROFILE_SETTINGS_CATEGORIES.map((category) => {
const categoryItems = GROUPED_PROFILE_SETTINGS[category];

if (categoryItems.length === 0) return null;

return (
<div key={category} className="shrink-0">
<div className="p-2 text-caption-md-medium text-tertiary capitalize">{t(category)}</div>
<div className="flex flex-col">
{categoryItems.map((item) => (
<SettingsSidebarItem
key={item.key}
as="button"
onClick={() => updateActiveTab(item.key)}
isActive={activeTab === item.key}
icon={ICONS[item.key]}
label={t(item.i18n_label)}
/>
))}
</div>
</div>
);
})}
{profileTabId && <ProfileSettingsSidebarWorkspaceOptions />}
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The profileTabId parameter is used in the conditional render at line 63 to show/hide ProfileSettingsSidebarWorkspaceOptions, but this parameter is coming from useParams() which pulls from the route. Since this is in a modal context (ProfileSettingsSidebarItemCategories), the profileTabId from route params may not be reliable or expected. Consider passing this as a prop instead or clarifying the intended behavior.

Copilot uses AI. Check for mistakes.
updateEmptyState: (state: boolean) => void;
};

export const ActivityProfileSettingsList = observer(function ProfileActivityListPage(props: Props) {
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The function name ActivityProfileSettingsList in the observer wrapper doesn't match the parameter name in the function definition (ProfileActivityListPage). While both refer to the same component, this inconsistency could cause confusion. Consider using consistent naming.

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +34
if (!currentUser || !isAValidTab)
return (
<div className="size-full grid place-items-center px-4">
<LogoSpinner />
</div>
);

Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The component returns null when currentUser is not available or when isAValidTab is false, but there's no loading state differentiation. When the tab is invalid, the user sees a loading spinner which is misleading. Consider showing different UI for invalid tabs vs loading states, or redirecting to a valid tab.

Suggested change
if (!currentUser || !isAValidTab)
return (
<div className="size-full grid place-items-center px-4">
<LogoSpinner />
</div>
);
if (!currentUser)
return (
<div className="size-full grid place-items-center px-4">
<LogoSpinner />
</div>
);
if (!isAValidTab)
return (
<div className="size-full grid place-items-center px-4">
<p className="text-muted-foreground">Invalid profile settings tab.</p>
</div>
);

Copilot uses AI. Check for mistakes.
console.error("Error updating theme:", error);
}
},
[updateUserTheme]
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The useCallback dependency array includes only updateUserTheme but the callback also uses setTheme. While setTheme from next-themes is stable, for completeness and consistency with React best practices, it should be included in the dependency array.

Suggested change
[updateUserTheme]
[updateUserTheme, setTheme]

Copilot uses AI. Check for mistakes.
export const clientLoader = ({ params, request }: Route.ClientLoaderArgs) => {
const searchParams = new URL(request.url).searchParams;
const splat = params["*"] || "";
throw redirect(`/settings/profile/${splat || "general"}?${searchParams.toString()}`);
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The redirect preserves query parameters using searchParams.toString(), but if there are no search parameters, this will append an empty ? to the URL (e.g., /settings/profile/general?). Consider only appending the search params if they exist: const search = searchParams.toString(); throw redirect(\/settings/profile/${splat || "general"}${search ? `?${search}` : ""}`);`

Suggested change
throw redirect(`/settings/profile/${splat || "general"}?${searchParams.toString()}`);
const search = searchParams.toString();
throw redirect(
`/settings/profile/${splat || "general"}${search ? `?${search}` : ""}`,
);

Copilot uses AI. Check for mistakes.
export const clientLoader = ({ params, request }: Route.ClientLoaderArgs) => {
const searchParams = new URL(request.url).searchParams;
const splat = params["*"] || "";
throw redirect(`/settings/profile/${splat || "general"}?${searchParams.toString()}`);
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

Same issue as workspace-account-settings.tsx - the redirect may append an empty ? when there are no search parameters. Consider conditionally appending search params.

Suggested change
throw redirect(`/settings/profile/${splat || "general"}?${searchParams.toString()}`);
const query = searchParams.toString();
const target = query
? `/settings/profile/${splat || "general"}?${query}`
: `/settings/profile/${splat || "general"}`;
throw redirect(target);

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +28
const isAValidTab = PROFILE_SETTINGS_TABS.includes(profileTabId as TProfileSettingsTabs);

if (!currentUser || !isAValidTab)
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The variable isAValidTab uses "isA" prefix which is unconventional. Typically, boolean variables use "is" prefix followed by an adjective/noun. Consider renaming to isValidTab for better readability and consistency with naming conventions.

Suggested change
const isAValidTab = PROFILE_SETTINGS_TABS.includes(profileTabId as TProfileSettingsTabs);
if (!currentUser || !isAValidTab)
const isValidTab = PROFILE_SETTINGS_TABS.includes(profileTabId as TProfileSettingsTabs);
if (!currentUser || !isValidTab)

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +27
setTimeout(() => {
toggleProfileSettingsModal({
activeTab: null,
});
}, 300);
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The setTimeout delay of 300ms is used to reset the activeTab after closing the modal. However, this magic number should be documented or made into a constant explaining why this specific delay is needed (likely to match animation duration). Consider extracting this as a named constant like MODAL_CLOSE_ANIMATION_DURATION = 300.

Copilot uses AI. Check for mistakes.
Comment on lines +163 to +167
updateUserAndProfile
.then(() => {
return;
})
.catch(() => {});
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The promise is being created but then immediately followed with .then(() => { return; }) which doesn't add any value. The empty return statement inside the then block is redundant. This can be simplified to just updateUserAndProfile.catch(() => {}); or removed entirely since setPromiseToast already handles the promise.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/web/app/(all)/settings/profile/layout.tsx (1)

7-21: Potential non-scrollable settings content due to overflow-hidden everywhere.

With the current nesting, longer profile pages can end up clipped unless every child handles its own scrolling. If this layout is meant to own scrolling, consider making the Outlet wrapper scrollable and adding min-h-0 for correct flex overflow.

Suggested adjustment
-        <div className="relative flex size-full overflow-hidden bg-canvas p-2">
-          <main className="relative flex flex-col size-full overflow-hidden bg-surface-1 rounded-lg border border-subtle">
-            <div className="size-full overflow-hidden">
+        <div className="relative flex size-full overflow-hidden bg-canvas p-2">
+          <main className="relative flex flex-col size-full min-h-0 overflow-hidden bg-surface-1 rounded-lg border border-subtle">
+            <div className="size-full min-h-0 overflow-auto">
               <Outlet />
             </div>
           </main>
         </div>
apps/web/core/components/profile/sidebar.tsx (1)

66-79: Missing dependency in useEffect may cause stale closure.

The effect references profileSidebarCollapsed and toggleProfileSidebar but has an empty dependency array. This can cause the handleToggleProfileSidebar function to capture stale values of profileSidebarCollapsed.

Proposed fix
   useEffect(() => {
     const handleToggleProfileSidebar = () => {
       if (window && window.innerWidth < 768) {
         toggleProfileSidebar(true);
       }
       if (window && profileSidebarCollapsed && window.innerWidth >= 768) {
         toggleProfileSidebar(false);
       }
     };

     window.addEventListener("resize", handleToggleProfileSidebar);
     handleToggleProfileSidebar();
     return () => window.removeEventListener("resize", handleToggleProfileSidebar);
-  }, []);
+  }, [profileSidebarCollapsed, toggleProfileSidebar]);
apps/web/core/store/base-command-palette.store.ts (1)

103-116: Add profileSettingsModal.isOpen to getCoreModalsState().

The profileSettingsModal is part of the command palette store with dedicated toggle functionality, but it's not tracked in getCoreModalsState(). This method is used to compute isAnyModalOpen which determines focus management and modal closing behavior in peek-overview components (e.g., preventing unwanted closure while any modal is open). Without including profileSettingsModal.isOpen, the focus management logic may behave incorrectly when the profile settings modal is active.

🤖 Fix all issues with AI agents
In @apps/web/app/routes/redirects/core/api-tokens.tsx:
- Around line 3-4: clientLoader currently throws a hardcoded redirect and drops
query params; change its signature to accept ({ request }:
Route.ClientLoaderArgs) and preserve search parameters by building the target
with the incoming URL's search (e.g., const url = new URL(request.url); throw
redirect(`/settings/profile/api-tokens${url.search}`)); also remove the trailing
slash in the target to match other redirects.

In @apps/web/core/components/profile/sidebar.tsx:
- Line 36: The destructured userId from useParams() is string | string[]; before
the comparison currentUser?.id === userId in this file/component, explicitly
narrow or convert userId to a string (e.g., call .toString() or handle array
case like other profile components) or assert it as string so the types match;
update the comparison to use the normalized string variable (mirroring the
approach in profile-issues-filter.tsx).

In
@apps/web/core/components/settings/profile/content/pages/activity/activity-list.tsx:
- Around line 33-42: The SWR cache key for useSWR only includes cursor but the
fetcher userService.getUserActivity also depends on perPage, causing stale
results when perPage changes; update the USER_ACTIVITY key generator to accept
per_page and call useSWR with USER_ACTIVITY({ cursor, per_page: perPage }) (and
ensure the fetcher call uses the same per_page param), so the key uniquely
reflects both cursor and perPage and invalidates cache correctly for
userProfileActivity.

In @apps/web/core/components/settings/profile/content/pages/general/form.tsx:
- Around line 202-218: The img inside the clickable avatar button is given its
own onClick and role="button", creating nested button semantics; remove the
onClick and role attributes from the img (the parent button already calls
setIsImageUploadModalOpen(true)), leaving the img as a plain image using
src={getFileURL(userAvatar)} and alt={currentUser?.display_name} so clicks are
handled by the outer button and HTML remains valid.
- Around line 146-147: Both updateCurrentUserDetail and updateCurrentUserProfile
attach .finally(() => setIsLoading(false)) causing duplicate state updates when
awaited together; remove the per-promise finally handlers and instead call
setIsLoading(false) once in the finally of the combined Promise.all (or after
awaiting Promise.all of updateCurrentUser(userPayload) and
updateUserProfile(profilePayload)) so setIsLoading is reset only once; update
references: updateCurrentUserDetail, updateCurrentUserProfile,
updateCurrentUser, updateUserProfile, setIsLoading, and Promise.all.

In
@apps/web/core/components/settings/profile/content/pages/notifications/email-notification-form.tsx:
- Around line 29-46: The toggle is being updated optimistically and not reverted
on API failure; modify handleSettingChange (and each caller that does
onChange(newValue)) to capture the previous value before calling onChange, call
userService.updateCurrentUserEmailNotificationSettings as now, and in the catch
block call onChange(previousValue) (or otherwise restore the original boolean)
so the UI is reverted when updateCurrentUserEmailNotificationSettings fails;
ensure you reference the same key (key: keyof IUserEmailNotificationSettings)
when reverting so the correct toggle is restored.

In @apps/web/core/components/settings/profile/content/pages/security.tsx:
- Around line 12-13: The import for SettingsHeading is using default-import
syntax but SettingsHeading is a named export; change the import statement to use
a named import (e.g., import { SettingsHeading } from
"@/components/settings/heading") so the component resolves correctly; update the
import line at the top of the file where SettingsHeading is currently imported
and verify usages of SettingsHeading remain unchanged.

In @apps/web/core/components/settings/profile/content/root.tsx:
- Around line 26-28: The Suspense wrapper around PageComponent is missing a
fallback, causing a blank area during lazy loading; update the Suspense usage to
provide a fallback UI (e.g., a spinner or Skeleton component) by supplying the
fallback prop to Suspense so that a loading indicator is rendered while
PageComponent loads, and reuse or create a lightweight Loading/Skeleton
component if needed to keep UX consistent.

In @apps/web/core/components/settings/profile/sidebar/header.tsx:
- Around line 8-30: ProfileSettingsSidebarHeader currently calls
getFileURL(currentUser?.avatar_url ?? "") and renders names via
{currentUser?.first_name} {currentUser?.last_name}, which can call
getFileURL("") and show "undefined undefined"; change to pass undefined to
getFileURL when no avatar (e.g., getFileURL(currentUser?.avatar_url)) and render
a safe name fallback using display_name or a single-field fallback (e.g., use
currentUser?.display_name ?? currentUser?.first_name ?? currentUser?.last_name
or join available parts) so Avatar receives a proper src/name and the UI never
shows "undefined undefined".

In @apps/web/core/components/settings/profile/sidebar/workspace-options.tsx:
- Around line 21-29: The workspace items always pass isActive={false} so the
current workspace is never highlighted; update the mapping in
workspace-options.tsx to determine isActive by comparing each workspace.slug to
the active workspace slug (e.g., accept a workspaceSlug prop on the component or
read it from the store/hook used by the app) and pass isActive={workspace.slug
=== workspaceSlug} into SettingsSidebarItem (keep the same keys and props like
iconNode, href, label). Ensure workspaceSlug is provided to this component or
obtained via the appropriate hook before the map.

In @apps/web/core/components/workspace/sidebar/user-menu-root.tsx:
- Around line 117-128: The "Preferences" menu item is hardcoded; update the
CustomMenu.MenuItem that calls toggleProfileSettingsModal (activeTab:
"preferences") to use the translation function t() instead of the literal
"Preferences" so it matches other items (e.g., t("settings"), t("sign_out")) —
replace the string "Preferences" with t("preferences") (or the correct key used
in your i18n files) inside the MenuItem rendering.
- Around line 29-30: Replace the hardcoded const isUserInstanceAdmin = false
with a real call to the existing API/service: call
currentUserInstanceAdminStatus() (or wire it into the user store) and use its
boolean result for isUserInstanceAdmin; fetch the status asynchronously (e.g.,
useEffect or the store's initialization) and handle loading/errors so the God
Mode menu item visibility in user-menu-root.tsx is driven by the returned
boolean rather than a constant.
🧹 Nitpick comments (19)
apps/web/core/components/home/widgets/empty-states/no-projects.tsx (1)

87-113: CTA link update looks right; verify icon link flow and consider removing unnecessary template literal

link: \/settings/profile/general`matches the move to global profile settings, but the avatar/namestill targets/${workspaceSlug}/profile/${currentUser?.id}`—please confirm that’s still intended (or that the redirect lands users in the same new modal/settings experience). Also, since there’s no interpolation, prefer a plain string.

Proposed tweak
-        link: `/settings/profile/general`,
+        link: "/settings/profile/general",
packages/types/src/settings.ts (1)

1-1: Consider deriving the union from an exported const list to avoid drift (type vs runtime).

If these tab ids are also used to render UI or validate params, having a single runtime source-of-truth helps.

Proposed refactor
-export type TProfileSettingsTabs = "general" | "preferences" | "activity" | "notifications" | "security" | "api-tokens";
+export const PROFILE_SETTINGS_TABS = [
+  "general",
+  "preferences",
+  "activity",
+  "notifications",
+  "security",
+  "api-tokens",
+] as const;
+
+export type TProfileSettingsTabs = (typeof PROFILE_SETTINGS_TABS)[number];
apps/web/core/components/global/timezone-select.tsx (1)

34-49: Consider deterministic border class selection for clarity.

The placement="bottom-end" prop is valid and fully supported by CustomSearchSelect. For better readability, use a ternary operator to explicitly select between border colors rather than conditionally adding classes:

Suggested refactoring
-        buttonClassName={cn(buttonClassName, "border border-subtle-1", {
-          "border-danger-strong": error,
-        })}
+        buttonClassName={cn(
+          "border",
+          error ? "border-danger-strong" : "border-subtle-1",
+          buttonClassName
+        )}
apps/web/core/components/settings/sidebar/item.tsx (2)

1-6: Import organization doesn't match the comment grouping.

The // plane imports comment on line 5 suggests grouping, but cn from @plane/utils appears before the type imports from lucide-react and @plane/propel/icons. Consider reordering for consistency.

Suggested import order
 import React from "react";
 import Link from "next/link";
+import type { LucideIcon } from "lucide-react";
 // plane imports
+import type { ISvgIcons } from "@plane/propel/icons";
 import { cn } from "@plane/utils";
-import type { LucideIcon } from "lucide-react";
-import type { ISvgIcons } from "@plane/propel/icons";

30-39: Inconsistent icon wrapper between icon and iconNode branches.

When using the icon prop, the icon is wrapped in a size-4 container with centering. However, when using iconNode, it's rendered directly without any wrapper, which may cause visual inconsistency.

Proposed fix for consistent icon rendering
   const content = (
     <>
       {"icon" in props ? (
         <span className="shrink-0 size-4 grid place-items-center">{<props.icon className="size-3.5" />}</span>
       ) : (
-        props.iconNode
+        <span className="shrink-0 size-4 grid place-items-center">{props.iconNode}</span>
       )}
       <span className="truncate">{label}</span>
     </>
   );
apps/web/core/components/settings/profile/content/pages/general/form.tsx (3)

27-39: Avoid using any type for cover_image_asset.

The cover_image_asset field is typed as any, which bypasses TypeScript's type checking. Consider using a more specific type or unknown if the shape is truly dynamic.

 type TUserProfileForm = {
   avatar_url: string;
   cover_image: string;
-  cover_image_asset: any;
+  cover_image_asset: unknown;
   cover_image_url: string;
   // ...
 };

163-167: Remove empty .then() and .catch() handlers.

These handlers don't perform any meaningful operations and add unnecessary code. If setPromiseToast already handles success/error feedback, these can be removed.


86-110: Mixed async/await with .then()/.catch() pattern.

The function uses await but then chains .then() and .catch(). Consider using consistent try/catch for better readability.

Refactored with consistent async/await
   const handleProfilePictureDelete = async (url: string | null | undefined) => {
     if (!url) return;
-    await updateCurrentUser({
-      avatar_url: "",
-    })
-      .then(() => {
-        setToast({
-          type: TOAST_TYPE.SUCCESS,
-          title: "Success!",
-          message: "Profile picture deleted successfully.",
-        });
-        setValue("avatar_url", "");
-        return;
-      })
-      .catch(() => {
-        setToast({
-          type: TOAST_TYPE.ERROR,
-          title: "Error!",
-          message: "There was some error in deleting your profile picture. Please try again.",
-        });
-      })
-      .finally(() => {
-        setIsImageUploadModalOpen(false);
-      });
+    try {
+      await updateCurrentUser({ avatar_url: "" });
+      setToast({
+        type: TOAST_TYPE.SUCCESS,
+        title: "Success!",
+        message: "Profile picture deleted successfully.",
+      });
+      setValue("avatar_url", "");
+    } catch {
+      setToast({
+        type: TOAST_TYPE.ERROR,
+        title: "Error!",
+        message: "There was some error in deleting your profile picture. Please try again.",
+      });
+    } finally {
+      setIsImageUploadModalOpen(false);
+    }
   };
apps/web/core/components/settings/profile/content/pages/activity/activity-list.tsx (3)

59-59: Avoid any type for activity items.

The activityItem parameter is typed as any, which disables type checking. Consider defining or importing a proper type for user activity items.

-          {userProfileActivity.results.map((activityItem: any) => {
+          {userProfileActivity.results.map((activityItem) => {

If the userService.getUserActivity response is properly typed, the inference should work. Otherwise, define an IUserActivity type.


54-54: Acknowledge the TODO for future refactoring.

The TODO comment indicates awareness that this component needs refactoring. Consider creating a tracking issue if one doesn't exist.

Would you like me to open a new issue to track the refactoring of this component?


81-83: Unusual !text-20 class pattern.

The !text-20 class uses the !important modifier prefix. This typically indicates a specificity issue. Consider reviewing whether the base styles can be adjusted instead.

apps/web/ce/components/common/modal/global.tsx (1)

10-12: Clarify or remove unused workspaceSlug prop.

The TGlobalModalsProps type defines workspaceSlug, but it's never used in the component (indicated by the _props parameter name). If this prop is for future extensibility when more workspace-scoped modals are added, consider adding a JSDoc comment explaining this. Otherwise, remove the unused prop from the type definition to keep the interface clean.

♻️ Suggested cleanup if not needed for future use
-type TGlobalModalsProps = {
-  workspaceSlug: string;
-};
-
 /**
  * GlobalModals component manages all workspace-level modals across Plane applications.
  *
  * This includes:
  * - Profile settings modal
  */
-export const GlobalModals = observer(function GlobalModals(_props: TGlobalModalsProps) {
+export const GlobalModals = observer(function GlobalModals() {
   return (
     <Suspense fallback={null}>
       <ProfileSettingsModal />
     </Suspense>
   );
 });
apps/web/app/(all)/[workspaceSlug]/layout.tsx (1)

17-17: Unused prop: workspaceSlug passed to GlobalModals.

Looking at the GlobalModals implementation in apps/web/ce/components/common/modal/global.tsx, the component signature uses _props (underscore prefix), indicating the prop is intentionally unused. Consider removing this prop to avoid confusion and keep the interface clean.

♻️ Proposed refactor to remove unused prop
-            <GlobalModals workspaceSlug={workspaceSlug} />
+            <GlobalModals />

You'll also need to update the GlobalModals component interface accordingly.

apps/web/core/components/settings/profile/modal.tsx (2)

19-28: Fragile animation timing with magic number.

The 300ms delay in setTimeout is hardcoded and tightly coupled to the modal's close animation duration. If the animation timing changes in ModalCore, this will break or cause visual glitches.

Consider:

  1. Extracting the duration as a named constant (e.g., MODAL_CLOSE_ANIMATION_DURATION)
  2. Using the same constant in both ModalCore's transition and this timeout
  3. Or handle the state reset via an onAnimationComplete callback if available
♻️ Example refactor with named constant
+const MODAL_CLOSE_ANIMATION_DURATION = 300;
+
 const handleClose = useCallback(() => {
   toggleProfileSettingsModal({
     isOpen: false,
   });
   setTimeout(() => {
     toggleProfileSettingsModal({
       activeTab: null,
     });
-  }, 300);
+  }, MODAL_CLOSE_ANIMATION_DURATION);
 }, [toggleProfileSettingsModal]);

36-36: Fixed height may not be responsive.

The modal uses a fixed height of h-175 (700px). This could cause issues on smaller screens or different viewport sizes.

Consider using max-h-175 instead to allow the modal to shrink on smaller screens while maintaining a maximum height on larger displays.

-      className="h-175"
+      className="max-h-[90vh]"
apps/web/core/components/settings/profile/content/pages/preferences/language-and-timezone-list.tsx (2)

25-57: Use translation keys for toast messages.

The toast messages in handleTimezoneChange and handleLanguageChange are hardcoded in English (e.g., "Success!", "Timezone updated successfully"), while the rest of the component uses t() for translations. This creates i18n inconsistency.

Proposed fix using translations
     const handleTimezoneChange = async (value: string) => {
       try {
         await updateCurrentUser({ user_timezone: value });
         setToast({
-          title: "Success!",
-          message: "Timezone updated successfully",
+          title: t("success"),
+          message: t("timezone_updated_successfully"),
           type: TOAST_TYPE.SUCCESS,
         });
       } catch (_error) {
         setToast({
-          title: "Error!",
-          message: "Failed to update timezone",
+          title: t("error"),
+          message: t("failed_to_update_timezone"),
           type: TOAST_TYPE.ERROR,
         });
       }
     };

     const handleLanguageChange = async (value: string) => {
       try {
         await updateUserProfile({ language: value });
         setToast({
-          title: "Success!",
-          message: "Language updated successfully",
+          title: t("success"),
+          message: t("language_updated_successfully"),
           type: TOAST_TYPE.SUCCESS,
         });
       } catch (_error) {
         setToast({
-          title: "Error!",
-          message: "Failed to update language",
+          title: t("error"),
+          message: t("failed_to_update_language"),
           type: TOAST_TYPE.ERROR,
         });
       }
     };

93-98: Use translation keys for StartOfWeekPreference strings.

The title and description props are hardcoded English strings, inconsistent with the translated t("timezone") and t("language") used elsewhere in this component.

Proposed fix
         <StartOfWeekPreference
           option={{
-            title: "First day of the week",
-            description: "This will change how all calendars in your app look.",
+            title: t("first_day_of_the_week"),
+            description: t("first_day_of_the_week_description"),
           }}
         />
apps/web/core/components/appearance/theme-switcher.tsx (1)

35-56: Use translation keys for toast messages and add setTheme to dependencies.

  1. Toast messages are hardcoded in English while t() is available for translations.
  2. setTheme is used inside the callback but not listed in dependencies.
Proposed fix
   const handleThemeChange = useCallback(
     (themeOption: I_THEME_OPTION) => {
       try {
         setTheme(themeOption.value);
         const updatePromise = updateUserTheme({ theme: themeOption.value });
         setPromiseToast(updatePromise, {
-          loading: "Updating theme...",
+          loading: t("updating_theme"),
           success: {
-            title: "Success!",
-            message: () => "Theme updated successfully!",
+            title: t("success"),
+            message: () => t("theme_updated_successfully"),
           },
           error: {
-            title: "Error!",
-            message: () => "Failed to update the theme",
+            title: t("error"),
+            message: () => t("failed_to_update_theme"),
           },
         });
       } catch (error) {
         console.error("Error updating theme:", error);
       }
     },
-    [updateUserTheme]
+    [setTheme, updateUserTheme, t]
   );
apps/web/app/(all)/settings/profile/[profileTabId]/page.tsx (1)

28-33: Consider separating loading state from invalid tab handling.

Currently, both !currentUser (loading) and !isAValidTab (invalid route) render the same LogoSpinner. This makes it impossible to distinguish between a user still loading and an invalid tab URL. For invalid tabs, consider redirecting to a default tab or showing an error state instead.

Proposed approach
+  // Handle loading state
+  if (!currentUser)
+    return (
+      <div className="size-full grid place-items-center px-4">
+        <LogoSpinner />
+      </div>
+    );
+
+  // Redirect invalid tabs to default
+  if (!isAValidTab) {
+    router.replace("/settings/profile/general");
+    return (
+      <div className="size-full grid place-items-center px-4">
+        <LogoSpinner />
+      </div>
+    );
+  }
-  if (!currentUser || !isAValidTab)
-    return (
-      <div className="size-full grid place-items-center px-4">
-        <LogoSpinner />
-      </div>
-    );

Comment on lines +3 to +4
export const clientLoader = () => {
throw redirect(`/settings/profile/api-tokens/`);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Inconsistent redirect pattern may lose search parameters.

Other redirect handlers in this PR (profile-settings.tsx, workspace-account-settings.tsx) preserve and forward search parameters using Route.ClientLoaderArgs with request.url. This simplified version doesn't forward any query parameters.

🔧 Proposed fix to preserve search parameters
-export const clientLoader = () => {
-  throw redirect(`/settings/profile/api-tokens/`);
+export const clientLoader = ({ request }: Route.ClientLoaderArgs) => {
+  const searchParams = new URL(request.url).searchParams;
+  throw redirect(`/settings/profile/api-tokens?${searchParams.toString()}`);
 };

Also note: The trailing slash in the redirect path may be unnecessary and inconsistent with other redirects in this PR.

📝 Committable suggestion

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

Suggested change
export const clientLoader = () => {
throw redirect(`/settings/profile/api-tokens/`);
export const clientLoader = ({ request }: Route.ClientLoaderArgs) => {
const searchParams = new URL(request.url).searchParams;
throw redirect(`/settings/profile/api-tokens?${searchParams.toString()}`);
};
🤖 Prompt for AI Agents
In @apps/web/app/routes/redirects/core/api-tokens.tsx around lines 3 - 4,
clientLoader currently throws a hardcoded redirect and drops query params;
change its signature to accept ({ request }: Route.ClientLoaderArgs) and
preserve search parameters by building the target with the incoming URL's search
(e.g., const url = new URL(request.url); throw
redirect(`/settings/profile/api-tokens${url.search}`)); also remove the trailing
slash in the target to match other redirects.

const ref = useRef<HTMLDivElement>(null);
// router
const { userId, workspaceSlug } = useParams();
const { userId } = useParams();
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n apps/web/core/components/profile/sidebar.tsx | head -100

Repository: makeplane/plane

Length of output: 4397


🏁 Script executed:

wc -l apps/web/core/components/profile/sidebar.tsx

Repository: makeplane/plane

Length of output: 107


🏁 Script executed:

rg 'const\s*\{\s*\w+\s*\}\s*=\s*useParams\(' apps/web/core/components/ -A5 | head -80

Repository: makeplane/plane

Length of output: 6678


🏁 Script executed:

rg 'userId.*useParams' apps/web/ -B2 -A5 | head -100

Repository: makeplane/plane

Length of output: 8571


🏁 Script executed:

sed -n '88,100p' apps/web/core/components/profile/sidebar.tsx

Repository: makeplane/plane

Length of output: 516


🏁 Script executed:

rg 'currentUser\?.id.*userId' apps/web/core/components/profile/sidebar.tsx -B3 -A3

Repository: makeplane/plane

Length of output: 314


🏁 Script executed:

cat apps/web/tsconfig.json | grep -A 10 '"strict'

Repository: makeplane/plane

Length of output: 439


🏁 Script executed:

rg 'strict.*true|compilerOptions' apps/web/tsconfig.json -A 15 | head -30

Repository: makeplane/plane

Length of output: 793


🏁 Script executed:

sed -n '30,50p' apps/web/core/components/profile/sidebar.tsx

Repository: makeplane/plane

Length of output: 838


🏁 Script executed:

rg '@ts-ignore|@ts-expect-error|as string|as const' apps/web/core/components/profile/sidebar.tsx

Repository: makeplane/plane

Length of output: 41


🏁 Script executed:

rg 'userId.*string' apps/web/core/components/profile/ -B2 -A2

Repository: makeplane/plane

Length of output: 41


🏁 Script executed:

rg 'useParams|return.*Record' node_modules/next/navigation.d.ts 2>/dev/null | head -20

Repository: makeplane/plane

Length of output: 41


🏁 Script executed:

grep -r "useParams" apps/web/core/components/profile/ -l | while read f; do echo "=== $f ==="; grep -A 20 "useParams" "$f" | head -25; done

Repository: makeplane/plane

Length of output: 7791


🏁 Script executed:

find apps/web/app -name "*profile*" -type d | head -5

Repository: makeplane/plane

Length of output: 318


🏁 Script executed:

ls -la apps/web/app/\(all\)/\[workspaceSlug\]/\(projects\)/profile/

Repository: makeplane/plane

Length of output: 227


🏁 Script executed:

grep -r "userId\?.toString\|userId as string" apps/web/core/components/profile/ -B2 -A2

Repository: makeplane/plane

Length of output: 2358


🏁 Script executed:

sed -n '1,30p' apps/web/core/components/profile/profile-issues-filter.tsx

Repository: makeplane/plane

Length of output: 1292


Add explicit type narrowing or assertion for userId before comparison on line 92.

The useParams() hook returns Record<string, string | string[]>, so destructured userId has type string | string[]. Before the comparison currentUser?.id === userId on line 92, either convert it with .toString() (as done in profile-issues-filter.tsx and other profile components) or narrow the type explicitly. This ensures type consistency across the profile components.

🤖 Prompt for AI Agents
In @apps/web/core/components/profile/sidebar.tsx at line 36, The destructured
userId from useParams() is string | string[]; before the comparison
currentUser?.id === userId in this file/component, explicitly narrow or convert
userId to a string (e.g., call .toString() or handle array case like other
profile components) or assert it as string so the types match; update the
comparison to use the normalized string variable (mirroring the approach in
profile-issues-filter.tsx).

Comment on lines +33 to +42
const { data: userProfileActivity } = useSWR(
USER_ACTIVITY({
cursor,
}),
() =>
userService.getUserActivity({
cursor,
per_page: perPage,
})
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find USER_ACTIVITY constant definition
rg -n "USER_ACTIVITY" --type=ts -B2 -A10 apps/web/core/

Repository: makeplane/plane

Length of output: 6465


🏁 Script executed:

#!/bin/bash
# Find all usages of USER_ACTIVITY to see scope of changes
rg -n "USER_ACTIVITY\(" --type=ts -B1 -A3

Repository: makeplane/plane

Length of output: 1046


SWR cache key doesn't include per_page, which may cause stale data.

The SWR key only includes cursor, but the fetch function uses per_page. If per_page changes while cursor stays the same, SWR will return cached data with the old page size.

Proposed fix
   const { data: userProfileActivity } = useSWR(
-    USER_ACTIVITY({
-      cursor,
-    }),
+    USER_ACTIVITY({ cursor, per_page: perPage }),
     () =>
       userService.getUserActivity({
         cursor,
         per_page: perPage,
       })
   );

This requires updating the USER_ACTIVITY constant to accept per_page as a parameter.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
@apps/web/core/components/settings/profile/content/pages/activity/activity-list.tsx
around lines 33 - 42, The SWR cache key for useSWR only includes cursor but the
fetcher userService.getUserActivity also depends on perPage, causing stale
results when perPage changes; update the USER_ACTIVITY key generator to accept
per_page and call useSWR with USER_ACTIVITY({ cursor, per_page: perPage }) (and
ensure the fetcher call uses the same per_page param), so the key uniquely
reflects both cursor and perPage and invalidates cache correctly for
userProfileActivity.

Comment on lines +146 to +147
const updateCurrentUserDetail = updateCurrentUser(userPayload).finally(() => setIsLoading(false));
const updateCurrentUserProfile = updateUserProfile(profilePayload).finally(() => setIsLoading(false));
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Double setIsLoading(false) calls cause redundant state updates.

Both updateCurrentUserDetail and updateCurrentUserProfile have .finally(() => setIsLoading(false)) attached. When Promise.all resolves, setIsLoading(false) is called twice. Consider moving the loading state reset to a single location.

Proposed fix
-    const updateCurrentUserDetail = updateCurrentUser(userPayload).finally(() => setIsLoading(false));
-    const updateCurrentUserProfile = updateUserProfile(profilePayload).finally(() => setIsLoading(false));
+    const updateCurrentUserDetail = updateCurrentUser(userPayload);
+    const updateCurrentUserProfile = updateUserProfile(profilePayload);

     const promises = [updateCurrentUserDetail, updateCurrentUserProfile];
     const updateUserAndProfile = Promise.all(promises);

     setPromiseToast(updateUserAndProfile, {
       loading: "Updating...",
       success: {
         title: "Success!",
         message: () => `Profile updated successfully.`,
       },
       error: {
         title: "Error!",
         message: () => `There was some error in updating your profile. Please try again.`,
       },
     });
-    updateUserAndProfile
-      .then(() => {
-        return;
-      })
-      .catch(() => {});
+    updateUserAndProfile.finally(() => setIsLoading(false));
📝 Committable suggestion

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

Suggested change
const updateCurrentUserDetail = updateCurrentUser(userPayload).finally(() => setIsLoading(false));
const updateCurrentUserProfile = updateUserProfile(profilePayload).finally(() => setIsLoading(false));
const updateCurrentUserDetail = updateCurrentUser(userPayload);
const updateCurrentUserProfile = updateUserProfile(profilePayload);
const promises = [updateCurrentUserDetail, updateCurrentUserProfile];
const updateUserAndProfile = Promise.all(promises);
setPromiseToast(updateUserAndProfile, {
loading: "Updating...",
success: {
title: "Success!",
message: () => `Profile updated successfully.`,
},
error: {
title: "Error!",
message: () => `There was some error in updating your profile. Please try again.`,
},
});
updateUserAndProfile.finally(() => setIsLoading(false));
🤖 Prompt for AI Agents
In @apps/web/core/components/settings/profile/content/pages/general/form.tsx
around lines 146 - 147, Both updateCurrentUserDetail and
updateCurrentUserProfile attach .finally(() => setIsLoading(false)) causing
duplicate state updates when awaited together; remove the per-promise finally
handlers and instead call setIsLoading(false) once in the finally of the
combined Promise.all (or after awaiting Promise.all of
updateCurrentUser(userPayload) and updateUserProfile(profilePayload)) so
setIsLoading is reset only once; update references: updateCurrentUserDetail,
updateCurrentUserProfile, updateCurrentUser, updateUserProfile, setIsLoading,
and Promise.all.

Comment on lines +202 to +218
<button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
{!userAvatar || userAvatar === "" ? (
<div className="h-16 w-16 rounded-md bg-layer-1 p-2">
<CircleUserRound className="h-full w-full text-secondary" />
</div>
) : (
<div className="relative h-16 w-16 overflow-hidden">
<img
src={getFileURL(userAvatar)}
className="absolute left-0 top-0 h-full w-full rounded-lg object-cover"
onClick={() => setIsImageUploadModalOpen(true)}
alt={currentUser?.display_name}
role="button"
/>
</div>
)}
</button>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Nested button inside button creates invalid HTML.

The outer <button> (line 202) contains an <img> with role="button" and onClick (lines 212-214). This creates confusing semantics. The onClick on the image is redundant since clicking it will already trigger the parent button's handler.

Proposed fix - remove redundant onClick from img
                       <div className="relative h-16 w-16 overflow-hidden">
                         <img
                           src={getFileURL(userAvatar)}
                           className="absolute left-0 top-0 h-full w-full rounded-lg object-cover"
-                          onClick={() => setIsImageUploadModalOpen(true)}
                           alt={currentUser?.display_name}
-                          role="button"
                         />
                       </div>
📝 Committable suggestion

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

Suggested change
<button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
{!userAvatar || userAvatar === "" ? (
<div className="h-16 w-16 rounded-md bg-layer-1 p-2">
<CircleUserRound className="h-full w-full text-secondary" />
</div>
) : (
<div className="relative h-16 w-16 overflow-hidden">
<img
src={getFileURL(userAvatar)}
className="absolute left-0 top-0 h-full w-full rounded-lg object-cover"
onClick={() => setIsImageUploadModalOpen(true)}
alt={currentUser?.display_name}
role="button"
/>
</div>
)}
</button>
<button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
{!userAvatar || userAvatar === "" ? (
<div className="h-16 w-16 rounded-md bg-layer-1 p-2">
<CircleUserRound className="h-full w-full text-secondary" />
</div>
) : (
<div className="relative h-16 w-16 overflow-hidden">
<img
src={getFileURL(userAvatar)}
className="absolute left-0 top-0 h-full w-full rounded-lg object-cover"
alt={currentUser?.display_name}
/>
</div>
)}
</button>
🤖 Prompt for AI Agents
In @apps/web/core/components/settings/profile/content/pages/general/form.tsx
around lines 202 - 218, The img inside the clickable avatar button is given its
own onClick and role="button", creating nested button semantics; remove the
onClick and role attributes from the img (the parent button already calls
setIsImageUploadModalOpen(true)), leaving the img as a plain image using
src={getFileURL(userAvatar)} and alt={currentUser?.display_name} so clicks are
handled by the outer button and HTML remains valid.

Comment on lines +26 to +28
<Suspense>
<PageComponent />
</Suspense>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing Suspense fallback degrades loading experience.

The Suspense component has no fallback prop, which means nothing will render while the lazy-loaded component is loading. This creates a jarring user experience with an empty content area.

🎨 Proposed fix with loading state
-      <Suspense>
+      <Suspense fallback={<div className="p-8"><Spinner /></div>}>
         <PageComponent />
       </Suspense>

Or use a skeleton loader for a more polished experience.

📝 Committable suggestion

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

Suggested change
<Suspense>
<PageComponent />
</Suspense>
<Suspense fallback={<div className="p-8"><Spinner /></div>}>
<PageComponent />
</Suspense>
🤖 Prompt for AI Agents
In @apps/web/core/components/settings/profile/content/root.tsx around lines 26 -
28, The Suspense wrapper around PageComponent is missing a fallback, causing a
blank area during lazy loading; update the Suspense usage to provide a fallback
UI (e.g., a spinner or Skeleton component) by supplying the fallback prop to
Suspense so that a loading indicator is rendered while PageComponent loads, and
reuse or create a lightweight Loading/Skeleton component if needed to keep UX
consistent.

Comment on lines +8 to +30
export const ProfileSettingsSidebarHeader = observer(function ProfileSettingsSidebarHeader() {
// store hooks
const { data: currentUser } = useUser();

return (
<div className="shrink-0 flex items-center gap-2">
<div className="shrink-0">
<Avatar
src={getFileURL(currentUser?.avatar_url ?? "")}
name={currentUser?.display_name}
size={32}
shape="circle"
className="text-16"
/>
</div>
<div className="truncate">
<p className="text-body-sm-medium truncate">
{currentUser?.first_name} {currentUser?.last_name}
</p>
<p className="text-caption-md-regular truncate">{currentUser?.email}</p>
</div>
</div>
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid getFileURL("") + improve name fallback to prevent “undefined undefined”.

Suggested tweak
 export const ProfileSettingsSidebarHeader = observer(function ProfileSettingsSidebarHeader() {
   // store hooks
   const { data: currentUser } = useUser();

+  const fullName = [currentUser?.first_name, currentUser?.last_name].filter(Boolean).join(" ");
+  const primaryLabel = fullName || currentUser?.display_name || currentUser?.email || "User";
+
   return (
     <div className="shrink-0 flex items-center gap-2">
       <div className="shrink-0">
         <Avatar
-          src={getFileURL(currentUser?.avatar_url ?? "")}
-          name={currentUser?.display_name}
+          src={currentUser?.avatar_url ? getFileURL(currentUser.avatar_url) : undefined}
+          name={primaryLabel}
           size={32}
           shape="circle"
           className="text-16"
         />
       </div>
       <div className="truncate">
         <p className="text-body-sm-medium truncate">
-          {currentUser?.first_name} {currentUser?.last_name}
+          {primaryLabel}
         </p>
         <p className="text-caption-md-regular truncate">{currentUser?.email}</p>
       </div>
     </div>
   );
 });
📝 Committable suggestion

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

Suggested change
export const ProfileSettingsSidebarHeader = observer(function ProfileSettingsSidebarHeader() {
// store hooks
const { data: currentUser } = useUser();
return (
<div className="shrink-0 flex items-center gap-2">
<div className="shrink-0">
<Avatar
src={getFileURL(currentUser?.avatar_url ?? "")}
name={currentUser?.display_name}
size={32}
shape="circle"
className="text-16"
/>
</div>
<div className="truncate">
<p className="text-body-sm-medium truncate">
{currentUser?.first_name} {currentUser?.last_name}
</p>
<p className="text-caption-md-regular truncate">{currentUser?.email}</p>
</div>
</div>
);
export const ProfileSettingsSidebarHeader = observer(function ProfileSettingsSidebarHeader() {
// store hooks
const { data: currentUser } = useUser();
const fullName = [currentUser?.first_name, currentUser?.last_name].filter(Boolean).join(" ");
const primaryLabel = fullName || currentUser?.display_name || currentUser?.email || "User";
return (
<div className="shrink-0 flex items-center gap-2">
<div className="shrink-0">
<Avatar
src={currentUser?.avatar_url ? getFileURL(currentUser.avatar_url) : undefined}
name={primaryLabel}
size={32}
shape="circle"
className="text-16"
/>
</div>
<div className="truncate">
<p className="text-body-sm-medium truncate">
{primaryLabel}
</p>
<p className="text-caption-md-regular truncate">{currentUser?.email}</p>
</div>
</div>
);
});
🤖 Prompt for AI Agents
In @apps/web/core/components/settings/profile/sidebar/header.tsx around lines 8
- 30, ProfileSettingsSidebarHeader currently calls
getFileURL(currentUser?.avatar_url ?? "") and renders names via
{currentUser?.first_name} {currentUser?.last_name}, which can call
getFileURL("") and show "undefined undefined"; change to pass undefined to
getFileURL when no avatar (e.g., getFileURL(currentUser?.avatar_url)) and render
a safe name fallback using display_name or a single-field fallback (e.g., use
currentUser?.display_name ?? currentUser?.first_name ?? currentUser?.last_name
or join available parts) so Avatar receives a proper src/name and the UI never
shows "undefined undefined".

Comment on lines +21 to +29
{Object.values(workspaces).map((workspace) => (
<SettingsSidebarItem
key={workspace.id}
as="link"
href={`/${workspace.slug}/`}
iconNode={<WorkspaceLogo logo={workspace.logo_url} name={workspace.name} classNames="shrink-0" />}
label={workspace.name}
isActive={false}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing active workspace indication.

All workspace items have isActive={false} hardcoded (line 28), so the current workspace is never highlighted. This degrades the user experience as users cannot easily identify which workspace they're currently in.

🔧 Proposed fix to highlight active workspace

The component should receive the current workspaceSlug (perhaps from the parent or via a store hook) and compare it with each workspace's slug:

-export const ProfileSettingsSidebarWorkspaceOptions = observer(function ProfileSettingsSidebarWorkspaceOptions() {
+export const ProfileSettingsSidebarWorkspaceOptions = observer(function ProfileSettingsSidebarWorkspaceOptions(props: { currentWorkspaceSlug?: string }) {
+  const { currentWorkspaceSlug } = props;
   // store hooks
   const { workspaces } = useWorkspace();
   // translation
   const { t } = useTranslation();

   return (
     <div className="shrink-0">
       <div className="p-2 text-caption-md-medium text-tertiary capitalize">{t("workspace")}</div>
       <div className="flex flex-col">
         {Object.values(workspaces).map((workspace) => (
           <SettingsSidebarItem
             key={workspace.id}
             as="link"
             href={`/${workspace.slug}/`}
             iconNode={<WorkspaceLogo logo={workspace.logo_url} name={workspace.name} classNames="shrink-0" />}
             label={workspace.name}
-            isActive={false}
+            isActive={workspace.slug === currentWorkspaceSlug}
           />
         ))}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @apps/web/core/components/settings/profile/sidebar/workspace-options.tsx
around lines 21 - 29, The workspace items always pass isActive={false} so the
current workspace is never highlighted; update the mapping in
workspace-options.tsx to determine isActive by comparing each workspace.slug to
the active workspace slug (e.g., accept a workspaceSlug prop on the component or
read it from the store/hook used by the app) and pass isActive={workspace.slug
=== workspaceSlug} into SettingsSidebarItem (keep the same keys and props like
iconNode, href, label). Ensure workspaceSlug is provided to this component or
obtained via the appropriate hook before the map.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
apps/web/core/components/workspace/sidebar/user-menu-root.tsx (1)

26-27: Consolidate duplicate useUser calls into a single destructuring.

The hook is called twice on consecutive lines. Destructure both data and signOut in one call to reduce overhead.

Suggested fix
-  const { data: currentUser } = useUser();
-  const { signOut } = useUser();
+  const { data: currentUser, signOut } = useUser();
apps/web/core/components/settings/profile/content/pages/security.tsx (1)

89-94: Improve type safety for error handling.

The error handling accesses error.error_code after checking error instanceof Error, but the standard Error type doesn't include an error_code property. While the in operator check and optional chaining make this safe at runtime, TypeScript's type system won't properly infer this without explicit type assertions.

As per coding guidelines, all files should be fully typed with strict mode enabled.

♻️ Proposed fix with explicit type handling
-    } catch (error: unknown) {
-      let errorInfo = undefined;
-      if (error instanceof Error) {
-        const code = "error_code" in error ? error.error_code?.toString() : undefined;
-        errorInfo = code ? authErrorHandler(code as EAuthenticationErrorCodes) : undefined;
-      }
+    } catch (error: unknown) {
+      let errorInfo = undefined;
+      if (error instanceof Error) {
+        const errorWithCode = error as Error & { error_code?: string | number };
+        const code = errorWithCode.error_code?.toString();
+        errorInfo = code ? authErrorHandler(code as EAuthenticationErrorCodes) : undefined;
+      }

Alternatively, consider defining a custom error type interface for authentication errors that includes the error_code property.

apps/web/core/components/settings/profile/content/pages/notifications/email-notification-form.tsx (2)

42-48: Log errors for debugging.

The error is caught but not logged, making debugging difficult in production. Consider logging the error and, if the API provides specific error messages, displaying them to the user for better feedback.

🔍 Proposed improvement
-  } catch (_error) {
+  } catch (error) {
+    console.error("Failed to update email notification setting:", error);
     setToast({
       title: t("error"),
       type: TOAST_TYPE.ERROR,
-      message: t("failed_to_update_email_notification_setting"),
+      message: error instanceof Error ? error.message : t("failed_to_update_email_notification_setting"),
     });
   }

57-158: Consider reducing code duplication.

The same pattern is repeated five times with only field names and translations differing. Refactoring to use a configuration array would reduce duplication and improve maintainability.

♻️ Suggested refactor using config array
+ const notificationSettings: Array<{
+   name: keyof IUserEmailNotificationSettings;
+   title: string;
+   description: string;
+   nested?: boolean;
+ }> = [
+   { name: "property_change", title: t("property_changes"), description: t("property_changes_description") },
+   { name: "state_change", title: t("state_change"), description: t("state_change_description") },
+   { name: "issue_completed", title: t("issue_completed"), description: t("issue_completed_description"), nested: true },
+   { name: "comment", title: t("comments"), description: t("comments_description") },
+   { name: "mention", title: t("mentions"), description: t("mentions_description") },
+ ];
+
  return (
    <div className="flex flex-col gap-y-1">
-     <SettingsControlItem
-       title={t("property_changes")}
-       description={t("property_changes_description")}
-       control={
-         <Controller
-           control={control}
-           name="property_change"
-           render={({ field: { value, onChange } }) => (
-             <ToggleSwitch
-               value={value}
-               onChange={(newValue) => {
-                 onChange(newValue);
-                 handleSettingChange("property_change", newValue);
-               }}
-               size="sm"
-             />
-           )}
-         />
-       }
-     />
-     {/* ... other repeated items ... */}
+     {notificationSettings.map((setting) => {
+       const item = (
+         <SettingsControlItem
+           key={setting.name}
+           title={setting.title}
+           description={setting.description}
+           control={
+             <Controller
+               control={control}
+               name={setting.name}
+               render={({ field: { value, onChange } }) => (
+                 <ToggleSwitch
+                   value={value}
+                   onChange={(newValue) => {
+                     onChange(newValue);
+                     handleSettingChange(setting.name, newValue);
+                   }}
+                   size="sm"
+                 />
+               )}
+             />
+           }
+         />
+       );
+       return setting.nested ? (
+         <div key={setting.name} className="border-l-3 border-subtle-1 pl-3">
+           {item}
+         </div>
+       ) : item;
+     })}
    </div>
  );
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 50aa9a6 and c6f1239.

📒 Files selected for processing (3)
  • apps/web/core/components/settings/profile/content/pages/notifications/email-notification-form.tsx
  • apps/web/core/components/settings/profile/content/pages/security.tsx
  • apps/web/core/components/workspace/sidebar/user-menu-root.tsx
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx,mts,cts}

📄 CodeRabbit inference engine (.github/instructions/typescript.instructions.md)

**/*.{ts,tsx,mts,cts}: Use const type parameters for more precise literal inference in TypeScript 5.0+
Use the satisfies operator to validate types without widening them
Leverage inferred type predicates to reduce the need for explicit is return types in filter/check functions
Use NoInfer<T> utility to block inference for specific type arguments when they should be determined by other arguments
Utilize narrowing in switch(true) blocks for control flow analysis (TypeScript 5.3+)
Rely on narrowing from direct boolean comparisons for type guards
Trust preserved narrowing in closures when variables aren't modified after the check (TypeScript 5.4+)
Use constant indices to narrow object/array properties (TypeScript 5.5+)
Use standard ECMAScript decorators (Stage 3) instead of legacy experimentalDecorators
Use using declarations for explicit resource management with Disposable pattern instead of manual cleanup (TypeScript 5.2+)
Use with { type: "json" } for import attributes; avoid deprecated assert syntax (TypeScript 5.3/5.8+)
Use import type explicitly when importing types to ensure they are erased during compilation, respecting verbatimModuleSyntax flag
Use .ts, .mts, .cts extensions in import type statements (TypeScript 5.2+)
Use import type { Type } from "mod" with { "resolution-mode": "import" } for specific module resolution contexts (TypeScript 5.3+)
Use new iterator methods (map, filter, etc.) if targeting modern environments (TypeScript 5.6+)
Utilize new Set methods like union, intersection, etc., when available (TypeScript 5.5+)
Use Object.groupBy / Map.groupBy standard methods for grouping instead of external libraries (TypeScript 5.4+)
Use Promise.withResolvers() for creating promises with exposed resolve/reject functions (TypeScript 5.7+)
Use copying array methods (toSorted, toSpliced, with) for immutable array operations (TypeScript 5.2+)
Avoid accessing instance fields via super in classes (TypeScript 5....

Files:

  • apps/web/core/components/settings/profile/content/pages/security.tsx
  • apps/web/core/components/settings/profile/content/pages/notifications/email-notification-form.tsx
  • apps/web/core/components/workspace/sidebar/user-menu-root.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Enable TypeScript strict mode and ensure all files are fully typed

Files:

  • apps/web/core/components/settings/profile/content/pages/security.tsx
  • apps/web/core/components/settings/profile/content/pages/notifications/email-notification-form.tsx
  • apps/web/core/components/workspace/sidebar/user-menu-root.tsx
**/*.{js,jsx,ts,tsx,json,css}

📄 CodeRabbit inference engine (AGENTS.md)

Use Prettier with Tailwind plugin for code formatting, run pnpm fix:format

Files:

  • apps/web/core/components/settings/profile/content/pages/security.tsx
  • apps/web/core/components/settings/profile/content/pages/notifications/email-notification-form.tsx
  • apps/web/core/components/workspace/sidebar/user-menu-root.tsx
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,jsx,ts,tsx}: Use ESLint with shared config across packages, adhering to max warnings limits per package
Use camelCase for variable and function names, PascalCase for components and types
Use try-catch with proper error types and log errors appropriately

Files:

  • apps/web/core/components/settings/profile/content/pages/security.tsx
  • apps/web/core/components/settings/profile/content/pages/notifications/email-notification-form.tsx
  • apps/web/core/components/workspace/sidebar/user-menu-root.tsx
🧠 Learnings (6)
📚 Learning: 2025-11-25T10:18:05.172Z
Learnt from: CR
Repo: makeplane/plane PR: 0
File: .github/instructions/typescript.instructions.md:0-0
Timestamp: 2025-11-25T10:18:05.172Z
Learning: Applies to **/*.{ts,tsx,mts,cts} : Avoid `import ... assert` syntax; use `with` instead for import attributes

Applied to files:

  • apps/web/core/components/settings/profile/content/pages/security.tsx
📚 Learning: 2025-10-01T15:30:17.605Z
Learnt from: lifeiscontent
Repo: makeplane/plane PR: 7888
File: packages/propel/src/avatar/avatar.stories.tsx:2-3
Timestamp: 2025-10-01T15:30:17.605Z
Learning: In the makeplane/plane repository, avoid suggesting inline type imports (e.g., `import { Avatar, type TAvatarSize }`) due to bundler compatibility issues. Keep type imports and value imports as separate statements.

Applied to files:

  • apps/web/core/components/settings/profile/content/pages/security.tsx
📚 Learning: 2025-07-14T11:22:43.964Z
Learnt from: gakshita
Repo: makeplane/plane PR: 7393
File: apps/admin/app/(all)/(dashboard)/email/email-config-form.tsx:104-104
Timestamp: 2025-07-14T11:22:43.964Z
Learning: In the Plane project's SMTP configuration implementation, the email configuration form (email-config-form.tsx) hardcodes ENABLE_SMTP to "1" in form submission because the form is only rendered when SMTP is enabled. The enable/disable functionality is managed at the page level (page.tsx) with a toggle, and the form only handles configuration details when SMTP is already enabled.

Applied to files:

  • apps/web/core/components/settings/profile/content/pages/notifications/email-notification-form.tsx
📚 Learning: 2025-10-10T13:25:14.810Z
Learnt from: gakshita
Repo: makeplane/plane PR: 7949
File: apps/web/core/components/issues/issue-modal/form.tsx:183-189
Timestamp: 2025-10-10T13:25:14.810Z
Learning: In `apps/web/core/components/issues/issue-modal/form.tsx`, the form reset effect uses a `dataResetProperties` dependency array prop (default: []) to give parent components explicit control over when the form resets. Do not suggest adding the `data` prop itself to the dependency array, as this would cause unwanted resets on every render when the data object reference changes, disrupting user input. The current pattern is intentional and allows the parent to trigger resets only when specific conditions are met.

Applied to files:

  • apps/web/core/components/settings/profile/content/pages/notifications/email-notification-form.tsx
📚 Learning: 2025-12-17T10:58:59.591Z
Learnt from: CR
Repo: makeplane/plane PR: 0
File: packages/tailwind-config/AGENTS.md:0-0
Timestamp: 2025-12-17T10:58:59.591Z
Learning: Applies to packages/tailwind-config/**/*.{ts,tsx} : Sidebar menu items should use transparent backgrounds with hover states like `hover:bg-layer-1-hover` rather than base layer backgrounds, maintaining visual lightness while providing hover feedback

Applied to files:

  • apps/web/core/components/workspace/sidebar/user-menu-root.tsx
📚 Learning: 2025-10-21T17:22:05.204Z
Learnt from: lifeiscontent
Repo: makeplane/plane PR: 7989
File: apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx:45-46
Timestamp: 2025-10-21T17:22:05.204Z
Learning: In the makeplane/plane repository, the refactor from useParams() to params prop is specifically scoped to page.tsx and layout.tsx files in apps/web/app (Next.js App Router pattern). Other components (hooks, regular client components, utilities) should continue using the useParams() hook as that is the correct pattern for non-route components.

Applied to files:

  • apps/web/core/components/workspace/sidebar/user-menu-root.tsx
🧬 Code graph analysis (1)
apps/web/core/components/workspace/sidebar/user-menu-root.tsx (4)
packages/i18n/src/hooks/use-translation.ts (1)
  • useTranslation (23-35)
apps/web/core/components/common/cover-image.tsx (1)
  • CoverImage (26-44)
packages/i18n/src/store/index.ts (1)
  • t (222-243)
packages/constants/src/endpoints.ts (1)
  • GOD_MODE_URL (7-7)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: check:types
  • GitHub Check: check:lint
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (9)
apps/web/core/components/workspace/sidebar/user-menu-root.tsx (4)

34-42: LGTM!

The fire-and-forget pattern with .catch() for error toast is appropriate for sign-out flows where success typically triggers a redirect.


44-48: LGTM!

The effect correctly synchronizes menu state with the sidebar dropdown toggle, and dependencies are properly listed.


76-103: LGTM!

The cover image header implementation correctly uses CoverImage with showDefaultWhenEmpty, and the centered avatar with user details provides a clean visual hierarchy.


104-133: LGTM!

The profile settings modal triggers are well-structured with distinct activeTab values, and the sign-out item is cleanly separated. Menu item styling is consistent.

apps/web/core/components/settings/profile/content/pages/security.tsx (3)

12-13: LGTM - Import aligns with new settings structure.

The import of SettingsHeading correctly supports the migration from legacy layout components to the new unified profile settings UI.


42-42: LGTM - Component export follows best practices.

The named export with observer wrapping is appropriate for the refactored settings structure and enables proper reactivity for MobX store integration.


121-256: LGTM - Well-structured responsive layout.

The refactored layout successfully migrates from legacy wrappers to the new SettingsHeading component while maintaining all functionality:

  • Responsive grid (sm:grid-cols-2) appropriately displays password fields side-by-side on larger screens
  • Conditional old password field takes full width when present
  • Password visibility toggles, validation, and error states are properly preserved
  • Form semantics and accessibility structure remain intact

The changes align well with the PR's objective of improving profile settings UI consistency.

apps/web/core/components/settings/profile/content/pages/notifications/email-notification-form.tsx (2)

51-53: Verify form reset behavior with MobX observer.

Including data directly in the dependency array may cause unnecessary form resets if the object reference changes on every render. While observer should provide stable references for unchanged data, consider adding a control mechanism (similar to the dataResetProperties pattern used elsewhere in the codebase) to give explicit control over when the form resets.

Based on learnings, this codebase has patterns for controlling form resets with explicit dependency arrays to prevent unwanted resets during user input.


18-19: > Likely an incorrect or invalid review comment.

const { signOut } = useUser();
const { toggleProfileSettingsModal } = useCommandPalette();
// derived values
const isUserInstanceAdmin = false;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how isUserInstanceAdmin or similar admin checks are implemented elsewhere
rg -n --type=ts --type=tsx -C3 'isUserInstanceAdmin|isInstanceAdmin|instance.*admin' -g '!node_modules'

Repository: makeplane/plane

Length of output: 86


🏁 Script executed:

#!/bin/bash
# Read the specific file to see the context around line 30 and lines 134-141
wc -l "apps/web/core/components/workspace/sidebar/user-menu-root.tsx"

Repository: makeplane/plane

Length of output: 124


🏁 Script executed:

#!/bin/bash
# Read the complete file to examine isUserInstanceAdmin usage
cat -n "apps/web/core/components/workspace/sidebar/user-menu-root.tsx"

Repository: makeplane/plane

Length of output: 5875


🏁 Script executed:

#!/bin/bash
# Search for admin patterns with corrected syntax (ts files only or use glob)
rg -n 'isUserInstanceAdmin|isInstanceAdmin|instance.*admin' --type ts -C2

Repository: makeplane/plane

Length of output: 10082


Hardcoded isUserInstanceAdmin = false makes God Mode section unreachable.

Lines 134-141 form dead code because false && ... is always false. Either implement proper admin status detection using the existing currentUserInstanceAdminStatus() service method, or remove the unreachable code path and the unused router import.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants