import { useEffect, useRef, useState } from "react"; import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; import { pointerOutsideOfPreview } from "@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview"; import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview"; import { attachInstruction, extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item"; import { observer } from "mobx-react"; import Link from "next/link"; import { useRouter } from "next/router"; import { createRoot } from "react-dom/client"; import { MoreVertical, PenSquare, LinkIcon, Star, FileText, Settings, Share2, LogOut, ChevronDown, MoreHorizontal, Inbox, } from "lucide-react"; import { Disclosure, Transition } from "@headlessui/react"; // icons // ui import { CustomMenu, Tooltip, ArchiveIcon, PhotoFilterIcon, DiceIcon, ContrastIcon, LayersIcon, setPromiseToast, DropIndicator, } from "@plane/ui"; import { LeaveProjectModal, ProjectLogo, PublishProjectModal } from "@/components/project"; import { EUserProjectRoles } from "@/constants/project"; import { cn } from "@/helpers/common.helper"; // hooks 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 type Props = { projectId: string; handleCopyText: () => void; handleOnProjectDrop?: ( sourceId: string | undefined, destinationId: string | undefined, shouldDropAtEnd: boolean ) => void; projectListType: "JOINED" | "FAVORITES"; disableDrag?: boolean; disableDrop?: boolean; isLastChild: boolean; }; const navigation = (workspaceSlug: string, projectId: string) => [ { name: "Issues", href: `/${workspaceSlug}/projects/${projectId}/issues`, Icon: LayersIcon, }, { name: "Cycles", href: `/${workspaceSlug}/projects/${projectId}/cycles`, Icon: ContrastIcon, }, { name: "Modules", href: `/${workspaceSlug}/projects/${projectId}/modules`, Icon: DiceIcon, }, { name: "Views", href: `/${workspaceSlug}/projects/${projectId}/views`, Icon: PhotoFilterIcon, }, { name: "Pages", href: `/${workspaceSlug}/projects/${projectId}/pages`, Icon: FileText, }, { name: "Inbox", href: `/${workspaceSlug}/projects/${projectId}/inbox`, Icon: Inbox, }, { name: "Settings", href: `/${workspaceSlug}/projects/${projectId}/settings`, Icon: Settings, }, ]; export const ProjectSidebarListItem: React.FC = observer((props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { projectId, handleCopyText, disableDrag, disableDrop, isLastChild, handleOnProjectDrop, projectListType } = props; // store hooks const { sidebarCollapsed: isCollapsed, toggleSidebar } = useAppTheme(); const { setTrackElement } = useEventTracker(); const { addProjectToFavorites, removeProjectFromFavorites, getProjectById } = useProject(); const { isMobile } = usePlatformOS(); // states const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false); const [publishModalOpen, setPublishModal] = useState(false); const [isMenuActive, setIsMenuActive] = useState(false); const [isDragging, setIsDragging] = useState(false); const [instruction, setInstruction] = useState<"DRAG_OVER" | "DRAG_BELOW" | undefined>(undefined); // refs const actionSectionRef = useRef(null); const projectRef = useRef(null); const dragHandleRef = useRef(null); // router const router = useRouter(); const { workspaceSlug, projectId: URLProjectId } = router.query; // derived values const project = getProjectById(projectId); // auth const isAdmin = project?.member_role === EUserProjectRoles.ADMIN; const isViewerOrGuest = project?.member_role && [EUserProjectRoles.VIEWER, EUserProjectRoles.GUEST].includes(project.member_role); const handleAddToFavorites = () => { if (!workspaceSlug || !project) return; const addToFavoritePromise = addProjectToFavorites(workspaceSlug.toString(), project.id); setPromiseToast(addToFavoritePromise, { loading: "Adding project to favorites...", success: { title: "Success!", message: () => "Project added to favorites.", }, error: { title: "Error!", message: () => "Couldn't add the project to favorites. Please try again.", }, }); }; const handleRemoveFromFavorites = () => { if (!workspaceSlug || !project) return; const removeFromFavoritePromise = removeProjectFromFavorites(workspaceSlug.toString(), project.id); setPromiseToast(removeFromFavoritePromise, { loading: "Removing project from favorites...", success: { title: "Success!", message: () => "Project removed from favorites.", }, error: { title: "Error!", message: () => "Couldn't remove the project from favorites. Please try again.", }, }); }; const handleLeaveProject = () => { setTrackElement("APP_SIDEBAR_PROJECT_DROPDOWN"); setLeaveProjectModal(true); }; const handleLeaveProjectModalClose = () => { setLeaveProjectModal(false); }; const handleProjectClick = () => { if (window.innerWidth < 768) { toggleSidebar(); } }; useEffect(() => { const element = projectRef.current; const dragHandleElement = dragHandleRef.current; if (!element) return; return combine( draggable({ element, canDrag: () => !disableDrag && !isCollapsed, dragHandle: dragHandleElement ?? undefined, getInitialData: () => ({ id: projectId, dragInstanceId: "PROJECTS" }), onDragStart: () => { setIsDragging(true); }, onDrop: () => { setIsDragging(false); }, onGenerateDragPreview: ({ nativeSetDragImage }) => { // Add a custom drag image setCustomNativeDragPreview({ getOffset: pointerOutsideOfPreview({ x: "0px", y: "0px" }), render: ({ container }) => { const root = createRoot(container); root.render(
{project && }

