Skip to content

feat(datagrid): paste cells from clipboard (Cmd/Ctrl+V) - #612

Merged
debba merged 6 commits into
TabularisDB:mainfrom
ymadd:feat/datagrid-paste
Aug 15, 2026
Merged

feat(datagrid): paste cells from clipboard (Cmd/Ctrl+V)#612
debba merged 6 commits into
TabularisDB:mainfrom
ymadd:feat/datagrid-paste

Conversation

@ymadd

@ymadd ymadd commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Closes #611

Summary

The data grid supports copying cells/rows/ranges but not pasting. This PR adds spreadsheet-style paste as staged edits — pasted values go through the existing pending-changes flow (apply/rollback), never directly to the database.

Behavior

  • Cmd/Ctrl+V pastes at the selection, in priority order: cell range top-left → right-clicked cell (context-menu "Paste") → top of row selection → focused cell.
  • Parsing: tab-separated cells win (spreadsheet convention). Multi-line text without tabs is parsed as CSV with double-quote escaping, preferring the configured CSV delimiter — so the grid's own copy formats (comma/semicolon/pipe) round-trip. A single line without tabs is always one value, so free text like hello, world lands in one cell.
  • A leading header row is dropped only when it matches the grid's column names positionally from the paste anchor (round-trips the "export column names" option without swallowing external data that merely mentions column names).
  • A single copied value fills the whole selected range / selected rows.
  • The matrix is clipped at the grid edges. Existing rows require an identifiable row key (keyless grids: only pending-insertion rows accept a paste). Aliases and computed result columns are skipped — same guard as inline editing.
  • Pasting a cell's original value back clears its pending change, same as inline editing.

Prerequisite fix (first commit)

handlePendingChange / handlePendingInsertionChange computed the next pending state from a tabsRef snapshot; with N staged cells in one tick, React batches the updates and only the last cell survived. updateTab now also accepts an updater function and both handlers compute inside it. Multi-cell paste is the first caller that hits this, but it hardens every rapid-succession staging path.

Known limitations

  • The grid's CSV copy output does not quote embedded delimiters/newlines (rowToCSV), so values containing the delimiter don't survive a copy→paste round-trip. Robust CSV serialization is follow-up material.
  • Cells containing quoted newlines (Excel multi-line cells) are split on the newline before quote handling.
  • A single-line, no-tab copy of one row (headers off) pastes as one value by design — single-line splitting can't be distinguished from free text with commas.
  • NULL copies as the literal string null, so pasting it into another cell stages that string, not SQL NULL.
  • Rows are not auto-created on overflow: pasting more rows than exist below the anchor clips at the last row (add pending-insertion rows first).

Testing

  • tests/utils/dataGrid.test.ts: 23 new cases for parsePasteMatrix / stripHeaderRow / computePasteTargets (TSV/CSV/quotes/delimiter detection/positional header stripping/range fill/clipping).
  • Manually verified against a MySQL database: range/row/single-cell paste, single-value fill, header round-trip, alias-column skip, keyless fallback, toast counts.
  • tsc --noEmit, eslint, and the vitest suite are green (the 3 failing files on main — ThemeProvider/SettingsProvider/useSidebarResize — fail identically without this change).

i18n

3 new keys (pasteCells, pastedCells, pasteNotEditable) added to all 11 locales.

@kilo-code-bot

kilo-code-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (18 files)
  • src/components/ui/DataGrid.tsx
  • src/contexts/EditorContext.ts
  • src/contexts/EditorProvider.tsx
  • src/i18n/locales/de.json
  • src/i18n/locales/en.json
  • src/i18n/locales/es.json
  • src/i18n/locales/fr.json
  • src/i18n/locales/it.json
  • src/i18n/locales/ja.json
  • src/i18n/locales/ko.json
  • src/i18n/locales/pt-BR.json
  • src/i18n/locales/ru.json
  • src/i18n/locales/tl.json
  • src/i18n/locales/zh.json
  • src/pages/Editor.tsx
  • src/utils/dataGrid.ts
  • src/utils/editor.ts
  • tests/utils/dataGrid.test.ts
Previous Review Summary (commit 65e772b)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 65e772b)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (18 files)
  • src/components/ui/DataGrid.tsx
  • src/contexts/EditorContext.ts
  • src/contexts/EditorProvider.tsx
  • src/i18n/locales/de.json
  • src/i18n/locales/en.json
  • src/i18n/locales/es.json
  • src/i18n/locales/fr.json
  • src/i18n/locales/it.json
  • src/i18n/locales/ja.json
  • src/i18n/locales/ko.json
  • src/i18n/locales/pt-BR.json
  • src/i18n/locales/ru.json
  • src/i18n/locales/tl.json
  • src/i18n/locales/zh.json
  • src/pages/Editor.tsx
  • src/utils/dataGrid.ts
  • src/utils/editor.ts
  • tests/utils/dataGrid.test.ts

Reviewed by glm-5.2 · Input: 53.5K · Output: 8.5K · Cached: 553.1K

@debba

debba commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Hey @ymadd, thanks for this one, really solid work.

I went through it locally today: checked out the branch, ran the full suite (3552 tests green, including your 23 new paste cases), tsc and eslint clean, and tested the paste flows by hand against a SQLite db (range paste, header round-trip, keyless table, alias columns, quoted CSV, and the no-op detection when you paste the original value back on its own cell). Everything behaved exactly as described in the PR. The functional update fix on updateTab is a nice catch too, the stale snapshot problem with multi-cell staging is real and this hardens it properly.

