"use client"; import * as React from "react"; import { ArrowDown, ArrowUp, ArrowUpDown, ChevronDown, ChevronRight, ListTodo, } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; import { TooltipProvider, } from "@/components/ui/tooltip"; import type { ViewConfig, ViewObject, ViewSort } from "@/lib/hooks/use-view-data"; import { useViewData } from "@/lib/hooks/use-view-data"; import { cn } from "@/lib/utils"; import { getDueDateValue, getPriorityValue, ListItem, LIST_ROW_GRID, } from "./list-item"; type SortField = "status" | "title" | "assignees" | "priority" | "dueDate"; function groupByField( objects: ViewObject[], groupBy: string, ): Record { const groups: Record = {}; for (const obj of objects) { const key = String( (obj as unknown as Record)[groupBy] ?? "No Value", ); if (!groups[key]) groups[key] = []; groups[key].push(obj); } return groups; } function rankPriority(p: string): number { const order = ["High", "Medium", "Low"]; const i = order.indexOf(p); return i === -1 ? 999 : i; } function firstAssigneeName(o: ViewObject): string { return o.assignees?.[0]?.user?.name ?? ""; } function compareDue(a: string, b: string): number { if (a === "—" && b === "—") return 0; if (a === "—") return 1; if (b === "—") return -1; const ta = new Date(a).getTime(); const tb = new Date(b).getTime(); if (Number.isNaN(ta) || Number.isNaN(tb)) return a.localeCompare(b); return ta - tb; } function sortItemsByField(items: ViewObject[], sort: ViewSort): ViewObject[] { const dir = sort.direction === "asc" ? 1 : -1; const copy = [...items]; copy.sort((a, b) => { let cmp = 0; switch (sort.field) { case "priority": { const pa = getPriorityValue(a); const pb = getPriorityValue(b); cmp = rankPriority(pa) - rankPriority(pb); if (cmp === 0) cmp = pa.localeCompare(pb); break; } case "dueDate": cmp = compareDue(getDueDateValue(a), getDueDateValue(b)); break; case "assignees": cmp = firstAssigneeName(a).localeCompare(firstAssigneeName(b)); break; case "title": cmp = a.title.localeCompare(b.title); break; case "status": cmp = String(a.status ?? "").localeCompare(String(b.status ?? "")); break; default: cmp = 0; } return cmp * dir; }); return copy; } function refineSort(items: ViewObject[], sorts: ViewSort[]): ViewObject[] { if (sorts.length === 0) return items; const s = sorts[0]; if (s.field === "title" || s.field === "status") { return items; } return sortItemsByField(items, s); } function nextSort(field: SortField, sorts: ViewSort[]): ViewSort[] { const first = sorts[0]; if (first?.field === field) { return [{ field, direction: first.direction === "asc" ? "desc" : "asc" }]; } return [{ field, direction: "asc" }]; } function SortHeaderButton({ label, field, sorts, onSort, }: { label: string; field: SortField; sorts: ViewSort[]; onSort: (field: SortField) => void; }) { const active = sorts[0]?.field === field; const dir = active ? sorts[0].direction : null; return ( ); } function ListSkeleton() { return (
{Array.from({ length: 8 }).map((_, i) => (
))}
); } function EmptyState() { return (

No items yet

Create a task or adjust filters to see work here.

); } export interface ListViewProps { config: ViewConfig; } export function ListView({ config }: ListViewProps) { const [sorts, setSorts] = React.useState(config.sorts); React.useEffect(() => { setSorts(config.sorts); }, [config.sorts]); const effectiveConfig = React.useMemo( () => ({ ...config, sorts }), [config, sorts], ); const { items, isLoading, total } = useViewData(effectiveConfig); const displayItems = React.useMemo( () => refineSort(items, sorts), [items, sorts], ); const displayGrouped = React.useMemo(() => { if (!config.groupBy) return null; return groupByField(displayItems, config.groupBy); }, [displayItems, config.groupBy]); const [selected, setSelected] = React.useState>(new Set()); const toggleSelect = React.useCallback((id: string) => { setSelected((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }, []); const [collapsed, setCollapsed] = React.useState>({}); const toggleGroup = React.useCallback((key: string) => { setCollapsed((c) => ({ ...c, [key]: !c[key] })); }, []); const onHeaderSort = React.useCallback((field: SortField) => { setSorts((s) => nextSort(field, s)); }, []); const isGrouped = Boolean(config.groupBy); const headerRow = (
); let body: React.ReactNode = null; if (isLoading) { body = ; } else if (total === 0) { body = ; } else if (isGrouped && displayGrouped) { const keys = Object.keys(displayGrouped).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }), ); body = (
{keys.map((groupKey, gi) => { const groupItems = displayGrouped[groupKey] ?? []; const expanded = !collapsed[groupKey]; return (
{expanded && (
{groupItems.map((obj, i) => ( ))}
)}
); })}
); } else { body = (
{displayItems.map((obj, i) => ( ))}
); } return (
{headerRow}
{body}
); }