{project?.name}

); return () => root.unmount(); }, nativeSetDragImage, }); }, }), dropTargetForElements({ element, canDrop: ({ source }) => !disableDrop && source?.data?.id !== projectId && source?.data?.dragInstanceId === "PROJECTS", getData: ({ input, element }) => { const data = { id: projectId }; // attach instruction for last in list return attachInstruction(data, { input, element, currentLevel: 0, indentPerLevel: 0, mode: isLastChild ? "last-in-group" : "standard", }); }, onDrag: ({ self }) => { const extractedInstruction = extractInstruction(self?.data)?.type; // check if the highlight is to be shown above or below setInstruction( extractedInstruction ? extractedInstruction === "reorder-below" && isLastChild ? "DRAG_BELOW" : "DRAG_OVER" : undefined ); }, onDragLeave: () => { setInstruction(undefined); }, onDrop: ({ self, source }) => { setInstruction(undefined); const extractedInstruction = extractInstruction(self?.data)?.type; const currentInstruction = extractedInstruction ? extractedInstruction === "reorder-below" && isLastChild ? "DRAG_BELOW" : "DRAG_OVER" : undefined; if (!currentInstruction) return; const sourceId = source?.data?.id as string | undefined; const destinationId = self?.data?.id as string | undefined; handleOnProjectDrop && handleOnProjectDrop(sourceId, destinationId, currentInstruction === "DRAG_BELOW"); highlightIssueOnDrop(`sidebar-${sourceId}-${projectListType}`); }, }) ); }, [projectRef?.current, dragHandleRef?.current, projectId, isLastChild, projectListType, handleOnProjectDrop]); useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false)); useOutsideClickDetector(projectRef, () => projectRef?.current?.classList?.remove(HIGHLIGHT_CLASS)); if (!project) return null; return ( <> setPublishModal(false)} /> {({ open }) => (
{!disableDrag && ( )}
{!isCollapsed &&

{project.name}

}
{!isCollapsed && (
{!isCollapsed && ( setIsMenuActive(!isMenuActive)} >
} className={cn("hidden flex-shrink-0 group-hover:block", { "!block": isMenuActive, })} buttonClassName="!text-custom-sidebar-text-400" ellipsis placement="bottom-start" > {!project.is_favorite && ( Add to favorites )} {project.is_favorite && ( Remove from favorites )} {/* publish project settings */} {isAdmin && ( setPublishModal(true)}>
{project.is_deployed ? "Publish settings" : "Publish"}
)}
Draft issues
Copy link {!isViewerOrGuest && (
Archives
)}
Settings
{/* leave project */} {isViewerOrGuest && (
Leave project
)} )}
{navigation(workspaceSlug as string, project?.id).map((item) => { if ( (item.name === "Cycles" && !project.cycle_view) || (item.name === "Modules" && !project.module_view) || (item.name === "Views" && !project.issue_views_view) || (item.name === "Pages" && !project.page_view) || (item.name === "Inbox" && !project.inbox_view) ) return; return (
{!isCollapsed && item.name}
); })}
{isLastChild && } )}
); });