Skip to main content

ImageViewer resolveExportSrc — resolve the download address before the default export (WEB-1541)

Version: 0.10.0 · Type: ✨ Feature

WEB-1541 · Parent: WEB-945 · Source incident: ANT-1620

Problem​

ImageViewer's export had exactly two granularities: the built-in default download (fetch to a blob → same-origin objectURL → <a download>, with cache: 'reload' to bypass an HTTP cache poisoned by a no-cors <img>, fallback to the raw address when the fetch fails, and delayed objectURL revocation), or a full onExport takeover that skips all of it.

The downstream requirement in ANT-1620 is narrower than either: on a web4 recording detail page left open past the presigned URL's TTL, downloading a highlight screenshot still uses the locally cached, now-expired URL and S3 answers AccessDenied / Request has expired. The <img> keeps showing the image — it loaded long ago and never re-requests — so the viewer-level self-heal never fires. Only the address needs to change; the blob / cache / cross-origin / filename behavior should stay exactly as-is.

With only onExport available, a consumer had to take over and re-implement the export orchestration (resolve address → download → fall back) outside the component. That is a second copy of logic the component already owns: if the default path later grows analytics, a failure toast, or multi-image support, the consumer's branch silently stops matching.

Changed Files​

  • packages/design/src/components/ImageViewer/ImageViewer.tsx
  • packages/design/src/components/ImageViewer/__tests__/ImageViewer.test.tsx
  • packages/design-site/docs/components/image-viewer.mdx (and its zh-CN copy)

Changes​

  1. Add an optional resolveExportSrc?: (image, index) => string | null | undefined | Promise<string | null | undefined> prop that answers only "which address to download". A returned address is used; an empty value or a thrown error falls back to image.src, matching the existing blob-fetch failure fallback so nothing fails silently.
  2. Extract the default export path into runDefaultExport(image, index): await resolveExportSrc inside a try / catch, then hand the resulting address to the internal download. The download implementation (downloadImageBySrc) stays module-private and is not part of the public API.
  3. Define the precedence explicitly: passing onExport is a full takeover, so resolveExportSrc is never invoked in that case. Documented in both the prop JSDoc and the component docs.
  4. Add four tests: download the resolved address, fall back to image.src on an empty resolve, fall back to image.src on a rejected resolve, and confirm onExport bypasses resolveExportSrc entirely.
  5. Document the third export mode in image-viewer.mdx (both locales): a new Props-table row plus an expanded "export download mechanics" section.

Impact​

Purely additive. Omitting resolveExportSrc leaves both the default download path and the onExport takeover path unchanged, hence minor rather than a breaking change. Note that the export button shows no loading state while the address resolves — the same wait window as today's fetch before the download starts.

The first consumers are web4's HighlightImageAttachment (re-signs via picture_link only when the URL is expired) and ImageFilePage (re-signs via useSourceUrlSelfHeal.resolveFreshUrl). Both now keep only the genuinely business-specific decision — whether to re-sign — and leave the fallback semantics to the component's documented contract.