From 6575a7403bcc920b56ae6903232cf3ee8d09952b Mon Sep 17 00:00:00 2001 From: reidmeyer Date: Sun, 16 Aug 2020 14:01:01 +0800 Subject: [PATCH 1/7] feat(download csv of incident table): uses json2csv. filters data updates package.json re #2292 --- package.json | 2 + src/incidents/list/ViewIncidentsTable.tsx | 120 +++++++++++++++++----- 2 files changed, 95 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index 5df817faf6..abf0c57f4e 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@hospitalrun/components": "~1.16.0", "@reduxjs/toolkit": "~1.4.0", "@types/escape-string-regexp": "~2.0.1", + "@types/json2csv": "~5.0.1", "@types/pouchdb-find": "~6.3.4", "bootstrap": "~4.5.0", "date-fns": "~2.15.0", @@ -15,6 +16,7 @@ "i18next": "~19.6.0", "i18next-browser-languagedetector": "~6.0.0", "i18next-xhr-backend": "~3.2.2", + "json2csv": "~5.0.1", "lodash": "^4.17.15", "node-sass": "~4.14.0", "pouchdb": "~7.2.1", diff --git a/src/incidents/list/ViewIncidentsTable.tsx b/src/incidents/list/ViewIncidentsTable.tsx index d57532e3f4..b4a57cc58b 100644 --- a/src/incidents/list/ViewIncidentsTable.tsx +++ b/src/incidents/list/ViewIncidentsTable.tsx @@ -1,5 +1,6 @@ -import { Spinner, Table } from '@hospitalrun/components' +import { Spinner, Table, Dropdown } from '@hospitalrun/components' import format from 'date-fns/format' +import { Parser } from 'json2csv' import React from 'react' import { useHistory } from 'react-router' @@ -22,33 +23,98 @@ function ViewIncidentsTable(props: Props) { return } + // filter data + const exportData = [{}] + let first = true + if (data != null) { + data.forEach((elm) => { + const entry = { + code: elm.code, + date: format(new Date(elm.date), 'yyyy-MM-dd hh:mm a'), + reportedBy: elm.reportedBy, + reportedOn: format(new Date(elm.reportedOn), 'yyyy-MM-dd hh:mm a'), + status: elm.status, + } + if (first) { + exportData[0] = entry + first = false + } else { + exportData.push(entry) + } + }) + } + + function downloadCSV() { + const fields = Object.keys(exportData[0]) + const opts = { fields } + const parser = new Parser(opts) + const csv = parser.parse(exportData) + console.log(csv) + + const filename = 'IncidenntsCSV.csv' + const text = csv + const element = document.createElement('a') + element.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(text)}`) + element.setAttribute('download', filename) + + element.style.display = 'none' + document.body.appendChild(element) + + element.click() + + document.body.removeChild(element) + } + + const dropdownItems = [ + { + onClick: function runfun() { + downloadCSV() + }, + text: 'CSV', + }, + ] + return ( - row.id} - data={data} - columns={[ - { label: t('incidents.reports.code'), key: 'code' }, - { - label: t('incidents.reports.dateOfIncident'), - key: 'date', - formatter: (row) => (row.date ? format(new Date(row.date), 'yyyy-MM-dd hh:mm a') : ''), - }, - { - label: t('incidents.reports.reportedBy'), - key: 'reportedBy', - formatter: (row) => extractUsername(row.reportedBy), - }, - { - label: t('incidents.reports.reportedOn'), - key: 'reportedOn', - formatter: (row) => - row.reportedOn ? format(new Date(row.reportedOn), 'yyyy-MM-dd hh:mm a') : '', - }, - { label: t('incidents.reports.status'), key: 'status' }, - ]} - actionsHeaderText={t('actions.label')} - actions={[{ label: t('actions.view'), action: (row) => history.push(`incidents/${row.id}`) }]} - /> + <> +
row.id} + data={data} + columns={[ + { + label: t('incidents.reports.code'), + key: 'code', + }, + { + label: t('incidents.reports.dateOfIncident'), + key: 'date', + formatter: (row) => (row.date ? format(new Date(row.date), 'yyyy-MM-dd hh:mm a') : ''), + }, + { + label: t('incidents.reports.reportedBy'), + key: 'reportedBy', + formatter: (row) => extractUsername(row.reportedBy), + }, + { + label: t('incidents.reports.reportedOn'), + key: 'reportedOn', + formatter: (row) => + row.reportedOn ? format(new Date(row.reportedOn), 'yyyy-MM-dd hh:mm a') : '', + }, + { + label: t('incidents.reports.status'), + key: 'status', + }, + ]} + actionsHeaderText={t('actions.label')} + actions={[ + { + label: t('actions.view'), + action: (row) => history.push(`incidents/${row.id}`), + }, + ]} + /> + + ) } From fd2ec6961f51dc7259b2c8872e60461a4eb1ae38 Mon Sep 17 00:00:00 2001 From: reidmeyer Date: Mon, 17 Aug 2020 19:06:26 +0800 Subject: [PATCH 2/7] fix(typo): fixed typo and used locale re #2292 --- src/incidents/list/ViewIncidentsTable.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/incidents/list/ViewIncidentsTable.tsx b/src/incidents/list/ViewIncidentsTable.tsx index b4a57cc58b..c52f3f1de0 100644 --- a/src/incidents/list/ViewIncidentsTable.tsx +++ b/src/incidents/list/ViewIncidentsTable.tsx @@ -51,7 +51,9 @@ function ViewIncidentsTable(props: Props) { const csv = parser.parse(exportData) console.log(csv) - const filename = 'IncidenntsCSV.csv' + const incidentsText = t('incidents.label') + const filename = incidentsText.concat('.csv') + const text = csv const element = document.createElement('a') element.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(text)}`) From a5f60b228ceccc7030b36c4498ec1c227897092a Mon Sep 17 00:00:00 2001 From: reidmeyer Date: Tue, 18 Aug 2020 20:55:03 +0800 Subject: [PATCH 3/7] feat(incidentscsv): fixes re #2292 --- src/incidents/list/ViewIncidentsTable.tsx | 59 ++++++++++++------- .../enUs/translations/incidents/index.ts | 1 + 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/src/incidents/list/ViewIncidentsTable.tsx b/src/incidents/list/ViewIncidentsTable.tsx index c52f3f1de0..eecad38e6e 100644 --- a/src/incidents/list/ViewIncidentsTable.tsx +++ b/src/incidents/list/ViewIncidentsTable.tsx @@ -25,34 +25,42 @@ function ViewIncidentsTable(props: Props) { // filter data const exportData = [{}] - let first = true - if (data != null) { - data.forEach((elm) => { - const entry = { - code: elm.code, - date: format(new Date(elm.date), 'yyyy-MM-dd hh:mm a'), - reportedBy: elm.reportedBy, - reportedOn: format(new Date(elm.reportedOn), 'yyyy-MM-dd hh:mm a'), - status: elm.status, - } - if (first) { - exportData[0] = entry - first = false - } else { - exportData.push(entry) - } - }) + + function populateExportData() { + let first = true + if (data != null) { + data.forEach((elm) => { + const entry = { + code: elm.code, + date: format(new Date(elm.date), 'yyyy-MM-dd hh:mm a'), + reportedBy: elm.reportedBy, + reportedOn: format(new Date(elm.reportedOn), 'yyyy-MM-dd hh:mm a'), + status: elm.status, + } + if (first) { + exportData[0] = entry + first = false + } else { + exportData.push(entry) + } + }) + } } function downloadCSV() { + populateExportData() + const fields = Object.keys(exportData[0]) const opts = { fields } const parser = new Parser(opts) const csv = parser.parse(exportData) - console.log(csv) const incidentsText = t('incidents.label') - const filename = incidentsText.concat('.csv') + + const filename = incidentsText + .concat('-') + .concat(format(new Date(Date.now()), 'yyyy-MM-dd--hh-mma')) + .concat('.csv') const text = csv const element = document.createElement('a') @@ -76,8 +84,20 @@ function ViewIncidentsTable(props: Props) { }, ] + const dropStyle = { + marginLeft: 'auto', // note the capital 'W' here + marginBottom: '4px', // 'ms' is the only lowercase vendor prefix + } + return ( <> +
row.id} data={data} @@ -115,7 +135,6 @@ function ViewIncidentsTable(props: Props) { }, ]} /> - ) } diff --git a/src/shared/locales/enUs/translations/incidents/index.ts b/src/shared/locales/enUs/translations/incidents/index.ts index 6223bafb29..f39d87cb59 100644 --- a/src/shared/locales/enUs/translations/incidents/index.ts +++ b/src/shared/locales/enUs/translations/incidents/index.ts @@ -17,6 +17,7 @@ export default { resolve: 'Resolve Incident', dateOfIncident: 'Date of Incident', department: 'Department', + download: 'Download', category: 'Category', categoryItem: 'Category Item', description: 'Description of Incident', From 1fb36c97f16760ba8867e61d3413f34b3611a7be Mon Sep 17 00:00:00 2001 From: reidmeyer Date: Sun, 23 Aug 2020 20:03:42 +0800 Subject: [PATCH 4/7] feat(incidentscsv): made functions more abstract made getcsv and downloadlink functions re #2292 --- src/incidents/list/ViewIncidentsTable.tsx | 19 +++---------------- src/shared/util/DataHelpers.tsx | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 16 deletions(-) create mode 100644 src/shared/util/DataHelpers.tsx diff --git a/src/incidents/list/ViewIncidentsTable.tsx b/src/incidents/list/ViewIncidentsTable.tsx index eecad38e6e..4c77371a48 100644 --- a/src/incidents/list/ViewIncidentsTable.tsx +++ b/src/incidents/list/ViewIncidentsTable.tsx @@ -1,10 +1,10 @@ import { Spinner, Table, Dropdown } from '@hospitalrun/components' import format from 'date-fns/format' -import { Parser } from 'json2csv' import React from 'react' import { useHistory } from 'react-router' import useTranslator from '../../shared/hooks/useTranslator' +import { DownloadLink, getCSV } from '../../shared/util/DataHelpers' import { extractUsername } from '../../shared/util/extractUsername' import useIncidents from '../hooks/useIncidents' import IncidentSearchRequest from '../model/IncidentSearchRequest' @@ -50,10 +50,7 @@ function ViewIncidentsTable(props: Props) { function downloadCSV() { populateExportData() - const fields = Object.keys(exportData[0]) - const opts = { fields } - const parser = new Parser(opts) - const csv = parser.parse(exportData) + const csv = getCSV(exportData) const incidentsText = t('incidents.label') @@ -62,17 +59,7 @@ function ViewIncidentsTable(props: Props) { .concat(format(new Date(Date.now()), 'yyyy-MM-dd--hh-mma')) .concat('.csv') - const text = csv - const element = document.createElement('a') - element.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(text)}`) - element.setAttribute('download', filename) - - element.style.display = 'none' - document.body.appendChild(element) - - element.click() - - document.body.removeChild(element) + DownloadLink(csv, filename) } const dropdownItems = [ diff --git a/src/shared/util/DataHelpers.tsx b/src/shared/util/DataHelpers.tsx new file mode 100644 index 0000000000..c8797f3235 --- /dev/null +++ b/src/shared/util/DataHelpers.tsx @@ -0,0 +1,22 @@ +import { Parser } from 'json2csv' + +export function getCSV(data: T[]): string { + const fields = Object.keys(data[0]) + const opts = { fields } + const parser = new Parser(opts) + const csv = parser.parse(data) + return csv +} + +export function DownloadLink(data: string, fileName: string) { + const text = data + const element = document.createElement('a') + element.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(text)}`) + element.setAttribute('download', fileName) + + element.style.display = 'none' + document.body.appendChild(element) + element.click() + + return document.body.removeChild(element) +} From dc3c93eeecac464ba0e3fdc9111d1ed32ac566b2 Mon Sep 17 00:00:00 2001 From: reidmeyer Date: Sun, 6 Sep 2020 13:51:21 +0800 Subject: [PATCH 5/7] feat(added tests): incidentsDownloadCSV re #2292 --- .../list/ViewIncidentsTable.test.tsx | 56 ++++++++++++++++++- .../shared/utils/DataHelpers.test.ts | 33 +++++++++++ src/incidents/list/ViewIncidentsTable.tsx | 44 +++++++-------- 3 files changed, 109 insertions(+), 24 deletions(-) create mode 100644 src/__tests__/shared/utils/DataHelpers.test.ts diff --git a/src/__tests__/incidents/list/ViewIncidentsTable.test.tsx b/src/__tests__/incidents/list/ViewIncidentsTable.test.tsx index d01ef96a52..b3df031f4c 100644 --- a/src/__tests__/incidents/list/ViewIncidentsTable.test.tsx +++ b/src/__tests__/incidents/list/ViewIncidentsTable.test.tsx @@ -1,4 +1,4 @@ -import { Table } from '@hospitalrun/components' +import { Table, Dropdown } from '@hospitalrun/components' import { mount, ReactWrapper } from 'enzyme' import { createMemoryHistory } from 'history' import React from 'react' @@ -6,7 +6,7 @@ import { act } from 'react-dom/test-utils' import { Router } from 'react-router' import IncidentFilter from '../../../incidents/IncidentFilter' -import ViewIncidentsTable from '../../../incidents/list/ViewIncidentsTable' +import ViewIncidentsTable, { populateExportData } from '../../../incidents/list/ViewIncidentsTable' import IncidentSearchRequest from '../../../incidents/model/IncidentSearchRequest' import IncidentRepository from '../../../shared/db/IncidentRepository' import Incident from '../../../shared/model/Incident' @@ -73,6 +73,58 @@ describe('View Incidents Table', () => { expect(incidentsTable.prop('actionsHeaderText')).toEqual('actions.label') }) + it('should display a download button', async () => { + const expectedIncidents: Incident[] = [ + { + id: 'incidentId1', + code: 'someCode', + date: new Date(2020, 7, 4, 0, 0, 0, 0).toISOString(), + reportedOn: new Date(2020, 8, 4, 0, 0, 0, 0).toISOString(), + reportedBy: 'com.test:user', + status: 'reported', + } as Incident, + ] + const { wrapper } = await setup({ status: IncidentFilter.all }, expectedIncidents) + + const dropDownButton = wrapper.find(Dropdown) + expect(dropDownButton.exists()).toBeTruthy() + }) + + it('should populate export data correctly', async () => { + const data = [ + { + category: 'asdf', + categoryItem: 'asdf', + code: 'I-eClU6OdkR', + createdAt: '2020-09-06T04:02:38.011Z', + date: '2020-09-06T04:02:32.855Z', + department: 'asdf', + description: 'asdf', + id: 'af9f968f-61d9-47c3-9321-5da3f381c38b', + reportedBy: 'some user', + reportedOn: '2020-09-06T04:02:38.011Z', + rev: '1-91d1ba60588b779c9554c7e20e15419c', + status: 'reported', + updatedAt: '2020-09-06T04:02:38.011Z', + }, + ] + + const expectedExportData = [ + { + code: 'I-eClU6OdkR', + date: '2020-09-06 12:02 PM', + reportedBy: 'some user', + reportedOn: '2020-09-06 12:02 PM', + status: 'reported', + }, + ] + + const exportData = [{}] + populateExportData(exportData, data) + + expect(exportData).toEqual(expectedExportData) + }) + it('should format the data correctly', async () => { const expectedIncidents: Incident[] = [ { diff --git a/src/__tests__/shared/utils/DataHelpers.test.ts b/src/__tests__/shared/utils/DataHelpers.test.ts new file mode 100644 index 0000000000..e1bf52e693 --- /dev/null +++ b/src/__tests__/shared/utils/DataHelpers.test.ts @@ -0,0 +1,33 @@ +import { getCSV, DownloadLink } from '../../../shared/util/DataHelpers' + +describe('Use Data Helpers util', () => { + it('should construct csv', () => { + const input = [ + { + code: 'I-eClU6OdkR', + date: '2020-09-06 12:02 PM', + reportedBy: 'some user', + reportedOn: '2020-09-06 12:02 PM', + status: 'reported', + }, + ] + const output = getCSV(input) + const expectedOutput = + '"code","date","reportedBy","reportedOn","status"\r\n"I-eClU6OdkR","2020-09-06 12:02 PM","some user","2020-09-06 12:02 PM","reported"' + expect(output).toMatch(expectedOutput) + }) + + it('should download data as expected', () => { + const response = DownloadLink('data to be downloaded', 'filename.txt') + + const element = document.createElement('a') + element.setAttribute( + 'href', + `data:text/plain;charset=utf-8,${encodeURIComponent('data to be downloaded')}`, + ) + element.setAttribute('download', 'filename.txt') + + element.style.display = 'none' + expect(response).toEqual(element) + }) +}) diff --git a/src/incidents/list/ViewIncidentsTable.tsx b/src/incidents/list/ViewIncidentsTable.tsx index 4c77371a48..e38f723de2 100644 --- a/src/incidents/list/ViewIncidentsTable.tsx +++ b/src/incidents/list/ViewIncidentsTable.tsx @@ -13,6 +13,27 @@ interface Props { searchRequest: IncidentSearchRequest } +export function populateExportData(dataToPopulate: any, theData: any) { + let first = true + if (theData != null) { + theData.forEach((elm: any) => { + const entry = { + code: elm.code, + date: format(new Date(elm.date), 'yyyy-MM-dd hh:mm a'), + reportedBy: elm.reportedBy, + reportedOn: format(new Date(elm.reportedOn), 'yyyy-MM-dd hh:mm a'), + status: elm.status, + } + if (first) { + dataToPopulate[0] = entry + first = false + } else { + dataToPopulate.push(entry) + } + }) + } +} + function ViewIncidentsTable(props: Props) { const { searchRequest } = props const { t } = useTranslator() @@ -26,29 +47,8 @@ function ViewIncidentsTable(props: Props) { // filter data const exportData = [{}] - function populateExportData() { - let first = true - if (data != null) { - data.forEach((elm) => { - const entry = { - code: elm.code, - date: format(new Date(elm.date), 'yyyy-MM-dd hh:mm a'), - reportedBy: elm.reportedBy, - reportedOn: format(new Date(elm.reportedOn), 'yyyy-MM-dd hh:mm a'), - status: elm.status, - } - if (first) { - exportData[0] = entry - first = false - } else { - exportData.push(entry) - } - }) - } - } - function downloadCSV() { - populateExportData() + populateExportData(exportData, data) const csv = getCSV(exportData) From 02294e6b7c0c21173dede054fb23fdf4d47a3c96 Mon Sep 17 00:00:00 2001 From: reidmeyer Date: Sun, 6 Sep 2020 14:33:35 +0800 Subject: [PATCH 6/7] fix(incidentscvstests): fixed timezone test bug re #2292 --- src/__tests__/incidents/list/ViewIncidentsTable.test.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/__tests__/incidents/list/ViewIncidentsTable.test.tsx b/src/__tests__/incidents/list/ViewIncidentsTable.test.tsx index b3df031f4c..c0168d6794 100644 --- a/src/__tests__/incidents/list/ViewIncidentsTable.test.tsx +++ b/src/__tests__/incidents/list/ViewIncidentsTable.test.tsx @@ -1,4 +1,5 @@ import { Table, Dropdown } from '@hospitalrun/components' +import format from 'date-fns/format' import { mount, ReactWrapper } from 'enzyme' import { createMemoryHistory } from 'history' import React from 'react' @@ -112,9 +113,9 @@ describe('View Incidents Table', () => { const expectedExportData = [ { code: 'I-eClU6OdkR', - date: '2020-09-06 12:02 PM', + date: format(new Date(data[0].date), 'yyyy-MM-dd hh:mm a'), reportedBy: 'some user', - reportedOn: '2020-09-06 12:02 PM', + reportedOn: format(new Date(data[0].reportedOn), 'yyyy-MM-dd hh:mm a'), status: 'reported', }, ] From d577970aa13c5afb440ec3ae885efceee9c58739 Mon Sep 17 00:00:00 2001 From: reidmeyer Date: Sun, 6 Sep 2020 15:07:17 +0800 Subject: [PATCH 7/7] feat(incidentscsvtest): newline bug in tests re #2292 --- src/__tests__/shared/utils/DataHelpers.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__tests__/shared/utils/DataHelpers.test.ts b/src/__tests__/shared/utils/DataHelpers.test.ts index e1bf52e693..16342f7414 100644 --- a/src/__tests__/shared/utils/DataHelpers.test.ts +++ b/src/__tests__/shared/utils/DataHelpers.test.ts @@ -11,9 +11,9 @@ describe('Use Data Helpers util', () => { status: 'reported', }, ] - const output = getCSV(input) + const output = getCSV(input).replace(/(\r\n|\n|\r)/gm, '') const expectedOutput = - '"code","date","reportedBy","reportedOn","status"\r\n"I-eClU6OdkR","2020-09-06 12:02 PM","some user","2020-09-06 12:02 PM","reported"' + '"code","date","reportedBy","reportedOn","status""I-eClU6OdkR","2020-09-06 12:02 PM","some user","2020-09-06 12:02 PM","reported"' expect(output).toMatch(expectedOutput) })