From c016f8e37e92873dd355c0d1d78b7629bb37f6ec Mon Sep 17 00:00:00 2001 From: zzj3720 <17165520+zzj3720@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:43:40 +0000 Subject: [PATCH] feat(editor): support linked-doc in rich-text column (#9634) close: BS-2345 --- .gitignore | 3 - .../src/properties/rich-text/cell-renderer.ts | 377 +++++++++++++----- .../src/properties/rich-text/define.ts | 24 +- .../e2e/blocksuite/database/clipboard.spec.ts | 39 +- .../e2e/blocksuite/database/rich-text.spec.ts | 99 +++++ .../e2e/blocksuite/database/utils.ts | 98 +++-- 6 files changed, 488 insertions(+), 152 deletions(-) create mode 100644 tests/affine-local/e2e/blocksuite/database/rich-text.spec.ts diff --git a/.gitignore b/.gitignore index a8b851052..73e51296a 100644 --- a/.gitignore +++ b/.gitignore @@ -84,6 +84,3 @@ packages/frontend/core/public/static/templates # script af af.cmd - -# AI agent memories -memories.md diff --git a/blocksuite/affine/block-database/src/properties/rich-text/cell-renderer.ts b/blocksuite/affine/block-database/src/properties/rich-text/cell-renderer.ts index eac5cbff5..2f4ea3fa0 100644 --- a/blocksuite/affine/block-database/src/properties/rich-text/cell-renderer.ts +++ b/blocksuite/affine/block-database/src/properties/rich-text/cell-renderer.ts @@ -1,10 +1,18 @@ -import { - type AffineInlineEditor, - DefaultInlineManagerExtension, - type RichText, +import type { + AffineInlineEditor, + RichText, } from '@blocksuite/affine-components/rich-text'; +import { DefaultInlineManagerExtension } from '@blocksuite/affine-components/rich-text'; +import type { RootBlockModel } from '@blocksuite/affine-model'; +import { + ParseDocUrlProvider, + TelemetryProvider, +} from '@blocksuite/affine-shared/services'; import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; -import { getViewportElement } from '@blocksuite/affine-shared/utils'; +import { + getViewportElement, + isValidUrl, +} from '@blocksuite/affine-shared/utils'; import { BaseCellRenderer, createFromBaseCellRenderer, @@ -12,8 +20,11 @@ import { } from '@blocksuite/data-view'; import { IS_MAC } from '@blocksuite/global/env'; import { assertExists } from '@blocksuite/global/utils'; +import type { DeltaInsert } from '@blocksuite/inline'; +import type { BlockSnapshot } from '@blocksuite/store'; import { Text } from '@blocksuite/store'; -import { css, nothing, type PropertyValues } from 'lit'; +import { computed, signal } from '@preact/signals-core'; +import { css, nothing } from 'lit'; import { query } from 'lit/decorators.js'; import { keyed } from 'lit/directives/keyed.js'; import { html } from 'lit/static-html.js'; @@ -23,9 +34,11 @@ import type { DatabaseBlockComponent } from '../../database-block.js'; import { richTextColumnModelConfig } from './define.js'; function toggleStyle( - inlineEditor: AffineInlineEditor, + inlineEditor: AffineInlineEditor | null, attrs: AffineTextAttributes ): void { + if (!inlineEditor) return; + const inlineRange = inlineEditor.getInlineRange(); if (!inlineRange) return; @@ -68,7 +81,113 @@ function toggleStyle( inlineEditor.syncInlineRange(); } -export class RichTextCell extends BaseCellRenderer { +abstract class BaseRichTextCell extends BaseCellRenderer { + static override styles = css` + affine-database-rich-text-cell, + affine-database-rich-text-cell-editing { + display: flex; + align-items: center; + width: 100%; + user-select: none; + } + + .affine-database-rich-text { + display: flex; + flex-direction: column; + justify-content: center; + width: 100%; + height: 100%; + outline: none; + font-size: var(--data-view-cell-text-size); + line-height: var(--data-view-cell-text-line-height); + word-break: break-all; + } + + .affine-database-rich-text v-line { + display: flex !important; + align-items: center; + height: 100%; + width: 100%; + } + + .affine-database-rich-text v-line > div { + flex-grow: 1; + } + + .data-view-header-area-icon { + height: max-content; + display: flex; + align-items: center; + margin-right: 8px; + padding: 2px; + border-radius: 4px; + margin-top: 2px; + background-color: var(--affine-background-secondary-color); + } + + .data-view-header-area-icon svg { + width: 14px; + height: 14px; + fill: var(--affine-icon-color); + color: var(--affine-icon-color); + } + `; + + get inlineEditor() { + return this.richText?.inlineEditor; + } + + get inlineManager() { + return this.view + .contextGet(HostContextKey) + ?.std.get(DefaultInlineManagerExtension.identifier); + } + + get service() { + return this.view + .contextGet(HostContextKey) + ?.std.getService('affine:database'); + } + + get topContenteditableElement() { + const databaseBlock = + this.closest('affine-database'); + return databaseBlock?.topContenteditableElement; + } + + get attributeRenderer() { + return this.inlineManager?.getRenderer(); + } + + get attributesSchema() { + return this.inlineManager?.getSchema(); + } + + get host() { + return this.view.contextGet(HostContextKey); + } + + @query('rich-text') + accessor richText!: RichText; + + @query('.affine-database-rich-text') + accessor _richTextElement!: HTMLElement; + + docId$ = signal(); + + isLinkedDoc$ = computed(() => false); + + linkedDocTitle$ = computed(() => { + if (!this.docId$.value) { + return this.value; + } + const doc = this.host?.std.workspace.getDoc(this.docId$.value); + const root = doc?.root as RootBlockModel; + return root.title; + }); +} + +export class RichTextCell extends BaseRichTextCell { static override styles = css` affine-database-rich-text-cell { display: flex; @@ -101,39 +220,6 @@ export class RichTextCell extends BaseCellRenderer { } `; - get attributeRenderer() { - return this.inlineManager?.getRenderer(); - } - - get attributesSchema() { - return this.inlineManager?.getSchema(); - } - - get inlineEditor() { - assertExists(this._richTextElement); - const inlineEditor = this._richTextElement.inlineEditor; - assertExists(inlineEditor); - return inlineEditor; - } - - get inlineManager() { - return this.view - .contextGet(HostContextKey) - ?.std.get(DefaultInlineManagerExtension.identifier); - } - - get service() { - return this.view - .contextGet(HostContextKey) - ?.std.getService('affine:database'); - } - - get topContenteditableElement() { - const databaseBlock = - this.closest('affine-database'); - return databaseBlock?.topContenteditableElement; - } - private changeUserSelectAccordToReadOnly() { if (this && this instanceof HTMLElement) { this.style.userSelect = this.readonly ? 'text' : 'none'; @@ -163,18 +249,9 @@ export class RichTextCell extends BaseCellRenderer { >` ); } - - override updated(changedProperties: PropertyValues) { - if (changedProperties.has('readonly')) { - this.changeUserSelectAccordToReadOnly(); - } - } - - @query('rich-text') - private accessor _richTextElement: RichText | null = null; } -export class RichTextCellEditing extends BaseCellRenderer { +export class RichTextCellEditing extends BaseRichTextCell { static override styles = css` affine-database-rich-text-cell-editing { display: flex; @@ -227,6 +304,7 @@ export class RichTextCellEditing extends BaseCellRenderer { } const inlineEditor = this.inlineEditor; + if (!inlineEditor) return; switch (event.key) { // bold ctrl+b @@ -234,7 +312,7 @@ export class RichTextCellEditing extends BaseCellRenderer { case 'b': if (event.metaKey || event.ctrlKey) { event.preventDefault(); - toggleStyle(this.inlineEditor, { bold: true }); + toggleStyle(inlineEditor, { bold: true }); } break; // italic ctrl+i @@ -242,7 +320,7 @@ export class RichTextCellEditing extends BaseCellRenderer { case 'i': if (event.metaKey || event.ctrlKey) { event.preventDefault(); - toggleStyle(this.inlineEditor, { italic: true }); + toggleStyle(inlineEditor, { italic: true }); } break; // underline ctrl+u @@ -250,7 +328,7 @@ export class RichTextCellEditing extends BaseCellRenderer { case 'u': if (event.metaKey || event.ctrlKey) { event.preventDefault(); - toggleStyle(this.inlineEditor, { underline: true }); + toggleStyle(inlineEditor, { underline: true }); } break; // strikethrough ctrl+shift+s @@ -293,42 +371,124 @@ export class RichTextCellEditing extends BaseCellRenderer { } }; - get attributeRenderer() { - return this.inlineManager?.getRenderer(); - } - - get attributesSchema() { - return this.inlineManager?.getSchema(); - } - - // eslint-disable-next-line sonarjs/no-identical-functions - get inlineEditor() { - assertExists(this._richTextElement); - const inlineEditor = this._richTextElement.inlineEditor; + private readonly _onCopy = (e: ClipboardEvent) => { + const inlineEditor = this.inlineEditor; assertExists(inlineEditor); - return inlineEditor; - } - // eslint-disable-next-line sonarjs/no-identical-functions - get inlineManager() { - return this.view - .contextGet(HostContextKey) - ?.std.get(DefaultInlineManagerExtension.identifier); - } + const inlineRange = inlineEditor.getInlineRange(); + if (!inlineRange) return; - // eslint-disable-next-line sonarjs/no-identical-functions - get service() { - return this.view - .contextGet(HostContextKey) - ?.std.getService('affine:database'); - } + const text = inlineEditor.yTextString.slice( + inlineRange.index, + inlineRange.index + inlineRange.length + ); - // eslint-disable-next-line sonarjs/no-identical-functions - get topContenteditableElement() { - const databaseBlock = - this.closest('affine-database'); - return databaseBlock?.topContenteditableElement; - } + e.clipboardData?.setData('text/plain', text); + e.preventDefault(); + e.stopPropagation(); + }; + + private readonly _onCut = (e: ClipboardEvent) => { + const inlineEditor = this.inlineEditor; + assertExists(inlineEditor); + + const inlineRange = inlineEditor.getInlineRange(); + if (!inlineRange) return; + + const text = inlineEditor.yTextString.slice( + inlineRange.index, + inlineRange.index + inlineRange.length + ); + inlineEditor.deleteText(inlineRange); + inlineEditor.setInlineRange({ + index: inlineRange.index, + length: 0, + }); + + e.clipboardData?.setData('text/plain', text); + e.preventDefault(); + e.stopPropagation(); + }; + + private readonly _onPaste = (e: ClipboardEvent) => { + const inlineEditor = this.inlineEditor; + if (!inlineEditor) return; + + const inlineRange = inlineEditor.getInlineRange(); + if (!inlineRange) return; + + if (e.clipboardData) { + try { + const getDeltas = (snapshot: BlockSnapshot): DeltaInsert[] => { + // @ts-expect-error FIXME: ts error + const text = snapshot.props?.text?.delta; + return text + ? [...text, ...(snapshot.children?.flatMap(getDeltas) ?? [])] + : snapshot.children?.flatMap(getDeltas); + }; + const snapshot = this.std?.clipboard?.readFromClipboard( + e.clipboardData + )['BLOCKSUITE/SNAPSHOT']; + const deltas = ( + JSON.parse(snapshot).snapshot.content as BlockSnapshot[] + ).flatMap(getDeltas); + deltas.forEach(delta => this.insertDelta(delta)); + return; + } catch { + // + } + } + const text = e.clipboardData + ?.getData('text/plain') + ?.replace(/\r?\n|\r/g, '\n'); + if (!text) return; + e.preventDefault(); + e.stopPropagation(); + if (isValidUrl(text)) { + const std = this.std; + const result = std?.getOptional(ParseDocUrlProvider)?.parseDocUrl(text); + if (result) { + const text = ' '; + inlineEditor.insertText(inlineRange, text, { + reference: { + type: 'LinkedPage', + pageId: result.docId, + params: { + blockIds: result.blockIds, + elementIds: result.elementIds, + mode: result.mode, + }, + }, + }); + inlineEditor.setInlineRange({ + index: inlineRange.index + text.length, + length: 0, + }); + + // Track when a linked doc is created in database rich-text column + std?.getOptional(TelemetryProvider)?.track('LinkedDocCreated', { + module: 'database rich-text cell', + type: 'paste', + segment: 'database', + parentFlavour: 'affine:database', + }); + } else { + inlineEditor.insertText(inlineRange, text, { + link: text, + }); + inlineEditor.setInlineRange({ + index: inlineRange.index + text.length, + length: 0, + }); + } + } else { + inlineEditor.insertText(inlineRange, text); + inlineEditor.setInlineRange({ + index: inlineRange.index + text.length, + length: 0, + }); + } + }; override connectedCallback() { super.connectedCallback(); @@ -341,7 +501,7 @@ export class RichTextCellEditing extends BaseCellRenderer { if (e.key === 'a' && (IS_MAC ? e.metaKey : e.ctrlKey)) { e.stopPropagation(); e.preventDefault(); - this.inlineEditor.selectAll(); + this.inlineEditor?.selectAll(); } }; this.addEventListener('keydown', selectAll); @@ -349,13 +509,32 @@ export class RichTextCellEditing extends BaseCellRenderer { } override firstUpdated() { - this._richTextElement?.updateComplete + this.richText?.updateComplete .then(() => { + const inlineEditor = this.inlineEditor; + if (!inlineEditor) return; + this.disposables.add( - this.inlineEditor.slots.keydown.on(this._handleKeyDown) + inlineEditor.slots.keydown.on(this._handleKeyDown) ); - this.inlineEditor.focusEnd(); + this.disposables.addFromEvent( + this._richTextElement!, + 'copy', + this._onCopy + ); + this.disposables.addFromEvent( + this._richTextElement!, + 'cut', + this._onCut + ); + this.disposables.addFromEvent( + this._richTextElement!, + 'paste', + this._onPaste + ); + + inlineEditor.focusEnd(); }) .catch(console.error); } @@ -377,8 +556,22 @@ export class RichTextCellEditing extends BaseCellRenderer { >`; } - @query('rich-text') - private accessor _richTextElement: RichText | null = null; + private get std() { + return this.view.contextGet(HostContextKey)?.std; + } + + insertDelta = (delta: DeltaInsert) => { + const inlineEditor = this.inlineEditor; + const range = inlineEditor?.getInlineRange(); + if (!range || !delta.insert) { + return; + } + inlineEditor?.insertText(range, delta.insert, delta.attributes); + inlineEditor?.setInlineRange({ + index: range.index + delta.insert.length, + length: 0, + }); + }; } declare global { diff --git a/blocksuite/affine/block-database/src/properties/rich-text/define.ts b/blocksuite/affine/block-database/src/properties/rich-text/define.ts index 8c2ad9e0e..4302bc635 100644 --- a/blocksuite/affine/block-database/src/properties/rich-text/define.ts +++ b/blocksuite/affine/block-database/src/properties/rich-text/define.ts @@ -1,6 +1,10 @@ +import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; import { propertyType, t } from '@blocksuite/data-view'; +import type { DeltaInsert } from '@blocksuite/inline'; import { Text } from '@blocksuite/store'; +import { HostContextKey } from '../../context/host-context.js'; +import { isLinkedDoc } from '../../utils/title-doc.js'; import { type RichTextCellType, toYText } from '../utils.js'; export const richTextColumnType = propertyType('rich-text'); @@ -16,7 +20,25 @@ export const richTextColumnModelConfig = value: new Text(value), }; }, - cellToJson: ({ value }) => value?.toString() ?? null, + cellToJson: ({ value, dataSource }) => { + const host = dataSource.contextGet(HostContextKey); + if (host) { + const collection = host.std.workspace; + const yText = toYText(value); + const deltas = yText.toDelta(); + const text = deltas + .map((delta: DeltaInsert) => { + if (isLinkedDoc(delta)) { + const linkedDocId = delta.attributes?.reference?.pageId as string; + return collection.getDoc(linkedDocId)?.meta?.title; + } + return delta.insert; + }) + .join(''); + return text; + } + return value?.toString() ?? null; + }, cellFromJson: ({ value }) => typeof value !== 'string' ? undefined : new Text(value), onUpdate: ({ value, callback }) => { diff --git a/tests/affine-local/e2e/blocksuite/database/clipboard.spec.ts b/tests/affine-local/e2e/blocksuite/database/clipboard.spec.ts index c3e3e2f84..4f45c1e40 100644 --- a/tests/affine-local/e2e/blocksuite/database/clipboard.spec.ts +++ b/tests/affine-local/e2e/blocksuite/database/clipboard.spec.ts @@ -1,11 +1,10 @@ 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, + addRows, + initDatabaseByOneStep, + pasteString, + selectCell, verifyCellContents, } from './utils'; @@ -14,16 +13,14 @@ test.describe('Database Clipboard Operations', () => { page, }) => { // Open the home page and wait for the editor to load - await openHomePage(page); - await waitForEditorLoad(page); - + await initDatabaseByOneStep(page); // Create a database block with two rows - await initDatabaseWithRows(page, 2); + await addRows(page, 2); // Select the first cell and paste data - await selectFirstCell(page); + await selectCell(page, 0, false); const mockExcelData = 'Cell 1A\tCell 1B\nCell 2A\tCell 2B'; - await pasteExcelData(page, mockExcelData); + await pasteString(page, mockExcelData); // Verify cell contents await verifyCellContents(page, [ @@ -38,16 +35,14 @@ test.describe('Database Clipboard Operations', () => { page, }) => { // Open the home page and wait for the editor to load - await openHomePage(page); - await waitForEditorLoad(page); - + await initDatabaseByOneStep(page); // Create a database block with two rows - await initDatabaseWithRows(page, 2); + await addRows(page, 2); // Select the first cell and paste data with empty cells - await selectFirstCell(page); + await selectCell(page, 0, false); const mockExcelData = 'Cell 1A\t\nCell 2A\tCell 2B'; - await pasteExcelData(page, mockExcelData); + await pasteString(page, mockExcelData); // Verify cell contents including empty cells await verifyCellContents(page, ['Cell 1A', '', 'Cell 2A', 'Cell 2B']); @@ -55,16 +50,14 @@ test.describe('Database Clipboard Operations', () => { 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); - + await initDatabaseByOneStep(page); // Create a database block with one row - await initDatabaseWithRows(page, 1); + await addRows(page, 1); // Select the first cell and paste data larger than table - await selectFirstCell(page); + await selectCell(page, 0, false); const mockExcelData = 'Cell 1A\tCell 1B\nCell 2A\tCell 2B'; - await pasteExcelData(page, mockExcelData); + await pasteString(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/rich-text.spec.ts b/tests/affine-local/e2e/blocksuite/database/rich-text.spec.ts new file mode 100644 index 000000000..d67bdaa8e --- /dev/null +++ b/tests/affine-local/e2e/blocksuite/database/rich-text.spec.ts @@ -0,0 +1,99 @@ +import { test } from '@affine-test/kit/playwright'; +import { openHomePage } from '@affine-test/kit/utils/load-page'; +import { + clickNewPageButton, + waitForEditorLoad, +} from '@affine-test/kit/utils/page-logic'; +import { expect } from '@playwright/test'; + +import { + addColumn, + createDatabaseBlock, + createNewPage, + gotoContentFromTitle, + selectCell, +} from './utils'; + +test.describe('Database Rich Text Column', () => { + test('paste document link into rich text cell', async ({ page }) => { + // Step 1: Open home page + await openHomePage(page); + await waitForEditorLoad(page); + + // Step 2: Create a new page + await createNewPage(page); + + await gotoContentFromTitle(page); + + // Step 3: Create a database in the page + await createDatabaseBlock(page); + + // Step 4: Add a text column + await addColumn(page, 'Text'); + + // Step 5: Create a new page to get its link + await clickNewPageButton(page, 'Test Page'); + const pageUrl = page.url(); + + // Step 6: Go back to database page + await page.goBack(); + await waitForEditorLoad(page); + + // Step 7: Select and edit the rich text cell + const richTextCell = await selectCell(page, 2); + + // Step 8: Paste the document link + await page.evaluate(url => { + const clipboardData = new DataTransfer(); + clipboardData.setData('text/plain', url); + const pasteEvent = new ClipboardEvent('paste', { + clipboardData, + bubbles: true, + cancelable: true, + }); + document.activeElement?.dispatchEvent(pasteEvent); + }, pageUrl); + + // Step 9: Verify the result + const referenceTitle = richTextCell.locator('.affine-reference-title'); + await expect(referenceTitle).toBeVisible(); + await expect(referenceTitle).toContainText('Test Page'); + }); + + test('add document link via @ in rich text cell', async ({ page }) => { + // Step 1: Open home page + await openHomePage(page); + await waitForEditorLoad(page); + + // Step 2: Create a new page + await createNewPage(page); + await gotoContentFromTitle(page); + + // Step 3: Create a database in the page + await createDatabaseBlock(page); + + // Step 4: Add a text column + await addColumn(page, 'Text'); + + // Step 5: Create a new page as reference target + await clickNewPageButton(page, 'Reference Target'); + await page.goBack(); + await waitForEditorLoad(page); + + // Step 6: Select and edit the rich text cell + const richTextCell = await selectCell(page, 2); + await richTextCell.click(); + await page.keyboard.type('@'); + + // Step 7: Wait for reference picker and select the page + const linkedDocPopover = page.locator('.linked-doc-popover'); + await expect(linkedDocPopover).toBeVisible(); + const targetPage = linkedDocPopover.getByText('Reference Target'); + await targetPage.click(); + + // Step 8: Verify the result + const referenceTitle = richTextCell.locator('.affine-reference-title'); + await expect(referenceTitle).toBeVisible(); + await expect(referenceTitle).toContainText('Reference Target'); + }); +}); diff --git a/tests/affine-local/e2e/blocksuite/database/utils.ts b/tests/affine-local/e2e/blocksuite/database/utils.ts index bce4152ce..0f49fe135 100644 --- a/tests/affine-local/e2e/blocksuite/database/utils.ts +++ b/tests/affine-local/e2e/blocksuite/database/utils.ts @@ -1,45 +1,36 @@ +import { openHomePage } from '@affine-test/kit/utils/load-page'; import { addDatabase, clickNewPageButton, + waitForEditorLoad, } 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) { +export async function createNewPage(page: Page) { await clickNewPageButton(page); - await page.waitForTimeout(500); +} + +export const gotoContentFromTitle = async (page: Page) => { await page.keyboard.press('Enter'); +}; + +export async function createDatabaseBlock(page: Page) { await addDatabase(page); } -/** - * Initialize a database with specified number of rows - */ -export async function initDatabaseWithRows(page: Page, rowCount: number) { - await createDatabaseBlock(page); +export async function addRows(page: Page, rowCount: number) { 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) { +export async function pasteString(page: Page, data: string) { await page.evaluate(data => { const clipboardData = new DataTransfer(); clipboardData.setData('text/plain', data); @@ -48,24 +39,25 @@ export async function pasteExcelData(page: Page, data: string) { bubbles: true, cancelable: true, }); - document.activeElement?.dispatchEvent(pasteEvent); + const activeElement = document.activeElement; + if (activeElement) { + pasteEvent.preventDefault(); + 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(); +export async function selectCell(page: Page, nth: number, editing = true) { + const firstCell = page.locator('affine-database-cell-container').nth(nth); + // First click for focus + await firstCell.click({ delay: 100 }); + // Second click for edit mode + if (editing) { + await firstCell.click({ delay: 100 }); + } + return firstCell; } -/** - * 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[] @@ -78,3 +70,43 @@ export async function verifyCellContents( ); } } + +export async function selectColumnType(page: Page, columnType: string) { + const typeMenu = page.locator('affine-menu').getByText('Type'); + await page.waitForTimeout(100); + await typeMenu.hover(); + await page.waitForTimeout(100); + await page.keyboard.type(columnType); + await page.waitForTimeout(100); + await page.keyboard.press('ArrowDown'); + await page.waitForTimeout(100); + await page.keyboard.press('Enter'); + await page.waitForTimeout(100); +} + +export async function addColumn(page: Page, type: string) { + await clickAddColumnButton(page); + await selectColumnType(page, type); +} + +export async function clickAddColumnButton(page: Page) { + const addColumnButton = page.locator('.header-add-column-button'); + await addColumnButton.click(); +} + +export async function changeColumnType( + page: Page, + columnIndex: number, + columnType: string +) { + const header = page.locator('affine-database-header-column').nth(columnIndex); + await header.click(); + await selectColumnType(page, columnType); +} +export const initDatabaseByOneStep = async (page: Page) => { + await openHomePage(page); + await createNewPage(page); + await waitForEditorLoad(page); + await gotoContentFromTitle(page); + await createDatabaseBlock(page); +};