[WEB-1716] dev: app sidebar revamp (#4921)

* dev: revamp app sidebar

* dev: revamp app sidebar

* chore: tooltips added

* fix: drag handle

* chore: update chevron angle

* fix: workspace sidebar dropdown
This commit is contained in:
Aaryan Khandelwal
2024-06-24 20:10:30 +05:30
committed by GitHub
parent 3eda3845fa
commit 535a27309e
15 changed files with 779 additions and 616 deletions

View File

@@ -1,18 +1,23 @@
import { FC, useRef } from "react";
import { observer } from "mobx-react";
// components
import { ProjectSidebarList } from "@/components/project";
import {
WorkspaceHelpSection,
WorkspaceSidebarDropdown,
WorkspaceSidebarMenu,
WorkspaceSidebarQuickAction,
SidebarDropdown,
SidebarHelpSection,
SidebarProjectsList,
SidebarQuickActions,
SidebarUserMenu,
SidebarWorkspaceMenu,
} from "@/components/workspace";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useAppTheme } from "@/hooks/store";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
// plane web components
import { SidebarAppSwitcher } from "@/plane-web/components/sidebar";
export interface IAppSidebar { }
export interface IAppSidebar {}
export const AppSidebar: FC<IAppSidebar> = observer(() => {
// store hooks
@@ -30,19 +35,42 @@ export const AppSidebar: FC<IAppSidebar> = observer(() => {
return (
<div
className={`fixed inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-custom-sidebar-border-200 bg-custom-sidebar-background-100
duration-300 md:relative
${sidebarCollapsed ? "-ml-[250px]" : ""}
sm:${sidebarCollapsed ? "-ml-[250px]" : ""}
md:ml-0 ${sidebarCollapsed ? "w-[70px]" : "w-[250px]"}
`}
className={cn(
"fixed inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-custom-sidebar-border-200 bg-custom-sidebar-background-100 duration-300 w-[250px] md:relative md:ml-0",
{
"w-[70px] -ml-[250px]": sidebarCollapsed,
}
)}
>
<div ref={ref} className="flex h-full w-full flex-1 flex-col">
<WorkspaceSidebarDropdown />
<WorkspaceSidebarQuickAction />
<WorkspaceSidebarMenu />
<ProjectSidebarList />
<WorkspaceHelpSection />
<div
ref={ref}
className={cn("size-full flex flex-col flex-1 p-4 pb-0", {
"p-2": sidebarCollapsed,
})}
>
<SidebarDropdown />
<div className="flex-shrink-0 h-4" />
<SidebarAppSwitcher />
<SidebarQuickActions />
<hr
className={cn("flex-shrink-0 border-custom-sidebar-border-200 h-[0.5px] w-3/5 mx-auto my-2", {
"opacity-0": !sidebarCollapsed,
})}
/>
<SidebarUserMenu />
<hr
className={cn("flex-shrink-0 border-custom-sidebar-border-200 h-[0.5px] w-3/5 mx-auto my-2", {
"opacity-0": !sidebarCollapsed,
})}
/>
<SidebarWorkspaceMenu />
<hr
className={cn("flex-shrink-0 border-custom-sidebar-border-200 h-[0.5px] w-3/5 mx-auto my-2", {
"opacity-0": !sidebarCollapsed,
})}
/>
<SidebarProjectsList />
<SidebarHelpSection />
</div>
</div>
);

View File

@@ -1 +1 @@
export const AppSwitcher = () => null;
export const SidebarAppSwitcher = () => null;

View File

@@ -4,17 +4,18 @@ import React, { Fragment } from "react";
import { observer } from "mobx-react";
import { Bell } from "lucide-react";
import { Popover, Transition } from "@headlessui/react";
// hooks
// icons
// components
// ui
import { Tooltip } from "@plane/ui";
// components
import { EmptyState } from "@/components/empty-state";
import { SnoozeNotificationModal, NotificationCard, NotificationHeader } from "@/components/notifications";
import { NotificationsLoader } from "@/components/ui";
// constants
import { EmptyStateType } from "@/constants/empty-state";
// helpers
import { cn } from "@/helpers/common.helper";
import { getNumberCount } from "@/helpers/string.helper";
// hooks
import { useAppTheme } from "@/hooks/store";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
import { usePlatformOS } from "@/hooks/use-platform-os";
@@ -93,18 +94,23 @@ export const NotificationPopover = observer(() => {
isMobile={isMobile}
>
<button
className={`group relative flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
isActive
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
} ${isSidebarCollapsed ? "justify-center" : ""}`}
className={cn(
`group relative flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-sm font-medium leading-5 outline-none ${
isActive
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-90"
}`,
{
"p-0 size-8 aspect-square justify-center mx-auto": isSidebarCollapsed,
}
)}
onClick={() => {
if (window.innerWidth < 768) toggleSidebar();
if (!isActive) setFetchNotifications(true);
setIsActive(!isActive);
}}
>
<Bell className="h-4 w-4" />
<Bell className="size-4" />
{isSidebarCollapsed ? null : <span>Notifications</span>}
{totalNotificationCount && totalNotificationCount > 0 ? (
isSidebarCollapsed ? (

View File

@@ -13,8 +13,6 @@ export * from "./form";
export * from "./join-project-modal";
export * from "./leave-project-modal";
export * from "./member-select";
export * from "./sidebar-list-item";
export * from "./sidebar-list";
export * from "./integration-card";
export * from "./member-list";
export * from "./member-list-item";

View File

@@ -1,12 +1,9 @@
export * from "./settings";
export * from "./sidebar";
export * from "./views";
export * from "./confirm-workspace-member-remove";
export * from "./create-workspace-form";
export * from "./delete-workspace-modal";
export * from "./help-section";
export * from "./logo";
export * from "./send-workspace-invitation-modal";
export * from "./sidebar-dropdown";
export * from "./sidebar-menu";
export * from "./sidebar-quick-action";
export * from "./workspace-active-cycles-upgrade";

View File

@@ -1,159 +0,0 @@
import { useRef, useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { ChevronUp, PenSquare, Search } from "lucide-react";
// types
import { TIssue } from "@plane/types";
// components
import { CreateUpdateIssueModal } from "@/components/issues";
// constants
import { EIssuesStoreType } from "@/constants/issue";
import { EUserWorkspaceRoles } from "@/constants/workspace";
// hooks
import { useAppTheme, useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store";
import useLocalStorage from "@/hooks/use-local-storage";
export const WorkspaceSidebarQuickAction = observer(() => {
// router
const { workspaceSlug: routerWorkspaceSlug } = useParams();
const workspaceSlug = routerWorkspaceSlug?.toString();
// states
const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false);
// store hooks
const { toggleCreateIssueModal, toggleCommandPaletteModal } = useCommandPalette();
const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme();
const { setTrackElement } = useEventTracker();
const { joinedProjectIds } = useProject();
const {
membership: { currentWorkspaceRole },
} = useUser();
const { storedValue, setValue } = useLocalStorage<Record<string, Partial<TIssue>>>("draftedIssue", {});
//useState control for displaying draft issue button instead of group hover
const [isDraftButtonOpen, setIsDraftButtonOpen] = useState(false);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const timeoutRef = useRef<any>();
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
const disabled = joinedProjectIds.length === 0;
const onMouseEnter = () => {
// if enter before time out clear the timeout
timeoutRef?.current && clearTimeout(timeoutRef.current);
setIsDraftButtonOpen(true);
};
const onMouseLeave = () => {
timeoutRef.current = setTimeout(() => {
setIsDraftButtonOpen(false);
}, 300);
};
const workspaceDraftIssue = workspaceSlug ? storedValue?.[workspaceSlug] ?? undefined : undefined;
const removeWorkspaceDraftIssue = () => {
const draftIssues = storedValue ?? {};
if (workspaceSlug && draftIssues[workspaceSlug]) delete draftIssues[workspaceSlug];
setValue(draftIssues);
return Promise.resolve();
};
return (
<>
<CreateUpdateIssueModal
isOpen={isDraftIssueModalOpen}
onClose={() => setIsDraftIssueModalOpen(false)}
data={workspaceDraftIssue ?? {}}
onSubmit={() => removeWorkspaceDraftIssue()}
isDraft
/>
<div
className={`mt-4 flex w-full cursor-pointer items-center justify-between px-4 ${
isSidebarCollapsed ? "flex-col gap-1" : "gap-2"
}`}
>
{isAuthorizedUser && (
<div
className={`relative flex w-full cursor-pointer items-center justify-between gap-1 rounded px-2 ${
isSidebarCollapsed
? "px-2 hover:bg-custom-sidebar-background-80"
: "border-[0.5px] border-custom-border-200 px-3 shadow-custom-sidebar-shadow-2xs"
}`}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<button
type="button"
className={`relative flex flex-shrink-0 flex-grow items-center gap-2 rounded py-1.5 outline-none ${
isSidebarCollapsed ? "justify-center" : ""
} ${disabled ? "cursor-not-allowed opacity-50" : ""}`}
onClick={() => {
setTrackElement("APP_SIDEBAR_QUICK_ACTIONS");
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
}}
disabled={disabled}
>
<PenSquare className="h-4 w-4 text-custom-sidebar-text-300" />
{!isSidebarCollapsed && <span className="text-sm font-medium">New Issue</span>}
</button>
{!disabled && workspaceDraftIssue && (
<>
<div
className={`h-8 w-0.5 bg-custom-sidebar-background-80 ${isSidebarCollapsed ? "hidden" : "block"}`}
/>
<button
type="button"
className={`ml-1.5 flex flex-shrink-0 items-center justify-center rounded py-1.5 ${
isSidebarCollapsed ? "hidden" : "block"
}`}
>
<ChevronUp
className={`h-4 w-4 rotate-180 transform !text-custom-sidebar-text-300 transition-transform duration-300 ${
isDraftButtonOpen ? "rotate-0" : ""
}`}
/>
</button>
<div
className={`fixed left-4 mt-0 h-10 w-[203px] pt-2 ${isSidebarCollapsed ? "top-[5.5rem]" : "top-24"} ${
isDraftButtonOpen ? "block" : "hidden"
}`}
>
<div className="h-full w-full">
<button
onClick={() => setIsDraftIssueModalOpen(true)}
className="flex w-full flex-shrink-0 items-center rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-3 py-[10px] text-sm text-custom-text-300 shadow"
>
<PenSquare size={16} className="mr-2 !text-lg !leading-4 text-custom-sidebar-text-300" />
Last Drafted Issue
</button>
</div>
</div>
</>
)}
</div>
)}
<button
className={`flex flex-shrink-0 items-center gap-2 rounded p-2 outline-none ${
isAuthorizedUser ? "justify-center" : "w-full"
} ${
isSidebarCollapsed
? "hover:bg-custom-sidebar-background-80"
: "border-[0.5px] border-custom-border-200 shadow-custom-sidebar-shadow-2xs"
}`}
onClick={() => toggleCommandPaletteModal(true)}
>
<Search className="h-4 w-4 text-custom-sidebar-text-300" />
{!isAuthorizedUser && !isSidebarCollapsed && <span className="text-xs font-medium">Open command menu</span>}
</button>
</div>
</>
);
});

View File

@@ -13,12 +13,10 @@ import { Menu, Transition } from "@headlessui/react";
import { IWorkspace } from "@plane/types";
// plane ui
import { Avatar, Loader, TOAST_TYPE, setToast } from "@plane/ui";
import { GOD_MODE_URL } from "@/helpers/common.helper";
import { GOD_MODE_URL, cn } from "@/helpers/common.helper";
// hooks
import { useAppTheme, useUser, useUserProfile, useWorkspace } from "@/hooks/store";
// plane web components
import { AppSwitcher } from "@/plane-web/components/sidebar";
import { WorkspaceLogo } from "./logo";
import { WorkspaceLogo } from "../logo";
// Static Data
const userLinks = (workspaceSlug: string) => [
@@ -35,6 +33,7 @@ const userLinks = (workspaceSlug: string) => [
icon: Settings,
},
];
const profileLinks = (workspaceSlug: string, userId: string) => [
{
name: "My activity",
@@ -47,7 +46,8 @@ const profileLinks = (workspaceSlug: string, userId: string) => [
link: "/profile",
},
];
export const WorkspaceSidebarDropdown = observer(() => {
export const SidebarDropdown = observer(() => {
// router params
const { workspaceSlug } = useParams();
// store hooks
@@ -77,10 +77,12 @@ export const WorkspaceSidebarDropdown = observer(() => {
},
],
});
const handleWorkspaceNavigation = (workspace: IWorkspace) =>
updateUserProfile({
last_workspace_id: workspace?.id,
});
const handleSignOut = async () => {
await signOut().catch(() =>
setToast({
@@ -90,6 +92,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
})
);
};
const handleItemClick = () => {
if (window.innerWidth < 768) {
toggleSidebar();
@@ -98,32 +101,41 @@ export const WorkspaceSidebarDropdown = observer(() => {
const workspacesList = Object.values(workspaces ?? {});
// TODO: fix workspaces list scroll
return (
<div className="flex items-center gap-x-3 gap-y-2 px-4 pt-4">
<Menu as="div" className="relative h-full flex-grow truncate text-left">
<div className="flex items-center justify-center gap-x-3 gap-y-2">
<Menu
as="div"
className={cn("relative h-full truncate text-left flex-grow flex justify-stretch", {
"flex-grow-0 justify-center": sidebarCollapsed,
})}
>
{({ open }) => (
<>
<Menu.Button className="group/menu-button h-full w-full truncate rounded-md text-sm font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:outline-none">
<div
className={`flex items-center gap-x-2 truncate rounded p-1 ${
sidebarCollapsed ? "justify-center" : "justify-between"
}`}
>
<div className="flex items-center gap-2 truncate">
<WorkspaceLogo logo={activeWorkspace?.logo} name={activeWorkspace?.name} />
{!sidebarCollapsed && (
<h4 className="truncate text-base font-medium text-custom-text-100">
{activeWorkspace?.name ? activeWorkspace.name : "Loading..."}
</h4>
)}
</div>
<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,
}
)}
>
<div className="flex-grow flex items-center gap-2 truncate">
<WorkspaceLogo logo={activeWorkspace?.logo} name={activeWorkspace?.name} />
{!sidebarCollapsed && (
<ChevronDown
className={`mx-1 hidden h-4 w-4 flex-shrink-0 group-hover/menu-button:block ${
open ? "rotate-180" : ""
} text-custom-sidebar-text-400 duration-300`}
/>
<h4 className="truncate text-base font-medium text-custom-text-100">
{activeWorkspace?.name ?? "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}
@@ -135,61 +147,59 @@ export const WorkspaceSidebarDropdown = observer(() => {
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items as={Fragment}>
<div className="fixed left-4 z-20 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="fixed top-12 left-4 z-20 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="vertical-scrollbar scrollbar-sm mb-2 flex max-h-96 flex-col items-start justify-start gap-2 overflow-y-scroll px-4">
<h6 className="sticky top-0 z-10 h-full w-full bg-custom-sidebar-background-100 pb-1 pt-3 text-sm font-medium text-custom-sidebar-text-400">
{currentUser?.email}
</h6>
<AppSwitcher />
{workspacesList ? (
<div className="flex h-full w-full flex-col items-start justify-start gap-1.5">
{workspacesList.length > 0 &&
workspacesList.map((workspace) => (
<Link
key={workspace.id}
href={`/${workspace.slug}`}
onClick={() => {
handleWorkspaceNavigation(workspace);
handleItemClick();
}}
className="w-full"
<div className="size-full flex flex-col items-start justify-start gap-1.5">
{workspacesList.map((workspace) => (
<Link
key={workspace.id}
href={`/${workspace.slug}`}
onClick={() => {
handleWorkspaceNavigation(workspace);
handleItemClick();
}}
className="w-full"
>
<Menu.Item
as="div"
className="flex items-center justify-between gap-1 rounded p-1 text-sm text-custom-sidebar-text-100 hover:bg-custom-sidebar-background-80"
>
<Menu.Item
as="div"
className="flex items-center justify-between gap-1 rounded p-1 text-sm text-custom-sidebar-text-100 hover:bg-custom-sidebar-background-80"
>
<div className="flex items-center justify-start gap-2.5 truncate">
<span
className={`relative flex h-6 w-6 flex-shrink-0 items-center justify-center p-2 text-xs uppercase ${
!workspace?.logo && "rounded bg-custom-primary-500 text-white"
}`}
>
{workspace?.logo && workspace.logo !== "" ? (
<img
src={workspace.logo}
className="absolute left-0 top-0 h-full w-full rounded object-cover"
alt="Workspace Logo"
/>
) : (
workspace?.name?.charAt(0) ?? "..."
)}
</span>
<h5
className={`truncate text-sm font-medium ${
workspaceSlug === workspace.slug ? "" : "text-custom-text-200"
}`}
>
{workspace.name}
</h5>
</div>
{workspace.id === activeWorkspace?.id && (
<span className="flex-shrink-0 p-1">
<Check className="h-5 w-5 text-custom-sidebar-text-100" />
</span>
)}
</Menu.Item>
</Link>
))}
<div className="flex items-center justify-start gap-2.5 truncate">
<span
className={`relative flex h-6 w-6 flex-shrink-0 items-center justify-center p-2 text-xs uppercase ${
!workspace?.logo && "rounded bg-custom-primary-500 text-white"
}`}
>
{workspace?.logo && workspace.logo !== "" ? (
<img
src={workspace.logo}
className="absolute left-0 top-0 h-full w-full rounded object-cover"
alt="Workspace Logo"
/>
) : (
workspace?.name?.charAt(0) ?? "..."
)}
</span>
<h5
className={`truncate text-sm font-medium ${
workspaceSlug === workspace.slug ? "" : "text-custom-text-200"
}`}
>
{workspace.name}
</h5>
</div>
{workspace.id === activeWorkspace?.id && (
<span className="flex-shrink-0 p-1">
<Check className="h-5 w-5 text-custom-sidebar-text-100" />
</span>
)}
</Menu.Item>
</Link>
))}
</div>
) : (
<div className="w-full">
@@ -200,7 +210,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
</div>
)}
</div>
<div className="flex w-full flex-col items-start justify-start gap-2 px-4 py-2 text-sm">
<div className="w-full flex flex-col items-start justify-start gap-2 px-4 py-2 text-sm">
<Link href="/create-workspace" className="w-full">
<Menu.Item
as="div"
@@ -236,7 +246,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
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="h-4 w-4 flex-shrink-0" />
<LogOut className="size-4 flex-shrink-0" />
Sign out
</Menu.Item>
</div>
@@ -299,7 +309,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
onClick={handleSignOut}
>
<LogOut className="h-4 w-4 stroke-[1.5]" />
<LogOut className="size-4 stroke-[1.5]" />
Sign out
</Menu.Item>
</div>

View File

@@ -39,7 +39,7 @@ export interface WorkspaceHelpSectionProps {
setSidebarActive?: React.Dispatch<React.SetStateAction<boolean>>;
}
export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(() => {
export const SidebarHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(() => {
// store hooks
const { sidebarCollapsed, toggleSidebar } = useAppTheme();
const { toggleShortcutModal } = useCommandPalette();
@@ -63,7 +63,7 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observe
<>
<div
className={cn(
"flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 px-4 h-14 flex-shrink-0",
"flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 h-14 flex-shrink-0",
{
"flex-col h-auto py-1.5": isCollapsed,
}

View File

@@ -0,0 +1,7 @@
export * from "./dropdown";
export * from "./help-section";
export * from "./projects-list-item";
export * from "./projects-list";
export * from "./quick-actions";
export * from "./user-menu";
export * from "./workspace-menu";

View File

@@ -10,9 +10,7 @@ import { observer } from "mobx-react";
import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
import { createRoot } from "react-dom/client";
// icons
import {
MoreVertical,
PenSquare,
LinkIcon,
Star,
@@ -20,11 +18,10 @@ import {
Settings,
Share2,
LogOut,
ChevronDown,
MoreHorizontal,
Inbox,
ChevronRight,
} from "lucide-react";
// headless ui
import { Disclosure, Transition } from "@headlessui/react";
// ui
import {
@@ -37,6 +34,7 @@ import {
LayersIcon,
setPromiseToast,
DropIndicator,
DragHandle,
} from "@plane/ui";
// components
import { Logo } from "@/components/common";
@@ -49,10 +47,8 @@ import { cn } from "@/helpers/common.helper";
import { useAppTheme, useEventTracker, useProject } from "@/hooks/store";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
import { usePlatformOS } from "@/hooks/use-platform-os";
import { HIGHLIGHT_CLASS, highlightIssueOnDrop } from "../issues/issue-layouts/utils";
// helpers
// components
// constants
import { HIGHLIGHT_CLASS, highlightIssueOnDrop } from "../../issues/issue-layouts/utils";
type Props = {
projectId: string;
@@ -106,12 +102,11 @@ const navigation = (workspaceSlug: string, projectId: string) => [
},
];
export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
const { projectId, handleCopyText, disableDrag, disableDrop, isLastChild, handleOnProjectDrop, projectListType } =
props;
// store hooks
const { sidebarCollapsed: isSideBarCollapsed, toggleSidebar } = useAppTheme();
const { sidebarCollapsed: isSidebarCollapsed, toggleSidebar } = useAppTheme();
const { setTrackElement } = useEventTracker();
const { addProjectToFavorites, removeProjectFromFavorites, getProjectById } = useProject();
const { isMobile } = usePlatformOS();
@@ -175,10 +170,6 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
setLeaveProjectModal(true);
};
const handleLeaveProjectModalClose = () => {
setLeaveProjectModal(false);
};
const handleProjectClick = () => {
if (window.innerWidth < 768) {
toggleSidebar();
@@ -194,7 +185,7 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
return combine(
draggable({
element,
canDrag: () => !disableDrag && !isSideBarCollapsed,
canDrag: () => !disableDrag && !isSidebarCollapsed,
dragHandle: dragHandleElement ?? undefined,
getInitialData: () => ({ id: projectId, dragInstanceId: "PROJECTS" }),
onDragStart: () => {
@@ -282,20 +273,22 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
return (
<>
<PublishProjectModal isOpen={publishModalOpen} project={project} onClose={() => setPublishModal(false)} />
<LeaveProjectModal project={project} isOpen={leaveProjectModalOpen} onClose={handleLeaveProjectModalClose} />
<LeaveProjectModal project={project} isOpen={leaveProjectModalOpen} onClose={() => setLeaveProjectModal(false)} />
<Disclosure key={`${project.id}_${URLProjectId}`} ref={projectRef} defaultOpen={URLProjectId === project.id}>
{({ open }) => (
<div
id={`sidebar-${projectId}-${projectListType}`}
className={cn("rounded relative", { "bg-custom-sidebar-background-80 opacity-60": isDragging })}
className={cn("relative", {
"bg-custom-sidebar-background-80 opacity-60": isDragging,
})}
>
<DropIndicator classNames="absolute top-0" isVisible={instruction === "DRAG_OVER"} />
<div
className={cn(
"h-9 group relative flex w-full px-3 py-2 items-center rounded-md text-custom-sidebar-text-100 hover:bg-custom-sidebar-background-80",
"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-80": isMenuActive,
"pr-0": !isSideBarCollapsed,
"bg-custom-sidebar-background-90": isMenuActive,
"p-0 size-7 aspect-square justify-center mx-auto": isSidebarCollapsed,
}
)}
>
@@ -309,148 +302,153 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
<button
type="button"
className={cn(
"absolute -left-[18px] flex opacity-0 rounded text-custom-sidebar-text-400 ml-2 cursor-grab",
"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",
{
"group-hover:opacity-100": !isSideBarCollapsed,
"cursor-not-allowed opacity-60": project.sort_order === null,
"cursor-grabbing": isDragging,
flex: isMenuActive,
hidden: isSideBarCollapsed,
"!hidden": isSidebarCollapsed,
}
)}
ref={dragHandleRef}
>
<MoreVertical className="-ml-3 h-3.5" />
<MoreVertical className="-ml-5 h-3.5" />
<DragHandle className="bg-transparent" />
</button>
</Tooltip>
)}
<Tooltip
tooltipContent={`${project.name}`}
position="right"
disabled={!isSideBarCollapsed}
isMobile={isMobile}
>
<Disclosure.Button
as="div"
className={cn(
"flex flex-grow cursor-pointer select-none items-center justify-between truncate text-left text-sm font-medium",
{
"justify-center": isSideBarCollapsed,
}
)}
>
<div
className={cn("flex w-full flex-grow items-center gap-2.5 truncate", {
"justify-center": isSideBarCollapsed,
})}
>
<div className="size-5 grid place-items-center flex-shrink-0">
<Logo logo={project.logo_props} />
</div>
{!isSideBarCollapsed && <p className="truncate text-custom-sidebar-text-200">{project.name}</p>}
{isSidebarCollapsed ? (
<Disclosure.Button as="button" className="grid place-items-center">
<div className="size-4 grid place-items-center flex-shrink-0">
<Logo logo={project.logo_props} size={16} />
</div>
{!isSideBarCollapsed && (
<ChevronDown
className={cn(
"hidden h-4 w-4 flex-shrink-0 text-custom-sidebar-text-400 duration-300 group-hover:block",
{
"rotate-180": open,
block: isMenuActive,
}
)}
/>
)}
</Disclosure.Button>
</Tooltip>
{!isSideBarCollapsed && (
<CustomMenu
customButton={
<div
ref={actionSectionRef}
className="size-5 flex items-center justify-center cursor-pointer px-1 text-custom-sidebar-text-400 duration-300"
onClick={() => setIsMenuActive(!isMenuActive)}
) : (
<>
<Tooltip
tooltipContent={`${project.name}`}
position="right"
disabled={!isSidebarCollapsed}
isMobile={isMobile}
>
<Link
href={`/${workspaceSlug}/projects/${project.id}/issues`}
className={cn("flex-grow flex items-center gap-1.5 truncate text-left select-none", {
"justify-center": isSidebarCollapsed,
})}
>
<MoreHorizontal className="h-3.5 w-3.5" />
</div>
}
className={cn("hidden flex-shrink-0 mr-1 group-hover:block", {
"!block": isMenuActive,
})}
buttonClassName="!text-custom-sidebar-text-400"
placement="bottom-start"
>
{!project.is_favorite && (
<CustomMenu.MenuItem onClick={handleAddToFavorites}>
<span className="flex items-center justify-start gap-2">
<Star className="h-3.5 w-3.5 stroke-[1.5]" />
<span>Add to favorites</span>
</span>
</CustomMenu.MenuItem>
)}
{project.is_favorite && (
<CustomMenu.MenuItem onClick={handleRemoveFromFavorites}>
<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>
)}
{/* 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>{project.anchor ? "Publish settings" : "Publish"}</div>
</div>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem>
<Link href={`/${workspaceSlug}/projects/${project?.id}/draft-issues/`}>
<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>Draft issues</span>
<div className="size-4 grid place-items-center flex-shrink-0">
<Logo logo={project.logo_props} size={16} />
</div>
{!isSidebarCollapsed && (
<p className="truncate text-sm font-medium text-custom-sidebar-text-200">{project.name}</p>
)}
</Link>
</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>Copy link</span>
</span>
</CustomMenu.MenuItem>
{!isViewerOrGuest && (
</Tooltip>
<Disclosure.Button
as="button"
className={cn(
"hidden group-hover/project-item:inline-block p-0.5 rounded hover:bg-custom-sidebar-background-80",
{
"inline-block": isMenuActive,
}
)}
>
<ChevronRight
className={cn("size-3.5 flex-shrink-0 text-custom-sidebar-text-400 transition-transform", {
"rotate-90": open,
})}
/>
</Disclosure.Button>
<CustomMenu
customButton={
<span
ref={actionSectionRef}
className="grid place-items-center p-0.5 text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80 rounded"
onClick={() => setIsMenuActive(!isMenuActive)}
>
<MoreHorizontal className="size-4" />
</span>
}
className={cn(
"opacity-0 pointer-events-none flex-shrink-0 mr-1 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"
>
{!project.is_favorite && (
<CustomMenu.MenuItem onClick={handleAddToFavorites}>
<span className="flex items-center justify-start gap-2">
<Star className="h-3.5 w-3.5 stroke-[1.5]" />
<span>Add to favorites</span>
</span>
</CustomMenu.MenuItem>
)}
{project.is_favorite && (
<CustomMenu.MenuItem onClick={handleRemoveFromFavorites}>
<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>
)}
{/* 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>{project.anchor ? "Publish settings" : "Publish"}</div>
</div>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem>
<Link href={`/${workspaceSlug}/projects/${project?.id}/archives/issues`}>
<Link href={`/${workspaceSlug}/projects/${project?.id}/draft-issues/`}>
<div className="flex items-center justify-start gap-2">
<ArchiveIcon className="h-3.5 w-3.5 stroke-[1.5]" />
<span>Archives</span>
<PenSquare className="h-3.5 w-3.5 stroke-[1.5] text-custom-text-300" />
<span>Draft issues</span>
</div>
</Link>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem>
<Link href={`/${workspaceSlug}/projects/${project?.id}/settings`}>
<div className="flex items-center justify-start gap-2">
<Settings className="h-3.5 w-3.5 stroke-[1.5]" />
<span>Settings</span>
</div>
</Link>
</CustomMenu.MenuItem>
{/* leave project */}
{isViewerOrGuest && (
<CustomMenu.MenuItem onClick={handleLeaveProject}>
<div className="flex items-center justify-start gap-2">
<LogOut className="h-3.5 w-3.5 stroke-[1.5]" />
<span>Leave project</span>
</div>
<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>Copy link</span>
</span>
</CustomMenu.MenuItem>
)}
</CustomMenu>
{!isViewerOrGuest && (
<CustomMenu.MenuItem>
<Link href={`/${workspaceSlug}/projects/${project?.id}/archives/issues`}>
<div className="flex items-center justify-start gap-2">
<ArchiveIcon className="h-3.5 w-3.5 stroke-[1.5]" />
<span>Archives</span>
</div>
</Link>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem>
<Link href={`/${workspaceSlug}/projects/${project?.id}/settings`}>
<div className="flex items-center justify-start gap-2">
<Settings className="h-3.5 w-3.5 stroke-[1.5]" />
<span>Settings</span>
</div>
</Link>
</CustomMenu.MenuItem>
{/* leave project */}
{isViewerOrGuest && (
<CustomMenu.MenuItem onClick={handleLeaveProject}>
<div className="flex items-center justify-start gap-2">
<LogOut className="h-3.5 w-3.5 stroke-[1.5]" />
<span>Leave project</span>
</div>
</CustomMenu.MenuItem>
)}
</CustomMenu>
</>
)}
</div>
@@ -462,8 +460,8 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
<Disclosure.Panel className={`mt-1 space-y-1 ${isSideBarCollapsed ? "" : "ml-[1.25rem]"}`}>
{navigation(workspaceSlug as string, project?.id).map((item) => {
<Disclosure.Panel as="div" className="mt-1 space-y-1">
{navigation(workspaceSlug?.toString(), project?.id).map((item) => {
if (
(item.name === "Cycles" && !project.cycle_view) ||
(item.name === "Modules" && !project.module_view) ||
@@ -475,26 +473,27 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
return (
<Link key={item.name} href={item.href} onClick={handleProjectClick}>
<span className="block w-full">
<Tooltip
isMobile={isMobile}
tooltipContent={`${project?.name}: ${item.name}`}
position="right"
className="ml-2"
disabled={!isSideBarCollapsed}
<Tooltip
isMobile={isMobile}
tooltipContent={`${project?.name}: ${item.name}`}
position="right"
className="ml-2"
disabled={!isSidebarCollapsed}
>
<div
className={cn(
"flex items-center gap-1.5 rounded-md pl-[30px] pr-2 py-1.5 outline-none text-custom-sidebar-text-300 hover:bg-custom-sidebar-background-90 focus:bg-custom-sidebar-background-90",
{
"text-custom-primary-100 bg-custom-primary-100/10 hover:bg-custom-primary-100/10":
pathname.includes(item.href),
"p-0 size-7 justify-center mx-auto": isSidebarCollapsed,
}
)}
>
<div
className={`group flex items-center gap-2.5 rounded-md px-2 py-1.5 text-xs font-medium outline-none ${
pathname.includes(item.href)
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-300 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
} ${isSideBarCollapsed ? "justify-center" : ""}`}
>
<item.Icon className="h-4 w-4 stroke-[1.5]" />
{!isSideBarCollapsed && item.name}
</div>
</Tooltip>
</span>
<item.Icon className="flex-shrink-0 size-4 stroke-[1.5]" />
{!isSidebarCollapsed && <span className="text-xs font-medium">{item.name}</span>}
</div>
</Tooltip>
</Link>
);
})}

View File

@@ -5,14 +5,15 @@ 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 } from "next/navigation";
import { ChevronDown, ChevronRight, Plus } from "lucide-react";
import { ChevronRight, Plus } from "lucide-react";
import { Disclosure, Transition } from "@headlessui/react";
// types
import { IProject } from "@plane/types";
// ui
import { TOAST_TYPE, setToast } from "@plane/ui";
import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
// components
import { CreateProjectModal, ProjectSidebarListItem } from "@/components/project";
import { CreateProjectModal } from "@/components/project";
import { SidebarProjectsListItem } from "@/components/workspace";
// constants
import { EUserWorkspaceRoles } from "@/constants/workspace";
// helpers
@@ -22,7 +23,7 @@ import { copyUrlToClipboard } from "@/helpers/string.helper";
// hooks
import { useAppTheme, useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store";
export const ProjectSidebarList: FC = observer(() => {
export const SidebarProjectsList: FC = observer(() => {
// get local storage data for isFavoriteProjectsListOpen and isAllProjectsListOpen
const isFavProjectsListOpenInLocalStorage = localStorage.getItem("isFavoriteProjectsListOpen");
const isAllProjectsListOpenInLocalStorage = localStorage.getItem("isAllProjectsListOpen");
@@ -51,7 +52,7 @@ export const ProjectSidebarList: FC = observer(() => {
} = useProject();
// router params
const { workspaceSlug } = useParams();
// auth
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
const handleCopyText = (projectId: string) => {
@@ -141,126 +142,102 @@ export const ProjectSidebarList: FC = observer(() => {
}
};
const projectSections: {
key: "all" | "favorite";
type: "FAVORITES" | "JOINED";
title: string;
shortTitle: string;
projects: string[];
isOpen: boolean;
}[] = [
{
key: "favorite",
type: "FAVORITES",
title: "Favorites",
shortTitle: "FP",
projects: favoriteProjects,
isOpen: isFavoriteProjectsListOpen,
},
{
key: "all",
type: "JOINED",
title: "My projects",
shortTitle: "MP",
projects: joinedProjects,
isOpen: isAllProjectsListOpen,
},
];
return (
<>
{workspaceSlug && (
<CreateProjectModal
isOpen={isProjectModalOpen}
onClose={() => {
setIsProjectModalOpen(false);
}}
onClose={() => setIsProjectModalOpen(false)}
setToFavorite={isFavoriteProjectCreate}
workspaceSlug={workspaceSlug.toString()}
/>
)}
<div
ref={containerRef}
className={cn(
"vertical-scrollbar h-full space-y-2 !overflow-y-scroll pl-4",
isCollapsed ? "scrollbar-sm" : "scrollbar-md",
{
"border-t border-custom-sidebar-border-300": isScrolled,
"pr-1": !isCollapsed,
}
)}
className={cn("vertical-scrollbar h-full space-y-2 !overflow-y-scroll scrollbar-sm -mr-3 -ml-4 pl-4", {
"border-t border-custom-sidebar-border-300": isScrolled,
})}
>
<div>
{favoriteProjects && favoriteProjects.length > 0 && (
<Disclosure as="div" className="flex flex-col" defaultOpen={isFavoriteProjectCreate}>
{projectSections.map((section) => {
if (!section.projects || section.projects.length === 0) return;
return (
<Disclosure key={section.title} as="div" className="flex flex-col" defaultOpen={section.isOpen}>
<>
{!isCollapsed && (
<div className="group flex w-full items-center justify-between rounded p-1.5 text-xs text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80">
<Disclosure.Button
as="button"
type="button"
className="group flex w-full items-center gap-1 whitespace-nowrap rounded px-1.5 text-left text-sm font-semibold text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80"
onClick={() => toggleListDisclosure(!isFavoriteProjectsListOpen, "favorite")}
>
Favorites
{isFavoriteProjectsListOpen ? (
<ChevronDown className="h-3.5 w-3.5" />
) : (
<ChevronRight className="h-3.5 w-3.5" />
)}
</Disclosure.Button>
{isAuthorizedUser && (
<button
className="opacity-0 group-hover:opacity-100"
onClick={() => {
setTrackElement("APP_SIDEBAR_FAVORITES_BLOCK");
setIsFavoriteProjectCreate(true);
setIsProjectModalOpen(true);
}}
>
<Plus className="h-3 w-3" />
</button>
)}
</div>
)}
<Transition
show={isFavoriteProjectsListOpen}
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"
>
{isFavoriteProjectsListOpen && (
<Disclosure.Panel as="div" static>
{favoriteProjects.map((projectId, index) => (
<ProjectSidebarListItem
key={projectId}
projectId={projectId}
handleCopyText={() => handleCopyText(projectId)}
projectListType="FAVORITES"
disableDrag
disableDrop
isLastChild={index === favoriteProjects.length - 1}
/>
))}
</Disclosure.Panel>
<div
className={cn(
"group w-full flex items-center justify-between px-2 py-0.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,
}
)}
</Transition>
</>
</Disclosure>
)}
</div>
<div>
{joinedProjects && joinedProjects.length > 0 && (
<Disclosure as="div" className="flex flex-col" defaultOpen={isAllProjectsListOpen}>
<>
{!isCollapsed && (
<div className="group flex w-full items-center justify-between rounded p-1.5 text-xs text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80">
<Disclosure.Button
as="button"
type="button"
className="group flex w-full items-center gap-1 whitespace-nowrap rounded px-1.5 text-left text-sm font-semibold text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80"
onClick={() => toggleListDisclosure(!isAllProjectsListOpen, "all")}
>
Your projects
{isAllProjectsListOpen ? (
<ChevronDown className="h-3.5 w-3.5" />
) : (
<ChevronRight className="h-3.5 w-3.5" />
)}
</Disclosure.Button>
{isAuthorizedUser && (
>
<Disclosure.Button
as="button"
type="button"
className={cn(
"group w-full flex items-center gap-1 whitespace-nowrap text-left text-sm font-medium text-custom-sidebar-text-400",
{
"!text-center w-8 px-2 py-0.5 justify-center": isCollapsed,
}
)}
onClick={() => toggleListDisclosure(!section.isOpen, section.key)}
>
<Tooltip tooltipHeading={section.title} tooltipContent="">
<span>{isCollapsed ? section.shortTitle : section.title}</span>
</Tooltip>
{!isCollapsed && (
<ChevronRight
className={cn("flex-shrink-0 size-3.5 transition-all", {
"rotate-90": section.isOpen,
})}
/>
)}
</Disclosure.Button>
{!isCollapsed && isAuthorizedUser && (
<Tooltip tooltipHeading="Create project" tooltipContent="">
<button
className="opacity-0 group-hover:opacity-100"
className="opacity-0 group-hover:opacity-100 p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0"
onClick={() => {
setTrackElement("Sidebar");
setIsFavoriteProjectCreate(false);
setTrackElement(`APP_SIDEBAR_${section.type}_BLOCK`);
setIsFavoriteProjectCreate(section.key === "favorite");
setIsProjectModalOpen(true);
}}
>
<Plus className="h-3 w-3" />
<Plus className="size-3" />
</button>
)}
</div>
)}
</Tooltip>
)}
</div>
<Transition
show={isAllProjectsListOpen}
show={section.isOpen}
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
@@ -268,15 +245,17 @@ export const ProjectSidebarList: FC = observer(() => {
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
{isAllProjectsListOpen && (
<Disclosure.Panel as="div" static>
{joinedProjects.map((projectId, index) => (
<ProjectSidebarListItem
{section.isOpen && (
<Disclosure.Panel as="div" className="mt-3" static>
{section.projects.map((projectId, index) => (
<SidebarProjectsListItem
key={projectId}
projectId={projectId}
projectListType="JOINED"
handleCopyText={() => handleCopyText(projectId)}
isLastChild={index === joinedProjects.length - 1}
projectListType={section.type}
disableDrag={section.key === "favorite"}
disableDrop={section.key === "favorite"}
isLastChild={index === section.projects.length - 1}
handleOnProjectDrop={handleOnProjectDrop}
/>
))}
@@ -285,10 +264,9 @@ export const ProjectSidebarList: FC = observer(() => {
</Transition>
</>
</Disclosure>
)}
</div>
{isAuthorizedUser && joinedProjects && joinedProjects.length === 0 && (
);
})}
{isAuthorizedUser && joinedProjects?.length === 0 && (
<button
type="button"
className="flex w-full items-center gap-2 px-3 text-sm text-custom-sidebar-text-200"
@@ -297,7 +275,7 @@ export const ProjectSidebarList: FC = observer(() => {
toggleCreateProjectModal(true);
}}
>
<Plus className="h-5 w-5" />
<Plus className="size-5" />
{!isCollapsed && "Add Project"}
</button>
)}

View File

@@ -0,0 +1,151 @@
import { useRef, useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { ChevronUp, PenSquare, Search } from "lucide-react";
// types
import { TIssue } from "@plane/types";
// components
import { CreateUpdateIssueModal } from "@/components/issues";
// constants
import { EIssuesStoreType } from "@/constants/issue";
import { EUserWorkspaceRoles } from "@/constants/workspace";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useAppTheme, useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store";
import useLocalStorage from "@/hooks/use-local-storage";
export const SidebarQuickActions = observer(() => {
// states
const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false);
const [isDraftButtonOpen, setIsDraftButtonOpen] = useState(false);
// refs
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const timeoutRef = useRef<any>();
// router
const { workspaceSlug: routerWorkspaceSlug } = useParams();
const workspaceSlug = routerWorkspaceSlug?.toString();
// store hooks
const { toggleCreateIssueModal, toggleCommandPaletteModal } = useCommandPalette();
const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme();
const { setTrackElement } = useEventTracker();
const { joinedProjectIds } = useProject();
const {
membership: { currentWorkspaceRole },
} = useUser();
// local storage
const { storedValue, setValue } = useLocalStorage<Record<string, Partial<TIssue>>>("draftedIssue", {});
// derived values
const disabled = joinedProjectIds.length === 0;
const workspaceDraftIssue = workspaceSlug ? storedValue?.[workspaceSlug] ?? undefined : undefined;
// auth
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
const handleMouseEnter = () => {
// if enter before time out clear the timeout
timeoutRef?.current && clearTimeout(timeoutRef.current);
setIsDraftButtonOpen(true);
};
const handleMouseLeave = () => {
timeoutRef.current = setTimeout(() => {
setIsDraftButtonOpen(false);
}, 300);
};
const removeWorkspaceDraftIssue = () => {
const draftIssues = storedValue ?? {};
if (workspaceSlug && draftIssues[workspaceSlug]) delete draftIssues[workspaceSlug];
setValue(draftIssues);
return Promise.resolve();
};
return (
<>
<CreateUpdateIssueModal
isOpen={isDraftIssueModalOpen}
onClose={() => setIsDraftIssueModalOpen(false)}
data={workspaceDraftIssue ?? {}}
onSubmit={() => removeWorkspaceDraftIssue()}
isDraft
/>
<div
className={cn("flex items-center justify-between gap-1 cursor-pointer", {
"flex-col gap-0": isSidebarCollapsed,
})}
>
{isAuthorizedUser && (
<div
className={cn(
"relative flex-grow flex items-center justify-between gap-1 rounded h-8 hover:bg-custom-sidebar-background-90",
{
"size-8 aspect-square": isSidebarCollapsed,
"px-3 border-[0.5px] border-custom-sidebar-border-300": !isSidebarCollapsed,
}
)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<button
type="button"
className={cn("relative flex flex-shrink-0 flex-grow items-center gap-2 rounded outline-none", {
"justify-center": isSidebarCollapsed,
"cursor-not-allowed opacity-50": disabled,
})}
onClick={() => {
setTrackElement("APP_SIDEBAR_QUICK_ACTIONS");
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
}}
disabled={disabled}
>
<PenSquare className="size-4 text-custom-sidebar-text-300" />
{!isSidebarCollapsed && <span className="text-sm font-medium">New issue</span>}
</button>
{!disabled && workspaceDraftIssue && (
<>
{!isSidebarCollapsed && (
<button type="button" className="grid place-items-center">
<ChevronUp
className={cn(
"size-4 transform !text-custom-sidebar-text-300 transition-transform duration-300",
{
"rotate-180": isDraftButtonOpen,
}
)}
/>
</button>
)}
{isDraftButtonOpen && (
<div
className={`fixed left-4 mt-0 h-10 w-[203px] pt-2 ${isSidebarCollapsed ? "top-[5.5rem]" : "top-24"}`}
>
<div className="h-full w-full">
<button
onClick={() => setIsDraftIssueModalOpen(true)}
className="flex w-full flex-shrink-0 items-center rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-3 py-[10px] text-sm text-custom-text-300 shadow"
>
<PenSquare size={16} className="mr-2 !text-lg !leading-4 text-custom-sidebar-text-300" />
Last Drafted Issue
</button>
</div>
</div>
)}
</>
)}
</div>
)}
<button
className={cn(
"flex-shrink-0 size-8 aspect-square grid place-items-center rounded hover:bg-custom-sidebar-background-90 outline-none",
{
"border-[0.5px] border-custom-sidebar-border-300": !isSidebarCollapsed,
}
)}
onClick={() => toggleCommandPaletteModal(true)}
>
<Search className="size-4 text-custom-sidebar-text-300" />
</button>
</div>
</>
);
});

View File

@@ -4,22 +4,21 @@ import React from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
import { Crown } from "lucide-react";
// ui
import { Tooltip } from "@plane/ui";
// components
import { NotificationPopover } from "@/components/notifications";
// constants
import { SIDEBAR_MENU_ITEMS } from "@/constants/dashboard";
import { SIDEBAR_USER_MENU_ITEMS } from "@/constants/dashboard";
import { SIDEBAR_CLICKED } from "@/constants/event-tracker";
import { EUserWorkspaceRoles } from "@/constants/workspace";
// helper
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useAppTheme, useEventTracker, useUser } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
export const WorkspaceSidebarMenu = observer(() => {
export const SidebarUserMenu = observer(() => {
// store hooks
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
const { captureEvent } = useEventTracker();
@@ -44,40 +43,38 @@ export const WorkspaceSidebarMenu = observer(() => {
};
return (
<div className="w-full cursor-pointer space-y-2 p-4">
{SIDEBAR_MENU_ITEMS.map(
<div
className={cn("w-full space-y-1", {
"space-y-0": sidebarCollapsed,
})}
>
{SIDEBAR_USER_MENU_ITEMS.map(
(link) =>
workspaceMemberInfo >= link.access && (
<Link key={link.key} href={`/${workspaceSlug}${link.href}`} onClick={() => handleLinkClick(link.key)}>
<span className="my-1 block w-full">
<Tooltip
tooltipContent={link.label}
position="right"
className="ml-2"
disabled={!sidebarCollapsed}
isMobile={isMobile}
>
<div
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
link.highlight(pathname, `/${workspaceSlug}`)
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
} ${sidebarCollapsed ? "justify-center" : ""}`}
>
<Tooltip
tooltipContent={link.label}
position="right"
className="ml-2"
disabled={!sidebarCollapsed}
isMobile={isMobile}
>
<div
className={cn(
"group w-full flex items-center gap-1.5 rounded-md px-2 py-1.5 outline-none text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-90 focus:bg-custom-sidebar-background-90",
{
<link.Icon
className={cn("h-4 w-4", {
"rotate-180": link.key === "active-cycles",
})}
/>
"text-custom-primary-100 bg-custom-primary-100/10 hover:bg-custom-primary-100/10": link.highlight(
pathname,
`/${workspaceSlug}`
),
"p-0 size-8 aspect-square justify-center mx-auto": sidebarCollapsed,
}
{!sidebarCollapsed && <p className="leading-5">{link.label}</p>}
{!sidebarCollapsed && link.key === "active-cycles" && (
<Crown className="h-3.5 w-3.5 text-amber-400" />
)}
</div>
</Tooltip>
</span>
)}
>
{<link.Icon className="size-4" />}
{!sidebarCollapsed && <p className="text-sm leading-5 font-medium">{link.label}</p>}
</div>
</Tooltip>
</Link>
)
)}

View File

@@ -0,0 +1,141 @@
"use client";
import React, { useEffect, useState } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
import { ChevronRight, Crown, Settings } from "lucide-react";
import { Disclosure, Transition } from "@headlessui/react";
// ui
import { Tooltip } from "@plane/ui";
// constants
import { SIDEBAR_WORKSPACE_MENU_ITEMS } from "@/constants/dashboard";
import { SIDEBAR_CLICKED } from "@/constants/event-tracker";
import { EUserWorkspaceRoles } from "@/constants/workspace";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useAppTheme, useEventTracker, useUser } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
export const SidebarWorkspaceMenu = observer(() => {
// states
const [isWorkspaceMenuOpen, setIsWorkspaceMenuOpen] = useState(true);
// store hooks
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
const { captureEvent } = useEventTracker();
const { isMobile } = usePlatformOS();
const {
membership: { currentWorkspaceRole },
} = useUser();
// router params
const { workspaceSlug } = useParams();
// pathname
const pathname = usePathname();
// computed
const workspaceMemberInfo = currentWorkspaceRole || EUserWorkspaceRoles.GUEST;
const handleLinkClick = (itemKey: string) => {
if (window.innerWidth < 768) {
toggleSidebar();
}
captureEvent(SIDEBAR_CLICKED, {
destination: itemKey,
});
};
useEffect(() => {
if (sidebarCollapsed) setIsWorkspaceMenuOpen(true);
}, [sidebarCollapsed]);
return (
<Disclosure as="div" defaultOpen>
{!sidebarCollapsed && (
<div className="group/workspace-button flex items-center justify-between text-custom-sidebar-text-400 px-2 py-0.5 hover:bg-custom-sidebar-background-90 rounded">
<Disclosure.Button
as="button"
className="flex-grow flex items-center gap-1 text-sm font-medium"
onClick={() => setIsWorkspaceMenuOpen((prev) => !prev)}
>
<span>Workspace</span>
<ChevronRight
className={cn("flex-shrink-0 size-3.5 transition-all", {
"rotate-90": isWorkspaceMenuOpen,
})}
/>
</Disclosure.Button>
<Link
href={`/${workspaceSlug}/settings`}
className="flex-shrink-0 hidden group-hover/workspace-button:block rounded p-0.5 hover:bg-custom-sidebar-background-80"
>
<Tooltip tooltipHeading="Workspace settings" tooltipContent="">
<Settings className="size-3" />
</Tooltip>
</Link>
</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"
>
{isWorkspaceMenuOpen && (
<Disclosure.Panel
as="div"
className={cn("mt-3 space-y-1", {
"space-y-0 mt-0": sidebarCollapsed,
})}
static
>
{SIDEBAR_WORKSPACE_MENU_ITEMS.map(
(link) =>
workspaceMemberInfo >= link.access && (
<Link
key={link.key}
href={`/${workspaceSlug}${link.href}`}
onClick={() => handleLinkClick(link.key)}
className="block"
>
<Tooltip
tooltipContent={link.label}
position="right"
className="ml-2"
disabled={!sidebarCollapsed}
isMobile={isMobile}
>
<div
className={cn(
"group w-full flex items-center gap-1.5 rounded-md px-2 py-1.5 outline-none text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-90 focus:bg-custom-sidebar-background-90",
{
"text-custom-primary-100 bg-custom-primary-100/10 hover:bg-custom-primary-100/10":
link.highlight(pathname, `/${workspaceSlug}`),
"p-0 size-8 aspect-square justify-center mx-auto": sidebarCollapsed,
}
)}
>
{
<link.Icon
className={cn("size-4", {
"rotate-180": link.key === "active-cycles",
})}
/>
}
{!sidebarCollapsed && <p className="text-sm leading-5 font-medium">{link.label}</p>}
{!sidebarCollapsed && link.key === "active-cycles" && (
<Crown className="size-3.5 text-amber-400" />
)}
</div>
</Tooltip>
</Link>
)
)}
</Disclosure.Panel>
)}
</Transition>
</Disclosure>
);
});

View File

@@ -251,7 +251,49 @@ export const CREATED_ISSUES_EMPTY_STATES = {
},
};
export const SIDEBAR_MENU_ITEMS: {
export const SIDEBAR_WORKSPACE_MENU_ITEMS: {
key: string;
label: string;
href: string;
access: EUserWorkspaceRoles;
highlight: (pathname: string, baseUrl: string) => boolean;
Icon: React.FC<Props>;
}[] = [
{
key: "all-issues",
label: "All Issues",
href: `/workspace-views/all-issues`,
access: EUserWorkspaceRoles.GUEST,
highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/workspace-views/`),
Icon: CheckCircle,
},
{
key: "projects",
label: "Projects",
href: `/projects`,
access: EUserWorkspaceRoles.GUEST,
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/projects/`,
Icon: Briefcase,
},
{
key: "active-cycles",
label: "Active Cycles",
href: `/active-cycles`,
access: EUserWorkspaceRoles.GUEST,
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/active-cycles/`,
Icon: ContrastIcon,
},
{
key: "analytics",
label: "Analytics",
href: `/analytics`,
access: EUserWorkspaceRoles.MEMBER,
highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/analytics/`),
Icon: BarChart2,
},
];
export const SIDEBAR_USER_MENU_ITEMS: {
key: string;
label: string;
href: string;
@@ -267,36 +309,4 @@ export const SIDEBAR_MENU_ITEMS: {
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/`,
Icon: Home,
},
{
key: "analytics",
label: "Analytics",
href: `/analytics`,
access: EUserWorkspaceRoles.MEMBER,
highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/analytics/`),
Icon: BarChart2,
},
{
key: "projects",
label: "Projects",
href: `/projects`,
access: EUserWorkspaceRoles.GUEST,
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/projects/`,
Icon: Briefcase,
},
{
key: "all-issues",
label: "All Issues",
href: `/workspace-views/all-issues`,
access: EUserWorkspaceRoles.GUEST,
highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/workspace-views/`),
Icon: CheckCircle,
},
{
key: "active-cycles",
label: "Active Cycles",
href: `/active-cycles`,
access: EUserWorkspaceRoles.GUEST,
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/active-cycles/`,
Icon: ContrastIcon,
},
];