summaryrefslogtreecommitdiff
path: root/components/client-data-table/data-table-sort-list.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/client-data-table/data-table-sort-list.tsx')
-rw-r--r--components/client-data-table/data-table-sort-list.tsx272
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