From acd7990be7242778a4df4e7c11b775e03aa3fbd1 Mon Sep 17 00:00:00 2001 From: tanner-ricks <182143365+tanner-ricks@users.noreply.github.com> Date: Tue, 15 Oct 2024 14:31:05 -0500 Subject: [PATCH 01/13] initial commit --- e2e/example.spec.demo.ts | 10 +- e2e/fixtures/testFixture.ts | 15 +- e2e/pages/filing-app/formAlerts.spec.ts | 2 +- e2e/pages/filing-app/unavailableApis.spec.ts | 8 +- src/components/FormErrorHeader.data.ts | 19 +++ src/components/StepIndicator.tsx | 2 +- src/pages/Filing/FilingApp/FilingContact.tsx | 4 +- .../Filing/FilingApp/FilingSteps.helpers.tsx | 2 +- .../FilingApp/InstitutionCard.helpers.ts | 2 +- src/pages/PointOfContact/index.tsx | 138 ++++++++++++++---- src/types/filingTypes.ts | 1 + src/types/formTypes.ts | 23 +++ 12 files changed, 171 insertions(+), 55 deletions(-) diff --git a/e2e/example.spec.demo.ts b/e2e/example.spec.demo.ts index 346785d27..162a05df8 100644 --- a/e2e/example.spec.demo.ts +++ b/e2e/example.spec.demo.ts @@ -72,14 +72,14 @@ test('proof of concept', async ({ page }) => { await expect(page.locator('h1')).toContainText('Review warnings'); }); - await test.step('Review warnings: navigate to Provide point of contact', async () => { + await test.step('Review warnings: navigate to Provide filing details', async () => { await page.getByText('I verify the accuracy of').click(); await page.getByRole('button', { name: 'Continue to next step' }).click(); - await expect(page.locator('h1')).toContainText('Provide point of contact'); + await expect(page.locator('h1')).toContainText('Provide filing details'); }); - await test.step('Provide point of contact: navigate to Sign and submit', async () => { - await test.step('Provide point of contact: fill out form', async () => { + await test.step('Provide filing details: navigate to Sign and submit', async () => { + await test.step('Provide filing details: fill out form', async () => { await page.getByLabel('First name').fill(pointOfContactJson.first_name); await page.getByLabel('Last name').fill(pointOfContactJson.last_name); await page @@ -111,7 +111,7 @@ test('proof of concept', async ({ page }) => { .getByLabel('ZIP codeZIP code must be in') .fill(pointOfContactJson.hq_address_zip); }); - await test.step('Provide point of contact: continue to next step', async () => { + await test.step('Provide filing details: continue to next step', async () => { await page.getByRole('button', { name: 'Continue to next step' }).click(); await expect(page.locator('h1')).toContainText('Sign and submit'); }); diff --git a/e2e/fixtures/testFixture.ts b/e2e/fixtures/testFixture.ts index 569fbba7d..a9604ce2b 100644 --- a/e2e/fixtures/testFixture.ts +++ b/e2e/fixtures/testFixture.ts @@ -313,13 +313,12 @@ export const test = baseTest.extend<{ use, ) => { navigateToReviewWarningsAfterOnlyWarningsUpload; - await test.step('Review warnings: navigate to Provide point of contact', async () => { + await test.step('Review warnings: navigate to Provide filing details', async () => { await page.getByText('I verify the accuracy of').click(); await page.getByRole('button', { name: 'Continue to next step' }).click(); - await expect(page.locator('h1')).toContainText( - 'Provide point of contact', - { timeout: 30_000 }, - ); + await expect(page.locator('h1')).toContainText('Provide filing details', { + timeout: 30_000, + }); }); await use(page); }, @@ -329,8 +328,8 @@ export const test = baseTest.extend<{ use, ) => { navigateToProvidePointOfContact; - await test.step('Provide point of contact: navigate to Sign and submit', async () => { - await test.step('Provide point of contact: fill out form', async () => { + await test.step('Provide filing details: navigate to Sign and submit', async () => { + await test.step('Provide filing details: fill out form', async () => { await page.getByLabel('First name').fill(pointOfContactJson.first_name); await page.getByLabel('Last name').fill(pointOfContactJson.last_name); await page @@ -362,7 +361,7 @@ export const test = baseTest.extend<{ .getByLabel('ZIP codeZIP code must be in') .fill(pointOfContactJson.hq_address_zip); }); - await test.step('Provide point of contact: continue to next step', async () => { + await test.step('Provide filing details: continue to next step', async () => { await page .getByRole('button', { name: 'Continue to next step' }) .click(); diff --git a/e2e/pages/filing-app/formAlerts.spec.ts b/e2e/pages/filing-app/formAlerts.spec.ts index 1cd3482ee..34182f865 100644 --- a/e2e/pages/filing-app/formAlerts.spec.ts +++ b/e2e/pages/filing-app/formAlerts.spec.ts @@ -233,7 +233,7 @@ test('Form Alerts', async ({ // Point of contact page await test.step('Point of contact page', async () => { await expect(page.locator('h1'), 'h1 is correct').toContainText( - 'Provide point of contact', + 'Provide filing details', ); // Submit Incomplete form diff --git a/e2e/pages/filing-app/unavailableApis.spec.ts b/e2e/pages/filing-app/unavailableApis.spec.ts index 8e39b94f3..71b4cdfc0 100644 --- a/e2e/pages/filing-app/unavailableApis.spec.ts +++ b/e2e/pages/filing-app/unavailableApis.spec.ts @@ -177,10 +177,10 @@ test('Blocking API Calls - Error Boundaries', async ({ }); }); - // Provide point of contact page - await test.step('Provide point of contact page', async () => { + // Provide filing details page + await test.step('Provide filing details page', async () => { await expect(page.locator('h1'), 'h1 is correct').toContainText( - 'Provide point of contact', + 'Provide filing details', ); // Block API Call: /v1/admin/me @@ -203,7 +203,7 @@ test('Blocking API Calls - Error Boundaries', async ({ await test.step('Unblock API', async () => { await blockApi(page, '**/v1/admin/me/', false); await expect(page.locator('h1'), 'h1 is correct').toContainText( - 'Provide point of contact', + 'Provide filing details', ); }); }); diff --git a/src/components/FormErrorHeader.data.ts b/src/components/FormErrorHeader.data.ts index 9cea9a43e..f86a9ce4b 100644 --- a/src/components/FormErrorHeader.data.ts +++ b/src/components/FormErrorHeader.data.ts @@ -170,6 +170,25 @@ export const IdFormHeaderErrors: IdFormHeaderErrorsType = { export type IdFormHeaderErrorsValues = (typeof IdFormHeaderErrors)[IdZodSchemaErrorsValues]; +// Voluntary Reporter Status - Zod Schema Error Messages +export const VrsZodSchemaErrors = { + isVoluntaryMin: 'You must indicate your voluntary reporter status.', +} as const; + +export type VrsZodSchemaErrorsType = typeof VrsZodSchemaErrors; +export type VrsZodSchemaErrorsKeys = keyof typeof VrsZodSchemaErrors; +export type VrsZodSchemaErrorsValues = + (typeof VrsZodSchemaErrors)[VrsZodSchemaErrorsKeys]; + +// Point of Contact - Form Header Error Messages +export type VrsFormHeaderErrorsType = Record; +export const VrsFormHeaderErrors: VrsFormHeaderErrorsType = { + [VrsZodSchemaErrors.isVoluntaryMin]: + 'Indicate your voluntary reporter status', +} as const; +export type VrsFormHeaderErrorsValues = + (typeof VrsFormHeaderErrors)[VrsZodSchemaErrorsValues]; + // Point of Contact - Zod Schema Error Messages export const PocZodSchemaErrors = { firstNameMin: diff --git a/src/components/StepIndicator.tsx b/src/components/StepIndicator.tsx index 7f074e1b4..5a6d13275 100644 --- a/src/components/StepIndicator.tsx +++ b/src/components/StepIndicator.tsx @@ -19,7 +19,7 @@ export const mockSteps: StepType[] = [ { status: STEP_CURRENT, label: 'Upload file' }, { status: STEP_INCOMPLETE, label: 'Resolve errors' }, { status: STEP_INCOMPLETE, label: 'Review warnings' }, - { status: STEP_INCOMPLETE, label: 'Provide point of contact' }, + { status: STEP_INCOMPLETE, label: 'Provide filing details' }, { status: STEP_INCOMPLETE, label: 'Sign and submit' }, ]; diff --git a/src/pages/Filing/FilingApp/FilingContact.tsx b/src/pages/Filing/FilingApp/FilingContact.tsx index 2bfdd8a93..9613a9ad8 100644 --- a/src/pages/Filing/FilingApp/FilingContact.tsx +++ b/src/pages/Filing/FilingApp/FilingContact.tsx @@ -1,7 +1,7 @@ -import PointOfContact from 'pages/PointOfContact'; +import FilingDetails from 'pages/PointOfContact'; function FilingContact(): JSX.Element { - return ; + return ; } export default FilingContact; diff --git a/src/pages/Filing/FilingApp/FilingSteps.helpers.tsx b/src/pages/Filing/FilingApp/FilingSteps.helpers.tsx index f34edc650..b1cf1ca6b 100644 --- a/src/pages/Filing/FilingApp/FilingSteps.helpers.tsx +++ b/src/pages/Filing/FilingApp/FilingSteps.helpers.tsx @@ -95,7 +95,7 @@ export const getFilingSteps = ( }, { status: getContactStatus(currentSubmission, currentFiling), - label: 'Provide point of contact', + label: 'Provide filing details', isCurrent: isStepCurrent('/contact'), }, { diff --git a/src/pages/Filing/FilingApp/InstitutionCard.helpers.ts b/src/pages/Filing/FilingApp/InstitutionCard.helpers.ts index 02d460325..ff7a24009 100644 --- a/src/pages/Filing/FilingApp/InstitutionCard.helpers.ts +++ b/src/pages/Filing/FilingApp/InstitutionCard.helpers.ts @@ -109,7 +109,7 @@ export function deriveCardContent({ // Latest submission has no point of contact info populated case POINT_OF_CONTACT: { - title = 'Provide point of contact'; + title = 'Provide filing details'; description = 'You have completed the validation steps. Next, provide the contact information of a person that the Bureau or other regulators may contact with questions about your filing.'; diff --git a/src/pages/PointOfContact/index.tsx b/src/pages/PointOfContact/index.tsx index dc51a995a..f60157050 100644 --- a/src/pages/PointOfContact/index.tsx +++ b/src/pages/PointOfContact/index.tsx @@ -8,6 +8,7 @@ import SectionIntro from 'components/SectionIntro'; import { Alert, Paragraph, + RadioButton, SelectSingle, TextIntroduction, } from 'design-system-react'; @@ -17,8 +18,14 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useQueryClient } from '@tanstack/react-query'; import WrapperPageContent from 'WrapperPageContent'; import FormErrorHeader from 'components/FormErrorHeader'; -import type { PocFormHeaderErrorsType } from 'components/FormErrorHeader.data'; -import { PocFormHeaderErrors } from 'components/FormErrorHeader.data'; +import type { + PocFormHeaderErrorsType, + VrsFormHeaderErrorsType, +} from 'components/FormErrorHeader.data'; +import { + PocFormHeaderErrors, + VrsFormHeaderErrors, +} from 'components/FormErrorHeader.data'; import FormMain from 'components/FormMain'; import FormParagraph from 'components/FormParagraph'; import InputErrorMessage from 'components/InputErrorMessage'; @@ -31,16 +38,17 @@ import { formatPointOfContactObject, scrollToElement, } from 'pages/ProfileForm/ProfileFormUtils'; +import type { MouseEventHandler } from 'react'; import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import { useNavigate, useParams } from 'react-router-dom'; import type { FilingType } from 'types/filingTypes'; import type { ContactInfoKeys, + FilingDetailsSchema, FinancialInstitutionRS, - PointOfContactSchema, } from 'types/formTypes'; -import { ContactInfoMap, pointOfContactSchema } from 'types/formTypes'; +import { ContactInfoMap, filingDetailsSchema } from 'types/formTypes'; import { inputCharLimit } from 'utils/constants'; import useAddressStates from 'utils/useAddressStates'; import useFilingStatus from 'utils/useFilingStatus'; @@ -48,6 +56,7 @@ import useInstitutionDetails from 'utils/useInstitutionDetails'; import useSubmitPointOfContact from 'utils/useSubmitPointOfContact'; const defaultValuesPOC = { + isVoluntary: undefined, firstName: '', lastName: '', phone: '', @@ -61,13 +70,13 @@ const defaultValuesPOC = { hq_address_zip: '', }; -function PointOfContact(): JSX.Element { - const [previousContactInfoValid, setPreviousContactInfoValid] = +function FilingDetails(): JSX.Element { + const [previousFilingDetailsValid, setPreviousFilingDetailsValid] = useState(false); const queryClient = useQueryClient(); const navigate = useNavigate(); const { lei, year } = useParams(); - const formErrorHeaderId = 'PointOfContactFormErrors'; + const formErrorHeaderId = 'FilingDetailsFormErrors'; const { data: filing, isLoading: isFilingLoading, @@ -107,8 +116,8 @@ function PointOfContact(): JSX.Element { getValues, setValue, formState: { errors: formErrors, isDirty }, - } = useForm({ - resolver: zodResolver(pointOfContactSchema), + } = useForm({ + resolver: zodResolver(filingDetailsSchema), defaultValues: defaultValuesPOC, }); @@ -118,11 +127,18 @@ function PointOfContact(): JSX.Element { // @ts-expect-error Part of code cleanup for post-mvp see: https://github.com/cfpb/sbl-frontend/issues/717 const checkPreviousContactInfo = async (): void => { const passesValidation = await trigger(); - if (passesValidation) setPreviousContactInfoValid(true); + if (passesValidation) setPreviousFilingDetailsValid(true); }; if (!filing) return; + const isVoluntary = (filing as FilingType).is_voluntary; + + if (typeof isVoluntary === 'boolean') { + setValue('isVoluntary', isVoluntary); + checkPreviousContactInfo(); + } + const contactInfo = (filing as FilingType).contact_info; if (contactInfo) { @@ -140,8 +156,9 @@ function PointOfContact(): JSX.Element { const onClearform = (): void => { reset(); setValue('hq_address_state', ''); + // setValue('isVoluntary', null); scrollToElement('firstName'); - setPreviousContactInfoValid(false); // If success alert is visible, this will disable it + setPreviousFilingDetailsValid(false); // If success alert is visible, this will disable it }; const onPreviousClick = (): void => @@ -151,6 +168,14 @@ function PointOfContact(): JSX.Element { setValue('hq_address_state', value, { shouldDirty: true }); }; + const onClickVoluntaryReporter: MouseEventHandler = () => { + setValue('isVoluntary', true, { shouldDirty: true }); + }; + + const onClickNotVoluntaryReporter: MouseEventHandler = () => { + setValue('isVoluntary', false, { shouldDirty: true }); + }; + // Navigate to Sign and Submit const navigateSignSubmit = (): void => navigate(`/filing/${year}/${lei}/submit`); @@ -204,6 +229,8 @@ function PointOfContact(): JSX.Element { if (isLoading) return ; + const isVoluntary = watch('isVoluntary'); + return (
@@ -217,42 +244,89 @@ function PointOfContact(): JSX.Element { - Your financial institution's point of contact information - will not be published with your financial institution's - data. This information is required pursuant to{' '} - . + In order to continue to the next step, you are required to + complete all fields with the exception of the fields labeled + optional. } /> - {previousContactInfoValid && Object.keys(formErrors).length === 0 ? ( + {previousFilingDetailsValid && Object.keys(formErrors).length === 0 ? ( ) : null} - - alertHeading='There was a problem updating your point of contact information' + + alertHeading='There was a problem updating your filing details' errors={formErrors} id={formErrorHeaderId} - formErrorHeaderObject={PocFormHeaderErrors} + formErrorHeaderObject={{ + ...VrsFormHeaderErrors, + ...PocFormHeaderErrors, + }} keyLogicFunc={normalKeyLogic} /> -
- - You are required to complete all fields with the exception of the - street address lines labeled optional. Your point of contact - information will not be saved until you provide all required - information and continue to the next step. - -
{/* eslint-disable-next-line @typescript-eslint/no-misused-promises */} +
+ + Pursuant to , + select voluntary reporter if your financial institution is + voluntarily reporting covered applications from small businesses. + +
+
+ + + The Consumer Financial Protection Bureau (CFPB) is collecting + data to test the functionality of the Small Business Lending + Data Filing Platform.{' '} + View Privacy Notice + + + +
+ {formErrors.isVoluntary?.message ? ( + + {formErrors.isVoluntary.message} + + ) : null} +
+
+
+
+ + Pursuant to , + provide the name and business contact information of a person + that who may be contacted + with questions about your financial institution's filing. The + information you provide will not be published with your financial + institution's data. + +
The Consumer Financial Protection Bureau (CFPB) is collecting data @@ -383,8 +457,8 @@ function PointOfContact(): JSX.Element { ); } -PointOfContact.defaultProps = { +FilingDetails.defaultProps = { onSubmit: undefined, }; -export default PointOfContact; +export default FilingDetails; diff --git a/src/types/filingTypes.ts b/src/types/filingTypes.ts index 9f7f35a05..1ddd42945 100644 --- a/src/types/filingTypes.ts +++ b/src/types/filingTypes.ts @@ -65,6 +65,7 @@ export const FilingSchema = z.object({ }) .array(), institution_snapshot_id: z.string(), + is_voluntary: z.boolean(), contact_info: z.union([ z.object({ id: z.number(), diff --git a/src/types/formTypes.ts b/src/types/formTypes.ts index 6533a2aa9..2733b11a8 100644 --- a/src/types/formTypes.ts +++ b/src/types/formTypes.ts @@ -266,6 +266,21 @@ export interface FormattedUserProfileObjectType { leis?: InstitutionDetailsApiType['lei'][]; } +// Voluntary Reporter Status +export const voluntaryReporterStatusSchema = z.object({ + isVoluntary: z + .boolean({ + invalid_type_error: 'You must indicate your voluntary reporter status.', + required_error: 'You must indicate your voluntary reporter status.', + description: 'You must indicate your voluntary reporter status.', + }) + .nullable(), +}); + +export type VoluntaryReporterStatusSchema = z.infer< + typeof voluntaryReporterStatusSchema +>; + // NOTE: Placeholder for possible future use // eslint-disable-next-line @typescript-eslint/no-unused-vars const internationalPhoneNumberRegex = @@ -404,3 +419,11 @@ export type FormattedPointOfContactSchema = Omit< phone_number: string; phone_ext: string | undefined; }; + +// Filing Details +export const filingDetailsSchema = z.intersection( + voluntaryReporterStatusSchema, + pointOfContactSchema, +); + +export type FilingDetailsSchema = z.infer; From 3a24df8a1f9846b47404a07a0cf304ebbc489778 Mon Sep 17 00:00:00 2001 From: tanner-ricks <182143365+tanner-ricks@users.noreply.github.com> Date: Tue, 15 Oct 2024 15:44:20 -0500 Subject: [PATCH 02/13] updated sign and submit --- src/components/CommonLinks.tsx | 2 +- .../Filing/FilingApp/FilingSubmit.helpers.tsx | 30 ++++++++----------- src/pages/Filing/FilingApp/FilingSubmit.tsx | 15 +++++++--- src/pages/PointOfContact/index.tsx | 10 +++---- 4 files changed, 28 insertions(+), 29 deletions(-) diff --git a/src/components/CommonLinks.tsx b/src/components/CommonLinks.tsx index c723726c4..2c89182b4 100644 --- a/src/components/CommonLinks.tsx +++ b/src/components/CommonLinks.tsx @@ -50,7 +50,7 @@ interface UpdatePointOfContactProperties { } function UpdatePointOfContact({ - label = 'update your point of contact information', + label = 'update your filing details', className = 'font-normal', }: UpdatePointOfContactProperties): ReactElement { const { lei, year } = useParams(); diff --git a/src/pages/Filing/FilingApp/FilingSubmit.helpers.tsx b/src/pages/Filing/FilingApp/FilingSubmit.helpers.tsx index 8a6f8fbf9..3277cfe67 100644 --- a/src/pages/Filing/FilingApp/FilingSubmit.helpers.tsx +++ b/src/pages/Filing/FilingApp/FilingSubmit.helpers.tsx @@ -1,7 +1,7 @@ import Links from 'components/CommonLinks'; import FormSectionWrapper from 'components/FormSectionWrapper'; import SectionIntro from 'components/SectionIntro'; -import { Checkbox, WellContainer } from 'design-system-react'; +import { Checkbox, Label, WellContainer } from 'design-system-react'; import type { ChangeEvent, ReactNode } from 'react'; import { useParams } from 'react-router-dom'; import type { FilingType, SubmissionResponse } from 'types/filingTypes'; @@ -126,29 +126,23 @@ export function FileInformation({ } export function VoluntaryReportingStatus({ - onChange, - value, + data, + heading = 'Confirm voluntary reporter status', + description = getDescriptionForSignAndSubmitSection('poc'), }: { - onChange: (event: ChangeEvent) => void; - value: boolean; + data: FilingType; + // eslint-disable-next-line react/require-default-props + heading?: string; + // eslint-disable-next-line react/require-default-props + description?: ReactNode; }): JSX.Element { return ( - - Pursuant to , indicate - whether your financial institution is voluntarily reporting covered - applications from small businesses. Leave the box unchecked if you are - not a voluntary reporter. - + {description} - + + {data?.is_voluntary ? 'Voluntary reporter' : 'Not a voluntary reporter'} ); diff --git a/src/pages/Filing/FilingApp/FilingSubmit.tsx b/src/pages/Filing/FilingApp/FilingSubmit.tsx index a77ab0d63..0075518e8 100644 --- a/src/pages/Filing/FilingApp/FilingSubmit.tsx +++ b/src/pages/Filing/FilingApp/FilingSubmit.tsx @@ -174,10 +174,6 @@ export function FilingSubmit(): JSX.Element { ) : ( '' )} -
+ +
+ +
+ {/* @ts-expect-error Part of code cleanup for post-mvp see: https://github.com/cfpb/sbl-frontend/issues/717 */} 0 ? user.name : user.email} diff --git a/src/pages/PointOfContact/index.tsx b/src/pages/PointOfContact/index.tsx index f60157050..4d48c5531 100644 --- a/src/pages/PointOfContact/index.tsx +++ b/src/pages/PointOfContact/index.tsx @@ -7,6 +7,7 @@ import InputEntry from 'components/InputEntry'; import SectionIntro from 'components/SectionIntro'; import { Alert, + Label, Paragraph, RadioButton, SelectSingle, @@ -286,12 +287,9 @@ function FilingDetails(): JSX.Element {
- - The Consumer Financial Protection Bureau (CFPB) is collecting - data to test the functionality of the Small Business Lending - Data Filing Platform.{' '} - View Privacy Notice - + Date: Wed, 16 Oct 2024 12:30:51 -0500 Subject: [PATCH 03/13] Refactors for better abstraction --- src/components/RadioButtonGroup.tsx | 57 ++++++++++++++ .../VoluntaryReporterStatus.tsx | 77 +++++++++++++++++++ src/pages/PointOfContact/index.tsx | 62 +++------------ src/types/formTypes.ts | 17 ++++ 4 files changed, 162 insertions(+), 51 deletions(-) create mode 100644 src/components/RadioButtonGroup.tsx create mode 100644 src/pages/PointOfContact/VoluntaryReporterStatus.tsx diff --git a/src/components/RadioButtonGroup.tsx b/src/components/RadioButtonGroup.tsx new file mode 100644 index 000000000..5fe55a38d --- /dev/null +++ b/src/components/RadioButtonGroup.tsx @@ -0,0 +1,57 @@ +import { Label } from 'design-system-react'; + +function RadioButtonGroup({ + id = 'radio-button-group', + label = 'Radio Group', + onChange = undefined, + children, +}: { + id?: string | null | undefined; + label?: string | null | undefined; + onChange?: React.ChangeEventHandler | null | undefined; + children: React.ReactNode; +}): JSX.Element { + const ENTER_KEY_CODE = 13; + + const onKeyDown = (event: React.KeyboardEvent): void => { + if ( + !event.altKey && + !event.ctrlKey && + !event.metaKey && + !event.shiftKey && + event.keyCode === ENTER_KEY_CODE + ) { + event.preventDefault(); + event.stopPropagation(); + (event.target as HTMLInputElement).click(); + } + }; + + return ( + <> + + {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} +
+ {children} +
+ + ); +} + +RadioButtonGroup.defaultProps = { + id: 'radio-button-group', + label: 'Radio Group', + onChange: undefined, +}; + +export default RadioButtonGroup; diff --git a/src/pages/PointOfContact/VoluntaryReporterStatus.tsx b/src/pages/PointOfContact/VoluntaryReporterStatus.tsx new file mode 100644 index 000000000..789706f48 --- /dev/null +++ b/src/pages/PointOfContact/VoluntaryReporterStatus.tsx @@ -0,0 +1,77 @@ +import Links from 'components/CommonLinks'; +import FieldGroup from 'components/FieldGroup'; +import SectionIntro from 'components/SectionIntro'; +import { RadioButton } from 'design-system-react'; +import InputErrorMessage from 'components/InputErrorMessage'; +import type { FieldErrors } from 'react-hook-form'; +import type { FilingDetailsSchema } from 'types/formTypes'; +import FormSectionWrapper from 'components/FormSectionWrapper'; +import RadioButtonGroup from '../../components/RadioButtonGroup'; + +function VoluntaryReporterStatus({ + value, + disabled = false, + formErrors, + onChange, +}: { + value?: boolean | null | undefined; + disabled?: boolean | null | undefined; + formErrors?: FieldErrors | null | undefined; + onChange?: ((selected: boolean) => void) | null | undefined; +}): JSX.Element { + const onGroupChange = (event: React.ChangeEvent): void => { + onChange?.((event.target as HTMLInputElement).id === 'is-voluntary'); + }; + + return ( + + + Pursuant to , select + voluntary reporter if your financial institution is voluntarily + reporting covered applications from small businesses. + + + + + + +
+ {formErrors?.isVoluntary?.message ? ( + + {formErrors?.isVoluntary.message} + + ) : null} +
+
+
+ ); +} + +VoluntaryReporterStatus.defaultProps = { + disabled: false, + formErrors: undefined, + onChange: undefined, + value: undefined, +}; + +export default VoluntaryReporterStatus; diff --git a/src/pages/PointOfContact/index.tsx b/src/pages/PointOfContact/index.tsx index 4d48c5531..40be3eaa3 100644 --- a/src/pages/PointOfContact/index.tsx +++ b/src/pages/PointOfContact/index.tsx @@ -7,9 +7,7 @@ import InputEntry from 'components/InputEntry'; import SectionIntro from 'components/SectionIntro'; import { Alert, - Label, Paragraph, - RadioButton, SelectSingle, TextIntroduction, } from 'design-system-react'; @@ -39,7 +37,6 @@ import { formatPointOfContactObject, scrollToElement, } from 'pages/ProfileForm/ProfileFormUtils'; -import type { MouseEventHandler } from 'react'; import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import { useNavigate, useParams } from 'react-router-dom'; @@ -55,6 +52,7 @@ import useAddressStates from 'utils/useAddressStates'; import useFilingStatus from 'utils/useFilingStatus'; import useInstitutionDetails from 'utils/useInstitutionDetails'; import useSubmitPointOfContact from 'utils/useSubmitPointOfContact'; +import VoluntaryReporterStatus from './VoluntaryReporterStatus'; const defaultValuesPOC = { isVoluntary: undefined, @@ -157,7 +155,7 @@ function FilingDetails(): JSX.Element { const onClearform = (): void => { reset(); setValue('hq_address_state', ''); - // setValue('isVoluntary', null); + setValue('isVoluntary', null); scrollToElement('firstName'); setPreviousFilingDetailsValid(false); // If success alert is visible, this will disable it }; @@ -169,14 +167,6 @@ function FilingDetails(): JSX.Element { setValue('hq_address_state', value, { shouldDirty: true }); }; - const onClickVoluntaryReporter: MouseEventHandler = () => { - setValue('isVoluntary', true, { shouldDirty: true }); - }; - - const onClickNotVoluntaryReporter: MouseEventHandler = () => { - setValue('isVoluntary', false, { shouldDirty: true }); - }; - // Navigate to Sign and Submit const navigateSignSubmit = (): void => navigate(`/filing/${year}/${lei}/submit`); @@ -226,12 +216,14 @@ function FilingDetails(): JSX.Element { } }; + const onVoluntaryReporterStatusChange = (selected: boolean): void => { + setValue('isVoluntary', selected, { shouldDirty: true }); + }; + // TODO: Redirect the user if the filing period or lei are not valid if (isLoading) return ; - const isVoluntary = watch('isVoluntary'); - return (
@@ -278,43 +270,11 @@ function FilingDetails(): JSX.Element { /> {/* eslint-disable-next-line @typescript-eslint/no-misused-promises */} -
- - Pursuant to , - select voluntary reporter if your financial institution is - voluntarily reporting covered applications from small businesses. - -
-
- - - - -
- {formErrors.isVoluntary?.message ? ( - - {formErrors.isVoluntary.message} - - ) : null} -
-
-
+
Pursuant to , diff --git a/src/types/formTypes.ts b/src/types/formTypes.ts index 2733b11a8..84bf90ecb 100644 --- a/src/types/formTypes.ts +++ b/src/types/formTypes.ts @@ -281,6 +281,23 @@ export type VoluntaryReporterStatusSchema = z.infer< typeof voluntaryReporterStatusSchema >; +export const VoluntaryReporterStatusMap = { + is_voluntary: 'isVoluntary', +} as const; + +export type VoluntaryReporterStatusMapType = typeof VoluntaryReporterStatusMap; +export type VoluntaryReporterStatusKeys = + keyof typeof VoluntaryReporterStatusMap; +export type VoluntaryReporterStatusValues = + (typeof VoluntaryReporterStatusMap)[VoluntaryReporterStatusKeys]; + +export type FormattedVoluntaryReporterStatusSchema = Omit< + VoluntaryReporterStatusSchema, + 'isVoluntary' +> & { + is_voluntary: boolean; +}; + // NOTE: Placeholder for possible future use // eslint-disable-next-line @typescript-eslint/no-unused-vars const internationalPhoneNumberRegex = From f58daa309cb3c0ad628134eadd4e5036dfad7bd7 Mon Sep 17 00:00:00 2001 From: tanner-ricks <182143365+tanner-ricks@users.noreply.github.com> Date: Wed, 16 Oct 2024 13:19:43 -0500 Subject: [PATCH 04/13] a few test fixes --- .../filing-app/filing-step-routing/pointOfContact.spec.ts | 2 +- e2e/pages/filing-app/formAlerts.spec.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/e2e/pages/filing-app/filing-step-routing/pointOfContact.spec.ts b/e2e/pages/filing-app/filing-step-routing/pointOfContact.spec.ts index 05720496b..12af376c2 100644 --- a/e2e/pages/filing-app/filing-step-routing/pointOfContact.spec.ts +++ b/e2e/pages/filing-app/filing-step-routing/pointOfContact.spec.ts @@ -7,7 +7,7 @@ const currentStepPath = '/contact'; const userShouldNotAccess = ['/submit']; -const afterRedirectHeading = 'Provide point of contact'; +const afterRedirectHeading = 'Provide filing details'; const afterRedirectURL = /.*\/contact$/; test(testLabel, async ({ page, navigateToProvidePointOfContact }) => { diff --git a/e2e/pages/filing-app/formAlerts.spec.ts b/e2e/pages/filing-app/formAlerts.spec.ts index 34182f865..c59bf57d5 100644 --- a/e2e/pages/filing-app/formAlerts.spec.ts +++ b/e2e/pages/filing-app/formAlerts.spec.ts @@ -247,13 +247,14 @@ test('Form Alerts', async ({ page.locator('.m-notification__error'), 'Error alert is visible', ).toContainText( - 'There was a problem updating your point of contact informationEnter the first name of the point of contactEnter the last name of the point of contactEnter the phone number of the point of contactEnter the email address of the point of contactEnter the street address of the point of contactEnter the city of the point of contactSelect the state or territory of the point of contactEnter the ZIP code of the point of contact', + 'There was a problem updating your filing detailsIndicate your voluntary reporter statusEnter the first name of the point of contactEnter the last name of the point of contactEnter the phone number of the point of contactEnter the email address of the point of contactEnter the street address of the point of contactEnter the city of the point of contactSelect the state or territory of the point of contactEnter the ZIP code of the point of contact', ); }); // Submit Completed form await test.step('Submit Completed form', async () => { await test.step('Complete form', async () => { + await page.getByText('Voluntary reporter', { exact: true }).click(); await page.getByLabel('First name').fill('Playwright'); await page.getByLabel('Last name').fill('Test'); await page.getByLabel('Phone number').fill('555-555-5555'); From e5a97251cfceab6b149c222277c1ba9ce6c6e1ba Mon Sep 17 00:00:00 2001 From: tanner-ricks <182143365+tanner-ricks@users.noreply.github.com> Date: Wed, 16 Oct 2024 13:43:48 -0500 Subject: [PATCH 05/13] removing remporary text highlight --- src/pages/PointOfContact/index.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pages/PointOfContact/index.tsx b/src/pages/PointOfContact/index.tsx index 40be3eaa3..68d6928a2 100644 --- a/src/pages/PointOfContact/index.tsx +++ b/src/pages/PointOfContact/index.tsx @@ -278,11 +278,10 @@ function FilingDetails(): JSX.Element {
Pursuant to , - provide the name and business contact information of a person - that who may be contacted - with questions about your financial institution's filing. The - information you provide will not be published with your financial - institution's data. + provide the name and business contact information of a person that + who may be contacted with questions about your financial + institution's filing. The information you provide will not be + published with your financial institution's data.
From a8d45584e430ac8103afcd3f471e04d886eb2c70 Mon Sep 17 00:00:00 2001 From: tanner-ricks <182143365+tanner-ricks@users.noreply.github.com> Date: Thu, 17 Oct 2024 11:57:52 -0500 Subject: [PATCH 06/13] Updated some verbiage and fixed a styling thing --- src/pages/PointOfContact/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/PointOfContact/index.tsx b/src/pages/PointOfContact/index.tsx index 68d6928a2..860cece12 100644 --- a/src/pages/PointOfContact/index.tsx +++ b/src/pages/PointOfContact/index.tsx @@ -275,11 +275,11 @@ function FilingDetails(): JSX.Element { formErrors={formErrors} onChange={onVoluntaryReporterStatusChange} /> -
+
Pursuant to , provide the name and business contact information of a person that - who may be contacted with questions about your financial + the CFPB or other regulators may contact about the financial institution's filing. The information you provide will not be published with your financial institution's data. From 16bf1f76328fc32a67532d80df3129000ad4991f Mon Sep 17 00:00:00 2001 From: tanner-ricks <182143365+tanner-ricks@users.noreply.github.com> Date: Tue, 22 Oct 2024 14:31:05 -0500 Subject: [PATCH 07/13] Merged from main --- e2e/example.spec.demo.ts | 2 +- e2e/fixtures/testFixture.ts | 2 +- .../checkCupFormErrors.spec.ts | 40 +++++ e2e/pages/filing-app/formAlerts.spec.ts | 6 +- .../checkPocFormErrors.spec.ts | 147 +++++++++++++++++- .../NonAssociatedUserProfile.spec.ts | 79 ++++------ .../UpdateInstitutionProfile.spec.ts | 110 +++++++++++++ e2e/utils/inputValidators.ts | 73 +++++++++ src/components/InputEntry.tsx | 18 +-- src/components/TextInput.tsx | 53 +++++++ src/pages/PointOfContact/index.tsx | 9 +- .../AddFinancialInstitution.tsx | 2 + .../Step1Form/Step1FormInfoFieldGroup.tsx | 4 +- src/types/formTypes.ts | 14 +- src/utils/constants.ts | 9 +- 15 files changed, 487 insertions(+), 81 deletions(-) create mode 100644 e2e/utils/inputValidators.ts create mode 100644 src/components/TextInput.tsx diff --git a/e2e/example.spec.demo.ts b/e2e/example.spec.demo.ts index 162a05df8..6d2ef0425 100644 --- a/e2e/example.spec.demo.ts +++ b/e2e/example.spec.demo.ts @@ -86,7 +86,7 @@ test('proof of concept', async ({ page }) => { .getByLabel('Phone numberPhone number') .fill(pointOfContactJson.phone_number); await page - .getByLabel('Extension (optional)Extension') + .getByLabel('Phone extension (optional)') .fill(pointOfContactJson.phone_ext); await page .getByLabel('Email addressEmail address') diff --git a/e2e/fixtures/testFixture.ts b/e2e/fixtures/testFixture.ts index a9604ce2b..50e7268d7 100644 --- a/e2e/fixtures/testFixture.ts +++ b/e2e/fixtures/testFixture.ts @@ -336,7 +336,7 @@ export const test = baseTest.extend<{ .getByLabel('Phone numberPhone number') .fill(pointOfContactJson.phone_number); await page - .getByLabel('Extension (optional)Extension') + .getByLabel('Phone Extension (optional)') .fill(pointOfContactJson.phone_ext); await page .getByLabel('Email addressEmail address') diff --git a/e2e/pages/filing-app/complete-user-profile/checkCupFormErrors.spec.ts b/e2e/pages/filing-app/complete-user-profile/checkCupFormErrors.spec.ts index 08cd3ded5..15460661e 100644 --- a/e2e/pages/filing-app/complete-user-profile/checkCupFormErrors.spec.ts +++ b/e2e/pages/filing-app/complete-user-profile/checkCupFormErrors.spec.ts @@ -1,5 +1,8 @@ import { expect } from '@playwright/test'; import { test } from '../../../fixtures/testFixture'; +import { DefaultInputCharLimit } from 'utils/constants'; +import { assertTextInput } from '../../../utils/inputValidators'; +import { controlUnicode } from '../../../utils/unicodeConstants'; test('Complete the User Profile: Checking for form errors based on user input', async ({ page, @@ -33,3 +36,40 @@ test('Complete the User Profile: Checking for form errors based on user input', ); }); }); + +test('Complete the User Profile: Checking for input length restriction', async ({ + page, +}) => { + test.slow(); + + await test.step('Complete the User Profile: Check that the error header render when no input is filled', async () => { + await page.getByLabel('Submit User Profile').click(); + await expect(page.locator('#step1FormErrorHeader div').first()).toBeVisible( + { + timeout: 30_000, + }, + ); + }); + + await test.step('Complete the User Profile: Check the first and last names for invalid input', async () => { + const expectedValues = { + firstField: controlUnicode.slice(0, DefaultInputCharLimit), + lastField: controlUnicode.slice(0, DefaultInputCharLimit), + }; + const unexpectedValues = { + firstField: controlUnicode, + lastField: controlUnicode, + }; + + await assertTextInput(page, 'First name', { + fill: controlUnicode, + expected: expectedValues.firstField, + unexpected: unexpectedValues.firstField, + }); + await assertTextInput(page, 'Last name', { + fill: controlUnicode, + expected: expectedValues.lastField, + unexpected: unexpectedValues.lastField, + }); + }); +}); diff --git a/e2e/pages/filing-app/formAlerts.spec.ts b/e2e/pages/filing-app/formAlerts.spec.ts index c59bf57d5..1eeb9b9b1 100644 --- a/e2e/pages/filing-app/formAlerts.spec.ts +++ b/e2e/pages/filing-app/formAlerts.spec.ts @@ -1,5 +1,6 @@ import { expect } from '@playwright/test'; import { test } from '../../fixtures/testFixture'; +import pointOfContactJson from '../../test-data/point-of-contact/point-of-contact-data-1.json'; import { ResultUploadMessage, uploadFile } from '../../utils/uploadFile'; test('Form Alerts', async ({ @@ -247,7 +248,7 @@ test('Form Alerts', async ({ page.locator('.m-notification__error'), 'Error alert is visible', ).toContainText( - 'There was a problem updating your filing detailsIndicate your voluntary reporter statusEnter the first name of the point of contactEnter the last name of the point of contactEnter the phone number of the point of contactEnter the email address of the point of contactEnter the street address of the point of contactEnter the city of the point of contactSelect the state or territory of the point of contactEnter the ZIP code of the point of contact', + 'There was a problem updating your filing detailsEnter the first name of the point of contactEnter the last name of the point of contactEnter the phone number of the point of contactEnter a valid phone extensionEnter the email address of the point of contactEnter the street address of the point of contactEnter the city of the point of contactSelect the state or territory of the point of contactEnter the ZIP code of the point of contact', ); }); @@ -258,6 +259,9 @@ test('Form Alerts', async ({ await page.getByLabel('First name').fill('Playwright'); await page.getByLabel('Last name').fill('Test'); await page.getByLabel('Phone number').fill('555-555-5555'); + await page + .getByLabel('Phone extension (optional)') + .fill(pointOfContactJson.phone_ext); await page.getByLabel('Email address').fill('playwright@test.com'); await page.getByLabel('Street address line 1').fill('555 Main St.'); await page.getByLabel('City').fill('Utah (U'); diff --git a/e2e/pages/filing-app/point-of-contact/checkPocFormErrors.spec.ts b/e2e/pages/filing-app/point-of-contact/checkPocFormErrors.spec.ts index 4bf896c32..93f48ebb1 100644 --- a/e2e/pages/filing-app/point-of-contact/checkPocFormErrors.spec.ts +++ b/e2e/pages/filing-app/point-of-contact/checkPocFormErrors.spec.ts @@ -1,5 +1,16 @@ import { expect } from '@playwright/test'; import { test } from '../../../fixtures/testFixture'; +import { controlUnicode } from '../../../utils/unicodeConstants'; +import { + assertTextInput, + assertSelectInput, +} from '../../../utils/inputValidators'; +import { + DefaultInputCharLimit, + PhoneInputCharLimit, + EmailInputCharLimit, + ZipInputCharLimit, +} from 'utils/constants'; test('Point of Contact: Checking for form errors based on user input', async ({ page, @@ -12,7 +23,7 @@ test('Point of Contact: Checking for form errors based on user input', async ({ await test.step('Point of Contact: Check that the error header render when no input is filled', async () => { await page.getByRole('button', { name: 'Continue to next step' }).click(); await expect( - page.locator('#PointOfContactFormErrors div').first(), + page.locator('#FilingDetailsFormErrors div').first(), ).toBeVisible(); }); @@ -29,11 +40,141 @@ test('Point of Contact: Checking for form errors based on user input', async ({ await expect(page.locator('form')).toContainText( 'The last name must not contain invalid characters', ); - await expect(page.locator('#PointOfContactFormErrors')).toContainText( + await expect(page.locator('#FilingDetailsFormErrors')).toContainText( 'The first name must not contain invalid characters', ); - await expect(page.locator('#PointOfContactFormErrors')).toContainText( + await expect(page.locator('#FilingDetailsFormErrors')).toContainText( 'The last name must not contain invalid characters', ); }); }); + +test('Point of Contact: Checking for unicode tolerance based on user input', async ({ + page, + navigateToProvidePointOfContact, +}) => { + test.slow(); + + navigateToProvidePointOfContact; + + await test.step('Point of Contact: Check that the error header render when no input is filled', async () => { + await page.getByRole('button', { name: 'Continue to next step' }).click(); + await expect( + page.locator('#FilingDetailsFormErrors div').first(), + ).toBeVisible(); + }); + + await test.step('Point of Contact: Check the first and last names for invalid input', async () => { + const expectedValues = { + firstField: controlUnicode.slice(0, DefaultInputCharLimit), + lastField: controlUnicode.slice(0, DefaultInputCharLimit), + phoneField: controlUnicode.slice(0, PhoneInputCharLimit), + extensionField: controlUnicode.slice(0, DefaultInputCharLimit), + emailField: controlUnicode.slice(0, EmailInputCharLimit), + addressField1: controlUnicode.slice(0, DefaultInputCharLimit), + addressField2: controlUnicode.slice(0, DefaultInputCharLimit), + addressField3: controlUnicode.slice(0, DefaultInputCharLimit), + addressField4: controlUnicode.slice(0, DefaultInputCharLimit), + cityField: controlUnicode.slice(0, DefaultInputCharLimit), + stateField: 'TX', + zipField: controlUnicode.slice(0, ZipInputCharLimit), + }; + const unexpectedValues = { + firstField: controlUnicode, + lastField: controlUnicode, + phoneField: controlUnicode, + extensionField: controlUnicode, + emailField: controlUnicode, + addressField1: controlUnicode, + addressField2: controlUnicode, + addressField3: controlUnicode, + addressField4: controlUnicode, + cityField: controlUnicode, + stateField: '', + zipField: controlUnicode, + }; + + await assertTextInput(page, 'First name', { + fill: controlUnicode, + expected: expectedValues.firstField, + unexpected: unexpectedValues.firstField, + }); + await assertTextInput(page, 'Last name', { + fill: controlUnicode, + expected: expectedValues.lastField, + unexpected: unexpectedValues.lastField, + }); + await assertTextInput(page, 'Phone number', { + fill: controlUnicode, + expected: expectedValues.phoneField, + unexpected: unexpectedValues.phoneField, + }); + await assertTextInput(page, 'Extension', { + fill: controlUnicode, + expected: expectedValues.extensionField, + unexpected: unexpectedValues.extensionField, + }); + await assertTextInput(page, 'Email address', { + fill: controlUnicode, + expected: expectedValues.emailField, + unexpected: unexpectedValues.emailField, + }); + await assertTextInput(page, 'Street address line 1', { + fill: controlUnicode, + expected: expectedValues.addressField1, + unexpected: unexpectedValues.addressField1, + }); + await assertTextInput(page, 'Street address line 2', { + fill: controlUnicode, + expected: expectedValues.addressField2, + unexpected: unexpectedValues.addressField2, + }); + await assertTextInput(page, 'Street address line 3', { + fill: controlUnicode, + expected: expectedValues.addressField3, + unexpected: unexpectedValues.addressField3, + }); + await assertTextInput(page, 'Street address line 4', { + fill: controlUnicode, + expected: expectedValues.addressField4, + unexpected: unexpectedValues.addressField4, + }); + await assertTextInput(page, 'City', { + fill: controlUnicode, + expected: expectedValues.cityField, + unexpected: unexpectedValues.cityField, + }); + await assertSelectInput(page, 'State or territory', { + fill: { label: 'Texas (TX)' }, + expected: expectedValues.stateField, + unexpected: unexpectedValues.stateField, + }); + await assertTextInput(page, 'Zip code', { + fill: controlUnicode, + expected: expectedValues.zipField, + unexpected: unexpectedValues.zipField, + }); + + await page.getByRole('button', { name: 'Continue to next step' }).click(); + + await expect(page.locator('#FilingDetailsFormErrors')).toContainText( + 'Enter a valid phone number', + ); + await expect(page.locator('#FilingDetailsFormErrors')).toContainText( + 'Enter a valid email address', + ); + await expect(page.locator('#FilingDetailsFormErrors')).toContainText( + 'Enter a valid ZIP code', + ); + + await expect(page.locator('form')).toContainText( + 'You must enter a valid phone number.', + ); + await expect(page.locator('form')).toContainText( + 'You must enter a valid email address.', + ); + await expect(page.locator('form')).toContainText( + 'You must enter a valid ZIP code.', + ); + }); +}); diff --git a/e2e/pages/shared-lending-platform/NonAssociatedUserProfile.spec.ts b/e2e/pages/shared-lending-platform/NonAssociatedUserProfile.spec.ts index e93fe637d..8f81f86e5 100644 --- a/e2e/pages/shared-lending-platform/NonAssociatedUserProfile.spec.ts +++ b/e2e/pages/shared-lending-platform/NonAssociatedUserProfile.spec.ts @@ -1,6 +1,8 @@ import { expect } from '@playwright/test'; import { test } from '../../fixtures/testFixture'; import { controlUnicode } from '../../utils/unicodeConstants'; +import { assertTextInput } from '../../utils/inputValidators'; +import { DefaultInputCharLimit, LeiInputCharLimit } from 'utils/constants'; const expectedNoAssociationsSummaryUrl = /\/profile\/complete\/summary\/submitted$/; @@ -40,62 +42,37 @@ test('Complete User Profile with Bad Unicode -- No Associations -- process', asy await test.step('Fillout Complete User Profile (No Associations) with bad unicode and verify values', async () => { const expectedValues = { - firstField: controlUnicode.slice(0, 255), - lastField: controlUnicode.slice(0, 255), - // TODO: Update with correct value after char limit in place, see: - // https://github.com/cfpb/sbl-frontend/issues/972 - finField: controlUnicode, - // TODO: Update with correct value after char limit in place, see: - // https://github.com/cfpb/sbl-frontend/issues/972 - leiField: controlUnicode, + firstField: controlUnicode.slice(0, DefaultInputCharLimit), + lastField: controlUnicode.slice(0, DefaultInputCharLimit), + finField: controlUnicode.slice(0, DefaultInputCharLimit), + leiField: controlUnicode.slice(0, LeiInputCharLimit), }; const unexpectedValues = { firstField: controlUnicode, lastField: controlUnicode, - // TODO: Change to controlUnicode after char limit in place, see: - // https://github.com/cfpb/sbl-frontend/issues/972 - finField: '', - // TODO: Change to controlUnicode after char limit in place, see: - // https://github.com/cfpb/sbl-frontend/issues/972 - leiField: '', + finField: controlUnicode, + leiField: controlUnicode, }; - await page.getByLabel('First name').click(); - await page.getByLabel('First name').fill(controlUnicode); - - await page.getByLabel('Last name').click(); - await page.getByLabel('Last name').fill(controlUnicode); - - await page.getByLabel('Financial institution name').click(); - await page.getByLabel('Financial institution name').fill(controlUnicode); - - await page.getByLabel('Legal Entity Identifier (LEI)').click(); - await page.getByLabel('Legal Entity Identifier (LEI)').fill(controlUnicode); - - await expect(page.getByLabel('First name')).not.toHaveValue( - unexpectedValues.firstField, - ); - await expect(page.getByLabel('Last name')).not.toHaveValue( - unexpectedValues.lastField, - ); - await expect(page.getByLabel('Financial institution name')).not.toHaveValue( - unexpectedValues.finField, - ); - await expect( - page.getByLabel('Legal Entity Identifier (LEI)'), - ).not.toHaveValue(unexpectedValues.leiField); - - await expect(page.getByLabel('First name')).toHaveValue( - expectedValues.firstField, - ); - await expect(page.getByLabel('Last name')).toHaveValue( - expectedValues.lastField, - ); - await expect(page.getByLabel('Financial institution name')).toHaveValue( - expectedValues.finField, - ); - await expect(page.getByLabel('Legal Entity Identifier (LEI)')).toHaveValue( - expectedValues.leiField, - ); + await assertTextInput(page, 'First name', { + fill: controlUnicode, + expected: expectedValues.firstField, + unexpected: unexpectedValues.firstField, + }); + await assertTextInput(page, 'Last name', { + fill: controlUnicode, + expected: expectedValues.lastField, + unexpected: unexpectedValues.lastField, + }); + await assertTextInput(page, 'Financial institution name', { + fill: controlUnicode, + expected: expectedValues.finField, + unexpected: unexpectedValues.finField, + }); + await assertTextInput(page, 'Legal Entity Identifier (LEI)', { + fill: controlUnicode, + expected: expectedValues.leiField, + unexpected: unexpectedValues.leiField, + }); }); }); diff --git a/e2e/pages/shared-lending-platform/UpdateInstitutionProfile.spec.ts b/e2e/pages/shared-lending-platform/UpdateInstitutionProfile.spec.ts index 240aa4b5e..6e5797225 100644 --- a/e2e/pages/shared-lending-platform/UpdateInstitutionProfile.spec.ts +++ b/e2e/pages/shared-lending-platform/UpdateInstitutionProfile.spec.ts @@ -1,5 +1,8 @@ import { expect } from '@playwright/test'; import { test } from '../../fixtures/testFixture'; +import { DefaultInputCharLimit } from 'utils/constants'; +import { assertTextInput } from '../../utils/inputValidators'; +import { controlUnicode } from '../../utils/unicodeConstants'; test('Update Institution Profile Page', async ({ page, @@ -191,3 +194,110 @@ test('Update Institution Profile Page', async ({ }); }); }); + +test('Update Institution Profile Page: Check Character Limits', async ({ + page, + navigateToFilingHome, +}) => { + test.slow(); + + // Profile page + await test.step('User Profile Page', async () => { + navigateToFilingHome; + await page.goto('/profile/view'); + }); + + await test.step('Click: Institution link', async () => { + await page + .getByRole('link', { name: 'RegTech Regional Reserve - ' }) + .click(); + }); + + // Institution Profile page + await test.step('Institution Profile Page', async () => { + await page + .getByRole('link', { + name: 'Update your financial institution profile', + }) + .first() + .click(); + }); + + // Update Institution Profile page + await test.step('Update Institution Profile Page', async () => { + const expectedValues = { + otherField: controlUnicode.slice(0, DefaultInputCharLimit), + parentNameField: controlUnicode.slice(0, DefaultInputCharLimit), + parentLeiField: controlUnicode.slice(0, DefaultInputCharLimit), + parentRssdField: '', + topNameField: controlUnicode.slice(0, DefaultInputCharLimit), + topLeiField: controlUnicode.slice(0, DefaultInputCharLimit), + topRssdField: '', + }; + const unexpectedValues = { + otherField: controlUnicode, + parentNameField: controlUnicode, + parentLeiField: controlUnicode, + parentRssdField: controlUnicode, + topNameField: controlUnicode, + topLeiField: controlUnicode, + topRssdField: controlUnicode, + }; + + // Reset Form + await test.step('Click: Reset form', async () => { + await page.getByRole('button', { name: 'Reset form' }).click(); + await expect( + page.getByLabel('You must enter a type of'), + 'Other field reset', + ).not.toBeEnabled(); + }); + + await test.step('Click: Other checkbox', async () => { + await page.getByText('Other', { exact: true }).check(); + }); + + await assertTextInput(page, 'You must enter a type of', { + fill: controlUnicode, + expected: expectedValues.otherField, + unexpected: unexpectedValues.otherField, + }); + + await assertTextInput(page, '#parent_legal_name', { + isLocator: true, + fill: controlUnicode, + expected: expectedValues.parentNameField, + unexpected: unexpectedValues.parentNameField, + }); + await assertTextInput(page, '#parent_lei', { + isLocator: true, + fill: controlUnicode, + expected: expectedValues.parentLeiField, + unexpected: unexpectedValues.parentLeiField, + }); + await assertTextInput(page, '#parent_rssd_id', { + isLocator: true, + fill: controlUnicode, + expected: expectedValues.parentRssdField, + unexpected: unexpectedValues.parentRssdField, + }); + await assertTextInput(page, '#top_holder_legal_name', { + isLocator: true, + fill: controlUnicode, + expected: expectedValues.topNameField, + unexpected: unexpectedValues.topNameField, + }); + await assertTextInput(page, '#top_holder_lei', { + isLocator: true, + fill: controlUnicode, + expected: expectedValues.topLeiField, + unexpected: unexpectedValues.topLeiField, + }); + await assertTextInput(page, '#top_holder_rssd_id', { + isLocator: true, + fill: controlUnicode, + expected: expectedValues.topRssdField, + unexpected: unexpectedValues.topRssdField, + }); + }); +}); diff --git a/e2e/utils/inputValidators.ts b/e2e/utils/inputValidators.ts new file mode 100644 index 000000000..2e5b2b0c4 --- /dev/null +++ b/e2e/utils/inputValidators.ts @@ -0,0 +1,73 @@ +import type { ElementHandle, Page } from '@playwright/test'; +import { expect } from '@playwright/test'; + +export const assertTextInput = async ( + page: Page, + label: RegExp | string, + values: { + isLocator?: boolean; + fill: string; + expected: string; + unexpected: string; + }, +) => { + if (values?.isLocator) { + await page.locator(label as string).click(); + // Clear Input + await page.locator(label as string).fill(''); + await expect(page.locator(label as string)).toHaveValue(''); + // Fill Input + await page.locator(label as string).fill(values.fill); + await expect(page.locator(label as string)).not.toHaveValue( + values.unexpected, + ); + await expect(page.locator(label as string)).toHaveValue(values.expected); + } else { + await page.getByLabel(label).click(); + // Clear Input + await page.getByLabel(label).fill(''); + await expect(page.getByLabel(label)).toHaveValue(''); + // Fill Input + await page.getByLabel(label).fill(values.fill); + await expect(page.getByLabel(label)).not.toHaveValue(values.unexpected); + await expect(page.getByLabel(label)).toHaveValue(values.expected); + } +}; + +export const assertSelectInput = async ( + page: Page, + label: RegExp | string, + values: { + isLocator?: boolean; + fill: + | ElementHandle + | string + | readonly { + value?: string; + label?: string; + index?: number; + }[] + | readonly ElementHandle[] + | readonly string[] + | { + value?: string; + label?: string; + index?: number; + } + | null; + expected: string; + unexpected: string; + }, +) => { + if (values?.isLocator) { + await page.locator(label as string).selectOption(values.fill); + await expect(page.locator(label as string)).not.toHaveValue( + values.unexpected, + ); + await expect(page.locator(label as string)).toHaveValue(values.expected); + } else { + await page.getByLabel(label).selectOption(values.fill); + await expect(page.getByLabel(label)).not.toHaveValue(values.unexpected); + await expect(page.getByLabel(label)).toHaveValue(values.expected); + } +}; diff --git a/src/components/InputEntry.tsx b/src/components/InputEntry.tsx index 05d7c93d5..01946a385 100644 --- a/src/components/InputEntry.tsx +++ b/src/components/InputEntry.tsx @@ -1,17 +1,19 @@ /* eslint-disable react/require-default-props */ -import type { PropsWithoutRef, ReactNode } from 'react'; +import type { ComponentPropsWithoutRef, ReactNode } from 'react'; import { forwardRef } from 'react'; import { Element } from 'react-scroll'; import InputErrorMessage from 'components/InputErrorMessage'; import LabelOptional from 'components/LabelOptional'; -import { Heading, TextInput } from 'design-system-react'; +import { Heading } from 'design-system-react'; +import { TextInput } from 'components/TextInput'; import { DisplayField } from 'pages/Filing/ViewInstitutionProfile/DisplayField'; +import type { InputType } from 'design-system-react/dist/components/TextInput/TextInput'; -interface InputEntryProperties - extends PropsWithoutRef { +interface InputEntryProperties extends ComponentPropsWithoutRef<'input'> { id: string; label: JSX.Element | string; + type?: InputType; errorMessage?: string | undefined; isDisabled?: boolean; isLast?: boolean; @@ -27,6 +29,7 @@ const InputEntry = forwardRef( { className = '', id, + name = '', errorMessage, label, isDisabled = false, @@ -64,13 +67,10 @@ const InputEntry = forwardRef( {children} ( + ({ id, name, type = 'text', maxLength, ...properties }, reference) => { + const innerMaxLength = React.useMemo(() => { + if (maxLength && maxLength > 0) { + return maxLength; + } + switch (type) { + case 'url': { + return UrlInputCharLimit; + } + case 'tel': { + return PhoneInputCharLimit; + } + case 'email': { + return EmailInputCharLimit; + } + case 'number': + case 'password': + case 'search': + case 'text': { + return DefaultInputCharLimit; + } + default: { + return DefaultInputCharLimit; + } + } + }, [type, maxLength]); + + return ( + + ); + }, +); + +export default TextInput; diff --git a/src/pages/PointOfContact/index.tsx b/src/pages/PointOfContact/index.tsx index 540f4e98d..b1b3dbbac 100644 --- a/src/pages/PointOfContact/index.tsx +++ b/src/pages/PointOfContact/index.tsx @@ -48,7 +48,7 @@ import type { FinancialInstitutionRS, } from 'types/formTypes'; import { ContactInfoMap, filingDetailsSchema } from 'types/formTypes'; -import { inputCharLimit } from 'utils/constants'; +import { PhoneInputCharLimit, ZipInputCharLimit } from 'utils/constants'; import useAddressStates from 'utils/useAddressStates'; import useFilingStatus from 'utils/useFilingStatus'; import useInstitutionDetails from 'utils/useInstitutionDetails'; @@ -303,7 +303,6 @@ function FilingDetails(): JSX.Element { label='First name' id='firstName' {...register('firstName')} - maxLength={inputCharLimit} errorMessage={formErrors.firstName?.message} showError /> @@ -311,7 +310,6 @@ function FilingDetails(): JSX.Element { label='Last name' id='lastName' {...register('lastName')} - maxLength={inputCharLimit} errorMessage={formErrors.lastName?.message} showError /> @@ -321,7 +319,9 @@ function FilingDetails(): JSX.Element { className='w-full bpMED:flex-[2]' label='Phone number' id='phone' + type='tel' {...register('phone')} + maxLength={PhoneInputCharLimit} helperText='Phone number must be in 555-555-5555 format.' errorMessage={formErrors.phone?.message} showError @@ -334,7 +334,6 @@ function FilingDetails(): JSX.Element { {...register('phoneExtension', { // onChange: handlePhoneExtensionInput, })} - maxLength={inputCharLimit} isOptional errorMessage={formErrors.phoneExtension?.message} showError @@ -344,6 +343,7 @@ function FilingDetails(): JSX.Element { diff --git a/src/pages/ProfileForm/CreateProfileForm/AddFinancialInstitution.tsx b/src/pages/ProfileForm/CreateProfileForm/AddFinancialInstitution.tsx index ce12aeaad..bfe68fa23 100644 --- a/src/pages/ProfileForm/CreateProfileForm/AddFinancialInstitution.tsx +++ b/src/pages/ProfileForm/CreateProfileForm/AddFinancialInstitution.tsx @@ -4,6 +4,7 @@ import { InstitutionHelperText } from 'pages/Filing/formHelpers'; import type { FieldErrors, UseFormRegister } from 'react-hook-form'; import type { ValidationSchemaCPF } from 'types/formTypes'; import { formDelimiter } from 'utils/common'; +import { LeiInputCharLimit } from 'utils/constants'; interface AddFinancialInstitutionProperties { index: number; @@ -37,6 +38,7 @@ function AddFinancialInstitution({ // See `getAllProperties.tsx` for field naming convention id={`financialInstitutions${formDelimiter}${index}${formDelimiter}lei`} {...register(`financialInstitutions.${index}.lei` as const)} + maxLength={LeiInputCharLimit} errorMessage={ formErrors.financialInstitutions?.[`${index}`]?.lei?.message } diff --git a/src/pages/ProfileForm/Step1Form/Step1FormInfoFieldGroup.tsx b/src/pages/ProfileForm/Step1Form/Step1FormInfoFieldGroup.tsx index daa16ca85..c7f78054b 100644 --- a/src/pages/ProfileForm/Step1Form/Step1FormInfoFieldGroup.tsx +++ b/src/pages/ProfileForm/Step1Form/Step1FormInfoFieldGroup.tsx @@ -4,7 +4,6 @@ import InputEntry from 'components/InputEntry'; import { Link } from 'components/Link'; import type { FieldErrors, UseFormRegister } from 'react-hook-form'; import type { BasicInfoSchema, ValidationSchema } from 'types/formTypes'; -import { inputCharLimit } from 'utils/constants'; // TODO: Refactor to take a generic and pass these TS schemas in type FormSchema = BasicInfoSchema | ValidationSchema; @@ -32,7 +31,6 @@ function Step1FormInfoFieldGroup({ label='First name' id='firstName' {...register('firstName')} - maxLength={inputCharLimit} errorMessage={formErrors.firstName?.message} showError /> @@ -40,7 +38,6 @@ function Step1FormInfoFieldGroup({ label='Last name' id='lastName' {...register('lastName')} - maxLength={inputCharLimit} errorMessage={formErrors.lastName?.message} showError /> @@ -48,6 +45,7 @@ function Step1FormInfoFieldGroup({ Date: Thu, 24 Oct 2024 10:11:23 -0500 Subject: [PATCH 08/13] small optimization and bug fix --- src/pages/PointOfContact/index.tsx | 7 ++++++- src/types/formTypes.ts | 12 +++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/pages/PointOfContact/index.tsx b/src/pages/PointOfContact/index.tsx index b1b3dbbac..5e6630f4a 100644 --- a/src/pages/PointOfContact/index.tsx +++ b/src/pages/PointOfContact/index.tsx @@ -123,6 +123,7 @@ function FilingDetails(): JSX.Element { /** Populate form with pre-existing data, when it exists */ useEffect(() => { + let shouldCheck = false; // Checks if the fetched contact info passes validation // @ts-expect-error Part of code cleanup for post-mvp see: https://github.com/cfpb/sbl-frontend/issues/717 const checkPreviousContactInfo = async (): void => { @@ -136,7 +137,7 @@ function FilingDetails(): JSX.Element { if (typeof isVoluntary === 'boolean') { setValue('isVoluntary', isVoluntary); - checkPreviousContactInfo(); + shouldCheck = true; } const contactInfo = (filing as FilingType).contact_info; @@ -149,6 +150,10 @@ function FilingDetails(): JSX.Element { } } // eslint-disable-next-line @typescript-eslint/no-floating-promises + shouldCheck = true; + } + + if (shouldCheck) { checkPreviousContactInfo(); } }, [filing, setValue, trigger]); diff --git a/src/types/formTypes.ts b/src/types/formTypes.ts index 38aa5781e..e67615317 100644 --- a/src/types/formTypes.ts +++ b/src/types/formTypes.ts @@ -273,13 +273,11 @@ export interface FormattedUserProfileObjectType { // Voluntary Reporter Status export const voluntaryReporterStatusSchema = z.object({ - isVoluntary: z - .boolean({ - invalid_type_error: 'You must indicate your voluntary reporter status.', - required_error: 'You must indicate your voluntary reporter status.', - description: 'You must indicate your voluntary reporter status.', - }) - .nullable(), + isVoluntary: z.boolean({ + invalid_type_error: 'You must indicate your voluntary reporter status.', + required_error: 'You must indicate your voluntary reporter status.', + description: 'You must indicate your voluntary reporter status.', + }), }); export type VoluntaryReporterStatusSchema = z.infer< From 70345404e3fc950b0aac3a548d34c9221b9997d9 Mon Sep 17 00:00:00 2001 From: tanner-ricks <182143365+tanner-ricks@users.noreply.github.com> Date: Thu, 24 Oct 2024 11:49:30 -0500 Subject: [PATCH 09/13] Refactors and adding data persistence --- .../requests/submitVoluntaryReporterStatus.ts | 26 ++++ .../PointOfContact/FilingDetailsUtils.ts | 37 +++++ .../VoluntaryReporterStatus.tsx | 5 +- src/pages/PointOfContact/index.tsx | 143 ++++++++++++------ src/pages/ProfileForm/ProfileFormUtils.ts | 22 --- src/types/formTypes.ts | 3 +- .../useSubmitVoluntaryReporterStatus.tsx | 47 ++++++ 7 files changed, 214 insertions(+), 69 deletions(-) create mode 100644 src/api/requests/submitVoluntaryReporterStatus.ts create mode 100644 src/pages/PointOfContact/FilingDetailsUtils.ts create mode 100644 src/utils/useSubmitVoluntaryReporterStatus.tsx diff --git a/src/api/requests/submitVoluntaryReporterStatus.ts b/src/api/requests/submitVoluntaryReporterStatus.ts new file mode 100644 index 000000000..90b287ba5 --- /dev/null +++ b/src/api/requests/submitVoluntaryReporterStatus.ts @@ -0,0 +1,26 @@ +import { filingApiClient, request } from 'api/axiosService'; +import type { SblAuthProperties } from 'api/useSblAuth'; +import type { FormattedVoluntaryReporterStatusSchema } from 'types/formTypes'; + +interface Options { + data: FormattedVoluntaryReporterStatusSchema; + lei: string; + filingPeriod: string; +} + +const submitVoluntaryReporterStatus = async ( + auth: SblAuthProperties, + options: Options, +): Promise => { + const { data, lei, filingPeriod } = options; + + return request({ + axiosInstance: filingApiClient, + url: `/v1/filing/institutions/${lei}/filings/${filingPeriod}/is-voluntary`, + method: 'put', + data, + headers: { Authorization: `Bearer ${auth.user?.access_token}` }, + }); +}; + +export default submitVoluntaryReporterStatus; diff --git a/src/pages/PointOfContact/FilingDetailsUtils.ts b/src/pages/PointOfContact/FilingDetailsUtils.ts new file mode 100644 index 000000000..c449ad0a5 --- /dev/null +++ b/src/pages/PointOfContact/FilingDetailsUtils.ts @@ -0,0 +1,37 @@ +import type { + FilingDetailsSchema, + FormattedPointOfContactSchema, + FormattedVoluntaryReporterStatusSchema, +} from 'types/formTypes'; + +export const formatPointOfContactObject = ( + filingDetailsObject: FilingDetailsSchema, +): FormattedPointOfContactSchema => { + const formattedObject: FormattedPointOfContactSchema = { + // NOTE: 'id' is not necessary + first_name: filingDetailsObject.firstName, + last_name: filingDetailsObject.lastName, + phone_number: filingDetailsObject.phone, + phone_ext: filingDetailsObject.phoneExtension, + email: filingDetailsObject.email, + hq_address_street_1: filingDetailsObject.hq_address_street_1, + hq_address_street_2: filingDetailsObject.hq_address_street_2, + hq_address_street_3: filingDetailsObject.hq_address_street_3, + hq_address_street_4: filingDetailsObject.hq_address_street_4, + hq_address_city: filingDetailsObject.hq_address_city, + hq_address_state: filingDetailsObject.hq_address_state, + hq_address_zip: filingDetailsObject.hq_address_zip, + }; + + return formattedObject; +}; + +export const formatVoluntaryReporterStatusObject = ( + filingDetailsObject: FilingDetailsSchema, +): FormattedVoluntaryReporterStatusSchema => { + const formattedObject: FormattedVoluntaryReporterStatusSchema = { + is_voluntary: filingDetailsObject.isVoluntary, + }; + + return formattedObject; +}; diff --git a/src/pages/PointOfContact/VoluntaryReporterStatus.tsx b/src/pages/PointOfContact/VoluntaryReporterStatus.tsx index 789706f48..09f521a15 100644 --- a/src/pages/PointOfContact/VoluntaryReporterStatus.tsx +++ b/src/pages/PointOfContact/VoluntaryReporterStatus.tsx @@ -25,7 +25,10 @@ function VoluntaryReporterStatus({ return ( - + Pursuant to , select voluntary reporter if your financial institution is voluntarily reporting covered applications from small businesses. diff --git a/src/pages/PointOfContact/index.tsx b/src/pages/PointOfContact/index.tsx index 5e6630f4a..0c458e53e 100644 --- a/src/pages/PointOfContact/index.tsx +++ b/src/pages/PointOfContact/index.tsx @@ -33,10 +33,7 @@ import { LoadingContent } from 'components/Loading'; import FilingNavButtons from 'pages/Filing/FilingApp/FilingNavButtons'; import FilingSteps from 'pages/Filing/FilingApp/FilingSteps'; import InstitutionHeading from 'pages/Filing/FilingApp/InstitutionHeading'; -import { - formatPointOfContactObject, - scrollToElement, -} from 'pages/ProfileForm/ProfileFormUtils'; +import { scrollToElement } from 'pages/ProfileForm/ProfileFormUtils'; import type React from 'react'; import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; @@ -53,13 +50,19 @@ import useAddressStates from 'utils/useAddressStates'; import useFilingStatus from 'utils/useFilingStatus'; import useInstitutionDetails from 'utils/useInstitutionDetails'; import useSubmitPointOfContact from 'utils/useSubmitPointOfContact'; +import useSubmitVoluntaryReporterStatus from 'utils/useSubmitVoluntaryReporterStatus'; import VoluntaryReporterStatus from './VoluntaryReporterStatus'; +import { + formatPointOfContactObject, + formatVoluntaryReporterStatusObject, +} from './FilingDetailsUtils'; const defaultValuesPOC = { isVoluntary: undefined, firstName: '', lastName: '', phone: '', + phoneExtension: '', email: '', hq_address_street_1: '', hq_address_street_2: '', @@ -71,12 +74,19 @@ const defaultValuesPOC = { }; function FilingDetails(): JSX.Element { + const formErrorHeaderId = 'FilingDetailsFormErrors'; + const [previousFilingDetailsValid, setPreviousFilingDetailsValid] = useState(false); + const [scrollTarget, setScrollTarget] = useState(''); + const [isLoading, setIsLoading] = useState(true); + const [isSubmitting, setIsSubmitting] = useState(false); + const queryClient = useQueryClient(); const navigate = useNavigate(); const { lei, year } = useParams(); - const formErrorHeaderId = 'FilingDetailsFormErrors'; + + /** Load Filing Status Data */ const { data: filing, isLoading: isFilingLoading, @@ -84,6 +94,8 @@ function FilingDetails(): JSX.Element { isError: isErrorFilingStatus, // @ts-expect-error Part of code cleanup for post-mvp see: https://github.com/cfpb/sbl-frontend/issues/717 } = useFilingStatus(lei, year); + + /** Load Institution Details for the Filing Institution */ const { data: institution, isLoading: isLoadingInstitution, @@ -92,7 +104,7 @@ function FilingDetails(): JSX.Element { // @ts-expect-error Part of code cleanup for post-mvp see: https://github.com/cfpb/sbl-frontend/issues/717 } = useInstitutionDetails(lei); - // States or Territories -- in options + /** Load States or Territories for Select -- in options */ const { data: stateOptions, isLoading: isLoadingStateOptions, @@ -101,13 +113,24 @@ function FilingDetails(): JSX.Element { isError: isErrorStateOptions, } = useAddressStates(); - const isLoading = [ - isLoadingInstitution, - isFilingLoading, - isLoadingStateOptions, - ].some(Boolean); - const [isSubmitting, setIsSubmitting] = useState(false); + /** Use Voluntary Reporter Status Submission */ + const { mutateAsync: mutateSubmitVoluntaryReporterStatus } = + useSubmitVoluntaryReporterStatus({ + // @ts-expect-error Part of code cleanup for post-mvp see: https://github.com/cfpb/sbl-frontend/issues/717 + lei, + // @ts-expect-error Part of code cleanup for post-mvp see: https://github.com/cfpb/sbl-frontend/issues/717 + filingPeriod: year, + }); + + /** Use Point Of Contact Submission */ + const { mutateAsync: mutateSubmitPointOfContact } = useSubmitPointOfContact({ + // @ts-expect-error Part of code cleanup for post-mvp see: https://github.com/cfpb/sbl-frontend/issues/717 + lei, + // @ts-expect-error Part of code cleanup for post-mvp see: https://github.com/cfpb/sbl-frontend/issues/717 + filingPeriod: year, + }); + /** Use Zod Form */ const { register, watch, @@ -121,6 +144,32 @@ function FilingDetails(): JSX.Element { defaultValues: defaultValuesPOC, }); + /** ******************************************************** */ + /* Use Effects */ + /** ******************************************************** */ + + /** Determine total loading state when individual data loading states change */ + useEffect(() => { + setIsLoading( + isLoadingInstitution || isFilingLoading || isLoadingStateOptions, + ); + }, [ + setIsLoading, + isLoadingInstitution, + isFilingLoading, + isLoadingStateOptions, + ]); + + /** Scrolls to configured target when the scroll target changes and is not empty. + * Then resets the scroll target. This is done this way to debounce a bug where + * the scroll doesn't work correctly when you clear the form and it has errors */ + useEffect(() => { + if (scrollTarget) { + scrollToElement(scrollTarget); + setScrollTarget(''); + } + }, [scrollTarget]); + /** Populate form with pre-existing data, when it exists */ useEffect(() => { let shouldCheck = false; @@ -133,6 +182,7 @@ function FilingDetails(): JSX.Element { if (!filing) return; + // Check is Voluntary const isVoluntary = (filing as FilingType).is_voluntary; if (typeof isVoluntary === 'boolean') { @@ -140,6 +190,7 @@ function FilingDetails(): JSX.Element { shouldCheck = true; } + // check Contact Info const contactInfo = (filing as FilingType).contact_info; if (contactInfo) { @@ -149,34 +200,25 @@ function FilingDetails(): JSX.Element { setValue(mappedProperty, contactInfo[property]); } } - // eslint-disable-next-line @typescript-eslint/no-floating-promises shouldCheck = true; } + // Validate if need to if (shouldCheck) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises checkPreviousContactInfo(); } }, [filing, setValue, trigger]); - const onClearform = (): void => { - reset(); - setValue('hq_address_state', ''); - setValue('isVoluntary', null); - scrollToElement('firstName'); - setPreviousFilingDetailsValid(false); // If success alert is visible, this will disable it - }; - - const onPreviousClick = (): void => - navigate(`/filing/${year}/${lei}/warnings`); + /** ******************************************************** */ + /* Change Handlers */ + /** ******************************************************** */ - const onSelectState = ({ value }: { value: string }): void => { - setValue('hq_address_state', value, { shouldDirty: true }); + /** Handle change to Voluntary Reporter Status */ + const onVoluntaryReporterStatusChange = (selected: boolean): void => { + setValue('isVoluntary', selected, { shouldDirty: true }); }; - // Navigate to Sign and Submit - const navigateSignSubmit = (): void => - navigate(`/filing/${year}/${lei}/submit`); - // Note: Design Choice to be made: ignore non-number input or just rely on error handling // const handlePhoneExtensionInput = ( // event: React.ChangeEvent, @@ -184,12 +226,25 @@ function FilingDetails(): JSX.Element { // setValue('phoneExtension', processNumbersOnlyString(event.target.value)); // }; - const { mutateAsync: mutateSubmitPointOfContact } = useSubmitPointOfContact({ - // @ts-expect-error Part of code cleanup for post-mvp see: https://github.com/cfpb/sbl-frontend/issues/717 - lei, - // @ts-expect-error Part of code cleanup for post-mvp see: https://github.com/cfpb/sbl-frontend/issues/717 - filingPeriod: year, - }); + /** Handle change to State selection */ + const onSelectState = ({ value }: { value: string }): void => { + setValue('hq_address_state', value, { shouldDirty: true }); + }; + + /** ******************************************************** */ + /* Nav Button Click Handlers */ + /** ******************************************************** */ + + const onClearform = (): void => { + reset(); + setPreviousFilingDetailsValid(false); // If success alert is visible, this will disable it + // Set the scroll target to the voluntary reporter containing div whch then triggers the scroll + setScrollTarget('voluntary-reporter-status'); + }; + + const onPreviousClick = (): void => { + navigate(`/filing/${year}/${lei}/warnings`); + }; // NOTE: This function is used for submitting the multipart/formData const onSubmitButtonAction = async ( @@ -199,7 +254,7 @@ function FilingDetails(): JSX.Element { const passesValidation = await trigger(); if (!passesValidation) { - scrollToElement(formErrorHeaderId); + setScrollTarget(formErrorHeaderId); return; } @@ -208,16 +263,18 @@ function FilingDetails(): JSX.Element { try { setIsSubmitting(true); const preFormattedData = getValues(); - const formattedUserProfileObject = - formatPointOfContactObject(preFormattedData); + const formattedPOCObject = formatPointOfContactObject(preFormattedData); + const formattedVRSObject = + formatVoluntaryReporterStatusObject(preFormattedData); - await mutateSubmitPointOfContact({ data: formattedUserProfileObject }); + await mutateSubmitPointOfContact({ data: formattedPOCObject }); + await mutateSubmitVoluntaryReporterStatus({ data: formattedVRSObject }); await queryClient.invalidateQueries({ queryKey: [`fetch-filing-submission`, lei, year], }); - navigateSignSubmit(); + navigate(`/filing/${year}/${lei}/submit`); } catch (error) { // eslint-disable-next-line no-console console.log(error); @@ -225,14 +282,10 @@ function FilingDetails(): JSX.Element { setIsSubmitting(false); } } else { - navigateSignSubmit(); + navigate(`/filing/${year}/${lei}/submit`); } }; - const onVoluntaryReporterStatusChange = (selected: boolean): void => { - setValue('isVoluntary', selected, { shouldDirty: true }); - }; - // TODO: Redirect the user if the filing period or lei are not valid if (isLoading) return ; diff --git a/src/pages/ProfileForm/ProfileFormUtils.ts b/src/pages/ProfileForm/ProfileFormUtils.ts index 3e3411a89..7739a9bc8 100644 --- a/src/pages/ProfileForm/ProfileFormUtils.ts +++ b/src/pages/ProfileForm/ProfileFormUtils.ts @@ -28,28 +28,6 @@ export const formatUserProfileObject = ( return formattedObject; }; -export const formatPointOfContactObject = ( - userProfileObject: PointOfContactSchema, -): FormattedPointOfContactSchema => { - const formattedObject: FormattedPointOfContactSchema = { - // NOTE: 'id' is not necessary - first_name: userProfileObject.firstName, - last_name: userProfileObject.lastName, - phone_number: userProfileObject.phone, - phone_ext: userProfileObject.phoneExtension, - email: userProfileObject.email, - hq_address_street_1: userProfileObject.hq_address_street_1, - hq_address_street_2: userProfileObject.hq_address_street_2, - hq_address_street_3: userProfileObject.hq_address_street_3, - hq_address_street_4: userProfileObject.hq_address_street_4, - hq_address_city: userProfileObject.hq_address_city, - hq_address_state: userProfileObject.hq_address_state, - hq_address_zip: userProfileObject.hq_address_zip, - }; - - return formattedObject; -}; - // Set Checkbox of associated financial institutions to `false`/unchecked export const formatDataCheckedState = ( fiDataInput: InstitutionDetailsApiType[] = [], diff --git a/src/types/formTypes.ts b/src/types/formTypes.ts index e67615317..44a3f2902 100644 --- a/src/types/formTypes.ts +++ b/src/types/formTypes.ts @@ -373,10 +373,11 @@ export const pointOfContactSchema = z.object({ }), phoneExtension: z .string() + .trim() .max(phoneExtensionNumberLimit, { message: PocZodSchemaErrors.phoneExtension, }) - .regex(/^\d+$/, { + .regex(/^\d*$/, { message: PocZodSchemaErrors.phoneExtension, }) .optional(), diff --git a/src/utils/useSubmitVoluntaryReporterStatus.tsx b/src/utils/useSubmitVoluntaryReporterStatus.tsx new file mode 100644 index 000000000..7024ebffc --- /dev/null +++ b/src/utils/useSubmitVoluntaryReporterStatus.tsx @@ -0,0 +1,47 @@ +import type { UseMutationResult } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; +import { getRetries } from 'api/common'; +import submitVoluntaryReporterStatus from 'api/requests/submitVoluntaryReporterStatus'; +import useSblAuth from 'api/useSblAuth'; +import type { AxiosError } from 'axios'; +import type { FilingPeriodType } from 'types/filingTypes'; +import type { + FormattedVoluntaryReporterStatusSchema, + InstitutionDetailsApiType, +} from 'types/formTypes'; +import { UPLOAD_SUBMIT_MAX_RETRIES } from './constants'; + +interface UseSubmitVoluntaryReporterStatusProperties { + lei: InstitutionDetailsApiType['lei']; + filingPeriod: FilingPeriodType; +} + +interface SubmitVoluntaryReporterStatusMutationProperties { + data: FormattedVoluntaryReporterStatusSchema; +} + +const useSubmitVoluntaryReporterStatus = ({ + lei, + filingPeriod, +}: UseSubmitVoluntaryReporterStatusProperties): UseMutationResult< + null, + AxiosError, + SubmitVoluntaryReporterStatusMutationProperties +> => { + const auth = useSblAuth(); + return useMutation< + null, + AxiosError, + SubmitVoluntaryReporterStatusMutationProperties + >({ + mutationFn: async ({ + data, + }: SubmitVoluntaryReporterStatusMutationProperties): Promise => { + return submitVoluntaryReporterStatus(auth, { data, lei, filingPeriod }); + }, + + retry: getRetries(UPLOAD_SUBMIT_MAX_RETRIES), + }); +}; + +export default useSubmitVoluntaryReporterStatus; From 3c49517256eb9f862e86d77d086918ea8a787f60 Mon Sep 17 00:00:00 2001 From: tanner-ricks <182143365+tanner-ricks@users.noreply.github.com> Date: Thu, 24 Oct 2024 11:52:13 -0500 Subject: [PATCH 10/13] small format thing --- src/pages/PointOfContact/index.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/pages/PointOfContact/index.tsx b/src/pages/PointOfContact/index.tsx index 0c458e53e..eb9c74156 100644 --- a/src/pages/PointOfContact/index.tsx +++ b/src/pages/PointOfContact/index.tsx @@ -144,9 +144,9 @@ function FilingDetails(): JSX.Element { defaultValues: defaultValuesPOC, }); - /** ******************************************************** */ - /* Use Effects */ - /** ******************************************************** */ + /* ********************************************************* */ + /* Use Effects */ + /* ********************************************************* */ /** Determine total loading state when individual data loading states change */ useEffect(() => { @@ -210,9 +210,9 @@ function FilingDetails(): JSX.Element { } }, [filing, setValue, trigger]); - /** ******************************************************** */ - /* Change Handlers */ - /** ******************************************************** */ + /* ********************************************************* */ + /* Change Handlers */ + /* ********************************************************* */ /** Handle change to Voluntary Reporter Status */ const onVoluntaryReporterStatusChange = (selected: boolean): void => { @@ -231,9 +231,9 @@ function FilingDetails(): JSX.Element { setValue('hq_address_state', value, { shouldDirty: true }); }; - /** ******************************************************** */ - /* Nav Button Click Handlers */ - /** ******************************************************** */ + /* ********************************************************* */ + /* Nav Button Click Handlers */ + /* ********************************************************* */ const onClearform = (): void => { reset(); From 9c6b7a25b2e389605fa8127598916241307d782a Mon Sep 17 00:00:00 2001 From: tanner-ricks <182143365+tanner-ricks@users.noreply.github.com> Date: Thu, 24 Oct 2024 12:39:54 -0500 Subject: [PATCH 11/13] Updating to new verbiage --- src/pages/PointOfContact/index.tsx | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/pages/PointOfContact/index.tsx b/src/pages/PointOfContact/index.tsx index eb9c74156..e8d80b16c 100644 --- a/src/pages/PointOfContact/index.tsx +++ b/src/pages/PointOfContact/index.tsx @@ -26,7 +26,6 @@ import { VrsFormHeaderErrors, } from 'components/FormErrorHeader.data'; import FormMain from 'components/FormMain'; -import FormParagraph from 'components/FormParagraph'; import InputErrorMessage from 'components/InputErrorMessage'; import { Link } from 'components/Link'; import { LoadingContent } from 'components/Loading'; @@ -304,7 +303,7 @@ function FilingDetails(): JSX.Element { In order to continue to the next step, you are required to @@ -344,19 +343,14 @@ function FilingDetails(): JSX.Element {
Pursuant to , - provide the name and business contact information of a person that - the CFPB or other regulators may contact about the financial - institution's filing. The information you provide will not be - published with your financial institution's data. + provide the name and business contact information of a person who + may be contacted about your financial institution's filing. + This information will not be published with your financial + institution's data.{' '} + View Privacy Notice
- - The Consumer Financial Protection Bureau (CFPB) is collecting data - to test the functionality of the Small Business Lending Data - Filing Platform.{' '} - View Privacy Notice - Date: Thu, 24 Oct 2024 12:49:02 -0500 Subject: [PATCH 12/13] Fixed a test --- e2e/pages/filing-app/formAlerts.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/pages/filing-app/formAlerts.spec.ts b/e2e/pages/filing-app/formAlerts.spec.ts index 1eeb9b9b1..0e1a949c9 100644 --- a/e2e/pages/filing-app/formAlerts.spec.ts +++ b/e2e/pages/filing-app/formAlerts.spec.ts @@ -248,7 +248,7 @@ test('Form Alerts', async ({ page.locator('.m-notification__error'), 'Error alert is visible', ).toContainText( - 'There was a problem updating your filing detailsEnter the first name of the point of contactEnter the last name of the point of contactEnter the phone number of the point of contactEnter a valid phone extensionEnter the email address of the point of contactEnter the street address of the point of contactEnter the city of the point of contactSelect the state or territory of the point of contactEnter the ZIP code of the point of contact', + 'There was a problem updating your filing detailsEnter the first name of the point of contactEnter the last name of the point of contactEnter the phone number of the point of contactEnter the email address of the point of contactEnter the street address of the point of contactEnter the city of the point of contactSelect the state or territory of the point of contactEnter the ZIP code of the point of contact', ); }); From 05b35ca9d73423162d2ee81cb4cf8b25e0adefc3 Mon Sep 17 00:00:00 2001 From: tanner-ricks <182143365+tanner-ricks@users.noreply.github.com> Date: Mon, 28 Oct 2024 12:21:08 -0500 Subject: [PATCH 13/13] updated per PR comment --- src/pages/Filing/FilingApp/FilingSubmit.helpers.tsx | 2 +- src/pages/PointOfContact/VoluntaryReporterStatus.tsx | 5 ++--- src/pages/PointOfContact/index.tsx | 12 +++++++++--- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/pages/Filing/FilingApp/FilingSubmit.helpers.tsx b/src/pages/Filing/FilingApp/FilingSubmit.helpers.tsx index 3277cfe67..adca0e802 100644 --- a/src/pages/Filing/FilingApp/FilingSubmit.helpers.tsx +++ b/src/pages/Filing/FilingApp/FilingSubmit.helpers.tsx @@ -137,7 +137,7 @@ export function VoluntaryReportingStatus({ description?: ReactNode; }): JSX.Element { return ( - + {description} diff --git a/src/pages/PointOfContact/VoluntaryReporterStatus.tsx b/src/pages/PointOfContact/VoluntaryReporterStatus.tsx index 09f521a15..da8a4e0ae 100644 --- a/src/pages/PointOfContact/VoluntaryReporterStatus.tsx +++ b/src/pages/PointOfContact/VoluntaryReporterStatus.tsx @@ -5,7 +5,6 @@ import { RadioButton } from 'design-system-react'; import InputErrorMessage from 'components/InputErrorMessage'; import type { FieldErrors } from 'react-hook-form'; import type { FilingDetailsSchema } from 'types/formTypes'; -import FormSectionWrapper from 'components/FormSectionWrapper'; import RadioButtonGroup from '../../components/RadioButtonGroup'; function VoluntaryReporterStatus({ @@ -24,7 +23,7 @@ function VoluntaryReporterStatus({ }; return ( - + <> -
+ ); } diff --git a/src/pages/PointOfContact/index.tsx b/src/pages/PointOfContact/index.tsx index e8d80b16c..0ace82c26 100644 --- a/src/pages/PointOfContact/index.tsx +++ b/src/pages/PointOfContact/index.tsx @@ -55,6 +55,7 @@ import { formatPointOfContactObject, formatVoluntaryReporterStatusObject, } from './FilingDetailsUtils'; +import FormParagraph from 'components/FormParagraph'; const defaultValuesPOC = { isVoluntary: undefined, @@ -340,17 +341,22 @@ function FilingDetails(): JSX.Element { formErrors={formErrors} onChange={onVoluntaryReporterStatusChange} /> -
+
Pursuant to , provide the name and business contact information of a person who may be contacted about your financial institution's filing. This information will not be published with your financial - institution's data.{' '} - View Privacy Notice + institution's data.
+ + The Consumer Financial Protection Bureau (CFPB) is accepting data + to test the functionality of the Small Business Lending Data + Filing Platform.{' '} + View Privacy Notice +