import { useCallback, useMemo, useState } from "react"; import { NavLink, useLocation } from "@/lib/router"; import { useQuery } from "@tanstack/react-query"; import { ChevronRight, Plus } from "lucide-react"; import { DndContext, PointerSensor, closestCenter, type DragEndEvent, useSensor, useSensors, } from "@dnd-kit/core"; import { SortableContext, arrayMove, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { useCompany } from "../context/CompanyContext"; import { useDialog } from "../context/DialogContext"; import { useSidebar } from "../context/SidebarContext"; import { authApi } from "../api/auth"; import { projectsApi } from "../api/projects"; import { queryKeys } from "../lib/queryKeys"; import { cn, projectRouteRef } from "../lib/utils"; import { useProjectOrder } from "../hooks/useProjectOrder"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; import { PluginSlotMount, usePluginSlots } from "@/plugins/slots"; import type { Project } from "@paperclipai/shared"; type ProjectSidebarSlot = ReturnType["slots"][number]; function SortableProjectItem({ activeProjectRef, companyId, companyPrefix, isMobile, project, projectSidebarSlots, setSidebarOpen, }: { activeProjectRef: string | null; companyId: string | null; companyPrefix: string | null; isMobile: boolean; project: Project; projectSidebarSlots: ProjectSidebarSlot[]; setSidebarOpen: (open: boolean) => void; }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: project.id }); const routeRef = projectRouteRef(project); return (
{ if (isMobile) setSidebarOpen(false); }} className={cn( "flex items-center gap-2.5 px-3 py-1.5 text-[13px] font-medium transition-colors", activeProjectRef === routeRef || activeProjectRef === project.id ? "bg-accent text-foreground" : "text-foreground/80 hover:bg-accent/50 hover:text-foreground", )} > {project.name} {projectSidebarSlots.length > 0 && (
{projectSidebarSlots.map((slot) => ( ))}
)}
); } export function SidebarProjects() { const [open, setOpen] = useState(true); const { selectedCompany, selectedCompanyId } = useCompany(); const { openNewProject } = useDialog(); const { isMobile, setSidebarOpen } = useSidebar(); const location = useLocation(); const { data: projects } = useQuery({ queryKey: queryKeys.projects.list(selectedCompanyId!), queryFn: () => projectsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: session } = useQuery({ queryKey: queryKeys.auth.session, queryFn: () => authApi.getSession(), }); const { slots: projectSidebarSlots } = usePluginSlots({ slotTypes: ["projectSidebarItem"], entityType: "project", companyId: selectedCompanyId, enabled: !!selectedCompanyId, }); const currentUserId = session?.user?.id ?? session?.session?.userId ?? null; const visibleProjects = useMemo( () => (projects ?? []).filter((project: Project) => !project.archivedAt), [projects], ); const { orderedProjects, persistOrder } = useProjectOrder({ projects: visibleProjects, companyId: selectedCompanyId, userId: currentUserId, }); const projectMatch = location.pathname.match(/^\/(?:[^/]+\/)?projects\/([^/]+)/); const activeProjectRef = projectMatch?.[1] ?? null; const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8 }, }), ); const handleDragEnd = useCallback( (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) return; const ids = orderedProjects.map((project) => project.id); const oldIndex = ids.indexOf(active.id as string); const newIndex = ids.indexOf(over.id as string); if (oldIndex === -1 || newIndex === -1) return; persistOrder(arrayMove(ids, oldIndex, newIndex)); }, [orderedProjects, persistOrder], ); return (
Projects
project.id)} strategy={verticalListSortingStrategy} >
{orderedProjects.map((project: Project) => ( ))}
); }