Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2c6a4ac
feat: add toast notifications for cell copy operations
sadmann7 Nov 21, 2025
7b176c1
fix: copy focused cell when no selection exists
sadmann7 Nov 21, 2025
5ed7773
feat: add Airtable-style paste expansion dialog
sadmann7 Nov 21, 2025
18cbff2
feat: add paste dialog types to data-grid.ts
sadmann7 Nov 21, 2025
7abce04
fix: correct onClick type for paste dialog cancel button
sadmann7 Nov 21, 2025
5b0623f
chore: lint
sadmann7 Nov 21, 2025
dfc2dcf
debug: add logging to diagnose paste expansion issue
sadmann7 Nov 21, 2025
d7361e8
debug: add more detailed paste loop logging
sadmann7 Nov 21, 2025
0cc2f8a
fix: use table row count instead of stale data.length after adding rows
sadmann7 Nov 21, 2025
d6b0a92
feat: implement working paste with row expansion
sadmann7 Nov 21, 2025
31738a0
feat: add enablePaste flag to control paste functionality
sadmann7 Nov 21, 2025
c833d51
refactor: use faker nanoid for row creation instead of incremental IDs
sadmann7 Nov 21, 2025
e98172c
fix: clear cell selections when creating a new row (Airtable behavior)
sadmann7 Nov 21, 2025
2a6aee6
refactor: add React.memo to DataGridPasteDialog and update registry
sadmann7 Nov 21, 2025
1071698
refactor: improve paste dialog copy and styling
sadmann7 Nov 21, 2025
d112faf
refactor: use refs and memoize callbacks in paste dialog
sadmann7 Nov 21, 2025
534c75d
chore: rebuild registry
sadmann7 Nov 21, 2025
66ee94f
feat: improve paste dialog styling with radio components
sadmann7 Nov 21, 2025
6f61fe0
feat: auto-select pasted cells and reorganize code
sadmann7 Nov 21, 2025
efb65c5
feat: add onRowsAdd for efficient bulk row creation
sadmann7 Nov 21, 2025
b3a7601
chore: rebuild registry again
sadmann7 Nov 21, 2025
f491c16
chore: rebuild registry again again again
sadmann7 Nov 21, 2025
3d970ff
fix: file cell copy and paste functionality
sadmann7 Nov 21, 2025
58f79f2
refactor: address code review feedback
sadmann7 Nov 21, 2025
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
13 changes: 10 additions & 3 deletions public/r/data-grid.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions public/r/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@
"calendar",
"checkbox",
"command",
"dialog",
"dropdown-menu",
"input",
"popover",
Expand Down Expand Up @@ -375,6 +376,11 @@
"type": "registry:component",
"target": "src/components/data-grid/data-grid-context-menu.tsx"
},
{
"path": "src/components/data-grid/data-grid-paste-dialog.tsx",
"type": "registry:component",
"target": "src/components/data-grid/data-grid-paste-dialog.tsx"
},
{
"path": "src/components/data-grid/data-grid-row.tsx",
"type": "registry:component",
Expand Down
6 changes: 6 additions & 0 deletions registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@
"calendar",
"checkbox",
"command",
"dialog",
"dropdown-menu",
"input",
"popover",
Expand Down Expand Up @@ -375,6 +376,11 @@
"type": "registry:component",
"target": "src/components/data-grid/data-grid-context-menu.tsx"
},
{
"path": "src/components/data-grid/data-grid-paste-dialog.tsx",
"type": "registry:component",
"target": "src/components/data-grid/data-grid-paste-dialog.tsx"
},
{
"path": "src/components/data-grid/data-grid-row.tsx",
"type": "registry:component",
Expand Down
27 changes: 24 additions & 3 deletions src/components/data-grid/data-grid-demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ function generatePerson(id: number): Person {
});

return {
id: id.toString(),
id: faker.string.nanoid(8),
name: `${firstName} ${lastName}`,
age: faker.number.int({ min: 22, max: 65 }),
email: faker.internet.email({ firstName, lastName }).toLowerCase(),
Expand Down Expand Up @@ -368,18 +368,18 @@ export function DataGridDemo() {

const onRowAdd: NonNullable<UseDataGridProps<Person>["onRowAdd"]> =
React.useCallback(() => {
// Called when user manually adds a single row (e.g., clicking "Add Row" button)
// 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(),
id: faker.string.nanoid(8),
},
]);

Expand All @@ -389,6 +389,25 @@ export function DataGridDemo() {
};
}, [data.length]);

const onRowsAdd: NonNullable<UseDataGridProps<Person>["onRowsAdd"]> =
React.useCallback((count: number) => {
// Called when paste operation needs to create multiple rows at once
// This is more efficient than calling onRowAdd multiple times - only a single API call needed
// In a real app, you would make a server call here:
// await fetch('/api/people/bulk', {
// method: 'POST',
// body: JSON.stringify({ count })
// });

// For this demo, create multiple rows in a single state update
setData((prev) => {
const newRows = Array.from({ length: count }, () => ({
id: faker.string.nanoid(8),
}));
return [...prev, ...newRows];
});
}, []);

