feat: init
This commit is contained in:
38
apps/web/core/components/settings/sidebar/header.tsx
Normal file
38
apps/web/core/components/settings/sidebar/header.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { getUserRole } from "@plane/utils";
|
||||
// components
|
||||
import { WorkspaceLogo } from "@/components/workspace/logo";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
// plane web imports
|
||||
import { SubscriptionPill } from "@/plane-web/components/common/subscription/subscription-pill";
|
||||
|
||||
export const SettingsSidebarHeader = observer((props: { customHeader?: React.ReactNode }) => {
|
||||
const { customHeader } = props;
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
return customHeader
|
||||
? customHeader
|
||||
: currentWorkspace && (
|
||||
<div className="flex w-full gap-3 items-center justify-between px-2">
|
||||
<div className="flex w-full gap-3 items-center overflow-hidden">
|
||||
<WorkspaceLogo
|
||||
logo={currentWorkspace.logo_url ?? ""}
|
||||
name={currentWorkspace.name ?? ""}
|
||||
classNames="size-8 border border-custom-border-200"
|
||||
/>
|
||||
<div className="w-full overflow-hidden">
|
||||
<div className="text-base font-medium text-custom-text-200 truncate text-ellipsis ">
|
||||
{currentWorkspace.name ?? "Workspace"}
|
||||
</div>
|
||||
<div className="text-sm text-custom-text-300 capitalize">
|
||||
{getUserRole(currentWorkspace.role)?.toLowerCase() || "guest"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<SubscriptionPill />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
1
apps/web/core/components/settings/sidebar/index.ts
Normal file
1
apps/web/core/components/settings/sidebar/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
95
apps/web/core/components/settings/sidebar/nav-item.tsx
Normal file
95
apps/web/core/components/settings/sidebar/nav-item.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Disclosure } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import type { EUserWorkspaceRoles } from "@plane/types";
|
||||
import { cn, joinUrlPath } from "@plane/utils";
|
||||
// hooks
|
||||
import { useUserSettings } from "@/hooks/store/user";
|
||||
|
||||
export type TSettingItem = {
|
||||
key: string;
|
||||
i18n_label: string;
|
||||
href: string;
|
||||
access?: EUserWorkspaceRoles[];
|
||||
icon?: React.ReactNode;
|
||||
};
|
||||
export type TSettingsSidebarNavItemProps = {
|
||||
workspaceSlug: string;
|
||||
setting: TSettingItem;
|
||||
isActive: boolean | ((data: { href: string }) => boolean);
|
||||
actionIcons?: (props: { type: string; size?: number; className?: string }) => React.ReactNode;
|
||||
appendItemsToTitle?: (key: string) => React.ReactNode;
|
||||
renderChildren?: (key: string) => React.ReactNode;
|
||||
};
|
||||
|
||||
const SettingsSidebarNavItem = observer((props: TSettingsSidebarNavItemProps) => {
|
||||
const { workspaceSlug, setting, isActive, actionIcons, appendItemsToTitle, renderChildren } = props;
|
||||
// router
|
||||
const { projectId } = useParams();
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
// state
|
||||
const [isExpanded, setIsExpanded] = useState(projectId === setting.key);
|
||||
// hooks
|
||||
const { toggleSidebar } = useUserSettings();
|
||||
// derived
|
||||
const buttonClass = cn(
|
||||
"flex w-full items-center px-2 py-1.5 rounded text-custom-text-200 justify-between",
|
||||
"hover:bg-custom-primary-100/10",
|
||||
{
|
||||
"text-custom-text-200 bg-custom-background-80/75": typeof isActive === "function" ? isActive(setting) : isActive,
|
||||
"text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-90 active:bg-custom-sidebar-background-90":
|
||||
typeof isActive === "function" ? !isActive(setting) : !isActive,
|
||||
}
|
||||
);
|
||||
|
||||
const titleElement = (
|
||||
<>
|
||||
<div className="flex items-center gap-1.5 overflow-hidden">
|
||||
{setting.icon
|
||||
? setting.icon
|
||||
: actionIcons && actionIcons({ type: setting.key, size: 16, className: "w-4 h-4" })}
|
||||
<div className="text-sm font-medium truncate">{t(setting.i18n_label)}</div>
|
||||
</div>
|
||||
{appendItemsToTitle?.(setting.key)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Disclosure as="div" className="flex flex-col w-full" defaultOpen={isExpanded} key={setting.key}>
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className={cn(
|
||||
"group w-full flex items-center gap-1 whitespace-nowrap text-left text-sm font-semibold text-custom-sidebar-text-400"
|
||||
)}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{renderChildren ? (
|
||||
<div className={buttonClass}>{titleElement}</div>
|
||||
) : (
|
||||
<Link
|
||||
href={joinUrlPath(workspaceSlug, setting.href)}
|
||||
className={buttonClass}
|
||||
onClick={() => toggleSidebar(true)}
|
||||
>
|
||||
{titleElement}
|
||||
</Link>
|
||||
)}
|
||||
</Disclosure.Button>
|
||||
{/* Nested Navigation */}
|
||||
{isExpanded && (
|
||||
<Disclosure.Panel as="div" className={cn("relative flex flex-col gap-0.5 mt-1 pl-6 mb-1.5")} static>
|
||||
<div className="absolute left-[15px] top-0 bottom-1 w-[1px] bg-custom-border-200" />
|
||||
{renderChildren?.(setting.key)}
|
||||
</Disclosure.Panel>
|
||||
)}
|
||||
</Disclosure>
|
||||
);
|
||||
});
|
||||
|
||||
export default SettingsSidebarNavItem;
|
||||
77
apps/web/core/components/settings/sidebar/root.tsx
Normal file
77
apps/web/core/components/settings/sidebar/root.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { cn } from "@plane/utils";
|
||||
import { SettingsSidebarHeader } from "./header";
|
||||
import type { TSettingItem } from "./nav-item";
|
||||
import SettingsSidebarNavItem from "./nav-item";
|
||||
|
||||
interface SettingsSidebarProps {
|
||||
isMobile?: boolean;
|
||||
customHeader?: React.ReactNode;
|
||||
categories: string[];
|
||||
groupedSettings: {
|
||||
[key: string]: TSettingItem[];
|
||||
};
|
||||
workspaceSlug: string;
|
||||
isActive: boolean | ((data: { href: string }) => boolean);
|
||||
shouldRender: boolean | ((setting: TSettingItem) => boolean);
|
||||
actionIcons?: (props: { type: string; size?: number; className?: string }) => React.ReactNode;
|
||||
appendItemsToTitle?: (key: string) => React.ReactNode;
|
||||
renderChildren?: (key: string) => React.ReactNode;
|
||||
}
|
||||
|
||||
export const SettingsSidebar = observer((props: SettingsSidebarProps) => {
|
||||
const {
|
||||
isMobile = false,
|
||||
customHeader,
|
||||
categories,
|
||||
groupedSettings,
|
||||
workspaceSlug,
|
||||
isActive,
|
||||
shouldRender,
|
||||
actionIcons,
|
||||
appendItemsToTitle,
|
||||
renderChildren,
|
||||
} = props;
|
||||
// hooks
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("flex w-[250px] flex-col gap-2 flex-shrink-0 overflow-y-scroll h-full md:pt-page-y ", {
|
||||
"absolute left-0 top-[42px] z-50 h-fit max-h-[400px] overflow-scroll bg-custom-background-100 border border-custom-border-100 rounded shadow-sm p-4":
|
||||
isMobile,
|
||||
})}
|
||||
>
|
||||
{/* Header */}
|
||||
<SettingsSidebarHeader customHeader={customHeader} />
|
||||
{/* Navigation */}
|
||||
<div className="divide-y divide-custom-border-100 overflow-x-hidden w-full h-full overflow-y-scroll vertical-scrollbar scrollbar-sm">
|
||||
{categories.map((category) => {
|
||||
if (groupedSettings[category].length === 0) return null;
|
||||
return (
|
||||
<div key={category} className="py-3">
|
||||
<span className="text-sm font-semibold text-custom-text-350 capitalize mb-2 px-2">{t(category)}</span>
|
||||
<div className="relative flex flex-col gap-0.5 h-full mt-2">
|
||||
{groupedSettings[category].map(
|
||||
(setting) =>
|
||||
(typeof shouldRender === "function" ? shouldRender(setting) : shouldRender) && (
|
||||
<SettingsSidebarNavItem
|
||||
key={setting.key}
|
||||
setting={setting}
|
||||
workspaceSlug={workspaceSlug}
|
||||
isActive={isActive}
|
||||
appendItemsToTitle={appendItemsToTitle}
|
||||
renderChildren={renderChildren}
|
||||
actionIcons={actionIcons}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user