feat(editor): add replace action in attachment toolbar (#12512)

Closes:
[BS-3549](https://linear.app/affine-design/issue/BS-3549/附件-toolbar-上添加-replace-action)

[Screen Recording 2025-06-04 at 15.37.40.mov <span
class="graphite__hidden">(uploaded via Graphite)</span> <img
class="graphite__hidden"
src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/8ypiIKZXudF5a0tIgIzf/480c8690-7ec9-4188-92fd-ee3339afb558.mov"
/>](https://app.graphite.dev/media/video/8ypiIKZXudF5a0tIgIzf/480c8690-7ec9-4188-92fd-ee3339afb558.mov)



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added the ability to replace attachments directly from the toolbar,
allowing users to select and update files seamlessly.
- **Bug Fixes**
- Improved handling when replacing embedded attachments with unsupported
file types, ensuring the view falls back to a card view as needed.
- **Tests**
- Introduced end-to-end tests to verify attachment replacement and
correct UI behavior in both standard and edgeless editing modes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Fangdun Tsai
2025-06-11 14:57:31 +08:00
committed by GitHub
parent 814364489f
commit a71904e641
10 changed files with 282 additions and 64 deletions

View File

@@ -1,8 +1,7 @@
import type { AttachmentBlockModel } from '@blocksuite/affine/model';
import { Entity, LiveData, ObjectPool } from '@toeverything/infra';
import { catchError, from, map, of, startWith, switchMap } from 'rxjs';
import { downloadBlobToBuffer } from '../../media/utils';
import type { WorkspaceService } from '../../workspace';
import type { PDFMeta } from '../renderer';
import { PDFRenderer } from '../renderer';
import { PDFPage } from './pdf-page';
@@ -27,8 +26,8 @@ export type PDFRendererState =
error: Error;
};
export class PDF extends Entity<AttachmentBlockModel> {
public readonly id: string = this.props.id;
export class PDF extends Entity<{ blobId: string }> {
public readonly id: string = this.props.blobId;
readonly renderer = new PDFRenderer();
readonly pages = new ObjectPool<string, PDFPage>({
onDelete: page => page.dispose(),
@@ -36,8 +35,26 @@ export class PDF extends Entity<AttachmentBlockModel> {
readonly state$ = LiveData.from<PDFRendererState>(
// @ts-expect-error type alias
from(downloadBlobToBuffer(this.props)).pipe(
switchMap(data => this.renderer.ob$('open', { data })),
from(
this.workspaceService.workspace.engine.blob
.get(this.id)
.then(blobRecord => {
if (blobRecord) {
const { data, mime: type } = blobRecord;
const blob = new Blob([data], { type });
return blob.arrayBuffer();
}
return null;
})
).pipe(
switchMap(data => {
if (data) {
return this.renderer.ob$('open', { data });
}
throw new Error('PDF not found');
}),
map(meta => ({ status: PDFStatus.Opened, meta })),
// @ts-expect-error type alias
startWith({ status: PDFStatus.Opening }),
@@ -46,7 +63,7 @@ export class PDF extends Entity<AttachmentBlockModel> {
{ status: PDFStatus.IDLE }
);
constructor() {
constructor(private readonly workspaceService: WorkspaceService) {
super();
this.disposables.push(() => this.pages.clear());
}

View File

@@ -1,6 +1,6 @@
import type { Framework } from '@toeverything/infra';
import { WorkspaceScope } from '../workspace';
import { WorkspaceScope, WorkspaceService } from '../workspace';
import { PDF } from './entities/pdf';
import { PDFPage } from './entities/pdf-page';
import { PDFService } from './services/pdf';
@@ -9,7 +9,7 @@ export function configurePDFModule(framework: Framework) {
framework
.scope(WorkspaceScope)
.service(PDFService)
.entity(PDF)
.entity(PDF, [WorkspaceService])
.entity(PDFPage);
}

View File

@@ -1,4 +1,3 @@
import type { AttachmentBlockModel } from '@blocksuite/affine/model';
import { ObjectPool, Service } from '@toeverything/infra';
import { PDF } from '../entities/pdf';
@@ -19,11 +18,11 @@ export class PDFService extends Service {
});
}
get(model: AttachmentBlockModel) {
let rc = this.PDFs.get(model.id);
get(blobId: string) {
let rc = this.PDFs.get(blobId);
if (!rc) {
rc = this.PDFs.put(model.id, this.framework.createEntity(PDF, model));
rc = this.PDFs.put(blobId, this.framework.createEntity(PDF, { blobId }));
}
return { pdf: rc.obj, release: rc.release };