From bca4311a028168662906a5c7c192ce49c48f1a8f Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Mon, 6 Jan 2025 12:55:59 -0500 Subject: [PATCH 01/53] boilerplate + nav (with wrong permissions) --- tdrs-frontend/src/components/Header/Header.jsx | 10 ++++++++++ .../src/components/Reports/FRAReports.jsx | 9 +++++++++ .../src/components/Reports/FRAReports.test.js | 10 ++++++++++ tdrs-frontend/src/components/Reports/index.js | 3 +++ tdrs-frontend/src/components/Routes/Routes.js | 15 ++++++++++++++- 5 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 tdrs-frontend/src/components/Reports/FRAReports.jsx create mode 100644 tdrs-frontend/src/components/Reports/FRAReports.test.js diff --git a/tdrs-frontend/src/components/Header/Header.jsx b/tdrs-frontend/src/components/Header/Header.jsx index b77de65f7..468dea4ed 100644 --- a/tdrs-frontend/src/components/Header/Header.jsx +++ b/tdrs-frontend/src/components/Header/Header.jsx @@ -127,6 +127,16 @@ function Header() { href="/data-files" /> + + + {(userAccessRequestPending || userAccessRequestApproved) && ( ( +
+

FRA Reports

+
+) + +export default FRAReports diff --git a/tdrs-frontend/src/components/Reports/FRAReports.test.js b/tdrs-frontend/src/components/Reports/FRAReports.test.js new file mode 100644 index 000000000..0578168bb --- /dev/null +++ b/tdrs-frontend/src/components/Reports/FRAReports.test.js @@ -0,0 +1,10 @@ +import React from 'react' +import { FRAReports } from '.' +import { render } from '@testing-library/react' + +describe('FRA Reports Page', () => { + it('Renders', () => { + const { getByText } = render() + expect(getByText('FRA Reports')).toBeInTheDocument() + }) +}) diff --git a/tdrs-frontend/src/components/Reports/index.js b/tdrs-frontend/src/components/Reports/index.js index cb80a4814..f6541cca1 100644 --- a/tdrs-frontend/src/components/Reports/index.js +++ b/tdrs-frontend/src/components/Reports/index.js @@ -1,3 +1,6 @@ import Reports from './Reports' +import FRAReports from './FRAReports' export default Reports + +export { FRAReports } diff --git a/tdrs-frontend/src/components/Routes/Routes.js b/tdrs-frontend/src/components/Routes/Routes.js index 530777cf2..dbe3ee681 100644 --- a/tdrs-frontend/src/components/Routes/Routes.js +++ b/tdrs-frontend/src/components/Routes/Routes.js @@ -5,7 +5,7 @@ import SplashPage from '../SplashPage' import Profile from '../Profile' import PrivateRoute from '../PrivateRoute' import LoginCallback from '../LoginCallback' -import Reports from '../Reports' +import Reports, { FRAReports } from '../Reports' import { useSelector } from 'react-redux' import { accountIsInReview } from '../../selectors/auth' @@ -50,6 +50,19 @@ const AppRoutes = () => { } /> + + + + } + /> Date: Thu, 9 Jan 2025 17:14:44 -0500 Subject: [PATCH 02/53] search form components + state --- .../src/components/Reports/FRAReports.jsx | 400 +++++++++++++++++- .../src/components/Reports/Reports.jsx | 26 +- tdrs-frontend/src/components/Reports/utils.js | 24 ++ tdrs-frontend/src/selectors/auth.js | 6 + 4 files changed, 427 insertions(+), 29 deletions(-) create mode 100644 tdrs-frontend/src/components/Reports/utils.js diff --git a/tdrs-frontend/src/components/Reports/FRAReports.jsx b/tdrs-frontend/src/components/Reports/FRAReports.jsx index 9933e0e73..2318cfd1e 100644 --- a/tdrs-frontend/src/components/Reports/FRAReports.jsx +++ b/tdrs-frontend/src/components/Reports/FRAReports.jsx @@ -1,9 +1,401 @@ -import React from 'react' +import React, { useState, createContext, useContext } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import classNames from 'classnames' -const FRAReports = () => ( -
-

FRA Reports

+import Button from '../Button' +import STTComboBox from '../STTComboBox' +import { quarters, constructYearOptions } from './utils' +import { accountCanSelectStt } from '../../selectors/auth' + +// const FRAContext = createContext({ +// reportType: null, +// fiscalYear: null, +// fiscalQuarter: null, +// selectedFile: null, +// submissionHistory: null, +// }) + +const SelectSTT = ({ valid, value, setValue }) => ( +
+ +
+) + +const SelectReportType = ({ valid, value, setValue }) => ( +
+
+ File Type +
+ setValue('workOutcomesForTanfExiters')} + /> + +
+
+ setValue('secondarySchoolAttainment')} + /> + +
+
+ setValue('supplementalWorkOutcomes')} + /> + +
+
+
+) + +const SelectFiscalYear = ({ valid, value, setValue }) => ( +
+ +
+) + +const SelectQuarter = ({ valid, value, setValue }) => ( +
+
) +const FiscalQuarterExplainer = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + +
Identifying the right Fiscal Year and Quarter
Fiscal QuarterCalendar Period
Quarter 1Oct 1 - Dec 31
Quarter 2Jan 1 - Mar 31
Quarter 3Apr 1 - Jun 30
Quarter 4Jul 1 - Sep 30
+) + +const SearchForm = ({ handleSearch }) => { + // const [selectedStt, setSelectedStt] = useState(null) + // const [selectedReportType, setSelectedReportType] = useState(null) + // const [selectedFiscalYear, setSelectedFiscalYear] = useState(null) + // const [selectedFiscalQuarter, setSelectedFiscalQuarter] = useState(null) + + const needsSttSelection = useSelector(accountCanSelectStt) + const sttList = useSelector((state) => state?.stts?.sttList) + const user = useSelector((state) => state.auth.user) + const userProfileStt = user?.stt?.name + const missingStt = !needsSttSelection && !userProfileStt + + const uploadedFiles = [] + const setErrorModalVisible = () => null + const errorsRef = null + const errorsCount = 0 + + const [form, setFormState] = useState({ + errors: 0, + stt: { + value: needsSttSelection ? null : userProfileStt, + valid: false, + touched: false, + }, + reportType: { + value: 'workOutcomesForTanfExiters', + valid: false, + touched: false, + }, + fiscalYear: { + value: '', + valid: false, + touched: false, + }, + fiscalQuarter: { + value: '', + valid: false, + touched: false, + }, + }) + + const setFormValue = (field, value) => { + console.log(`${field}: ${value}`) + const newFormState = { ...form } + + if (!!value) { + newFormState[field].value = value + newFormState[field].valid = true + } + newFormState[field].touched = true + + setFormState(newFormState) + } + + const validateForm = (selectedValues) => { + const validatedForm = { ...form } + let isValid = true + let errors = 0 + + console.log('selected values: ', selectedValues) + + Object.keys(selectedValues).forEach((key) => { + if (!!selectedValues[key]) { + validatedForm[key].valid = true + } else { + validatedForm[key].valid = false + isValid = false + errors += 1 + } + validatedForm[key].touched = true + }) + setFormState({ ...validatedForm, errors }) + return isValid + } + + const onClickSearch = () => { + // if un-uploaded file selection + // "are you sure modal" + + // const currentStt = needsSttSelection ? form.stt.value : userProfileStt + // const stt = sttList?.find((stt) => stt?.name === currentStt) + + const formValues = { + stt: sttList?.find((stt) => stt?.name === form.stt.value), + } + Object.keys(form).forEach((key) => { + if (key !== 'errors' && key !== 'stt') { + formValues[key] = form[key].value + } + }) + + // console.log(form) + + let isValid = validateForm(formValues) + + if (isValid) { + console.log('searching') + handleSearch(formValues) + } else { + console.log('not vlaid') + } + + // setSelectedStt(null) + // setSelectedReportType(null) + // setSelectedFiscalYear(null) + // setSelectedFiscalQuarter(null) + } + + return ( + <> + {missingStt && ( +
+ An STT is not set for this user. +
+ )} + {Boolean(form.errors) && ( +
+ There {errorsCount === 1 ? 'is' : 'are'} {form.errors} error(s) in + this form +
+ )} +
+
+
+ {needsSttSelection && ( + setFormValue('stt', val)} + /> + )} + setFormValue('reportType', val)} + /> +
+
+
+
+ setFormValue('fiscalYear', val)} + /> + setFormValue('fiscalQuarter', val)} + /> + +
+
+ +
+
+
+ + ) +} + +const UploadForm = () => <> + +const SubmissionHistory = () => <> + +const FRAReports = () => { + const isUploadReportToggled = useState(false) + const [reportType, setReportType] = useState(null) + const [fiscalYear, setFiscalYear] = useState(null) + const [fiscalQuarter, setFiscalQuarter] = useState(null) + + const handleSearch = ( + reportTypeValue, + fiscalYearValue, + fiscalQuarterValue + ) => { + setReportType(reportTypeValue) + setFiscalYear(fiscalYearValue) + setFiscalQuarter(fiscalQuarterValue) + // dispatch() + } + + const [selectedFile, setSelectedFile] = useState(null) + + const stt = useSelector((state) => state.stts?.stt) + + // const fraSubmissionHistory = useSelector((state) => state.fraReports) + + // const context = useContext(FRAContext) + + return ( +
+ {/* */} + {/* */} +
+ +
+ {isUploadReportToggled && ( + <> + + + + )} +
+ ) +} + export default FRAReports diff --git a/tdrs-frontend/src/components/Reports/Reports.jsx b/tdrs-frontend/src/components/Reports/Reports.jsx index 19e0f3ec8..6b4814328 100644 --- a/tdrs-frontend/src/components/Reports/Reports.jsx +++ b/tdrs-frontend/src/components/Reports/Reports.jsx @@ -19,6 +19,7 @@ import SegmentedControl from '../SegmentedControl' import SubmissionHistory from '../SubmissionHistory' import ReprocessedModal from '../SubmissionHistory/ReprocessedModal' import { selectPrimaryUserRole } from '../../selectors/auth' +import { quarters, constructYearOptions } from './utils' /** * Reports is the home page for users to file a report. @@ -59,13 +60,6 @@ function Reports() { const [reprocessedModalVisible, setReprocessedModalVisible] = useState(false) const [reprocessedDate, setReprocessedDate] = useState('') - const quarters = { - Q1: 'Quarter 1 (October - December)', - Q2: 'Quarter 2 (January - March)', - Q3: 'Quarter 3 (April - June)', - Q4: 'Quarter 4 (July - September)', - } - const currentStt = isOFAAdmin || isDIGITTeam || isSystemAdmin ? selectedStt : userProfileStt @@ -172,24 +166,6 @@ function Reports() { setTouched((currentForm) => ({ ...currentForm, stt: true })) } - const constructYearOptions = () => { - const years = [] - const today = new Date(Date.now()) - - const fiscalYear = - today.getMonth() > 8 ? today.getFullYear() + 1 : today.getFullYear() - - for (let i = fiscalYear; i >= 2021; i--) { - const option = ( - - ) - years.push(option) - } - return years - } - useEffect(() => { if (sttList.length === 0) { dispatch(fetchSttList()) diff --git a/tdrs-frontend/src/components/Reports/utils.js b/tdrs-frontend/src/components/Reports/utils.js new file mode 100644 index 000000000..c0982c236 --- /dev/null +++ b/tdrs-frontend/src/components/Reports/utils.js @@ -0,0 +1,24 @@ +export const quarters = { + Q1: 'Quarter 1 (October - December)', + Q2: 'Quarter 2 (January - March)', + Q3: 'Quarter 3 (April - June)', + Q4: 'Quarter 4 (July - September)', +} + +export const constructYearOptions = () => { + const years = [] + const today = new Date(Date.now()) + + const fiscalYear = + today.getMonth() > 8 ? today.getFullYear() + 1 : today.getFullYear() + + for (let i = fiscalYear; i >= 2021; i--) { + const option = ( + + ) + years.push(option) + } + return years +} diff --git a/tdrs-frontend/src/selectors/auth.js b/tdrs-frontend/src/selectors/auth.js index 35d915c43..9dbc22536 100644 --- a/tdrs-frontend/src/selectors/auth.js +++ b/tdrs-frontend/src/selectors/auth.js @@ -76,3 +76,9 @@ export const accountCanViewKibana = (state) => export const accountCanViewPlg = (state) => accountStatusIsApproved(state) && ['OFA System Admin', 'Developer'].includes(selectPrimaryUserRole(state)?.name) + +export const accountCanSelectStt = (state) => + accountStatusIsApproved(state) && + ['OFA System Admin', 'OFA Admin', 'DIGIT Team'].includes( + selectPrimaryUserRole(state)?.name + ) From c8ad47833c826b75a450a860fffe24a3d4e71068 Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Fri, 17 Jan 2025 10:48:04 -0500 Subject: [PATCH 03/53] search + upload form components --- .../src/components/Reports/FRAReports.jsx | 261 ++++++++++++++++-- 1 file changed, 244 insertions(+), 17 deletions(-) diff --git a/tdrs-frontend/src/components/Reports/FRAReports.jsx b/tdrs-frontend/src/components/Reports/FRAReports.jsx index 2318cfd1e..95c9cc930 100644 --- a/tdrs-frontend/src/components/Reports/FRAReports.jsx +++ b/tdrs-frontend/src/components/Reports/FRAReports.jsx @@ -1,11 +1,21 @@ -import React, { useState, createContext, useContext } from 'react' +import React, { + useState, + createContext, + useContext, + useRef, + useEffect, +} from 'react' import { useDispatch, useSelector } from 'react-redux' import classNames from 'classnames' +import { fileInput } from '@uswds/uswds/src/js/components' +import fileTypeChecker from 'file-type-checker' import Button from '../Button' import STTComboBox from '../STTComboBox' import { quarters, constructYearOptions } from './utils' import { accountCanSelectStt } from '../../selectors/auth' +import { handlePreview } from '../FileUpload/utils' +import createFileInputErrorState from '../../utils/createFileInputErrorState' // const FRAContext = createContext({ // reportType: null, @@ -15,6 +25,12 @@ import { accountCanSelectStt } from '../../selectors/auth' // submissionHistory: null, // }) +const INVALID_FILE_ERROR = + 'We can’t process that file format. Please provide a plain text file.' + +const INVALID_EXT_ERROR = + 'Invalid extension. Accepted file types are: .txt, .ms##, .ts##, or .ts###.' + const SelectSTT = ({ valid, value, setValue }) => (
( })} name="reportingYears" id="reportingYears" - onChange={setValue} + onChange={(e) => setValue(e.target.value)} value={value} aria-describedby="years-error-alert" > @@ -132,7 +148,7 @@ const SelectQuarter = ({ valid, value, setValue }) => ( })} name="quarter" id="quarter" - onChange={setValue} + onChange={(e) => setValue(e.target.value)} value={value} aria-describedby="quarter-error-alert" > @@ -275,7 +291,7 @@ const SearchForm = ({ handleSearch }) => { let isValid = validateForm(formValues) if (isValid) { - console.log('searching') + console.log('searching:', formValues) handleSearch(formValues) } else { console.log('not vlaid') @@ -352,30 +368,217 @@ const SearchForm = ({ handleSearch }) => { ) } -const UploadForm = () => <> +const UploadForm = ({ + handleCancel, + handleUpload, + handleDownload, + file, + setLocalAlertState, +}) => { + const [error, setError] = useState(null) + const [selectedFile, setSelectedFile] = useState(file || null) + // const [file, setFile] = useState(null) + const inputRef = useRef(null) + + useEffect(() => { + // `init` for the uswds fileInput must be called on the + // initial render for it to load properly + fileInput.init() + }, []) + + useEffect(() => { + const trySettingPreview = () => { + const targetClassName = 'usa-file-input__preview input #fra-file-upload' + const previewState = handlePreview(file?.name, targetClassName) + if (!previewState) { + setTimeout(trySettingPreview, 100) + } + } + if (file?.id) { + trySettingPreview() + } + }, [file]) + + const onFileChanged = (e) => { + setError(null) + setLocalAlertState({ + active: false, + type: null, + message: null, + }) + + // const { name: section } = e.target + const fileInputValue = e.target.files[0] + const input = inputRef.current + const dropTarget = inputRef.current.parentNode + + const blob = fileInputValue.slice(0, 4) + + const filereader = new FileReader() + const types = ['png', 'gif', 'jpeg'] + filereader.onload = () => { + const re = /(\.txt|\.ms\d{2}|\.ts\d{2,3})$/i + if (!re.exec(fileInputValue.name)) { + setError(INVALID_EXT_ERROR) + return + } + + const isImg = fileTypeChecker.validateFileType(filereader.result, types) + + if (isImg) { + createFileInputErrorState(input, dropTarget) + setError(INVALID_FILE_ERROR) + } else { + setSelectedFile(fileInputValue) + } + } + + filereader.readAsArrayBuffer(blob) + } + + const onSubmit = (e) => { + e.preventDefault() + + if (selectedFile && selectedFile.id) { + setLocalAlertState({ + active: true, + type: 'error', + message: 'No changes have been made to data files', + }) + return + } + + handleUpload(selectedFile) + } + + return ( + <> +
+
+ +
+ {error && ( + + )} +
+
+ {'ariaDescription'} +
+ +
+ {selectedFile?.id ? ( + + ) : null} +
+
+ +
+ + + +
+
+ + ) +} const SubmissionHistory = () => <> const FRAReports = () => { - const isUploadReportToggled = useState(false) + const [isUploadReportToggled, setUploadReportToggled] = useState(false) + const [stt, setStt] = useState(null) const [reportType, setReportType] = useState(null) const [fiscalYear, setFiscalYear] = useState(null) const [fiscalQuarter, setFiscalQuarter] = useState(null) + // const [selectedFile, setSelectedFile] = useState(null) + + const alertRef = useRef(null) + const [localAlert, setLocalAlertState] = useState({ + active: false, + type: null, + message: null, + }) + + const handleSearch = ({ + stt: selectedStt, + reportType: selectedReportType, + fiscalYear: selectedFiscalYear, + fiscalQuarter: selectedFiscalQuarter, + }) => { + setStt(selectedStt) + setReportType(selectedReportType) + setFiscalYear(selectedFiscalYear) + setFiscalQuarter(selectedFiscalQuarter) + + const onSearchSuccess = () => setUploadReportToggled(true) + const onSearchError = () => null - const handleSearch = ( - reportTypeValue, - fiscalYearValue, - fiscalQuarterValue - ) => { - setReportType(reportTypeValue) - setFiscalYear(fiscalYearValue) - setFiscalQuarter(fiscalQuarterValue) // dispatch() } - const [selectedFile, setSelectedFile] = useState(null) + const handleUpload = ({ file: selectedFile }) => { + const onFileUploadSuccess = () => + setLocalAlertState({ + active: true, + type: 'success', + message: `Successfully submitted section(s): ${'formattedSections'} on ${new Date().toDateString()}`, + }) + + const onFileUploadError = (error) => + setLocalAlertState({ + active: true, + type: 'error', + message: ''.concat(error.message, ': ', error.response?.data?.file[0]), + }) - const stt = useSelector((state) => state.stts?.stt) + // dispatch() + } + + useEffect(() => { + if (localAlert.active && alertRef && alertRef.current) { + alertRef.current.scrollIntoView({ behavior: 'smooth' }) + } + }, [localAlert, alertRef]) + + // const stt = useSelector((state) => state.stts?.stt) // const fraSubmissionHistory = useSelector((state) => state.fraReports) @@ -390,7 +593,31 @@ const FRAReports = () => {
{isUploadReportToggled && ( <> - +

+ {`${stt.name} - ${reportType.toUpperCase()} - Fiscal Year ${fiscalYear} - ${ + quarters[fiscalQuarter] + }`} +

+ {localAlert.active && ( +
+
+

{localAlert.message}

+
+
+ )} + )} From 7996526209aa64c1f1d218d6035b5758d2052a85 Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Tue, 21 Jan 2025 16:27:05 -0500 Subject: [PATCH 04/53] redux, frontend api setup for fra reports --- tdrs-frontend/src/actions/fraReports.js | 104 ++++++++++++++++++ .../src/components/Reports/FRAReports.jsx | 94 +++++++++------- tdrs-frontend/src/reducers/fraReports.js | 43 ++++++++ tdrs-frontend/src/reducers/index.js | 2 + tdrs-frontend/src/utils/stringUtils.js | 8 ++ 5 files changed, 210 insertions(+), 41 deletions(-) create mode 100644 tdrs-frontend/src/actions/fraReports.js create mode 100644 tdrs-frontend/src/reducers/fraReports.js diff --git a/tdrs-frontend/src/actions/fraReports.js b/tdrs-frontend/src/actions/fraReports.js new file mode 100644 index 000000000..3043d3bc7 --- /dev/null +++ b/tdrs-frontend/src/actions/fraReports.js @@ -0,0 +1,104 @@ +import { v4 as uuidv4 } from 'uuid' +import axios from 'axios' +import axiosInstance from '../axios-instance' +import { objectToUrlParams } from '../utils/stringUtils' + +const BACKEND_URL = process.env.REACT_APP_BACKEND_URL + +export const SET_IS_LOADING_SUBMISSION_HISTORY = + 'SET_IS_LOADING_SUBMISSION_HISTORY' +export const SET_FRA_SUBMISSION_HISTORY = 'SET_FRA_SUBMISSION_HISTORY' +export const SET_IS_UPLOADING_FRA_REPORT = 'SET_IS_UPLOADING_FRA_REPORT' + +export const getFraSubmissionHistory = + ({ stt, reportType, fiscalQuarter, fiscalYear }, onSuccess, onError) => + async (dispatch) => { + dispatch({ + type: SET_IS_LOADING_SUBMISSION_HISTORY, + payload: { isLoadingSubmissionHistory: true }, + }) + + // do work + try { + console.log('params', { stt, reportType, fiscalQuarter, fiscalYear }) + + const requestParams = { + stt: stt.id, + file_type: reportType, + year: fiscalYear, + quarter: fiscalQuarter, + } + console.log('params', requestParams) + console.log(objectToUrlParams(requestParams)) + + const response = await axios.get( + `${BACKEND_URL}/data_files/?${objectToUrlParams(requestParams)}`, + { + responseType: 'json', + } + ) + + dispatch({ + type: SET_FRA_SUBMISSION_HISTORY, + payload: { submissionHistory: response?.data }, + }) + + onSuccess() + } catch (error) { + onError(error) + } + + dispatch({ + type: SET_IS_LOADING_SUBMISSION_HISTORY, + payload: { isLoadingSubmissionHistory: false }, + }) + } + +export const uploadFraReport = + ( + { stt, reportType, fiscalQuarter, fiscalYear, file, user }, + onSuccess, + onError + ) => + async (dispatch) => { + dispatch({ + type: SET_IS_UPLOADING_FRA_REPORT, + payload: { isUploadingFraReport: true }, + }) + + // do work + const formData = new FormData() + const fraReportData = { + file: file, + original_filename: file.name, + slug: uuidv4(), + user: user.id, + section: reportType, + year: fiscalYear, + stt: stt.id, + quarter: fiscalQuarter, + ssp: false, + } + for (const [key, value] of Object.entries(fraReportData)) { + formData.append(key, value) + } + + try { + const response = await axiosInstance.post( + `${process.env.REACT_APP_BACKEND_URL}/data_files/`, + formData, + { + headers: { 'Content-Type': 'multipart/form-data' }, + withCredentials: true, + } + ) + onSuccess() + } catch (error) { + onError(error) + } + + dispatch({ + type: SET_IS_UPLOADING_FRA_REPORT, + payload: { isUploadingFraReport: false }, + }) + } diff --git a/tdrs-frontend/src/components/Reports/FRAReports.jsx b/tdrs-frontend/src/components/Reports/FRAReports.jsx index 95c9cc930..9aba3e994 100644 --- a/tdrs-frontend/src/components/Reports/FRAReports.jsx +++ b/tdrs-frontend/src/components/Reports/FRAReports.jsx @@ -17,6 +17,11 @@ import { accountCanSelectStt } from '../../selectors/auth' import { handlePreview } from '../FileUpload/utils' import createFileInputErrorState from '../../utils/createFileInputErrorState' +import { + getFraSubmissionHistory, + uploadFraReport, +} from '../../actions/fraReports' + // const FRAContext = createContext({ // reportType: null, // fiscalYear: null, @@ -195,15 +200,9 @@ const FiscalQuarterExplainer = () => ( ) -const SearchForm = ({ handleSearch }) => { - // const [selectedStt, setSelectedStt] = useState(null) - // const [selectedReportType, setSelectedReportType] = useState(null) - // const [selectedFiscalYear, setSelectedFiscalYear] = useState(null) - // const [selectedFiscalQuarter, setSelectedFiscalQuarter] = useState(null) - +const SearchForm = ({ handleSearch, user }) => { const needsSttSelection = useSelector(accountCanSelectStt) const sttList = useSelector((state) => state?.stts?.sttList) - const user = useSelector((state) => state.auth.user) const userProfileStt = user?.stt?.name const missingStt = !needsSttSelection && !userProfileStt @@ -270,7 +269,8 @@ const SearchForm = ({ handleSearch }) => { return isValid } - const onClickSearch = () => { + const onClickSearch = (e) => { + e.preventDefault() // if un-uploaded file selection // "are you sure modal" @@ -321,7 +321,7 @@ const SearchForm = ({ handleSearch }) => { this form
)} -
+
{needsSttSelection && ( @@ -351,11 +351,7 @@ const SearchForm = ({ handleSearch }) => { value={form.fiscalQuarter.value} setValue={(val) => setFormValue('fiscalQuarter', val)} /> -
@@ -373,6 +369,7 @@ const UploadForm = ({ handleUpload, handleDownload, file, + localAlert, setLocalAlertState, }) => { const [error, setError] = useState(null) @@ -429,6 +426,7 @@ const UploadForm = ({ createFileInputErrorState(input, dropTarget) setError(INVALID_FILE_ERROR) } else { + console.log('fileInputValue', fileInputValue) setSelectedFile(fileInputValue) } } @@ -448,7 +446,7 @@ const UploadForm = ({ return } - handleUpload(selectedFile) + handleUpload({ file: selectedFile }) } return ( @@ -506,7 +504,9 @@ const UploadForm = ({ @@ -524,11 +524,14 @@ const SubmissionHistory = () => <> const FRAReports = () => { const [isUploadReportToggled, setUploadReportToggled] = useState(false) - const [stt, setStt] = useState(null) - const [reportType, setReportType] = useState(null) - const [fiscalYear, setFiscalYear] = useState(null) - const [fiscalQuarter, setFiscalQuarter] = useState(null) + const [searchFormValues, setSearchFormValues] = useState(null) + // const [stt, setStt] = useState(null) + // const [reportType, setReportType] = useState(null) + // const [fiscalYear, setFiscalYear] = useState(null) + // const [fiscalQuarter, setFiscalQuarter] = useState(null) + const user = useSelector((state) => state.auth.user) // const [selectedFile, setSelectedFile] = useState(null) + const dispatch = useDispatch() const alertRef = useRef(null) const [localAlert, setLocalAlertState] = useState({ @@ -537,21 +540,17 @@ const FRAReports = () => { message: null, }) - const handleSearch = ({ - stt: selectedStt, - reportType: selectedReportType, - fiscalYear: selectedFiscalYear, - fiscalQuarter: selectedFiscalQuarter, - }) => { - setStt(selectedStt) - setReportType(selectedReportType) - setFiscalYear(selectedFiscalYear) - setFiscalQuarter(selectedFiscalQuarter) - - const onSearchSuccess = () => setUploadReportToggled(true) - const onSearchError = () => null - - // dispatch() + const handleSearch = (values) => { + setUploadReportToggled(false) + setSearchFormValues(null) + + const onSearchSuccess = () => { + setUploadReportToggled(true) + setSearchFormValues(values) + } + const onSearchError = (e) => console.error(e) + + dispatch(getFraSubmissionHistory(values, onSearchSuccess, onSearchError)) } const handleUpload = ({ file: selectedFile }) => { @@ -562,14 +561,26 @@ const FRAReports = () => { message: `Successfully submitted section(s): ${'formattedSections'} on ${new Date().toDateString()}`, }) - const onFileUploadError = (error) => + const onFileUploadError = (error) => { + console.log(error) setLocalAlertState({ active: true, type: 'error', - message: ''.concat(error.message, ': ', error.response?.data?.file[0]), + message: ''.concat(error.message, ': ', error.response?.data?.detail), }) + } - // dispatch() + dispatch( + uploadFraReport( + { + ...searchFormValues, + file: selectedFile, + user, + }, + onFileUploadSuccess, + onFileUploadError + ) + ) } useEffect(() => { @@ -589,7 +600,7 @@ const FRAReports = () => { {/* */} {/* */}
- +
{isUploadReportToggled && ( <> @@ -598,8 +609,8 @@ const FRAReports = () => { className="font-serif-xl margin-top-5 margin-bottom-0 text-normal" tabIndex="-1" > - {`${stt.name} - ${reportType.toUpperCase()} - Fiscal Year ${fiscalYear} - ${ - quarters[fiscalQuarter] + {`${searchFormValues.stt.name} - ${searchFormValues.reportType.toUpperCase()} - Fiscal Year ${searchFormValues.fiscalYear} - ${ + quarters[searchFormValues.fiscalQuarter] }`} {localAlert.active && ( @@ -616,6 +627,7 @@ const FRAReports = () => { )} diff --git a/tdrs-frontend/src/reducers/fraReports.js b/tdrs-frontend/src/reducers/fraReports.js new file mode 100644 index 000000000..d7e76396e --- /dev/null +++ b/tdrs-frontend/src/reducers/fraReports.js @@ -0,0 +1,43 @@ +import { + SET_IS_LOADING_SUBMISSION_HISTORY, + SET_FRA_SUBMISSION_HISTORY, + SET_IS_UPLOADING_FRA_REPORT, +} from '../actions/fraReports' + +const initialState = { + isLoadingSubmissionHistory: false, + isUploadingFraReport: false, + submissionHistory: null, +} + +const fraReports = (state = initialState, action) => { + const { type, payload = {} } = action + + switch (type) { + case SET_IS_LOADING_SUBMISSION_HISTORY: { + const { isLoadingSubmissionHistory } = payload + return { + ...state, + isLoadingSubmissionHistory, + } + } + case SET_FRA_SUBMISSION_HISTORY: { + const { submissionHistory } = payload + return { + ...state, + submissionHistory, + } + } + case SET_IS_UPLOADING_FRA_REPORT: { + const { isUploadingFraReport } = payload + return { + ...state, + isUploadingFraReport, + } + } + default: + return state + } +} + +export default fraReports diff --git a/tdrs-frontend/src/reducers/index.js b/tdrs-frontend/src/reducers/index.js index 2757e71b6..e94b726bb 100644 --- a/tdrs-frontend/src/reducers/index.js +++ b/tdrs-frontend/src/reducers/index.js @@ -5,6 +5,7 @@ import auth from './auth' import stts from './sttList' import requestAccess from './requestAccess' import reports from './reports' +import fraReports from './fraReports' /** * Combines all store reducers @@ -18,6 +19,7 @@ const createRootReducer = (history) => stts, requestAccess, reports, + fraReports, }) export default createRootReducer diff --git a/tdrs-frontend/src/utils/stringUtils.js b/tdrs-frontend/src/utils/stringUtils.js index e7c2947ce..40ab03e71 100644 --- a/tdrs-frontend/src/utils/stringUtils.js +++ b/tdrs-frontend/src/utils/stringUtils.js @@ -4,3 +4,11 @@ export const toTitleCase = (str) => /\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() ) + +export const objectToUrlParams = (obj) => { + const arr = [] + Object.keys(obj).forEach((key) => { + arr.push(`${key}=${obj[key]}`) + }) + return `${arr.join('&')}` +} From 7025dab91bb274ac3778700fba3cebecd8f0b5d9 Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Wed, 22 Jan 2025 10:01:54 -0500 Subject: [PATCH 05/53] add error modal --- tdrs-frontend/src/actions/fraReports.js | 3 +- .../src/components/Reports/FRAReports.jsx | 389 ++++++++++-------- 2 files changed, 217 insertions(+), 175 deletions(-) diff --git a/tdrs-frontend/src/actions/fraReports.js b/tdrs-frontend/src/actions/fraReports.js index 3043d3bc7..abf96d8c3 100644 --- a/tdrs-frontend/src/actions/fraReports.js +++ b/tdrs-frontend/src/actions/fraReports.js @@ -18,7 +18,6 @@ export const getFraSubmissionHistory = payload: { isLoadingSubmissionHistory: true }, }) - // do work try { console.log('params', { stt, reportType, fiscalQuarter, fiscalYear }) @@ -66,7 +65,6 @@ export const uploadFraReport = payload: { isUploadingFraReport: true }, }) - // do work const formData = new FormData() const fraReportData = { file: file, @@ -92,6 +90,7 @@ export const uploadFraReport = withCredentials: true, } ) + onSuccess() } catch (error) { onError(error) diff --git a/tdrs-frontend/src/components/Reports/FRAReports.jsx b/tdrs-frontend/src/components/Reports/FRAReports.jsx index 9aba3e994..5d43de632 100644 --- a/tdrs-frontend/src/components/Reports/FRAReports.jsx +++ b/tdrs-frontend/src/components/Reports/FRAReports.jsx @@ -1,10 +1,4 @@ -import React, { - useState, - createContext, - useContext, - useRef, - useEffect, -} from 'react' +import React, { useState, useRef, useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' import classNames from 'classnames' import { fileInput } from '@uswds/uswds/src/js/components' @@ -16,20 +10,13 @@ import { quarters, constructYearOptions } from './utils' import { accountCanSelectStt } from '../../selectors/auth' import { handlePreview } from '../FileUpload/utils' import createFileInputErrorState from '../../utils/createFileInputErrorState' +import Modal from '../Modal' import { getFraSubmissionHistory, uploadFraReport, } from '../../actions/fraReports' -// const FRAContext = createContext({ -// reportType: null, -// fiscalYear: null, -// fiscalQuarter: null, -// selectedFile: null, -// submissionHistory: null, -// }) - const INVALID_FILE_ERROR = 'We can’t process that file format. Please provide a plain text file.' @@ -46,53 +33,27 @@ const SelectSTT = ({ valid, value, setValue }) => (
) -const SelectReportType = ({ valid, value, setValue }) => ( +const SelectReportType = ({ valid, value, setValue, options }) => (
File Type -
- setValue('workOutcomesForTanfExiters')} - /> - -
-
- setValue('secondarySchoolAttainment')} - /> - -
-
- setValue('supplementalWorkOutcomes')} - /> - -
+ + {options.map(({ label, value }, index) => ( +
+ setValue(value)} + /> + +
+ ))}
) @@ -200,41 +161,19 @@ const FiscalQuarterExplainer = () => ( ) -const SearchForm = ({ handleSearch, user }) => { - const needsSttSelection = useSelector(accountCanSelectStt) - const sttList = useSelector((state) => state?.stts?.sttList) - const userProfileStt = user?.stt?.name +const SearchForm = ({ + handleSearch, + reportTypeOptions, + form, + setFormState, + needsSttSelection, + userProfileStt, + sttList, +}) => { const missingStt = !needsSttSelection && !userProfileStt - - const uploadedFiles = [] - const setErrorModalVisible = () => null const errorsRef = null const errorsCount = 0 - const [form, setFormState] = useState({ - errors: 0, - stt: { - value: needsSttSelection ? null : userProfileStt, - valid: false, - touched: false, - }, - reportType: { - value: 'workOutcomesForTanfExiters', - valid: false, - touched: false, - }, - fiscalYear: { - value: '', - valid: false, - touched: false, - }, - fiscalQuarter: { - value: '', - valid: false, - touched: false, - }, - }) - const setFormValue = (field, value) => { console.log(`${field}: ${value}`) const newFormState = { ...form } @@ -248,61 +187,6 @@ const SearchForm = ({ handleSearch, user }) => { setFormState(newFormState) } - const validateForm = (selectedValues) => { - const validatedForm = { ...form } - let isValid = true - let errors = 0 - - console.log('selected values: ', selectedValues) - - Object.keys(selectedValues).forEach((key) => { - if (!!selectedValues[key]) { - validatedForm[key].valid = true - } else { - validatedForm[key].valid = false - isValid = false - errors += 1 - } - validatedForm[key].touched = true - }) - setFormState({ ...validatedForm, errors }) - return isValid - } - - const onClickSearch = (e) => { - e.preventDefault() - // if un-uploaded file selection - // "are you sure modal" - - // const currentStt = needsSttSelection ? form.stt.value : userProfileStt - // const stt = sttList?.find((stt) => stt?.name === currentStt) - - const formValues = { - stt: sttList?.find((stt) => stt?.name === form.stt.value), - } - Object.keys(form).forEach((key) => { - if (key !== 'errors' && key !== 'stt') { - formValues[key] = form[key].value - } - }) - - // console.log(form) - - let isValid = validateForm(formValues) - - if (isValid) { - console.log('searching:', formValues) - handleSearch(formValues) - } else { - console.log('not vlaid') - } - - // setSelectedStt(null) - // setSelectedReportType(null) - // setSelectedFiscalYear(null) - // setSelectedFiscalQuarter(null) - } - return ( <> {missingStt && ( @@ -321,7 +205,7 @@ const SearchForm = ({ handleSearch, user }) => { this form )} - +
{needsSttSelection && ( @@ -333,6 +217,7 @@ const SearchForm = ({ handleSearch, user }) => { )} setFormValue('reportType', val)} />
@@ -368,12 +253,13 @@ const UploadForm = ({ handleCancel, handleUpload, handleDownload, - file, localAlert, setLocalAlertState, + file, + setSelectedFile, }) => { const [error, setError] = useState(null) - const [selectedFile, setSelectedFile] = useState(file || null) + // const [selectedFile, setSelectedFile] = useState(file || null) // const [file, setFile] = useState(null) const inputRef = useRef(null) @@ -437,7 +323,7 @@ const UploadForm = ({ const onSubmit = (e) => { e.preventDefault() - if (selectedFile && selectedFile.id) { + if (file && file.id) { setLocalAlertState({ active: true, type: 'error', @@ -446,7 +332,7 @@ const UploadForm = ({ return } - handleUpload({ file: selectedFile }) + handleUpload({ file }) } return ( @@ -488,7 +374,7 @@ const UploadForm = ({ data-errormessage={'INVALID_FILE_ERROR'} />
- {selectedFile?.id ? ( + {file?.id ? ( @@ -524,13 +408,42 @@ const SubmissionHistory = () => <> const FRAReports = () => { const [isUploadReportToggled, setUploadReportToggled] = useState(false) + const [errorModalVisible, setErrorModalVisible] = useState(false) const [searchFormValues, setSearchFormValues] = useState(null) - // const [stt, setStt] = useState(null) - // const [reportType, setReportType] = useState(null) - // const [fiscalYear, setFiscalYear] = useState(null) - // const [fiscalQuarter, setFiscalQuarter] = useState(null) + const user = useSelector((state) => state.auth.user) - // const [selectedFile, setSelectedFile] = useState(null) + const sttList = useSelector((state) => state?.stts?.sttList) + const needsSttSelection = useSelector(accountCanSelectStt) + const userProfileStt = user?.stt?.name + + const [temporaryFormState, setTemporaryFormState] = useState({ + errors: 0, + stt: { + value: needsSttSelection ? null : userProfileStt, + valid: false, + touched: false, + }, + reportType: { + value: 'workOutcomesForTanfExiters', + valid: false, + touched: false, + }, + fiscalYear: { + value: '', + valid: false, + touched: false, + }, + fiscalQuarter: { + value: '', + valid: false, + touched: false, + }, + }) + const [selectedFile, setSelectedFile] = useState(null) + + // const stt = useSelector((state) => state.stts?.stt) + // const fraSubmissionHistory = useSelector((state) => state.fraReports) + const dispatch = useDispatch() const alertRef = useRef(null) @@ -540,17 +453,107 @@ const FRAReports = () => { message: null, }) - const handleSearch = (values) => { + const reportTypeOptions = [ + { + value: 'workOutcomesForTanfExiters', + label: 'Work Outcomes for TANF Exiters', + }, + { + value: 'secondarySchoolAttainment', + label: 'Secondary School Attainment', + }, + { value: 'supplementalWorkOutcomes', label: 'Supplemental Work Outcomes' }, + ] + + const resetPreviousValues = () => { + setTemporaryFormState({ + errors: 0, + stt: { + ...temporaryFormState.stt, + value: searchFormValues.stt.name, + }, + reportType: { + ...temporaryFormState.reportType, + value: searchFormValues.reportType, + }, + fiscalYear: { + ...temporaryFormState.fiscalYear, + value: searchFormValues.fiscalYear, + }, + fiscalQuarter: { + ...temporaryFormState.fiscalQuarter, + value: searchFormValues.fiscalQuarter, + }, + }) + } + + const validateSearchForm = (selectedValues) => { + const validatedForm = { ...temporaryFormState } + let isValid = true + let errors = 0 + + console.log('selected values: ', selectedValues) + + Object.keys(selectedValues).forEach((key) => { + if (!!selectedValues[key]) { + validatedForm[key].valid = true + } else { + validatedForm[key].valid = false + isValid = false + errors += 1 + } + validatedForm[key].touched = true + }) + + if (!isValid) { + setTemporaryFormState({ ...validatedForm, errors }) + } + + return isValid + } + + const handleSearch = (e) => { + e.preventDefault() + + if (selectedFile && !selectedFile.id) { + setErrorModalVisible(true) + return + } + + const form = temporaryFormState + + const formValues = { + stt: sttList?.find((stt) => stt?.name === form.stt.value), + } + Object.keys(form).forEach((key) => { + if (key !== 'errors' && key !== 'stt') { + formValues[key] = form[key].value + } + }) + + // console.log(form) + + let isValid = validateSearchForm(formValues) + + if (!isValid) { + console.log('not valid') + return + } + + console.log('searching:', formValues) + setUploadReportToggled(false) setSearchFormValues(null) const onSearchSuccess = () => { setUploadReportToggled(true) - setSearchFormValues(values) + setSearchFormValues(formValues) } const onSearchError = (e) => console.error(e) - dispatch(getFraSubmissionHistory(values, onSearchSuccess, onSearchError)) + dispatch( + getFraSubmissionHistory(formValues, onSearchSuccess, onSearchError) + ) } const handleUpload = ({ file: selectedFile }) => { @@ -589,18 +592,33 @@ const FRAReports = () => { } }, [localAlert, alertRef]) - // const stt = useSelector((state) => state.stts?.stt) + const makeHeaderLabel = () => { + if (isUploadReportToggled) { + const { stt, reportType, fiscalQuarter, fiscalYear } = searchFormValues + const reportTypeLabel = reportTypeOptions.find( + (o) => o.value === reportType + ).label + const quarterLabel = quarters[fiscalQuarter] - // const fraSubmissionHistory = useSelector((state) => state.fraReports) + return `${stt.name} - ${reportTypeLabel} - Fiscal Year ${fiscalYear} - ${quarterLabel}` + } - // const context = useContext(FRAContext) + return null + } return ( -
- {/* */} - {/* */} + <>
- +
{isUploadReportToggled && ( <> @@ -609,9 +627,7 @@ const FRAReports = () => { className="font-serif-xl margin-top-5 margin-bottom-0 text-normal" tabIndex="-1" > - {`${searchFormValues.stt.name} - ${searchFormValues.reportType.toUpperCase()} - Fiscal Year ${searchFormValues.fiscalYear} - ${ - quarters[searchFormValues.fiscalQuarter] - }`} + {makeHeaderLabel()} {localAlert.active && (
{ handleUpload={handleUpload} localAlert={localAlert} setLocalAlertState={setLocalAlertState} + file={selectedFile} + setSelectedFile={setSelectedFile} /> )} -
+ + { + setErrorModalVisible(false) + resetPreviousValues() + }, + }, + { + key: '2', + text: 'Discard and Search', + onClick: () => { + setErrorModalVisible(false) + setSelectedFile(null) + handleSearch({ preventDefault: () => null }) + }, + }, + ]} + /> + ) } From bfd1dc1c68839e8ca167adb564d6dac76620e3f1 Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Wed, 22 Jan 2025 11:40:33 -0500 Subject: [PATCH 06/53] fix sttList and class names --- .../src/components/Reports/FRAReports.jsx | 51 +++++++++++++++---- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/tdrs-frontend/src/components/Reports/FRAReports.jsx b/tdrs-frontend/src/components/Reports/FRAReports.jsx index 5d43de632..f2389bf60 100644 --- a/tdrs-frontend/src/components/Reports/FRAReports.jsx +++ b/tdrs-frontend/src/components/Reports/FRAReports.jsx @@ -16,6 +16,7 @@ import { getFraSubmissionHistory, uploadFraReport, } from '../../actions/fraReports' +import { fetchSttList } from '../../actions/sttList' const INVALID_FILE_ERROR = 'We can’t process that file format. Please provide a plain text file.' @@ -257,6 +258,7 @@ const UploadForm = ({ setLocalAlertState, file, setSelectedFile, + section, }) => { const [error, setError] = useState(null) // const [selectedFile, setSelectedFile] = useState(file || null) @@ -335,6 +337,12 @@ const UploadForm = ({ handleUpload({ file }) } + const formattedSectionName = section.toLowerCase().replace(' ', '-') + + const ariaDescription = file + ? `Selected File ${file?.fileName}. To change the selected file, click this button.` + : `Drag file here or choose from folder.` + return ( <> @@ -342,13 +350,13 @@ const UploadForm = ({ className={`usa-form-group ${error ? 'usa-form-group--error' : ''}`} >
{error && (
- {'ariaDescription'} + {ariaDescription}
@@ -416,6 +424,8 @@ const FRAReports = () => { const needsSttSelection = useSelector(accountCanSelectStt) const userProfileStt = user?.stt?.name + console.log(userProfileStt) + const [temporaryFormState, setTemporaryFormState] = useState({ errors: 0, stt: { @@ -439,6 +449,7 @@ const FRAReports = () => { touched: false, }, }) + console.log(temporaryFormState) const [selectedFile, setSelectedFile] = useState(null) // const stt = useSelector((state) => state.stts?.stt) @@ -465,6 +476,12 @@ const FRAReports = () => { { value: 'supplementalWorkOutcomes', label: 'Supplemental Work Outcomes' }, ] + useEffect(() => { + if (sttList.length === 0) { + dispatch(fetchSttList()) + } + }, [dispatch, sttList]) + const resetPreviousValues = () => { setTemporaryFormState({ errors: 0, @@ -505,9 +522,7 @@ const FRAReports = () => { validatedForm[key].touched = true }) - if (!isValid) { - setTemporaryFormState({ ...validatedForm, errors }) - } + setTemporaryFormState({ ...validatedForm, errors }) return isValid } @@ -522,9 +537,15 @@ const FRAReports = () => { const form = temporaryFormState + console.log('form', form) + const formValues = { stt: sttList?.find((stt) => stt?.name === form.stt.value), } + + console.log('formvalues', formValues) + console.log('sttList', sttList) + Object.keys(form).forEach((key) => { if (key !== 'errors' && key !== 'stt') { formValues[key] = form[key].value @@ -592,12 +613,19 @@ const FRAReports = () => { } }, [localAlert, alertRef]) + const getReportTypeLabel = () => { + if (isUploadReportToggled) { + const { reportType } = searchFormValues + return reportTypeOptions.find((o) => o.value === reportType).label + } + + return null + } + const makeHeaderLabel = () => { if (isUploadReportToggled) { const { stt, reportType, fiscalQuarter, fiscalYear } = searchFormValues - const reportTypeLabel = reportTypeOptions.find( - (o) => o.value === reportType - ).label + const reportTypeLabel = getReportTypeLabel() const quarterLabel = quarters[fiscalQuarter] return `${stt.name} - ${reportTypeLabel} - Fiscal Year ${fiscalYear} - ${quarterLabel}` @@ -647,6 +675,7 @@ const FRAReports = () => { setLocalAlertState={setLocalAlertState} file={selectedFile} setSelectedFile={setSelectedFile} + section={getReportTypeLabel()} /> From c6694b07c72236a7b076eca73814af140650b085 Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Wed, 22 Jan 2025 11:48:07 -0500 Subject: [PATCH 07/53] fix vars --- tdrs-frontend/src/components/Reports/FRAReports.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tdrs-frontend/src/components/Reports/FRAReports.jsx b/tdrs-frontend/src/components/Reports/FRAReports.jsx index f2389bf60..5366e1e83 100644 --- a/tdrs-frontend/src/components/Reports/FRAReports.jsx +++ b/tdrs-frontend/src/components/Reports/FRAReports.jsx @@ -376,10 +376,10 @@ const UploadForm = ({ id="fra-file-upload" className="usa-file-input" type="file" - name={'sectionName'} + name={section} aria-describedby={`${formattedSectionName}-file`} aria-hidden="false" - data-errormessage={'INVALID_FILE_ERROR'} + data-errormessage={INVALID_FILE_ERROR} />
{file?.id ? ( @@ -388,7 +388,7 @@ const UploadForm = ({ type="button" onClick={handleDownload} > - Download Section {'sectionNumber'} + Download {section} ) : null}
@@ -582,7 +582,7 @@ const FRAReports = () => { setLocalAlertState({ active: true, type: 'success', - message: `Successfully submitted section(s): ${'formattedSections'} on ${new Date().toDateString()}`, + message: `Successfully submitted section(s): ${formattedSections} on ${new Date().toDateString()}`, }) const onFileUploadError = (error) => { From 71e1e6eeeaefa5eed4e454f789b33c9562f2cd17 Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Thu, 23 Jan 2025 13:19:06 -0500 Subject: [PATCH 08/53] reorg + fix unreferenced var --- .../src/components/Reports/FRAReports.jsx | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tdrs-frontend/src/components/Reports/FRAReports.jsx b/tdrs-frontend/src/components/Reports/FRAReports.jsx index 5366e1e83..52e9ee417 100644 --- a/tdrs-frontend/src/components/Reports/FRAReports.jsx +++ b/tdrs-frontend/src/components/Reports/FRAReports.jsx @@ -476,12 +476,6 @@ const FRAReports = () => { { value: 'supplementalWorkOutcomes', label: 'Supplemental Work Outcomes' }, ] - useEffect(() => { - if (sttList.length === 0) { - dispatch(fetchSttList()) - } - }, [dispatch, sttList]) - const resetPreviousValues = () => { setTemporaryFormState({ errors: 0, @@ -582,7 +576,7 @@ const FRAReports = () => { setLocalAlertState({ active: true, type: 'success', - message: `Successfully submitted section(s): ${formattedSections} on ${new Date().toDateString()}`, + message: `Successfully submitted section(s): ${getReportTypeLabel()} on ${new Date().toDateString()}`, }) const onFileUploadError = (error) => { @@ -607,12 +601,6 @@ const FRAReports = () => { ) } - useEffect(() => { - if (localAlert.active && alertRef && alertRef.current) { - alertRef.current.scrollIntoView({ behavior: 'smooth' }) - } - }, [localAlert, alertRef]) - const getReportTypeLabel = () => { if (isUploadReportToggled) { const { reportType } = searchFormValues @@ -624,7 +612,7 @@ const FRAReports = () => { const makeHeaderLabel = () => { if (isUploadReportToggled) { - const { stt, reportType, fiscalQuarter, fiscalYear } = searchFormValues + const { stt, fiscalQuarter, fiscalYear } = searchFormValues const reportTypeLabel = getReportTypeLabel() const quarterLabel = quarters[fiscalQuarter] @@ -634,6 +622,18 @@ const FRAReports = () => { return null } + useEffect(() => { + if (sttList.length === 0) { + dispatch(fetchSttList()) + } + }, [dispatch, sttList]) + + useEffect(() => { + if (localAlert.active && alertRef && alertRef.current) { + alertRef.current.scrollIntoView({ behavior: 'smooth' }) + } + }, [localAlert, alertRef]) + return ( <>
From 8c3616444d464ffecb375c0a493fbb507e24c559 Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Thu, 23 Jan 2025 13:19:15 -0500 Subject: [PATCH 09/53] search form tests --- .../src/components/Reports/FRAReports.test.js | 244 +++++++++++++++++- 1 file changed, 241 insertions(+), 3 deletions(-) diff --git a/tdrs-frontend/src/components/Reports/FRAReports.test.js b/tdrs-frontend/src/components/Reports/FRAReports.test.js index 0578168bb..3f540b832 100644 --- a/tdrs-frontend/src/components/Reports/FRAReports.test.js +++ b/tdrs-frontend/src/components/Reports/FRAReports.test.js @@ -1,10 +1,248 @@ import React from 'react' import { FRAReports } from '.' -import { render } from '@testing-library/react' +import { fireEvent, waitFor, render } from '@testing-library/react' +import configureStore from '../../configureStore' +import { Provider } from 'react-redux' + +const initialState = { + auth: { + authenticated: false, + }, + stts: { + sttList: [ + { + id: 1, + type: 'state', + code: 'AL', + name: 'Alabama', + ssp: true, + }, + { + id: 2, + type: 'state', + code: 'AK', + name: 'Alaska', + ssp: false, + }, + ], + loading: false, + }, +} + +const mockStore = (initial = {}) => configureStore(initial) describe('FRA Reports Page', () => { it('Renders', () => { - const { getByText } = render() - expect(getByText('FRA Reports')).toBeInTheDocument() + const store = mockStore() + const { getByText, queryByText } = render( + + + + ) + + // search form elements exist + expect(getByText('File Type')).toBeInTheDocument() + expect(getByText('Fiscal Year (October - September)')).toBeInTheDocument() + expect(getByText('Quarter')).toBeInTheDocument() + expect( + getByText('Identifying the right Fiscal Year and Quarter') + ).toBeInTheDocument() + expect(getByText('Work Outcomes for TANF Exiters')).toBeInTheDocument() + + // error and upload for elements do not + expect(queryByText('Submit Report')).not.toBeInTheDocument() + }) + + describe('Search form', () => { + it('Shows STT combobox if admin role', () => { + const state = { + ...initialState, + auth: { + authenticated: true, + user: { + email: 'hi@bye.com', + stt: null, + roles: [{ id: 1, name: 'OFA System Admin', permission: [] }], + account_approval_status: 'Approved', + }, + }, + } + + const store = mockStore(state) + + const { getByText, queryByText } = render( + + + + ) + + expect( + getByText('Associated State, Tribe, or Territory*') + ).toBeInTheDocument() + }) + + it('Does not show STT combobox if not admin', () => { + const state = { + ...initialState, + auth: { + authenticated: true, + user: { + email: 'hi@bye.com', + stt: { + id: 2, + type: 'state', + code: 'AK', + name: 'Alaska', + }, + roles: [{ id: 1, name: 'Data Analyst', permission: [] }], + account_approval_status: 'Approved', + }, + }, + } + + const store = mockStore(state) + + const { queryByText } = render( + + + + ) + + expect( + queryByText('Associated State, Tribe, or Territory*') + ).not.toBeInTheDocument() + }) + + it('Shows missing STT error if STT not set', () => { + const state = { + ...initialState, + auth: { + authenticated: true, + user: { + email: 'hi@bye.com', + stt: null, + roles: [{ id: 1, name: 'Data Analyst', permission: [] }], + account_approval_status: 'Approved', + }, + }, + } + + const store = mockStore(state) + + const { getByText, queryByText } = render( + + + + ) + + expect( + queryByText('Associated State, Tribe, or Territory*') + ).not.toBeInTheDocument() + expect(getByText('An STT is not set for this user.')).toBeInTheDocument() + }) + + it('Shows errors if required values are not set', () => { + const state = { + ...initialState, + auth: { + authenticated: true, + user: { + email: 'hi@bye.com', + stt: null, + roles: [{ id: 1, name: 'OFA System Admin', permission: [] }], + account_approval_status: 'Approved', + }, + }, + } + + const store = mockStore(state) + + const { getByText, queryByText } = render( + + + + ) + + // don't fill out any form values + fireEvent.click(getByText(/Search/, { selector: 'button' })) + + // upload form not displayed + expect(queryByText('Submit Report')).not.toBeInTheDocument() + + // fields all have errors + expect( + getByText('A state, tribe, or territory is required') + ).toBeInTheDocument() + expect(getByText('A fiscal year is required')).toBeInTheDocument() + expect(getByText('A quarter is required')).toBeInTheDocument() + expect(getByText('There are 3 error(s) in this form')).toBeInTheDocument() + }) + + it('Shows upload form once search has been clicked', async () => { + const state = { + ...initialState, + auth: { + authenticated: true, + user: { + email: 'hi@bye.com', + stt: { + id: 2, + type: 'state', + code: 'AK', + name: 'Alaska', + }, + roles: [{ id: 1, name: 'Data Analyst', permission: [] }], + account_approval_status: 'Approved', + }, + }, + } + + const store = mockStore(state) + + const { getByText, queryByText, getByLabelText } = render( + + + + ) + + // fill out the form values before clicking search + const yearsDropdown = getByLabelText('Fiscal Year (October - September)') + fireEvent.change(yearsDropdown, { target: { value: '2021' } }) + + const quarterDropdown = getByLabelText('Quarter') + fireEvent.change(quarterDropdown, { target: { value: 'Q1' } }) + + fireEvent.click(getByText(/Search/, { selector: 'button' })) + + // upload form displayed + await waitFor(() => { + expect( + getByText( + 'Alaska - Work Outcomes for TANF Exiters - Fiscal Year 2021 - Quarter 1 (October - December)' + ) + ).toBeInTheDocument() + expect(getByText('Submit Report')).toBeInTheDocument() + }) + + // fields don't have errors + expect( + queryByText('A state, tribe, or territory is required') + ).not.toBeInTheDocument() + expect(queryByText('A fiscal year is required')).not.toBeInTheDocument() + expect(queryByText('A quarter is required')).not.toBeInTheDocument() + expect( + queryByText('There are 3 error(s) in this form') + ).not.toBeInTheDocument() + }) + }) + + describe('Upload form', () => { + it('Allows text files to be selected and submitted', () => {}) + + it('Shows an error if a non-allowed file type is selected', () => {}) + + it('Shows a message if search is clicked with an non-uploaded file', () => {}) + + it('Does not show a message if search is clicked after uploading a file', () => {}) }) }) From e25ca72af867c2d75d198fc68fb29ddae806108c Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Tue, 28 Jan 2025 08:42:14 -0500 Subject: [PATCH 10/53] temp comment two tests --- .../src/components/Reports/FRAReports.test.js | 183 +++++++++++++++++- 1 file changed, 177 insertions(+), 6 deletions(-) diff --git a/tdrs-frontend/src/components/Reports/FRAReports.test.js b/tdrs-frontend/src/components/Reports/FRAReports.test.js index 3f540b832..71239d10e 100644 --- a/tdrs-frontend/src/components/Reports/FRAReports.test.js +++ b/tdrs-frontend/src/components/Reports/FRAReports.test.js @@ -1,8 +1,9 @@ import React from 'react' -import { FRAReports } from '.' +import * as axios from 'axios' import { fireEvent, waitFor, render } from '@testing-library/react' -import configureStore from '../../configureStore' import { Provider } from 'react-redux' +import { FRAReports } from '.' +import configureStore from '../../configureStore' const initialState = { auth: { @@ -31,6 +32,9 @@ const initialState = { const mockStore = (initial = {}) => configureStore(initial) +const makeTestFile = (name, contents = ['test'], type = 'text/plain') => + new File(contents, name, { type }) + describe('FRA Reports Page', () => { it('Renders', () => { const store = mockStore() @@ -237,12 +241,179 @@ describe('FRA Reports Page', () => { }) describe('Upload form', () => { - it('Allows text files to be selected and submitted', () => {}) + const setup = async () => { + const state = { + ...initialState, + auth: { + authenticated: true, + user: { + email: 'hi@bye.com', + stt: { + id: 2, + type: 'state', + code: 'AK', + name: 'Alaska', + }, + roles: [{ id: 1, name: 'Data Analyst', permission: [] }], + account_approval_status: 'Approved', + }, + }, + } - it('Shows an error if a non-allowed file type is selected', () => {}) + const store = mockStore(state) + const origDispatch = store.dispatch + store.dispatch = jest.fn(origDispatch) + + const component = render( + + + + ) + + const { getByLabelText, getByText } = component + + // fill out the form values before clicking search + const yearsDropdown = getByLabelText('Fiscal Year (October - September)') + fireEvent.change(yearsDropdown, { target: { value: '2021' } }) - it('Shows a message if search is clicked with an non-uploaded file', () => {}) + const quarterDropdown = getByLabelText('Quarter') + fireEvent.change(quarterDropdown, { target: { value: 'Q1' } }) + + fireEvent.click(getByText(/Search/, { selector: 'button' })) + + await waitFor(() => { + expect( + getByText( + 'Alaska - Work Outcomes for TANF Exiters - Fiscal Year 2021 - Quarter 1 (October - December)' + ) + ).toBeInTheDocument() + expect(getByText('Submit Report')).toBeInTheDocument() + }) + + return { ...component, ...store } + } + + // it('Allows text files to be selected and submitted', async () => { + // const { getByText, dispatch, getByRole, container } = await setup() + + // const uploadForm = container.querySelector('#fra-file-upload') + // await waitFor(() => { + // fireEvent.change(uploadForm, { + // target: { files: [makeTestFile('report.txt')] }, + // }) + // }) + + // const submitButton = getByText('Submit Report') + // fireEvent.click(submitButton) + + // // await waitFor(() => getByText('asdfasdf')) + + // await waitFor(() => getByRole('alert')) + // expect(dispatch).toHaveBeenCalledTimes(1) + // }) + + it('Shows an error if a non-allowed file type is selected', async () => { + const { getByText, dispatch, getByRole, container } = await setup() + + const uploadForm = container.querySelector('#fra-file-upload') + fireEvent.change(uploadForm, { + target: { files: [makeTestFile('report.png', ['png'], 'img/png')] }, + }) + await waitFor(() => { + expect( + getByText( + 'Invalid extension. Accepted file types are: .txt, .ms##, .ts##, or .ts###.' + ) + ).toBeInTheDocument() + }) + + const submitButton = getByText('Submit Report', { selector: 'button' }) + fireEvent.click(submitButton) + + await waitFor(() => getByRole('alert')) + expect(dispatch).toHaveBeenCalledTimes(1) + }) + + it('Shows a message if search is clicked with an non-uploaded file', async () => { + const { getByText, container, getByLabelText, queryByText } = + await setup() + + const uploadForm = container.querySelector('#fra-file-upload') + fireEvent.change(uploadForm, { + target: { files: [makeTestFile('report.txt')] }, + }) + + await waitFor(() => { + expect( + getByText('You have selected the file: report.txt') + ).toBeInTheDocument() + }) + + const yearsDropdown = getByLabelText('Fiscal Year (October - September)') + fireEvent.change(yearsDropdown, { target: { value: '2024' } }) + + const quarterDropdown = getByLabelText('Quarter') + fireEvent.change(quarterDropdown, { target: { value: 'Q2' } }) + + await waitFor(() => { + expect( + getByText('Quarter 2 (January - March)', { selector: 'option' }) + .selected + ).toBe(true) + }) + + fireEvent.click(getByText(/Search/, { selector: 'button' })) + + await waitFor(() => + expect(queryByText('Files Not Submitted')).toBeInTheDocument() + ) + }) - it('Does not show a message if search is clicked after uploading a file', () => {}) + // it('Does not show a message if search is clicked after uploading a file', async () => { + // const { + // getByText, + // container, + // getByLabelText, + // queryByText, + // dispatch, + // getByRole, + // } = await setup() + + // const mockAxios = jest.genMockFromModule('axios') + // mockAxios.create = jest.fn(() => mockAxios) + // mockAxios.post.mockResolvedValue({ status: 201 }) + + // const uploadForm = container.querySelector('#fra-file-upload') + // fireEvent.change(uploadForm, { + // target: { files: [makeTestFile('report.txt')] }, + // }) + + // const submitButton = getByText('Submit Report') + // fireEvent.click(submitButton) + + // // await waitFor(() => getByRole('alert')) + // expect(dispatch).toHaveBeenCalledTimes(1) + // expect(mockAxios.create).toHaveBeenCalledTimes(1) + // expect(mockAxios.post).toHaveBeenCalledTimes(1) + + // const yearsDropdown = getByLabelText('Fiscal Year (October - September)') + // fireEvent.change(yearsDropdown, { target: { value: '2024' } }) + + // const quarterDropdown = getByLabelText('Quarter') + // fireEvent.change(quarterDropdown, { target: { value: 'Q2' } }) + + // await waitFor(() => { + // expect( + // getByText('Quarter 2 (January - March)', { selector: 'option' }) + // .selected + // ).toBe(true) + // }) + + // fireEvent.click(getByText(/Search/, { selector: 'button' })) + + // await waitFor(() => + // expect(queryByText('Files Not Submitted')).not.toBeInTheDocument() + // ) + // }) }) }) From dedee5871eead774637cc271517e9b90c81393e2 Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Tue, 28 Jan 2025 08:42:26 -0500 Subject: [PATCH 11/53] fix aria --- tdrs-frontend/src/components/Reports/FRAReports.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tdrs-frontend/src/components/Reports/FRAReports.jsx b/tdrs-frontend/src/components/Reports/FRAReports.jsx index 52e9ee417..06a2ad8b9 100644 --- a/tdrs-frontend/src/components/Reports/FRAReports.jsx +++ b/tdrs-frontend/src/components/Reports/FRAReports.jsx @@ -340,7 +340,7 @@ const UploadForm = ({ const formattedSectionName = section.toLowerCase().replace(' ', '-') const ariaDescription = file - ? `Selected File ${file?.fileName}. To change the selected file, click this button.` + ? `Selected File ${file?.name}. To change the selected file, click this button.` : `Drag file here or choose from folder.` return ( @@ -398,7 +398,7 @@ const UploadForm = ({ From 902ca31d9ab3a25d6463e938badb671215430b4d Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Tue, 28 Jan 2025 08:55:58 -0500 Subject: [PATCH 12/53] fix test --- .../src/components/Reports/FRAReports.test.js | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/tdrs-frontend/src/components/Reports/FRAReports.test.js b/tdrs-frontend/src/components/Reports/FRAReports.test.js index 71239d10e..22dce372c 100644 --- a/tdrs-frontend/src/components/Reports/FRAReports.test.js +++ b/tdrs-frontend/src/components/Reports/FRAReports.test.js @@ -1,5 +1,5 @@ import React from 'react' -import * as axios from 'axios' +import axios from 'axios' import { fireEvent, waitFor, render } from '@testing-library/react' import { Provider } from 'react-redux' import { FRAReports } from '.' @@ -80,9 +80,7 @@ describe('FRA Reports Page', () => { ) - expect( - getByText('Associated State, Tribe, or Territory*') - ).toBeInTheDocument() + expect(getByText('State, Tribe, or Territory*')).toBeInTheDocument() }) it('Does not show STT combobox if not admin', () => { @@ -379,10 +377,6 @@ describe('FRA Reports Page', () => { // getByRole, // } = await setup() - // const mockAxios = jest.genMockFromModule('axios') - // mockAxios.create = jest.fn(() => mockAxios) - // mockAxios.post.mockResolvedValue({ status: 201 }) - // const uploadForm = container.querySelector('#fra-file-upload') // fireEvent.change(uploadForm, { // target: { files: [makeTestFile('report.txt')] }, @@ -392,9 +386,7 @@ describe('FRA Reports Page', () => { // fireEvent.click(submitButton) // // await waitFor(() => getByRole('alert')) - // expect(dispatch).toHaveBeenCalledTimes(1) - // expect(mockAxios.create).toHaveBeenCalledTimes(1) - // expect(mockAxios.post).toHaveBeenCalledTimes(1) + // expect(dispatch).toHaveBeenCalledTimes(2) // const yearsDropdown = getByLabelText('Fiscal Year (October - September)') // fireEvent.change(yearsDropdown, { target: { value: '2024' } }) From f2f42d6a51f146a21eeecbd87a1fab23ebb7eb47 Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Tue, 28 Jan 2025 13:15:50 -0500 Subject: [PATCH 13/53] fix tests and stub future work --- tdrs-frontend/src/actions/fraReports.js | 23 ++-- .../src/components/Reports/FRAReports.jsx | 14 ++- .../src/components/Reports/FRAReports.test.js | 119 +++++++++--------- 3 files changed, 88 insertions(+), 68 deletions(-) diff --git a/tdrs-frontend/src/actions/fraReports.js b/tdrs-frontend/src/actions/fraReports.js index abf96d8c3..130312a0d 100644 --- a/tdrs-frontend/src/actions/fraReports.js +++ b/tdrs-frontend/src/actions/fraReports.js @@ -11,7 +11,11 @@ export const SET_FRA_SUBMISSION_HISTORY = 'SET_FRA_SUBMISSION_HISTORY' export const SET_IS_UPLOADING_FRA_REPORT = 'SET_IS_UPLOADING_FRA_REPORT' export const getFraSubmissionHistory = - ({ stt, reportType, fiscalQuarter, fiscalYear }, onSuccess, onError) => + ( + { stt, reportType, fiscalQuarter, fiscalYear }, + onSuccess = () => null, + onError = () => null + ) => async (dispatch) => { dispatch({ type: SET_IS_LOADING_SUBMISSION_HISTORY, @@ -19,16 +23,12 @@ export const getFraSubmissionHistory = }) try { - console.log('params', { stt, reportType, fiscalQuarter, fiscalYear }) - const requestParams = { stt: stt.id, file_type: reportType, year: fiscalYear, quarter: fiscalQuarter, } - console.log('params', requestParams) - console.log(objectToUrlParams(requestParams)) const response = await axios.get( `${BACKEND_URL}/data_files/?${objectToUrlParams(requestParams)}`, @@ -56,8 +56,8 @@ export const getFraSubmissionHistory = export const uploadFraReport = ( { stt, reportType, fiscalQuarter, fiscalYear, file, user }, - onSuccess, - onError + onSuccess = () => null, + onError = () => null ) => async (dispatch) => { dispatch({ @@ -91,6 +91,15 @@ export const uploadFraReport = } ) + // dispatch( + // getFraSubmissionHistory({ + // stt, + // reportType, + // fiscalQuarter, + // fiscalYear, + // }) + // ) + // or, dispatch the state update if response from upload can contain updated submission history onSuccess() } catch (error) { onError(error) diff --git a/tdrs-frontend/src/components/Reports/FRAReports.jsx b/tdrs-frontend/src/components/Reports/FRAReports.jsx index 06a2ad8b9..a4a362b1d 100644 --- a/tdrs-frontend/src/components/Reports/FRAReports.jsx +++ b/tdrs-frontend/src/components/Reports/FRAReports.jsx @@ -325,6 +325,10 @@ const UploadForm = ({ const onSubmit = (e) => { e.preventDefault() + if (!!error) { + return + } + if (file && file.id) { setLocalAlertState({ active: true, @@ -398,7 +402,7 @@ const UploadForm = ({ @@ -424,8 +428,6 @@ const FRAReports = () => { const needsSttSelection = useSelector(accountCanSelectStt) const userProfileStt = user?.stt?.name - console.log(userProfileStt) - const [temporaryFormState, setTemporaryFormState] = useState({ errors: 0, stt: { @@ -449,7 +451,7 @@ const FRAReports = () => { touched: false, }, }) - console.log(temporaryFormState) + const [selectedFile, setSelectedFile] = useState(null) // const stt = useSelector((state) => state.stts?.stt) @@ -572,12 +574,14 @@ const FRAReports = () => { } const handleUpload = ({ file: selectedFile }) => { - const onFileUploadSuccess = () => + const onFileUploadSuccess = () => { + setSelectedFile(null) // once we have the latest file in submission history, conditional setting of state in constructor should be sufficient setLocalAlertState({ active: true, type: 'success', message: `Successfully submitted section(s): ${getReportTypeLabel()} on ${new Date().toDateString()}`, }) + } const onFileUploadError = (error) => { console.log(error) diff --git a/tdrs-frontend/src/components/Reports/FRAReports.test.js b/tdrs-frontend/src/components/Reports/FRAReports.test.js index 22dce372c..7ec05b138 100644 --- a/tdrs-frontend/src/components/Reports/FRAReports.test.js +++ b/tdrs-frontend/src/components/Reports/FRAReports.test.js @@ -1,5 +1,4 @@ import React from 'react' -import axios from 'axios' import { fireEvent, waitFor, render } from '@testing-library/react' import { Provider } from 'react-redux' import { FRAReports } from '.' @@ -240,6 +239,7 @@ describe('FRA Reports Page', () => { describe('Upload form', () => { const setup = async () => { + window.HTMLElement.prototype.scrollIntoView = () => {} const state = { ...initialState, auth: { @@ -291,24 +291,33 @@ describe('FRA Reports Page', () => { return { ...component, ...store } } - // it('Allows text files to be selected and submitted', async () => { - // const { getByText, dispatch, getByRole, container } = await setup() - - // const uploadForm = container.querySelector('#fra-file-upload') - // await waitFor(() => { - // fireEvent.change(uploadForm, { - // target: { files: [makeTestFile('report.txt')] }, - // }) - // }) + it('Allows text files to be selected and submitted', async () => { + const { getByText, dispatch, getByRole, container } = await setup() - // const submitButton = getByText('Submit Report') - // fireEvent.click(submitButton) + const uploadForm = container.querySelector('#fra-file-upload') + fireEvent.change(uploadForm, { + target: { files: [makeTestFile('report.txt')] }, + }) + await waitFor(() => + expect( + getByText( + 'Selected File report.txt. To change the selected file, click this button.' + ) + ).toBeInTheDocument() + ) - // // await waitFor(() => getByText('asdfasdf')) + const submitButton = getByText('Submit Report') + fireEvent.click(submitButton) - // await waitFor(() => getByRole('alert')) - // expect(dispatch).toHaveBeenCalledTimes(1) - // }) + await waitFor(() => + expect( + getByText( + `Successfully submitted section(s): Work Outcomes for TANF Exiters on ${new Date().toDateString()}` + ) + ).toBeInTheDocument() + ) + await waitFor(() => expect(dispatch).toHaveBeenCalledTimes(2)) + }) it('Shows an error if a non-allowed file type is selected', async () => { const { getByText, dispatch, getByRole, container } = await setup() @@ -367,45 +376,43 @@ describe('FRA Reports Page', () => { ) }) - // it('Does not show a message if search is clicked after uploading a file', async () => { - // const { - // getByText, - // container, - // getByLabelText, - // queryByText, - // dispatch, - // getByRole, - // } = await setup() - - // const uploadForm = container.querySelector('#fra-file-upload') - // fireEvent.change(uploadForm, { - // target: { files: [makeTestFile('report.txt')] }, - // }) - - // const submitButton = getByText('Submit Report') - // fireEvent.click(submitButton) - - // // await waitFor(() => getByRole('alert')) - // expect(dispatch).toHaveBeenCalledTimes(2) - - // const yearsDropdown = getByLabelText('Fiscal Year (October - September)') - // fireEvent.change(yearsDropdown, { target: { value: '2024' } }) - - // const quarterDropdown = getByLabelText('Quarter') - // fireEvent.change(quarterDropdown, { target: { value: 'Q2' } }) - - // await waitFor(() => { - // expect( - // getByText('Quarter 2 (January - March)', { selector: 'option' }) - // .selected - // ).toBe(true) - // }) - - // fireEvent.click(getByText(/Search/, { selector: 'button' })) - - // await waitFor(() => - // expect(queryByText('Files Not Submitted')).not.toBeInTheDocument() - // ) - // }) + it('Does not show a message if search is clicked after uploading a file', async () => { + const { getByText, container, getByLabelText, queryByText, dispatch } = + await setup() + + const uploadForm = container.querySelector('#fra-file-upload') + fireEvent.change(uploadForm, { + target: { files: [makeTestFile('report.txt')] }, + }) + await waitFor(() => + expect( + getByText( + 'Selected File report.txt. To change the selected file, click this button.' + ) + ).toBeInTheDocument() + ) + + fireEvent.click(getByText(/Submit Report/, { selector: 'button' })) + await waitFor(() => expect(dispatch).toHaveBeenCalledTimes(2)) + + const yearsDropdown = getByLabelText('Fiscal Year (October - September)') + fireEvent.change(yearsDropdown, { target: { value: '2024' } }) + + const quarterDropdown = getByLabelText('Quarter') + fireEvent.change(quarterDropdown, { target: { value: 'Q2' } }) + + await waitFor(() => { + expect( + getByText('Quarter 2 (January - March)', { selector: 'option' }) + .selected + ).toBe(true) + }) + + fireEvent.click(getByText(/Search/, { selector: 'button' })) + + await waitFor(() => + expect(queryByText('Files Not Submitted')).not.toBeInTheDocument() + ) + }) }) }) From dc4b650ea77b51bab2ec250646234eaea3ea4f5d Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Wed, 29 Jan 2025 13:14:25 -0500 Subject: [PATCH 14/53] add extra test cov --- .../src/components/Reports/FRAReports.jsx | 8 +- .../src/components/Reports/FRAReports.test.js | 120 ++++++++++++++++++ 2 files changed, 124 insertions(+), 4 deletions(-) diff --git a/tdrs-frontend/src/components/Reports/FRAReports.jsx b/tdrs-frontend/src/components/Reports/FRAReports.jsx index a4a362b1d..2dbfe95ad 100644 --- a/tdrs-frontend/src/components/Reports/FRAReports.jsx +++ b/tdrs-frontend/src/components/Reports/FRAReports.jsx @@ -523,10 +523,10 @@ const FRAReports = () => { return isValid } - const handleSearch = (e) => { + const handleSearch = (e, bypassSelectedFile = false) => { e.preventDefault() - if (selectedFile && !selectedFile.id) { + if (!bypassSelectedFile && selectedFile && !selectedFile.id) { setErrorModalVisible(true) return } @@ -701,10 +701,10 @@ const FRAReports = () => { { key: '2', text: 'Discard and Search', - onClick: () => { + onClick: (e) => { setErrorModalVisible(false) setSelectedFile(null) - handleSearch({ preventDefault: () => null }) + handleSearch(e, true) }, }, ]} diff --git a/tdrs-frontend/src/components/Reports/FRAReports.test.js b/tdrs-frontend/src/components/Reports/FRAReports.test.js index 7ec05b138..ea1ddd4d8 100644 --- a/tdrs-frontend/src/components/Reports/FRAReports.test.js +++ b/tdrs-frontend/src/components/Reports/FRAReports.test.js @@ -414,5 +414,125 @@ describe('FRA Reports Page', () => { expect(queryByText('Files Not Submitted')).not.toBeInTheDocument() ) }) + + it('Allows the user to cancel the error modal and retain previous search selections', async () => { + const { getByText, queryByText, getByLabelText, container, dispatch } = + await setup() + + const uploadForm = container.querySelector('#fra-file-upload') + fireEvent.change(uploadForm, { + target: { files: [makeTestFile('report.txt')] }, + }) + await waitFor(() => + expect( + getByText( + 'Selected File report.txt. To change the selected file, click this button.' + ) + ).toBeInTheDocument() + ) + + // make a change to the search selections and click search + const yearsDropdown = getByLabelText('Fiscal Year (October - September)') + fireEvent.change(yearsDropdown, { target: { value: '2024' } }) + + const quarterDropdown = getByLabelText('Quarter') + fireEvent.change(quarterDropdown, { target: { value: 'Q2' } }) + await waitFor(() => { + expect(getByText('2024', { selector: 'option' }).selected).toBe(true) + expect( + getByText('Quarter 2 (January - March)', { selector: 'option' }) + .selected + ).toBe(true) + }) + + fireEvent.click(getByText(/Search/, { selector: 'button' })) + + await waitFor(() => + expect(queryByText('Files Not Submitted')).toBeInTheDocument() + ) + + // click cancel + fireEvent.click(getByText(/Cancel/, { selector: '#modal button' })) + + // assert file still exists, search params are the same as initial, dispatch not called + await waitFor(() => { + expect(dispatch).toHaveBeenCalledTimes(1) + expect(queryByText('Files Not Submitted')).not.toBeInTheDocument() + expect( + getByText( + 'Selected File report.txt. To change the selected file, click this button.' + ) + ).toBeInTheDocument() + expect(getByText('2021', { selector: 'option' }).selected).toBe(true) + expect( + getByText('Quarter 1 (October - December)', { selector: 'option' }) + .selected + ).toBe(true) + }) + }) + + it('Allows the user to discard the error modal and continue with a new search', async () => { + const { getByText, queryByText, getByLabelText, container, dispatch } = + await setup() + + const uploadForm = container.querySelector('#fra-file-upload') + fireEvent.change(uploadForm, { + target: { files: [makeTestFile('report.txt')] }, + }) + await waitFor(() => + expect( + getByText( + 'Selected File report.txt. To change the selected file, click this button.' + ) + ).toBeInTheDocument() + ) + + // make a change to the search selections and click search + const yearsDropdown = getByLabelText('Fiscal Year (October - September)') + fireEvent.change(yearsDropdown, { target: { value: '2024' } }) + + const quarterDropdown = getByLabelText('Quarter') + fireEvent.change(quarterDropdown, { target: { value: 'Q2' } }) + await waitFor(() => { + expect(getByText('2024', { selector: 'option' }).selected).toBe(true) + expect( + getByText('Quarter 2 (January - March)', { selector: 'option' }) + .selected + ).toBe(true) + }) + + fireEvent.click(getByText(/Search/, { selector: 'button' })) + + await waitFor(() => + expect(queryByText('Files Not Submitted')).toBeInTheDocument() + ) + + // click discard + const button = getByText(/Discard and Search/, { + selector: '#modal button', + }) + fireEvent.click(button) + + // assert file discarded, search params updated + await waitFor(() => { + // expect(dispatch).toHaveBeenCalledTimes(2) + expect(queryByText('Files Not Submitted')).not.toBeInTheDocument() + expect( + queryByText( + 'Selected File report.txt. To change the selected file, click this button.' + ) + ).not.toBeInTheDocument() + expect(getByText('2024', { selector: 'option' }).selected).toBe(true) + expect( + getByText('Quarter 2 (January - March)', { selector: 'option' }) + .selected + ).toBe(true) + expect( + getByText( + 'Alaska - Work Outcomes for TANF Exiters - Fiscal Year 2024 - Quarter 2 (January - March)' + ) + ).toBeInTheDocument() + }) + }) }) }) From 37f8a4ae0a93e97ce73a2aed1b2bdc830b8b8990 Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Thu, 6 Feb 2025 09:44:18 -0500 Subject: [PATCH 15/53] implement upload api --- tdrs-backend/tdpservice/data_files/serializers.py | 6 ------ tdrs-frontend/src/components/Reports/FRAReports.jsx | 1 + 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/tdrs-backend/tdpservice/data_files/serializers.py b/tdrs-backend/tdpservice/data_files/serializers.py index fb2eca791..2460c1366 100644 --- a/tdrs-backend/tdpservice/data_files/serializers.py +++ b/tdrs-backend/tdpservice/data_files/serializers.py @@ -114,9 +114,3 @@ def validate_file(self, file): validate_file_extension(file.name) validate_file_infection(file, file.name, user) return file - - def validate_section(self, section): - """Validate the section field.""" - if DataFile.Section.is_fra(section): - raise serializers.ValidationError("Section cannot be FRA") - return section diff --git a/tdrs-frontend/src/components/Reports/FRAReports.jsx b/tdrs-frontend/src/components/Reports/FRAReports.jsx index 2dbfe95ad..d1fef019c 100644 --- a/tdrs-frontend/src/components/Reports/FRAReports.jsx +++ b/tdrs-frontend/src/components/Reports/FRAReports.jsx @@ -596,6 +596,7 @@ const FRAReports = () => { uploadFraReport( { ...searchFormValues, + reportType: getReportTypeLabel(), file: selectedFile, user, }, From 691d413697e315f1709504ced5691c7ea98f6b5d Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Thu, 6 Feb 2025 09:55:03 -0500 Subject: [PATCH 16/53] handle upload errors and no file selected --- .../src/components/Reports/FRAReports.jsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tdrs-frontend/src/components/Reports/FRAReports.jsx b/tdrs-frontend/src/components/Reports/FRAReports.jsx index d1fef019c..048ec5335 100644 --- a/tdrs-frontend/src/components/Reports/FRAReports.jsx +++ b/tdrs-frontend/src/components/Reports/FRAReports.jsx @@ -259,8 +259,9 @@ const UploadForm = ({ file, setSelectedFile, section, + error, + setError, }) => { - const [error, setError] = useState(null) // const [selectedFile, setSelectedFile] = useState(file || null) // const [file, setFile] = useState(null) const inputRef = useRef(null) @@ -325,7 +326,7 @@ const UploadForm = ({ const onSubmit = (e) => { e.preventDefault() - if (!!error) { + if (!!error || !file) { return } @@ -422,6 +423,7 @@ const FRAReports = () => { const [isUploadReportToggled, setUploadReportToggled] = useState(false) const [errorModalVisible, setErrorModalVisible] = useState(false) const [searchFormValues, setSearchFormValues] = useState(null) + const [uploadError, setUploadError] = useState(null) const user = useSelector((state) => state.auth.user) const sttList = useSelector((state) => state?.stts?.sttList) @@ -579,7 +581,7 @@ const FRAReports = () => { setLocalAlertState({ active: true, type: 'success', - message: `Successfully submitted section(s): ${getReportTypeLabel()} on ${new Date().toDateString()}`, + message: `Successfully submitted section: ${getReportTypeLabel()} on ${new Date().toDateString()}`, }) } @@ -676,11 +678,18 @@ const FRAReports = () => { )} { + setSelectedFile(null) + setUploadError(null) + setUploadReportToggled(false) + }} localAlert={localAlert} setLocalAlertState={setLocalAlertState} file={selectedFile} setSelectedFile={setSelectedFile} section={getReportTypeLabel()} + error={uploadError} + setError={setUploadError} /> @@ -705,6 +714,7 @@ const FRAReports = () => { onClick: (e) => { setErrorModalVisible(false) setSelectedFile(null) + setUploadError(null) handleSearch(e, true) }, }, From 9dfad44e27d65ef7238eab24e146daa1cf8ce00d Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Thu, 6 Feb 2025 10:03:32 -0500 Subject: [PATCH 17/53] fix test --- tdrs-frontend/src/components/Reports/FRAReports.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tdrs-frontend/src/components/Reports/FRAReports.test.js b/tdrs-frontend/src/components/Reports/FRAReports.test.js index ea1ddd4d8..1d8506609 100644 --- a/tdrs-frontend/src/components/Reports/FRAReports.test.js +++ b/tdrs-frontend/src/components/Reports/FRAReports.test.js @@ -312,7 +312,7 @@ describe('FRA Reports Page', () => { await waitFor(() => expect( getByText( - `Successfully submitted section(s): Work Outcomes for TANF Exiters on ${new Date().toDateString()}` + `Successfully submitted section: Work Outcomes for TANF Exiters on ${new Date().toDateString()}` ) ).toBeInTheDocument() ) From 16da0a2e9b72712fe92116edf1db1f82ac909b2c Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Fri, 7 Feb 2025 09:11:44 -0500 Subject: [PATCH 18/53] update errors count on input change --- .../src/components/Reports/FRAReports.jsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/tdrs-frontend/src/components/Reports/FRAReports.jsx b/tdrs-frontend/src/components/Reports/FRAReports.jsx index 048ec5335..66f81747e 100644 --- a/tdrs-frontend/src/components/Reports/FRAReports.jsx +++ b/tdrs-frontend/src/components/Reports/FRAReports.jsx @@ -169,11 +169,9 @@ const SearchForm = ({ setFormState, needsSttSelection, userProfileStt, - sttList, }) => { const missingStt = !needsSttSelection && !userProfileStt const errorsRef = null - const errorsCount = 0 const setFormValue = (field, value) => { console.log(`${field}: ${value}`) @@ -182,10 +180,21 @@ const SearchForm = ({ if (!!value) { newFormState[field].value = value newFormState[field].valid = true + } else { + newFormState[field].valid = false } newFormState[field].touched = true - setFormState(newFormState) + let errors = 0 + Object.keys(newFormState).forEach((key) => { + if (key !== 'errors') { + if (newFormState[key].touched && !newFormState[key].valid) { + errors += 1 + } + } + }) + + setFormState({ ...newFormState, errors }) } return ( @@ -202,7 +211,7 @@ const SearchForm = ({ ref={errorsRef} tabIndex="-1" > - There {errorsCount === 1 ? 'is' : 'are'} {form.errors} error(s) in + There {form.errors === 1 ? 'is' : 'are'} {form.errors} error(s) in this form
)} @@ -652,7 +661,6 @@ const FRAReports = () => { setFormState={setTemporaryFormState} needsSttSelection={needsSttSelection} userProfileStt={userProfileStt} - sttList={sttList} />
{isUploadReportToggled && ( From 556d89f23c2627695397e0c14a3b273e2f3d5b8d Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Fri, 7 Feb 2025 09:47:12 -0500 Subject: [PATCH 19/53] fix test --- tdrs-backend/tdpservice/data_files/test/test_api.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tdrs-backend/tdpservice/data_files/test/test_api.py b/tdrs-backend/tdpservice/data_files/test/test_api.py index 49f0a279b..5496ce492 100644 --- a/tdrs-backend/tdpservice/data_files/test/test_api.py +++ b/tdrs-backend/tdpservice/data_files/test/test_api.py @@ -230,9 +230,8 @@ def test_create_data_file_file_entry(self, api_client, data_file_data, user): def test_create_data_file_fra(self, api_client, data_file_data, user): """Test ability to create data file metadata registry.""" response = self.post_data_file_fra(api_client, data_file_data) - from rest_framework.exceptions import ErrorDetail - assert response.data == {'section': [ErrorDetail(string='Section cannot be FRA', code='invalid')]} - self.assert_data_file_error(response) + self.assert_data_file_created(response) + self.assert_data_file_exists(data_file_data, 1, user) def test_data_file_file_version_increment( self, From 5816e4fac55b55632eb19d4ecb8aa9f92dfdd017 Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Fri, 7 Feb 2025 15:42:50 -0500 Subject: [PATCH 20/53] create + implement reusable form input components --- .../src/components/ComboBox/index.js | 3 - .../{ComboBox => Form}/ComboBox.jsx | 0 .../{ComboBox => Form}/ComboBox.test.js | 0 .../src/components/Form/DropdownSelect.jsx | 40 ++++++ .../src/components/Form/RadioSelect.jsx | 26 ++++ tdrs-frontend/src/components/Form/index.js | 5 + .../src/components/Reports/FRAReports.jsx | 122 +++++++----------- tdrs-frontend/src/components/Reports/utils.js | 14 ++ .../components/STTComboBox/STTComboBox.jsx | 2 +- 9 files changed, 132 insertions(+), 80 deletions(-) delete mode 100644 tdrs-frontend/src/components/ComboBox/index.js rename tdrs-frontend/src/components/{ComboBox => Form}/ComboBox.jsx (100%) rename tdrs-frontend/src/components/{ComboBox => Form}/ComboBox.test.js (100%) create mode 100644 tdrs-frontend/src/components/Form/DropdownSelect.jsx create mode 100644 tdrs-frontend/src/components/Form/RadioSelect.jsx create mode 100644 tdrs-frontend/src/components/Form/index.js diff --git a/tdrs-frontend/src/components/ComboBox/index.js b/tdrs-frontend/src/components/ComboBox/index.js deleted file mode 100644 index 0dec99394..000000000 --- a/tdrs-frontend/src/components/ComboBox/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import ComboBox from './ComboBox' - -export default ComboBox diff --git a/tdrs-frontend/src/components/ComboBox/ComboBox.jsx b/tdrs-frontend/src/components/Form/ComboBox.jsx similarity index 100% rename from tdrs-frontend/src/components/ComboBox/ComboBox.jsx rename to tdrs-frontend/src/components/Form/ComboBox.jsx diff --git a/tdrs-frontend/src/components/ComboBox/ComboBox.test.js b/tdrs-frontend/src/components/Form/ComboBox.test.js similarity index 100% rename from tdrs-frontend/src/components/ComboBox/ComboBox.test.js rename to tdrs-frontend/src/components/Form/ComboBox.test.js diff --git a/tdrs-frontend/src/components/Form/DropdownSelect.jsx b/tdrs-frontend/src/components/Form/DropdownSelect.jsx new file mode 100644 index 000000000..36e1e685d --- /dev/null +++ b/tdrs-frontend/src/components/Form/DropdownSelect.jsx @@ -0,0 +1,40 @@ +import { React } from 'react' +import classNames from 'classnames' + +const DropdownSelect = ({ + label, + fieldName, + setValue, + options, + errorText, + valid, + value, +}) => ( + +) + +export default DropdownSelect diff --git a/tdrs-frontend/src/components/Form/RadioSelect.jsx b/tdrs-frontend/src/components/Form/RadioSelect.jsx new file mode 100644 index 000000000..162b951c4 --- /dev/null +++ b/tdrs-frontend/src/components/Form/RadioSelect.jsx @@ -0,0 +1,26 @@ +import { React } from 'react' + +const RadioSelect = ({ label, fieldName, setValue, options, valid, value }) => ( +
+ {label} + + {options.map(({ label, value }, index) => ( +
+ setValue(value)} + /> + +
+ ))} +
+) + +export default RadioSelect diff --git a/tdrs-frontend/src/components/Form/index.js b/tdrs-frontend/src/components/Form/index.js new file mode 100644 index 000000000..011b33a7b --- /dev/null +++ b/tdrs-frontend/src/components/Form/index.js @@ -0,0 +1,5 @@ +import ComboBox from './ComboBox' +import RadioSelect from './RadioSelect' +import DropdownSelect from './DropdownSelect' + +export { ComboBox, RadioSelect, DropdownSelect } diff --git a/tdrs-frontend/src/components/Reports/FRAReports.jsx b/tdrs-frontend/src/components/Reports/FRAReports.jsx index 66f81747e..3b69b5d65 100644 --- a/tdrs-frontend/src/components/Reports/FRAReports.jsx +++ b/tdrs-frontend/src/components/Reports/FRAReports.jsx @@ -6,7 +6,7 @@ import fileTypeChecker from 'file-type-checker' import Button from '../Button' import STTComboBox from '../STTComboBox' -import { quarters, constructYearOptions } from './utils' +import { quarters, constructYearOptions, constructYears } from './utils' import { accountCanSelectStt } from '../../selectors/auth' import { handlePreview } from '../FileUpload/utils' import createFileInputErrorState from '../../utils/createFileInputErrorState' @@ -17,6 +17,7 @@ import { uploadFraReport, } from '../../actions/fraReports' import { fetchSttList } from '../../actions/sttList' +import { DropdownSelect, RadioSelect } from '../Form' const INVALID_FILE_ERROR = 'We can’t process that file format. Please provide a plain text file.' @@ -36,26 +37,14 @@ const SelectSTT = ({ valid, value, setValue }) => ( const SelectReportType = ({ valid, value, setValue, options }) => (
-
- File Type - - {options.map(({ label, value }, index) => ( -
- setValue(value)} - /> - -
- ))} -
+
) @@ -65,33 +54,24 @@ const SelectFiscalYear = ({ valid, value, setValue }) => ( 'usa-form-group--error': !valid, })} > - + ({ + label: year, + value: year, + })), + ]} + />
) @@ -101,34 +81,24 @@ const SelectQuarter = ({ valid, value, setValue }) => ( 'usa-form-group--error': !valid, })} > - + ({ + label: quarterDescription, + value: quarter, + })), + ]} + />
) diff --git a/tdrs-frontend/src/components/Reports/utils.js b/tdrs-frontend/src/components/Reports/utils.js index c0982c236..9b5e80005 100644 --- a/tdrs-frontend/src/components/Reports/utils.js +++ b/tdrs-frontend/src/components/Reports/utils.js @@ -5,6 +5,20 @@ export const quarters = { Q4: 'Quarter 4 (July - September)', } +export const constructYears = () => { + const years = [] + const today = new Date(Date.now()) + + const fiscalYear = + today.getMonth() > 8 ? today.getFullYear() + 1 : today.getFullYear() + + for (let i = fiscalYear; i >= 2021; i--) { + years.push(i) + } + + return years +} + export const constructYearOptions = () => { const years = [] const today = new Date(Date.now()) diff --git a/tdrs-frontend/src/components/STTComboBox/STTComboBox.jsx b/tdrs-frontend/src/components/STTComboBox/STTComboBox.jsx index 83f87e049..be5794f9a 100644 --- a/tdrs-frontend/src/components/STTComboBox/STTComboBox.jsx +++ b/tdrs-frontend/src/components/STTComboBox/STTComboBox.jsx @@ -2,7 +2,7 @@ import React, { useEffect, useState, useRef } from 'react' import PropTypes from 'prop-types' import { useDispatch, useSelector } from 'react-redux' import { fetchSttList } from '../../actions/sttList' -import ComboBox from '../ComboBox' +import { ComboBox } from '../Form' import Modal from '../Modal' import { toTitleCase } from '../../utils/stringUtils' From 1d347f9ab3c8fd5dfff1c3fca13feb1725567b86 Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Fri, 7 Feb 2025 15:55:31 -0500 Subject: [PATCH 21/53] implement conditional classNames --- .../src/components/Form/DropdownSelect.jsx | 55 +++++---- .../src/components/Form/RadioSelect.jsx | 51 +++++---- .../src/components/Reports/FRAReports.jsx | 105 ++++++++---------- 3 files changed, 109 insertions(+), 102 deletions(-) diff --git a/tdrs-frontend/src/components/Form/DropdownSelect.jsx b/tdrs-frontend/src/components/Form/DropdownSelect.jsx index 36e1e685d..fc9396fa8 100644 --- a/tdrs-frontend/src/components/Form/DropdownSelect.jsx +++ b/tdrs-frontend/src/components/Form/DropdownSelect.jsx @@ -9,32 +9,39 @@ const DropdownSelect = ({ errorText, valid, value, + classes, }) => ( -
) export default RadioSelect diff --git a/tdrs-frontend/src/components/Reports/FRAReports.jsx b/tdrs-frontend/src/components/Reports/FRAReports.jsx index 3b69b5d65..d6ea0a447 100644 --- a/tdrs-frontend/src/components/Reports/FRAReports.jsx +++ b/tdrs-frontend/src/components/Reports/FRAReports.jsx @@ -36,70 +36,59 @@ const SelectSTT = ({ valid, value, setValue }) => ( ) const SelectReportType = ({ valid, value, setValue, options }) => ( -
- -
+ ) const SelectFiscalYear = ({ valid, value, setValue }) => ( -
- ({ - label: year, - value: year, - })), - ]} - /> -
+ ({ + label: year, + value: year, + })), + ]} + /> ) const SelectQuarter = ({ valid, value, setValue }) => ( -
- ({ - label: quarterDescription, - value: quarter, - })), - ]} - /> -
+ ({ + label: quarterDescription, + value: quarter, + })), + ]} + /> ) const FiscalQuarterExplainer = () => ( From cd9f375ec99e898a3907869d16a5ee8315f20ef3 Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Fri, 7 Feb 2025 16:07:20 -0500 Subject: [PATCH 22/53] remove comments and console.logs --- .../src/components/Reports/FRAReports.jsx | 27 ++----------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/tdrs-frontend/src/components/Reports/FRAReports.jsx b/tdrs-frontend/src/components/Reports/FRAReports.jsx index d6ea0a447..b73de8fb6 100644 --- a/tdrs-frontend/src/components/Reports/FRAReports.jsx +++ b/tdrs-frontend/src/components/Reports/FRAReports.jsx @@ -6,7 +6,7 @@ import fileTypeChecker from 'file-type-checker' import Button from '../Button' import STTComboBox from '../STTComboBox' -import { quarters, constructYearOptions, constructYears } from './utils' +import { quarters, constructYears } from './utils' import { accountCanSelectStt } from '../../selectors/auth' import { handlePreview } from '../FileUpload/utils' import createFileInputErrorState from '../../utils/createFileInputErrorState' @@ -133,7 +133,6 @@ const SearchForm = ({ const errorsRef = null const setFormValue = (field, value) => { - console.log(`${field}: ${value}`) const newFormState = { ...form } if (!!value) { @@ -230,8 +229,6 @@ const UploadForm = ({ error, setError, }) => { - // const [selectedFile, setSelectedFile] = useState(file || null) - // const [file, setFile] = useState(null) const inputRef = useRef(null) useEffect(() => { @@ -261,7 +258,6 @@ const UploadForm = ({ message: null, }) - // const { name: section } = e.target const fileInputValue = e.target.files[0] const input = inputRef.current const dropTarget = inputRef.current.parentNode @@ -283,7 +279,6 @@ const UploadForm = ({ createFileInputErrorState(input, dropTarget) setError(INVALID_FILE_ERROR) } else { - console.log('fileInputValue', fileInputValue) setSelectedFile(fileInputValue) } } @@ -368,11 +363,7 @@ const UploadForm = ({
- @@ -424,7 +415,6 @@ const FRAReports = () => { const [selectedFile, setSelectedFile] = useState(null) - // const stt = useSelector((state) => state.stts?.stt) // const fraSubmissionHistory = useSelector((state) => state.fraReports) const dispatch = useDispatch() @@ -475,8 +465,6 @@ const FRAReports = () => { let isValid = true let errors = 0 - console.log('selected values: ', selectedValues) - Object.keys(selectedValues).forEach((key) => { if (!!selectedValues[key]) { validatedForm[key].valid = true @@ -503,32 +491,22 @@ const FRAReports = () => { const form = temporaryFormState - console.log('form', form) - const formValues = { stt: sttList?.find((stt) => stt?.name === form.stt.value), } - console.log('formvalues', formValues) - console.log('sttList', sttList) - Object.keys(form).forEach((key) => { if (key !== 'errors' && key !== 'stt') { formValues[key] = form[key].value } }) - // console.log(form) - let isValid = validateSearchForm(formValues) if (!isValid) { - console.log('not valid') return } - console.log('searching:', formValues) - setUploadReportToggled(false) setSearchFormValues(null) @@ -554,7 +532,6 @@ const FRAReports = () => { } const onFileUploadError = (error) => { - console.log(error) setLocalAlertState({ active: true, type: 'error', From b808d2638d213e8d614ea2931164abce1a95e5f2 Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Fri, 7 Feb 2025 16:56:52 -0500 Subject: [PATCH 23/53] unused import --- tdrs-backend/tdpservice/data_files/test/test_api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tdrs-backend/tdpservice/data_files/test/test_api.py b/tdrs-backend/tdpservice/data_files/test/test_api.py index 368933110..5496ce492 100644 --- a/tdrs-backend/tdpservice/data_files/test/test_api.py +++ b/tdrs-backend/tdpservice/data_files/test/test_api.py @@ -1,7 +1,6 @@ """Tests for DataFiles Application.""" import os from rest_framework import status -from rest_framework.exceptions import ErrorDetail import pytest import base64 import openpyxl From 91ce6f3569d120beec797f753830be46c6c4e31e Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Fri, 7 Feb 2025 17:13:21 -0500 Subject: [PATCH 24/53] additional tests --- .../src/components/Reports/FRAReports.test.js | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/tdrs-frontend/src/components/Reports/FRAReports.test.js b/tdrs-frontend/src/components/Reports/FRAReports.test.js index 1d8506609..6b9378055 100644 --- a/tdrs-frontend/src/components/Reports/FRAReports.test.js +++ b/tdrs-frontend/src/components/Reports/FRAReports.test.js @@ -179,6 +179,66 @@ describe('FRA Reports Page', () => { expect(getByText('There are 3 error(s) in this form')).toBeInTheDocument() }) + it('Updates form validation if values are changed', async () => { + const state = { + ...initialState, + auth: { + authenticated: true, + user: { + email: 'hi@bye.com', + stt: null, + roles: [{ id: 1, name: 'OFA System Admin', permission: [] }], + account_approval_status: 'Approved', + }, + }, + } + + const store = mockStore(state) + + const { getByText, queryByText, getByLabelText } = render( + + + + ) + + // don't fill out any form values + fireEvent.click(getByText(/Search/, { selector: 'button' })) + + // upload form not displayed + expect(queryByText('Submit Report')).not.toBeInTheDocument() + + // fields all have errors + expect( + getByText('A state, tribe, or territory is required') + ).toBeInTheDocument() + expect(getByText('A fiscal year is required')).toBeInTheDocument() + expect(getByText('A quarter is required')).toBeInTheDocument() + expect(getByText('There are 3 error(s) in this form')).toBeInTheDocument() + + const yearsDropdown = getByLabelText( + 'Fiscal Year (October - September)', + { exact: false } + ) + fireEvent.change(yearsDropdown, { target: { value: '2021' } }) + + await waitFor(() => { + expect(queryByText('A fiscal year is required')).not.toBeInTheDocument() + expect( + getByText('There are 2 error(s) in this form') + ).toBeInTheDocument() + }) + + const quarterDropdown = getByLabelText('Quarter', { exact: false }) + fireEvent.change(quarterDropdown, { target: { value: 'Q1' } }) + + await waitFor(() => { + expect(queryByText('A quarter is required')).not.toBeInTheDocument() + expect( + getByText('There is 1 error(s) in this form') + ).toBeInTheDocument() + }) + }) + it('Shows upload form once search has been clicked', async () => { const state = { ...initialState, @@ -376,6 +436,30 @@ describe('FRA Reports Page', () => { ) }) + it('Cancels the upload if Cancel is clicked', async () => { + const { getByText, container, queryByText } = await setup() + + const uploadForm = container.querySelector('#fra-file-upload') + fireEvent.change(uploadForm, { + target: { files: [makeTestFile('report.txt')] }, + }) + + await waitFor(() => { + expect( + getByText('You have selected the file: report.txt') + ).toBeInTheDocument() + }) + + fireEvent.click(getByText(/Cancel/, { selector: 'button' })) + + await waitFor(() => { + expect( + queryByText('You have selected the file: report.txt') + ).not.toBeInTheDocument() + expect(queryByText(/Submit Report/)).not.toBeInTheDocument() + }) + }) + it('Does not show a message if search is clicked after uploading a file', async () => { const { getByText, container, getByLabelText, queryByText, dispatch } = await setup() From 72e90a5a90b1b0241a3c05118ff9a3606818903a Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Fri, 7 Feb 2025 17:44:19 -0500 Subject: [PATCH 25/53] implement fra reports feature flag --- tdrs-backend/tdpservice/users/serializers.py | 4 +++- .../src/components/Header/Header.jsx | 1 + .../PermissionGuard/PermissionGuard.jsx | 19 +++++++++++++++++-- .../components/PrivateRoute/PrivateRoute.js | 2 ++ tdrs-frontend/src/components/Routes/Routes.js | 1 + tdrs-frontend/src/selectors/auth.js | 3 +++ 6 files changed, 27 insertions(+), 3 deletions(-) diff --git a/tdrs-backend/tdpservice/users/serializers.py b/tdrs-backend/tdpservice/users/serializers.py index 46a0b3904..7015f6fd3 100644 --- a/tdrs-backend/tdpservice/users/serializers.py +++ b/tdrs-backend/tdpservice/users/serializers.py @@ -115,7 +115,8 @@ class Meta: 'date_joined', 'access_request', 'access_requested_date', - 'account_approval_status' + 'account_approval_status', + 'feature_flags', ] read_only_fields = ( 'id', @@ -131,6 +132,7 @@ class Meta: 'access_request', 'access_requested_date', 'account_approval_status', + 'feature_flags', ) """Enforce first and last name to be in API call and not empty""" diff --git a/tdrs-frontend/src/components/Header/Header.jsx b/tdrs-frontend/src/components/Header/Header.jsx index 468dea4ed..98f9efe84 100644 --- a/tdrs-frontend/src/components/Header/Header.jsx +++ b/tdrs-frontend/src/components/Header/Header.jsx @@ -130,6 +130,7 @@ function Header() { { if (requiresApproval && !isApproved) { @@ -17,12 +19,22 @@ const isAllowed = ( return true } + if (!requiredFeatureFlags) { + return true + } + for (var i = 0; i < requiredPermissions.length; i++) { if (!permissions.includes(requiredPermissions[i])) { return false } } + for (var f = 0; f < requiredFeatureFlags.length; f++) { + if (featureFlags[requiredFeatureFlags[f]] !== true) { + return false + } + } + return true } @@ -30,14 +42,17 @@ const PermissionGuard = ({ children, requiresApproval = false, requiredPermissions = [], + requiredFeatureFlags = [], notAllowedComponent = null, }) => { const permissions = useSelector(selectUserPermissions) const isApproved = useSelector(accountStatusIsApproved) + const featureFlags = useSelector(selectFeatureFlags) return isAllowed( - { permissions, isApproved }, + { permissions, isApproved, featureFlags }, requiredPermissions, + requiredFeatureFlags, requiresApproval ) ? children diff --git a/tdrs-frontend/src/components/PrivateRoute/PrivateRoute.js b/tdrs-frontend/src/components/PrivateRoute/PrivateRoute.js index 33df4eb64..2ce9e2989 100644 --- a/tdrs-frontend/src/components/PrivateRoute/PrivateRoute.js +++ b/tdrs-frontend/src/components/PrivateRoute/PrivateRoute.js @@ -17,6 +17,7 @@ function PrivateRoute({ children, title, requiredPermissions, + requiredFeatureFlags, requiresApproval, }) { const authenticated = useSelector((state) => state.auth.authenticated) @@ -45,6 +46,7 @@ function PrivateRoute({ } > diff --git a/tdrs-frontend/src/components/Routes/Routes.js b/tdrs-frontend/src/components/Routes/Routes.js index dbe3ee681..89a735b4a 100644 --- a/tdrs-frontend/src/components/Routes/Routes.js +++ b/tdrs-frontend/src/components/Routes/Routes.js @@ -57,6 +57,7 @@ const AppRoutes = () => { diff --git a/tdrs-frontend/src/selectors/auth.js b/tdrs-frontend/src/selectors/auth.js index 9dbc22536..64169fd45 100644 --- a/tdrs-frontend/src/selectors/auth.js +++ b/tdrs-frontend/src/selectors/auth.js @@ -2,6 +2,9 @@ const valueIsEmpty = (val) => val === null || val === undefined || val === '' export const selectUser = (state) => state.auth.user || null +export const selectFeatureFlags = (state) => + selectUser(state)?.feature_flags || {} + // could memoize these with `createSelector` from `reselect` export const selectUserAccountApprovalStatus = (state) => selectUser(state)?.['account_approval_status'] From 97271f63c7559a52beaac774e8a47c9f6405c21a Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Fri, 7 Feb 2025 18:15:25 -0500 Subject: [PATCH 26/53] implement file encoding check --- .../src/components/FileUpload/FileUpload.jsx | 42 ++------------- .../src/components/FileUpload/utils.jsx | 36 +++++++++++++ .../src/components/Reports/FRAReports.jsx | 51 ++++++++++++------- 3 files changed, 73 insertions(+), 56 deletions(-) diff --git a/tdrs-frontend/src/components/FileUpload/FileUpload.jsx b/tdrs-frontend/src/components/FileUpload/FileUpload.jsx index 9357a7c2a..ea44cea6c 100644 --- a/tdrs-frontend/src/components/FileUpload/FileUpload.jsx +++ b/tdrs-frontend/src/components/FileUpload/FileUpload.jsx @@ -2,7 +2,6 @@ import React, { useRef, useEffect } from 'react' import PropTypes from 'prop-types' import { useDispatch, useSelector } from 'react-redux' import fileTypeChecker from 'file-type-checker' -import languageEncoding from 'detect-file-encoding-and-language' import { clearError, @@ -14,7 +13,11 @@ import { } from '../../actions/reports' import Button from '../Button' import createFileInputErrorState from '../../utils/createFileInputErrorState' -import { handlePreview, getTargetClassName } from './utils' +import { + handlePreview, + getTargetClassName, + tryGetUTF8EncodedFile, +} from './utils' const INVALID_FILE_ERROR = 'We can’t process that file format. Please provide a plain text file.' @@ -35,41 +38,6 @@ const INVALID_EXT_ERROR = ( ) -// The package author suggests using a minimum of 500 words to determine the encoding. However, datafiles don't have -// "words" so we're using bytes instead to determine the encoding. See: https://www.npmjs.com/package/detect-file-encoding-and-language -const MIN_BYTES = 500 - -/* istanbul ignore next */ -const tryGetUTF8EncodedFile = async function (fileBytes, file) { - // Create a small view of the file to determine the encoding. - const btyesView = new Uint8Array(fileBytes.slice(0, MIN_BYTES)) - const blobView = new Blob([btyesView], { type: 'text/plain' }) - try { - const fileInfo = await languageEncoding(blobView) - const bom = btyesView.slice(0, 3) - const hasBom = bom[0] === 0xef && bom[1] === 0xbb && bom[2] === 0xbf - if ((fileInfo && fileInfo.encoding !== 'UTF-8') || hasBom) { - const utf8Encoder = new TextEncoder() - const decoder = new TextDecoder(fileInfo.encoding) - const decodedString = decoder.decode( - hasBom ? fileBytes.slice(3) : fileBytes - ) - const utf8Bytes = utf8Encoder.encode(decodedString) - return new File([utf8Bytes], file.name, file.options) - } - return file - } catch (error) { - // This is a last ditch fallback to ensure consistent functionality and also allows the unit tests to work in the - // same way they did before this change. When the unit tests (i.e. Node environment) call `languageEncoding` it - // expects a Buffer/string/URL object. When the browser calls `languageEncoding`, it expects a Blob/File object. - // There is not a convenient way or universal object to handle both cases. Thus, when the tests run the call to - // `languageEncoding`, it raises an exception and we return the file as is which is then dispatched as it would - // have been before this change. - console.error('Caught error while handling file encoding. Error:', error) - return file - } -} - const load = (file, section, input, dropTarget, dispatch) => { const filereader = new FileReader() const types = ['png', 'gif', 'jpeg'] diff --git a/tdrs-frontend/src/components/FileUpload/utils.jsx b/tdrs-frontend/src/components/FileUpload/utils.jsx index 96a74fe0d..536c369ce 100644 --- a/tdrs-frontend/src/components/FileUpload/utils.jsx +++ b/tdrs-frontend/src/components/FileUpload/utils.jsx @@ -1,6 +1,7 @@ //This file contains modified versions of code from: //https://github.com/uswds/uswds/blob/develop/src/js/components/file-input.js import escapeHtml from '../../utils/escapeHtml' +import languageEncoding from 'detect-file-encoding-and-language' export const PREFIX = 'usa' @@ -95,3 +96,38 @@ export const handlePreview = (fileName, targetClassName) => { } return true } + +// The package author suggests using a minimum of 500 words to determine the encoding. However, datafiles don't have +// "words" so we're using bytes instead to determine the encoding. See: https://www.npmjs.com/package/detect-file-encoding-and-language +const MIN_BYTES = 500 + +/* istanbul ignore next */ +export const tryGetUTF8EncodedFile = async function (fileBytes, file) { + // Create a small view of the file to determine the encoding. + const btyesView = new Uint8Array(fileBytes.slice(0, MIN_BYTES)) + const blobView = new Blob([btyesView], { type: 'text/plain' }) + try { + const fileInfo = await languageEncoding(blobView) + const bom = btyesView.slice(0, 3) + const hasBom = bom[0] === 0xef && bom[1] === 0xbb && bom[2] === 0xbf + if ((fileInfo && fileInfo.encoding !== 'UTF-8') || hasBom) { + const utf8Encoder = new TextEncoder() + const decoder = new TextDecoder(fileInfo.encoding) + const decodedString = decoder.decode( + hasBom ? fileBytes.slice(3) : fileBytes + ) + const utf8Bytes = utf8Encoder.encode(decodedString) + return new File([utf8Bytes], file.name, file.options) + } + return file + } catch (error) { + // This is a last ditch fallback to ensure consistent functionality and also allows the unit tests to work in the + // same way they did before this change. When the unit tests (i.e. Node environment) call `languageEncoding` it + // expects a Buffer/string/URL object. When the browser calls `languageEncoding`, it expects a Blob/File object. + // There is not a convenient way or universal object to handle both cases. Thus, when the tests run the call to + // `languageEncoding`, it raises an exception and we return the file as is which is then dispatched as it would + // have been before this change. + console.error('Caught error while handling file encoding. Error:', error) + return file + } +} diff --git a/tdrs-frontend/src/components/Reports/FRAReports.jsx b/tdrs-frontend/src/components/Reports/FRAReports.jsx index b73de8fb6..c07bfa8e6 100644 --- a/tdrs-frontend/src/components/Reports/FRAReports.jsx +++ b/tdrs-frontend/src/components/Reports/FRAReports.jsx @@ -8,7 +8,7 @@ import Button from '../Button' import STTComboBox from '../STTComboBox' import { quarters, constructYears } from './utils' import { accountCanSelectStt } from '../../selectors/auth' -import { handlePreview } from '../FileUpload/utils' +import { handlePreview, tryGetUTF8EncodedFile } from '../FileUpload/utils' import createFileInputErrorState from '../../utils/createFileInputErrorState' import Modal from '../Modal' @@ -221,7 +221,6 @@ const UploadForm = ({ handleCancel, handleUpload, handleDownload, - localAlert, setLocalAlertState, file, setSelectedFile, @@ -250,7 +249,7 @@ const UploadForm = ({ } }, [file]) - const onFileChanged = (e) => { + const onFileChanged = async (e) => { setError(null) setLocalAlertState({ active: false, @@ -265,25 +264,40 @@ const UploadForm = ({ const blob = fileInputValue.slice(0, 4) const filereader = new FileReader() - const types = ['png', 'gif', 'jpeg'] - filereader.onload = () => { - const re = /(\.txt|\.ms\d{2}|\.ts\d{2,3})$/i - if (!re.exec(fileInputValue.name)) { - setError(INVALID_EXT_ERROR) - return - } + const imgFileTypes = ['png', 'gif', 'jpeg'] + const allowedExtensions = /(\.txt|\.ms\d{2}|\.ts\d{2,3})$/i + + const loadFile = () => + new Promise((resolve, reject) => { + filereader.onerror = () => { + filereader.abort() + reject(new Error('Problem loading input file')) + } - const isImg = fileTypeChecker.validateFileType(filereader.result, types) + filereader.onload = () => resolve({ result: filereader.result }) - if (isImg) { - createFileInputErrorState(input, dropTarget) - setError(INVALID_FILE_ERROR) - } else { - setSelectedFile(fileInputValue) - } + filereader.readAsArrayBuffer(blob) + }) + + if (!allowedExtensions.exec(fileInputValue.name)) { + setError(INVALID_EXT_ERROR) + return + } + + const { result } = await loadFile() + + const isImg = fileTypeChecker.validateFileType(result, imgFileTypes) + if (isImg) { + createFileInputErrorState(input, dropTarget) + setError(INVALID_FILE_ERROR) } - filereader.readAsArrayBuffer(blob) + const encodedFile = await tryGetUTF8EncodedFile( + filereader.result, + fileInputValue + ) + + setSelectedFile(encodedFile) } const onSubmit = (e) => { @@ -627,7 +641,6 @@ const FRAReports = () => { setUploadError(null) setUploadReportToggled(false) }} - localAlert={localAlert} setLocalAlertState={setLocalAlertState} file={selectedFile} setSelectedFile={setSelectedFile} From 641f999efddc521cdf1f9e3817160b747660cc56 Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Fri, 7 Feb 2025 18:16:35 -0500 Subject: [PATCH 27/53] return --- tdrs-frontend/src/components/Reports/FRAReports.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/tdrs-frontend/src/components/Reports/FRAReports.jsx b/tdrs-frontend/src/components/Reports/FRAReports.jsx index c07bfa8e6..ec847f0b8 100644 --- a/tdrs-frontend/src/components/Reports/FRAReports.jsx +++ b/tdrs-frontend/src/components/Reports/FRAReports.jsx @@ -290,6 +290,7 @@ const UploadForm = ({ if (isImg) { createFileInputErrorState(input, dropTarget) setError(INVALID_FILE_ERROR) + return } const encodedFile = await tryGetUTF8EncodedFile( From 0b0be2fcce30a16f33185b2f3ff047c04777664e Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Mon, 10 Feb 2025 19:52:18 -0500 Subject: [PATCH 28/53] show message on no file selected --- tdrs-frontend/src/components/Reports/FRAReports.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tdrs-frontend/src/components/Reports/FRAReports.jsx b/tdrs-frontend/src/components/Reports/FRAReports.jsx index ec847f0b8..40588013b 100644 --- a/tdrs-frontend/src/components/Reports/FRAReports.jsx +++ b/tdrs-frontend/src/components/Reports/FRAReports.jsx @@ -304,11 +304,11 @@ const UploadForm = ({ const onSubmit = (e) => { e.preventDefault() - if (!!error || !file) { + if (!!error) { return } - if (file && file.id) { + if (!file || (file && file.id)) { setLocalAlertState({ active: true, type: 'error', From 16fc35d0b710f4f28390a033e9e713c72e1d4de5 Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Mon, 10 Feb 2025 20:31:47 -0500 Subject: [PATCH 29/53] clear alert on search --- tdrs-frontend/src/components/Reports/FRAReports.jsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tdrs-frontend/src/components/Reports/FRAReports.jsx b/tdrs-frontend/src/components/Reports/FRAReports.jsx index 40588013b..e715fcf41 100644 --- a/tdrs-frontend/src/components/Reports/FRAReports.jsx +++ b/tdrs-frontend/src/components/Reports/FRAReports.jsx @@ -524,6 +524,11 @@ const FRAReports = () => { setUploadReportToggled(false) setSearchFormValues(null) + setLocalAlertState({ + active: false, + type: null, + message: null, + }) const onSearchSuccess = () => { setUploadReportToggled(true) From 6b3a133a9e001e839e83dcb28f692c5d51779dea Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Tue, 11 Feb 2025 08:44:32 -0500 Subject: [PATCH 30/53] add backend feat flag check --- .../tdpservice/data_files/serializers.py | 10 ++++++++++ .../0044_alter_user_feature_flags.py | 18 ++++++++++++++++++ tdrs-backend/tdpservice/users/models.py | 4 ++-- 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 tdrs-backend/tdpservice/users/migrations/0044_alter_user_feature_flags.py diff --git a/tdrs-backend/tdpservice/data_files/serializers.py b/tdrs-backend/tdpservice/data_files/serializers.py index 2460c1366..5d9138413 100644 --- a/tdrs-backend/tdpservice/data_files/serializers.py +++ b/tdrs-backend/tdpservice/data_files/serializers.py @@ -114,3 +114,13 @@ def validate_file(self, file): validate_file_extension(file.name) validate_file_infection(file, file.name, user) return file + + def validate_section(self, section): + """Validate the section field.""" + if DataFile.Section.is_fra(section): + user = self.context.get('user') + print('*********') + print(user.has_fra_access) + if not user.has_fra_access: + raise serializers.ValidationError("Section cannot be FRA") + return section diff --git a/tdrs-backend/tdpservice/users/migrations/0044_alter_user_feature_flags.py b/tdrs-backend/tdpservice/users/migrations/0044_alter_user_feature_flags.py new file mode 100644 index 000000000..1f302266f --- /dev/null +++ b/tdrs-backend/tdpservice/users/migrations/0044_alter_user_feature_flags.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.15 on 2025-02-11 13:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0042_user_feature_flags'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='feature_flags', + field=models.JSONField(blank=True, default=dict, help_text='Feature flags for this user. This is a JSON field that can be used to store key-value pairs. E.g: {"fra_reports": true}'), + ), + ] diff --git a/tdrs-backend/tdpservice/users/models.py b/tdrs-backend/tdpservice/users/models.py index e150634cd..7c2f2346e 100644 --- a/tdrs-backend/tdpservice/users/models.py +++ b/tdrs-backend/tdpservice/users/models.py @@ -118,14 +118,14 @@ class Meta: feature_flags = models.JSONField( default=dict, help_text='Feature flags for this user. This is a JSON field that can be used to store key-value pairs. ' + - 'E.g: {"fra_access": true}', + 'E.g: {"fra_reports": true}', blank=True, ) @property def has_fra_access(self): """Return whether or not the user has FRA access.""" - return self.feature_flags.get('fra_access', False) + return self.feature_flags.get('fra_reports', False) def __str__(self): """Return the username as the string representation of the object.""" From 84dd2824268779349ed6376ba4208b9a184f2900 Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Tue, 11 Feb 2025 08:52:14 -0500 Subject: [PATCH 31/53] fix tests --- tdrs-backend/tdpservice/data_files/serializers.py | 2 -- tdrs-backend/tdpservice/data_files/test/test_api.py | 11 ++++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/tdrs-backend/tdpservice/data_files/serializers.py b/tdrs-backend/tdpservice/data_files/serializers.py index 5d9138413..cf15363bc 100644 --- a/tdrs-backend/tdpservice/data_files/serializers.py +++ b/tdrs-backend/tdpservice/data_files/serializers.py @@ -119,8 +119,6 @@ def validate_section(self, section): """Validate the section field.""" if DataFile.Section.is_fra(section): user = self.context.get('user') - print('*********') - print(user.has_fra_access) if not user.has_fra_access: raise serializers.ValidationError("Section cannot be FRA") return section diff --git a/tdrs-backend/tdpservice/data_files/test/test_api.py b/tdrs-backend/tdpservice/data_files/test/test_api.py index 5496ce492..34742f357 100644 --- a/tdrs-backend/tdpservice/data_files/test/test_api.py +++ b/tdrs-backend/tdpservice/data_files/test/test_api.py @@ -1,6 +1,7 @@ """Tests for DataFiles Application.""" import os from rest_framework import status +from rest_framework.exceptions import ErrorDetail import pytest import base64 import openpyxl @@ -227,9 +228,17 @@ def test_create_data_file_file_entry(self, api_client, data_file_data, user): self.assert_data_file_created(response) self.assert_data_file_exists(data_file_data, 1, user) - def test_create_data_file_fra(self, api_client, data_file_data, user): + def test_create_data_file_fra_no_feat_flag(self, api_client, data_file_data, user): """Test ability to create data file metadata registry.""" response = self.post_data_file_fra(api_client, data_file_data) + assert response.data == {'section': [ErrorDetail(string='Section cannot be FRA', code='invalid')]} + self.assert_data_file_error(response) + + def test_create_data_file_fra_with_feat_flag(self, api_client, data_file_data, user): + """Test ability to create data file metadata registry.""" + user.feature_flags = {"fra_reports": True} + user.save() + response = self.post_data_file_fra(api_client, data_file_data) self.assert_data_file_created(response) self.assert_data_file_exists(data_file_data, 1, user) From fe16f4e379e2a50d0dc5d967288709a9f78525a7 Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Tue, 11 Feb 2025 09:08:46 -0500 Subject: [PATCH 32/53] fix test --- tdrs-backend/tdpservice/users/test/test_models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tdrs-backend/tdpservice/users/test/test_models.py b/tdrs-backend/tdpservice/users/test/test_models.py index f38c30d65..17479d930 100644 --- a/tdrs-backend/tdpservice/users/test/test_models.py +++ b/tdrs-backend/tdpservice/users/test/test_models.py @@ -79,7 +79,7 @@ def test_user_with_fra_access(client, admin_user, stt): """Test that a user with FRA access can only have an STT.""" admin_user.stt = stt admin_user.is_superuser = True - admin_user.feature_flags = {"fra_access": False} + admin_user.feature_flags = {"fra_reports": False} admin_user.clean() admin_user.save() @@ -94,7 +94,7 @@ def test_user_with_fra_access(client, admin_user, stt): response = client.get(f"/admin/data_files/datafile/{datafile.id}/change/") assert response.status_code == 302 - admin_user.feature_flags = {"fra_access": True} + admin_user.feature_flags = {"fra_reports": True} admin_user.save() response = client.get(f"/admin/data_files/datafile/{datafile.id}/change/") From c4eb8e4b017799c446d4ab6891f4639ad0d58b91 Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Tue, 11 Feb 2025 13:37:22 -0500 Subject: [PATCH 33/53] change page title --- tdrs-frontend/src/components/Header/Header.jsx | 4 ++-- tdrs-frontend/src/components/Routes/Routes.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tdrs-frontend/src/components/Header/Header.jsx b/tdrs-frontend/src/components/Header/Header.jsx index 98f9efe84..099b8914f 100644 --- a/tdrs-frontend/src/components/Header/Header.jsx +++ b/tdrs-frontend/src/components/Header/Header.jsx @@ -134,8 +134,8 @@ function Header() { > {(userAccessRequestPending || userAccessRequestApproved) && ( diff --git a/tdrs-frontend/src/components/Routes/Routes.js b/tdrs-frontend/src/components/Routes/Routes.js index 89a735b4a..ab829b961 100644 --- a/tdrs-frontend/src/components/Routes/Routes.js +++ b/tdrs-frontend/src/components/Routes/Routes.js @@ -52,10 +52,10 @@ const AppRoutes = () => { /> Date: Tue, 11 Feb 2025 13:48:12 -0500 Subject: [PATCH 34/53] change Data Files -> TANF Data Files --- tdrs-frontend/src/components/Header/Header.jsx | 2 +- tdrs-frontend/src/components/Routes/Routes.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tdrs-frontend/src/components/Header/Header.jsx b/tdrs-frontend/src/components/Header/Header.jsx index 099b8914f..e07246ddd 100644 --- a/tdrs-frontend/src/components/Header/Header.jsx +++ b/tdrs-frontend/src/components/Header/Header.jsx @@ -123,7 +123,7 @@ function Header() { > diff --git a/tdrs-frontend/src/components/Routes/Routes.js b/tdrs-frontend/src/components/Routes/Routes.js index ab829b961..cbad2fe13 100644 --- a/tdrs-frontend/src/components/Routes/Routes.js +++ b/tdrs-frontend/src/components/Routes/Routes.js @@ -42,7 +42,7 @@ const AppRoutes = () => { path="/data-files" element={ From e538493800749a1089f73ec9ceac0f33d568141d Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Tue, 11 Feb 2025 14:02:17 -0500 Subject: [PATCH 35/53] fix tests --- tdrs-frontend/src/components/Header/Header.test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tdrs-frontend/src/components/Header/Header.test.js b/tdrs-frontend/src/components/Header/Header.test.js index 81e5159b2..e1115689f 100644 --- a/tdrs-frontend/src/components/Header/Header.test.js +++ b/tdrs-frontend/src/components/Header/Header.test.js @@ -46,7 +46,7 @@ describe('Header', () => { ) const welcomeLink = screen.getByText('Home') expect(welcomeLink).toBeInTheDocument() - const dataFilesLink = screen.getByText('Data Files') + const dataFilesLink = screen.getByText('TANF Data Files') expect(dataFilesLink).toBeInTheDocument() const profileLink = screen.getByText('Profile') expect(profileLink).toBeInTheDocument() @@ -103,7 +103,7 @@ describe('Header', () => { ) - const dataFilesTab = screen.getByText('Data Files') + const dataFilesTab = screen.getByText('TANF Data Files') expect(dataFilesTab.parentNode).toHaveClass('usa-current') }) @@ -163,7 +163,7 @@ describe('Header', () => { ) expect(queryByText('Welcome')).not.toBeInTheDocument() - expect(queryByText('Data Files')).not.toBeInTheDocument() + expect(queryByText('TANF Data Files')).not.toBeInTheDocument() expect(queryByText('Profile')).not.toBeInTheDocument() expect(queryByText('Admin')).not.toBeInTheDocument() }) @@ -190,7 +190,7 @@ describe('Header', () => { ) - expect(queryByText('Data Files')).not.toBeInTheDocument() + expect(queryByText('TANF Data Files')).not.toBeInTheDocument() expect(queryByText('Profile')).toBeInTheDocument() expect(queryByText('Admin')).toBeInTheDocument() }) @@ -222,7 +222,7 @@ describe('Header', () => { ) - expect(queryByText('Data Files')).not.toBeInTheDocument() + expect(queryByText('TANF Data Files')).not.toBeInTheDocument() expect(queryByText('Profile')).toBeInTheDocument() expect(queryByText('Admin')).not.toBeInTheDocument() }) @@ -236,7 +236,7 @@ describe('Header', () => { ) - expect(queryByText('Data Files')).toBeInTheDocument() + expect(queryByText('TANF Data Files')).toBeInTheDocument() expect(queryByText('Profile')).toBeInTheDocument() expect(queryByText('Admin')).toBeInTheDocument() }) From 0398d056bb8e57762e518ad6662420c0ae5f173b Mon Sep 17 00:00:00 2001 From: Jan Timpe Date: Tue, 11 Feb 2025 14:27:12 -0500 Subject: [PATCH 36/53] disable other report types --- tdrs-frontend/src/components/Form/RadioSelect.jsx | 3 ++- tdrs-frontend/src/components/Reports/FRAReports.jsx | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tdrs-frontend/src/components/Form/RadioSelect.jsx b/tdrs-frontend/src/components/Form/RadioSelect.jsx index 814029548..0795b19ed 100644 --- a/tdrs-frontend/src/components/Form/RadioSelect.jsx +++ b/tdrs-frontend/src/components/Form/RadioSelect.jsx @@ -14,7 +14,7 @@ const RadioSelect = ({
{label} - {options.map(({ label, value }, index) => ( + {options.map(({ label, value, disabled }, index) => (
setValue(value)} + disabled={!!disabled} />