summaryrefslogtreecommitdiff
path: root/components/data-table/data-table-pin-left.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/data-table/data-table-pin-left.tsx')
-rw-r--r--components/data-table/data-table-pin-left.tsx243
1 files changed, 217 insertions, 26 deletions
diff --git a/components/data-table/data-table-pin-left.tsx b/components/data-table/data-table-pin-left.tsx
index 81e83564..e79e01eb 100644
--- a/components/data-table/data-table-pin-left.tsx
+++ b/components/data-table/data-table-pin-left.tsx
@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
-import { type Table } from "@tanstack/react-table"
+import { type Column, type Table } from "@tanstack/react-table"
import { Check, ChevronsUpDown, MoveLeft } from "lucide-react"
import { cn, toSentenceCase } from "@/lib/utils"
@@ -13,6 +13,7 @@ import {
CommandInput,
CommandItem,
CommandList,
+ CommandSeparator,
} from "@/components/ui/command"
import {
Popover,
@@ -21,14 +22,152 @@ import {
} from "@/components/ui/popover"
/**
- * “Pin Left” Popover. Lists columns that can be pinned.
- * If pinned===‘left’ → checked, if pinned!==‘left’ → unchecked.
- * Toggling check => pin(‘left’) or pin(false).
+ * Helper function to check if a column is a parent column (has subcolumns)
+ */
+function isParentColumn<TData>(column: Column<TData>): boolean {
+ return column.columns && column.columns.length > 0
+}
+
+/**
+ * Helper function to pin all subcolumns of a parent column
+ */
+function pinSubColumns<TData>(
+ column: Column<TData>,
+ pinType: false | "left" | "right"
+): void {
+ // If this is a parent column, pin all its subcolumns
+ if (isParentColumn(column)) {
+ column.columns.forEach((subColumn) => {
+ // Recursively handle nested columns
+ pinSubColumns(subColumn, pinType)
+ })
+ } else {
+ // For leaf columns, apply the pin if possible
+ if (column.getCanPin?.()) {
+ column.pin?.(pinType)
+ }
+ }
+}
+
+/**
+ * Checks if all subcolumns of a parent column are pinned to the specified side
+ */
+function areAllSubColumnsPinned<TData>(
+ column: Column<TData>,
+ pinType: "left" | "right"
+): boolean {
+ if (isParentColumn(column)) {
+ // Check if all subcolumns are pinned
+ return column.columns.every((subColumn) =>
+ areAllSubColumnsPinned(subColumn, pinType)
+ )
+ } else {
+ // For leaf columns, check if it's pinned to the specified side
+ return column.getIsPinned?.() === pinType
+ }
+}
+
+/**
+ * Helper function to get the display name of a column
+ */
+function getColumnDisplayName<TData>(column: Column<TData>): string {
+ // First try to use excelHeader from meta if available
+ const excelHeader = column.columnDef.meta?.excelHeader
+ if (excelHeader) {
+ return excelHeader
+ }
+
+ // Fall back to converting the column ID to sentence case
+ return toSentenceCase(column.id)
+}
+
+/**
+ * Array of column IDs that should be auto-pinned to the left when available
+ */
+const AUTO_PIN_LEFT_COLUMNS = ['select']
+
+/**
+ * "Pin Left" Popover. Supports pinning both individual columns and header groups.
*/
export function PinLeftButton<TData>({ table }: { table: Table<TData> }) {
const [open, setOpen] = React.useState(false)
const triggerRef = React.useRef<HTMLButtonElement>(null)
+ // Try to auto-pin select and action columns if they exist
+ React.useEffect(() => {
+ AUTO_PIN_LEFT_COLUMNS.forEach((columnId) => {
+ const column = table.getColumn(columnId)
+ if (column?.getCanPin?.()) {
+ column.pin?.("left")
+ }
+ })
+ }, [table])
+
+ // Get all columns that can be pinned (excluding auto-pinned columns)
+ const pinnableColumns = React.useMemo(() => {
+ return table.getAllColumns().filter((column) => {
+ // Skip auto-pinned columns
+ if (AUTO_PIN_LEFT_COLUMNS.includes(column.id)) {
+ return false
+ }
+
+ // If it's a leaf column, check if it can be pinned
+ if (!isParentColumn(column)) {
+ return column.getCanPin?.()
+ }
+
+ // If it's a parent column, check if at least one subcolumn can be pinned
+ return column.columns.some((subCol) => {
+ if (isParentColumn(subCol)) {
+ // Recursively check nested columns
+ return subCol.columns.some(c => c.getCanPin?.())
+ }
+ return subCol.getCanPin?.()
+ })
+ })
+ }, [table])
+
+ // Get flat list of all leaf columns for display
+ const allPinnableLeafColumns = React.useMemo(() => {
+ const leafColumns: Column<TData>[] = []
+
+ // Function to recursively collect leaf columns
+ const collectLeafColumns = (column: Column<TData>) => {
+ if (isParentColumn(column)) {
+ column.columns.forEach(collectLeafColumns)
+ } else if (column.getCanPin?.() && !AUTO_PIN_LEFT_COLUMNS.includes(column.id)) {
+ leafColumns.push(column)
+ }
+ }
+
+ // Process all columns
+ table.getAllColumns().forEach(collectLeafColumns)
+
+ return leafColumns
+ }, [table])
+
+ // Handle column pinning
+ const handleColumnPin = React.useCallback((column: Column<TData>) => {
+ // For parent columns, pin/unpin all subcolumns
+ if (isParentColumn(column)) {
+ const allPinned = areAllSubColumnsPinned(column, "left")
+ pinSubColumns(column, allPinned ? false : "left")
+ } else {
+ // For leaf columns, toggle pin state
+ const isPinned = column.getIsPinned?.() === "left"
+ column.pin?.(isPinned ? false : "left")
+ }
+ }, [])
+
+ // Check if a column or its subcolumns are pinned left
+ const isColumnPinned = React.useCallback((column: Column<TData>): boolean => {
+ if (isParentColumn(column)) {
+ return areAllSubColumnsPinned(column, "left")
+ } else {
+ return column.getIsPinned?.() === "left"
+ }
+ }, [])
+
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
@@ -39,53 +178,105 @@ export function PinLeftButton<TData>({ table }: { table: Table<TData> }) {
className="h-8 gap-2"
>
<MoveLeft className="size-4" />
-
+
<span className="hidden sm:inline">
- Left
+ 왼쪽 고정
</span>
-
<ChevronsUpDown className="ml-1 size-4 opacity-50 hidden sm:inline" />
</Button>
</PopoverTrigger>
-
+
<PopoverContent
align="end"
- className="w-44 p-0"
+ className="w-56 p-0"
onCloseAutoFocus={() => triggerRef.current?.focus()}
>
<Command>
<CommandInput placeholder="Search columns..." />
- <CommandList>
+ <CommandList className="max-h-[300px]">
<CommandEmpty>No columns found.</CommandEmpty>
<CommandGroup>
- {table
- .getAllLeafColumns()
- .filter((col) => col.getCanPin?.())
- .map((column) => {
- const pinned = column.getIsPinned?.() // 'left'|'right'|false
- // => pinned === 'left' => checked
- return (
+ {/* Parent Columns with subcolumns */}
+ {pinnableColumns
+ .filter(isParentColumn)
+ .map((parentColumn) => (
+ <React.Fragment key={parentColumn.id}>
+ {/* Parent column header - can pin/unpin all children at once */}
<CommandItem
- key={column.id}
onSelect={() => {
- // if currently pinned===left => unpin
- // else => pin left
- column.pin?.(pinned === "left" ? false : "left")
+ handleColumnPin(parentColumn)
}}
+ className="font-medium bg-muted/50"
>
<span className="truncate">
- {toSentenceCase(column.id)}
+ {getColumnDisplayName(parentColumn)}
</span>
- {/* Check if pinned===‘left’ */}
<Check
className={cn(
"ml-auto size-4 shrink-0",
- pinned === "left" ? "opacity-100" : "opacity-0"
+ isColumnPinned(parentColumn) ? "opacity-100" : "opacity-0"
)}
/>
</CommandItem>
- )
- })}
+
+ {/* Individual subcolumns */}
+ {parentColumn.columns
+ .filter(col => !isParentColumn(col) && col.getCanPin?.())
+ .map(subColumn => (
+ <CommandItem
+ key={subColumn.id}
+ onSelect={() => {
+ handleColumnPin(subColumn)
+ }}
+ className="pl-6 text-sm"
+ >
+ <span className="truncate">
+ {getColumnDisplayName(subColumn)}
+ </span>
+ <Check
+ className={cn(
+ "ml-auto size-4 shrink-0",
+ isColumnPinned(subColumn) ? "opacity-100" : "opacity-0"
+ )}
+ />
+ </CommandItem>
+ ))}
+ </React.Fragment>
+ ))}
+ </CommandGroup>
+
+ {/* Separator if we have both parent columns and standalone leaf columns */}
+ {pinnableColumns.some(isParentColumn) &&
+ allPinnableLeafColumns.some(col => !pinnableColumns.find(parent =>
+ isParentColumn(parent) && parent.columns.includes(col))
+ ) && (
+ <CommandSeparator />
+ )}
+
+ {/* Standalone leaf columns (not part of any parent column group) */}
+ <CommandGroup>
+ {allPinnableLeafColumns
+ .filter(col => !pinnableColumns.find(parent =>
+ isParentColumn(parent) && parent.columns.includes(col)
+ ))
+ .map((column) => (
+ <CommandItem
+ key={column.id}
+ onSelect={() => {
+ handleColumnPin(column)
+ }}
+ >
+ <span className="truncate">
+ {getColumnDisplayName(column)}
+ </span>
+ <Check
+ className={cn(
+ "ml-auto size-4 shrink-0",
+ isColumnPinned(column) ? "opacity-100" : "opacity-0"
+ )}
+ />
+ </CommandItem>
+ ))}
</CommandGroup>
</CommandList>
</Command>