import React, { useState, FC } from "react"; import { useRouter } from "next/router"; import { mutate } from "swr"; // react-beautiful-dnd import { DragDropContext, Draggable, DropResult, Droppable } from "react-beautiful-dnd"; // headless ui import { Disclosure } from "@headlessui/react"; // hooks import useToast from "hooks/use-toast"; import useTheme from "hooks/use-theme"; import useUserAuth from "hooks/use-user-auth"; import useProjects from "hooks/use-projects"; // components import { DeleteProjectModal, SingleSidebarProject } from "components/project"; // services import projectService from "services/project.service"; // icons import { Icon } from "components/ui"; import { PlusIcon } from "@heroicons/react/24/outline"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; import { orderArrayBy } from "helpers/array.helper"; // types import { IProject } from "types"; // fetch-keys import { PROJECTS_LIST } from "constants/fetch-keys"; export const ProjectSidebarList: FC = () => { const [deleteProjectModal, setDeleteProjectModal] = useState(false); const [projectToDelete, setProjectToDelete] = useState<IProject | null>(null); // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; const { user } = useUserAuth(); const { collapsed: sidebarCollapse } = useTheme(); const { setToastAlert } = useToast(); const { projects: allProjects } = useProjects(); const joinedProjects = allProjects?.filter((p) => p.sort_order); const favoriteProjects = allProjects?.filter((p) => p.is_favorite); const otherProjects = allProjects?.filter((p) => p.sort_order === null); const orderedJoinedProjects: IProject[] | undefined = joinedProjects ? orderArrayBy(joinedProjects, "sort_order", "ascending") : undefined; const orderedFavProjects: IProject[] | undefined = favoriteProjects ? orderArrayBy(favoriteProjects, "sort_order", "ascending") : undefined; const handleDeleteProject = (project: IProject) => { setProjectToDelete(project); setDeleteProjectModal(true); }; const handleCopyText = (projectId: string) => { const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues`).then(() => { setToastAlert({ type: "success", title: "Link Copied!", message: "Project link copied to clipboard.", }); }); }; const onDragEnd = async (result: DropResult) => { const { source, destination, draggableId } = result; if (!destination || !workspaceSlug) return; if (source.index === destination.index) return; const projectsList = (destination.droppableId === "joined-projects" ? orderedJoinedProjects : orderedFavProjects) ?? []; let updatedSortOrder = projectsList[source.index].sort_order; if (destination.index === 0) updatedSortOrder = (projectsList[0].sort_order as number) - 1000; else if (destination.index === projectsList.length - 1) updatedSortOrder = (projectsList[projectsList.length - 1].sort_order as number) + 1000; else { const destinationSortingOrder = projectsList[destination.index].sort_order as number; const relativeDestinationSortingOrder = source.index < destination.index ? (projectsList[destination.index + 1].sort_order as number) : (projectsList[destination.index - 1].sort_order as number); updatedSortOrder = Math.round( (destinationSortingOrder + relativeDestinationSortingOrder) / 2 ); } mutate<IProject[]>( PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }), (prevData) => { if (!prevData) return prevData; return prevData.map((p) => p.id === draggableId ? { ...p, sort_order: updatedSortOrder } : p ); }, false ); await projectService .setProjectView(workspaceSlug as string, draggableId, { sort_order: updatedSortOrder }) .catch(() => { setToastAlert({ type: "error", title: "Error!", message: "Something went wrong. Please try again.", }); }); }; return ( <> <DeleteProjectModal isOpen={deleteProjectModal} onClose={() => setDeleteProjectModal(false)} data={projectToDelete} user={user} /> <div className="h-full overflow-y-auto px-5 space-y-3 pt-3 border-t border-custom-sidebar-border-300"> <DragDropContext onDragEnd={onDragEnd}> <Droppable droppableId="favorite-projects"> {(provided) => ( <div ref={provided.innerRef} {...provided.droppableProps}> {orderedFavProjects && orderedFavProjects.length > 0 && ( <Disclosure as="div" className="flex flex-col space-y-2" defaultOpen={true}> {({ open }) => ( <> {!sidebarCollapse && ( <Disclosure.Button as="button" type="button" className="group flex items-center gap-1 px-1.5 text-xs font-semibold text-custom-sidebar-text-200 text-left hover:bg-custom-sidebar-background-80 rounded w-min whitespace-nowrap" > Favorites <Icon iconName={open ? "arrow_drop_down" : "arrow_right"} className="group-hover:opacity-100 opacity-0 !text-lg" /> </Disclosure.Button> )} <Disclosure.Panel as="div" className="space-y-2"> {orderedFavProjects.map((project, index) => ( <Draggable key={project.id} draggableId={project.id} index={index} isDragDisabled={project.sort_order === null} > {(provided, snapshot) => ( <div ref={provided.innerRef} {...provided.draggableProps}> <SingleSidebarProject key={project.id} project={project} sidebarCollapse={sidebarCollapse} provided={provided} snapshot={snapshot} handleDeleteProject={() => handleDeleteProject(project)} handleCopyText={() => handleCopyText(project.id)} shortContextMenu /> </div> )} </Draggable> ))} </Disclosure.Panel> {provided.placeholder} </> )} </Disclosure> )} </div> )} </Droppable> </DragDropContext> <DragDropContext onDragEnd={onDragEnd}> <Droppable droppableId="joined-projects"> {(provided) => ( <div ref={provided.innerRef} {...provided.droppableProps}> {orderedJoinedProjects && orderedJoinedProjects.length > 0 && ( <Disclosure as="div" className="flex flex-col space-y-2" defaultOpen={true}> {({ open }) => ( <> {!sidebarCollapse && ( <Disclosure.Button as="button" type="button" className="group flex items-center gap-1 px-1.5 text-xs font-semibold text-custom-sidebar-text-200 text-left hover:bg-custom-sidebar-background-80 rounded w-min whitespace-nowrap" > Projects <Icon iconName={open ? "arrow_drop_down" : "arrow_right"} className="group-hover:opacity-100 opacity-0 !text-lg" /> </Disclosure.Button> )} <Disclosure.Panel as="div" className="space-y-2"> {orderedJoinedProjects.map((project, index) => ( <Draggable key={project.id} draggableId={project.id} index={index}> {(provided, snapshot) => ( <div ref={provided.innerRef} {...provided.draggableProps}> <SingleSidebarProject key={project.id} project={project} sidebarCollapse={sidebarCollapse} provided={provided} snapshot={snapshot} handleDeleteProject={() => handleDeleteProject(project)} handleCopyText={() => handleCopyText(project.id)} /> </div> )} </Draggable> ))} </Disclosure.Panel> {provided.placeholder} </> )} </Disclosure> )} </div> )} </Droppable> </DragDropContext> {otherProjects && otherProjects.length > 0 && ( <Disclosure as="div" className="flex flex-col space-y-2" defaultOpen={projectId && otherProjects.find((p) => p.id === projectId) ? true : false} > {({ open }) => ( <> {!sidebarCollapse && ( <Disclosure.Button as="button" type="button" className="group flex items-center gap-1 px-1.5 text-xs font-semibold text-custom-sidebar-text-200 text-left hover:bg-custom-sidebar-background-80 rounded w-min whitespace-nowrap" > Other Projects <Icon iconName={open ? "arrow_drop_down" : "arrow_right"} className="group-hover:opacity-100 opacity-0 !text-lg" /> </Disclosure.Button> )} <Disclosure.Panel as="div" className="space-y-2"> {otherProjects?.map((project, index) => ( <SingleSidebarProject key={project.id} project={project} sidebarCollapse={sidebarCollapse} handleDeleteProject={() => handleDeleteProject(project)} handleCopyText={() => handleCopyText(project.id)} shortContextMenu /> ))} </Disclosure.Panel> </> )} </Disclosure> )} {allProjects && allProjects.length === 0 && ( <button type="button" className="flex w-full items-center gap-2 px-3 py-2 text-sm text-custom-sidebar-text-200 mt-5" onClick={() => { const e = new KeyboardEvent("keydown", { key: "p", }); document.dispatchEvent(e); }} > <PlusIcon className="h-5 w-5" /> {!sidebarCollapse && "Add Project"} </button> )} </div> </> ); };