From c49c7786136e50a4e979d88ce8c9487fbb043cc7 Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Thu, 14 May 2026 11:33:48 +0800 Subject: [PATCH] =?UTF-8?q?fix(editor):=20align=20Preview=20gate=20with=20?= =?UTF-8?q?Download=20=E2=80=94=20survive=20URL-only=20sources=20(#2566)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Eye button required a fully resolved Attachment record (URL-lookup via `resolveAttachment(href)`) before showing. Download only required the URL, falling back to `openExternal(href)` when the lookup missed. Result: any case where the URL in markdown couldn't be reverse-matched to the entity's `attachments` prop (cross-comment copy-paste, stale caches) silently hid the Preview button while Download kept working — edit and readonly surfaces diverged for the same content. Widen the Preview gate to mirror Download: show the Eye whenever the filename indicates a previewable type. Introduce a `PreviewSource` tagged union — `{ kind: "full", attachment }` for the existing path, `{ kind: "url", url, filename }` for the fallback. Media kinds (pdf/video/audio) render directly from the URL; text kinds still require an attachment id because the /content proxy is ID-keyed, so `tryOpen` rejects URL+text combinations and PreviewContent has a defensive fallback for direct mounts. Side effects: - `getPreviewKind` gains filename-extension fallbacks for video/audio (was PDF-only); without these the URL-only path can't infer kind when content_type is empty. - AttachmentList in comment-card.tsx unchanged behaviorally — only the tryOpen call site is updated to the new signature. Pre-existing architectural issues (AttachmentList readonly-only, URL-based attachment lookup, per-entity ownership) are intentionally out of scope. Co-authored-by: Claude Opus 4.7 (1M context) --- .../editor/attachment-preview-modal.test.tsx | 146 +++++++++++++++-- .../views/editor/attachment-preview-modal.tsx | 152 ++++++++++++++---- .../views/editor/extensions/file-card.tsx | 34 ++-- packages/views/editor/readonly-content.tsx | 30 ++-- packages/views/editor/utils/preview.ts | 18 ++- .../views/issues/components/comment-card.tsx | 2 +- 6 files changed, 311 insertions(+), 71 deletions(-) diff --git a/packages/views/editor/attachment-preview-modal.test.tsx b/packages/views/editor/attachment-preview-modal.test.tsx index 6d9fdb873..cd051010e 100644 --- a/packages/views/editor/attachment-preview-modal.test.tsx +++ b/packages/views/editor/attachment-preview-modal.test.tsx @@ -4,6 +4,12 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import type { ReactElement } from "react"; import type { Attachment } from "@multica/core/types"; +const openExternalMock = vi.hoisted(() => vi.fn()); + +vi.mock("../platform", () => ({ + openExternal: openExternalMock, +})); + // vi.hoisted: factories run before module evaluation, letting us name mocks // referenced from inside vi.mock factories below. The Error classes must be // hoisted too because vi.mock is itself hoisted above the top-level `class` @@ -70,7 +76,11 @@ vi.mock("../i18n", () => ({ }), })); -import { AttachmentPreviewModal } from "./attachment-preview-modal"; +import { + AttachmentPreviewModal, + useAttachmentPreview, +} from "./attachment-preview-modal"; +import { renderHook, act as hookAct } from "@testing-library/react"; // Fresh QueryClient per render — no retries (preview errors are typed, // not transient) and no caching across tests so each scenario is hermetic. @@ -112,7 +122,7 @@ afterEach(() => { describe("AttachmentPreviewModal — dispatch", () => { it("renders a PDF iframe pointing at the signed download URL", () => { const att = makeAttachment({ filename: "manual.pdf", content_type: "application/pdf" }); - render( {}} />); + render( {}} />); const iframe = document.querySelector("iframe"); expect(iframe).toBeTruthy(); expect(iframe?.getAttribute("src")).toBe(att.download_url); @@ -120,7 +130,7 @@ describe("AttachmentPreviewModal — dispatch", () => { it("renders a