/** * Download data as CSV file. * * @param headers - Array of header names. If omitted and data is array of objects, keys will be taken from first object. * @param data - Array of rows. Each row can be either an object (key -> value) or an array of values. * @param filename - optional file name to use for downloaded file * @param delimiter - csv delimiter (default is comma) * @param addBOM - add UTF-8 BOM to the file to make Excel detect UTF-8 correctly * Usage examples: * 1) With headers and array of objects * downloadCsv(['name', 'age'], [{name: 'Alice', age: 25}, {name: 'Bob', age: 30}], 'people.csv'); * 2) Without headers (automatically uses object keys) * downloadCsv(null, [{name: 'Alice', age: 25}, {name: 'Bob', age: 30}], 'people.csv'); * 3) With array-of-arrays * downloadCsv(['col1', 'col2'], [['a', 'b'], ['c', 'd']], 'matrix.csv'); */ export function downloadCsv( headers: string[] | null, headerLabels: string[], data: Array | any[]>, filename = 'data.csv', delimiter = ',', addBOM = true, ) { if (!Array.isArray(data) || data.length === 0) { // still create an empty CSV containing only headers const csvHeader = headers ? headers.join(delimiter) : '' const csvString = addBOM ? '\uFEFF' + csvHeader : csvHeader const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' }) const link = document.createElement('a') link.href = URL.createObjectURL(blob) link.setAttribute('download', filename) document.body.appendChild(link) link.click() document.body.removeChild(link) return } // if headers not provided and rows are objects, take keys from first object let _headers: string[] | null = headers if (!_headers) { const firstRow = data[0] if (typeof firstRow === 'object' && !Array.isArray(firstRow)) { _headers = Object.keys(firstRow) } else if (Array.isArray(firstRow)) { // if rows are arrays and no headers provided, we won't add header row _headers = null } } const escape = (val: unknown) => { if (val === null || typeof val === 'undefined') return '' const str = String(val) const needsQuoting = str.includes(delimiter) || str.includes('\n') || str.includes('\r') || str.includes('"') if (!needsQuoting) return str return '"' + str.replace(/"/g, '""') + '"' } const rows: string[] = data.map((row) => { if (Array.isArray(row)) { return row.map(escape).join(delimiter) } // object row - map using headers if available, otherwise use object values if (_headers && Array.isArray(_headers)) { return _headers.map((h) => escape((row as Record)[h])).join(delimiter) } return Object.values(row).map(escape).join(delimiter) }) const headerRow = headerLabels ? headerLabels.join(delimiter) : _headers ? _headers.join(delimiter) : null const csvString = (addBOM ? '\uFEFF' : '') + [headerRow, ...rows].filter(Boolean).join('\r\n') const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' }) const link = document.createElement('a') link.href = URL.createObjectURL(blob) link.setAttribute('download', filename) document.body.appendChild(link) link.click() document.body.removeChild(link) } /** * Download data as XLS (Excel) file using xlsx library. * * @param headers - Array of header names. If omitted and data is array of objects, keys will be taken from first object. * @param data - Array of rows. Each row can be either an object (key -> value) or an array of values. * @param filename - optional file name to use for downloaded file (default: 'data.xlsx') * @param sheetName - optional sheet name in workbook (default: 'Sheet1') * Usage examples: * 1) With headers and array of objects * await downloadXls(['name', 'age'], [{name: 'Alice', age: 25}, {name: 'Bob', age: 30}], 'people.xlsx'); * 2) Without headers (automatically uses object keys) * await downloadXls(null, [{name: 'Alice', age: 25}, {name: 'Bob', age: 30}], 'people.xlsx'); * 3) With custom sheet name * await downloadXls(['col1', 'col2'], [['a', 'b'], ['c', 'd']], 'matrix.xlsx', 'MyData'); */ export async function downloadXls( headers: string[] | null, headerLabels: string[], data: Array | any[]>, filename = 'data.xlsx', sheetName = 'Sheet1', ) { // Dynamically import xlsx to avoid server-side issues const { utils, write } = await import('xlsx') const { saveAs } = await import('file-saver') if (!Array.isArray(data) || data.length === 0) { // Create empty sheet with headers only const ws = utils.aoa_to_sheet(headers ? [headers] : [[]]) const wb = utils.book_new() utils.book_append_sheet(wb, ws, sheetName) const wbout = write(wb, { bookType: 'xlsx', type: 'array' }) saveAs(new Blob([wbout], { type: 'application/octet-stream' }), filename) return } // if headers not provided and rows are objects, take keys from first object let _headers: string[] | null = headers if (!_headers) { const firstRow = data[0] if (typeof firstRow === 'object' && !Array.isArray(firstRow)) { _headers = Object.keys(firstRow) } else if (Array.isArray(firstRow)) { _headers = null } } // Convert data rows to 2D array const rows: any[][] = data.map((row) => { if (Array.isArray(row)) { return row } // object row - map using headers if available, otherwise use object values if (_headers && Array.isArray(_headers)) { return _headers.map((h) => (row as Record)[h] ?? '') } return Object.values(row) }) // Combine headers/labels and rows for sheet // If caller provided headerLabels (as display labels), prefer them. const sheetHeader = headerLabels ? headerLabels : _headers ? _headers : null const sheetData = sheetHeader ? [sheetHeader, ...rows] : rows // Create worksheet and workbook const ws = utils.aoa_to_sheet(sheetData) const wb = utils.book_new() utils.book_append_sheet(wb, ws, sheetName) // Write and save file const wbout = write(wb, { bookType: 'xlsx', type: 'array' }) saveAs(new Blob([wbout], { type: 'application/octet-stream' }), filename) }