chore: language support phase 2 (#6323)

* fix: adding langauge support for sidebar items

* fix: worksapce sidebar item refactor

* chore: code cleanup

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
This commit is contained in:
Prateek Shourya 2025-01-06 15:32:11 +05:30 committed by GitHub
parent 732963b591
commit a6216beb7e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 431 additions and 356 deletions

View file

@ -0,0 +1 @@
export const SIDEBAR_CLICKED = "Sidenav clicked";

View file

@ -1,6 +1,7 @@
export * from "./ai";
export * from "./auth";
export * from "./endpoints";
export * from "./event";
export * from "./file";
export * from "./instance";
export * from "./issue";

View file

@ -19,3 +19,20 @@ export type TUserStatus = {
status: EUserStatus | undefined;
message?: string;
};
export enum EUserPermissionsLevel {
WORKSPACE = "WORKSPACE",
PROJECT = "PROJECT",
}
export enum EUserWorkspaceRoles {
ADMIN = 20,
MEMBER = 15,
GUEST = 5,
}
export enum EUserProjectRoles {
ADMIN = 20,
MEMBER = 15,
GUEST = 5,
}

View file

@ -1,2 +1,3 @@
export * from "./use-local-storage";
export * from "./use-outside-click-detector";
export * from "./use-platform-os";

View file

@ -0,0 +1,34 @@
import { useState, useEffect } from "react";
export const usePlatformOS = () => {
const [platformData, setPlatformData] = useState({
isMobile: false,
platform: "",
});
useEffect(() => {
const detectPlatform = () => {
const userAgent = window.navigator.userAgent;
const isMobile = /iPhone|iPad|iPod|Android/i.test(userAgent);
let platform = "";
if (!isMobile) {
if (userAgent.indexOf("Win") !== -1) {
platform = "Windows";
} else if (userAgent.indexOf("Mac") !== -1) {
platform = "MacOS";
} else if (userAgent.indexOf("Linux") !== -1) {
platform = "Linux";
} else {
platform = "Unknown";
}
}
setPlatformData({ isMobile, platform });
};
detectPlatform();
}, []);
return platformData;
};

View file

@ -43,6 +43,7 @@
"activity": "Activity",
"appearance": "Appearance",
"notifications": "Notifications",
"inbox": "Inbox",
"workspaces": "Workspaces",
"create_workspace": "Create workspace",
"invitations": "Invitations",

View file

@ -314,5 +314,6 @@
"change_parent_issue": "Cambiar problema padre",
"remove_parent_issue": "Eliminar problema padre",
"add_parent": "Agregar padre",
"loading_members": "Cargando miembros..."
"loading_members": "Cargando miembros...",
"inbox": "bandeja de entrada"
}

View file

@ -314,5 +314,6 @@
"change_parent_issue": "Modifier le problème parent",
"remove_parent_issue": "Supprimer le problème parent",
"add_parent": "Ajouter un parent",
"loading_members": "Chargement des membres..."
"loading_members": "Chargement des membres...",
"inbox": "boîte de réception"
}

View file

@ -314,5 +314,6 @@
"change_parent_issue": "親問題を変更",
"remove_parent_issue": "親問題を削除",
"add_parent": "親問題を追加",
"loading_members": "メンバーを読み込んでいます..."
"loading_members": "メンバーを読み込んでいます...",
"inbox": "受信箱"
}

View file

