diff --git a/public/r/data-grid-select-column.json b/public/r/data-grid-select-column.json index b679dd55..945169bc 100644 --- a/public/r/data-grid-select-column.json +++ b/public/r/data-grid-select-column.json @@ -12,7 +12,7 @@ "files": [ { "path": "src/components/data-grid/data-grid-select-column.tsx", - "content": "\"use client\";\n\nimport type {\n CellContext,\n ColumnDef,\n HeaderContext,\n} from \"@tanstack/react-table\";\nimport * as React from \"react\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { cn } from \"@/lib/utils\";\n\ntype HitboxSize = \"default\" | \"sm\" | \"lg\";\n\ninterface DataGridSelectHitboxProps {\n htmlFor: string;\n children: React.ReactNode;\n size?: HitboxSize;\n debug?: boolean;\n}\n\nfunction DataGridSelectHitbox({\n htmlFor,\n children,\n size,\n debug,\n}: DataGridSelectHitboxProps) {\n return (\n \n {children}\n \n \n );\n}\n\ninterface DataGridSelectCheckboxProps\n extends Omit, \"id\"> {\n rowNumber?: number;\n hitboxSize?: HitboxSize;\n debug?: boolean;\n}\n\nfunction DataGridSelectCheckbox({\n rowNumber,\n hitboxSize,\n debug,\n checked,\n className,\n ...props\n}: DataGridSelectCheckboxProps) {\n const id = React.useId();\n\n if (rowNumber !== undefined) {\n return (\n \n \n {rowNumber}\n \n \n \n );\n }\n\n return (\n \n \n \n );\n}\n\ninterface DataGridSelectHeaderProps\n extends Pick, \"table\"> {\n hitboxSize?: HitboxSize;\n debug?: boolean;\n}\n\nfunction DataGridSelectHeader({\n table,\n hitboxSize,\n debug,\n}: DataGridSelectHeaderProps) {\n const onCheckedChange = React.useCallback(\n (value: boolean) => table.toggleAllPageRowsSelected(value),\n [table],\n );\n\n return (\n \n );\n}\n\ninterface DataGridSelectCellProps\n extends Pick, \"row\" | \"table\"> {\n hitboxSize?: HitboxSize;\n enableRowMarkers?: boolean;\n debug?: boolean;\n}\n\nfunction DataGridSelectCell({\n row,\n table,\n hitboxSize,\n enableRowMarkers,\n debug,\n}: DataGridSelectCellProps) {\n const meta = table.options.meta;\n const rowNumber = enableRowMarkers\n ? (meta?.getVisualRowIndex?.(row.id) ?? row.index + 1)\n : undefined;\n\n const onCheckedChange = React.useCallback(\n (value: boolean) => {\n if (meta?.onRowSelect) {\n meta.onRowSelect(row.index, value, false);\n } else {\n row.toggleSelected(value);\n }\n },\n [meta, row],\n );\n\n const onClick = React.useCallback(\n (event: React.MouseEvent) => {\n if (event.shiftKey) {\n event.preventDefault();\n meta?.onRowSelect?.(row.index, !row.getIsSelected(), true);\n }\n },\n [meta, row],\n );\n\n return (\n \n );\n}\n\ninterface GetDataGridSelectColumnOptions\n extends Omit>, \"id\" | \"header\" | \"cell\"> {\n enableRowMarkers?: boolean;\n hitboxSize?: HitboxSize;\n debug?: boolean;\n}\n\nexport function getDataGridSelectColumn({\n size = 40,\n hitboxSize = \"default\",\n enableHiding = false,\n enableResizing = false,\n enableSorting = false,\n enableRowMarkers = false,\n debug = false,\n ...props\n}: GetDataGridSelectColumnOptions = {}): ColumnDef {\n return {\n id: \"select\",\n header: ({ table }) => (\n \n ),\n cell: ({ row, table }) => (\n \n ),\n size,\n enableHiding,\n enableResizing,\n enableSorting,\n ...props,\n };\n}\n", + "content": "\"use client\";\n\nimport type {\n CellContext,\n ColumnDef,\n HeaderContext,\n} from \"@tanstack/react-table\";\nimport * as React from \"react\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { cn } from \"@/lib/utils\";\n\ntype HitboxSize = \"default\" | \"sm\" | \"lg\";\n\ninterface DataGridSelectHitboxProps {\n htmlFor: string;\n children: React.ReactNode;\n size?: HitboxSize;\n debug?: boolean;\n}\n\nfunction DataGridSelectHitbox({\n htmlFor,\n children,\n size,\n debug,\n}: DataGridSelectHitboxProps) {\n return (\n \n {children}\n \n \n );\n}\n\ninterface DataGridSelectCheckboxProps\n extends Omit, \"id\"> {\n rowNumber?: number;\n hitboxSize?: HitboxSize;\n debug?: boolean;\n}\n\nfunction DataGridSelectCheckbox({\n rowNumber,\n hitboxSize,\n debug,\n checked,\n className,\n ...props\n}: DataGridSelectCheckboxProps) {\n const id = React.useId();\n\n if (rowNumber !== undefined) {\n return (\n \n \n {rowNumber}\n \n \n \n );\n }\n\n return (\n \n \n \n );\n}\n\ninterface DataGridSelectHeaderProps\n extends Pick, \"table\"> {\n hitboxSize?: HitboxSize;\n readOnly?: boolean;\n debug?: boolean;\n}\n\nfunction DataGridSelectHeader({\n table,\n hitboxSize,\n readOnly,\n debug,\n}: DataGridSelectHeaderProps) {\n const onCheckedChange = React.useCallback(\n (value: boolean) => table.toggleAllPageRowsSelected(value),\n [table],\n );\n\n if (readOnly) {\n return (\n
\n #\n
\n );\n }\n\n return (\n \n );\n}\n\ninterface DataGridSelectCellProps\n extends Pick, \"row\" | \"table\"> {\n hitboxSize?: HitboxSize;\n enableRowMarkers?: boolean;\n readOnly?: boolean;\n debug?: boolean;\n}\n\nfunction DataGridSelectCell({\n row,\n table,\n hitboxSize,\n enableRowMarkers,\n readOnly,\n debug,\n}: DataGridSelectCellProps) {\n const meta = table.options.meta;\n const rowNumber = enableRowMarkers\n ? (meta?.getVisualRowIndex?.(row.id) ?? row.index + 1)\n : undefined;\n\n const onCheckedChange = React.useCallback(\n (value: boolean) => {\n if (meta?.onRowSelect) {\n meta.onRowSelect(row.index, value, false);\n } else {\n row.toggleSelected(value);\n }\n },\n [meta, row],\n );\n\n const onClick = React.useCallback(\n (event: React.MouseEvent) => {\n if (event.shiftKey) {\n event.preventDefault();\n meta?.onRowSelect?.(row.index, !row.getIsSelected(), true);\n }\n },\n [meta, row],\n );\n\n if (readOnly) {\n return (\n
\n {rowNumber ?? row.index + 1}\n
\n );\n }\n\n return (\n \n );\n}\n\ninterface GetDataGridSelectColumnOptions\n extends Omit>, \"id\" | \"header\" | \"cell\"> {\n enableRowMarkers?: boolean;\n readOnly?: boolean;\n hitboxSize?: HitboxSize;\n debug?: boolean;\n}\n\nexport function getDataGridSelectColumn({\n size = 40,\n hitboxSize = \"default\",\n enableHiding = false,\n enableResizing = false,\n enableSorting = false,\n enableRowMarkers = false,\n readOnly = false,\n debug = false,\n ...props\n}: GetDataGridSelectColumnOptions = {}): ColumnDef {\n return {\n id: \"select\",\n header: ({ table }) => (\n \n ),\n cell: ({ row, table }) => (\n \n ),\n size,\n enableHiding,\n enableResizing,\n enableSorting,\n ...props,\n };\n}\n", "type": "registry:component", "target": "src/components/data-grid/data-grid-select-column.tsx" } diff --git a/src/app/data-grid-live/components/data-grid-action-bar.tsx b/src/app/data-grid-live/components/data-grid-action-bar.tsx new file mode 100644 index 00000000..babc2304 --- /dev/null +++ b/src/app/data-grid-live/components/data-grid-action-bar.tsx @@ -0,0 +1,119 @@ +"use client"; + +import type { Table, TableMeta } from "@tanstack/react-table"; +import { CheckCircle2, Palette, Trash2, X } from "lucide-react"; +import * as React from "react"; + +import { + ActionBar, + ActionBarClose, + ActionBarGroup, + ActionBarItem, + ActionBarSelection, + ActionBarSeparator, +} from "@/components/ui/action-bar"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import type { CellSelectOption } from "@/types/data-grid"; + +interface DataGridActionBarProps { + table: Table; + tableMeta: TableMeta; + selectedCellCount: number; + statusOptions?: CellSelectOption[]; + styleOptions?: CellSelectOption[]; + onStatusUpdate?: (value: string) => void; + onStyleUpdate?: (value: string) => void; + onDelete?: () => void; +} + +export function DataGridActionBar({ + table, + tableMeta, + selectedCellCount, + statusOptions, + styleOptions, + onStatusUpdate, + onStyleUpdate, + onDelete, +}: DataGridActionBarProps) { + const onOpenChange = React.useCallback( + (open: boolean) => { + if (!open) { + table.toggleAllRowsSelected(false); + tableMeta.onSelectionClear?.(); + } + }, + [table, tableMeta], + ); + + return ( + 0} + onOpenChange={onOpenChange} + > + + {selectedCellCount} + {selectedCellCount === 1 ? "cell" : "cells"} selected + + + + + + + + {statusOptions && statusOptions.length > 0 && onStatusUpdate && ( + + + + + Status + + + + {statusOptions.map((option) => ( + onStatusUpdate(option.value)} + > + {option.label} + + ))} + + + )} + {styleOptions && styleOptions.length > 0 && onStyleUpdate && ( + + + + + Style + + + + {styleOptions.map((option) => ( + onStyleUpdate(option.value)} + > + {option.label} + + ))} + + + )} + {onDelete && ( + + + Delete + + )} + + + ); +} diff --git a/src/app/data-grid-live/components/data-grid-live-demo.tsx b/src/app/data-grid-live/components/data-grid-live-demo.tsx index 69ef5b7a..0990c049 100644 --- a/src/app/data-grid-live/components/data-grid-live-demo.tsx +++ b/src/app/data-grid-live/components/data-grid-live-demo.tsx @@ -2,12 +2,9 @@ import { useLiveQuery } from "@tanstack/react-db"; import type { ColumnDef, SortingState } from "@tanstack/react-table"; -import { CheckCircle2, Palette, Trash2, X } from "lucide-react"; import * as React from "react"; import { use } from "react"; import { toast } from "sonner"; - -import { skatersCollection } from "@/app/data-grid-live/lib/collections"; import { generateRandomSkater, getSkaterStatusIcon, @@ -21,20 +18,6 @@ import { DataGridRowHeightMenu } from "@/components/data-grid/data-grid-row-heig import { getDataGridSelectColumn } from "@/components/data-grid/data-grid-select-column"; import { DataGridSortMenu } from "@/components/data-grid/data-grid-sort-menu"; import { DataGridViewMenu } from "@/components/data-grid/data-grid-view-menu"; -import { - ActionBar, - ActionBarClose, - ActionBarGroup, - ActionBarItem, - ActionBarSelection, - ActionBarSeparator, -} from "@/components/ui/action-bar"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; import { skaters } from "@/db/schema"; import { type UseDataGridProps, useDataGrid } from "@/hooks/use-data-grid"; import { @@ -45,7 +28,9 @@ import { useWindowSize } from "@/hooks/use-window-size"; import { getFilterFn } from "@/lib/data-grid-filters"; import { generateId } from "@/lib/id"; import { useUploadThing } from "@/lib/uploadthing"; +import { skatersCollection } from "../lib/collections"; import type { SkaterSchema } from "../lib/validation"; +import { DataGridActionBar } from "./data-grid-action-bar"; const stanceOptions = skaters.stance.enumValues.map((stance) => ({ label: stance.charAt(0).toUpperCase() + stance.slice(1), @@ -511,11 +496,33 @@ export function DataGridLiveDemo() { enablePaste: true, }); - const updateSelectedSkaters = React.useCallback( - ( - field: "status" | "style", - value: SkaterSchema["status"] | SkaterSchema["style"], - ) => { + const onStatusUpdate = React.useCallback( + (value: string) => { + const selectedRows = table.getSelectedRowModel().rows; + if (selectedRows.length === 0) { + toast.error("No skaters selected"); + return; + } + + // Use batch update - single transaction for all updates + skatersCollection.update( + selectedRows.map((row) => row.original.id), + (drafts) => { + for (const draft of drafts) { + draft.status = value as never; + } + }, + ); + + toast.success( + `${selectedRows.length} skater${selectedRows.length === 1 ? "" : "s"} updated`, + ); + }, + [table], + ); + + const onStyleUpdate = React.useCallback( + (value: string) => { const selectedRows = table.getSelectedRowModel().rows; if (selectedRows.length === 0) { toast.error("No skaters selected"); @@ -527,7 +534,7 @@ export function DataGridLiveDemo() { selectedRows.map((row) => row.original.id), (drafts) => { for (const draft of drafts) { - draft[field] = value as never; + draft.style = value as never; } }, ); @@ -539,7 +546,7 @@ export function DataGridLiveDemo() { [table], ); - const deleteSelectedSkaters = React.useCallback(() => { + const onDelete = React.useCallback(() => { const selectedRows = table.getSelectedRowModel().rows; if (selectedRows.length === 0) { toast.error("No skaters selected"); @@ -584,72 +591,16 @@ export function DataGridLiveDemo() { tableMeta={tableMeta} height={height} /> - 0} - onOpenChange={(open) => { - if (!open) { - table.toggleAllRowsSelected(false); - tableMeta.onSelectionClear?.(); - } - }} - > - - {selectedCellCount} - {selectedCellCount === 1 ? "cell" : "cells"} selected - - - - - - - - - - - - Status - - - - {statusOptions.map((option) => ( - updateSelectedSkaters("status", option.value)} - > - {option.label} - - ))} - - - - - - - Style - - - - {styleOptions.map((option) => ( - updateSelectedSkaters("style", option.value)} - > - {option.label} - - ))} - - - - - Delete - - - + ); } diff --git a/src/app/data-grid/components/data-grid-demo.tsx b/src/app/data-grid/components/data-grid-demo.tsx index d9481516..becf96df 100644 --- a/src/app/data-grid/components/data-grid-demo.tsx +++ b/src/app/data-grid/components/data-grid-demo.tsx @@ -99,7 +99,10 @@ export function DataGridDemo() { const columns = React.useMemo[]>( () => [ - getDataGridSelectColumn({ enableRowMarkers: true }), + getDataGridSelectColumn({ + enableRowMarkers: true, + readOnly: true, + }), { id: "name", accessorKey: "name", diff --git a/src/components/data-grid/data-grid-select-column.tsx b/src/components/data-grid/data-grid-select-column.tsx index 9910cba6..ab3ef850 100644 --- a/src/components/data-grid/data-grid-select-column.tsx +++ b/src/components/data-grid/data-grid-select-column.tsx @@ -106,12 +106,14 @@ function DataGridSelectCheckbox({ interface DataGridSelectHeaderProps extends Pick, "table"> { hitboxSize?: HitboxSize; + readOnly?: boolean; debug?: boolean; } function DataGridSelectHeader({ table, hitboxSize, + readOnly, debug, }: DataGridSelectHeaderProps) { const onCheckedChange = React.useCallback( @@ -119,6 +121,14 @@ function DataGridSelectHeader({ [table], ); + if (readOnly) { + return ( +
+ # +
+ ); + } + return ( extends Pick, "row" | "table"> { hitboxSize?: HitboxSize; enableRowMarkers?: boolean; + readOnly?: boolean; debug?: boolean; } @@ -145,6 +156,7 @@ function DataGridSelectCell({ table, hitboxSize, enableRowMarkers, + readOnly, debug, }: DataGridSelectCellProps) { const meta = table.options.meta; @@ -173,6 +185,14 @@ function DataGridSelectCell({ [meta, row], ); + if (readOnly) { + return ( +
+ {rowNumber ?? row.index + 1} +
+ ); + } + return ( ({ interface GetDataGridSelectColumnOptions extends Omit>, "id" | "header" | "cell"> { enableRowMarkers?: boolean; + readOnly?: boolean; hitboxSize?: HitboxSize; debug?: boolean; } @@ -200,6 +221,7 @@ export function getDataGridSelectColumn({ enableResizing = false, enableSorting = false, enableRowMarkers = false, + readOnly = false, debug = false, ...props }: GetDataGridSelectColumnOptions = {}): ColumnDef { @@ -209,6 +231,7 @@ export function getDataGridSelectColumn({ ), @@ -217,6 +240,7 @@ export function getDataGridSelectColumn({ row={row} table={table} enableRowMarkers={enableRowMarkers} + readOnly={readOnly} hitboxSize={hitboxSize} debug={debug} />