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}
/>