1/** 2 * Download utilities â kills the boilerplate around Blob â file download. 3 * 4 * The pattern below was duplicated across 31+ files (44+ call sites). Each site 5 * was hand-rolling `URL.createObjectURL + <a> element + appendChild + click + 6 * removeChild + URL.revokeObjectURL`. This module consolidates that into a 7 * single function so callers can focus on what they're downloading, not the 8 * mechanics of triggering it. 9 * 10 * @see PATTERNS_BIBLE.md â "Never roll your own when a util exists" 11 */ 12 13/** 14 * Trigger a browser download from a Blob. 15 * 16 * Replaces the boilerplate: 17 * const url = URL.createObjectURL(blob); 18 * const a = document.createElement('a'); 19 * a.href = url; a.download = fileName; 20 * document.body.appendChild(a); a.click(); document.body.removeChild(a); 21 * URL.revokeObjectURL(url); 22 * 23 * Optionally fires a success toast â saves a follow-up `window.showToast?.()` 24 * call at the caller, and ensures success messaging stays consistent across 25 * exporters. 26 * 27 * @param {Blob} blob - The blob to download (e.g. PDF / XLSX / DOCX). Required. 28 * @param {string} [fileName='download'] - Download filename with extension. 29 * Falls back to 'download' if missing. 30 * @param {Object} [options] 31 * @param {string} [options.successMessage] - Toast title to show after the 32 * download is triggered. Omit for silent. 33 * @param {string} [options.successDescription] - Optional toast subtitle. 34 * Usually the filename or extra context. 35 * @returns {void} 36 * 37 * @example 38 * // Simplest â silent download 39 * downloadBlob(blob, 'Cash_Flow.pdf'); 40 * 41 * @example 42 * // With success toast 43 * downloadBlob(blob, fileName, { 44 * successMessage: 'Cash Flow PDF downloaded', 45 * }); 46 * 47 * @example 48 * // Inside a try/catch around an export API call 49 * try { 50 * const { blob, fileName } = await api.exportPdf(projectId); 51 * downloadBlob(blob, fileName || 'Export.pdf', { successMessage: 'PDF downloaded' }); 52 * } catch (err) { 53 * if (err.name === 'AbortError') return; 54 * window.showToast?.('Export failed', 'error', { description: err.message }); 55 * } 56 */ 57export function downloadBlob(blob, fileName = 'download', options = {}) { 58 if (!blob) return; 59 60 const url = URL.createObjectURL(blob); 61 const a = document.createElement('a'); 62 a.href = url; 63 a.download = fileName || 'download'; 64 a.style.display = 'none'; 65 document.body.appendChild(a); 66 a.click(); 67 document.body.removeChild(a); 68 URL.revokeObjectURL(url); 69 70 if (options.successMessage) { 71 const toastOpts = options.successDescription 72 ? { description: options.successDescription } 73 : undefined; 74 window.showToast?.(options.successMessage, 'success', toastOpts); 75 } 76} 77 78/** 79 * Trigger a browser download for a server URL (no blob in hand) â e.g. the 80 * FILE_READY notification auto-download, where the backend generated the file 81 * and the notification carries its link. For in-memory blobs use downloadBlob. 82 * 83 * @param {string} url - Absolute or app-relative file URL. Required. 84 * @param {string} [fileName] - Optional download-attribute filename hint. 85 */ 86export function downloadUrl(url, fileName = '') { 87 if (!url) return; 88 89 const a = document.createElement('a'); 90 a.href = url; 91 a.download = fileName; 92 a.style.display = 'none'; 93 document.body.appendChild(a); 94 a.click(); 95 document.body.removeChild(a); 96} 97 98/** 99 * Open a Blob in a new browser tab for inline preview (e.g. a PDF the user 100 * wants to look at without saving). The created object URL is revoked after 101 * a delay so the new tab has time to finish loading before the URL invalidates. 102 * 103 * Replaces the boilerplate: 104 * const url = URL.createObjectURL(blob); 105 * window.open(url, '_blank'); 106 * setTimeout(() => URL.revokeObjectURL(url), 60_000); 107 * 108 * @param {Blob} blob - The blob to preview (typically a PDF). Required. 109 * @param {Object} [options] 110 * @param {number} [options.revokeAfterMs=60000] - Delay before revoking the object URL. 111 * 60s is a safe default for a PDF 112 * to load in the new tab. 113 * @param {string} [options.popupBlockedMessage='Popup blocked'] - Toast title shown when 114 * the browser blocks 115 * `window.open`. Pass an 116 * empty string to silence. 117 * @returns {boolean} `true` if the window was opened (or attempted), `false` if the 118 * blob was missing or the browser blocked the popup. 119 * 120 * @example 121 * // Inside a Send modal's "preview" link 122 * await safeAsync(async () => { 123 * const { blob } = await api.fetchPdfBlob(projectId, id); 124 * previewBlob(blob); 125 * }, { label: 'AccInvoices', toast: 'Preview failed' }); 126 */ 127export function previewBlob(blob, options = {}) { 128 if (!blob) return false;
129 const url = URL.createObjectURL(blob); 130 const win = window.open(url, '_blank'); 131 // No cross-tab "PDF loaded" event we can observe, so the timeout is the 132 // pragmatic cleanup â 60s is enough for a PDF to render in the new tab. 133 setTimeout(() => URL.revokeObjectURL(url), options.revokeAfterMs ?? 60_000); 134 if (!win) { 135 const title = options.popupBlockedMessage ?? 'Popup blocked'; 136 if (title) { 137 window.showToast?.(title, 'error', { 138 description: 'Allow popups for this site and try again.', 139 }); 140 } 141 return false; 142 } 143 return true; 144}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.