[WEB-4328] feat: sidebar revamp (#7217)

* chore: sidebar peek state added to theme store

* chore: extended sidebar wrapper added

* chore: resizeable sidebar component added

* chore: appsidebar root component

* chore: updated sidebar and applied necessary changes across codebase

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: breadcrumb changes

* chore: sidebar improvements and fixes

* chore: enhancements and fixes

* fix: peek sidebar leave

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: icons added

* chore: add dock variable and toggle function to theme store

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: theme and workspace store updated

* chore: workspace content wrapper and apprail context

* chore: workspace and project wrapper updated

* chore: app rail component

* chore: content wrapper

* chore: sidebar component updated

* chore: layout changes and code refactoring

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: appsidebar toggle button

* chore: code refactor

* chore: workspace menu improvements

* chore: sidebar spacing and padding improvements

* chore: settings layout improvement

* chore: enhancements

* chore: extended sidebar code refactor

* chore: code refactor

* fix: merge conflict

* fix: merge conflict

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: code refactor
This commit is contained in:
Anmol Singh Bhatia 2025-07-08 20:18:39 +05:30 committed by GitHub
parent fd9da3164e
commit 0225d806cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
69 changed files with 2126 additions and 1143 deletions

View file

@ -1,27 +1,31 @@
"use client";
import { ReactNode } from "react";
// components
import { observer } from "mobx-react";
// plane imports
import { Row } from "@plane/ui";
import { SidebarHamburgerToggle } from "@/components/core";
// components
import { AppSidebarToggleButton } from "@/components/sidebar";
// hooks
import { useAppTheme } from "@/hooks/store";
export interface AppHeaderProps {
header: ReactNode;
mobileHeader?: ReactNode;
}
export const AppHeader = (props: AppHeaderProps) => {
export const AppHeader = observer((props: AppHeaderProps) => {
const { header, mobileHeader } = props;
// store hooks
const { sidebarCollapsed } = useAppTheme();
return (
<div className="z-[18]">
<Row className="h-[3.75rem] flex gap-2 w-full items-center border-b border-custom-border-200 bg-custom-sidebar-background-100">
<div className="block bg-custom-sidebar-background-100 md:hidden">
<SidebarHamburgerToggle />
</div>
{sidebarCollapsed && <AppSidebarToggleButton />}
<div className="w-full">{header}</div>
</Row>
{mobileHeader && mobileHeader}
</div>
);
};
});

View file

@ -1,20 +1,26 @@
"use client";
import { observer } from "mobx-react";
import { Menu } from "lucide-react";
import { PanelRight } from "lucide-react";
import { useAppTheme } from "@/hooks/store";
export const SidebarHamburgerToggle = observer(() => {
// store hooks
const { toggleSidebar } = useAppTheme();
const handleClick = (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
toggleSidebar();
};
return (
<button
type="button"
className="group flex-shrink-0 size-7 grid place-items-center rounded bg-custom-background-80 transition-all hover:bg-custom-background-90 md:hidden"
onClick={() => toggleSidebar()}
className="group flex-shrink-0 size-7 grid place-items-center rounded hover:bg-custom-background-80 transition-all bg-custom-background-90"
onClick={handleClick}
>
<Menu className="size-3.5 text-custom-text-200 transition-all group-hover:text-custom-text-100" />
<PanelRight className="size-3.5 text-custom-text-200 transition-all group-hover:text-custom-text-100" />
</button>
);
});

View file

@ -2,6 +2,7 @@
import { observer } from "mobx-react";
import Link from "next/link";
import { useTheme } from "next-themes";
import { ChevronLeftIcon } from "lucide-react";
import { useTranslation } from "@plane/i18n";
import { getButtonStyling } from "@plane/ui/src/button";
@ -15,16 +16,16 @@ export const SettingsHeader = observer(() => {
const { t } = useTranslation();
const { currentWorkspace } = useWorkspace();
const { isScrolled } = useUserSettings();
// resolved theme
const { resolvedTheme } = useTheme();
// redirect url for normal mode
return (
<div
className={cn(
"bg-custom-background-90 px-4 py-4 gap-2 md:px-12 md:py-8 transition-all duration-300 ease-in-out relative",
{
"!pt-4 flex md:flex-col": isScrolled,
}
)}
className={cn("bg-custom-background-90 p-page-x transition-all duration-300 ease-in-out relative", {
"!pt-4 flex md:flex-col": isScrolled,
"bg-custom-background-90/50": resolvedTheme === "dark",
})}
>
<Link
href={`/${currentWorkspace?.slug}`}
@ -41,7 +42,7 @@ export const SettingsHeader = observer(() => {
<Link
href={`/${currentWorkspace?.slug}`}
className={cn(
"group flex gap-2 text-custom-text-300 mb-4 border border-transparent w-fit rounded-lg",
"group flex gap-2 text-custom-text-300 mb-3 border border-transparent w-fit rounded-lg",
!isScrolled ? "hover:bg-custom-background-100 hover:border-custom-border-200 items-center pr-2 " : " h-0 m-0"
)}
>

View file

@ -1,5 +1,6 @@
import { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
import { ScrollArea } from "@plane/ui";
import { cn } from "@plane/utils";
import { SettingsSidebarHeader } from "./header";
import SettingsSidebarNavItem, { TSettingItem } from "./nav-item";
@ -45,12 +46,15 @@ export const SettingsSidebar = observer((props: SettingsSidebarProps) => {
{/* Header */}
<SettingsSidebarHeader customHeader={customHeader} />
{/* Navigation */}
<div className="divide-y divide-custom-border-100 overflow-x-hidden w-full h-full overflow-y-scroll">
<ScrollArea
className="divide-y divide-custom-border-100 overflow-x-hidden w-full h-full overflow-y-scroll"
type="hover"
>
{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">{t(category)}</span>
<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) =>
@ -70,7 +74,7 @@ export const SettingsSidebar = observer((props: SettingsSidebarProps) => {
</div>
);
})}
</div>
</ScrollArea>
</div>
);
});

View file

@ -1 +1,4 @@
export * from "./sidebar-navigation";
export * from "./sidebar-navigation";
export * from "./resizable-sidebar";
export * from "./sidebar-item";
export * from "./sidebar-toggle-button";

View file

@ -0,0 +1,287 @@
"use client";
import React, { Dispatch, ReactElement, SetStateAction, useCallback, useEffect, useState, useRef } from "react";
// helpers
import { cn } from "@plane/utils";
interface ResizableSidebarProps {
showPeek?: boolean;
togglePeek: (value?: boolean) => void;
isCollapsed?: boolean;
width: number;
setWidth: Dispatch<SetStateAction<number>>;
defaultWidth?: number;
minWidth?: number;
maxWidth?: number;
defaultCollapsed?: boolean;
peekDuration?: number;
toggleCollapsed: (value?: boolean) => void;
onWidthChange?: (width: number) => void;
onCollapsedChange?: (collapsed: boolean) => void;
className?: string;
children?: ReactElement;
extendedSidebar?: ReactElement;
isAnyExtendedSidebarExpanded?: boolean;
isAnySidebarDropdownOpen?: boolean;
disablePeekTrigger?: boolean;
}
export function ResizableSidebar({
showPeek = false,
togglePeek,
peekDuration = 500,
isCollapsed = false,
toggleCollapsed: toggleCollapsedProp,
onCollapsedChange,
width,
setWidth,
onWidthChange,
minWidth = 236,
maxWidth = 350,
className = "",
children,
extendedSidebar,
isAnyExtendedSidebarExpanded = false,
isAnySidebarDropdownOpen = false,
disablePeekTrigger = false,
}: ResizableSidebarProps) {
// states
const [isResizing, setIsResizing] = useState(false);
const [isHoveringTrigger, setIsHoveringTrigger] = useState(false);
// refs
const peekTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
// handlers
const setShowPeek = useCallback(
(value: boolean) => {
togglePeek(value);
},
[togglePeek]
);
const handleResize = useCallback(
(e: MouseEvent) => {
if (!isResizing) return;
const newWidth = Math.min(Math.max(e.clientX, minWidth), maxWidth);
setWidth(newWidth);
},
[isResizing, minWidth, maxWidth, setWidth]
);
const startResizing = useCallback(() => {
setIsResizing(true);
}, []);
const stopResizing = useCallback(() => {
setIsResizing(false);
}, []);
const toggleCollapsed = useCallback(() => {
toggleCollapsedProp();
setShowPeek(false);
setIsHoveringTrigger(false);
if (peekTimeoutRef.current) {
clearTimeout(peekTimeoutRef.current);
}
}, [toggleCollapsedProp, setShowPeek]);
const handleTriggerEnter = useCallback(() => {
if (isCollapsed) {
setIsHoveringTrigger(true);
setShowPeek(true);
if (peekTimeoutRef.current) {
clearTimeout(peekTimeoutRef.current);
}
}
}, [isCollapsed, setShowPeek]);
const handleTriggerLeave = useCallback(() => {
if (isCollapsed && !isAnyExtendedSidebarExpanded) {
setIsHoveringTrigger(false);
peekTimeoutRef.current = setTimeout(() => {
setShowPeek(false);
}, peekDuration);
}
}, [isCollapsed, peekDuration, setShowPeek, isAnyExtendedSidebarExpanded]);
const handlePeekEnter = useCallback(() => {
if (isCollapsed && showPeek) {
if (peekTimeoutRef.current) {
clearTimeout(peekTimeoutRef.current);
}
}
}, [isCollapsed, showPeek]);
const handlePeekLeave = useCallback(() => {
if (isCollapsed && !isAnyExtendedSidebarExpanded && !isAnySidebarDropdownOpen) {
peekTimeoutRef.current = setTimeout(() => {
setShowPeek(false);
}, peekDuration);
}
}, [isCollapsed, peekDuration, setShowPeek, isAnyExtendedSidebarExpanded, isAnySidebarDropdownOpen]);
// Set up event listeners for resizing
useEffect(() => {
if (isResizing) {
document.addEventListener("mousemove", handleResize);
document.addEventListener("mouseup", stopResizing);
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
}
return () => {
document.removeEventListener("mousemove", handleResize);
document.removeEventListener("mouseup", stopResizing);
document.body.style.cursor = "";
document.body.style.userSelect = "";
};
}, [isResizing, handleResize, stopResizing]);
// Clean up timeout on unmount
useEffect(
() => () => {
if (peekTimeoutRef.current) {
clearTimeout(peekTimeoutRef.current);
}
},
[]
);
useEffect(() => {
if (!isAnySidebarDropdownOpen && isCollapsed && isHoveringTrigger) {
handlePeekLeave();
}
}, [isAnySidebarDropdownOpen]);
useEffect(() => {
if (!isAnyExtendedSidebarExpanded && isCollapsed && isHoveringTrigger) {
handlePeekLeave();
}
}, [isAnyExtendedSidebarExpanded]);
// Reset peek when sidebar is expanded
useEffect(() => {
if (!isCollapsed) {
setShowPeek(false);
setIsHoveringTrigger(false);
if (peekTimeoutRef.current) {
clearTimeout(peekTimeoutRef.current);
}
}
}, [isCollapsed, setShowPeek]);
// Call external handlers when state changes
useEffect(() => {
onWidthChange?.(width);
}, [width, onWidthChange]);
useEffect(() => {
onCollapsedChange?.(isCollapsed);
}, [isCollapsed, onCollapsedChange]);
return (
<>
{/* Main Sidebar */}
<div
className={cn(
"h-full z-20 bg-custom-background-100 border-r border-custom-sidebar-border-200",
!isResizing && "transition-all duration-300 ease-in-out",
isCollapsed ? "translate-x-[-100%] opacity-0 w-0" : "translate-x-0 opacity-100",
className
)}
style={{
width: `${isCollapsed ? 0 : width}px`,
minWidth: `${isCollapsed ? 0 : width}px`,
maxWidth: `${isCollapsed ? 0 : width}px`,
}}
role="complementary"
aria-label="Main sidebar"
>
<aside
className={cn(
"group/sidebar h-full w-full bg-custom-background-100 overflow-hidden relative flex flex-col pt-3",
isAnyExtendedSidebarExpanded && "rounded-none"
)}
>
{children}
{/* Resize Handle */}
<div
className={cn(
"transition-all duration-200 cursor-ew-resize absolute h-full w-1 z-[20]",
!isResizing && "hover:bg-custom-background-90",
isResizing && "w-1.5 bg-custom-background-80",
"top-0 right-0"
)}
// onDoubleClick toggle sidebar
onDoubleClick={() => toggleCollapsed()}
onMouseDown={startResizing}
role="separator"
aria-label="Resize sidebar"
/>
</aside>
</div>
{/* Peek Trigger Area */}
{isCollapsed && !disablePeekTrigger && (
<div
className={cn(
"absolute top-0 left-0 w-1 h-full z-50 bg-transparent",
"transition-opacity duration-200",
isHoveringTrigger ? "opacity-100" : "opacity-0"
)}
onMouseEnter={handleTriggerEnter}
onMouseLeave={handleTriggerLeave}
role="button"
aria-label="Show sidebar peek"
/>
)}
{/* Peek View */}
<div
className={cn(
"absolute left-0 z-20 bg-custom-background-100 shadow-sm h-full",
!isResizing && "transition-all duration-300 ease-in-out",
isCollapsed && showPeek ? "translate-x-0 opacity-100" : "translate-x-[-100%] opacity-0",
"pointer-events-none",
isCollapsed && showPeek && "pointer-events-auto",
!showPeek ? "w-0" : "w-full"
)}
style={{
width: `${width}px`,
}}
onMouseEnter={handlePeekEnter}
onMouseLeave={handlePeekLeave}
role="complementary"
aria-label="Sidebar peek view"
>
<aside
className={cn(
"group/sidebar h-full w-full bg-custom-background-100 overflow-hidden relative flex flex-col z-20 pt-4",
"self-center border-r border-custom-sidebar-border-200 rounded-md rounded-tl-none rounded-bl-none",
isAnyExtendedSidebarExpanded && "rounded-none"
)}
>
{children}
{/* Resize Handle */}
<div
className={cn(
"transition-all duration-200 cursor-ew-resize absolute h-full w-1 z-[20]",
!isResizing && "hover:bg-custom-background-90",
isResizing && "bg-custom-background-80",
"top-0 right-0"
)}
// onDoubleClick toggle sidebar
onDoubleClick={() => toggleCollapsed()}
onMouseDown={startResizing}
role="separator"
aria-label="Resize sidebar"
/>
</aside>
</div>
{/* Extended Sidebar */}
{extendedSidebar && extendedSidebar}
</>
);
}

View file

@ -0,0 +1,158 @@
import Link from "next/link";
import { cn } from "@plane/utils";
// ============================================================================
// TYPES
// ============================================================================
interface AppSidebarItemData {
href?: string;
label?: string;
icon?: React.ReactNode;
isActive?: boolean;
onClick?: () => void;
disabled?: boolean;
}
interface AppSidebarItemProps {
variant?: "link" | "button";
item?: AppSidebarItemData;
}
interface AppSidebarItemLabelProps {
highlight?: boolean;
label?: string;
}
interface AppSidebarItemIconProps {
icon?: React.ReactNode;
highlight?: boolean;
}
interface AppSidebarLinkItemProps {
href?: string;
children: React.ReactNode;
className?: string;
}
interface AppSidebarButtonItemProps {
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
className?: string;
}
// ============================================================================
// STYLES
// ============================================================================
const styles = {
base: "group flex flex-col gap-0.5 items-center justify-center text-custom-text-300",
icon: "flex items-center justify-center gap-2 size-8 rounded-md text-custom-text-300",
iconActive: "bg-custom-background-80 text-custom-text-200",
iconInactive: "group-hover:text-custom-text-200 group-hover:bg-custom-background-80",
label: "text-xs font-semibold",
labelActive: "text-custom-text-200",
labelInactive: "group-hover:text-custom-text-200 text-custom-text-300",
} as const;
// ============================================================================
// SUB-COMPONENTS
// ============================================================================
const AppSidebarItemLabel: React.FC<AppSidebarItemLabelProps> = ({ highlight = false, label }) => {
if (!label) return null;
return (
<span
className={cn(styles.label, {
[styles.labelActive]: highlight,
[styles.labelInactive]: !highlight,
})}
>
{label}
</span>
);
};
const AppSidebarItemIcon: React.FC<AppSidebarItemIconProps> = ({ icon, highlight }) => {
if (!icon) return null;
return (
<div
className={cn(styles.icon, {
[styles.iconActive]: highlight,
[styles.iconInactive]: !highlight,
})}
>
{icon}
</div>
);
};
const AppSidebarLinkItem: React.FC<AppSidebarLinkItemProps> = ({ href, children, className }) => {
if (!href) return null;
return (
<Link href={href} className={cn(styles.base, className)}>
{children}
</Link>
);
};
const AppSidebarButtonItem: React.FC<AppSidebarButtonItemProps> = ({
children,
onClick,
disabled = false,
className,
}) => (
<button className={cn(styles.base, className)} onClick={onClick} disabled={disabled} type="button">
{children}
</button>
);
// ============================================================================
// MAIN COMPONENT
// ============================================================================
type AppSidebarItemComponent = React.FC<AppSidebarItemProps> & {
Label: React.FC<AppSidebarItemLabelProps>;
Icon: React.FC<AppSidebarItemIconProps>;
Link: React.FC<AppSidebarLinkItemProps>;
Button: React.FC<AppSidebarButtonItemProps>;
};
const AppSidebarItem: AppSidebarItemComponent = ({ variant = "link", item }) => {
if (!item) return null;
const { icon, isActive, label, href, onClick, disabled } = item;
const commonItems = (
<>
<AppSidebarItemIcon icon={icon} highlight={isActive} />
<AppSidebarItemLabel highlight={isActive} label={label} />
</>
);
if (variant === "link") {
return <AppSidebarLinkItem href={href}>{commonItems}</AppSidebarLinkItem>;
}
return (
<AppSidebarButtonItem onClick={onClick} disabled={disabled}>
{commonItems}
</AppSidebarButtonItem>
);
};
// ============================================================================
// COMPOUND COMPONENT ASSIGNMENT
// ============================================================================
AppSidebarItem.Label = AppSidebarItemLabel;
AppSidebarItem.Icon = AppSidebarItemIcon;
AppSidebarItem.Link = AppSidebarLinkItem;
AppSidebarItem.Button = AppSidebarButtonItem;
export { AppSidebarItem };
export type { AppSidebarItemData, AppSidebarItemProps };

View file

@ -0,0 +1,23 @@
"use client";
import { observer } from "mobx-react";
import { PanelLeft } from "lucide-react";
// hooks
import { useAppTheme } from "@/hooks/store";
export const AppSidebarToggleButton = observer(() => {
// store hooks
const { toggleSidebar, sidebarPeek, toggleSidebarPeek } = useAppTheme();
return (
<button
className="flex items-center justify-center size-6 rounded-md text-custom-text-400 hover:text-custom-primary-100 hover:bg-custom-background-90"
onClick={() => {
if (sidebarPeek) toggleSidebarPeek(false);
toggleSidebar();
}}
>
<PanelLeft className="size-4" />
</button>
);
});

View file

@ -12,11 +12,10 @@ import { useWorkspaceNotifications } from "@/hooks/store";
type TNotificationAppSidebarOption = {
workspaceSlug: string;
isSidebarCollapsed: boolean | undefined;
};
export const NotificationAppSidebarOption: FC<TNotificationAppSidebarOption> = observer((props) => {
const { workspaceSlug, isSidebarCollapsed } = props;
const { workspaceSlug } = props;
// hooks
const { unreadNotificationsCount, getUnreadNotificationsCount } = useWorkspaceNotifications();
@ -33,9 +32,6 @@ export const NotificationAppSidebarOption: FC<TNotificationAppSidebarOption> = o
if (totalNotifications <= 0) return <></>;
if (isSidebarCollapsed)
return <div className="absolute right-2 top-1.5 h-2 w-2 rounded-full bg-custom-primary-300" />;
return (
<div className="ml-auto">
<CountChip count={`${isMentionsEnabled ? `@ ` : ``}${getNumberCount(totalNotifications)}`} />

View file

@ -9,6 +9,8 @@ import { Breadcrumbs, Header } from "@plane/ui";
import { BreadcrumbLink } from "@/components/common";
import { SidebarHamburgerToggle } from "@/components/core";
import { NotificationSidebarHeaderOptions } from "@/components/workspace-notifications";
// hooks
import { useAppTheme } from "@/hooks/store";
type TNotificationSidebarHeader = {
workspaceSlug: string;
@ -17,14 +19,14 @@ type TNotificationSidebarHeader = {
export const NotificationSidebarHeader: FC<TNotificationSidebarHeader> = observer((props) => {
const { workspaceSlug } = props;
const { t } = useTranslation();
const { sidebarCollapsed } = useAppTheme();
if (!workspaceSlug) return <></>;
return (
<Header className="my-auto bg-custom-background-100">
<Header.LeftItem>
<div className="block bg-custom-sidebar-background-100 md:hidden">
<SidebarHamburgerToggle />
</div>
{sidebarCollapsed && <SidebarHamburgerToggle />}
<Breadcrumbs>
<Breadcrumbs.Item
component={

View file

@ -64,7 +64,7 @@ const SidebarDropdownItem = observer((props: TProps) => {
</span>
<div className="w-[inherit]">
<div
className={`truncate text-ellipsis text-sm font-medium ${workspaceSlug === workspace.slug ? "" : "text-custom-text-200"}`}
className={`truncate text-left text-ellipsis text-sm font-medium ${workspaceSlug === workspace.slug ? "" : "text-custom-text-200"}`}
>
{workspace.name}
</div>

View file

@ -1,258 +1,22 @@
"use client";
import { Fragment, Ref, useState } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { usePopper } from "react-popper";
// icons
import { ChevronDown, CirclePlus, LogOut, Mails, Settings } from "lucide-react";
// ui
import { Menu, Transition } from "@headlessui/react";
// plane imports
import { GOD_MODE_URL } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IWorkspace } from "@plane/types";
import { Avatar, Loader, TOAST_TYPE, setToast } from "@plane/ui";
import { orderWorkspacesList, cn, getFileURL } from "@plane/utils";
// helpers
// hooks
import { useAppTheme, useUser, useUserProfile, useWorkspace } from "@/hooks/store";
// plane web helpers
import { getIsWorkspaceCreationDisabled } from "@/plane-web/helpers/instance.helper";
import { useAppRail } from "@/hooks/use-app-rail";
// components
import { WorkspaceLogo } from "../logo";
import SidebarDropdownItem from "./dropdown-item";
import { WorkspaceAppSwitcher } from "@/plane-web/components/workspace/app-switcher";
import { UserMenuRoot } from "./user-menu-root";
import { WorkspaceMenuRoot } from "./workspace-menu-root";
export const SidebarDropdown = observer(() => {
const { workspaceSlug } = useParams();
// store hooks
const { sidebarCollapsed, toggleSidebar } = useAppTheme();
const { data: currentUser } = useUser();
const { signOut } = useUser();
const { updateUserProfile } = useUserProfile();
const { currentWorkspace: activeWorkspace, workspaces } = useWorkspace();
// derived values
const isWorkspaceCreationEnabled = getIsWorkspaceCreationDisabled() === false;
const isUserInstanceAdmin = false;
// translation
const { t } = useTranslation();
// popper-js refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
// popper-js init
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: "right",
modifiers: [{ name: "preventOverflow", options: { padding: 12 } }],
});
// hooks
const { shouldRenderAppRail, isEnabled: isAppRailEnabled } = useAppRail();
const handleWorkspaceNavigation = (workspace: IWorkspace) => updateUserProfile({ last_workspace_id: workspace?.id });
const handleSignOut = async () => {
await signOut().catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: t("sign_out.toast.error.title"),
message: t("sign_out.toast.error.message"),
})
);
};
const handleItemClick = () => {
if (window.innerWidth < 768) {
toggleSidebar();
}
};
const workspacesList = orderWorkspacesList(Object.values(workspaces ?? {}));
// TODO: fix workspaces list scroll
return (
<div
className={cn("flex items-center justify-center gap-x-3 gap-y-2", {
"flex-col gap-y-3": sidebarCollapsed,
})}
>
<Menu
as="div"
className={cn("relative h-full truncate text-left flex-grow flex justify-stretch", {
"flex-grow-0 justify-center": sidebarCollapsed,
})}
>
{({ open, close }) => (
<>
<Menu.Button
className={cn(
"group/menu-button flex items-center justify-between gap-1 p-1 truncate rounded text-sm font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:outline-none",
{ "flex-grow": !sidebarCollapsed }
)}
aria-label={t("aria_labels.projects_sidebar.open_workspace_switcher")}
>
<div className="flex-grow flex items-center gap-2 truncate">
<WorkspaceLogo logo={activeWorkspace?.logo_url} name={activeWorkspace?.name} />
{!sidebarCollapsed && (
<h4 className="truncate text-base font-medium text-custom-text-100">
{activeWorkspace?.name ?? t("loading")}
</h4>
)}
</div>
{!sidebarCollapsed && (
<ChevronDown
className={cn(
"flex-shrink-0 mx-1 hidden size-4 group-hover/menu-button:block text-custom-sidebar-text-400 duration-300",
{ "rotate-180": open }
)}
/>
)}
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items as={Fragment}>
<div className="fixed top-12 left-4 z-[21] mt-1 flex w-full max-w-[19rem] origin-top-left flex-col divide-y divide-custom-border-100 rounded-md border-[0.5px] border-custom-sidebar-border-300 bg-custom-sidebar-background-100 shadow-custom-shadow-rg outline-none">
<div className="overflow-x-hidden vertical-scrollbar scrollbar-sm flex max-h-96 flex-col items-start justify-start overflow-y-scroll">
<span className="rounded-md px-4 sticky top-0 z-[21] h-full w-full bg-custom-sidebar-background-100 pb-1 pt-3 text-sm font-medium text-custom-text-400 truncate flex-shrink-0">
{currentUser?.email}
</span>
{workspacesList ? (
<div className="size-full flex flex-col items-start justify-start">
{(activeWorkspace
? [
activeWorkspace,
...workspacesList.filter((workspace) => workspace.id !== activeWorkspace?.id),
]
: workspacesList
).map((workspace) => (
<SidebarDropdownItem
key={workspace.id}
workspace={workspace}
activeWorkspace={activeWorkspace}
handleItemClick={handleItemClick}
handleWorkspaceNavigation={handleWorkspaceNavigation}
handleClose={close}
/>
))}
</div>
) : (
<div className="w-full">
<Loader className="space-y-2">
<Loader.Item height="30px" />
<Loader.Item height="30px" />
</Loader>
</div>
)}
</div>
<div className="w-full flex flex-col items-start justify-start gap-2 px-4 py-2 text-sm">
{isWorkspaceCreationEnabled && (
<Link href="/create-workspace" className="w-full">
<Menu.Item
as="div"
className="flex items-center gap-2 rounded px-2 py-1 text-sm font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
>
<CirclePlus className="size-4 flex-shrink-0" />
{t("create_workspace")}
</Menu.Item>
</Link>
)}
<Link href="/invitations" className="w-full" onClick={handleItemClick}>
<Menu.Item
as="div"
className="flex items-center gap-2 rounded px-2 py-1 text-sm font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
>
<Mails className="h-4 w-4 flex-shrink-0" />
{t("workspace_invites")}
</Menu.Item>
</Link>
<div className="w-full">
<Menu.Item
as="button"
type="button"
className="flex w-full items-center gap-2 rounded px-2 py-1 text-sm font-medium text-red-600 hover:bg-custom-sidebar-background-80"
onClick={handleSignOut}
>
<LogOut className="size-4 flex-shrink-0" />
{t("sign_out")}
</Menu.Item>
</div>
</div>
</div>
</Menu.Items>
</Transition>
</>
)}
</Menu>
<Menu as="div" className="relative flex-shrink-0">
<Menu.Button
className="grid place-items-center outline-none"
ref={setReferenceElement}
aria-label={t("aria_labels.projects_sidebar.open_user_menu")}
>
<Avatar
name={currentUser?.display_name}
src={getFileURL(currentUser?.avatar_url ?? "")}
size={24}
shape="circle"
className="!text-base"
/>
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
className="absolute left-0 z-[21] mt-1 flex w-52 origin-top-left flex-col divide-y
divide-custom-sidebar-border-200 rounded-md border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 text-xs shadow-lg outline-none"
ref={setPopperElement as Ref<HTMLDivElement>}
style={styles.popper}
{...attributes.popper}
>
<div className="flex flex-col gap-2.5 pb-2">
<span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span>
<Link href={`/${workspaceSlug}/settings/account`}>
<Menu.Item as="div">
<span className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80">
<Settings className="h-4 w-4 stroke-[1.5]" />
<span>{t("settings")}</span>
</span>
</Menu.Item>
</Link>
</div>
<div className={`pt-2 ${isUserInstanceAdmin || false ? "pb-2" : ""}`}>
<Menu.Item
as="button"
type="button"
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
onClick={handleSignOut}
>
<LogOut className="size-4 stroke-[1.5]" />
{t("sign_out")}
</Menu.Item>
</div>
{isUserInstanceAdmin && (
<div className="p-2 pb-0">
<Link href={GOD_MODE_URL}>
<Menu.Item as="button" type="button" className="w-full">
<span className="flex w-full items-center justify-center rounded bg-custom-primary-100/20 px-2 py-1 text-sm font-medium text-custom-primary-100 hover:bg-custom-primary-100/30 hover:text-custom-primary-200">
{t("enter_god_mode")}
</span>
</Menu.Item>
</Link>
</div>
)}
</Menu.Items>
</Transition>
</Menu>
<div className="flex items-center justify-center gap-1.5 w-full">
<WorkspaceMenuRoot />
{isAppRailEnabled && !shouldRenderAppRail && <WorkspaceAppSwitcher />}
<UserMenuRoot />
</div>
);
});

View file

@ -25,7 +25,6 @@ import { CustomMenu, Tooltip, DropIndicator, FavoriteFolderIcon, DragHandle } fr
// helpers
import { cn } from "@plane/utils";
// hooks
import { useAppTheme } from "@/hooks/store";
import { useFavorite } from "@/hooks/store/use-favorite";
import { usePlatformOS } from "@/hooks/use-platform-os";
// local imports
@ -44,7 +43,6 @@ type Props = {
export const FavoriteFolder: React.FC<Props> = (props) => {
const { favorite, handleRemoveFromFavorites, isLastChild, handleDrop } = props;
// store hooks
const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme();
const { getGroupedFavorites } = useFavorite();
const { isMobile } = usePlatformOS();
const { workspaceSlug } = useParams();
@ -159,7 +157,6 @@ export const FavoriteFolder: React.FC<Props> = (props) => {
"group/project-item relative w-full px-2 py-1.5 flex items-center rounded-md text-custom-sidebar-text-100 hover:bg-custom-sidebar-background-90",
{
"bg-custom-sidebar-background-90": isMenuActive,
"p-0 size-8 aspect-square justify-center mx-auto": isSidebarCollapsed,
}
)}
>
@ -169,117 +166,95 @@ export const FavoriteFolder: React.FC<Props> = (props) => {
<GripVertical className="w-3 h-3" />
</div>
{isSidebarCollapsed ? (
<div
className={cn("flex-grow flex items-center gap-1.5 truncate text-left select-none", {
"justify-center": isSidebarCollapsed,
})}
>
<Tooltip tooltipContent={favorite.name} position="right" isMobile={isMobile}>
<>
<Tooltip tooltipContent={`${favorite.name}`} position="right" className="ml-8" isMobile={isMobile}>
<div className="flex-grow flex truncate">
<Disclosure.Button
as="button"
className="size-8 aspect-square flex-shrink-0 grid place-items-center"
type="button"
className="flex-grow flex items-center gap-1.5 text-left select-none w-full"
>
<div className="size-4 grid place-items-center flex-shrink-0">
<Tooltip
isMobile={isMobile}
tooltipContent={
favorite.sort_order === null ? "Join the project to rearrange" : "Drag to rearrange"
}
position="top-right"
disabled={isDragging}
>
<button
type="button"
className={cn(
"hidden group-hover/project-item:flex items-center justify-center absolute top-1/2 -left-3 -translate-y-1/2 rounded text-custom-sidebar-text-400 cursor-grab",
{
"cursor-not-allowed opacity-60": favorite.sort_order === null,
"cursor-grabbing": isDragging,
}
)}
>
<DragHandle className="bg-transparent" />
</button>
</Tooltip>
<div className="size-5 grid place-items-center flex-shrink-0">
<FavoriteFolderIcon />
</div>
<p className="truncate text-sm font-medium text-custom-sidebar-text-200">{favorite.name}</p>
</Disclosure.Button>
</Tooltip>
</div>
) : (
<>
<Tooltip tooltipContent={`${favorite.name}`} position="right" className="ml-8" isMobile={isMobile}>
<div className="flex-grow flex truncate">
<Disclosure.Button
as="button"
type="button"
className={cn("flex-grow flex items-center gap-1.5 text-left select-none w-full", {
"justify-center": isSidebarCollapsed,
})}
>
<Tooltip
isMobile={isMobile}
tooltipContent={
favorite.sort_order === null ? "Join the project to rearrange" : "Drag to rearrange"
}
position="top-right"
disabled={isDragging}
>
<button
type="button"
className={cn(
"hidden group-hover/project-item:flex items-center justify-center absolute top-1/2 -left-3 -translate-y-1/2 rounded text-custom-sidebar-text-400 cursor-grab",
{
"cursor-not-allowed opacity-60": favorite.sort_order === null,
"cursor-grabbing": isDragging,
"!hidden": isSidebarCollapsed,
}
)}
>
<DragHandle className="bg-transparent" />
</button>
</Tooltip>
<div className="size-5 grid place-items-center flex-shrink-0">
<FavoriteFolderIcon />
</div>
<p className="truncate text-sm font-medium text-custom-sidebar-text-200">{favorite.name}</p>
</Disclosure.Button>
</div>
</Tooltip>
<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"
>
<MoreHorizontal className="size-3" />
</span>
</div>
</Tooltip>
<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"
>
<MoreHorizontal className="size-3" />
</span>
}
menuButtonOnClick={() => setIsMenuActive(!isMenuActive)}
className={cn(
"opacity-0 pointer-events-none flex-shrink-0 group-hover/project-item:opacity-100 group-hover/project-item:pointer-events-auto",
{
"opacity-100 pointer-events-auto": isMenuActive,
}
menuButtonOnClick={() => setIsMenuActive(!isMenuActive)}
className={cn(
"opacity-0 pointer-events-none flex-shrink-0 group-hover/project-item:opacity-100 group-hover/project-item:pointer-events-auto",
{
"opacity-100 pointer-events-auto": isMenuActive,
}
)}
customButtonClassName="grid place-items-center"
placement="bottom-start"
ariaLabel={t("aria_labels.projects_sidebar.toggle_quick_actions_menu")}
>
<CustomMenu.MenuItem onClick={() => handleRemoveFromFavorites(favorite)}>
<span className="flex items-center justify-start gap-2">
<Star className="h-3.5 w-3.5 fill-yellow-500 stroke-yellow-500" />
<span>Remove from favorites</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => setFolderToRename(favorite.id)}>
<div className="flex items-center justify-start gap-2">
<PenSquare className="h-3.5 w-3.5 stroke-[1.5] text-custom-text-300" />
<span>Rename Folder</span>
</div>
</CustomMenu.MenuItem>
</CustomMenu>
<Disclosure.Button
as="button"
type="button"
className={cn(
"hidden group-hover/project-item:inline-block p-0.5 rounded hover:bg-custom-sidebar-background-80",
{
"inline-block": isMenuActive,
}
)}
aria-label={t(
open ? "aria_labels.projects_sidebar.close_folder" : "aria_labels.projects_sidebar.open_folder"
)}
>
<ChevronRight
className={cn("size-3 flex-shrink-0 text-custom-sidebar-text-400 transition-transform", {
"rotate-90": open,
})}
/>
</Disclosure.Button>
</>
)}
)}
customButtonClassName="grid place-items-center"
placement="bottom-start"
ariaLabel={t("aria_labels.projects_sidebar.toggle_quick_actions_menu")}
>
<CustomMenu.MenuItem onClick={() => handleRemoveFromFavorites(favorite)}>
<span className="flex items-center justify-start gap-2">
<Star className="h-3.5 w-3.5 fill-yellow-500 stroke-yellow-500" />
<span>Remove from favorites</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => setFolderToRename(favorite.id)}>
<div className="flex items-center justify-start gap-2">
<PenSquare className="h-3.5 w-3.5 stroke-[1.5] text-custom-text-300" />
<span>Rename Folder</span>
</div>
</CustomMenu.MenuItem>
</CustomMenu>
<Disclosure.Button
as="button"
type="button"
className={cn(
"hidden group-hover/project-item:inline-block p-0.5 rounded hover:bg-custom-sidebar-background-80",
{
"inline-block": isMenuActive,
}
)}
aria-label={t(
open ? "aria_labels.projects_sidebar.close_folder" : "aria_labels.projects_sidebar.open_folder"
)}
>
<ChevronRight
className={cn("size-3 flex-shrink-0 text-custom-sidebar-text-400 transition-transform", {
"rotate-90": open,
})}
/>
</Disclosure.Button>
</>
</div>
{favorite.children && favorite.children.length > 0 && (
<Transition
@ -290,12 +265,7 @@ export const FavoriteFolder: React.FC<Props> = (props) => {
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
<Disclosure.Panel
as="div"
className={cn("flex flex-col gap-0.5 mt-1", {
"px-2": !isSidebarCollapsed,
})}
>
<Disclosure.Panel as="div" className="flex flex-col gap-0.5 mt-1 px-2">
{orderBy(favorite.children, "sequence", "desc").map((child, index) => (
<FavoriteRoot
key={child.id}

View file

@ -3,7 +3,6 @@ import React, { FC } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { Tooltip } from "@plane/ui";
import { cn } from "@plane/utils";
import { useAppTheme } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
@ -11,28 +10,23 @@ type Props = {
href: string;
title: string;
icon: JSX.Element;
isSidebarCollapsed: boolean;
};
export const FavoriteItemTitle: FC<Props> = observer((props) => {
const { href, title, icon, isSidebarCollapsed } = props;
const { href, title, icon } = props;
// store hooks
const { toggleSidebar } = useAppTheme();
const { isMobile } = usePlatformOS();
const linkClass = "flex items-center gap-1.5 truncate w-full";
const collapsedClass =
"group/project-item cursor-pointer relative group w-full flex items-center justify-center gap-1.5 rounded px-2 py-1 outline-none text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-90 active:bg-custom-sidebar-background-90 truncate p-0 size-8 aspect-square mx-auto";
const handleOnClick = () => {
if (isMobile) toggleSidebar();
};
return (
<Tooltip tooltipContent={title} isMobile={isMobile} position="right" className={cn(!isSidebarCollapsed && "ml-8")}>
<Link href={href} className={isSidebarCollapsed ? collapsedClass : linkClass} draggable onClick={handleOnClick}>
<Tooltip tooltipContent={title} isMobile={isMobile} position="right" className="ml-8">
<Link href={href} className="flex items-center gap-1.5 truncate w-full" draggable onClick={handleOnClick}>
<span className="flex items-center justify-center size-5">{icon}</span>
{!isSidebarCollapsed && <span className="text-sm leading-5 font-medium flex-1 truncate">{title}</span>}
<span className="text-sm leading-5 font-medium flex-1 truncate">{title}</span>
</Link>
</Tooltip>
);

View file

@ -7,28 +7,23 @@ type Props = {
children: React.ReactNode;
elementRef: React.RefObject<HTMLDivElement>;
isMenuActive?: boolean;
sidebarCollapsed?: boolean;
};
export const FavoriteItemWrapper: FC<Props> = (props) => {
const { children, elementRef, isMenuActive = false, sidebarCollapsed = false } = props;
const { children, elementRef, isMenuActive = false } = props;
return (
<>
{sidebarCollapsed ? (
<div ref={elementRef}>{children}</div>
) : (
<div
ref={elementRef}
className={cn(
"group/project-item cursor-pointer relative group flex items-center justify-between w-full gap-1.5 rounded px-2 py-1 outline-none text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-90 active:bg-custom-sidebar-background-90",
{
"bg-custom-sidebar-background-90": isMenuActive,
}
)}
>
{children}
</div>
)}
<div
ref={elementRef}
className={cn(
"group/project-item cursor-pointer relative group flex items-center justify-between w-full gap-1.5 rounded px-2 py-1 outline-none text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-90 active:bg-custom-sidebar-background-90",
{
"bg-custom-sidebar-background-90": isMenuActive,
}
)}
>
{children}
</div>
</>
);
};

View file

@ -27,7 +27,6 @@ import {
FavoriteItemTitle,
} from "@/components/workspace/sidebar/favorites";
// hooks
import { useAppTheme } from "@/hooks/store";
import { useFavoriteItemDetails } from "@/hooks/use-favorite-item-details";
//helpers
import { getCanDrop, getInstructionFromPayload } from "../favorites.helpers";
@ -45,7 +44,6 @@ export const FavoriteRoot: FC<Props> = observer((props) => {
// props
const { isLastChild, parentId, workspaceSlug, favorite, handleRemoveFromFavorites, handleDrop } = props;
// store hooks
const { sidebarCollapsed } = useAppTheme();
const { itemLink, itemIcon, itemTitle } = useFavoriteItemDetails(workspaceSlug, favorite);
//state
const [isDragging, setIsDragging] = useState(false);
@ -82,12 +80,7 @@ export const FavoriteRoot: FC<Props> = observer((props) => {
const root = createRoot(container);
root.render(
<div className="rounded bg-custom-background-100 text-sm p-1 pr-2">
<FavoriteItemTitle
href={itemLink}
icon={itemIcon}
title={itemTitle}
isSidebarCollapsed={!!sidebarCollapsed}
/>
<FavoriteItemTitle href={itemLink} icon={itemIcon} title={itemTitle} />
</div>
);
return () => root.unmount();
@ -138,18 +131,16 @@ export const FavoriteRoot: FC<Props> = observer((props) => {
return (
<>
<DropIndicator isVisible={instruction === "reorder-above"} />
<FavoriteItemWrapper elementRef={elementRef} isMenuActive={isMenuActive} sidebarCollapsed={sidebarCollapsed}>
{!sidebarCollapsed && <FavoriteItemDragHandle isDragging={isDragging} sort_order={favorite.sort_order} />}
<FavoriteItemTitle href={itemLink} icon={itemIcon} title={itemTitle} isSidebarCollapsed={!!sidebarCollapsed} />
{!sidebarCollapsed && (
<FavoriteItemQuickAction
favorite={favorite}
ref={actionSectionRef}
isMenuActive={isMenuActive}
onChange={handleQuickAction}
handleRemoveFromFavorites={handleRemoveFromFavorites}
/>
)}
<FavoriteItemWrapper elementRef={elementRef} isMenuActive={isMenuActive}>
<FavoriteItemDragHandle isDragging={isDragging} sort_order={favorite.sort_order} />
<FavoriteItemTitle href={itemLink} icon={itemIcon} title={itemTitle} />
<FavoriteItemQuickAction
favorite={favorite}
ref={actionSectionRef}
isMenuActive={isMenuActive}
onChange={handleQuickAction}
handleRemoveFromFavorites={handleRemoveFromFavorites}
/>
</FavoriteItemWrapper>
{isLastChild && <DropIndicator isVisible={instruction === "reorder-below"} />}
</>

View file

@ -23,10 +23,8 @@ import { setToast, TOAST_TYPE, Tooltip } from "@plane/ui";
// helpers
import { cn } from "@plane/utils";
// hooks
import { useAppTheme } from "@/hooks/store";
import { useFavorite } from "@/hooks/store/use-favorite";
import useLocalStorage from "@/hooks/use-local-storage";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web components
import { FavoriteFolder } from "./favorite-folder";
import { FavoriteRoot } from "./favorite-items";
@ -40,19 +38,10 @@ export const SidebarFavoritesMenu = observer(() => {
// navigation
const { workspaceSlug } = useParams();
// store hooks
const { sidebarCollapsed } = useAppTheme();
const {
favoriteIds,
groupedFavorites,
deleteFavorite,
removeFromFavoriteFolder,
reOrderFavorite,
moveFavoriteToFolder,
} = useFavorite();
const { groupedFavorites, deleteFavorite, removeFromFavoriteFolder, reOrderFavorite, moveFavoriteToFolder } =
useFavorite();
// translation
const { t } = useTranslation();
// platform hooks
const { isMobile } = usePlatformOS();
// local storage
const { setValue: toggleFavoriteMenu, storedValue } = useLocalStorage<boolean>(IS_FAVORITE_MENU_OPEN, false);
// derived values
@ -154,10 +143,6 @@ export const SidebarFavoritesMenu = observer(() => {
[workspaceSlug, reOrderFavorite, t]
);
useEffect(() => {
if (sidebarCollapsed) toggleFavoriteMenu(true);
}, [sidebarCollapsed, toggleFavoriteMenu]);
useEffect(() => {
const element = elementRef.current;
@ -189,27 +174,48 @@ export const SidebarFavoritesMenu = observer(() => {
return (
<>
<Disclosure as="div" defaultOpen ref={containerRef}>
{!sidebarCollapsed && (
<div
ref={elementRef}
<div
ref={elementRef}
className={cn(
"group/favorites-button w-full flex items-center justify-between px-2 py-1.5 rounded text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-90"
)}
>
<Disclosure.Button
as="button"
type="button"
className={cn(
"group/favorites-button w-full flex items-center justify-between px-2 py-1.5 rounded text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-90",
"w-full flex items-center gap-1 whitespace-nowrap text-left text-sm font-semibold text-custom-sidebar-text-400",
{
"p-0 justify-center w-fit mx-auto bg-custom-sidebar-background-90 hover:bg-custom-sidebar-background-80":
sidebarCollapsed,
"bg-custom-sidebar-background-80 opacity-60": isDragging,
}
)}
onClick={() => toggleFavoriteMenu(!isFavoriteMenuOpen)}
aria-label={t(
isFavoriteMenuOpen
? "aria_labels.projects_sidebar.close_favorites_menu"
: "aria_labels.projects_sidebar.open_favorites_menu"
)}
>
<span className="text-sm font-semibold">{t("favorites")}</span>
</Disclosure.Button>
<div className="flex items-center opacity-0 pointer-events-none group-hover/favorites-button:opacity-100 group-hover/favorites-button:pointer-events-auto">
<Tooltip tooltipHeading={t("create_folder")} tooltipContent="">
<button
type="button"
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0 grid place-items-center"
onClick={() => {
setCreateNewFolder(true);
if (!isFavoriteMenuOpen) toggleFavoriteMenu(!isFavoriteMenuOpen);
}}
aria-label={t("aria_labels.projects_sidebar.create_favorites_folder")}
>
<FolderPlus className="size-3" />
</button>
</Tooltip>
<Disclosure.Button
as="button"
type="button"
className={cn(
"w-full flex items-center gap-1 whitespace-nowrap text-left text-sm font-semibold text-custom-sidebar-text-400",
{
"!text-center w-8 px-2 py-1.5 justify-center": sidebarCollapsed,
"bg-custom-sidebar-background-80 opacity-60": isDragging,
}
)}
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0 grid place-items-center"
onClick={() => toggleFavoriteMenu(!isFavoriteMenuOpen)}
aria-label={t(
isFavoriteMenuOpen
@ -217,42 +223,14 @@ export const SidebarFavoritesMenu = observer(() => {
: "aria_labels.projects_sidebar.open_favorites_menu"
)}
>
<span className="text-sm font-semibold">{t("favorites")}</span>
<ChevronRight
className={cn("flex-shrink-0 size-3 transition-all", {
"rotate-90": isFavoriteMenuOpen,
})}
/>
</Disclosure.Button>
<div className="flex items-center opacity-0 pointer-events-none group-hover/favorites-button:opacity-100 group-hover/favorites-button:pointer-events-auto">
<Tooltip tooltipHeading={t("create_folder")} tooltipContent="">
<button
type="button"
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0 grid place-items-center"
onClick={() => {
setCreateNewFolder(true);
if (!isFavoriteMenuOpen) toggleFavoriteMenu(!isFavoriteMenuOpen);
}}
aria-label={t("aria_labels.projects_sidebar.create_favorites_folder")}
>
<FolderPlus className="size-3" />
</button>
</Tooltip>
<Disclosure.Button
as="button"
type="button"
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0 grid place-items-center"
onClick={() => toggleFavoriteMenu(!isFavoriteMenuOpen)}
aria-label={t(
isFavoriteMenuOpen
? "aria_labels.projects_sidebar.close_favorites_menu"
: "aria_labels.projects_sidebar.open_favorites_menu"
)}
>
<ChevronRight
className={cn("flex-shrink-0 size-3 transition-all", {
"rotate-90": isFavoriteMenuOpen,
})}
/>
</Disclosure.Button>
</div>
</div>
)}
</div>
<Transition
show={isFavoriteMenuOpen}
enter="transition duration-100 ease-out"
@ -263,55 +241,34 @@ export const SidebarFavoritesMenu = observer(() => {
leaveTo="transform scale-95 opacity-0"
>
{isFavoriteMenuOpen && (
<Disclosure.Panel
as="div"
className={cn("flex flex-col mt-0.5 gap-0.5", {
"space-y-0 mt-0 ml-0": sidebarCollapsed,
})}
static
>
<Disclosure.Panel as="div" className="flex flex-col mt-0.5 gap-0.5" static>
{createNewFolder && <NewFavoriteFolder setCreateNewFolder={setCreateNewFolder} actionType="create" />}
{Object.keys(groupedFavorites).length === 0 ? (
<>
{!sidebarCollapsed && (
<span className="text-custom-text-400 text-xs font-medium px-8 py-1.5">
{t("no_favorites_yet")}
</span>
)}
<span className="text-custom-text-400 text-xs font-medium px-8 py-1.5">{t("no_favorites_yet")}</span>
</>
) : (
orderBy(Object.values(groupedFavorites), "sequence", "desc")
.filter((fav) => !fav.parent)
.map((fav, index, { length }) => (
<>
{fav?.id && (
<Tooltip
key={fav?.id}
tooltipContent={fav?.entity_data ? fav?.entity_data?.name : fav?.name}
position="right"
className="ml-2"
disabled={!sidebarCollapsed}
isMobile={isMobile}
>
{fav?.is_folder ? (
<FavoriteFolder
favorite={fav}
isLastChild={index === length - 1}
handleRemoveFromFavorites={handleRemoveFromFavorites}
handleRemoveFromFavoritesFolder={handleRemoveFromFavoritesFolder}
handleDrop={handleDrop}
/>
) : (
<FavoriteRoot
workspaceSlug={workspaceSlug.toString()}
favorite={fav}
isLastChild={index === length - 1}
parentId={undefined}
handleRemoveFromFavorites={handleRemoveFromFavorites}
handleDrop={handleDrop}
/>
)}
</Tooltip>
{fav?.is_folder ? (
<FavoriteFolder
favorite={fav}
isLastChild={index === length - 1}
handleRemoveFromFavorites={handleRemoveFromFavorites}
handleRemoveFromFavoritesFolder={handleRemoveFromFavoritesFolder}
handleDrop={handleDrop}
/>
) : (
<FavoriteRoot
workspaceSlug={workspaceSlug.toString()}
favorite={fav}
isLastChild={index === length - 1}
parentId={undefined}
handleRemoveFromFavorites={handleRemoveFromFavorites}
handleDrop={handleDrop}
/>
)}
</>
))
@ -320,10 +277,6 @@ export const SidebarFavoritesMenu = observer(() => {
)}
</Transition>
</Disclosure>
{sidebarCollapsed && favoriteIds.length > 0 && (
<hr className="flex-shrink-0 border-custom-sidebar-border-300 h-[0.5px] w-3/5 mx-auto my-1" />
)}
</>
);
});

View file

@ -0,0 +1,149 @@
"use client";
import React, { useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { FileText, HelpCircle, MessagesSquare, User } from "lucide-react";
import { useTranslation } from "@plane/i18n";
// ui
import { CustomMenu, Tooltip, ToggleSwitch } from "@plane/ui";
// components
import { cn } from "@plane/utils";
import { ProductUpdatesModal } from "@/components/global";
// helpers
// hooks
import { useCommandPalette, useInstance, useTransient, useUserSettings } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web components
import { PlaneVersionNumber } from "@/plane-web/components/global";
export interface WorkspaceHelpSectionProps {
setSidebarActive?: React.Dispatch<React.SetStateAction<boolean>>;
}
export const HelpMenu: React.FC<WorkspaceHelpSectionProps> = observer(() => {
const { workspaceSlug, projectId } = useParams();
// store hooks
const { t } = useTranslation();
const { toggleShortcutModal } = useCommandPalette();
const { isMobile } = usePlatformOS();
const { config } = useInstance();
const { isIntercomToggle, toggleIntercom } = useTransient();
const { canUseLocalDB, toggleLocalDB } = useUserSettings();
// states
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
const [isProductUpdatesModalOpen, setProductUpdatesModalOpen] = useState(false);
const handleCrispWindowShow = () => {
toggleIntercom(!isIntercomToggle);
};
return (
<>
<ProductUpdatesModal isOpen={isProductUpdatesModalOpen} handleClose={() => setProductUpdatesModalOpen(false)} />
<div className="relative flex flex-shrink-0 items-center gap-1 justify-evenly">
<CustomMenu
customButton={
<div
className={cn(
"grid place-items-center rounded-md p-1 outline-none text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90",
{
"bg-custom-background-90": isNeedHelpOpen,
}
)}
>
<Tooltip tooltipContent="Help" isMobile={isMobile} disabled={isNeedHelpOpen}>
<HelpCircle className="h-[18px] w-[18px] outline-none" />
</Tooltip>
</div>
}
customButtonClassName="relative grid place-items-center rounded-md p-1.5 outline-none"
menuButtonOnClick={() => !isNeedHelpOpen && setIsNeedHelpOpen(true)}
onMenuClose={() => setIsNeedHelpOpen(false)}
placement="top-end"
maxHeight="lg"
closeOnSelect
>
<CustomMenu.MenuItem>
<a
href="https://go.plane.so/p-docs"
target="_blank"
className="flex items-center justify- gap-x-2 rounded text-xs hover:bg-custom-background-80"
>
<FileText className="h-3.5 w-3.5 text-custom-text-200" size={14} />
<span className="text-xs">{t("documentation")}</span>
</a>
</CustomMenu.MenuItem>
{config?.intercom_app_id && config?.is_intercom_enabled && (
<CustomMenu.MenuItem>
<button
type="button"
onClick={handleCrispWindowShow}
className="flex w-full items-center gap-x-2 rounded text-xs hover:bg-custom-background-80"
>
<MessagesSquare className="h-3.5 w-3.5 text-custom-text-200" />
<span className="text-xs">{t("message_support")}</span>
</button>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem>
<a
href="mailto:sales@plane.so"
target="_blank"
className="flex items-center justify- gap-x-2 rounded text-xs hover:bg-custom-background-80"
>
<User className="h-3.5 w-3.5 text-custom-text-200" size={14} />
<span className="text-xs">{t("contact_sales")}</span>
</a>
</CustomMenu.MenuItem>
<div className="my-1 border-t border-custom-border-200" />
<CustomMenu.MenuItem>
<div
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
className="flex w-full items-center justify-between text-xs hover:bg-custom-background-80"
>
<span className="racking-tight">{t("hyper_mode")}</span>
<ToggleSwitch
value={canUseLocalDB}
onChange={() => toggleLocalDB(workspaceSlug?.toString(), projectId?.toString())}
/>
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem>
<button
type="button"
onClick={() => toggleShortcutModal(true)}
className="flex w-full items-center justify-start text-xs hover:bg-custom-background-80"
>
<span className="text-xs">{t("keyboard_shortcuts")}</span>
</button>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem>
<button
type="button"
onClick={() => setProductUpdatesModalOpen(true)}
className="flex w-full items-center justify-start text-xs hover:bg-custom-background-80"
>
<span className="text-xs">{t("whats_new")}</span>
</button>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem>
<a
href="https://go.plane.so/p-discord"
target="_blank"
className="flex items-center justify- gap-x-2 rounded text-xs hover:bg-custom-background-80"
>
<span className="text-xs">Discord</span>
</a>
</CustomMenu.MenuItem>
<div className="px-1 pt-2 mt-1 text-xs text-custom-text-200 border-t border-custom-border-200">
<PlaneVersionNumber />
</div>
</CustomMenu>
</div>
</>
);
});

View file

@ -26,7 +26,7 @@ export const SidebarHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(
const { workspaceSlug, projectId } = useParams();
// store hooks
const { t } = useTranslation();
const { sidebarCollapsed, toggleSidebar } = useAppTheme();
const { sidebarCollapsed: isCollapsed, toggleSidebar, sidebarPeek, toggleSidebarPeek } = useAppTheme();
const { toggleShortcutModal } = useCommandPalette();
const { isMobile } = usePlatformOS();
const { config } = useInstance();
@ -40,22 +40,11 @@ export const SidebarHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(
toggleIntercom(!isIntercomToggle);
};
const isCollapsed = sidebarCollapsed || false;
return (
<>
<ProductUpdatesModal isOpen={isProductUpdatesModalOpen} handleClose={() => setProductUpdatesModalOpen(false)} />
<div
className={cn(
"flex w-full items-center justify-between px-2 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 h-12 flex-shrink-0",
{
"flex-col h-auto py-1.5": isCollapsed,
}
)}
>
<div
className={`relative flex flex-shrink-0 items-center gap-1 ${isCollapsed ? "flex-col justify-center" : "justify-evenly"}`}
>
<div className="flex w-full items-center justify-between px-2 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 h-12 flex-shrink-0">
<div className="relative flex flex-shrink-0 items-center gap-1 justify-evenly">
<CustomMenu
customButton={
<div
@ -71,10 +60,10 @@ export const SidebarHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(
</Tooltip>
</div>
}
customButtonClassName={`relative grid place-items-center rounded-md p-1.5 outline-none ${isCollapsed ? "w-full" : ""}`}
customButtonClassName="relative grid place-items-center rounded-md p-1.5 outline-none"
menuButtonOnClick={() => !isNeedHelpOpen && setIsNeedHelpOpen(true)}
onMenuClose={() => setIsNeedHelpOpen(false)}
placement={isCollapsed ? "left-end" : "top-end"}
placement="top-end"
maxHeight="lg"
closeOnSelect
>
@ -158,23 +147,18 @@ export const SidebarHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(
</div>
</CustomMenu>
</div>
<div
className={cn("w-full flex-grow px-0.5", {
hidden: isCollapsed,
})}
>
<div className="w-full flex-grow px-0.5">
<WorkspaceEditionBadge />
</div>
<div
className={`flex flex-shrink-0 items-center gap-1 ${isCollapsed ? "flex-col justify-center" : "justify-evenly"}`}
>
<div className="flex flex-shrink-0 items-center gap-1 justify-evenly">
<Tooltip tooltipContent={`${isCollapsed ? "Expand" : "Hide"}`} isMobile={isMobile}>
<button
type="button"
className={`grid place-items-center rounded-md p-1 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
isCollapsed ? "w-full" : ""
}`}
onClick={() => toggleSidebar()}
className="grid place-items-center rounded-md p-1 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100"
onClick={() => {
if (sidebarPeek) toggleSidebarPeek(false);
toggleSidebar();
}}
aria-label={t(
isCollapsed
? "aria_labels.projects_sidebar.expand_sidebar"

View file

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

View file

@ -0,0 +1,136 @@
"use client";
import React, { useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { FileText, HelpCircle, MessagesSquare, User } from "lucide-react";
import { useTranslation } from "@plane/i18n";
// ui
import { CustomMenu, ToggleSwitch } from "@plane/ui";
// components
import { ProductUpdatesModal } from "@/components/global";
import { AppSidebarItem } from "@/components/sidebar";
// hooks
import { useCommandPalette, useInstance, useTransient, useUserSettings } from "@/hooks/store";
// plane web components
import { PlaneVersionNumber } from "@/plane-web/components/global";
export const HelpMenuRoot = observer(() => {
const { workspaceSlug, projectId } = useParams();
// store hooks
const { t } = useTranslation();
const { toggleShortcutModal } = useCommandPalette();
const { config } = useInstance();
const { isIntercomToggle, toggleIntercom } = useTransient();
const { canUseLocalDB, toggleLocalDB } = useUserSettings();
// states
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
const [isProductUpdatesModalOpen, setProductUpdatesModalOpen] = useState(false);
const handleCrispWindowShow = () => {
toggleIntercom(!isIntercomToggle);
};
return (
<>
<ProductUpdatesModal isOpen={isProductUpdatesModalOpen} handleClose={() => setProductUpdatesModalOpen(false)} />
<CustomMenu
customButton={
<AppSidebarItem
variant="button"
item={{
icon: <HelpCircle className="size-5" />,
isActive: isNeedHelpOpen,
}}
/>
}
// customButtonClassName="relative grid place-items-center rounded-md p-1.5 outline-none"
menuButtonOnClick={() => !isNeedHelpOpen && setIsNeedHelpOpen(true)}
onMenuClose={() => setIsNeedHelpOpen(false)}
placement="top-end"
maxHeight="lg"
closeOnSelect
>
<CustomMenu.MenuItem>
<a
href="https://go.plane.so/p-docs"
target="_blank"
className="flex items-center justify- gap-x-2 rounded text-xs hover:bg-custom-background-80"
>
<FileText className="h-3.5 w-3.5 text-custom-text-200" size={14} />
<span className="text-xs">{t("documentation")}</span>
</a>
</CustomMenu.MenuItem>
{config?.intercom_app_id && config?.is_intercom_enabled && (
<CustomMenu.MenuItem>
<button
type="button"
onClick={handleCrispWindowShow}
className="flex w-full items-center gap-x-2 rounded text-xs hover:bg-custom-background-80"
>
<MessagesSquare className="h-3.5 w-3.5 text-custom-text-200" />
<span className="text-xs">{t("message_support")}</span>
</button>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem>
<a
href="mailto:sales@plane.so"
target="_blank"
className="flex items-center justify- gap-x-2 rounded text-xs hover:bg-custom-background-80"
>
<User className="h-3.5 w-3.5 text-custom-text-200" size={14} />
<span className="text-xs">{t("contact_sales")}</span>
</a>
</CustomMenu.MenuItem>
<div className="my-1 border-t border-custom-border-200" />
<CustomMenu.MenuItem>
<div
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
className="flex w-full items-center justify-between text-xs hover:bg-custom-background-80"
>
<span className="racking-tight">{t("hyper_mode")}</span>
<ToggleSwitch
value={canUseLocalDB}
onChange={() => toggleLocalDB(workspaceSlug?.toString(), projectId?.toString())}
/>
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem>
<button
type="button"
onClick={() => toggleShortcutModal(true)}
className="flex w-full items-center justify-start text-xs hover:bg-custom-background-80"
>
<span className="text-xs">{t("keyboard_shortcuts")}</span>
</button>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem>
<button
type="button"
onClick={() => setProductUpdatesModalOpen(true)}
className="flex w-full items-center justify-start text-xs hover:bg-custom-background-80"
>
<span className="text-xs">{t("whats_new")}</span>
</button>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem>
<a
href="https://go.plane.so/p-discord"
target="_blank"
className="flex items-center justify- gap-x-2 rounded text-xs hover:bg-custom-background-80"
>
<span className="text-xs">Discord</span>
</a>
</CustomMenu.MenuItem>
<div className="px-1 pt-2 mt-1 text-xs text-custom-text-200 border-t border-custom-border-200">
<PlaneVersionNumber />
</div>
</CustomMenu>
</>
);
});

View file

@ -10,3 +10,4 @@ export * from "./user-menu-item";
export * from "./workspace-menu";
export * from "./workspace-menu-item";
export * from "./workspace-menu-header";
export * from "./help-section";

View file

@ -9,13 +9,11 @@ import { EUserPermissionsLevel, EUserPermissions } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { EUserProjectRoles } from "@plane/types";
// plane ui
import { Tooltip, DiceIcon, ContrastIcon, LayersIcon, Intake } from "@plane/ui";
import { DiceIcon, ContrastIcon, LayersIcon, Intake } from "@plane/ui";
// components
import { SidebarNavItem } from "@/components/sidebar";
// hooks
import { useAppTheme, useIssueDetail, useProject, useUserPermissions } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane-web constants
export type TNavigationItem = {
name: string;
@ -32,17 +30,15 @@ type TProjectItemsProps = {
workspaceSlug: string;
projectId: string;
additionalNavigationItems?: (workspaceSlug: string, projectId: string) => TNavigationItem[];
isSidebarCollapsed: boolean;
};
export const ProjectNavigation: FC<TProjectItemsProps> = observer((props) => {
const { workspaceSlug, projectId, additionalNavigationItems, isSidebarCollapsed } = props;
const { workspaceSlug, projectId, additionalNavigationItems } = props;
const { workItem: workItemIdentifierFromRoute } = useParams();
// store hooks
const { t } = useTranslation();
const { toggleSidebar } = useAppTheme();
const { getPartialProjectById } = useProject();
const { isMobile } = usePlatformOS();
const { allowPermissions } = useUserPermissions();
const {
issue: { getIssueIdByIdentifier, getIssueById },
@ -176,28 +172,14 @@ export const ProjectNavigation: FC<TProjectItemsProps> = observer((props) => {
if (!hasAccess) return null;
return (
<Tooltip
key={item.name}
isMobile={isMobile}
tooltipContent={`${project?.name}: ${t(item.i18n_key)}`}
position="right"
className="ml-2"
disabled={!isSidebarCollapsed}
>
<Link href={item.href} onClick={handleProjectClick}>
<SidebarNavItem
className={`pl-[18px] ${isSidebarCollapsed ? "p-0 size-7 justify-center mx-auto" : ""}`}
isActive={!!isActive(item)}
>
<div className="flex items-center gap-1.5 py-[1px]">
<item.icon
className={`flex-shrink-0 size-4 ${item.name === "Intake" ? "stroke-1" : "stroke-[1.5]"}`}
/>
{!isSidebarCollapsed && <span className="text-xs font-medium">{t(item.i18n_key)}</span>}
</div>
</SidebarNavItem>
</Link>
</Tooltip>
<Link key={item.key} href={item.href} onClick={handleProjectClick}>
<SidebarNavItem className="pl-[18px]" isActive={!!isActive(item)}>
<div className="flex items-center gap-1.5 py-[1px]">
<item.icon className={`flex-shrink-0 size-4 ${item.name === "Intake" ? "stroke-1" : "stroke-[1.5]"}`} />
<span className="text-xs font-medium">{t(item.i18n_key)}</span>
</div>
</SidebarNavItem>
</Link>
);
})}
</>

View file

@ -57,12 +57,13 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
renderInExtendedSidebar = false,
} = props;
// store hooks
const { sidebarCollapsed } = useAppTheme();
const { t } = useTranslation();
const { getPartialProjectById } = useProject();
const { isMobile } = usePlatformOS();
const { allowPermissions } = useUserPermissions();
const { getIsProjectListOpen, toggleProjectListOpen } = useCommandPalette();
const { toggleAnySidebarDropdown } = useAppTheme();
// states
const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false);
const [publishModalOpen, setPublishModal] = useState(false);
@ -99,8 +100,6 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
setLeaveProjectModal(true);
};
const isSidebarCollapsed = sidebarCollapsed && !renderInExtendedSidebar;
useEffect(() => {
const element = projectRef.current;
const dragHandleElement = dragHandleRef.current;
@ -110,7 +109,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
return combine(
draggable({
element,
canDrag: () => !disableDrag && !isSidebarCollapsed,
canDrag: () => !disableDrag,
dragHandle: dragHandleElement ?? undefined,
getInitialData: () => ({ id: projectId, dragInstanceId: "PROJECTS" }),
onDragStart: () => {
@ -190,6 +189,11 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
);
}, [projectId, isLastChild, projectListType, handleOnProjectDrop]);
useEffect(() => {
if (isMenuActive) toggleAnySidebarDropdown(true);
else toggleAnySidebarDropdown(false);
}, [isMenuActive]);
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
useOutsideClickDetector(projectRef, () => projectRef?.current?.classList?.remove(HIGHLIGHT_CLASS));
@ -218,7 +222,6 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
"group/project-item relative w-full px-2 py-1.5 flex items-center rounded-md text-custom-sidebar-text-100 hover:bg-custom-sidebar-background-90",
{
"bg-custom-sidebar-background-90": isMenuActive,
"p-0 size-8 aspect-square justify-center mx-auto": isSidebarCollapsed,
}
)}
id={`${project?.id}`}
@ -240,7 +243,6 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
"cursor-not-allowed opacity-60": project.sort_order === null,
"cursor-grabbing": isDragging,
flex: isMenuActive || renderInExtendedSidebar,
"!hidden": isSidebarCollapsed,
}
)}
ref={dragHandleRef}
@ -249,76 +251,53 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
</button>
</Tooltip>
)}
{isSidebarCollapsed ? (
<>
<ControlLink
href={`/${workspaceSlug}/projects/${project.id}/issues`}
className={cn("flex-grow flex items-center gap-1.5 truncate text-left select-none", {
"justify-center": isSidebarCollapsed,
})}
className="flex-grow flex truncate"
onClick={handleItemClick}
>
<Disclosure.Button as="button" className="size-8 aspect-square flex-shrink-0 grid place-items-center">
<Disclosure.Button
as="button"
type="button"
className={cn("flex-grow flex items-center gap-1.5 text-left select-none w-full", {})}
aria-label={
isProjectListOpen
? t("aria_labels.projects_sidebar.close_project_menu")
: t("aria_labels.projects_sidebar.open_project_menu")
}
>
<div className="size-4 grid place-items-center flex-shrink-0">
<Logo logo={project.logo_props} size={16} />
</div>
<p className="truncate text-sm font-medium text-custom-sidebar-text-200">{project.name}</p>
</Disclosure.Button>
</ControlLink>
) : (
<>
<Tooltip
tooltipContent={`${project.name}`}
position="right"
disabled={!isSidebarCollapsed}
isMobile={isMobile}
>
<ControlLink
href={`/${workspaceSlug}/projects/${project.id}/issues`}
className="flex-grow flex truncate"
onClick={handleItemClick}
<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"
onClick={() => setIsMenuActive(!isMenuActive)}
>
<Disclosure.Button
as="button"
type="button"
className={cn("flex-grow flex items-center gap-1.5 text-left select-none w-full", {
"justify-center": isSidebarCollapsed,
})}
aria-label={
isProjectListOpen
? t("aria_labels.projects_sidebar.close_project_menu")
: t("aria_labels.projects_sidebar.open_project_menu")
}
>
<div className="size-4 grid place-items-center flex-shrink-0">
<Logo logo={project.logo_props} size={16} />
</div>
<p className="truncate text-sm font-medium text-custom-sidebar-text-200">{project.name}</p>
</Disclosure.Button>
</ControlLink>
</Tooltip>
<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"
onClick={() => setIsMenuActive(!isMenuActive)}
>
<MoreHorizontal className="size-4" />
</span>
<MoreHorizontal className="size-4" />
</span>
}
className={cn(
"opacity-0 pointer-events-none flex-shrink-0 group-hover/project-item:opacity-100 group-hover/project-item:pointer-events-auto",
{
"opacity-100 pointer-events-auto": isMenuActive,
}
className={cn(
"opacity-0 pointer-events-none flex-shrink-0 group-hover/project-item:opacity-100 group-hover/project-item:pointer-events-auto",
{
"opacity-100 pointer-events-auto": isMenuActive,
}
)}
customButtonClassName="grid place-items-center"
placement="bottom-start"
ariaLabel={t("aria_labels.projects_sidebar.toggle_quick_actions_menu")}
useCaptureForOutsideClick
closeOnSelect
>
{/* TODO: Removed is_favorite logic due to the optimization in projects API */}
{/* {isAuthorized && (
)}
customButtonClassName="grid place-items-center"
placement="bottom-start"
ariaLabel={t("aria_labels.projects_sidebar.toggle_quick_actions_menu")}
useCaptureForOutsideClick
closeOnSelect
onMenuClose={() => setIsMenuActive(false)}
>
{/* TODO: Removed is_favorite logic due to the optimization in projects API */}
{/* {isAuthorized && (
<CustomMenu.MenuItem
onClick={project.is_favorite ? handleRemoveFromFavorites : handleAddToFavorites}
>
@ -333,82 +312,81 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
</CustomMenu.MenuItem>
)} */}
{/* publish project settings */}
{isAdmin && (
<CustomMenu.MenuItem onClick={() => setPublishModal(true)}>
<div className="relative flex flex-shrink-0 items-center justify-start gap-2">
<div className="flex h-4 w-4 cursor-pointer items-center justify-center rounded text-custom-sidebar-text-200 transition-all duration-300 hover:bg-custom-sidebar-background-80">
<Share2 className="h-3.5 w-3.5 stroke-[1.5]" />
</div>
<div>{t("publish_project")}</div>
{/* publish project settings */}
{isAdmin && (
<CustomMenu.MenuItem onClick={() => setPublishModal(true)}>
<div className="relative flex flex-shrink-0 items-center justify-start gap-2">
<div className="flex h-4 w-4 cursor-pointer items-center justify-center rounded text-custom-sidebar-text-200 transition-all duration-300 hover:bg-custom-sidebar-background-80">
<Share2 className="h-3.5 w-3.5 stroke-[1.5]" />
</div>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem onClick={handleCopyText}>
<span className="flex items-center justify-start gap-2">
<LinkIcon className="h-3.5 w-3.5 stroke-[1.5]" />
<span>{t("copy_link")}</span>
</span>
<div>{t("publish_project")}</div>
</div>
</CustomMenu.MenuItem>
{isAuthorized && (
<CustomMenu.MenuItem
onClick={() => {
router.push(`/${workspaceSlug}/projects/${project?.id}/archives/issues`);
}}
>
<div className="flex items-center justify-start gap-2 cursor-pointer">
<ArchiveIcon className="h-3.5 w-3.5 stroke-[1.5]" />
<span>{t("archives")}</span>
</div>
</CustomMenu.MenuItem>
)}
)}
<CustomMenu.MenuItem onClick={handleCopyText}>
<span className="flex items-center justify-start gap-2">
<LinkIcon className="h-3.5 w-3.5 stroke-[1.5]" />
<span>{t("copy_link")}</span>
</span>
</CustomMenu.MenuItem>
{isAuthorized && (
<CustomMenu.MenuItem
onClick={() => {
router.push(`/${workspaceSlug}/settings/projects/${project?.id}`);
router.push(`/${workspaceSlug}/projects/${project?.id}/archives/issues`);
}}
>
<div className="flex items-center justify-start gap-2 cursor-pointer">
<Settings className="h-3.5 w-3.5 stroke-[1.5]" />
<span>{t("settings")}</span>
<ArchiveIcon className="h-3.5 w-3.5 stroke-[1.5]" />
<span>{t("archives")}</span>
</div>
</CustomMenu.MenuItem>
{/* leave project */}
{!isAuthorized && (
<CustomMenu.MenuItem
onClick={handleLeaveProject}
data-ph-element={MEMBER_TRACKER_ELEMENTS.SIDEBAR_PROJECT_QUICK_ACTIONS}
>
<div className="flex items-center justify-start gap-2">
<LogOut className="h-3.5 w-3.5 stroke-[1.5]" />
<span>{t("leave_project")}</span>
</div>
</CustomMenu.MenuItem>
)}
</CustomMenu>
<Disclosure.Button
as="button"
type="button"
className={cn(
"hidden group-hover/project-item:inline-block p-0.5 rounded hover:bg-custom-sidebar-background-80",
{
"inline-block": isMenuActive,
}
)}
onClick={() => setIsProjectListOpen(!isProjectListOpen)}
aria-label={t(
isProjectListOpen
? "aria_labels.projects_sidebar.close_project_menu"
: "aria_labels.projects_sidebar.open_project_menu"
)}
)}
<CustomMenu.MenuItem
onClick={() => {
router.push(`/${workspaceSlug}/settings/projects/${project?.id}`);
}}
>
<ChevronRight
className={cn("size-4 flex-shrink-0 text-custom-sidebar-text-400 transition-transform", {
"rotate-90": isProjectListOpen,
})}
/>
</Disclosure.Button>
</>
)}
<div className="flex items-center justify-start gap-2 cursor-pointer">
<Settings className="h-3.5 w-3.5 stroke-[1.5]" />
<span>{t("settings")}</span>
</div>
</CustomMenu.MenuItem>
{/* leave project */}
{!isAuthorized && (
<CustomMenu.MenuItem
onClick={handleLeaveProject}
data-ph-element={MEMBER_TRACKER_ELEMENTS.SIDEBAR_PROJECT_QUICK_ACTIONS}
>
<div className="flex items-center justify-start gap-2">
<LogOut className="h-3.5 w-3.5 stroke-[1.5]" />
<span>{t("leave_project")}</span>
</div>
</CustomMenu.MenuItem>
)}
</CustomMenu>
<Disclosure.Button
as="button"
type="button"
className={cn(
"hidden group-hover/project-item:inline-block p-0.5 rounded hover:bg-custom-sidebar-background-80",
{
"inline-block": isMenuActive,
}
)}
onClick={() => setIsProjectListOpen(!isProjectListOpen)}
aria-label={t(
isProjectListOpen
? "aria_labels.projects_sidebar.close_project_menu"
: "aria_labels.projects_sidebar.open_project_menu"
)}
>
<ChevronRight
className={cn("size-4 flex-shrink-0 text-custom-sidebar-text-400 transition-transform", {
"rotate-90": isProjectListOpen,
})}
/>
</Disclosure.Button>
</>
</div>
<Transition
show={isProjectListOpen}
@ -421,11 +399,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
>
{isProjectListOpen && (
<Disclosure.Panel as="div" className="flex flex-col gap-0.5 mt-1">
<ProjectNavigationRoot
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
isSidebarCollapsed={!!isSidebarCollapsed}
/>
<ProjectNavigationRoot workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
</Disclosure.Panel>
)}
</Transition>

View file

@ -5,7 +5,7 @@ import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
import { Briefcase, ChevronRight, Plus } from "lucide-react";
import { ChevronRight, Plus } from "lucide-react";
import { Disclosure, Transition } from "@headlessui/react";
import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
@ -17,7 +17,7 @@ import { CreateProjectModal } from "@/components/project";
import { SidebarProjectsListItem } from "@/components/workspace";
// helpers
// hooks
import { useAppTheme, useCommandPalette, useProject, useUserPermissions } from "@/hooks/store";
import { useCommandPalette, useProject, useUserPermissions } from "@/hooks/store";
// plane web types
import { TProject } from "@/plane-web/types";
@ -32,7 +32,6 @@ export const SidebarProjectsList: FC = observer(() => {
// store hooks
const { t } = useTranslation();
const { toggleCreateProjectModal } = useCommandPalette();
const { sidebarCollapsed } = useAppTheme();
const { allowPermissions } = useUserPermissions();
const { loader, getPartialProjectById, joinedProjectIds: joinedProjects, updateProjectView } = useProject();
@ -86,8 +85,6 @@ export const SidebarProjectsList: FC = observer(() => {
});
};
const isCollapsed = sidebarCollapsed || false;
/**
* Implementing scroll animation styles based on the scroll length of the container
*/
@ -151,24 +148,11 @@ export const SidebarProjectsList: FC = observer(() => {
>
<>
<Disclosure as="div" className="flex flex-col" defaultOpen={isAllProjectsListOpen}>
<div
className={cn(
"group w-full flex items-center justify-between px-2 py-1.5 rounded text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-90",
{
"p-0 justify-center w-fit mx-auto bg-custom-sidebar-background-90 hover:bg-custom-sidebar-background-80":
isCollapsed,
}
)}
>
<div className="group w-full flex items-center justify-between px-2 py-1.5 rounded text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-90">
<Disclosure.Button
as="button"
type="button"
className={cn(
"w-full flex items-center gap-1 whitespace-nowrap text-left text-sm font-semibold text-custom-sidebar-text-400",
{
"!text-center w-8 px-2 py-1.5 justify-center": isCollapsed,
}
)}
className="w-full flex items-center gap-1 whitespace-nowrap text-left text-sm font-semibold text-custom-sidebar-text-400"
onClick={() => toggleListDisclosure(!isAllProjectsListOpen)}
aria-label={t(
isAllProjectsListOpen
@ -176,52 +160,42 @@ export const SidebarProjectsList: FC = observer(() => {
: "aria_labels.projects_sidebar.open_projects_menu"
)}
>
<Tooltip tooltipHeading={t("projects")} tooltipContent="" position="right" disabled={!isCollapsed}>
<>
{isCollapsed ? (
<Briefcase className="flex-shrink-0 size-3" />
) : (
<span className="text-sm font-semibold">{t("projects")}</span>
)}
</>
</Tooltip>
<span className="text-sm font-semibold">{t("projects")}</span>
</Disclosure.Button>
{!isCollapsed && (
<div className="flex items-center opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto">
{isAuthorizedUser && (
<Tooltip tooltipHeading={t("create_project")} tooltipContent="">
<button
type="button"
data-ph-element={PROJECT_TRACKER_ELEMENTS.SIDEBAR_CREATE_PROJECT_TOOLTIP}
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0"
onClick={() => {
setIsProjectModalOpen(true);
}}
aria-label={t("aria_labels.projects_sidebar.create_new_project")}
>
<Plus className="size-3" />
</button>
</Tooltip>
<div className="flex items-center opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto">
{isAuthorizedUser && (
<Tooltip tooltipHeading={t("create_project")} tooltipContent="">
<button
type="button"
data-ph-element={PROJECT_TRACKER_ELEMENTS.SIDEBAR_CREATE_PROJECT_TOOLTIP}
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0"
onClick={() => {
setIsProjectModalOpen(true);
}}
aria-label={t("aria_labels.projects_sidebar.create_new_project")}
>
<Plus className="size-3" />
</button>
</Tooltip>
)}
<Disclosure.Button
as="button"
type="button"
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0"
onClick={() => toggleListDisclosure(!isAllProjectsListOpen)}
aria-label={t(
isAllProjectsListOpen
? "aria_labels.projects_sidebar.close_projects_menu"
: "aria_labels.projects_sidebar.open_projects_menu"
)}
<Disclosure.Button
as="button"
type="button"
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0"
onClick={() => toggleListDisclosure(!isAllProjectsListOpen)}
aria-label={t(
isAllProjectsListOpen
? "aria_labels.projects_sidebar.close_projects_menu"
: "aria_labels.projects_sidebar.open_projects_menu"
)}
>
<ChevronRight
className={cn("flex-shrink-0 size-3 transition-all", {
"rotate-90": isAllProjectsListOpen,
})}
/>
</Disclosure.Button>
</div>
)}
>
<ChevronRight
className={cn("flex-shrink-0 size-3 transition-all", {
"rotate-90": isAllProjectsListOpen,
})}
/>
</Disclosure.Button>
</div>
</div>
<Transition
show={isAllProjectsListOpen}
@ -240,13 +214,7 @@ export const SidebarProjectsList: FC = observer(() => {
</Loader>
)}
{isAllProjectsListOpen && (
<Disclosure.Panel
as="div"
className={cn("flex flex-col gap-0.5", {
"space-y-0 ml-0": isCollapsed,
})}
static
>
<Disclosure.Panel as="div" className="flex flex-col gap-0.5" static>
<>
{joinedProjects.map((projectId, index) => (
<SidebarProjectsListItem
@ -270,18 +238,13 @@ export const SidebarProjectsList: FC = observer(() => {
{isAuthorizedUser && joinedProjects?.length === 0 && (
<button
type="button"
className={cn(
`w-full flex items-center gap-1.5 px-2 py-1.5 text-sm leading-5 font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-90 rounded-md`,
{
"p-0 size-8 aspect-square justify-center mx-auto": sidebarCollapsed,
}
)}
data-ph-element={PROJECT_TRACKER_ELEMENTS.SIDEBAR_CREATE_PROJECT_BUTTON}
className="w-full flex items-center gap-1.5 px-2 py-1.5 text-sm leading-5 font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-90 rounded-md"
onClick={() => {
toggleCreateProjectModal(true);
}}
>
{!isCollapsed && t("add_project")}
{t("add_project")}
</button>
)}
</div>

View file

@ -12,7 +12,7 @@ import { CreateUpdateIssueModal } from "@/components/issues";
// constants
// helpers
// hooks
import { useAppTheme, useCommandPalette, useProject, useUserPermissions } from "@/hooks/store";
import { useCommandPalette, useProject, useUserPermissions } from "@/hooks/store";
import useLocalStorage from "@/hooks/use-local-storage";
// plane web components
import { AppSearch } from "@/plane-web/components/workspace";
@ -30,7 +30,6 @@ export const SidebarQuickActions = observer(() => {
const workspaceSlug = routerWorkspaceSlug?.toString();
// store hooks
const { toggleCreateIssueModal } = useCommandPalette();
const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme();
const { joinedProjectIds } = useProject();
const { allowPermissions } = useUserPermissions();
// local storage
@ -73,19 +72,13 @@ export const SidebarQuickActions = observer(() => {
onSubmit={() => removeWorkspaceDraftIssue()}
isDraft
/>
<div
className={cn("flex items-center justify-between gap-1 cursor-pointer", {
"flex-col gap-0": isSidebarCollapsed,
})}
>
<div className={cn("flex items-center justify-between gap-1 cursor-pointer", {})}>
<button
type="button"
className={cn(
"relative flex flex-shrink-0 flex-grow items-center gap-2 h-8 text-custom-sidebar-text-300 rounded outline-none hover:bg-custom-sidebar-background-90",
"relative flex flex-shrink-0 flex-grow items-center gap-2 h-8 text-custom-sidebar-text-300 rounded outline-none hover:bg-custom-sidebar-background-90 px-3 border-[0.5px] border-custom-sidebar-border-300",
{
"justify-center size-8 aspect-square": isSidebarCollapsed,
"cursor-not-allowed opacity-50 ": disabled,
"px-3 border-[0.5px] border-custom-sidebar-border-300": !isSidebarCollapsed,
}
)}
data-ph-element={SIDEBAR_TRACKER_ELEMENTS.CREATE_WORK_ITEM_BUTTON}
@ -97,9 +90,7 @@ export const SidebarQuickActions = observer(() => {
disabled={disabled}
>
<PenSquare className="size-4" />
{!isSidebarCollapsed && (
<span className="text-sm font-medium truncate max-w-[145px]">{t("sidebar.new_work_item")}</span>
)}
<span className="text-sm font-medium truncate max-w-[145px]">{t("sidebar.new_work_item")}</span>
</button>
<AppSearch />
</div>

View file

@ -2,11 +2,13 @@
import React, { useMemo } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { Ellipsis } from "lucide-react";
import { ChevronRight, Ellipsis } from "lucide-react";
import { Disclosure, Transition } from "@headlessui/react";
// plane imports
import {
WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS,
WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS_LINKS,
WORKSPACE_SIDEBAR_STATIC_PINNED_NAVIGATION_ITEMS_LINKS,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { cn } from "@plane/utils";
@ -14,20 +16,30 @@ import { cn } from "@plane/utils";
import { SidebarNavItem } from "@/components/sidebar";
// store hooks
import { useAppTheme, useWorkspace } from "@/hooks/store";
import useLocalStorage from "@/hooks/use-local-storage";
// plane-web imports
import { SidebarItem } from "@/plane-web/components/workspace/sidebar";
export const SidebarMenuItems = observer(() => {
// routers
const { workspaceSlug } = useParams();
const { setValue: toggleWorkspaceMenu, storedValue: isWorkspaceMenuOpen } = useLocalStorage<boolean>(
"is_workspace_menu_open",
true
);
// store hooks
const { sidebarCollapsed, extendedSidebarCollapsed, toggleExtendedSidebar } = useAppTheme();
const { isExtendedSidebarOpened, toggleExtendedSidebar } = useAppTheme();
const { getNavigationPreferences } = useWorkspace();
// translation
const { t } = useTranslation();
// derived values
const currentWorkspaceNavigationPreferences = getNavigationPreferences(workspaceSlug.toString());
const toggleListDisclosure = (isOpen: boolean) => {
toggleWorkspaceMenu(isOpen);
};
const sortedNavigationItems = useMemo(
() =>
WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS.map((item) => {
@ -41,35 +53,86 @@ export const SidebarMenuItems = observer(() => {
);
return (
<div
className={cn("flex flex-col gap-0.5", {
"space-y-0": sidebarCollapsed,
})}
>
{WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS_LINKS.map((item, _index) => (
<SidebarItem key={`static_${_index}`} item={item} />
))}
{sortedNavigationItems.map((item, _index) => (
<SidebarItem key={`dynamic_${_index}`} item={item} />
))}
<SidebarNavItem className={`${sidebarCollapsed ? "p-0 size-8 aspect-square justify-center mx-auto" : ""}`}>
<button
type="button"
onClick={() => toggleExtendedSidebar()}
className={cn("flex items-center gap-1.5 text-sm font-medium flex-grow text-custom-text-350", {
"justify-center": sidebarCollapsed,
})}
id="extended-sidebar-toggle"
aria-label={t(
extendedSidebarCollapsed
? "aria_labels.projects_sidebar.open_extended_sidebar"
: "aria_labels.projects_sidebar.close_extended_sidebar"
)}
<>
<div className="flex flex-col gap-0.5">
{WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS_LINKS.map((item, _index) => (
<SidebarItem key={`static_${_index}`} item={item} />
))}
</div>
<Disclosure as="div" className="flex flex-col" defaultOpen={!!isWorkspaceMenuOpen}>
<div className="group w-full flex items-center justify-between px-2 py-1.5 rounded text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-90">
<Disclosure.Button
as="button"
type="button"
className="w-full flex items-center gap-1 whitespace-nowrap text-left text-sm font-semibold text-custom-sidebar-text-400"
onClick={() => toggleListDisclosure(!isWorkspaceMenuOpen)}
aria-label={t(
isWorkspaceMenuOpen
? "aria_labels.app_sidebar.close_workspace_menu"
: "aria_labels.app_sidebar.open_workspace_menu"
)}
>
<span className="text-sm font-semibold">{t("workspace")}</span>
</Disclosure.Button>
<div className="flex items-center opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto">
<Disclosure.Button
as="button"
type="button"
className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0"
onClick={() => toggleListDisclosure(!isWorkspaceMenuOpen)}
aria-label={t(
isWorkspaceMenuOpen
? "aria_labels.app_sidebar.close_workspace_menu"
: "aria_labels.app_sidebar.open_workspace_menu"
)}
>
<ChevronRight
className={cn("flex-shrink-0 size-3 transition-all", {
"rotate-90": isWorkspaceMenuOpen,
})}
/>
</Disclosure.Button>
</div>
</div>
<Transition
show={!!isWorkspaceMenuOpen}
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
<Ellipsis className="flex-shrink-0 size-4" />
{!sidebarCollapsed && <span>More</span>}
</button>
</SidebarNavItem>
</div>
{isWorkspaceMenuOpen && (
<Disclosure.Panel as="div" className="flex flex-col gap-0.5" static>
<>
{WORKSPACE_SIDEBAR_STATIC_PINNED_NAVIGATION_ITEMS_LINKS.map((item, _index) => (
<SidebarItem key={`static_${_index}`} item={item} />
))}
{sortedNavigationItems.map((item, _index) => (
<SidebarItem key={`dynamic_${_index}`} item={item} />
))}
<SidebarNavItem>
<button
type="button"
onClick={() => toggleExtendedSidebar()}
className="flex items-center gap-1.5 text-sm font-medium flex-grow text-custom-text-350"
id="extended-sidebar-toggle"
aria-label={t(
isExtendedSidebarOpened
? "aria_labels.app_sidebar.close_extended_sidebar"
: "aria_labels.app_sidebar.open_extended_sidebar"
)}
>
<Ellipsis className="flex-shrink-0 size-4" />
<span>{isExtendedSidebarOpened ? "Hide" : "More"}</span>
</button>
</SidebarNavItem>
</>
</Disclosure.Panel>
)}
</Transition>
</Disclosure>
</>
);
});

View file

@ -4,10 +4,9 @@ import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
// plane imports
import { EUserPermissionsLevel, SIDEBAR_TRACKER_ELEMENTS } from "@plane/constants";
import { usePlatformOS } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
import { EUserWorkspaceRoles } from "@plane/types";
import { Tooltip } from "@plane/ui";
// components
import { SidebarNavItem } from "@/components/sidebar";
import { NotificationAppSidebarOption } from "@/components/workspace-notifications";
@ -36,8 +35,7 @@ export const SidebarUserMenuItem: FC<SidebarUserMenuItemProps> = observer((props
const { t } = useTranslation();
// store hooks
const { allowPermissions } = useUserPermissions();
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
const { isMobile } = usePlatformOS();
const { toggleSidebar } = useAppTheme();
const isActive = pathname === item.href;
@ -59,30 +57,14 @@ export const SidebarUserMenuItem: FC<SidebarUserMenuItemProps> = observer((props
};
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>
<Link href={item.href} onClick={() => handleLinkClick(item.key)}>
<SidebarNavItem isActive={isActive}>
<div className="flex items-center gap-1.5 py-[1px]">
<item.Icon className="size-4 flex-shrink-0" />
<p className="text-sm leading-5 font-medium">{t(item.labelTranslationKey)}</p>
</div>
{item.key === "notifications" && <NotificationAppSidebarOption workspaceSlug={workspaceSlug.toString()} />}
</SidebarNavItem>
</Link>
);
});

View file

@ -0,0 +1,159 @@
"use client";
import { Fragment, Ref, useState, useEffect } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { usePopper } from "react-popper";
// icons
import { LogOut, PanelLeftDashed, Settings } from "lucide-react";
// ui
import { Menu, Transition } from "@headlessui/react";
// plane imports
import { GOD_MODE_URL } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Avatar, TOAST_TYPE, setToast } from "@plane/ui";
import { getFileURL } from "@plane/utils";
// hooks
import { useAppTheme, useUser } from "@/hooks/store";
import { useAppRail } from "@/hooks/use-app-rail";
type Props = {
size?: "sm" | "md";
};
export const UserMenuRoot = observer((props: Props) => {
const { size = "sm" } = props;
const { workspaceSlug } = useParams();
// store hooks
const { toggleAnySidebarDropdown, sidebarPeek, toggleSidebarPeek } = useAppTheme();
const { isEnabled, shouldRenderAppRail, toggleAppRail } = useAppRail();
const { data: currentUser } = useUser();
const { signOut } = useUser();
// derived values
const isUserInstanceAdmin = false;
// translation
const { t } = useTranslation();
// local state
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
// popper-js refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
// popper-js init
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: "right",
modifiers: [{ name: "preventOverflow", options: { padding: 12 } }],
});
const handleSignOut = async () => {
await signOut().catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: t("sign_out.toast.error.title"),
message: t("sign_out.toast.error.message"),
})
);
};
// Toggle sidebar dropdown state when either menu is open
useEffect(() => {
if (isUserMenuOpen) toggleAnySidebarDropdown(true);
else toggleAnySidebarDropdown(false);
}, [isUserMenuOpen]);
return (
<Menu as="div" className="relative flex-shrink-0">
{({ open, close }: { open: boolean; close: () => void }) => {
// Update local state directly
if (isUserMenuOpen !== open) {
setIsUserMenuOpen(open);
}
return (
<>
<Menu.Button
className="grid place-items-center outline-none"
ref={setReferenceElement}
aria-label={t("aria_labels.projects_sidebar.open_user_menu")}
>
<Avatar
name={currentUser?.display_name}
src={getFileURL(currentUser?.avatar_url ?? "")}
size={size === "sm" ? 24 : 28}
shape="circle"
className="!text-base"
/>
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
className="absolute left-0 z-[21] mt-1 flex w-44 origin-top-left flex-col divide-y
divide-custom-sidebar-border-200 rounded-md border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 text-xs shadow-lg outline-none"
ref={setPopperElement as Ref<HTMLDivElement>}
style={styles.popper}
{...attributes.popper}
>
<div className="flex flex-col gap-2.5 pb-2">
<span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span>
<Link href={`/${workspaceSlug}/settings/account`}>
<Menu.Item as="div">
<span className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80">
<Settings className="h-4 w-4 stroke-[1.5]" />
<span>{t("settings")}</span>
</span>
</Menu.Item>
</Link>
{isEnabled && (
<Menu.Item
as="button"
type="button"
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
onClick={() => {
if (sidebarPeek) toggleSidebarPeek(false);
toggleAppRail();
}}
>
<PanelLeftDashed className="h-4 w-4 stroke-[1.5]" />
<span>{shouldRenderAppRail ? "Undock AppRail" : "Dock AppRail"}</span>
</Menu.Item>
)}
</div>
<div className={`pt-2 ${isUserInstanceAdmin || false ? "pb-2" : ""}`}>
<Menu.Item
as="button"
type="button"
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
onClick={handleSignOut}
>
<LogOut className="size-4 stroke-[1.5]" />
{t("sign_out")}
</Menu.Item>
</div>
{isUserInstanceAdmin && (
<div className="p-2 pb-0">
<Link href={GOD_MODE_URL}>
<Menu.Item as="button" type="button" className="w-full">
<span className="flex w-full items-center justify-center rounded bg-custom-primary-100/20 px-2 py-1 text-sm font-medium text-custom-primary-100 hover:bg-custom-primary-100/30 hover:text-custom-primary-200">
{t("enter_god_mode")}
</span>
</Menu.Item>
</Link>
</div>
)}
</Menu.Items>
</Transition>
</>
);
}}
</Menu>
);
});

View file

@ -8,15 +8,12 @@ import { EUserWorkspaceRoles } from "@plane/types";
// plane imports
import { UserActivityIcon } from "@plane/ui";
// components
import { cn } from "@plane/utils";
import { SidebarUserMenuItem } from "@/components/workspace/sidebar";
// helpers
// hooks
import { useAppTheme, useUserPermissions, useUser } from "@/hooks/store";
import { useUserPermissions, useUser } from "@/hooks/store";
export const SidebarUserMenu = observer(() => {
const { workspaceSlug } = useParams();
const { sidebarCollapsed } = useAppTheme();
const { workspaceUserInfo } = useUserPermissions();
const { data: currentUser } = useUser();
@ -54,11 +51,7 @@ export const SidebarUserMenu = observer(() => {
const draftIssueCount = workspaceUserInfo[workspaceSlug.toString()]?.draft_issue_count;
return (
<div
className={cn("flex flex-col gap-0.5", {
"space-y-0": sidebarCollapsed,
})}
>
<div className="flex flex-col gap-0.5">
{SIDEBAR_USER_MENU_ITEMS.map((item) => (
<SidebarUserMenuItem key={item.key} item={item} draftIssueCount={draftIssueCount} />
))}

View file

@ -12,7 +12,7 @@ import { EUserWorkspaceRoles } from "@plane/types";
import { CustomMenu } from "@plane/ui";
import { cn } from "@plane/utils";
// store hooks
import { useAppTheme, useUserPermissions } from "@/hooks/store";
import { useUserPermissions } from "@/hooks/store";
export type SidebarWorkspaceMenuHeaderProps = {
isWorkspaceMenuOpen: boolean;
@ -27,7 +27,6 @@ export const SidebarWorkspaceMenuHeader: FC<SidebarWorkspaceMenuHeaderProps> = o
const actionSectionRef = useRef<HTMLDivElement | null>(null);
// hooks
const { workspaceSlug } = useParams();
const { sidebarCollapsed } = useAppTheme();
const { allowPermissions } = useUserPermissions();
const { t } = useTranslation();
@ -37,19 +36,8 @@ export const SidebarWorkspaceMenuHeader: FC<SidebarWorkspaceMenuHeaderProps> = o
// 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,
}
)}
>
<div className="flex px-2 bg-custom-sidebar-background-100 group/workspace-button hover:bg-custom-sidebar-background-90 rounded mt-2.5">
<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-sm font-semibold"

View file

@ -4,10 +4,8 @@ import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
// plane imports
import { EUserPermissionsLevel } from "@plane/constants";
import { usePlatformOS } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
import { EUserWorkspaceRoles } from "@plane/types";
import { Tooltip } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { SidebarNavItem } from "@/components/sidebar";
@ -35,8 +33,7 @@ export const SidebarWorkspaceMenuItem: FC<SidebarWorkspaceMenuItemProps> = obser
const { workspaceSlug } = useParams();
const { allowPermissions } = useUserPermissions();
// store hooks
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
const { isMobile } = usePlatformOS();
const { toggleSidebar } = useAppTheme();
const handleLinkClick = () => {
if (window.innerWidth < 768) {
@ -51,33 +48,20 @@ export const SidebarWorkspaceMenuItem: FC<SidebarWorkspaceMenuItemProps> = obser
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>
<Link href={item.href} onClick={() => handleLinkClick()}>
<SidebarNavItem isActive={isActive}>
<div className="flex items-center gap-1.5 py-[1px]">
<item.Icon
className={cn("size-4", {
"rotate-180": item.key === "active_cycles",
})}
/>
<p className="text-sm leading-5 font-medium">{t(item.labelTranslationKey)}</p>
</div>
<div className="flex-shrink-0">
<UpgradeBadge />
</div>
</SidebarNavItem>
</Link>
);
});

View file

@ -0,0 +1,216 @@
"use client";
import React, { Fragment, useState, useEffect } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
// icons
import { ChevronDown, CirclePlus, LogOut, Mails } from "lucide-react";
// ui
import { Menu, Transition } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { IWorkspace } from "@plane/types";
import { Loader, TOAST_TYPE, setToast } from "@plane/ui";
import { orderWorkspacesList, cn } from "@plane/utils";
// helpers
import { AppSidebarItem } from "@/components/sidebar";
// hooks
import { useAppTheme, useUser, useUserProfile, useWorkspace } from "@/hooks/store";
// plane web helpers
import { getIsWorkspaceCreationDisabled } from "@/plane-web/helpers/instance.helper";
// components
import { WorkspaceLogo } from "../logo";
import SidebarDropdownItem from "./dropdown-item";
type WorkspaceMenuRootProps = {
renderLogoOnly?: boolean;
};
export const WorkspaceMenuRoot = observer((props: WorkspaceMenuRootProps) => {
const { renderLogoOnly } = props;
// store hooks
const { toggleSidebar, toggleAnySidebarDropdown } = useAppTheme();
const { data: currentUser } = useUser();
const { signOut } = useUser();
const { updateUserProfile } = useUserProfile();
const { currentWorkspace: activeWorkspace, workspaces } = useWorkspace();
// derived values
const isWorkspaceCreationEnabled = getIsWorkspaceCreationDisabled() === false;
// translation
const { t } = useTranslation();
// local state
const [isWorkspaceMenuOpen, setIsWorkspaceMenuOpen] = useState(false);
const handleWorkspaceNavigation = (workspace: IWorkspace) => updateUserProfile({ last_workspace_id: workspace?.id });
const handleSignOut = async () => {
await signOut().catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: t("sign_out.toast.error.title"),
message: t("sign_out.toast.error.message"),
})
);
};
const handleItemClick = () => {
if (window.innerWidth < 768) {
toggleSidebar();
}
};
const workspacesList = orderWorkspacesList(Object.values(workspaces ?? {}));
// TODO: fix workspaces list scroll
// Toggle sidebar dropdown state when either menu is open
useEffect(() => {
if (isWorkspaceMenuOpen) toggleAnySidebarDropdown(true);
else toggleAnySidebarDropdown(false);
}, [isWorkspaceMenuOpen]);
const logo = activeWorkspace?.logo_url;
const name = activeWorkspace?.name;
return (
<Menu
as="div"
className={cn("relative h-full flex ", {
"justify-center text-center": renderLogoOnly,
"flex-grow justify-stretch text-left truncate": !renderLogoOnly,
})}
>
{({ open, close }: { open: boolean; close: () => void }) => {
// Update local state directly
if (isWorkspaceMenuOpen !== open) {
setIsWorkspaceMenuOpen(open);
}
return (
<>
{renderLogoOnly ? (
<Menu.Button className="flex items-center justify-center size-8">
<AppSidebarItem
variant="button"
item={{
icon: (
<WorkspaceLogo
logo={activeWorkspace?.logo_url}
name={activeWorkspace?.name}
classNames="size-8 rounded-md"
/>
),
}}
/>
</Menu.Button>
) : (
<Menu.Button
className={cn(
"group/menu-button flex items-center gap-1 p-1 truncate rounded text-sm font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:outline-none ",
{
"justify-center text-center": renderLogoOnly,
"justify-between flex-grow": !renderLogoOnly,
}
)}
aria-label={t("aria_labels.projects_sidebar.open_workspace_switcher")}
>
<div className="flex-grow flex items-center gap-2 truncate">
<WorkspaceLogo logo={activeWorkspace?.logo_url} name={activeWorkspace?.name} />
<h4 className="truncate text-base font-medium text-custom-text-100">
{activeWorkspace?.name ?? t("loading")}
</h4>
</div>
<ChevronDown
className={cn(
"flex-shrink-0 mx-1 hidden size-4 group-hover/menu-button:block text-custom-sidebar-text-400 duration-300",
{ "rotate-180": open }
)}
/>
</Menu.Button>
)}
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="trnsform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items as={Fragment}>
<div className="fixed top-12 left-4 z-[21] mt-1 flex w-[19rem] origin-top-left flex-col divide-y divide-custom-border-100 rounded-md border-[0.5px] border-custom-sidebar-border-300 bg-custom-sidebar-background-100 shadow-custom-shadow-rg outline-none">
<div className="overflow-x-hidden vertical-scrollbar scrollbar-sm flex max-h-96 flex-col items-start justify-start overflow-y-scroll">
<span className="rounded-md text-left px-4 sticky top-0 z-[21] h-full w-full bg-custom-sidebar-background-100 pb-1 pt-3 text-sm font-medium text-custom-text-400 truncate flex-shrink-0">
{currentUser?.email}
</span>
{workspacesList ? (
<div className="size-full flex flex-col items-start justify-start">
{(activeWorkspace
? [
activeWorkspace,
...workspacesList.filter((workspace) => workspace.id !== activeWorkspace?.id),
]
: workspacesList
).map((workspace) => (
<SidebarDropdownItem
key={workspace.id}
workspace={workspace}
activeWorkspace={activeWorkspace}
handleItemClick={handleItemClick}
handleWorkspaceNavigation={handleWorkspaceNavigation}
handleClose={close}
/>
))}
</div>
) : (
<div className="w-full">
<Loader className="space-y-2">
<Loader.Item height="30px" />
<Loader.Item height="30px" />
</Loader>
</div>
)}
</div>
<div className="w-full flex flex-col items-start justify-start gap-2 px-4 py-2 text-sm">
{isWorkspaceCreationEnabled && (
<Link href="/create-workspace" className="w-full">
<Menu.Item
as="div"
className="flex items-center gap-2 rounded px-2 py-1 text-sm font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
>
<CirclePlus className="size-4 flex-shrink-0" />
{t("create_workspace")}
</Menu.Item>
</Link>
)}
<Link href="/invitations" className="w-full" onClick={handleItemClick}>
<Menu.Item
as="div"
className="flex items-center gap-2 rounded px-2 py-1 text-sm font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
>
<Mails className="h-4 w-4 flex-shrink-0" />
{t("workspace_invites")}
</Menu.Item>
</Link>
<div className="w-full">
<Menu.Item
as="button"
type="button"
className="flex w-full items-center gap-2 rounded px-2 py-1 text-sm font-medium text-red-600 hover:bg-custom-sidebar-background-80"
onClick={handleSignOut}
>
<LogOut className="size-4 flex-shrink-0" />
{t("sign_out")}
</Menu.Item>
</div>
</div>
</div>
</Menu.Items>
</Transition>
</>
);
}}
</Menu>
);
});

View file

@ -1,6 +1,6 @@
"use client";
import React, { useEffect } from "react";
import React from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { BarChart2, Briefcase, Layers } from "lucide-react";
@ -11,25 +11,17 @@ import { ContrastIcon } from "@plane/ui";
// components
import { cn } from "@plane/utils";
import { SidebarWorkspaceMenuHeader, SidebarWorkspaceMenuItem } from "@/components/workspace/sidebar";
// helpers
// hooks
import { useAppTheme } from "@/hooks/store";
import useLocalStorage from "@/hooks/use-local-storage";
export const SidebarWorkspaceMenu = observer(() => {
// router params
const { workspaceSlug } = useParams();
// store hooks
const { sidebarCollapsed } = useAppTheme();
// local storage
const { setValue: toggleWorkspaceMenu, storedValue } = useLocalStorage<boolean>("is_workspace_menu_open", true);
// derived values
const isWorkspaceMenuOpen = !!storedValue;
useEffect(() => {
if (sidebarCollapsed) toggleWorkspaceMenu(true);
}, [sidebarCollapsed, toggleWorkspaceMenu]);
const SIDEBAR_WORKSPACE_MENU_ITEMS = [
{
key: "projects",
@ -74,13 +66,7 @@ export const SidebarWorkspaceMenu = observer(() => {
leaveTo="transform scale-95 opacity-0"
>
{isWorkspaceMenuOpen && (
<Disclosure.Panel
as="div"
className={cn("flex flex-col mt-0.5 gap-0.5", {
"space-y-0 mt-0 ml-0": sidebarCollapsed,
})}
static
>
<Disclosure.Panel as="div" className="flex flex-col mt-0.5 gap-0.5" static>
{SIDEBAR_WORKSPACE_MENU_ITEMS.map((item) => (
<SidebarWorkspaceMenuItem key={item.key} item={item} />
))}