Skip to content

Commit

Permalink
integrate new csv export/download library (#110)
Browse files Browse the repository at this point in the history
  • Loading branch information
jguevarra authored May 24, 2024
1 parent a29ff5b commit 44c0aeb
Show file tree
Hide file tree
Showing 9 changed files with 1,745 additions and 4,393 deletions.
5,861 changes: 1,580 additions & 4,281 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@
"lodash.isequal": "^4.5.0",
"money-clip": "^3.0.1",
"ol": "^6.2.1",
"papaparse": "^5.3.1",
"react": "^16.13.0",
"react-csv": "^2.0.3",
"react-datepicker": "^3.3.0",
"react-dom": "^16.13.0",
"react-dropzone": "^11.3.4",
"react-notification-system": "^0.4.0",
"react-papaparse": "^4.4.0",
"react-scripts": "^5.0.1",
"react-toastify": "^8.0.2",
"react-tooltip": "^4.4.3",
Expand Down
114 changes: 65 additions & 49 deletions src/app-bundles/create-jwt-api-bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,65 +28,81 @@ const shouldSkipToken = (method, path, unless) => {
return skip;
};

const processResponse = response => (
new Promise((resolve, reject) => {
const func = response.status < 400 ? resolve : reject;

// Handle no content - @TODO: test this
if (response.status === 204) {
func({
'status': response.status,
'json': {},
});
} else if (response.status === 401) {
store[doAuthLogout]();
} else {
response.json()
.then(json => func({
'status': response.status,
'json': json,
}))
.catch(e => console.error(e));
}
})
);
// const processResponse = response => (
// new Promise((resolve, reject) => {
// const func = response.status < 400 ? resolve : reject;

// // Handle no content - @TODO: test this
// if (response.status === 204) {
// func({
// 'status': response.status,
// 'json': {},
// });
// } else if (response.status === 401) {
// store[doAuthLogout]();
// } else {
// response.json()
// .then(json => func({
// 'status': response.status,
// 'json': json,
// }))
// .catch(e => console.error(e));
// }
// })
// );

const commonFetch = async (root, path, options, callback) => {
let attempts = 0;
const maxAttempts = 5;
const retryInterval = 5000;

const commonFetch = (root, path, options, callback) => {
fetch(`${root}${path}`, options)
.then(processResponse)
.then(response => {
const callFetch = async () => {
attempts++;
try {
const res = await fetch(`${root}${path}`, options);
const json = await res.json();
if (callback && typeof callback === 'function') {
callback(null, response.json);
callback(null, json);
return;
}
})
.catch(response => {
// console.log(response);
throw new ApiError(response.json, `Request returned a ${response.status}`);
})
.catch(err => {
callback(err);
});
} catch (e) {
if (options.method === 'GET' && attempts < maxAttempts) {
console.error('The following error occured:', e, `Retry ${attempts}`);
await new Promise(resolve => setTimeout(resolve, retryInterval));
await callFetch();
return;
}
else {
console.error(e);
if (callback && typeof callback === 'function') {
callback(e, null);
return;
}
}
}
};

await callFetch();
};

class ApiError extends Error {
constructor(data = {}, ...params) {
super(...params);
// class ApiError extends Error {
// constructor(data = {}, ...params) {
// super(...params);

if (Error.captureStackTrace) {
Error.captureStackTrace(this, ApiError);
}
// if (Error.captureStackTrace) {
// Error.captureStackTrace(this, ApiError);
// }

const dataKeys = Object.keys(data);
// const dataKeys = Object.keys(data);

this.name = 'Api Error';
this.timestamp = new Date();
// this.name = 'Api Error';
// this.timestamp = new Date();

dataKeys.forEach(key => {
this[key] = data[key];
});
};
};
// dataKeys.forEach(key => {
// this[key] = data[key];
// });
// };
// };

const createJwtApiBundle = (opts) => {
const defaults = {
Expand Down
45 changes: 23 additions & 22 deletions src/app-bundles/exports/exports-bundle.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,38 @@
import { toast } from 'react-toastify';
import { tSuccess, tError } from 'common/toast/toastHelper';
import { ExportToCsv } from 'export-to-csv';
import { sitesExportHeaders } from './helper';
import { queryFromObject } from 'utils';

const exportsBundle = {
name: 'exports',

doFetchExportsSites: (params) => ({ dispatch, apiGet }) => {
dispatch({ type: 'EXPORTS_SITES_FETCH_START'});
const toastId = toast.loading('Generating .xlsx file. One moment...');

const uri = `/psapi/export/sites${queryFromObject(params)}`;
getReducer: () => {
const initialData = {
exportData: [],
};

const options = {
filename: `sites-list-${new Date().toISOString()}`,
showLabels: true,
useBom: true,
headers: sitesExportHeaders,
return (state = initialData, { type, payload }) => {
switch (type) {
case 'UPDATE_EXPORT_DATA':
return {
...state,
exportData: payload,
};
default:
return state;
}
};
const csvExporter = new ExportToCsv(options);
},

selectExportData: state => state.exports.exportData,

doFetchExportsSites: (params) => ({ dispatch, apiGet }) => {
const uri = `/psapi/export/sites${queryFromObject(params)}`;

apiGet(uri, (err, body) => {
if (err) {
dispatch({ type: 'EXPORTS_SITES_FETCH_ERROR', payload: err});
tError(toastId, 'Failed to generated file. Please try again later.');
if (!err) {
dispatch({ type: 'UPDATE_EXPORT_DATA', payload: body});
} else {
csvExporter.generateCsv(body);
tSuccess(toastId, 'File Generated!');
dispatch({ type: 'EXPORTS_SITES_FETCH_ERROR', payload: err});
}
});

dispatch({ type: 'EXPORTS_SITES_FETCH_FINISHED'});
},
};

Expand Down
1 change: 1 addition & 0 deletions src/app-bundles/sites-bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,5 +139,6 @@ export default {
dispatch({ type: 'UPDATE_SITE_PARAMS', payload: { ...searchParams, ...paramObj } });
store.doDomainSeasonsFetch(searchParams?.year);
store.doSitesFetch();
store.doFetchExportsSites({ ...searchParams, ...paramObj });
},
};
34 changes: 17 additions & 17 deletions src/app-components/button/button.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import React from 'react';

import { classArray } from 'utils';

import '../../css/utils.scss';
import { classArray } from 'utils.js';

/**
* Reusable button with many options to style and transform.
*
* @param {string} size - one of `['small', 'large']` to size the button to your needs
* @param {string} variant - one of `['primary', 'secondary', 'success', 'warning', 'danger', 'info', 'light', 'dark', 'link']` to apply a class standard style
Expand All @@ -23,23 +21,23 @@ const Button = ({
variant = 'primary',
text = '',
title = '',
href = '',
type = 'button',
icon = null,
isOutline = false,
isDisabled = false,
isActive = false,
handleClick = () => {},
isLoading = false,
handleClick = () => { },
className = '',
...customProps
}) => {
const elem = href ? 'a' : 'button';
const classes = classArray([
'btn',
size && size === 'small' ? 'btn-sm' : size === 'large' ? 'btn-lg' : '',
`btn-${isOutline ? 'outline-' : ''}${variant}`,
isActive && 'active',
isDisabled && 'disabled not-allowed',
'pb-2',
className,
]);

Expand All @@ -50,19 +48,21 @@ const Button = ({
title: title || text,
disabled: isDisabled,
'aria-disabled': isDisabled,
...href ? { href: isDisabled ? null : href } : { onClick: handleClick },
onClick: handleClick,
tabIndex: 0,
...customProps,
};

const Child = () => (
<>
{icon}
{icon && text && <>&nbsp;</>}
{text}
</>
return (
<button {...buttonProps} {...customProps}>
<span className='align-middle'>
{isLoading && <span className='spinner-border spinner-border-sm' role='status' aria-hidden='true'></span>}
{!isLoading && icon}
{!isLoading && icon && text && <>&nbsp;</>}
{!isLoading && text}
</span>
</button>
);

return React.createElement(elem, buttonProps, <Child />);
};

export default Button;
export default Button;
46 changes: 46 additions & 0 deletions src/app-components/button/exportButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from 'react';
import { useCSVDownloader } from 'react-papaparse';

import { classArray } from 'utils.js';

const ExportButton = ({
size = '',
variant = 'primary',
icon = null,
isOutline = false,
isDisabled = false,
filename,
data,
className
}) => {
const { CSVDownloader, Type } = useCSVDownloader();

const classes = classArray([
'btn',
size && size === 'small' ? 'btn-sm' : size === 'large' ? 'btn-lg' : '',
`btn-${isOutline ? 'outline-' : ''}${variant}`,
isDisabled && 'disabled not-allowed',
'pb-2',
className,
]);

return (
<CSVDownloader
role='button'
type={Type?.Button}
filename={filename}
bom={true}
data={data}
className={classes}
title='Export as CSV'
disabled={isDisabled}
aria-disabled={isDisabled}
>
{icon}
<>&nbsp;</>
Export as CSV
</CSVDownloader>
);
};

export default ExportButton;
23 changes: 10 additions & 13 deletions src/app-pages/data-entry/sites-list/components/sites-list-table.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,36 @@ import { connect } from 'redux-bundler-react';
import { AgGridColumn } from 'ag-grid-react/lib/agGridColumn';
import { AgGridReact } from 'ag-grid-react/lib/agGridReact';

import Button from 'app-components/button';
import Icon from 'app-components/icon';
import SiteIdCellRenderer from 'common/gridCellRenderers/siteIdCellRenderer';
import ExportButton from 'app-components/button/exportButton';

import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-balham.css';

import './../../../data-summaries/data-summary.scss';
import Icon from 'app-components/icon/icon';

const SitesListTable = connect(
'doFetchExportsSites',
'selectSitesData',
'selectSitesParams',
'selectExportData',
({
doFetchExportsSites,
sitesData,
sitesParams,
exportData
}) => {
const cellStyle = (params) => ({
backgroundColor: params.data.bkgColor,
});

return (
<div className='pt-3'>
<Button
size='small'
<ExportButton
variant='info'
text='Export to CSV'
className='btn-width'
icon={<Icon icon='download' />}
onClick={() => doFetchExportsSites(sitesParams)}
size='small'
isOutline
isDisabled={sitesData.length === 0}
isDisabled={sitesData?.length === 0}
filename={`sites-list-${new Date().toISOString()}`}
data={exportData}
icon={<Icon icon='download' />}
/>
<div className='ag-theme-balham mt-2' style={{ height: '600px', width: '100%' }}>
<AgGridReact
Expand Down
Loading

0 comments on commit 44c0aeb

Please sign in to comment.