// @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} */ 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"; }