[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:
parent
ba5ba5bf54
commit
db8b67102d
216 changed files with 4684 additions and 5454 deletions
26
apps/web/ce/components/common/modal/global.tsx
Normal file
26
apps/web/ce/components/common/modal/global.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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 />}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,2 +1 @@
|
|||
export * from "./features";
|
||||
export * from "./tabs";
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
];
|
||||
Loading…
Add table
Add a link
Reference in a new issue