1"use client";
2
3
9import { Plus, X } from "lucide-react";
10import { cn } from "@/lib/utils";
11import { DatabaseProperty, GroupSpec } from "@/hooks/use-database";
12
13const inputCls =
14 "rounded border border-input bg-background px-1.5 py-0.5 text-xs outline-none focus-visible:ring-1 focus-visible:ring-ring";
15
16
17export const GROUPABLE_TYPES = new Set(["select", "status", "multi_select", "person", "date"]);
18
19export function isGroupable(p: DatabaseProperty): boolean {
20 return GROUPABLE_TYPES.has(p.type);
21}
22
23export interface GroupBuilderProps {
24 value: GroupSpec[] | null;
25 onChange: (group: GroupSpec[] | null) => void;
26 properties: DatabaseProperty[];
27
28 viewType?: string;
29 className?: string;
30}
31
32export function GroupBuilder({ value, onChange, properties, viewType, className }: GroupBuilderProps) {
33 const groups = value ?? [];
34 const used = new Set(groups.map((g) => g.propertyId));
35 const allCandidates = viewType === "board" ? properties.filter(isGroupable) : properties;
36 const available = allCandidates.filter((p) => !used.has(p.id));
37
38 if (allCandidates.length === 0) {
39 return (
40 <p className="px-1 py-2 text-xs text-muted-foreground">
41 {viewType === "board"
42 ? "Group by a select, status, multi-select, person or date property (add one first)."
43 : "Add properties before grouping."}
44 </p>
45 );
46 }
47
48 return (
49 <div className={className}>
50 {groups.map((g, i) => (
51 <div key={i} className="flex items-center gap-1.5 py-1">
52 <span className="text-xs text-muted-foreground">Level {i + 1}:</span>
53 <select
54 className={cn(inputCls, "w-36")}
55 value={g.propertyId}
56 onChange={(e) => {
57 const next = groups.map((x, j) => (j === i ? { ...x, propertyId: e.target.value } : x));
58 onChange(next);
59 }}
60 aria-label={`Group property level ${i + 1}`}
61 >
62 {allCandidates.map((p) => (
63 <option key={p.id} value={p.id}>
64 {p.name}
65 </option>
66 ))}
67 </select>
68 <select
69 className={cn(inputCls, "w-24")}
70 value={g.direction}
71 onChange={(e) => {
72 const next = groups.map((x, j) => (j === i ? { ...x, direction: e.target.value as "asc" | "desc" } : x));
73 onChange(next);
74 }}
75 aria-label={`Group direction level ${i + 1}`}
76 >
77 <option value="asc">Ascending</option>
78 <option value="desc">Descending</option>
79 </select>
80 <button
81 type="button"
82 className="rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-destructive"
83 onClick={() => onChange(groups.filter((_, j) => j !== i))}
84 aria-label="Remove group level"
85 >
86 <X className="size-3.5" />
87 </button>
88 </div>
89 ))}
90 {groups.length === 0 && <p className="py-1 text-xs text-muted-foreground">No grouping — records render in view order.</p>}
91 <div className="flex items-center gap-2 pt-1">
92 <button
93 type="button"
94 disabled={groups.length >= 3 || available.length === 0}
95 className="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs text-primary hover:bg-muted disabled:opacity-40"
96 onClick={() => {
97 const p = available[0];
98 onChange([...groups, { propertyId: p.id, direction: "asc" }]);
99 }}
100 >
101 <Plus className="size-3" /> Add group level
102 </button>
103 {groups.length > 0 && (
104 <button type="button" className="rounded px-1.5 py-0.5 text-xs text-muted-foreground hover:bg-muted hover:text-destructive" onClick={() => onChange(null)}>
105 Clear
106 </button>
107 )}
108 </div>
109 </div>
110 );
111}
112