Skip to content

Commit

Permalink
feat: adding Export Captures button (#168)
Browse files Browse the repository at this point in the history
* feat: added export captures button

* fix: update captures test to resolve CI/CD failed build

* fix: disable button while downloading & close the export form after download & user warning texts
  • Loading branch information
tranquanghuy0801 authored Nov 28, 2021
1 parent 3374b98 commit f59a11c
Show file tree
Hide file tree
Showing 7 changed files with 512 additions and 37,799 deletions.
38,093 changes: 297 additions & 37,796 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"prop-types": "*",
"react": "*",
"react-autosuggest": "^10.0.2",
"react-csv": "^2.0.3",
"react-dom": "*",
"react-fast-compare": "^3.2.0",
"react-infinite": "*",
Expand Down
34 changes: 32 additions & 2 deletions src/components/Captures/CaptureTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useEffect, useState, useContext } from 'react';
import {
Grid,
Table,
Button,
TableHead,
TableBody,
TableRow,
Expand All @@ -10,6 +11,7 @@ import {
TableSortLabel,
Typography,
} from '@material-ui/core';
import { GetApp } from '@material-ui/icons';
import { getDateTimeStringLocale } from '../../common/locale';
import { getVerificationStatus } from '../../common/utils';
import LinkToWebmap from '../common/LinkToWebmap';
Expand All @@ -18,6 +20,7 @@ import { SpeciesContext } from '../../context/SpeciesContext';
import CaptureDetailDialog from '../CaptureDetailDialog';
import { tokenizationStates } from '../../common/variables';
import useStyle from './CaptureTable.styles.js';
import ExportCaptures from 'components/ExportCaptures';

const columns = [
{
Expand All @@ -32,21 +35,25 @@ const columns = [
attr: 'deviceIdentifier',
label: 'Device Identifier',
noSort: false,
renderer: (val) => val,
},
{
attr: 'planterIdentifier',
label: 'Planter Identifier',
noSort: false,
renderer: (val) => val,
},
{
attr: 'verificationStatus',
label: 'Verification Status',
noSort: true,
renderer: (val) => val,
},
{
attr: 'speciesId',
label: 'Species',
noSort: true,
renderer: (val) => val,
},
{
attr: 'tokenId',
Expand Down Expand Up @@ -80,6 +87,7 @@ const CaptureTable = () => {
const speciesContext = useContext(SpeciesContext);
const [isDetailsPaneOpen, setIsDetailsPaneOpen] = useState(false);
const [speciesState, setSpeciesState] = useState({});
const [isOpenExport, setOpenExport] = useState(false);
const classes = useStyle();

useEffect(() => {
Expand Down Expand Up @@ -109,6 +117,10 @@ const CaptureTable = () => {
setIsDetailsPaneOpen(false);
};

const handleOpenExport = () => {
setOpenExport(true);
};

const handlePageChange = (e, page) => {
setPage(page);
};
Expand Down Expand Up @@ -151,7 +163,25 @@ const CaptureTable = () => {
<Typography variant="h5" className={classes.title}>
Captures
</Typography>
{tablePagination()}
<Grid className={classes.cornerTable}>
<Button
variant="outlined"
color="primary"
startIcon={<GetApp />}
className={classes.buttonCsv}
onClick={handleOpenExport}
>
Export Captures
</Button>
<ExportCaptures
isOpen={isOpenExport}
handleClose={() => setOpenExport(false)}
columns={columns}
filter={filter}
speciesState={speciesState}
/>
{tablePagination()}
</Grid>
</Grid>
<Table data-testid="captures-table">
<TableHead>
Expand Down Expand Up @@ -200,7 +230,7 @@ const CaptureTable = () => {
);
};

const formatCell = (capture, speciesState, attr, renderer) => {
export const formatCell = (capture, speciesState, attr, renderer) => {
if (attr === 'id' || attr === 'planterId') {
return (
<LinkToWebmap
Expand Down
10 changes: 10 additions & 0 deletions src/components/Captures/CaptureTable.styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ const useStyle = makeStyles((theme) => ({
title: {
paddingLeft: theme.spacing(4),
},
cornerTable: {
margin: theme.spacing(1),
'&>*': {
display: 'inline-flex',
margin: theme.spacing(1, 1),
},
},
buttonCsv: {
height: 36,
},
}));

export default useStyle;
2 changes: 1 addition & 1 deletion src/components/Captures/CaptureTable.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ describe('Captures', () => {

it('renders headers for captures table', () => {
const table = screen.getByRole(/table/i);
let item = screen.getByText(/Captures/i);
let item = screen.getAllByText(/Captures/i)[0];
expect(item).toBeInTheDocument();
item = within(table).getByText(/Capture ID/i);
expect(item).toBeInTheDocument();
Expand Down
137 changes: 137 additions & 0 deletions src/components/ExportCaptures.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import React, { useState, useContext, useRef } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Checkbox,
FormControl,
FormGroup,
FormControlLabel,
DialogContentText,
} from '@material-ui/core';
import { CapturesContext } from '../context/CapturesContext';
import { CSVLink } from 'react-csv';
import { formatCell } from './Captures/CaptureTable';
import { LinearProgress } from '@material-ui/core';

const useStyle = makeStyles((theme) => ({
container: {
position: 'relative',
padding: theme.spacing(0, 4),
},
textInput: {
margin: theme.spacing(2, 1),
flexGrow: 1,
},
formControl: {
margin: theme.spacing(2),
},
}));

const ExportCaptures = (props) => {
const { isOpen, handleClose, columns, filter, speciesState } = props;
const classes = useStyle();
let nameColumns = {};
columns.forEach(({ attr, renderer }) => {
nameColumns[attr] = { status: true, renderer: renderer };
});
// checkboxes to choose download columns
const [checkedColumns, setCheckColumns] = useState(nameColumns);
const capturesContext = useContext(CapturesContext);
const [downloadData, setDownloadData] = useState([]);
const [loading, setLoading] = useState(false);
const csvLink = useRef();

function handleChange(attr) {
const newStatus = !checkedColumns[attr].status;
setCheckColumns({
...checkedColumns,
[attr]: { ...checkedColumns[attr], status: newStatus },
});
}

function processDownloadData(captures, selectedColumns) {
return captures.map((capture) => {
let formatCapture = {};
Object.keys(selectedColumns).forEach((attr) => {
if (attr === 'id' || attr === 'planterId') {
formatCapture[attr] = capture[attr];
} else {
const renderer = selectedColumns[attr].renderer;
formatCapture[attr] = formatCell(
capture,
speciesState,
attr,
renderer,
);
}
});
return formatCapture;
});
}

async function downloadCaptures() {
setLoading(true);
const filterColumns = Object.entries(checkedColumns).filter(
(val) => val[1].status === true,
);
const selectedColumns = Object.fromEntries(filterColumns);
await capturesContext.getAllCaptures({ filter }).then((response) => {
setDownloadData(processDownloadData(response.data, selectedColumns));
setLoading(false);
});
csvLink.current.link.click();
handleClose();
}

return (
<Dialog
open={isOpen}
onClose={handleClose}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">Export Captures</DialogTitle>
<DialogContent>
<DialogContentText>Select the columns to download:</DialogContentText>
<FormControl component="fieldset" className={classes.formControl}>
<FormGroup>
{columns.map(({ attr, label }) => (
<FormControlLabel
key={attr}
control={
<Checkbox
checked={checkedColumns[attr].status}
onChange={() => handleChange(attr)}
name={attr}
/>
}
label={label}
/>
))}
</FormGroup>
</FormControl>
<DialogContentText>
Only the first 20,000 records will be downloaded.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button
color="primary"
variant="contained"
disabled={loading}
onClick={downloadCaptures}
>
Download
</Button>
<CSVLink data={downloadData} filename={'captures.csv'} ref={csvLink} />
</DialogActions>
{loading && <LinearProgress color="primary" />}
</Dialog>
);
};

export default ExportCaptures;
34 changes: 34 additions & 0 deletions src/context/CapturesContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const CapturesContext = createContext({
getCaptureCount: () => {},
getCapturesAsync: () => {},
getCaptureAsync: () => {},
getAllCaptures: () => {},
updateFilter: () => {},
// getLocationName: () => {},
});
Expand Down Expand Up @@ -131,6 +132,38 @@ export function CapturesProvider(props) {
setCaptures(response.data);
};

const getAllCaptures = async (filterInfo = {}) => {
log.debug('load all captures');
console.log('captures filterInfo -- ', filterInfo);

// if filterInfo contains new values override the defaults in state hooks
const { filter = new FilterModel() } = filterInfo;

const where = filter ? filter.getWhereObj() : {};

const lbFilter = {
where: { ...where },
order: [`${orderBy} ${order}`],
limit: 20000,
fields: {
id: true,
timeCreated: true,
status: true,
active: true,
approved: true,
planterId: true,
planterIdentifier: true,
deviceIdentifier: true,
speciesId: true,
tokenId: true,
},
};

const paramString = `filter=${JSON.stringify(lbFilter)}`;
const response = await queryCapturesApi({ paramString });
return response;
};

const getCaptureAsync = (id) => {
queryCapturesApi({ id })
.then((res) => {
Expand Down Expand Up @@ -195,6 +228,7 @@ export function CapturesProvider(props) {
getCaptureCount,
getCapturesAsync,
getCaptureAsync,
getAllCaptures,
updateFilter,
// getLocationName,
};
Expand Down

0 comments on commit f59a11c

Please sign in to comment.