Adds a /api/staff/file proxy that re-streams Civi attachments with Content-Disposition: inline so a native <dialog> lightbox can preview images and PDFs in place. Office docs keep their plain download link and gain a "View in Google Docs" secondary link (uses the Civi-signed URL so Google can fetch without our staff key). Also threads mime through /api/staff/report (Attachment.get mime_type) so the dispatcher picks the right affordance without relying solely on filename inference.
134 lines
4.0 KiB
TypeScript
134 lines
4.0 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { AttachmentLightbox } from "./AttachmentLightbox";
|
|
import { categoryFromMime, resolveMime } from "@/lib/mime.mjs";
|
|
|
|
/**
|
|
* Render an attachment row in the staff report with the right affordance
|
|
* for its type:
|
|
*
|
|
* - image / pdf -> button opens the inline lightbox
|
|
* - office -> download link + "View in Google Docs" secondary link
|
|
* - other -> plain download link
|
|
*
|
|
* The proxy URL is `/api/staff/file?id=&org=&key=` (re-streams with
|
|
* Content-Disposition: inline for previews, or `&dl=1` for downloads). The
|
|
* Civi-signed URL (carrying its short-lived fcs JWT) is passed straight to
|
|
* Google's Docs Viewer for office files; we deliberately don't proxy that
|
|
* one because Google's servers must fetch it without our staff key.
|
|
*/
|
|
export function FileLink({
|
|
fileId,
|
|
fileName,
|
|
civiSignedUrl,
|
|
mime: explicitMime,
|
|
org,
|
|
authKey,
|
|
civiBaseUrl,
|
|
}: {
|
|
fileId: number | string;
|
|
fileName: string;
|
|
/** From Attachment.get (includes fcs). Used for office Google Viewer + download fallback. */
|
|
civiSignedUrl?: string;
|
|
mime?: string;
|
|
org: number;
|
|
authKey: string;
|
|
civiBaseUrl: string;
|
|
}) {
|
|
const [open, setOpen] = useState(false);
|
|
|
|
const mime = resolveMime(explicitMime, fileName);
|
|
const category = categoryFromMime(mime);
|
|
|
|
const proxyBase =
|
|
`/api/staff/file?id=${encodeURIComponent(String(fileId))}` +
|
|
`&org=${encodeURIComponent(String(org))}` +
|
|
`&key=${encodeURIComponent(authKey)}`;
|
|
const previewSrc = proxyBase;
|
|
// Prefer Civi's signed URL for downloads when present (one fewer hop
|
|
// through our Lambda); the proxy is the fallback.
|
|
const downloadHref = civiSignedUrl
|
|
? absUrl(civiSignedUrl, civiBaseUrl)
|
|
: `${proxyBase}&dl=1`;
|
|
|
|
const linkClass =
|
|
"text-ink underline decoration-rule underline-offset-4 hover:decoration-ink";
|
|
|
|
if (category === "image" || category === "pdf") {
|
|
return (
|
|
<>
|
|
<button
|
|
type="button"
|
|
onClick={() => setOpen(true)}
|
|
className={
|
|
"bg-transparent p-0 text-left " +
|
|
linkClass +
|
|
" focus:outline-none focus-visible:ring-2 focus-visible:ring-leaf-700"
|
|
}
|
|
>
|
|
{fileName}
|
|
</button>
|
|
<AttachmentLightbox
|
|
open={open}
|
|
onClose={() => setOpen(false)}
|
|
previewSrc={previewSrc}
|
|
downloadHref={downloadHref}
|
|
filename={fileName}
|
|
mime={mime}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
if (category === "office") {
|
|
// Google Docs Viewer renders DOC/DOCX/XLS/XLSX in a new tab. It fetches
|
|
// the source URL server-side, so the URL must be reachable without our
|
|
// staff key — that's why we pass the Civi-signed URL straight through.
|
|
const gview = civiSignedUrl
|
|
? `https://docs.google.com/viewer?url=${encodeURIComponent(absUrl(civiSignedUrl, civiBaseUrl))}`
|
|
: null;
|
|
return (
|
|
<span className="inline-flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
|
<a
|
|
href={downloadHref}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className={linkClass}
|
|
>
|
|
{fileName}
|
|
</a>
|
|
{gview && (
|
|
<a
|
|
href={gview}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
title="Opens in Google Docs Viewer (file bytes are sent to Google to render)"
|
|
className="text-xs font-medium text-leaf-700 underline decoration-rule underline-offset-4 hover:decoration-ink hover:text-leaf-800"
|
|
>
|
|
View in Google Docs
|
|
</a>
|
|
)}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
// "other" — unknown types: just a download link.
|
|
return (
|
|
<a
|
|
href={downloadHref}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className={linkClass}
|
|
>
|
|
{fileName}
|
|
</a>
|
|
);
|
|
}
|
|
|
|
function absUrl(u: string, base: string): string {
|
|
if (u.startsWith("http")) return u;
|
|
if (!base) return u;
|
|
return `${base.replace(/\/+$/, "")}${u.startsWith("/") ? "" : "/"}${u}`;
|
|
}
|