@ -1,113 +0,0 @@
"use client";
// icons
import { Briefcase, Home, Inbox, Layers, PenSquare, BarChart2 } from "lucide-react";
// ui
import { UserActivityIcon, ContrastIcon } from "@plane/ui";
import { Props } from "@/components/icons/types";
// constants
import { TLinkOptions } from "@/constants/dashboard";
// plane web constants
import { EUserPermissions } from "@/plane-web/constants/user-permissions";
// plane web types
import { TSidebarUserMenuItemKeys, TSidebarWorkspaceMenuItemKeys } from "@/plane-web/types/dashboard";
export type TSidebarMenuItems<T extends TSidebarUserMenuItemKeys | TSidebarWorkspaceMenuItemKeys> = {
value: T;
label: string;
key: string;
href: string;
access: EUserPermissions[];
highlight: (pathname: string, baseUrl: string, options?: TLinkOptions) => boolean;
Icon: React.FC<Props>;
};
export type TSidebarUserMenuItems = TSidebarMenuItems<TSidebarUserMenuItemKeys>;
export const SIDEBAR_USER_MENU_ITEMS: TSidebarUserMenuItems[] = [
{
value: "home",
label: "Home",
key: "home",
href: ``,
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/`,
Icon: Home,
},
{
value: "your-work",
label: "Your work",
key: "your_work",
href: "/profile",
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
highlight: (pathname: string, baseUrl: string, options?: TLinkOptions) =>
options?.userId ? pathname.includes(`${baseUrl}/profile/${options?.userId}`) : false,
Icon: UserActivityIcon,
},
{
value: "notifications",
label: "Inbox",
key: "notifications",
href: `/notifications`,
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/notifications/`),
Icon: Inbox,
},
{
value: "drafts",
label: "Drafts",
key: "drafts",
href: `/drafts`,
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/drafts/`),
Icon: PenSquare,
},
];
export type TSidebarWorkspaceMenuItems = TSidebarMenuItems<TSidebarWorkspaceMenuItemKeys>;
export const SIDEBAR_WORKSPACE_MENU: Partial<Record<TSidebarWorkspaceMenuItemKeys, TSidebarWorkspaceMenuItems>> = {
projects: {
value: "projects",
key: "projects",
label: "Projects",
href: `/projects`,
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/projects/`,
Icon: Briefcase,
},
"all-issues": {
value: "all-issues",
key: "views",
label: "Views",
href: `/workspace-views/all-issues`,
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/workspace-views/`),
Icon: Layers,
},
"active-cycles": {
value: "active-cycles",
key: "active_cycles",
label: "Cycles",
href: `/active-cycles`,
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/active-cycles/`,
Icon: ContrastIcon,
},
analytics: {
value: "analytics",
key: "analytics",
label: "Analytics",
href: `/analytics`,
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/analytics/`),
Icon: BarChart2,
},
};
export const SIDEBAR_WORKSPACE_MENU_ITEMS: TSidebarWorkspaceMenuItems[] = [
SIDEBAR_WORKSPACE_MENU?.projects,
SIDEBAR_WORKSPACE_MENU?.["all-issues"],
SIDEBAR_WORKSPACE_MENU?.["active-cycles"],
SIDEBAR_WORKSPACE_MENU?.analytics,
].filter((item): item is TSidebarWorkspaceMenuItems => item !== undefined);

View file

@ -1,8 +0,0 @@
// plane web types
import { TSidebarUserMenuItemKeys, TSidebarWorkspaceMenuItemKeys } from "@/plane-web/types/dashboard";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const isUserFeatureEnabled = (featureKey: TSidebarUserMenuItemKeys) => true;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const isWorkspaceFeatureEnabled = (featureKey: TSidebarWorkspaceMenuItemKeys, workspaceSlug: string) => true;

View file

@ -1,3 +0,0 @@
export type TSidebarUserMenuItemKeys = "home" | "your-work" | "notifications" | "drafts";
export type TSidebarWorkspaceMenuItemKeys = "projects" | "all-issues" | "active-cycles" | "analytics";

View file

@ -6,4 +6,7 @@ export * from "./projects-list";
export * from "./project-navigation";
export * from "./quick-actions";
export * from "./user-menu";
export * from "./user-menu-item";
export * from "./workspace-menu";
export * from "./workspace-menu-item";
export * from "./workspace-menu-header";

View file

@ -0,0 +1,84 @@
import { FC } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
// plane imports
import { EUserPermissionsLevel, SIDEBAR_CLICKED, EUserWorkspaceRoles } from "@plane/constants";
import { usePlatformOS } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
import { Tooltip } from "@plane/ui";
// components
import { SidebarNavItem } from "@/components/sidebar";
import { NotificationAppSidebarOption } from "@/components/workspace-notifications";
// hooks
import { useAppTheme, useEventTracker, useUserPermissions } from "@/hooks/store";
export interface SidebarUserMenuItemProps {
item: {
key: string;
href: string;
access: EUserWorkspaceRoles[];
labelTranslationKey: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Icon: any;
};
draftIssueCount: number;
}
export const SidebarUserMenuItem: FC<SidebarUserMenuItemProps> = observer((props) => {
const { item, draftIssueCount } = props;
// nextjs hooks
const { workspaceSlug } = useParams();
const pathname = usePathname();
// package hooks
const { t } = useTranslation();
// store hooks
const { captureEvent } = useEventTracker();
const { allowPermissions } = useUserPermissions();
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
const { isMobile } = usePlatformOS();
const isActive = pathname === item.href;
if (item.key === "drafts" && draftIssueCount === 0) return null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (!allowPermissions(item.access as any, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString())) return null;
const handleLinkClick = (itemKey: string) => {
if (window.innerWidth < 768) {
toggleSidebar();
}
captureEvent(SIDEBAR_CLICKED, {
destination: itemKey,
});
};
return (
<Tooltip
tooltipContent={t(item.labelTranslationKey)}
position="right"
className="ml-2"
disabled={!sidebarCollapsed}
isMobile={isMobile}
>
<Link href={item.href} onClick={() => handleLinkClick(item.key)}>
<SidebarNavItem
className={`${sidebarCollapsed ? "p-0 size-8 aspect-square justify-center mx-auto" : ""}`}
isActive={isActive}
>
<div className="flex items-center gap-1.5 py-[1px]">
<item.Icon className="size-4 flex-shrink-0" />
{!sidebarCollapsed && <p className="text-sm leading-5 font-medium">{t(item.labelTranslationKey)}</p>}
</div>
{item.key === "notifications" && (
<NotificationAppSidebarOption
workspaceSlug={workspaceSlug.toString()}
isSidebarCollapsed={sidebarCollapsed ?? false}
/>
)}
</SidebarNavItem>
</Link>
</Tooltip>
);
});

View file

@ -2,58 +2,54 @@
import React from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
import { useTranslation } from "@plane/i18n";
import { useParams } from "next/navigation";
import { Home, Inbox, PenSquare } from "lucide-react";
// plane imports
import { EUserWorkspaceRoles } from "@plane/constants";
import { UserActivityIcon } from "@plane/ui";
// components
import { Tooltip } from "@plane/ui";
import { SidebarNavItem } from "@/components/sidebar";
import { NotificationAppSidebarOption } from "@/components/workspace-notifications";
// constants
import { SIDEBAR_CLICKED } from "@/constants/event-tracker";
import { SidebarUserMenuItem } from "@/components/workspace/sidebar";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useAppTheme, useEventTracker, useUser, useUserPermissions } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web constants
import { SIDEBAR_USER_MENU_ITEMS } from "@/plane-web/constants/dashboard";
import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// plane web helpers
import { isUserFeatureEnabled } from "@/plane-web/helpers/dashboard.helper";
import { useAppTheme, useUserPermissions, useUser } from "@/hooks/store";
export const SidebarUserMenu = observer(() => {
const { t } = useTranslation();
// store hooks
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
const { captureEvent } = useEventTracker();
const { isMobile } = usePlatformOS();
const { data: currentUser } = useUser();
const { allowPermissions, workspaceUserInfo } = useUserPermissions();
// router params
const { workspaceSlug } = useParams();
// pathname
const pathname = usePathname();
// computed
const { sidebarCollapsed } = useAppTheme();
const { workspaceUserInfo } = useUserPermissions();
const { data: currentUser } = useUser();
const getHref = (link: any) =>
`/${workspaceSlug}${link.href}${link.key === "your-work" ? `/${currentUser?.id}` : ""}`;
const handleLinkClick = (itemKey: string) => {
if (window.innerWidth < 768) {
toggleSidebar();
}
captureEvent(SIDEBAR_CLICKED, {
destination: itemKey,
});
};
const notificationIndicatorElement = (
<NotificationAppSidebarOption
workspaceSlug={workspaceSlug.toString()}
isSidebarCollapsed={sidebarCollapsed ?? false}
/>
);
const SIDEBAR_USER_MENU_ITEMS = [
{
key: "home",
labelTranslationKey: "home",
href: `/${workspaceSlug.toString()}/`,
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
Icon: Home,
},
{
key: "your-work",
labelTranslationKey: "your_work",
href: `/${workspaceSlug.toString()}/profile/${currentUser?.id}/`,
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
Icon: UserActivityIcon,
},
{
key: "notifications",
labelTranslationKey: "inbox",
href: `/${workspaceSlug.toString()}/notifications/`,
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
Icon: Inbox,
},
{
key: "drafts",
labelTranslationKey: "drafts",
href: `/${workspaceSlug.toString()}/drafts/`,
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
Icon: PenSquare,
},
];
const draftIssueCount = workspaceUserInfo[workspaceSlug.toString()]?.draft_issue_count;
@ -63,35 +59,9 @@ export const SidebarUserMenu = observer(() => {
"space-y-0": sidebarCollapsed,
})}
>
{SIDEBAR_USER_MENU_ITEMS.map((link) => {
if (link.value === "drafts" && draftIssueCount === 0) return null;
if (!isUserFeatureEnabled(link.value)) return null;
return (
allowPermissions(link.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString()) && (
<Tooltip
key={link.value}
tooltipContent={t(link.key)}
position="right"
className="ml-2"
disabled={!sidebarCollapsed}
isMobile={isMobile}
>
<Link key={link.value} href={getHref(link)} onClick={() => handleLinkClick(link.value)}>
<SidebarNavItem
className={`${sidebarCollapsed ? "p-0 size-8 aspect-square justify-center mx-auto" : ""}`}
isActive={link.highlight(pathname, `/${workspaceSlug}`, { userId: currentUser?.id })}
>
<div className="flex items-center gap-1.5 py-[1px]">
<link.Icon className="size-4 flex-shrink-0" />
{!sidebarCollapsed && <p className="text-sm leading-5 font-medium">{t(link.key)}</p>}
</div>
{link.value === "notifications" && notificationIndicatorElement}
</SidebarNavItem>
</Link>
</Tooltip>
)
);
})}
{SIDEBAR_USER_MENU_ITEMS.map((item) => (
<SidebarUserMenuItem key={item.key} item={item} draftIssueCount={draftIssueCount} />
))}
</div>
);
});

View file

@ -0,0 +1,116 @@
import { FC, useState, useRef } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { MoreHorizontal, ArchiveIcon, ChevronRight, Settings } from "lucide-react";
import { Disclosure } from "@headlessui/react";
// plane imports
import { EUserWorkspaceRoles, EUserPermissionsLevel } from "@plane/constants";
import { useOutsideClickDetector } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
import { CustomMenu } from "@plane/ui";
import { cn } from "@plane/utils";
// store hooks
import { useAppTheme, useUserPermissions } from "@/hooks/store";
export type SidebarWorkspaceMenuHeaderProps = {
isWorkspaceMenuOpen: boolean;
toggleWorkspaceMenu: (value: boolean) => void;
};
export const SidebarWorkspaceMenuHeader: FC<SidebarWorkspaceMenuHeaderProps> = observer((props) => {
const { isWorkspaceMenuOpen, toggleWorkspaceMenu } = props;
// state
const [isMenuActive, setIsMenuActive] = useState(false);
// refs
const actionSectionRef = useRef<HTMLDivElement | null>(null);
// hooks
const { workspaceSlug } = useParams();
const { sidebarCollapsed } = useAppTheme();
const { allowPermissions } = useUserPermissions();
const { t } = useTranslation();
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
// TODO: fix types
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isAdmin = allowPermissions([EUserWorkspaceRoles.ADMIN] as any, EUserPermissionsLevel.WORKSPACE);
if (sidebarCollapsed) {
return <></>;
}
return (
<div
className={cn(
"flex px-2 bg-custom-sidebar-background-100 group/workspace-button hover:bg-custom-sidebar-background-90 rounded",
{
"mt-2.5": !sidebarCollapsed,
}
)}
>
<Disclosure.Button
as="button"
className="flex-1 sticky top-0 z-10 w-full py-1.5 flex items-center justify-between gap-1 text-custom-sidebar-text-400 text-xs font-semibold"
onClick={() => toggleWorkspaceMenu(!isWorkspaceMenuOpen)}
>
<span>{t("workspace").toUpperCase()}</span>
</Disclosure.Button>
<CustomMenu
customButton={
<span
ref={actionSectionRef}
className="grid place-items-center p-0.5 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80 rounded my-auto"
onClick={() => {
setIsMenuActive(!isMenuActive);
}}
>
<MoreHorizontal className="size-4" />
</span>
}
className={cn(
"h-full flex items-center opacity-0 z-20 pointer-events-none flex-shrink-0 group-hover/workspace-button:opacity-100 group-hover/workspace-button:pointer-events-auto my-auto",
{
"opacity-100 pointer-events-auto": isMenuActive,
}
)}
customButtonClassName="grid place-items-center"
placement="bottom-start"
>
<CustomMenu.MenuItem>
<Link href={`/${workspaceSlug}/projects/archives`}>
<div className="flex items-center justify-start gap-2">
<ArchiveIcon className="h-3.5 w-3.5 stroke-[1.5]" />
<span>{t("archives")}</span>
</div>
</Link>
</CustomMenu.MenuItem>
{isAdmin && (
<CustomMenu.MenuItem>
<Link href={`/${workspaceSlug}/settings`}>
<div className="flex items-center justify-start gap-2">
<Settings className="h-3.5 w-3.5 stroke-[1.5]" />
<span>{t("settings")}</span>
</div>
</Link>
</CustomMenu.MenuItem>
)}
</CustomMenu>
<Disclosure.Button
as="button"
className="sticky top-0 z-10 group/workspace-button px-0.5 py-1.5 flex items-center justify-between gap-1 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-90 rounded text-xs font-semibold"
onClick={() => toggleWorkspaceMenu(!isWorkspaceMenuOpen)}
>
{" "}
<span className="flex-shrink-0 opacity-0 pointer-events-none group-hover/workspace-button:opacity-100 group-hover/workspace-button:pointer-events-auto rounded hover:bg-custom-sidebar-background-80">
<ChevronRight
className={cn("size-4 flex-shrink-0 text-custom-sidebar-text-400 transition-transform", {
"rotate-90": isWorkspaceMenuOpen,
})}
/>
</span>
</Disclosure.Button>
</div>
);
});

View file

@ -0,0 +1,82 @@
import { FC } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
// plane imports
import { EUserWorkspaceRoles, EUserPermissionsLevel } from "@plane/constants";
import { usePlatformOS } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
import { Tooltip } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { SidebarNavItem } from "@/components/sidebar";
// hooks
import { useAppTheme, useUserPermissions } from "@/hooks/store";
// plane web imports
import { UpgradeBadge } from "@/plane-web/components/workspace";
export type SidebarWorkspaceMenuItemProps = {
item: {
labelTranslationKey: string;
key: string;
href: string;
Icon: any;
access: EUserWorkspaceRoles[];
};
};
export const SidebarWorkspaceMenuItem: FC<SidebarWorkspaceMenuItemProps> = observer((props) => {
const { item } = props;
const { t } = useTranslation();
// nextjs hooks
const pathname = usePathname();
const { workspaceSlug } = useParams();
const { allowPermissions } = useUserPermissions();
// store hooks
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
const { isMobile } = usePlatformOS();
const handleLinkClick = () => {
if (window.innerWidth < 768) {
toggleSidebar();
}
};
if (!allowPermissions(item.access as any, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString())) {
return null;
}
const isActive = item.href === pathname;
return (
<Tooltip
tooltipContent={t(item.labelTranslationKey)}
position="right"
className="ml-2"
disabled={!sidebarCollapsed}
isMobile={isMobile}
>
<Link href={item.href} onClick={() => handleLinkClick()}>
<SidebarNavItem
className={`${sidebarCollapsed ? "p-0 size-8 aspect-square justify-center mx-auto" : ""}`}
isActive={isActive}
>
<div className="flex items-center gap-1.5 py-[1px]">
<item.Icon
className={cn("size-4", {
"rotate-180": item.key === "active-cycles",
})}
/>
{!sidebarCollapsed && <p className="text-sm leading-5 font-medium">{t(item.labelTranslationKey)}</p>}
</div>
{!sidebarCollapsed && item.key === "active-cycles" && (
<div className="flex-shrink-0">
<UpgradeBadge />
</div>
)}
</SidebarNavItem>
</Link>
</Tooltip>
);
});

View file

@ -1,150 +1,69 @@
"use client";
import React, { useEffect, useRef, useState } from "react";
import React, { useEffect } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
import { ArchiveIcon, ChevronRight, MoreHorizontal, Settings } from "lucide-react";
import { useParams } from "next/navigation";
import { BarChart2, Briefcase, Layers } from "lucide-react";
import { Disclosure, Transition } from "@headlessui/react";
import { useOutsideClickDetector } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
// ui
import { CustomMenu, Tooltip } from "@plane/ui";
import { EUserWorkspaceRoles } from "@plane/constants";
import { ContrastIcon } from "@plane/ui";
// components
import { SidebarNavItem } from "@/components/sidebar";
// constants
import { SIDEBAR_CLICKED } from "@/constants/event-tracker";
import { SidebarWorkspaceMenuHeader, SidebarWorkspaceMenuItem } from "@/components/workspace/sidebar";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useAppTheme, useEventTracker, useUserPermissions } from "@/hooks/store";
import { useAppTheme } from "@/hooks/store";
import useLocalStorage from "@/hooks/use-local-storage";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web components
import { UpgradeBadge } from "@/plane-web/components/workspace";
// plane web constants
import { SIDEBAR_WORKSPACE_MENU_ITEMS } from "@/plane-web/constants/dashboard";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// plane web hooks
import { isWorkspaceFeatureEnabled } from "@/plane-web/helpers/dashboard.helper";
export const SidebarWorkspaceMenu = observer(() => {
// state
const [isMenuActive, setIsMenuActive] = useState(false);
// refs
const actionSectionRef = useRef<HTMLDivElement | null>(null);
// router params
const { workspaceSlug } = useParams();
// pathname
const pathname = usePathname();
// store hooks
const { t } = useTranslation();
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
const { captureEvent } = useEventTracker();
const { isMobile } = usePlatformOS();
const { allowPermissions } = useUserPermissions();
const { sidebarCollapsed } = useAppTheme();
// local storage
const { setValue: toggleWorkspaceMenu, storedValue } = useLocalStorage<boolean>("is_workspace_menu_open", true);
// derived values
const isWorkspaceMenuOpen = !!storedValue;
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
const handleLinkClick = (itemKey: string) => {
if (window.innerWidth < 768) {
toggleSidebar();
}
captureEvent(SIDEBAR_CLICKED, {
destination: itemKey,
});
};
useEffect(() => {
if (sidebarCollapsed) toggleWorkspaceMenu(true);
}, [sidebarCollapsed, toggleWorkspaceMenu]);
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
const indicatorElement = (
<div className="flex-shrink-0">
<UpgradeBadge />
</div>
);
const SIDEBAR_WORKSPACE_MENU_ITEMS = [
{
key: "projects",
labelTranslationKey: "projects",
href: `/${workspaceSlug}/projects/`,
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
Icon: Briefcase,
},
{
key: "views",
labelTranslationKey: "views",
href: `/${workspaceSlug}/workspace-views/all-issues/`,
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
Icon: Layers,
},
{
key: "active-cycles",
labelTranslationKey: "cycles",
href: `/${workspaceSlug}/active-cycles/`,
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
Icon: ContrastIcon,
},
{
key: "analytics",
labelTranslationKey: "analytics",
href: `/${workspaceSlug}/analytics/`,
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
Icon: BarChart2,
},
];
return (
<Disclosure as="div" defaultOpen>
{!sidebarCollapsed && (
<div
className={cn(
"flex px-2 bg-custom-sidebar-background-100 group/workspace-button hover:bg-custom-sidebar-background-90 rounded",
{
"mt-2.5": !sidebarCollapsed,
}
)}
>
{" "}
<Disclosure.Button
as="button"
className="flex-1 sticky top-0 z-10 w-full py-1.5 flex items-center justify-between gap-1 text-custom-sidebar-text-400 text-xs font-semibold"
onClick={() => toggleWorkspaceMenu(!isWorkspaceMenuOpen)}
>
<span>{t("workspace").toUpperCase()}</span>
</Disclosure.Button>
<CustomMenu
customButton={
<span
ref={actionSectionRef}
className="grid place-items-center p-0.5 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80 rounded my-auto"
onClick={() => {
setIsMenuActive(!isMenuActive);
}}
>
<MoreHorizontal className="size-4" />
</span>
}
className={cn(
"h-full flex items-center opacity-0 z-20 pointer-events-none flex-shrink-0 group-hover/workspace-button:opacity-100 group-hover/workspace-button:pointer-events-auto my-auto",
{
"opacity-100 pointer-events-auto": isMenuActive,
}
)}
customButtonClassName="grid place-items-center"
placement="bottom-start"
>
<CustomMenu.MenuItem>
<Link href={`/${workspaceSlug}/projects/archives`}>
<div className="flex items-center justify-start gap-2">
<ArchiveIcon className="h-3.5 w-3.5 stroke-[1.5]" />
<span>{t("archives")}</span>
</div>
</Link>
</CustomMenu.MenuItem>
{isAdmin && (
<CustomMenu.MenuItem>
<Link href={`/${workspaceSlug}/settings`}>
<div className="flex items-center justify-start gap-2">
<Settings className="h-3.5 w-3.5 stroke-[1.5]" />
<span>{t("settings")}</span>
</div>
</Link>
</CustomMenu.MenuItem>
)}
</CustomMenu>
<Disclosure.Button
as="button"
className="sticky top-0 z-10 group/workspace-button px-0.5 py-1.5 flex items-center justify-between gap-1 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-90 rounded text-xs font-semibold"
onClick={() => toggleWorkspaceMenu(!isWorkspaceMenuOpen)}
>
{" "}
<span className="flex-shrink-0 opacity-0 pointer-events-none group-hover/workspace-button:opacity-100 group-hover/workspace-button:pointer-events-auto rounded hover:bg-custom-sidebar-background-80">
<ChevronRight
className={cn("size-4 flex-shrink-0 text-custom-sidebar-text-400 transition-transform", {
"rotate-90": isWorkspaceMenuOpen,
})}
/>
</span>
</Disclosure.Button>
</div>
)}
<SidebarWorkspaceMenuHeader isWorkspaceMenuOpen={isWorkspaceMenuOpen} toggleWorkspaceMenu={toggleWorkspaceMenu} />
<Transition
show={isWorkspaceMenuOpen}
enter="transition duration-100 ease-out"
@ -162,39 +81,9 @@ export const SidebarWorkspaceMenu = observer(() => {
})}
static
>
{SIDEBAR_WORKSPACE_MENU_ITEMS.map((link) => {
if (!isWorkspaceFeatureEnabled(link.value, workspaceSlug.toString())) return null;
return (
allowPermissions(link.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString()) && (
<Tooltip
key={link.value}
tooltipContent={t(link.key)}
position="right"
className="ml-2"
disabled={!sidebarCollapsed}
isMobile={isMobile}
>
<Link href={`/${workspaceSlug}${link.href}`} onClick={() => handleLinkClick(link.value)}>
<SidebarNavItem
key={link.value}
className={`${sidebarCollapsed ? "p-0 size-8 aspect-square justify-center mx-auto" : ""}`}
isActive={link.highlight(pathname, `/${workspaceSlug}`)}
>
<div className="flex items-center gap-1.5 py-[1px]">
<link.Icon
className={cn("size-4", {
"rotate-180": link.value === "active-cycles",
})}
/>
{!sidebarCollapsed && <p className="text-sm leading-5 font-medium">{t(link.key)}</p>}
</div>
{!sidebarCollapsed && link.value === "active-cycles" && indicatorElement}
</SidebarNavItem>
</Link>
</Tooltip>
)
);
})}
{SIDEBAR_WORKSPACE_MENU_ITEMS.map((item) => (
<SidebarWorkspaceMenuItem key={item.key} item={item} />
))}
</Disclosure.Panel>
)}
</Transition>

View file

@ -1 +0,0 @@
export * from "ce/constants/dashboard";

View file

@ -1 +0,0 @@
export * from "ce/helpers/dashboard.helper";

View file

@ -1 +0,0 @@
export * from "ce/types/dashboard";