Files
WebForm-mw/lib/mime.mjs
T
Joel Brock 6850ff9dee Staff report: inline lightbox for image/PDF attachments
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.
2026-06-15 11:56:45 -07:00

87 lines
2.8 KiB
JavaScript

// @ts-check
/**
* Mime helpers for staff-report attachment rendering.
*
* The staff report shows file attachments with three different affordances:
* - images / PDF -> inline lightbox preview
* - office docs -> plain download + "View in Google Docs" link
* - anything else -> plain download
*
* Civi can serve a mime via `Attachment.get`, but historical uploads may
* have a stale or missing `mime_type` column. Fall back to extension-based
* inference so we always reach a stable category.
*
* Written as JS+JSDoc rather than TS so Node's built-in --test runner can
* import this file directly without any tooling — matches the pattern set
* by lib/staff-field-mapping.mjs.
*
* @typedef {"image" | "pdf" | "office" | "other"} AttachmentCategory
*/
/** @type {Record<string, string>} */
const EXT_TO_MIME = {
pdf: "application/pdf",
png: "image/png",
jpg: "image/jpeg",
jpeg: "image/jpeg",
gif: "image/gif",
webp: "image/webp",
doc: "application/msword",
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
xls: "application/vnd.ms-excel",
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
};
const OFFICE_MIMES = new Set([
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
]);
/**
* Pull the lowercased extension from a filename, or "" if absent.
* @param {string | undefined | null} filename
* @returns {string}
*/
export function extOf(filename) {
if (!filename) return "";
const dot = filename.lastIndexOf(".");
if (dot < 0 || dot === filename.length - 1) return "";
return filename.slice(dot + 1).toLowerCase();
}
/**
* Map a filename's extension to a known mime, or null if unrecognised.
* @param {string | undefined | null} filename
* @returns {string | null}
*/
export function mimeFromFilename(filename) {
const ext = extOf(filename);
return EXT_TO_MIME[ext] ?? null;
}
/**
* Resolve a mime by trusting the explicit value first, then falling back to
* filename inference. Returns "application/octet-stream" if nothing matches.
* @param {string | undefined | null} explicit
* @param {string | undefined | null} filename
* @returns {string}
*/
export function resolveMime(explicit, filename) {
if (explicit && explicit !== "application/octet-stream") return explicit;
return mimeFromFilename(filename) ?? explicit ?? "application/octet-stream";
}
/**
* Categorise a mime for UI dispatch.
* @param {string} mime
* @returns {AttachmentCategory}
*/
export function categoryFromMime(mime) {
if (mime.startsWith("image/")) return "image";
if (mime === "application/pdf") return "pdf";
if (OFFICE_MIMES.has(mime)) return "office";
return "other";
}