diff --git a/src/components/data-preview/data-preview-data-table/data-preview-data-table.spec.js b/src/components/data-preview/data-preview-data-table/data-preview-data-table.spec.js index a44ba453c3..7986b4642a 100644 --- a/src/components/data-preview/data-preview-data-table/data-preview-data-table.spec.js +++ b/src/components/data-preview/data-preview-data-table/data-preview-data-table.spec.js @@ -669,7 +669,7 @@ describe('react-table', () => { ); expect(smallTableDownloadData.getState().csv).toEqual([ ['Record Date', 'String Value', 'String Value with Commas'], - ['2023-07-12', 'just a normal string', '"comma, separated, list"'], + ['2023-07-12', 'just a normal string', 'comma, separated, list'], ]); }); }); diff --git a/src/components/data-preview/data-preview-data-table/data-preview-data-table.tsx b/src/components/data-preview/data-preview-data-table/data-preview-data-table.tsx index 64b47c2694..a8ae7d9885 100644 --- a/src/components/data-preview/data-preview-data-table/data-preview-data-table.tsx +++ b/src/components/data-preview/data-preview-data-table/data-preview-data-table.tsx @@ -1,7 +1,7 @@ import React, { FunctionComponent, useContext, useEffect, useState } from 'react'; import { IDataTableProps } from '../../../models/IDataTableProps'; import { smallTableDownloadData } from '../../../recoil/smallTableDownloadData'; -import { constructDateHeader, getSortedColumnsData } from '../../dtg-table/data-table-helper'; +import { getSortedColumnsData } from '../../dtg-table/data-table-helper'; import { json2xml } from 'xml-js'; import { overlayContainerNoFooter, rawDataTableContainer } from './data-preview-data-table.module.scss'; import TableFooter from '../../table-components/table-footer/table-footer'; @@ -9,6 +9,7 @@ import DataPreviewDataTableBody from './data-preview-data-table-body/data-previe import DataPreviewDataTableHeader from './data-preview-data-table-header/data-preview-data-table-header'; import { DataTableContext } from '../data-preview-context'; import { getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, Table, useReactTable } from '@tanstack/react-table'; +import { setCsvDownload } from '../../table-components/helpers/data-download-helper'; const DataPreviewDataTable: FunctionComponent = ({ setTableColumnSortData, @@ -155,7 +156,7 @@ const DataPreviewDataTable: FunctionComponent = ({ } if (!table.getSortedRowModel()?.flatRows[0]?.original.columnName) { - let downloadData = []; + const downloadData = []; const downloadHeaders = []; const downloadHeaderKeys = []; table.getHeaderGroups()[0].headers.forEach(header => { @@ -181,20 +182,7 @@ const DataPreviewDataTable: FunctionComponent = ({ }; setSmallTableJSONData(JSON.stringify({ data: downloadData })); setSmallTableXMLData(json2xml(JSON.stringify(xmlData), { compact: true })); - downloadData = downloadData.map(entry => { - const dataWithTextQualifiers = []; - Object.values(entry).forEach(val => { - const stringValue = String(val ?? ''); - dataWithTextQualifiers.push(stringValue.includes(',') ? `"${stringValue}"` : stringValue); - }); - return dataWithTextQualifiers; - }); - downloadData.unshift(downloadHeaders); - if (hasDownloadTimestamp) { - const dateHeader = constructDateHeader(datasetName, dateRange); - downloadData.unshift(dateHeader); - } - setSmallTableCSVData(downloadData); + setCsvDownload(downloadData, downloadHeaders, setSmallTableCSVData, hasDownloadTimestamp, datasetName, dateRange); } }, [columnVisibility, table.getSortedRowModel(), table.getVisibleFlatColumns()]); diff --git a/src/components/data-preview/data-preview-filter-section/data-preview-download-wrapper/download-button/csv-direct-download/csv-direct-download.spec.js b/src/components/data-preview/data-preview-filter-section/data-preview-download-wrapper/download-button/csv-direct-download/csv-direct-download.spec.js index 067b8986eb..aee02d4a46 100644 --- a/src/components/data-preview/data-preview-filter-section/data-preview-download-wrapper/download-button/csv-direct-download/csv-direct-download.spec.js +++ b/src/components/data-preview/data-preview-filter-section/data-preview-download-wrapper/download-button/csv-direct-download/csv-direct-download.spec.js @@ -9,7 +9,7 @@ describe('CSV Direct Download Button', () => { it('renders a download link', () => { const { getByRole } = render(CSV} />); const downloadLink = getByRole('link', { hidden: true }); - expect(downloadLink).toHaveAttribute('href', 'data:text/csv;charset=utf-8,header 1,header 2'); + expect(downloadLink).toHaveAttribute('href', 'data:text/csv;charset=utf-8,\ufeff"header 1","header 2"'); }); it('direct CSV download with timestamp', () => { diff --git a/src/components/data-preview/data-preview-filter-section/data-preview-download-wrapper/download-button/csv-direct-download/csv-direct-download.tsx b/src/components/data-preview/data-preview-filter-section/data-preview-download-wrapper/download-button/csv-direct-download/csv-direct-download.tsx index c03a98205d..f7e1e197ef 100644 --- a/src/components/data-preview/data-preview-filter-section/data-preview-download-wrapper/download-button/csv-direct-download/csv-direct-download.tsx +++ b/src/components/data-preview/data-preview-filter-section/data-preview-download-wrapper/download-button/csv-direct-download/csv-direct-download.tsx @@ -40,7 +40,6 @@ const CsvDirectDownload: FunctionComponent = ({ filename, downloadData, handleCl onClick={handleClick} ref={ref} aria-hidden={downloadTimestamp} - enclosingCharacter="" tabIndex={downloadTimestamp ? -1 : 0} > {!downloadTimestamp && children} diff --git a/src/components/download-wrapper/download-item-button/download-item-button.jsx b/src/components/download-wrapper/download-item-button/download-item-button.jsx index a25e9ef3ee..d886a87b9e 100644 --- a/src/components/download-wrapper/download-item-button/download-item-button.jsx +++ b/src/components/download-wrapper/download-item-button/download-item-button.jsx @@ -104,7 +104,6 @@ const DownloadItemButton = ({ onClick={() => clickFunction(true)} ref={ref} aria-hidden={true} - enclosingCharacter="" tabIndex={-1} /> @@ -115,7 +114,6 @@ const DownloadItemButton = ({ data={smallTableCSVData} filename={downloadName + '.csv'} onClick={() => clickFunction(true)} - enclosingCharacter="" > {children} diff --git a/src/components/dtg-table/dtg-table.spec.js b/src/components/dtg-table/dtg-table.spec.js index 0a980989cc..d12a517f6a 100644 --- a/src/components/dtg-table/dtg-table.spec.js +++ b/src/components/dtg-table/dtg-table.spec.js @@ -722,7 +722,7 @@ describe('DTG Table Nested Table Detail View', () => { ); expect(smallTableDownloadData.getState().csv).toEqual([ ['Record Date', 'String Value', 'String Value with Commas'], - ['2023-07-12', 'just a normal string', '"comma, separated, list"'], + ['2023-07-12', 'just a normal string', 'comma, separated, list'], ]); }); diff --git a/src/components/table-components/helpers/data-download-helper.js b/src/components/table-components/helpers/data-download-helper.js index ac7473c743..cab0f7d216 100644 --- a/src/components/table-components/helpers/data-download-helper.js +++ b/src/components/table-components/helpers/data-download-helper.js @@ -25,16 +25,20 @@ export const getDownloadData = (tableRowModel, downloadHeaderKeys) => { return downloadData; }; +const csvFormulaaTrigger = /^[=+\-@\t\r]/; + +export const escapedCsvCell = value => { + let stringValue = String(value ?? ''); + + if (csvFormulaaTrigger.test(stringValue) && isNaN(Number(stringValue))) { + stringValue = `'${stringValue}`; + } + return stringValue.replace(/"/g, '""'); +}; + export const getDataWithTextQualifiers = downloadData => { if (downloadData) { - return downloadData.map(entry => { - const dataWithTextQualifiers = []; - Object.values(entry).forEach(val => { - const stringValue = String(val ?? ''); - dataWithTextQualifiers.push(stringValue.includes(',') ? `"${stringValue}"` : stringValue); - }); - return dataWithTextQualifiers; - }); + return downloadData.map(entry => Object.values(entry).map(escapedCsvCell)); } };