Skip to content

Commit 8e8058a

Browse files
committed
feat(editor): support pasting Excel data into database block (#9618)
close: BS-2338
1 parent 6feb4de commit 8e8058a

File tree

4 files changed

+184
-5
lines changed

4 files changed

+184
-5
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,6 @@ packages/frontend/core/public/static/templates
8484
# script
8585
af
8686
af.cmd
87+
88+
# AI agent memories
89+
memories.md

blocksuite/affine/data-view/src/view-presets/table/pc/controller/clipboard.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,35 @@ export class TableClipboardController implements ReactiveController {
8989
return;
9090
}
9191
if (tableSelection) {
92-
const json = await this.clipboard.readFromClipboard(clipboardData);
93-
const dataString = json[BLOCKSUITE_DATABASE_TABLE];
94-
if (!dataString) return;
95-
const jsonAreaData = JSON.parse(dataString) as JsonAreaData;
96-
pasteToCells(view, jsonAreaData, tableSelection);
92+
try {
93+
// First try to read internal format data
94+
const json = await this.clipboard.readFromClipboard(clipboardData);
95+
const dataString = json[BLOCKSUITE_DATABASE_TABLE];
96+
97+
if (dataString) {
98+
// If internal format data exists, use it
99+
const jsonAreaData = JSON.parse(dataString) as JsonAreaData;
100+
pasteToCells(view, jsonAreaData, tableSelection);
101+
return true;
102+
}
103+
} catch {
104+
// Ignore error when reading internal format, will fallback to plain text
105+
console.debug('No internal format data found, trying plain text');
106+
}
107+
108+
// Try reading plain text (possibly copied from Excel)
109+
const plainText = clipboardData.getData('text/plain');
110+
if (plainText) {
111+
// Split text by newlines and then by tabs for each line
112+
const rows = plainText
113+
.split(/\r?\n/)
114+
.map(line => line.split('\t').map(cell => cell.trim()))
115+
.filter(row => row.some(cell => cell !== '')); // Filter out empty rows
116+
117+
if (rows.length > 0) {
118+
pasteToCells(view, rows, tableSelection);
119+
}
120+
}
97121
}
98122

99123
return true;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { test } from '@affine-test/kit/playwright';
2+
import { openHomePage } from '@affine-test/kit/utils/load-page';
3+
import { waitForEditorLoad } from '@affine-test/kit/utils/page-logic';
4+
5+
import {
6+
initDatabaseWithRows,
7+
pasteExcelData,
8+
selectFirstCell,
9+
verifyCellContents,
10+
} from './utils';
11+
12+
test.describe('Database Clipboard Operations', () => {
13+
test('paste tab-separated data from Excel into database', async ({
14+
page,
15+
}) => {
16+
// Open the home page and wait for the editor to load
17+
await openHomePage(page);
18+
await waitForEditorLoad(page);
19+
20+
// Create a database block with two rows
21+
await initDatabaseWithRows(page, 2);
22+
23+
// Select the first cell and paste data
24+
await selectFirstCell(page);
25+
const mockExcelData = 'Cell 1A\tCell 1B\nCell 2A\tCell 2B';
26+
await pasteExcelData(page, mockExcelData);
27+
28+
// Verify cell contents
29+
await verifyCellContents(page, [
30+
'Cell 1A',
31+
'Cell 1B',
32+
'Cell 2A',
33+
'Cell 2B',
34+
]);
35+
});
36+
37+
test('handle empty cells when pasting tab-separated data', async ({
38+
page,
39+
}) => {
40+
// Open the home page and wait for the editor to load
41+
await openHomePage(page);
42+
await waitForEditorLoad(page);
43+
44+
// Create a database block with two rows
45+
await initDatabaseWithRows(page, 2);
46+
47+
// Select the first cell and paste data with empty cells
48+
await selectFirstCell(page);
49+
const mockExcelData = 'Cell 1A\t\nCell 2A\tCell 2B';
50+
await pasteExcelData(page, mockExcelData);
51+
52+
// Verify cell contents including empty cells
53+
await verifyCellContents(page, ['Cell 1A', '', 'Cell 2A', 'Cell 2B']);
54+
});
55+
56+
test('handle pasting data larger than selected area', async ({ page }) => {
57+
// Open the home page and wait for the editor to load
58+
await openHomePage(page);
59+
await waitForEditorLoad(page);
60+
61+
// Create a database block with one row
62+
await initDatabaseWithRows(page, 1);
63+
64+
// Select the first cell and paste data larger than table
65+
await selectFirstCell(page);
66+
const mockExcelData = 'Cell 1A\tCell 1B\nCell 2A\tCell 2B';
67+
await pasteExcelData(page, mockExcelData);
68+
69+
// Verify only the cells that exist are filled
70+
await verifyCellContents(page, ['Cell 1A', 'Cell 1B']);
71+
});
72+
});
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import {
2+
addDatabase,
3+
clickNewPageButton,
4+
} from '@affine-test/kit/utils/page-logic';
5+
import type { Page } from '@playwright/test';
6+
import { expect } from '@playwright/test';
7+
8+
/**
9+
* Create a new database block in the current page
10+
*/
11+
export async function createDatabaseBlock(page: Page) {
12+
await clickNewPageButton(page);
13+
await page.waitForTimeout(500);
14+
await page.keyboard.press('Enter');
15+
await addDatabase(page);
16+
}
17+
18+
/**
19+
* Initialize a database with specified number of rows
20+
*/
21+
export async function initDatabaseWithRows(page: Page, rowCount: number) {
22+
await createDatabaseBlock(page);
23+
for (let i = 0; i < rowCount; i++) {
24+
await addDatabaseRow(page);
25+
}
26+
}
27+
28+
/**
29+
* Add a new row to the database
30+
*/
31+
export async function addDatabaseRow(page: Page) {
32+
const addButton = page.locator('.data-view-table-group-add-row');
33+
await addButton.waitFor();
34+
await addButton.click();
35+
}
36+
37+
/**
38+
* Simulate pasting Excel data into database
39+
* @param page Playwright page object
40+
* @param data Tab-separated text data with newlines for rows
41+
*/
42+
export async function pasteExcelData(page: Page, data: string) {
43+
await page.evaluate(data => {
44+
const clipboardData = new DataTransfer();
45+
clipboardData.setData('text/plain', data);
46+
const pasteEvent = new ClipboardEvent('paste', {
47+
clipboardData,
48+
bubbles: true,
49+
cancelable: true,
50+
});
51+
document.activeElement?.dispatchEvent(pasteEvent);
52+
}, data);
53+
}
54+
55+
/**
56+
* Select the first cell in the database
57+
*/
58+
export async function selectFirstCell(page: Page) {
59+
const firstCell = page.locator('affine-database-cell-container').first();
60+
await firstCell.waitFor();
61+
await firstCell.click();
62+
}
63+
64+
/**
65+
* Verify the contents of multiple cells in sequence
66+
* @param page Playwright page object
67+
* @param expectedContents Array of expected cell contents in order
68+
*/
69+
export async function verifyCellContents(
70+
page: Page,
71+
expectedContents: string[]
72+
) {
73+
const cells = page.locator('affine-database-cell-container');
74+
for (let i = 0; i < expectedContents.length; i++) {
75+
const cell = cells.nth(i);
76+
await expect(cell.locator('uni-lit > *:first-child')).toHaveText(
77+
expectedContents[i]
78+
);
79+
}
80+
}

0 commit comments

Comments
 (0)