Initial commit: Plane
Some checks failed
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Admin Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Sync Repositories / sync_changes (push) Has been cancelled

Synced from upstream: 8853637e981ed7d8a6cff32bd98e7afe20f54362
This commit is contained in:
chuan
2025-11-07 00:00:52 +08:00
commit 8ebde8aa05
4886 changed files with 462270 additions and 0 deletions

View 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>
);
});

View File

@@ -0,0 +1 @@
export * from "./root";

View 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;

View 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>
);
});