diff --git a/bun.lockb b/bun.lockb index 7310b1b..f828862 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 913f8b2..91dd1ea 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@radix-ui/react-alert-dialog": "^1.1.4", "@radix-ui/react-avatar": "^1.1.0", "@radix-ui/react-checkbox": "^1.1.2", + "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-hover-card": "^1.1.1", diff --git a/src/components/ui/context-menu.tsx b/src/components/ui/context-menu.tsx new file mode 100644 index 0000000..6d0fd00 --- /dev/null +++ b/src/components/ui/context-menu.tsx @@ -0,0 +1,197 @@ +import * as React from "react" +import * as ContextMenuPrimitive from "@radix-ui/react-context-menu" +import { cn } from "@/lib/utils" +import { CheckIcon, ChevronRightIcon, DotFilledIcon } from "@radix-ui/react-icons" + +const ContextMenu = ContextMenuPrimitive.Root + +const ContextMenuTrigger = ContextMenuPrimitive.Trigger + +const ContextMenuGroup = ContextMenuPrimitive.Group + +const ContextMenuPortal = ContextMenuPrimitive.Portal + +const ContextMenuSub = ContextMenuPrimitive.Sub + +const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup + +const ContextMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName + +const ContextMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName + +const ContextMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName + +const ContextMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName + +const ContextMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +ContextMenuCheckboxItem.displayName = + ContextMenuPrimitive.CheckboxItem.displayName + +const ContextMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName + +const ContextMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName + +const ContextMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName + +const ContextMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +ContextMenuShortcut.displayName = "ContextMenuShortcut" + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, +} diff --git a/src/components/ui/data-table.tsx b/src/components/ui/data-table.tsx index cfc3b73..cc3aeb6 100644 --- a/src/components/ui/data-table.tsx +++ b/src/components/ui/data-table.tsx @@ -22,6 +22,13 @@ import { DropdownMenuContent, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from "@/components/ui/context-menu" import { TableRoot, TableBody, @@ -35,190 +42,221 @@ interface DataTableProps { columns: ColumnDef[] data: TData[] onRowSelectionChange?: (selectedIds: string[]) => void - selectedIds?: string[] getRowId?: (row: TData) => string searchValue?: string onSearchChange?: (value: string) => void } -export function DataTable({ - columns, - data, - onRowSelectionChange, - selectedIds = [], - getRowId, - searchValue, - onSearchChange, -}: DataTableProps) { - const [sorting, setSorting] = React.useState([]) - const [columnFilters, setColumnFilters] = React.useState([]) - const [columnVisibility, setColumnVisibility] = React.useState({}) - - // Initialize row selection from selectedIds - const initialRowSelection = React.useMemo(() => { - const selection: Record = {} - selectedIds.forEach(id => { - selection[id] = true +export interface DataTableRef { + selectRows: (rowIds: string[]) => void + selectAllRows: () => void + deselectAllRows: () => void + getSelectedRows: () => string[] + clearSelection: () => void +} + +const DataTable = React.forwardRef, DataTableProps>( + ({ columns, data, onRowSelectionChange, getRowId, searchValue, onSearchChange }, ref) => { + const [sorting, setSorting] = React.useState([]) + const [columnFilters, setColumnFilters] = React.useState([]) + const [columnVisibility, setColumnVisibility] = React.useState({}) + const [rowSelection, setRowSelection] = React.useState>({}) + + const table = useReactTable({ + data, + columns, + getRowId: getRowId || ((row: any) => row.id), + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + // Add these options to prevent potential issues + enableRowSelection: true, + enableMultiRowSelection: true, + enableSorting: true, + enableFilters: true, + enableColumnFilters: true, }) - return selection - }, [selectedIds]) - - const [rowSelection, setRowSelection] = React.useState>(initialRowSelection) - const table = useReactTable({ - data, - columns, - getRowId: getRowId || ((row: any) => row.id), - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - onColumnVisibilityChange: setColumnVisibility, - onRowSelectionChange: setRowSelection, - state: { - sorting, - columnFilters, - columnVisibility, - rowSelection, - }, - // Add these options to prevent potential issues - enableRowSelection: true, - enableMultiRowSelection: true, - enableSorting: true, - enableFilters: true, - enableColumnFilters: true, - }) + // Expose methods via ref + React.useImperativeHandle(ref, () => ({ + selectRows: (rowIds: string[]) => { + const newSelection: Record = {} + rowIds.forEach(id => { + newSelection[id] = true + }) + setRowSelection(newSelection) + }, + selectAllRows: () => { + const allRowIds = table.getRowModel().rows.map(row => row.id) + const newSelection: Record = {} + allRowIds.forEach(id => { + newSelection[id] = true + }) + setRowSelection(newSelection) + }, + deselectAllRows: () => { + setRowSelection({}) + }, + getSelectedRows: () => { + return Object.keys(rowSelection).filter(id => rowSelection[id]) + }, + clearSelection: () => { + setRowSelection({}) + }, + }), [table]) - // Sync external selection with table selection - React.useEffect(() => { - const newRowSelection: Record = {} - selectedIds.forEach((id) => { - newRowSelection[id] = true - }) - setRowSelection(newRowSelection) - }, [selectedIds]) + // Sync external search with table filter + React.useEffect(() => { + if (searchValue !== undefined) { + table.getColumn("albumName")?.setFilterValue(searchValue) + } + }, [searchValue, table]) - // Sync external search with table filter - React.useEffect(() => { - if (searchValue !== undefined) { - table.getColumn("albumName")?.setFilterValue(searchValue) + // Notify parent of selection changes + React.useEffect(() => { + const selectedRowIds = Object.keys(rowSelection).filter(id => rowSelection[id]) + onRowSelectionChange?.(selectedRowIds) + }, [rowSelection, onRowSelectionChange]) + + const handleShowAllColumns = () => { + table.toggleAllColumnsVisible(true) } - }, [searchValue, table]) - // Notify parent of selection changes - React.useEffect(() => { - const selectedRowIds = Object.keys(rowSelection).filter(id => rowSelection[id]) - onRowSelectionChange?.(selectedRowIds) - }, [rowSelection, onRowSelectionChange]) + const handleHideAllColumns = () => { + table.toggleAllColumnsVisible(false) + } - return ( -
-
- - - - - - {table - .getAllColumns() - .filter((column) => column.getCanHide()) - .map((column) => { - return ( - - column.toggleVisibility(!!value) - } - > - {column.id} - - ) - })} - - -
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ) - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} + return ( +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + + + header.column.toggleVisibility()} + > + {header.column.getIsVisible() ? "Hide Column" : "Show Column"} + + + + Show All Columns + + + Hide All Columns + + +
+ Column Visibility +
+ {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + column.toggleVisibility()} + className="flex items-center justify-between" + > + {column.id} +
+ + ) + })} + + + ) + })} - )) - ) : ( - - - No results. - - - )} - - -
-
-
- {table.getFilteredSelectedRowModel().rows.length} of{" "} - {table.getFilteredRowModel().rows.length} row(s) selected. + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
-
- - +
+
+ {table.getFilteredSelectedRowModel().rows.length} of{" "} + {table.getFilteredRowModel().rows.length} row(s) selected. +
+
+ + +
-
- ) -} \ No newline at end of file + ) + } +) + +DataTable.displayName = "DataTable" + +export default DataTable \ No newline at end of file diff --git a/src/pages/albums/index.tsx b/src/pages/albums/index.tsx index af4c45f..f018ce4 100644 --- a/src/pages/albums/index.tsx +++ b/src/pages/albums/index.tsx @@ -16,7 +16,7 @@ import { AlertDialog } from '@/components/ui/alert-dialog' import toast from 'react-hot-toast' import { useAlbumSelection } from '@/hooks' import FloatingBar from '@/components/shared/FloatingBar' -import { DataTable } from '@/components/ui/data-table' +import DataTable, { DataTableRef } from '@/components/ui/data-table' import { albumColumns } from '@/components/albums/AlbumTableColumns' const SORT_BY_OPTIONS = [ @@ -41,6 +41,8 @@ export default function AlbumListPage() { const [errorMessage, setErrorMessage] = useState('') const albumShareDialogRef = useRef(null); const [deleting, setDeleting] = useState(false) + + const dataTableRef = useRef>(null) // Separate selection state for table view const [tableSelectedIds, setTableSelectedIds] = useState([]) @@ -103,9 +105,9 @@ export default function AlbumListPage() { }) } - const handleTableSelectionChange = (selectedRowIds: string[]) => { + const handleTableSelectionChange = useCallback((selectedRowIds: string[]) => { setTableSelectedIds(selectedRowIds) - } + }, []) const handleSelectAll = () => { if (viewMode === 'table') { @@ -140,11 +142,11 @@ export default function AlbumListPage() { row.id} searchValue={search} onSearchChange={setSearch} + ref={dataTableRef} />
)