A few things before merging:

  1. The branch is quite behind main by now and there's a conflict in DataGrid.tsx, so it needs a rebase. This is the only real blocker.

  2. Paste skips the blob/JSON routing that inline editing does. Inline editing opens the sidebar for blob cells and the viewer for JSON, while paste stages a raw string into any physical column, blobs included, and you only find out at apply time. Fine for a first version, but I'd like a follow-up that skips blob columns the same way aliases are skipped.

  3. The "single value + row selection" fill writes into every column, primary key included. Pasting 42 with 3 rows selected stages a PK change on all of them. It's coherent with spreadsheet behavior but it's the sharpest edge of the PR. I would exclude PK columns from the row-fill case, or at least call it out somewhere.

  4. Minor: a clipboard read failure only goes to console.error. A toast would be more consistent with the rest of the grid.

  5. Also minor: right-click paste ignores the clicked cell when a cell range is active (the range top-left wins). It's documented in the code comment so I'm fine with it, just noting it for the record.

Rebase it and I'm happy to approve. Points 2 and 3 can be follow-ups.

ymadd added 3 commits August 13, 2026 20:03
handlePendingChange / handlePendingInsertionChange computed the next
pending state from a tabsRef snapshot taken before the update. When many
cells are staged in the same tick (e.g. a multi-cell clipboard paste),
every call reads the same stale snapshot and React batches the setState
calls, so only the last cell's change survives.

updateTab now also accepts an updater function ((tab) => partial) and
both handlers compute the next pending state inside it, so rapid
successive updates compose instead of clobbering each other.
Copying cells has been supported for a while, but there was no way to
paste back into the grid. This adds spreadsheet-style paste as staged
edits (pending changes), applied via the existing commit/rollback flow:

- Cmd/Ctrl+V pastes at the selection: the cell range's top-left, the
  top of the row selection, or the focused cell. A context-menu Paste
  entry pastes at the right-clicked cell.
- Tab-separated cells win (spreadsheet convention). Multi-line text
  without tabs is parsed as CSV with double-quote escaping, preferring
  the configured CSV delimiter, so the grid's own copy formats
  (comma/semicolon/pipe) round-trip. A single line without tabs is
  always one value, so free text like "hello, world" lands in one cell.
- A leading header row is dropped when every cell matches a column name
  (round-trip of the "export column names" option).
- A single copied value fills the whole selected range / selected rows.
- The paste matrix is clipped at the grid edges; existing rows require
  an identifiable key (insertion rows accept a paste regardless).
- Pasting a cell's original value back clears its pending change, same
  as inline editing.
- Exclude primary key columns from the single-value row-selection fill:
  a whole-row fill overwriting row identities is never what the user
  meant. Explicitly selected cells (range / focused cell) still accept
  PK values.
- Skip database-generated columns on paste, matching the inline-editing
  guard that main gained since this branch was cut.
- Surface clipboard read failures as an error toast instead of only
  console.error.
@ymadd
ymadd force-pushed the feat/datagrid-paste branch from 65e772b to ef57700 Compare August 14, 2026 13:30
@ymadd

ymadd commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review @debba — much appreciated, especially the hands-on testing!

All addressed and pushed:

  • (1) Rebased onto main. The DataGrid.tsx conflict was main's new generated-columns guard — while resolving it I also made paste skip generated columns and un-revealed masked cells, matching the inline-editing guards that landed on main since this branch was cut.
  • (3) Agreed, fixed. Primary key columns are now excluded from the single-value row-selection fill. The exclusion derives from column metadata's is_pk (not the row-identity fallback), so keyless tables still fill normally. Explicitly selected cells (range / focused cell) and multi-cell matrix pastes still accept PK values.
  • (4) Done. Clipboard read failures now surface an error toast (dataGrid.pasteReadFailed, all 11 locales).
  • (2) Will file a follow-up issue for skipping BLOB columns once this merges, and pick it up myself.
  • (5) 👍 Leaving as documented.

Re-verified after the rebase: full suite green (the 3 suites that fail to load do so on main as well), tsc/eslint clean, and re-tested the paste flows by hand against a MySQL table including the new PK-exclusion and keyless fill paths.

@debba

debba commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

@ymadd Do you think it would make sense to support pasting as new rows as well?

One possible approach would be to route the same Paste command based on the clipboard shape: partial matrices would keep updating existing cells, while full-width rows could become staged insertions. Alternatively, a separate explicit “Paste as new rows” action would avoid ambiguous cases.

Would you prefer to explore this within the current implementation or in a follow-up PR? If a follow-up sounds better, I’m happy to merge this PR as it is in the meantime.

@ymadd

ymadd commented Aug 15, 2026

Copy link
Copy Markdown
Contributor Author

Great idea — I'd take that on, but as a follow-up PR with an explicit "Paste as new rows" action in the context menu, rather than routing Cmd/Ctrl+V by clipboard shape. Shape alone isn't a reliable signal of intent: a full-width matrix can still be meant to update existing rows, while a perfectly valid insertion may omit generated, auto-increment, or defaulted columns. Keeping Cmd/Ctrl+V as an update operation keeps it predictable, and the explicit action gives the append case a clear home — still staged through the pending-changes flow.

It also crosses the current DataGrid/Editor boundary: the grid can edit existing pending-insertion rows and trigger the creation of a single one via onDuplicateRow, but creating N insertion rows from a pasted matrix needs a new bulk callback owned by the editor state — easier to review on its own.

So yes please — happy to have this merged as is. I'll file the follow-up issues right after (paste-as-new-rows, plus the BLOB-column guard you flagged) and pick both up.

@debba

debba commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Merging it right now .

@debba
debba merged commit 5b1abe3 into TabularisDB:main Aug 15, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat]: Paste cells from clipboard into the data grid

2 participants