Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import type { NextConfig } from "next";
import "./src/env.js";

const nextConfig: NextConfig = {
cacheComponents: true,
// Already doing typechecking as separate task in CI
typescript: { ignoreBuildErrors: true },
cacheComponents: true,
};

export default nextConfig;
8 changes: 4 additions & 4 deletions public/r/data-grid.json

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions src/app/data-grid/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { Suspense } from "react";
import { DataGridDemo } from "@/components/data-grid/data-grid-demo";
import { Skeleton } from "@/components/ui/skeleton";

export default async function DataGridPage() {
return (
<Suspense
fallback={
<div className="flex h-[600px] items-center justify-center">
Loading...
<div className="container flex h-[calc(100dvh-5rem)] flex-col gap-4 py-4">
<div className="flex items-center gap-2 self-end">
<Skeleton className="h-7 w-18" />
<Skeleton className="h-7 w-18" />
<Skeleton className="h-7 w-18" />
</div>
<Skeleton className="h-full w-full" />
</div>
}
>
Expand Down
41 changes: 36 additions & 5 deletions src/components/data-grid/data-grid-context-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"use client";

import type { Table, TableMeta } from "@tanstack/react-table";
import { CopyIcon, EraserIcon } from "lucide-react";
import { CopyIcon, EraserIcon, Trash2Icon } from "lucide-react";
import * as React from "react";
import { toast } from "sonner";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { parseCellKey } from "@/lib/data-grid";
Expand All @@ -26,6 +27,7 @@ export function DataGridContextMenu<TData>({
const selectionState = meta?.selectionState;
const dataGridRef = meta?.dataGridRef;
const onDataUpdate = meta?.onDataUpdate;
const onRowsDelete = meta?.onRowsDelete;

if (!contextMenu) return null;

Expand All @@ -37,6 +39,7 @@ export function DataGridContextMenu<TData>({
onContextMenuOpenChange={onContextMenuOpenChange}
selectionState={selectionState}
onDataUpdate={onDataUpdate}
onRowsDelete={onRowsDelete}
/>
);
}
Expand All @@ -48,6 +51,7 @@ interface ContextMenuProps<TData>
| "onContextMenuOpenChange"
| "selectionState"
| "onDataUpdate"
| "onRowsDelete"
>,
Required<Pick<TableMeta<TData>, "contextMenu">> {
table: Table<TData>;
Expand All @@ -73,6 +77,7 @@ function ContextMenuImpl<TData>({
onContextMenuOpenChange,
selectionState,
onDataUpdate,
onRowsDelete,
}: ContextMenuProps<TData>) {
const triggerStyle = React.useMemo<React.CSSProperties>(
() => ({
Expand Down Expand Up @@ -111,7 +116,6 @@ function ContextMenuImpl<TData>({
const rows = table.getRowModel().rows;
const columnIds: string[] = [];

// Collect all unique column IDs from selected cells
const selectedCellsArray = Array.from(selectionState.selectedCells);
for (const cellKey of selectedCellsArray) {
const { columnId } = parseCellKey(cellKey);
Expand All @@ -120,7 +124,6 @@ function ContextMenuImpl<TData>({
}
}

// Build grid of selected cells
const cellData = new Map<string, string>();
for (const cellKey of selectedCellsArray) {
const { rowIndex, columnId } = parseCellKey(cellKey);
Expand All @@ -136,7 +139,6 @@ function ContextMenuImpl<TData>({
}
}

// Get row indices and column indices
const rowIndices = new Set<number>();
const colIndices = new Set<number>();

Expand All @@ -153,7 +155,6 @@ function ContextMenuImpl<TData>({
const sortedColIndices = Array.from(colIndices).sort((a, b) => a - b);
const sortedColumnIds = sortedColIndices.map((i) => columnIds[i]);

// Build TSV (tab-separated values)
const tsvData = sortedRowIndices
.map((rowIndex) =>
sortedColumnIds
Expand Down Expand Up @@ -192,6 +193,27 @@ function ContextMenuImpl<TData>({
);
}, [onDataUpdate, selectionState]);

const onDelete = React.useCallback(async () => {
if (
!selectionState?.selectedCells ||
selectionState.selectedCells.size === 0
)
return;

const rowIndices = new Set<number>();
for (const cellKey of selectionState.selectedCells) {
const { rowIndex } = parseCellKey(cellKey);
rowIndices.add(rowIndex);
}

const rowIndicesArray = Array.from(rowIndices).sort((a, b) => a - b);
const rowCount = rowIndicesArray.length;

await onRowsDelete?.(rowIndicesArray);

toast.success(`${rowCount} row${rowCount !== 1 ? "s" : ""} deleted`);
}, [onRowsDelete, selectionState]);

return (
<DropdownMenu
open={contextMenu.open}
Expand All @@ -212,6 +234,15 @@ function ContextMenuImpl<TData>({
<EraserIcon />
Clear
</DropdownMenuItem>
{onRowsDelete && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive" onSelect={onDelete}>
<Trash2Icon />
Delete rows
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
);
Expand Down
57 changes: 37 additions & 20 deletions src/components/data-grid/data-grid-demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { DataGridRowHeightMenu } from "@/components/data-grid/data-grid-row-heig
import { DataGridSortMenu } from "@/components/data-grid/data-grid-sort-menu";
import { DataGridViewMenu } from "@/components/data-grid/data-grid-view-menu";
import { Checkbox } from "@/components/ui/checkbox";
import { useDataGrid } from "@/hooks/use-data-grid";
import { type UseDataGridProps, useDataGrid } from "@/hooks/use-data-grid";
import { useWindowSize } from "@/hooks/use-window-size";

interface Person {
Expand Down Expand Up @@ -284,25 +284,47 @@ export function DataGridDemo() {
[],
);

const onRowAdd = React.useCallback(() => {
const newId = data.length + 1;
setData((prev) => [
...prev,
{
id: newId.toString(),
},
]);
const onRowAdd: NonNullable<UseDataGridProps<Person>["onRowAdd"]> =
React.useCallback(() => {
// In a real app, you would make a server call here:
// await fetch('/api/people', {
// method: 'POST',
// body: JSON.stringify({ name: 'New Person' })
// });

// For this demo, just add a new row to the data
const newId = data.length + 1;
setData((prev) => [
...prev,
{
id: newId.toString(),
},
]);

return {
rowIndex: data.length,
columnId: "name",
};
}, [data.length]);

const onRowsDelete: NonNullable<UseDataGridProps<Person>["onRowsDelete"]> =
React.useCallback((rows) => {
Comment thread
sadmann7 marked this conversation as resolved.
// In a real app, you would make a server call here:
// await fetch('/api/people', {
// method: 'DELETE',
// body: JSON.stringify({ ids: rows.map(r => r.id) })
// });

return {
rowIndex: data.length,
columnId: "name",
};
}, [data.length]);
// For this demo, just filter out the deleted rows
setData((prev) => prev.filter((row) => !rows.includes(row)));
}, []);

const { table, ...dataGridProps } = useDataGrid({
columns,
data,
onDataChange: setData,
onRowAdd,
onRowsDelete,
getRowId: (row) => row.id,
initialState: {
columnPinning: {
Expand All @@ -325,12 +347,7 @@ export function DataGridDemo() {
<DataGridRowHeightMenu table={table} align="end" />
<DataGridViewMenu table={table} align="end" />
</div>
<DataGrid
{...dataGridProps}
table={table}
onRowAdd={onRowAdd}
height={height}
/>
<DataGrid {...dataGridProps} table={table} height={height} />
</div>
);
}
40 changes: 4 additions & 36 deletions src/components/data-grid/data-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,11 @@ import { DataGridSearch } from "@/components/data-grid/data-grid-search";
import type { useDataGrid } from "@/hooks/use-data-grid";
import { getCommonPinningStyles } from "@/lib/data-table";
import { cn } from "@/lib/utils";
import type { CellPosition } from "@/types/data-grid";

interface DataGridProps<TData>
extends ReturnType<typeof useDataGrid<TData>>,
React.ComponentProps<"div"> {
height?: number;
onRowAdd?: (event?: React.MouseEvent<HTMLDivElement>) =>
| Partial<CellPosition>
| Promise<Partial<CellPosition>>
| null
// biome-ignore lint/suspicious/noConfusingVoidType: void is needed here to allow functions without explicit return
| void;
}

export function DataGrid<TData>({
Expand All @@ -35,8 +28,7 @@ export function DataGrid<TData>({
height = 600,
searchState,
columnSizeVars,
scrollToRow,
onRowAdd: onRowAddProp,
onRowAdd,
className,
...props
}: DataGridProps<TData>) {
Expand All @@ -54,40 +46,16 @@ export function DataGrid<TData>({
[],
);

const onRowAdd = React.useCallback(
async (event?: React.MouseEvent<HTMLDivElement>) => {
if (!onRowAddProp) return;

const result = await onRowAddProp();

if (event?.defaultPrevented || result === null) return;

if (result) {
const adjustedRowIndex =
(result.rowIndex ?? 0) >= rows.length ? rows.length : result.rowIndex;

scrollToRow({
rowIndex: adjustedRowIndex,
columnId: result.columnId,
});
return;
}

scrollToRow({ rowIndex: rows.length });
},
[onRowAddProp, scrollToRow, rows.length],
);

const onAddRowKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (!onRowAddProp) return;
if (!onRowAdd) return;

if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
onRowAdd();
}
},
[onRowAddProp, onRowAdd],
[onRowAdd],
);

return (
Expand All @@ -102,7 +70,7 @@ export function DataGrid<TData>({
<div
role="grid"
aria-label="Data grid"
aria-rowcount={rows.length + (onRowAddProp ? 1 : 0)}
aria-rowcount={rows.length + (onRowAdd ? 1 : 0)}
aria-colcount={columns.length}
data-slot="grid"
tabIndex={0}
Expand Down
Loading