mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-16 13:57:02 +08:00
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:
@@ -66,7 +66,12 @@ export function PDFViewerEmbedded({ model }: AttachmentViewerProps) {
|
||||
useMemo(() => (pageEntity ? pageEntity.page.bitmap$ : null), [pageEntity])
|
||||
);
|
||||
|
||||
const [name, setName] = useState(model.props.name);
|
||||
const name = useLiveData(
|
||||
useMemo(() => LiveData.fromSignal(model.props.name$), [model])
|
||||
);
|
||||
const blobId = useLiveData(
|
||||
useMemo(() => LiveData.fromSignal(model.props.sourceId$), [model])
|
||||
);
|
||||
const [cursor, setCursor] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [visibility, setVisibility] = useState(false);
|
||||
@@ -107,8 +112,6 @@ export function PDFViewerEmbedded({ model }: AttachmentViewerProps) {
|
||||
};
|
||||
}, [cursor, meta, peek]);
|
||||
|
||||
useEffect(() => model.props.name$.subscribe(val => setName(val)), [model]);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
@@ -163,8 +166,9 @@ export function PDFViewerEmbedded({ model }: AttachmentViewerProps) {
|
||||
|
||||
useEffect(() => {
|
||||
if (!visibility) return;
|
||||
if (!blobId) return;
|
||||
|
||||
const pdfEntity = pdfService.get(model);
|
||||
const pdfEntity = pdfService.get(blobId);
|
||||
|
||||
setPdfEntity(pdfEntity);
|
||||
|
||||
@@ -172,7 +176,7 @@ export function PDFViewerEmbedded({ model }: AttachmentViewerProps) {
|
||||
pdfEntity.release();
|
||||
setPdfEntity(null);
|
||||
};
|
||||
}, [model, pdfService, visibility]);
|
||||
}, [blobId, pdfService, visibility]);
|
||||
|
||||
useEffect(() => {
|
||||
const viewer = viewerRef.current;
|
||||
|
||||
@@ -329,6 +329,9 @@ function PDFViewerContainer({
|
||||
[pdf]
|
||||
)
|
||||
);
|
||||
const blobId = useLiveData(
|
||||
useMemo(() => LiveData.fromSignal(model.props.sourceId$), [model])
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.status !== PDFStatus.Error) return;
|
||||
@@ -337,13 +340,15 @@ function PDFViewerContainer({
|
||||
}, [state]);
|
||||
|
||||
useEffect(() => {
|
||||
const { pdf, release } = pdfService.get(model);
|
||||
if (!blobId) return;
|
||||
|
||||
const { pdf, release } = pdfService.get(blobId);
|
||||
setPdf(pdf);
|
||||
|
||||
return () => {
|
||||
release();
|
||||
};
|
||||
}, [model, pdfService, setPdf]);
|
||||
}, [blobId, pdfService, setPdf]);
|
||||
|
||||
if (pdf && state.status === PDFStatus.Opened) {
|
||||
return <PDFViewerInner {...props} pdf={pdf} meta={state.meta} />;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user