From 362f89b669683c5e4309512f2a841204b98a0757 Mon Sep 17 00:00:00 2001 From: fundon Date: Tue, 29 Apr 2025 01:00:56 +0000 Subject: [PATCH] feat(editor): adjust attachment block UI (#11763) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes: [BS-3143](https://linear.app/affine-design/issue/BS-3143/更新-attachment-错误样式) Closes: [BS-3341](https://linear.app/affine-design/issue/BS-3341/attachment-select状态ui调整) ## Summary by CodeRabbit - **New Features** - Improved attachment block with unified reactive state handling for loading, errors, and downloads. - Enhanced card layouts with modular rendering and dynamic buttons based on state. - **Bug Fixes** - Fixed UI state for "Embed view" action, now correctly disabled when not embedded. - Resolved attachment card styling issues and text overflow with new utility classes. - **Refactor** - Streamlined attachment block rendering and state management for better performance and maintainability. - Updated toolbar configuration for consistent parameter naming. - Simplified embedded and open methods for improved clarity. - Made internal functions private to reduce exported API surface. - **Chores** - Removed unused icon export and legacy upload tracking functions. - Updated attachment utilities to use reactive state, added data refresh, and improved error handling. - Refined image dimension handling for consistent resizing behavior. - Improved test selectors to target focused attachment containers for better reliability. - Refactored CSS to separate container and card styling and adopt theme-based colors consistently. --- .../blocks/attachment/src/attachment-block.ts | 306 +++++++++++++----- .../blocks/attachment/src/configs/toolbar.ts | 14 +- .../affine/blocks/attachment/src/embed.ts | 2 +- .../affine/blocks/attachment/src/styles.ts | 94 +++--- .../affine/blocks/attachment/src/utils.ts | 171 +++------- blocksuite/affine/blocks/image/src/utils.ts | 10 +- .../affine/components/src/icons/text.ts | 5 - .../e2e/attachment-preview.spec.ts | 4 +- tests/blocksuite/e2e/attachment.spec.ts | 2 +- 9 files changed, 336 insertions(+), 272 deletions(-) diff --git a/blocksuite/affine/blocks/attachment/src/attachment-block.ts b/blocksuite/affine/blocks/attachment/src/attachment-block.ts index 2640557ba9..b4543f30fb 100644 --- a/blocksuite/affine/blocks/attachment/src/attachment-block.ts +++ b/blocksuite/affine/blocks/attachment/src/attachment-block.ts @@ -1,9 +1,9 @@ import { getEmbedCardIcons } from '@blocksuite/affine-block-embed'; -import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption'; import { - AttachmentIcon16, - getAttachmentFileIcon, -} from '@blocksuite/affine-components/icons'; + CaptionedBlockComponent, + SelectedStyle, +} from '@blocksuite/affine-components/caption'; +import { getAttachmentFileIcon } from '@blocksuite/affine-components/icons'; import { Peekable } from '@blocksuite/affine-components/peek'; import { toast } from '@blocksuite/affine-components/toast'; import { @@ -15,17 +15,22 @@ import { ThemeProvider, } from '@blocksuite/affine-shared/services'; import { humanFileSize } from '@blocksuite/affine-shared/utils'; +import { AttachmentIcon, ResetIcon, WarningIcon } from '@blocksuite/icons/lit'; import { BlockSelection } from '@blocksuite/std'; import { Slice } from '@blocksuite/store'; -import { html } from 'lit'; +import { type BlobState } from '@blocksuite/sync'; +import { effect, signal } from '@preact/signals-core'; +import { html, type TemplateResult } from 'lit'; import { property } from 'lit/decorators.js'; -import { classMap } from 'lit/directives/class-map.js'; +import { choose } from 'lit/directives/choose.js'; +import { type ClassInfo, classMap } from 'lit/directives/class-map.js'; import { styleMap } from 'lit/directives/style-map.js'; +import { when } from 'lit/directives/when.js'; import { AttachmentEmbedProvider } from './embed'; import { styles } from './styles'; -import { checkAttachmentBlob, downloadAttachmentBlob } from './utils'; - +import { downloadAttachmentBlob, refreshData } from './utils'; +type State = 'loading' | 'uploading' | 'warning' | 'oversize' | 'none'; @Peekable({ enableOn: ({ model }: AttachmentBlockComponent) => { return !model.doc.readonly && model.props.type.endsWith('pdf'); @@ -36,6 +41,8 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent>({}); + protected containerStyleMap = styleMap({ position: 'relative', width: '100%', @@ -63,26 +70,45 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent { - return this.std - .get(AttachmentEmbedProvider) - .embedded(this.model, this._maxFileSize); + return ( + Boolean(this.blobUrl) && + this.std + .get(AttachmentEmbedProvider) + .embedded(this.model, this._maxFileSize) + ); }; open = () => { - if (!this.blobUrl) { - return; - } - window.open(this.blobUrl, '_blank'); + const blobUrl = this.blobUrl; + if (!blobUrl) return; + window.open(blobUrl, '_blank'); }; refreshData = () => { - checkAttachmentBlob(this).catch(console.error); + refreshData(this.std, this).catch(console.error); + }; + + updateBlobState(state: Partial) { + this.blobState$.value = { ...this.blobState$.value, ...state }; + } + + determineState = ( + loading: boolean, + uploading: boolean, + overSize: boolean, + error: boolean + ): State => { + if (overSize) return 'oversize'; + if (error) return 'warning'; + if (uploading) return 'uploading'; + if (loading) return 'loading'; + return 'none'; }; protected get embedView() { return this.std .get(AttachmentEmbedProvider) - .render(this.model, this.blobUrl, this._maxFileSize); + .render(this.model, this.blobUrl ?? undefined, this._maxFileSize); } private _selectBlock() { @@ -96,9 +122,30 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent { + const blobId = this.model.props.sourceId$.value; + if (!blobId) return; + + const blobState$ = this.std.store.blobSync.blobState$(blobId); + if (!blobState$) return; + + const subscription = blobState$.subscribe(state => { + if (state.overSize || state.errorMessage) { + state.uploading = false; + state.downloading = false; + } + + this.updateBlobState(state); + }); + + return () => subscription.unsubscribe(); + }) + ); if (!this.model.props.style) { this.doc.withoutTransact(() => { @@ -107,27 +154,12 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent { - if (key === 'sourceId') { - // Reset the blob url when the sourceId is changed - if (this.blobUrl) { - URL.revokeObjectURL(this.blobUrl); - this.blobUrl = undefined; - } - this.refreshData(); - } - }); - - // Workaround for https://github.com/toeverything/blocksuite/issues/4724 - this.disposables.add( - this.std.get(ThemeProvider).theme$.subscribe(() => this.requestUpdate()) - ); } override disconnectedCallback() { - if (this.blobUrl) { - URL.revokeObjectURL(this.blobUrl); + const blobUrl = this.blobUrl; + if (blobUrl) { + URL.revokeObjectURL(blobUrl); } super.disconnectedCallback(); } @@ -148,71 +180,173 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent { + return null; + }; + + protected renderReloadButton = () => { + return html` + + `; + }; + + protected renderWithHorizontal( + classInfo: ClassInfo, + icon: TemplateResult, + title: string, + description: string, + kind: TemplateResult, + state: State + ) { + return html`
+
+
+
${icon}
+ +
+ ${title} +
+
+ +
+ + ${choose(state, [ + ['oversize', this.renderUpgradeButton], + ['warning', this.renderReloadButton], + ])} +
+
+ +
${kind}
+
`; + } + + protected renderWithVertical( + classInfo: ClassInfo, + icon: TemplateResult, + title: string, + description: string, + kind: TemplateResult, + state?: State + ) { + return html`
+
+
+
${icon}
+ +
+ ${title} +
+
+ + +
+ +
+ ${kind} + ${choose(state, [ + ['oversize', this.renderUpgradeButton], + ['warning', this.renderReloadButton], + ])} +
+
`; + } + + protected renderCard = () => { const { name, size, style } = this.model.props; const cardStyle = style ?? AttachmentBlockStyles[1]; - const theme = this.std.get(ThemeProvider).theme; + const theme = this.std.get(ThemeProvider).theme$.value; const { LoadingIcon } = getEmbedCardIcons(theme); - const titleIcon = this.loading ? LoadingIcon : AttachmentIcon16; - const titleText = this.loading ? 'Loading...' : name; - const infoText = this.error ? 'File loading failed.' : humanFileSize(size); + const blobState = this.blobState$.value; + const { + uploading = false, + downloading = false, + overSize = false, + errorMessage, + } = blobState; + const warning = !overSize && Boolean(errorMessage); + const error = overSize || warning; + const loading = !error && downloading; + const state = this.determineState(loading, uploading, overSize, error); - const fileType = name.split('.').pop() ?? ''; - const FileTypeIcon = getAttachmentFileIcon(fileType); + const classInfo = { + 'affine-attachment-card': true, + [cardStyle]: true, + error, + loading, + }; - const embedView = this.embedView; + const icon = loading + ? LoadingIcon + : error + ? WarningIcon() + : AttachmentIcon(); + const title = uploading ? 'Uploading...' : loading ? 'Loading...' : name; + const description = errorMessage || humanFileSize(size); + const kind = getAttachmentFileIcon(name.split('.').pop() ?? ''); + return when( + cardStyle === 'cubeThick', + () => + this.renderWithVertical( + classInfo, + icon, + title, + description, + kind, + state + ), + () => + this.renderWithHorizontal( + classInfo, + icon, + title, + description, + kind, + state + ) + ); + }; + + override renderBlock() { return html` -
- ${embedView - ? html`
- ${embedView} -
` - : html`
-
-
-
- ${titleIcon} -
- -
- ${titleText} -
-
- - -
- -
${FileTypeIcon}
-
`} +
+ ${when( + this.embedView, + () => + html`
+ ${this.embedView} +
`, + this.renderCard + )}
`; } @property({ attribute: false }) - accessor allowEmbed = false; + accessor blobUrl: string | null = null; - @property({ attribute: false }) - accessor blobUrl: string | undefined = undefined; - - @property({ attribute: false }) - accessor downloading = false; - - @property({ attribute: false }) - accessor error = false; - - @property({ attribute: false }) - accessor loading = false; + override accessor selectedStyle = SelectedStyle.Border; override accessor useCaptionEditor = true; } diff --git a/blocksuite/affine/blocks/attachment/src/configs/toolbar.ts b/blocksuite/affine/blocks/attachment/src/configs/toolbar.ts index 1e414d57bb..0133d8a2d6 100644 --- a/blocksuite/affine/blocks/attachment/src/configs/toolbar.ts +++ b/blocksuite/affine/blocks/attachment/src/configs/toolbar.ts @@ -69,6 +69,10 @@ export const attachmentViewDropdownMenu = { { id: 'embed', label: 'Embed view', + disabled: ctx => { + const block = ctx.getCurrentBlockByType(AttachmentBlockComponent); + return block ? !block.embedded() : true; + }, run(ctx) { const model = ctx.getCurrentModelByType(AttachmentBlockModel); if (!model) return; @@ -156,24 +160,24 @@ const builtinToolbarConfig = { actions: [ { id: 'a.rename', - content(cx) { - const block = cx.getCurrentBlockByType(AttachmentBlockComponent); + content(ctx) { + const block = ctx.getCurrentBlockByType(AttachmentBlockComponent); if (!block) return null; const abortController = new AbortController(); - abortController.signal.onabort = () => cx.show(); + abortController.signal.onabort = () => ctx.show(); return html` { - cx.hide(); + ctx.hide(); createLitPortal({ template: RenameModal({ model: block.model, - editorHost: cx.host, + editorHost: ctx.host, abortController, }), computePosition: { diff --git a/blocksuite/affine/blocks/attachment/src/embed.ts b/blocksuite/affine/blocks/attachment/src/embed.ts index 310e4584f1..93727441c4 100644 --- a/blocksuite/affine/blocks/attachment/src/embed.ts +++ b/blocksuite/affine/blocks/attachment/src/embed.ts @@ -187,7 +187,7 @@ const embedConfig: AttachmentEmbedConfig[] = [ /** * Turn the attachment block into an image block. */ -export async function turnIntoImageBlock(model: AttachmentBlockModel) { +async function turnIntoImageBlock(model: AttachmentBlockModel) { if (!model.doc.schema.flavourSchemaMap.has('affine:image')) { console.error('The image flavour is not supported!'); return; diff --git a/blocksuite/affine/blocks/attachment/src/styles.ts b/blocksuite/affine/blocks/attachment/src/styles.ts index 72508f8fa4..6ec0bb32ab 100644 --- a/blocksuite/affine/blocks/attachment/src/styles.ts +++ b/blocksuite/affine/blocks/attachment/src/styles.ts @@ -1,31 +1,33 @@ +import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; import { css } from 'lit'; export const styles = css` - .affine-attachment-card { - margin: 0 auto; + .affine-attachment-container { + border-radius: 8px; box-sizing: border-box; + user-select: none; + border: 1px solid ${unsafeCSSVarV2('layer/background/tertiary')}; + background: ${unsafeCSSVarV2('layer/background/primary')}; + overflow: hidden; + + &.focused { + border-color: ${unsafeCSSVarV2('layer/insideBorder/primaryBorder')}; + } + } + + .affine-attachment-card { display: flex; gap: 12px; - - width: 100%; - height: 100%; - padding: 12px; - border-radius: 8px; - border: 1px solid var(--affine-background-tertiary-color); - - opacity: var(--add, 1); - background: var(--affine-background-primary-color); - user-select: none; } .affine-attachment-content { - height: 100%; display: flex; flex-direction: column; align-items: flex-start; gap: 12px; flex: 1 0 0; + min-width: 0; } .affine-attachment-content-title { @@ -33,7 +35,6 @@ export const styles = css` flex-direction: row; gap: 8px; align-items: center; - align-self: stretch; } @@ -43,24 +44,18 @@ export const styles = css` height: 16px; align-items: center; justify-content: center; + color: var(--affine-text-primary-color); } - .affine-attachment-content-title-icon svg { - width: 16px; - height: 16px; - fill: var(--affine-background-primary-color); + .truncate { + align-self: stretch; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; } .affine-attachment-content-title-text { - display: -webkit-box; - -webkit-line-clamp: 1; - -webkit-box-orient: vertical; - - word-break: break-all; - overflow: hidden; - text-overflow: ellipsis; color: var(--affine-text-primary-color); - font-family: var(--affine-font-family); font-size: var(--affine-font-sm); font-style: normal; @@ -68,17 +63,15 @@ export const styles = css` line-height: 22px; } + .affine-attachment-content-description { + display: flex; + align-items: center; + align-self: stretch; + gap: 8px; + } + .affine-attachment-content-info { - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 1; - flex: 1 0 0; - - word-break: break-all; - overflow: hidden; color: var(--affine-text-secondary-color); - text-overflow: ellipsis; - font-family: var(--affine-font-family); font-size: var(--affine-font-xs); font-style: normal; @@ -86,6 +79,26 @@ export const styles = css` line-height: 20px; } + .affine-attachment-content-button { + display: flex; + height: 20px; + align-items: center; + align-self: stretch; + gap: 4px; + white-space: nowrap; + padding: 0 4px; + color: ${unsafeCSSVarV2('button/primary')}; + font-family: var(--affine-font-family); + font-size: var(--affine-font-xs); + font-style: normal; + font-weight: 500; + line-height: 20px; + + svg { + font-size: 16px; + } + } + .affine-attachment-banner { display: flex; align-items: center; @@ -93,16 +106,15 @@ export const styles = css` } .affine-attachment-card.loading { - background: var(--affine-background-secondary-color); - .affine-attachment-content-title-text { color: var(--affine-placeholder-color); } } - .affine-attachment-card.error, - .affine-attachment-card.unsynced { - background: var(--affine-background-secondary-color); + .affine-attachment-card.error { + .affine-attachment-content-title-icon { + color: ${unsafeCSSVarV2('status/error')}; + } } .affine-attachment-card.cubeThick { @@ -116,7 +128,7 @@ export const styles = css` } .affine-attachment-banner { - justify-content: flex-start; + justify-content: space-between; } } diff --git a/blocksuite/affine/blocks/attachment/src/utils.ts b/blocksuite/affine/blocks/attachment/src/utils.ts index 631d19cb51..eabc76f91f 100644 --- a/blocksuite/affine/blocks/attachment/src/utils.ts +++ b/blocksuite/affine/blocks/attachment/src/utils.ts @@ -21,128 +21,19 @@ import type { BlockModel } from '@blocksuite/store'; import type { AttachmentBlockComponent } from './attachment-block'; -const attachmentUploads = new Set(); -export function setAttachmentUploading(blockId: string) { - attachmentUploads.add(blockId); -} -export function setAttachmentUploaded(blockId: string) { - attachmentUploads.delete(blockId); -} -function isAttachmentUploading(blockId: string) { - return attachmentUploads.has(blockId); -} - -/** - * This function will not verify the size of the file. - */ -// TODO(@fundon): should remove -export async function uploadAttachmentBlob( - std: BlockStdScope, - blockId: string, - blob: Blob, - filetype: string, - isEdgeless?: boolean -): Promise { - if (isAttachmentUploading(blockId)) return; - - let sourceId: string | undefined; - - try { - setAttachmentUploading(blockId); - sourceId = await std.store.blobSync.set(blob); - } catch (error) { - console.error(error); - if (error instanceof Error) { - toast( - std.host, - `Failed to upload attachment! ${error.message || error.toString()}` - ); - } - } finally { - setAttachmentUploaded(blockId); - - const block = std.store.getBlock(blockId); - - std.store.withoutTransact(() => { - if (!block) return; - - std.store.updateBlock(block.model, { - sourceId, - } satisfies Partial); - }); - - std.getOptional(TelemetryProvider)?.track('AttachmentUploadedEvent', { - page: `${isEdgeless ? 'whiteboard' : 'doc'} editor`, - module: 'attachment', - segment: 'attachment', - control: 'uploader', - type: filetype, - category: block && sourceId ? 'success' : 'failure', - }); - } -} - export async function getAttachmentBlob(model: AttachmentBlockModel) { - const sourceId = model.props.sourceId; - if (!sourceId) { - return null; - } + const { + sourceId$: { value: sourceId }, + type$: { value: type }, + } = model.props; + if (!sourceId) return null; const doc = model.doc; let blob = await doc.blobSync.get(sourceId); - if (blob) { - blob = new Blob([blob], { type: model.props.type }); - } + if (!blob) return null; - return blob; -} - -// TODO(@fundon): should remove -export async function checkAttachmentBlob(block: AttachmentBlockComponent) { - const model = block.model; - const { id } = model; - const { sourceId } = model.props; - - if (isAttachmentUploading(id)) { - block.loading = true; - block.error = false; - block.allowEmbed = false; - if (block.blobUrl) { - URL.revokeObjectURL(block.blobUrl); - block.blobUrl = undefined; - } - return; - } - - try { - if (!sourceId) { - return; - } - - const blob = await getAttachmentBlob(model); - if (!blob) { - return; - } - - block.loading = false; - block.error = false; - block.allowEmbed = block.embedded(); - if (block.blobUrl) { - URL.revokeObjectURL(block.blobUrl); - } - block.blobUrl = URL.createObjectURL(blob); - } catch (error) { - console.warn(error, model, sourceId); - - block.loading = false; - block.error = true; - block.allowEmbed = false; - if (block.blobUrl) { - URL.revokeObjectURL(block.blobUrl); - block.blobUrl = undefined; - } - } + return new Blob([blob], { type }); } /** @@ -150,26 +41,22 @@ export async function checkAttachmentBlob(block: AttachmentBlockComponent) { * the download process may take a long time! */ export function downloadAttachmentBlob(block: AttachmentBlockComponent) { - const { host, model, loading, error, downloading, blobUrl } = block; - if (downloading) { - toast(host, 'Download in progress...'); - return; - } + const { host, model, blobUrl, blobState$ } = block; - if (loading) { - toast(host, 'Please wait, file is loading...'); + if (blobState$.peek().downloading) { + toast(host, 'Download in progress...'); return; } const name = model.props.name; const shortName = name.length < 20 ? name : name.slice(0, 20) + '...'; - if (error || !blobUrl) { + if (!blobUrl) { toast(host, `Failed to download ${shortName}!`); return; } - block.downloading = true; + block.updateBlobState({ downloading: true }); toast(host, `Downloading ${shortName}`); @@ -180,7 +67,34 @@ export function downloadAttachmentBlob(block: AttachmentBlockComponent) { tmpLink.dispatchEvent(event); tmpLink.remove(); - block.downloading = false; + block.updateBlobState({ downloading: false }); +} + +export async function refreshData( + std: BlockStdScope, + block: AttachmentBlockComponent +) { + const model = block.model; + const sourceId = model.props.sourceId$.peek(); + if (!sourceId) return; + + const blobUrl = block.blobUrl; + if (blobUrl) { + URL.revokeObjectURL(blobUrl); + block.blobUrl = null; + } + + let blob = await std.store.blobSync.get(sourceId); + if (!blob) { + block.updateBlobState({ errorMessage: 'File not found' }); + return; + } + + const type = model.props.type$.peek(); + + blob = new Blob([blob], { type }); + + block.blobUrl = URL.createObjectURL(blob); } export async function getFileType(file: File) { @@ -219,6 +133,7 @@ async function buildPropsWith( try { const { name, size } = file; + // TODO(@fundon): should re-upload when upload timeout const sourceId = await std.store.blobSync.set(file); type = await getFileType(file); @@ -233,6 +148,7 @@ async function buildPropsWith( category = 'failure'; throw err; } finally { + // TODO(@fundon): should change event name because this is just a local operation. std.getOptional(TelemetryProvider)?.track('AttachmentUploadedEvent', { page: `${mode} editor`, module: 'attachment', @@ -303,7 +219,6 @@ export async function addAttachments( const gap = 32; const width = EMBED_CARD_WIDTH.cubeThick; const height = EMBED_CARD_HEIGHT.cubeThick; - const flavour = AttachmentBlockSchema.model.flavour; const blocks = propsArray.map((props, index) => { @@ -312,7 +227,7 @@ export async function addAttachments( return { flavour, blockProps: { ...props, style, xywh } }; }); - const blockIds = std.store.addBlocks(blocks); + const blockIds = std.store.addBlocks(blocks, gfx.surface); gfx.selection.set({ elements: blockIds, diff --git a/blocksuite/affine/blocks/image/src/utils.ts b/blocksuite/affine/blocks/image/src/utils.ts index 72812e7d4b..6e610dbb53 100644 --- a/blocksuite/affine/blocks/image/src/utils.ts +++ b/blocksuite/affine/blocks/image/src/utils.ts @@ -481,10 +481,12 @@ export async function addImages( // If maxWidth is provided, limit the width of the image to maxWidth // Otherwise, use the original width - const width = maxWidth ? Math.min(props.width, maxWidth) : props.width; - const height = maxWidth - ? (props.height / props.width) * width - : props.height; + if (maxWidth) { + const p = props.height / props.width; + props.width = Math.min(props.width, maxWidth); + props.height = props.width * p; + } + const { width, height } = props; const xywh = calcBoundByOrigin( center, diff --git a/blocksuite/affine/components/src/icons/text.ts b/blocksuite/affine/components/src/icons/text.ts index 89b3515730..fee39e592b 100644 --- a/blocksuite/affine/components/src/icons/text.ts +++ b/blocksuite/affine/components/src/icons/text.ts @@ -340,11 +340,6 @@ export const FontFamilyIcon = icons.FontIcon({ height: '20', }); -export const AttachmentIcon16 = icons.AttachmentIcon({ - width: '16', - height: '16', -}); - export const TextBackgroundDuotoneIcon = html` { const attachment = page.locator('affine-attachment'); await attachment.click(); - const attachmentSelection = attachment.locator('affine-block-selection'); + const attachmentSelection = attachment.locator( + '.affine-attachment-container.focused' + ); const toolbar = locateToolbar(page); diff --git a/tests/blocksuite/e2e/attachment.spec.ts b/tests/blocksuite/e2e/attachment.spec.ts index e0d40e578f..276b1a5ddc 100644 --- a/tests/blocksuite/e2e/attachment.spec.ts +++ b/tests/blocksuite/e2e/attachment.spec.ts @@ -322,7 +322,7 @@ test(`support dragging attachment block directly`, async ({ await page.mouse.move(rect.x + 40, rect.y + rect.height + 80, { steps: 20 }); await page.mouse.up(); - const rects = page.locator('affine-block-selection').locator('visible=true'); + const rects = page.locator('.affine-attachment-container.focused'); await expect(rects).toHaveCount(1); expect(await getPageSnapshot(page, true)).toMatchSnapshot( `${testInfo.title}_3.json`