import { useCallback, useMemo } from "react"; import { Paperclip, Plus } from "lucide-react"; import { useQueries, useQuery } from "@tanstack/react-query"; import { DndContext, closestCenter, MouseSensor, useSensor, useSensors, type DragEndEvent, } from "@dnd-kit/core"; import { SortableContext, useSortable, verticalListSortingStrategy, arrayMove, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { useCompany } from "../context/CompanyContext"; import { useDialogActions } from "../context/DialogContext"; import { cn } from "../lib/utils"; import { queryKeys } from "../lib/queryKeys"; import { sidebarBadgesApi } from "../api/sidebarBadges"; import { heartbeatsApi } from "../api/heartbeats"; import { authApi } from "../api/auth"; import { useCompanyOrder } from "../hooks/useCompanyOrder"; import { useLocation, useNavigate } from "@/lib/router"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import type { Company } from "@paperclipai/shared"; import { CompanyPatternIcon } from "./CompanyPatternIcon"; function SortableCompanyItem({ company, isSelected, hasLiveAgents, hasUnreadInbox, onSelect, }: { company: Company; isSelected: boolean; hasLiveAgents: boolean; hasUnreadInbox: boolean; onSelect: () => void; }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: company.id }); const style = { transform: CSS.Transform.toString(transform), transition, zIndex: isDragging ? 10 : undefined, opacity: isDragging ? 0.8 : 1, }; return (
{ if (isDragging) { e.preventDefault(); return; } e.preventDefault(); onSelect(); }} className="relative flex items-center justify-center group overflow-visible" > {/* Selection indicator pill */}
{hasLiveAgents && ( )} {hasUnreadInbox && ( )}

{company.name}

); } export function CompanyRail() { const { companies, selectedCompanyId, setSelectedCompanyId } = useCompany(); const { openOnboarding } = useDialogActions(); const navigate = useNavigate(); const location = useLocation(); const isInstanceRoute = location.pathname.startsWith("/instance/"); const highlightedCompanyId = isInstanceRoute ? null : selectedCompanyId; const sidebarCompanies = useMemo( () => companies.filter((company) => company.status !== "archived"), [companies], ); const { data: session } = useQuery({ queryKey: queryKeys.auth.session, queryFn: () => authApi.getSession(), }); const currentUserId = session?.user?.id ?? session?.session?.userId ?? null; const companyIds = useMemo(() => sidebarCompanies.map((company) => company.id), [sidebarCompanies]); const liveRunsQueries = useQueries({ queries: companyIds.map((companyId) => ({ queryKey: queryKeys.liveRuns(companyId), queryFn: () => heartbeatsApi.liveRunsForCompany(companyId), refetchInterval: 10_000, })), }); const sidebarBadgeQueries = useQueries({ queries: companyIds.map((companyId) => ({ queryKey: queryKeys.sidebarBadges(companyId), queryFn: () => sidebarBadgesApi.get(companyId), refetchInterval: 15_000, })), }); const hasLiveAgentsByCompanyId = useMemo(() => { const result = new Map(); companyIds.forEach((companyId, index) => { result.set(companyId, (liveRunsQueries[index]?.data?.length ?? 0) > 0); }); return result; }, [companyIds, liveRunsQueries]); const hasUnreadInboxByCompanyId = useMemo(() => { const result = new Map(); companyIds.forEach((companyId, index) => { result.set(companyId, (sidebarBadgeQueries[index]?.data?.inbox ?? 0) > 0); }); return result; }, [companyIds, sidebarBadgeQueries]); const { orderedCompanies, persistOrder } = useCompanyOrder({ companies: sidebarCompanies, userId: currentUserId, }); // Require 8px of movement before starting a drag to avoid interfering with clicks const sensors = useSensors( // Keep sidebar reordering mouse-only so touch input can scroll/tap without drag affordances. useSensor(MouseSensor, { activationConstraint: { distance: 8 }, }) ); const handleDragEnd = useCallback( (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) return; const ids = orderedCompanies.map((c) => c.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)); }, [orderedCompanies, persistOrder] ); return (
{/* Paperclip icon - aligned with top sections (implied line, no visible border) */}
{/* Company list */}
c.id)} strategy={verticalListSortingStrategy} > {orderedCompanies.map((company) => ( { setSelectedCompanyId(company.id); if (isInstanceRoute) { navigate(`/${company.issuePrefix}/dashboard`); } }} /> ))}
{/* Separator before add button */}
{/* Add company button */}

Add company

); }