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

@@ -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;

View File

@@ -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} />;