feat(files): in-app .docx preview + allow office/text mimes
- .docx now renders client-side via docx-preview (fetches bytes from our own storage; works with private MinIO/disk). Drops Microsoft's hosted Office viewer which can't reach a private object store. - add office (.docx/.doc/.xlsx/.xls) + text/csv to PREVIEWABLE_MIMES so /api/v1/files/[id]/preview returns a URL instead of rejecting them (was surfacing as a misleading "Failed to load preview") - legacy .doc + spreadsheets fall through to a download CTA (can't render client-side); text/csv use the existing TextPreview Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { apiFetch } from '@/lib/api/client';
|
||||
import { isWordDocx } from '@/lib/constants/file-validation';
|
||||
|
||||
// yet-another-react-lightbox is ~50kb, lazy-load it.
|
||||
const Lightbox = dynamic(() => import('yet-another-react-lightbox'), { ssr: false });
|
||||
@@ -30,6 +31,16 @@ const PdfViewer = dynamic(() => import('./pdf-viewer').then((m) => ({ default: m
|
||||
),
|
||||
});
|
||||
|
||||
// docx-preview is lazy-loaded the same way — only .docx previews pull it in.
|
||||
const DocxViewer = dynamic(() => import('./docx-viewer').then((m) => ({ default: m.DocxViewer })), {
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
|
||||
Loading document viewer…
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
interface FilePreviewDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
@@ -185,24 +196,34 @@ export function FilePreviewDialog({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && previewUrl && kind === 'office' && (
|
||||
// Office documents render via Microsoft's hosted Office viewer
|
||||
// - public URL only; presigned download URLs include a token
|
||||
// in the query string so they work here even though the file
|
||||
// isn't world-public. The viewer streams the document and
|
||||
// renders a high-fidelity preview without us shipping a
|
||||
// headless LibreOffice. Falls back to "download to view" if
|
||||
// the embed loads but renders nothing (e.g. CORS rejected) -
|
||||
// detection is hard so we just keep the download CTA below.
|
||||
<iframe
|
||||
title={fileName ?? 'Office document preview'}
|
||||
src={`https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(
|
||||
previewUrl,
|
||||
)}`}
|
||||
className="h-full w-full"
|
||||
sandbox="allow-scripts allow-same-origin allow-popups"
|
||||
/>
|
||||
)}
|
||||
{!loading &&
|
||||
!error &&
|
||||
previewUrl &&
|
||||
kind === 'office' &&
|
||||
// Word .docx renders in-browser via docx-preview (fetches the
|
||||
// bytes from our own storage — works with private MinIO/disk).
|
||||
// We do NOT use Microsoft's hosted Office viewer: it requires a
|
||||
// publicly-reachable URL, which our private object store isn't.
|
||||
// Legacy .doc + spreadsheet formats can't be rendered client-
|
||||
// side, so they fall through to a download CTA.
|
||||
(isWordDocx(mimeType, fileName) ? (
|
||||
<DocxViewer url={previewUrl} fileName={fileName} />
|
||||
) : (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 p-6 text-center">
|
||||
<FileWarning className="size-8 text-muted-foreground" aria-hidden />
|
||||
<p className="text-sm font-medium">In-browser preview isn't available</p>
|
||||
<p className="max-w-xs text-xs text-muted-foreground">
|
||||
This Office format ({mimeType ?? 'unknown'}) can't be rendered in the
|
||||
browser. Download it to view locally.
|
||||
</p>
|
||||
<Button asChild>
|
||||
<a href={previewUrl} download={fileName ?? 'download'}>
|
||||
<Download className="mr-1.5 size-4" aria-hidden />
|
||||
Download
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{!loading && !error && previewUrl && kind === 'unknown' && (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 p-6 text-center">
|
||||
|
||||
Reference in New Issue
Block a user