[WEB-5860] [WEB-5861] [WEB-5862] style: improved settings interface (#8520)

* style: improved profile settings

* chore: minor improvements

* style: improved workspace settings

* style: workspace settings content

* style: improved project settings

* fix: project settings flat map

* chore: add back navigation from settings pages

* style: settings content

* style: estimates list

* refactor: remove old code

* refactor: removed unnecessary line breaks

* refactor: create a common component for page header

* chore: add fade-in animation to sidebar

* fix: formatting

* fix: project settings sidebar header

* fix: workspace settings sidebar header

* fix: settings content wrapper scroll

* chore: separate project settings features

* fix: formatting

* refactor: custom theme selector

* refactor: settings headings

* refactor: settings headings

* fix: project settings sidebar padding

* fix: sidebar header padding

* fix: sidebar item permissions

* fix: missing editable check

* refactor: remove unused files

* chore: remove unnecessary code

* chore: add missing translations

* fix: formatting
This commit is contained in:
Aaryan Khandelwal 2026-01-23 13:34:20 +05:30 committed by GitHub
parent ba5ba5bf54
commit db8b67102d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
216 changed files with 4684 additions and 5454 deletions

View file

@ -0,0 +1,26 @@
import { lazy, Suspense } from "react";
import { observer } from "mobx-react";
const ProfileSettingsModal = lazy(() =>
import("@/components/settings/profile/modal").then((module) => ({
default: module.ProfileSettingsModal,
}))
);
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) {
return (
<Suspense fallback={null}>
<ProfileSettingsModal />
</Suspense>
);
});

View file

