1"use client";
2
3
7import { useMemo } from "react";
8import { Inbox, MessageSquare, Plus } from "lucide-react";
9import { cn } from "@/lib/utils";
10import {
11 DatabaseProperty,
12 DatabaseView,
13 RecordDto,
14 UseDatabaseRecordsResult,
15 useCreateRecord,
16 valueToText
17} from "@/hooks/use-database";
18import { cellToText, formatDateValue } from "@/components/database/cell-renderer";
19
20export interface ListProps {
21 databaseId: string;
22 view: DatabaseView;
23 properties: DatabaseProperty[];
24 records: UseDatabaseRecordsResult;
25 onOpenRecord: (record: RecordDto) => void;
26 readOnly?: boolean;
27}
28
29export function ListView({ databaseId, view, properties, records, onOpenRecord, readOnly }: ListProps) {
30 const { data, isLoading } = records;
31 const createRecord = useCreateRecord(databaseId);
32 const titleProp = properties.find((p) => p.isTitle);
33
34 const inline = useMemo(() => {
35 const subs = view.config.subtitlePropertyId
36 ? properties.filter((p) => p.id === view.config.subtitlePropertyId)
37 : [];
38 const rest = properties.filter(
39 (p) => p !== titleProp && p.type !== "files" && p.type !== "relation" && !subs.includes(p)
40 );
41 return [...subs, ...rest].slice(0, 2);
42 }, [properties, titleProp, view.config.subtitlePropertyId]);
43
44 const dateProp = properties.find((p) => p.type === "date");
45
46 const addRow = () => {
47 createRecord.mutate({ properties: titleProp ? { [titleProp.id]: { text: "New item", marks: [] } } : {} });
48 };
49
50 if (isLoading) {
51 return (
52 <div className="space-y-1.5 p-4">
53 {Array.from({ length: 8 }).map((_, i) => (
54 <div key={i} className="h-9 animate-pulse rounded bg-muted" />
55 ))}
56 </div>
57 );
58 }
59
60 return (
61 <div className="h-full overflow-y-auto">
62 {data.records.length === 0 ? (
63 <div className="flex flex-col items-center gap-3 py-16 text-center">
64 <Inbox className="size-8 text-muted-foreground" aria-hidden />
65 <p className="text-sm text-muted-foreground">No items in this list.</p>
66 {!readOnly && (
67 <button type="button" className="inline-flex items-center gap-1 rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:bg-primary/90" onClick={addRow}>
68 <Plus className="size-4" /> Add item
69 </button>
70 )}
71 </div>
72 ) : (
73 <ul className="divide-y divide-border/60">
74 {data.records.map((r) => (
75 <li key={r.id}>
76 <button
77 type="button"
78 className={cn("flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm hover:bg-muted/50")}
79 onClick={() => onOpenRecord(r)}
80 >
81 <span className="w-5 text-center text-base" aria-hidden>
82 {r.icon ?? "•"}
83 </span>
84 <span className="min-w-0 flex-1 truncate font-medium">
85 {titleProp ? valueToText(r.properties[titleProp.id], titleProp) || "Untitled" : r.id.slice(-6)}
86 </span>
87 <span className="hidden max-w-md flex-1 truncate text-xs text-muted-foreground sm:inline">
88 {inline
89 .map((p) => {
90 const t = cellToText(r.properties[p.id], p);
91 return t ? `${p.name}: ${t}` : null;
92 })
93 .filter(Boolean)
94 .join(" · ")}
95 </span>
96 {r.subItemCount ? (
97 <span className="inline-flex items-center gap-1 text-xs text-muted-foreground" title="Sub-items">
98 <Inbox className="size-3" aria-hidden /> {r.subItemCount}
99 </span>
100 ) : null}
101 {dateProp && (
102 <span className="hidden w-24 text-right text-xs text-muted-foreground md:inline">
103 {formatDateValue(r.properties[dateProp.id]?.start)}
104 </span>
105 )}
106 </button>
107 </li>
108 ))}
109 </ul>
110 )}
111 </div>
112 );
113}
114