mirror of
https://github.com/makeplane/plane.git
synced 2026-01-26 08:09:47 -06:00
[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:
committed by
GitHub
parent
3eda3845fa
commit
535a27309e
@@ -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>
|
||||
);
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const AppSwitcher = () => null;
|
||||
export const SidebarAppSwitcher = () => null;
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
}
|
||||
7
web/core/components/workspace/sidebar/index.ts
Normal file
7
web/core/components/workspace/sidebar/index.ts
Normal 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";
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
@@ -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>
|
||||
)}
|
||||
151
web/core/components/workspace/sidebar/quick-actions.tsx
Normal file
151
web/core/components/workspace/sidebar/quick-actions.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
)
|
||||
)}
|
||||
141
web/core/components/workspace/sidebar/workspace-menu.tsx
Normal file
141
web/core/components/workspace/sidebar/workspace-menu.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user