diff --git a/tdrs-frontend/src/actions/reports.js b/tdrs-frontend/src/actions/reports.js index 8ddc58787..23c040d03 100644 --- a/tdrs-frontend/src/actions/reports.js +++ b/tdrs-frontend/src/actions/reports.js @@ -1,12 +1,21 @@ import { logErrorToServer } from '../utils/eventLogger' import { v4 as uuidv4 } from 'uuid' +import axios from 'axios' export const SET_FILE = 'SET_FILE' export const CLEAR_FILE = 'CLEAR_FILE' export const SET_FILE_ERROR = 'SET_FILE_ERROR' export const CLEAR_ERROR = 'CLEAR_ERROR' +export const START_FILE_DOWNLOAD = 'START_FILE_DOWNLOAD' +export const FILE_DOWNLOAD_ERROR = 'FILE_DOWNLOAD_ERROR' + +export const FETCH_FILE_LIST = 'FETCH_FILE_LIST' +export const SET_FILE_LIST = 'SET_FILE_LIST' +export const FETCH_FILE_LIST_ERROR = 'FETCH_FILE_LIST_ERROR' +export const DOWNLOAD_DIALOG_OPEN = 'DOWNLOAD_DIALOG_OPEN' + export const clearFile = ({ section }) => (dispatch) => { dispatch({ type: CLEAR_FILE, payload: { section } }) } @@ -14,6 +23,79 @@ export const clearFile = ({ section }) => (dispatch) => { export const clearError = ({ section }) => (dispatch) => { dispatch({ type: CLEAR_ERROR, payload: { section } }) } +/** + Get a list of files that can be downloaded, mainly used to decide + if the download button should be present. +*/ +export const getAvailableFileList = ({ year, quarter = 'Q1' }) => async ( + dispatch +) => { + dispatch({ + type: FETCH_FILE_LIST, + }) + try { + const response = await axios.get(`/mock_api/reports/${year}/${quarter}`, { + responseType: 'json', + }) + dispatch({ + type: SET_FILE_LIST, + payload: { + data: response.data, + }, + }) + } catch (error) { + dispatch({ + type: FETCH_FILE_LIST_ERROR, + payload: { + error, + year, + quarter, + }, + }) + } +} + +export const download = ({ year, quarter = 'Q1', section }) => async ( + dispatch +) => { + try { + if (!year) throw new Error('No year was provided to download action.') + dispatch({ type: START_FILE_DOWNLOAD }) + + const response = await axios.get( + `/mock_api/reports/data-files/${year}/${quarter}/${section}`, + { + responseType: 'blob', + } + ) + const data = response.data + + // Create a link and associate it with the blob returned from the file + // download - this allows us to trigger the file download dialog without + // having to change the route or reload the page. + const url = window.URL.createObjectURL(new Blob([data])) + const link = document.createElement('a') + + link.href = url + link.setAttribute('download', `${year}.${quarter}.${section}.txt`) + + document.body.appendChild(link) + + // Click the link to actually prompt the file download + link.click() + + // Cleanup afterwards to prevent unwanted side effects + document.body.removeChild(link) + dispatch({ type: DOWNLOAD_DIALOG_OPEN }) + } catch (error) { + dispatch({ + type: FILE_DOWNLOAD_ERROR, + payload: { error, year, quarter, section }, + }) + return false + } + return true +} // Main Redux action to add files to the state export const upload = ({ file, section }) => async (dispatch) => { diff --git a/tdrs-frontend/src/actions/reports.test.js b/tdrs-frontend/src/actions/reports.test.js index 17c10fb19..23f748e86 100644 --- a/tdrs-frontend/src/actions/reports.test.js +++ b/tdrs-frontend/src/actions/reports.test.js @@ -1,9 +1,15 @@ +import axios from 'axios' import thunk from 'redux-thunk' import configureStore from 'redux-mock-store' +import { v4 as uuidv4 } from 'uuid' import { SET_SELECTED_YEAR, + START_FILE_DOWNLOAD, SET_FILE, + SET_FILE_LIST, + FILE_DOWNLOAD_ERROR, + DOWNLOAD_DIALOG_OPEN, SET_FILE_ERROR, SET_SELECTED_STT, SET_SELECTED_QUARTER, @@ -11,6 +17,8 @@ import { setStt, setYear, upload, + download, + getAvailableFileList, } from './reports' describe('actions/reports', () => { @@ -55,6 +63,75 @@ describe('actions/reports', () => { }) }) + it('should dispatch OPEN_FILE_DIALOG when a file has been successfully downloaded', async () => { + window.URL.createObjectURL = jest.fn(() => null) + axios.get.mockImplementationOnce(() => + Promise.resolve({ + data: 'Some text', + }) + ) + const store = mockStore() + + await store.dispatch( + download({ + year: 2020, + section: 'Active Case Data', + }) + ) + const actions = store.getActions() + + expect(actions[0].type).toBe(START_FILE_DOWNLOAD) + try { + expect(actions[1].type).toBe(DOWNLOAD_DIALOG_OPEN) + } catch (err) { + throw actions[1].payload.error + } + }) + + it('should dispatch SET_FILE_LIST', async () => { + axios.get.mockImplementationOnce(() => + Promise.resolve({ + data: [ + { + fileName: 'test.txt', + section: 'Active Case Data', + uuid: uuidv4(), + }, + { + fileName: 'testb.txt', + section: 'Closed Case Data', + uuid: uuidv4(), + }, + ], + }) + ) + const store = mockStore() + + await store.dispatch( + getAvailableFileList({ + year: 2020, + }) + ) + const actions = store.getActions() + try { + expect(actions[1].type).toBe(SET_FILE_LIST) + } catch (err) { + throw actions[1].payload.error + } + }) + + it('should dispatch FILE_DOWNLOAD_ERROR if no year is provided to download', async () => { + const store = mockStore() + + await store.dispatch( + download({ + section: 'Active Case Data', + }) + ) + const actions = store.getActions() + expect(actions[0].type).toBe(FILE_DOWNLOAD_ERROR) + }) + it('should dispatch SET_SELECTED_YEAR', async () => { const store = mockStore() diff --git a/tdrs-frontend/src/actions/sttList.js b/tdrs-frontend/src/actions/sttList.js index d3e7f5c25..590fd378e 100644 --- a/tdrs-frontend/src/actions/sttList.js +++ b/tdrs-frontend/src/actions/sttList.js @@ -39,6 +39,7 @@ export const fetchSttList = () => async (dispatch) => { }) if (data) { + // shouldn't this logic be done by the backend serializer? data.forEach((item, i) => { if (item.name === 'Federal Government') { data.splice(i, 1) diff --git a/tdrs-frontend/src/components/FileUpload/FileUpload.jsx b/tdrs-frontend/src/components/FileUpload/FileUpload.jsx index 9239ec49d..47d521b7c 100644 --- a/tdrs-frontend/src/components/FileUpload/FileUpload.jsx +++ b/tdrs-frontend/src/components/FileUpload/FileUpload.jsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react' +import React, { useRef, useEffect } from 'react' import PropTypes from 'prop-types' import { useDispatch, useSelector } from 'react-redux' import fileType from 'file-type/browser' @@ -7,8 +7,13 @@ import { clearFile, SET_FILE_ERROR, upload, + download, } from '../../actions/reports' + +import Button from '../Button' + import createFileInputErrorState from '../../utils/createFileInputErrorState' +import { handlePreview, getTargetClassName } from './utils' const INVALID_FILE_ERROR = 'We can’t process that file format. Please provide a plain text file.' @@ -16,12 +21,17 @@ const INVALID_FILE_ERROR = function FileUpload({ section, setLocalAlertState }) { // e.g. 'Aggregate Case Data' => 'aggregate-case-data' // The set of uploaded files in our Redux state - const files = useSelector((state) => state.reports.files) + const { files, year } = useSelector((state) => state.reports) + const dispatch = useDispatch() // e.g. "1 - Active Case Data" => ["1", "Active Case Data"] const [sectionNumber, sectionName] = section.split(' - ') + const hasFile = files?.some((file) => { + return file.section === sectionName && file.uuid + }) + const selectedFile = files.find((file) => sectionName === file.section) const formattedSectionName = selectedFile.section @@ -29,13 +39,29 @@ function FileUpload({ section, setLocalAlertState }) { .map((word) => word.toLowerCase()) .join('-') - const fileName = selectedFile?.fileName + const targetClassName = getTargetClassName(formattedSectionName) + + const fileName = selectedFile?.fileName || 'report.txt' const hasUploadedFile = Boolean(fileName) const ariaDescription = hasUploadedFile ? `Selected File ${selectedFile?.fileName}. To change the selected file, click this button.` : `Drag file here or choose from folder.` + useEffect(() => { + const trySettingPreview = () => { + const previewState = handlePreview(fileName, targetClassName) + if (!previewState) { + setTimeout(trySettingPreview, 100) + } + } + if (hasFile) trySettingPreview() + }, [hasFile, fileName, targetClassName]) + + const downloadFile = ({ target }) => { + dispatch(clearError({ section: sectionName })) + dispatch(download({ section: sectionName, year })) + } const inputRef = useRef(null) const validateAndUploadFile = (event) => { @@ -167,6 +193,17 @@ function FileUpload({ section, setLocalAlertState }) { aria-hidden="false" data-errormessage={INVALID_FILE_ERROR} /> +