@ -74,7 +74,7 @@ export const TopNavigationRoot = observer(function TopNavigationRoot() {
<HelpMenuRoot />
<StarUsOnGitHubLink />
<div className="flex items-center justify-center size-8 hover:bg-layer-1-hover rounded-md">
<UserMenuRoot size="xs" />
<UserMenuRoot />
</div>
</div>
</div>

View file

@ -1,7 +0,0 @@
import { StartOfWeekPreference } from "@/components/profile/start-of-week-preference";
import { ThemeSwitcher } from "./theme-switcher";
export const PREFERENCE_COMPONENTS = {
theme: ThemeSwitcher,
start_of_week: StartOfWeekPreference,
};

View file

@ -10,8 +10,7 @@ import { applyCustomTheme } from "@plane/utils";
// components
import { CustomThemeSelector } from "@/components/core/theme/custom-theme-selector";
import { ThemeSwitch } from "@/components/core/theme/theme-switch";
// helpers
import { PreferencesSection } from "@/components/preferences/section";
import { SettingsControlItem } from "@/components/settings/control-item";
// hooks
import { useUserProfile } from "@/hooks/store/user";
@ -79,18 +78,16 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: {
return (
<>
<PreferencesSection
<SettingsControlItem
title={t(props.option.title)}
description={t(props.option.description)}
control={
<div>
<ThemeSwitch
value={currentTheme}
onChange={(themeOption) => {
void handleThemeChange(themeOption);
}}
/>
</div>
<ThemeSwitch
value={currentTheme}
onChange={(themeOption) => {
void handleThemeChange(themeOption);
}}
/>
}
/>
{userProfile.theme?.theme === "custom" && <CustomThemeSelector />}

View file

@ -6,6 +6,7 @@ import { useTranslation } from "@plane/i18n";
import type { TBillingFrequency, TProductBillingFrequency } from "@plane/types";
import { EProductSubscriptionEnum } from "@plane/types";
// components
import { SettingsBoxedControlItem } from "@/components/settings/boxed-control-item";
import { SettingsHeading } from "@/components/settings/heading";
// local imports
import { PlansComparison } from "./comparison/root";
@ -37,32 +38,28 @@ export const BillingRoot = observer(function BillingRoot() {
setProductBillingFrequency({ ...productBillingFrequency, [subscriptionType]: frequency });
return (
<section className="relative size-full flex flex-col overflow-y-auto scrollbar-hide">
<SettingsHeading
title={t("workspace_settings.settings.billing_and_plans.heading")}
description={t("workspace_settings.settings.billing_and_plans.description")}
/>
<section className="relative size-full overflow-y-auto scrollbar-hide">
<div>
<div className="py-6">
<div className="px-6 py-4 rounded-lg bg-layer-1">
<div className="flex gap-2 items-center justify-between">
<div className="flex flex-col gap-1">
<h4 className="text-h4-bold text-primary">Community</h4>
<div className="text-caption-md-medium text-secondary">
Unlimited projects, issues, cycles, modules, pages, and storage
</div>
</div>
</div>
</div>
<SettingsHeading
title={t("workspace_settings.settings.billing_and_plans.heading")}
description={t("workspace_settings.settings.billing_and_plans.description")}
/>
<div className="mt-6">
<SettingsBoxedControlItem
title="Community"
description="Unlimited projects, issues, cycles, modules, pages, and storage"
/>
</div>
<div className="text-h4-semibold mt-3">All plans</div>
</div>
<PlansComparison
isCompareAllFeaturesSectionOpen={isCompareAllFeaturesSectionOpen}
getBillingFrequency={getBillingFrequency}
setBillingFrequency={setBillingFrequency}
setIsCompareAllFeaturesSectionOpen={setIsCompareAllFeaturesSectionOpen}
/>
<div className="mt-10 flex flex-col gap-y-3">
<h4 className="text-h6-semibold">All plans</h4>
<PlansComparison
isCompareAllFeaturesSectionOpen={isCompareAllFeaturesSectionOpen}
getBillingFrequency={getBillingFrequency}
setBillingFrequency={setBillingFrequency}
setIsCompareAllFeaturesSectionOpen={setIsCompareAllFeaturesSectionOpen}
/>
</div>
</section>
);
});

View file

@ -1,15 +1,14 @@
import { useState } from "react";
import { observer } from "mobx-react";
// types
// plane imports
import { WORKSPACE_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { ChevronDownIcon, ChevronUpIcon } from "@plane/propel/icons";
import type { IWorkspace } from "@plane/types";
// ui
import { Collapsible } from "@plane/ui";
import { DeleteWorkspaceModal } from "./delete-workspace-modal";
// components
import { SettingsBoxedControlItem } from "@/components/settings/boxed-control-item";
// local imports
import { DeleteWorkspaceModal } from "./delete-workspace-modal";
type TDeleteWorkspace = {
workspace: IWorkspace | null;
@ -18,8 +17,8 @@ type TDeleteWorkspace = {
export const DeleteWorkspaceSection = observer(function DeleteWorkspaceSection(props: TDeleteWorkspace) {
const { workspace } = props;
// states
const [isOpen, setIsOpen] = useState(false);
const [deleteWorkspaceModal, setDeleteWorkspaceModal] = useState(false);
// translation
const { t } = useTranslation();
return (
@ -29,40 +28,19 @@ export const DeleteWorkspaceSection = observer(function DeleteWorkspaceSection(p
isOpen={deleteWorkspaceModal}
onClose={() => setDeleteWorkspaceModal(false)}
/>
<div className="border-t border-subtle">
<div className="w-full">
<Collapsible
isOpen={isOpen}
onToggle={() => setIsOpen(!isOpen)}
className="w-full"
buttonClassName="flex w-full items-center justify-between py-4"
title={
<>
<span className="text-body-md-medium tracking-tight">
{t("workspace_settings.settings.general.delete_workspace")}
</span>
{isOpen ? <ChevronUpIcon className="h-5 w-5" /> : <ChevronDownIcon className="h-5 w-5" />}
</>
}
<SettingsBoxedControlItem
title={t("workspace_settings.settings.general.delete_workspace")}
description={t("workspace_settings.settings.general.delete_workspace_description")}
control={
<Button
variant="error-outline"
onClick={() => setDeleteWorkspaceModal(true)}
data-ph-element={WORKSPACE_TRACKER_ELEMENTS.DELETE_WORKSPACE_BUTTON}
>
<div className="flex flex-col gap-4">
<span className="text-body-sm-regular tracking-tight">
{t("workspace_settings.settings.general.delete_workspace_description")}
</span>
<div>
<Button
variant="error-fill"
size="lg"
onClick={() => setDeleteWorkspaceModal(true)}
data-ph-element={WORKSPACE_TRACKER_ELEMENTS.DELETE_WORKSPACE_BUTTON}
>
{t("workspace_settings.settings.general.delete_btn")}
</Button>
</div>
</div>
</Collapsible>
</div>
</div>
{t("delete")}
</Button>
}
/>
</>
);
});

View file

@ -1,2 +1 @@
export * from "./features";
export * from "./tabs";

View file

@ -1,82 +0,0 @@
// icons
import { EUserPermissions } from "@plane/constants";
import { SettingIcon } from "@/components/icons/attachment";
// types
import type { Props } from "@/components/icons/types";
// constants
export const PROJECT_SETTINGS = {
general: {
key: "general",
i18n_label: "common.general",
href: ``,
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/`,
Icon: SettingIcon,
},
members: {
key: "members",
i18n_label: "common.members",
href: `/members`,
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/members/`,
Icon: SettingIcon,
},
features: {
key: "features",
i18n_label: "common.features",
href: `/features`,
access: [EUserPermissions.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/features/`,
Icon: SettingIcon,
},
states: {
key: "states",
i18n_label: "common.states",
href: `/states`,
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/states/`,
Icon: SettingIcon,
},
labels: {
key: "labels",
i18n_label: "common.labels",
href: `/labels`,
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/labels/`,
Icon: SettingIcon,
},
estimates: {
key: "estimates",
i18n_label: "common.estimates",
href: `/estimates`,
access: [EUserPermissions.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/estimates/`,
Icon: SettingIcon,
},
automations: {
key: "automations",
i18n_label: "project_settings.automations.label",
href: `/automations`,
access: [EUserPermissions.ADMIN],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/automations/`,
Icon: SettingIcon,
},
};
export const PROJECT_SETTINGS_LINKS: {
key: string;
i18n_label: string;
href: string;
access: EUserPermissions[];
highlight: (pathname: string, baseUrl: string) => boolean;
Icon: React.FC<Props>;
}[] = [
PROJECT_SETTINGS["general"],
PROJECT_SETTINGS["members"],
PROJECT_SETTINGS["features"],
PROJECT_SETTINGS["states"],
PROJECT_SETTINGS["labels"],
PROJECT_SETTINGS["estimates"],
PROJECT_SETTINGS["automations"],
];