diff options
Diffstat (limited to 'components/client-data-table/data-table-sort-list.tsx')
| -rw-r--r-- | components/client-data-table/data-table-sort-list.tsx | 272 |
1 files changed, 272 insertions, 0 deletions
diff --git a/components/client-data-table/data-table-sort-list.tsx b/components/client-data-table/data-table-sort-list.tsx new file mode 100644 index 00000000..b67fdde3 --- /dev/null +++ b/components/client-data-table/data-table-sort-list.tsx @@ -0,0 +1,272 @@ +"use client" + +import * as React from "react" +import { type SortingState, type Table } from "@tanstack/react-table" +import { + ArrowDownUp, + ChevronsUpDown, + GripVertical, + Trash2, +} from "lucide-react" + +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command" +import { + Sortable, + SortableItem, + SortableDragHandle, +} from "@/components/ui/sortable" +import { cn, toSentenceCase } from "@/lib/utils" + + +/** + * A simpler, local-state version of the column "sort list". + * - No `useQueryState` or URL sync + * - We store a local `sorting: SortingState` and whenever it changes, we do `table.setSorting(sorting)`. + */ +interface DataTableSortListLocalProps<TData> { + /** TanStack Table instance */ + table: Table<TData> +} + +export function ClientDataTableSortList<TData>({ table }: DataTableSortListLocalProps<TData>) { + + + // 2) local SortingState + const [sorting, setSorting] = React.useState<SortingState>([]) + + // 3) Keep the table in sync + React.useEffect(() => { + table.setSorting(sorting) + }, [sorting, table]) + + // 4) columns that can be sorted + const sortableColumns = React.useMemo(() => { + return table + .getAllColumns() + .filter((col) => col.getCanSort()) + .map((col) => ({ + id: col.id, + // excelHeader 를 사용 중이면 label이 없을 수 있으니 fallback + label: (col.columnDef.meta?.excelHeader as string) || toSentenceCase(col.id), + })) + }, [table]) + + // 5) "Add sort" → pick first unsorted column + function addSort() { + const used = new Set(sorting.map((s) => s.id)) + const firstUnused = sortableColumns.find((col) => !used.has(col.id)) + if (!firstUnused) return + setSorting((prev) => [...prev, { id: firstUnused.id, desc: false }]) + } + + // 6) update sort item by column id + function updateSort( + columnId: string, + patch: Partial<{ id: string; desc: boolean }> + ) { + setSorting((prev) => + prev.map((s) => (s.id === columnId ? { ...s, ...patch } : s)) + ) + } + + // 7) remove a sort item + function removeSort(columnId: string) { + setSorting((prev) => prev.filter((s) => s.id !== columnId)) + } + + // 8) reorder sorting items via drag + function moveSort(activeIndex: number, overIndex: number) { + setSorting((prev) => { + const arr = [...prev] + const [removed] = arr.splice(activeIndex, 1) + if (!removed) return prev + arr.splice(overIndex, 0, removed) + return arr + }) + } + + const isSortingEmpty = sorting.length === 0 + + return ( + <Sortable + value={sorting} + onValueChange={setSorting} + onMove={({ activeIndex, overIndex }) => moveSort(activeIndex, overIndex)} + overlay={ + <div className="flex items-center gap-2"> + <div className="h-8 w-[11.25rem] rounded-sm bg-primary/10" /> + <div className="h-8 w-24 rounded-sm bg-primary/10" /> + <div className="size-8 shrink-0 rounded-sm bg-primary/10" /> + <div className="size-8 shrink-0 rounded-sm bg-primary/10" /> + </div> + } + > + <Popover> + <PopoverTrigger asChild> + <Button variant="outline" size="sm" className="gap-2"> + <ArrowDownUp className="h-4 w-4" aria-hidden="true" /> + { "Sort"} + {sorting.length > 0 && ( + <Badge + variant="secondary" + className="h-[1.14rem] rounded-[0.2rem] px-[0.32rem] font-mono text-[0.65rem] font-normal" + > + {sorting.length} + </Badge> + )} + </Button> + </PopoverTrigger> + + <PopoverContent + align="start" + collisionPadding={16} + className={cn( + "flex w-[calc(100vw-theme(spacing.20))] min-w-72 max-w-[25rem] origin-[var(--radix-popover-content-transform-origin)] flex-col p-4 sm:w-[25rem]", + sorting.length > 0 ? "gap-3.5" : "gap-2" + )} + > + {isSortingEmpty ? ( + <div className="flex flex-col gap-1"> + <h4 className="font-medium leading-none"> + { "No sorting applied"} + </h4> + <p className="text-sm text-muted-foreground"> + { "Add sorting to organize your results."} + </p> + </div> + ) : ( + <h4 className="font-medium leading-none"> + { "Sort by"} + </h4> + )} + + {/* Sorting items */} + {sorting.length > 0 && ( + <div className="flex max-h-40 flex-col gap-2 overflow-y-auto p-0.5"> + {sorting.map((sortItem) => { + const col = sortableColumns.find((c) => c.id === sortItem.id) + const columnLabel = col ? col.label : toSentenceCase(sortItem.id) + + return ( + <SortableItem key={sortItem.id} value={sortItem.id} asChild> + <div className="flex items-center gap-2"> + {/* Column name selector */} + <Popover modal> + <PopoverTrigger asChild> + <Button + variant="outline" + size="sm" + className="h-8 w-[11.25rem] justify-between" + > + <span className="truncate">{columnLabel}</span> + <ChevronsUpDown className="h-4 w-4 opacity-50" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0"> + <Command> + <CommandInput placeholder={"Search columns..." } /> + <CommandList> + <CommandEmpty> + { "No columns found."} + </CommandEmpty> + <CommandGroup> + {sortableColumns.map((col) => ( + <CommandItem + key={col.id} + value={col.id} + onSelect={(val) => { + // change the ID of the sort item + updateSort(sortItem.id, { id: val, desc: false }) + }} + > + {col.label} + </CommandItem> + ))} + </CommandGroup> + </CommandList> + </Command> + </PopoverContent> + </Popover> + + {/* Sort direction */} + <Select + value={sortItem.desc ? "desc" : "asc"} + onValueChange={(val) => + updateSort(sortItem.id, { desc: val === "desc" }) + } + > + <SelectTrigger className="h-8 w-24"> + <SelectValue /> + </SelectTrigger> + <SelectContent className="min-w-[var(--radix-select-trigger-width)]"> + <SelectItem value="asc"> + {"Asc" } + </SelectItem> + <SelectItem value="desc"> + { "Desc"} + </SelectItem> + </SelectContent> + </Select> + + {/* remove sort */} + <Button + variant="outline" + size="icon" + onClick={() => removeSort(sortItem.id)} + className="size-8 shrink-0 rounded" + > + <Trash2 className="size-3.5" aria-hidden="true" /> + </Button> + + {/* drag handle */} + <SortableDragHandle + variant="outline" + size="icon" + className="size-8 shrink-0 rounded" + > + <GripVertical className="size-3.5" aria-hidden="true" /> + </SortableDragHandle> + </div> + </SortableItem> + ) + })} + </div> + )} + + {/* Footer: "Add sort" & "Reset" */} + <div className="flex items-center gap-2"> + <Button size="sm" onClick={addSort}> + {"Add sort" } + </Button> + {sorting.length > 0 && ( + <Button size="sm" variant="outline" onClick={() => setSorting([])}> + {"Reset sorting" } + </Button> + )} + </div> + </PopoverContent> + </Popover> + </Sortable> + ) +}
\ No newline at end of file |