const onRowsDelete: NonNullable<UseDataGridProps<Person>["onRowsDelete"]> =
React.useCallback((rows) => {
// In a real app, you would make a server call here:
Expand All @@ -406,6 +425,7 @@ export function DataGridDemo() {
data,
onDataChange: setData,
onRowAdd,
onRowsAdd,
onRowsDelete,
getRowId: (row) => row.id,
initialState: {
Expand All @@ -414,6 +434,7 @@ export function DataGridDemo() {
},
},
enableSearch: true,
enablePaste: true,
});

const height = Math.max(400, windowSize.height - 150);
Expand Down
146 changes: 146 additions & 0 deletions src/components/data-grid/data-grid-paste-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"use client";

import type { Table, TableMeta } from "@tanstack/react-table";
import * as React from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { cn } from "@/lib/utils";

interface DataGridPasteDialogProps<TData> {
table: Table<TData>;
}

export function DataGridPasteDialog<TData>({
table,
}: DataGridPasteDialogProps<TData>) {
const meta = table.options.meta;
const pasteDialog = meta?.pasteDialog;
const onPasteDialogOpenChange = meta?.onPasteDialogOpenChange;
const onPasteWithExpansion = meta?.onPasteWithExpansion;
const onPasteWithoutExpansion = meta?.onPasteWithoutExpansion;

if (!pasteDialog) return null;

return (
<PasteDialog
pasteDialog={pasteDialog}
onPasteDialogOpenChange={onPasteDialogOpenChange}
onPasteWithExpansion={onPasteWithExpansion}
onPasteWithoutExpansion={onPasteWithoutExpansion}
/>
);
}

interface PasteDialogProps
extends Pick<
TableMeta<unknown>,
| "onPasteDialogOpenChange"
| "onPasteWithExpansion"
| "onPasteWithoutExpansion"
>,
Required<Pick<TableMeta<unknown>, "pasteDialog">> {}

const PasteDialog = React.memo(PasteDialogImpl, (prev, next) => {
if (prev.pasteDialog.open !== next.pasteDialog.open) return false;
if (!next.pasteDialog.open) return true;
if (prev.pasteDialog.rowsNeeded !== next.pasteDialog.rowsNeeded) return false;

return true;
});

function PasteDialogImpl({
pasteDialog,
onPasteDialogOpenChange,
onPasteWithExpansion,
onPasteWithoutExpansion,
}: PasteDialogProps) {
const expandRadioRef = React.useRef<HTMLInputElement | null>(null);

const onCancel = React.useCallback(() => {
onPasteDialogOpenChange?.(false);
}, [onPasteDialogOpenChange]);

const onContinue = React.useCallback(() => {
if (expandRadioRef.current?.checked) {
onPasteWithExpansion?.();
} else {
onPasteWithoutExpansion?.();
}
}, [onPasteWithExpansion, onPasteWithoutExpansion]);

return (
<Dialog open={pasteDialog.open} onOpenChange={onPasteDialogOpenChange}>
<DialogContent data-grid-popover="">
<DialogHeader>
<DialogTitle>Do you want to add more rows?</DialogTitle>
<DialogDescription>
We need <strong>{pasteDialog.rowsNeeded}</strong> additional row
{pasteDialog.rowsNeeded !== 1 ? "s" : ""} to paste everything from
your clipboard.
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-3 py-1">
<label className="flex cursor-pointer items-start gap-3">
<RadioItem
ref={expandRadioRef}
name="expand-option"
value="expand"
defaultChecked
/>
<div className="flex flex-col gap-1">
<span className="font-medium text-sm leading-none">
Create new rows
</span>
<span className="text-muted-foreground text-sm">
Add {pasteDialog.rowsNeeded} new row
{pasteDialog.rowsNeeded !== 1 ? "s" : ""} to the table and paste
all data
</span>
</div>
</label>
<label className="flex cursor-pointer items-start gap-3">
<RadioItem name="expand-option" value="no-expand" />
<div className="flex flex-col gap-1">
<span className="font-medium text-sm leading-none">
Keep current rows
</span>
<span className="text-muted-foreground text-sm">
Paste only what fits in the existing rows
</span>
</div>
</label>
</div>
<DialogFooter>
<Button variant="outline" onClick={onCancel}>
Cancel
</Button>
<Button onClick={onContinue}>Continue</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

function RadioItem({ className, ...props }: React.ComponentProps<"input">) {
return (
<input
type="radio"
className={cn(
"relative size-4 shrink-0 appearance-none rounded-full border border-input bg-background shadow-xs outline-none transition-[color,box-shadow]",
"text-primary focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
"disabled:cursor-not-allowed disabled:opacity-50",
"checked:before:-translate-x-1/2 checked:before:-translate-y-1/2 checked:before:absolute checked:before:top-1/2 checked:before:left-1/2 checked:before:size-2 checked:before:rounded-full checked:before:bg-primary checked:before:content-['']",
"dark:bg-input/30",
className,
)}
{...props}
/>
);
}
2 changes: 2 additions & 0 deletions src/components/data-grid/data-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Plus } from "lucide-react";
import * as React from "react";
import { DataGridColumnHeader } from "@/components/data-grid/data-grid-column-header";
import { DataGridContextMenu } from "@/components/data-grid/data-grid-context-menu";
import { DataGridPasteDialog } from "@/components/data-grid/data-grid-paste-dialog";
import { DataGridRow } from "@/components/data-grid/data-grid-row";
import { DataGridSearch } from "@/components/data-grid/data-grid-search";
import type { useDataGrid } from "@/hooks/use-data-grid";
Expand Down Expand Up @@ -65,6 +66,7 @@ export function DataGrid<TData>({
>
{searchState && <DataGridSearch {...searchState} />}
<DataGridContextMenu table={table} />
<DataGridPasteDialog table={table} />
<div
role="grid"
aria-label="Data grid"
Expand Down
Loading