From 8e8058a44c30859351025f2af3f9bf11712f8aad Mon Sep 17 00:00:00 2001 From: zzj3720 <17165520+zzj3720@users.noreply.github.com> Date: Thu, 9 Jan 2025 14:35:18 +0000 Subject: [PATCH] feat(editor): support pasting Excel data into database block (#9618) close: BS-2338 --- .gitignore | 3 + .../table/pc/controller/clipboard.ts | 34 ++++++-- .../e2e/blocksuite/database/clipboard.spec.ts | 72 +++++++++++++++++ .../e2e/blocksuite/database/utils.ts | 80 +++++++++++++++++++ 4 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 tests/affine-local/e2e/blocksuite/database/clipboard.spec.ts create mode 100644 tests/affine-local/e2e/blocksuite/database/utils.ts diff --git a/.gitignore b/.gitignore index 73e51296a2..a8b851052e 100644 --- a/.gitignore +++ b/.gitignore @@ -84,3 +84,6 @@ packages/frontend/core/public/static/templates # script af af.cmd + +# AI agent memories +memories.md diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc/controller/clipboard.ts b/blocksuite/affine/data-view/src/view-presets/table/pc/controller/clipboard.ts index a1b3bc520a..f80a8c85ef 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc/controller/clipboard.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc/controller/clipboard.ts @@ -89,11 +89,35 @@ export class TableClipboardController implements ReactiveController { return; } if (tableSelection) { - const json = await this.clipboard.readFromClipboard(clipboardData); - const dataString = json[BLOCKSUITE_DATABASE_TABLE]; - if (!dataString) return; - const jsonAreaData = JSON.parse(dataString) as JsonAreaData; - pasteToCells(view, jsonAreaData, tableSelection); + try { + // First try to read internal format data + const json = await this.clipboard.readFromClipboard(clipboardData); + const dataString = json[BLOCKSUITE_DATABASE_TABLE]; + + if (dataString) { + // If internal format data exists, use it + const jsonAreaData = JSON.parse(dataString) as JsonAreaData; + pasteToCells(view, jsonAreaData, tableSelection); + return true; + } + } catch { + // Ignore error when reading internal format, will fallback to plain text + console.debug('No internal format data found, trying plain text'); + } + + // Try reading plain text (possibly copied from Excel) + const plainText = clipboardData.getData('text/plain'); + if (plainText) { + // Split text by newlines and then by tabs for each line + const rows = plainText + .split(/\r?\n/) + .map(line => line.split('\t').map(cell => cell.trim())) + .filter(row => row.some(cell => cell !== '')); // Filter out empty rows + + if (rows.length > 0) { + pasteToCells(view, rows, tableSelection); + } + } } return true; diff --git a/tests/affine-local/e2e/blocksuite/database/clipboard.spec.ts b/tests/affine-local/e2e/blocksuite/database/clipboard.spec.ts new file mode 100644 index 0000000000..c3e3e2f841 --- /dev/null +++ b/tests/affine-local/e2e/blocksuite/database/clipboard.spec.ts @@ -0,0 +1,72 @@ +import { test } from '@affine-test/kit/playwright'; +import { openHomePage } from '@affine-test/kit/utils/load-page'; +import { waitForEditorLoad } from '@affine-test/kit/utils/page-logic'; + +import { + initDatabaseWithRows, + pasteExcelData, + selectFirstCell, + verifyCellContents, +} from './utils'; + +test.describe('Database Clipboard Operations', () => { + test('paste tab-separated data from Excel into database', async ({ + page, + }) => { + // Open the home page and wait for the editor to load + await openHomePage(page); + await waitForEditorLoad(page); + + // Create a database block with two rows + await initDatabaseWithRows(page, 2); + + // Select the first cell and paste data + await selectFirstCell(page); + const mockExcelData = 'Cell 1A\tCell 1B\nCell 2A\tCell 2B'; + await pasteExcelData(page, mockExcelData); + + // Verify cell contents + await verifyCellContents(page, [ + 'Cell 1A', + 'Cell 1B', + 'Cell 2A', + 'Cell 2B', + ]); + }); + + test('handle empty cells when pasting tab-separated data', async ({ + page, + }) => { + // Open the home page and wait for the editor to load + await openHomePage(page); + await waitForEditorLoad(page); + + // Create a database block with two rows + await initDatabaseWithRows(page, 2); + + // Select the first cell and paste data with empty cells + await selectFirstCell(page); + const mockExcelData = 'Cell 1A\t\nCell 2A\tCell 2B'; + await pasteExcelData(page, mockExcelData); + + // Verify cell contents including empty cells + await verifyCellContents(page, ['Cell 1A', '', 'Cell 2A', 'Cell 2B']); + }); + + test('handle pasting data larger than selected area', async ({ page }) => { + // Open the home page and wait for the editor to load + await openHomePage(page); + await waitForEditorLoad(page); + + // Create a database block with one row + await initDatabaseWithRows(page, 1); + + // Select the first cell and paste data larger than table + await selectFirstCell(page); + const mockExcelData = 'Cell 1A\tCell 1B\nCell 2A\tCell 2B'; + await pasteExcelData(page, mockExcelData); + + // Verify only the cells that exist are filled + await verifyCellContents(page, ['Cell 1A', 'Cell 1B']); + }); +}); diff --git a/tests/affine-local/e2e/blocksuite/database/utils.ts b/tests/affine-local/e2e/blocksuite/database/utils.ts new file mode 100644 index 0000000000..bce4152ce1 --- /dev/null +++ b/tests/affine-local/e2e/blocksuite/database/utils.ts @@ -0,0 +1,80 @@ +import { + addDatabase, + clickNewPageButton, +} from '@affine-test/kit/utils/page-logic'; +import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; + +/** + * Create a new database block in the current page + */ +export async function createDatabaseBlock(page: Page) { + await clickNewPageButton(page); + await page.waitForTimeout(500); + await page.keyboard.press('Enter'); + await addDatabase(page); +} + +/** + * Initialize a database with specified number of rows + */ +export async function initDatabaseWithRows(page: Page, rowCount: number) { + await createDatabaseBlock(page); + for (let i = 0; i < rowCount; i++) { + await addDatabaseRow(page); + } +} + +/** + * Add a new row to the database + */ +export async function addDatabaseRow(page: Page) { + const addButton = page.locator('.data-view-table-group-add-row'); + await addButton.waitFor(); + await addButton.click(); +} + +/** + * Simulate pasting Excel data into database + * @param page Playwright page object + * @param data Tab-separated text data with newlines for rows + */ +export async function pasteExcelData(page: Page, data: string) { + await page.evaluate(data => { + const clipboardData = new DataTransfer(); + clipboardData.setData('text/plain', data); + const pasteEvent = new ClipboardEvent('paste', { + clipboardData, + bubbles: true, + cancelable: true, + }); + document.activeElement?.dispatchEvent(pasteEvent); + }, data); +} + +/** + * Select the first cell in the database + */ +export async function selectFirstCell(page: Page) { + const firstCell = page.locator('affine-database-cell-container').first(); + await firstCell.waitFor(); + await firstCell.click(); +} + +/** + * Verify the contents of multiple cells in sequence + * @param page Playwright page object + * @param expectedContents Array of expected cell contents in order + */ +export async function verifyCellContents( + page: Page, + expectedContents: string[] +) { + const cells = page.locator('affine-database-cell-container'); + for (let i = 0; i < expectedContents.length; i++) { + const cell = cells.nth(i); + await expect(cell.locator('uni-lit > *:first-child')).toHaveText( + expectedContents[i] + ); + } +}