Merge pull request #924 from nowcommunity/improve-export-filenames

Fix export related issues and change export file name to include name of table and a timestamp
This commit is contained in:
Kari Lintulaakso
2025-10-10 11:47:59 +03:00
committed by GitHub
20 changed files with 33 additions and 12 deletions
+2 -2
View File
@@ -132,8 +132,8 @@ router.get(`/export/:columnfilters/:sorting`, async (req, res) => {
})
res.attachment(`cross_search${currentDateAsString()}.csv`) // filename will get overwritten in frontend when fetching data from this route
// quoteColumns is needed to make sure linebreaks do not mess up the data
const stream = format({ headers: true, quoteColumns: true }).transform(
// this should match the csvConfig in frontend exportRows() function as closely as possible
const stream = format({ delimiter: ',', headers: true, quoteColumns: true }).transform(
transformFunction as FormatterRowTransformFunction<FormatterRow, FormatterRow>
)
+1 -1
View File
@@ -1,3 +1,3 @@
### Cross Search Export
Cross search has a [`/export` route](../../backend/src/routes/crossSearch.ts) which is used by the ["Export Table"](../../../frontend/src/components/CrossSearch/CrossSearchExportButton.tsx) button in a cross-search table view. This route uses the same parameter parsing and validation function as the `/all` route, and it also uses the same SQL query, but with LIMIT set to undefined. The resulting data is [streamed](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) to the user, where the browser will download it as a csv file. See the "Exporting" section in [TableView docs](../frontend/components/TableView.md) for more information.
Cross search has a [`/export` route](../../backend/src/routes/crossSearch.ts) which is used by the ["Export Table"](../../frontend/src/components/CrossSearch/CrossSearchExportMenuItem.tsx) button in a cross-search table view. This route uses the same parameter parsing and validation function as the `/all` route, and it also uses the same SQL query, but with LIMIT set to undefined. The resulting data is [streamed](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) to the user, where the browser will download it as a csv file. See the "Exporting" section in [TableView docs](../frontend/components/TableView.md) for more information.
@@ -14,7 +14,7 @@ export const CrossSearchExportMenuItem = ({ handleClose }: { handleClose: () =>
const token = useUser().token
const fetchOptions = token ? { headers: { Authorization: `Bearer ${token}` } } : {}
const filename = `cross_search${currentDateAsString()}.csv`
const filename = `locality-species-${currentDateAsString()}.csv`
const fetchCSVFile = async () => {
setLoading(true)
@@ -25,6 +25,7 @@ export const StagingView = <T extends MRT_RowData>() => {
<SelectingTable<Reference, T>
buttonText="Add existing reference"
data={data}
title="References"
isError={isError}
columns={referenceTableColumns}
idFieldName="rid"
@@ -16,6 +16,7 @@ export const SelectingTable = <T extends MRT_RowData, ParentType extends object>
buttonText,
data,
columns,
title,
editingAction,
tableRowAction,
fieldName,
@@ -28,6 +29,7 @@ export const SelectingTable = <T extends MRT_RowData, ParentType extends object>
buttonText: string
data: T[] | undefined
columns: MRT_ColumnDef<T>[]
title: string
editingAction?: (object: T) => void
tableRowAction?: (row: T) => void
fieldName: keyof ParentType
@@ -76,6 +78,7 @@ export const SelectingTable = <T extends MRT_RowData, ParentType extends object>
<TableView<T>
data={filteredData}
columns={columns}
title={title}
isFetching={false}
visibleColumns={visibleColumns}
selectorFn={editingAction ?? defaultEditingAction}
@@ -9,6 +9,7 @@ import { generateKml } from '@/util/kml'
import { generateSvg } from '../Map/generateSvg'
import { usePageContext } from '../Page'
import { LocalitySynonymsModal } from './LocalitySynonymsModal'
import { currentDateAsString } from '@/shared/currentDateAsString'
const decimalCount = (num: number) => {
const numAsString = num.toString()
@@ -403,7 +404,8 @@ export const LocalityTable = ({ selectorFn }: { selectorFn?: (newObject: Localit
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'localitiesWithSpecies.csv'
a.download = `localities-with-species-${currentDateAsString()}.csv`
a.click()
}
@@ -416,7 +418,7 @@ export const LocalityTable = ({ selectorFn }: { selectorFn?: (newObject: Localit
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'localities.kml'
a.download = `localities-${currentDateAsString()}.kml`
a.click()
}
@@ -427,7 +429,7 @@ export const LocalityTable = ({ selectorFn }: { selectorFn?: (newObject: Localit
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'map.svg'
a.download = `localities-map-${currentDateAsString()}.svg`
a.click()
}
@@ -192,6 +192,7 @@ export const LithologyTab = () => {
isError={isError}
columns={selectingTableColumns}
data={sedimentaryStructuresData}
title="Sedimentary structures"
fieldName="now_ss"
idFieldName="ss_value"
editingAction={(newSed: SedimentaryStructureValues) => {
@@ -38,6 +38,7 @@ export const MuseumTab = () => {
<SelectingTable<Museum, LocalityDetailsType>
buttonText="Select Museum"
data={museumData}
title="Museums"
isError={isError}
columns={columns}
fieldName="now_mus"
@@ -43,6 +43,7 @@ export const ProjectTab = () => {
<SelectingTable<Project, LocalityDetailsType>
buttonText="Select Project"
data={projectData}
title="Projects"
columns={columns}
isError={isError}
idFieldName="pid"
@@ -44,6 +44,7 @@ export const SpeciesTab = () => {
<SelectingTable<Species, Species>
dataCy="copy_existing_taxonomy_button"
buttonText="Copy existing taxonomy"
title="Copy existing taxonomy"
data={speciesData}
isError={isError}
columns={smallSpeciesTableColumns}
@@ -222,6 +223,7 @@ export const SpeciesTab = () => {
<SelectingTable<Species, LocalityDetailsType>
buttonText="Select Species"
data={speciesData}
title="Species"
isError={isError}
columns={speciesColumns}
fieldName="now_ls"
@@ -64,7 +64,7 @@ export const MuseumTable = ({ selectorFn }: { selectorFn?: (museum: Museum) => v
return (
<TableView<Museum>
title="Museum"
title="Museums"
selectorFn={selectorFn}
idFieldName="museum"
columns={columns}
@@ -124,6 +124,7 @@ export const AuthorTab: React.FC<AuthorTabProps> = ({ field_num_param, tab_name
<SelectingTable<ReferenceAuthorType, ReferenceDetailsType>
buttonText={`Select ${buttonText}`}
data={authorData}
title="Authors"
isError={isError}
columns={authorColumns}
fieldName="ref_authors"
@@ -88,6 +88,7 @@ export const JournalTab: React.FC<JournalTabProps> = ({ tab_name = 'Journal' })
<SelectingTable<ReferenceJournalType, ReferenceDetailsType>
buttonText="Select Journal"
data={journalData}
title="Journals"
isError={isError}
columns={journalColumns}
fieldName="ref_journal"
@@ -106,6 +106,7 @@ export const CoordinatorTab = () => {
<SelectingTable<PersonDetailsType, RegionDetails>
buttonText="Select Coordinator"
data={personsData}
title="Coordinators"
isError={isError}
columns={personTableColumns}
fieldName="now_reg_coord_people"
@@ -31,6 +31,7 @@ export const SequenceTable = ({ selectorFn }: { selectorFn?: (id: Sequence) => v
selectorFn={selectorFn}
idFieldName="sequence"
columns={columns}
title="Sequences"
isFetching={isFetching}
visibleColumns={visibleColumns}
data={sequenceQueryData}
@@ -50,6 +50,7 @@ export const LocalityTab = () => {
<SelectingTable<Editable<Locality>, SpeciesDetailsType>
buttonText="Select Locality"
columns={columns}
title="Localities"
isError={isError}
data={localitiesData}
fieldName="now_ls"
@@ -27,6 +27,7 @@ export const TaxonomyTab = () => {
dataCy="copy_existing_taxonomy_button"
buttonText="Copy existing taxonomy"
data={speciesQueryData}
title="Species"
isError={isError}
columns={smallSpeciesTableColumns}
fieldName="order_name" // this doesn't do anything here but is required
@@ -12,6 +12,7 @@ import { usePageContext } from '../Page'
export const TableToolBar = <T extends MRT_RowData>({
table,
tableName,
combinedExport,
kmlExport,
svgExport,
@@ -21,6 +22,7 @@ export const TableToolBar = <T extends MRT_RowData>({
showNewButton,
}: {
table: MRT_TableInstance<T>
tableName: string
combinedExport?: (lids: number[]) => Promise<void>
kmlExport?: (table: MRT_TableInstance<T>) => void
svgExport?: (table: MRT_TableInstance<T>) => void
@@ -87,7 +89,7 @@ export const TableToolBar = <T extends MRT_RowData>({
) : (
<MenuItem
onClick={() => {
exportRows(table)
exportRows(table, tableName)
handleClose()
}}
>
@@ -57,10 +57,9 @@ export const TableView = <T extends MRT_RowData>({
idFieldName: keyof T
checkRowRestriction?: (row: T) => boolean
selectorFn?: (id: T) => void
// here
tableRowAction?: (row: T) => void
url?: string
title?: string
title: string
combinedExport?: (lids: number[]) => Promise<void>
kmlExport?: (table: MRT_TableInstance<T>) => void
svgExport?: (table: MRT_TableInstance<T>) => void
@@ -259,6 +258,7 @@ export const TableView = <T extends MRT_RowData>({
{title && <Box className="title">{title}</Box>}
<TableToolBar<T>
table={table}
tableName={title}
combinedExport={combinedExport}
kmlExport={kmlExport}
svgExport={svgExport}
@@ -1,15 +1,17 @@
import { MRT_RowData, MRT_TableInstance } from 'material-react-table'
import { mkConfig, generateCsv, download } from 'export-to-csv'
import { AcceptedData } from 'node_modules/export-to-csv/output/lib/types'
import { currentDateAsString } from '@/shared/currentDateAsString'
export type ExportFn<T> = (data: T) => { [k: string]: AcceptedData }
export const exportRows = <T extends MRT_RowData>(table: MRT_TableInstance<T>) => {
export const exportRows = <T extends MRT_RowData>(table: MRT_TableInstance<T>, tableName: string) => {
const csvConfig = mkConfig({
fieldSeparator: ',',
decimalSeparator: '.',
useKeysAsHeaders: true,
quoteStrings: true, // If true, will wrap all values in double quotes
filename: `${tableName.toLowerCase()}-${currentDateAsString()}`,
})
const rowData = table.getPrePaginationRowModel().rows.map(row => {
const r = {} as Record<string, AcceptedData>