Skip to content

Commit

Permalink
feat: Adds validation on form submission (#337)
Browse files Browse the repository at this point in the history
* feat: add field error highlight on submission

* feat: Continued iteration in field validation

* feat: complete form field validation

* feat: adds conditional rendering of alert

* feat: update testing context values

* chore: PR fixes
  • Loading branch information
brobro10000 authored May 24, 2023
1 parent 7107b5d commit fc954ad
Show file tree
Hide file tree
Showing 24 changed files with 434 additions and 143 deletions.
1 change: 0 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ config.rules = {
}],
'react-hooks/exhaustive-deps': 'off',
'react/function-component-definition': 'off',

};

module.exports = config;
8 changes: 5 additions & 3 deletions src/Configuration/Provisioning/ProvisioningContext.jsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { createContext } from 'use-context-selector';
import PROVISIONING_PAGE_TEXT from './data/constants';

export const ProvisioningContext = createContext(null);
const ProvisioningContextProvider = ({ children }) => {
const { ALERTS } = PROVISIONING_PAGE_TEXT.FORM;
const contextValue = useState({
customers: [],
multipleFunds: undefined,
customCatalog: false,
alertMessage: ALERTS.unselectedAccountType,
alertMessage: undefined,
catalogQueries: {
data: [],
isLoading: true,
},
formData: {
policies: [],
},
showInvalidField: {
subsidy: [],
policies: [],
},
});

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import { selectProvisioningContext, sortedCatalogQueries } from '../../data/util

const ProvisioningFormCustomCatalogDropdown = () => {
const [selected, setSelected] = useState({ title: '' });
const [catalogQueries] = selectProvisioningContext('catalogQueries');
const { hydrateCatalogQueryData, setCatalogQueryCategory } = useProvisioningContext();
const [catalogQueries, showInvalidField] = selectProvisioningContext('catalogQueries', 'showInvalidField');
const { policies } = showInvalidField;
const isCatalogQueryMetadataDefinedAndFalse = policies[0]?.catalogQueryMetadata === false;
const { hydrateCatalogQueryData, setCatalogQueryCategory, setInvalidPolicyFields } = useProvisioningContext();
const { CUSTOM_CATALOG } = PROVISIONING_PAGE_TEXT.FORM;
const generateAutosuggestOptions = useCallback(() => {
const defaultDropdown = (
Expand Down Expand Up @@ -38,24 +40,37 @@ const ProvisioningFormCustomCatalogDropdown = () => {
catalogQuery: catalogQueries.data.find(({ uuid }) => uuid === valueUuid),
},
}, 0);
setInvalidPolicyFields({ catalogQueryMetadata: true }, 0);
}
setSelected(prevState => ({ selected: { ...prevState.selected, title: value } }));
};

return (
<div className="row">
<div className="col-10">
<Form.Autosuggest
<Form.Group
className="mt-4.5"
floatingLabel={CUSTOM_CATALOG.OPTIONS.enterpriseCatalogQuery.title}
helpMessage={CUSTOM_CATALOG.OPTIONS.enterpriseCatalogQuery.subtitle}
value={selected.title}
onSelected={handleOnSelected}
data-testid="autosuggest"
>
{generateAutosuggestOptions()}
</Form.Autosuggest>
<Form.Autosuggest
floatingLabel={CUSTOM_CATALOG.OPTIONS.enterpriseCatalogQuery.title}
helpMessage={CUSTOM_CATALOG.OPTIONS.enterpriseCatalogQuery.subtitle}
value={selected.title}
onSelected={handleOnSelected}
data-testid="custom-catalog-dropdown-autosuggest"
isInvalid={isCatalogQueryMetadataDefinedAndFalse}
>
{generateAutosuggestOptions()}
</Form.Autosuggest>
{isCatalogQueryMetadataDefinedAndFalse && (
<Form.Control.Feedback
type="invalid"
>
{CUSTOM_CATALOG.OPTIONS.enterpriseCatalogQuery.error}
</Form.Control.Feedback>
)}
</Form.Group>
</div>
{/* TODO: Button should be removed in favor of react-query's refetch functionality */}
<div className="col-2 align-self-center mb-3">
<Button onClick={hydrateCatalogQueryData}>Refresh</Button>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const ProvisioningFormCustomCatalogTitle = () => {
}, [formData.policies[0].catalogQueryMetadata.catalogQuery.title]);
return (
<article className="mt-4.5">
<Form.Group className="mt-4.5 mb-1">
<Form.Group className="mt-3.5 mb-1">
<Form.Control
floatingLabel={CUSTOM_CATALOG.OPTIONS.catalogTitle}
value={catalogQueryTitle}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ const ProvisioningFormDefineCustomCatalogHeader = ({ index }) => {
</Hyperlink>
</ActionRow>
{formData.policies[index].customerCatalog === false && (
<Stack direction="horizontal">
<Icon src={Warning} className="align-self-start" />
<Col className="col-8 pl-2">
{CUSTOM_CATALOG.HEADER.DEFINE.SUB_TITLE}
</Col>
</Stack>
<Stack direction="horizontal">
<Icon src={Warning} className="align-self-start" />
<Col className="col-8 pl-2">
{CUSTOM_CATALOG.HEADER.DEFINE.SUB_TITLE}
</Col>
</Stack>
)}
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe('ProvisioningFormCustomCatalogDropdown', () => {
it('renders the custom catalog dropdown options', async () => {
renderWithRouter(<ProvisioningFormCustomCatalogDropdownWrapper />);

const autoSuggestInput = screen.getByTestId('autosuggest');
const autoSuggestInput = screen.getByTestId('custom-catalog-dropdown-autosuggest');
const autoSuggestButton = screen.getAllByRole('button')[0];
// open dropdown
fireEvent.click(autoSuggestButton);
Expand All @@ -55,7 +55,7 @@ describe('ProvisioningFormCustomCatalogDropdown', () => {
it('setSelected is called when autosuggest option is selected', () => {
renderWithRouter(<ProvisioningFormCustomCatalogDropdownWrapper />);

const autoSuggestInput = screen.getByTestId('autosuggest');
const autoSuggestInput = screen.getByTestId('custom-catalog-dropdown-autosuggest');
const autoSuggestButton = screen.getAllByRole('button')[0];

// open dropdown
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,25 @@ import { extractDefinedCatalogTitle, indexOnlyPropType, selectProvisioningContex
import { isWholeDollarAmount } from '../../../../utils';

const ProvisioningFormAccountDetails = ({ index }) => {
const { ACCOUNT_DETAIL, ALERTS } = PROVISIONING_PAGE_TEXT.FORM;
const { setAccountName, setAccountValue } = useProvisioningContext();
const [multipleFunds, formData] = selectProvisioningContext('multipleFunds', 'formData');
const { ACCOUNT_DETAIL } = PROVISIONING_PAGE_TEXT.FORM;
const { setAccountName, setAccountValue, setInvalidPolicyFields } = useProvisioningContext();
const [multipleFunds, formData, showInvalidField] = selectProvisioningContext('multipleFunds', 'formData', 'showInvalidField');
const { policies } = showInvalidField;
const isAccountNameDefinedAndFalse = policies[index]?.accountName === false;
const isAccountValueDefinedAndFalse = policies[index]?.accountValue === false;
const formFeedbackText = multipleFunds
? ACCOUNT_DETAIL.OPTIONS.totalAccountValue.dynamicSubtitle(extractDefinedCatalogTitle(formData.policies[index]))
: ACCOUNT_DETAIL.OPTIONS.totalAccountValue.subtitle;
const [accountValueState, setAccountValueState] = useState('');
const [accountNameState, setAccountNameState] = useState('');
const [isWholeDollar, setIsWholeDollar] = useState(true);

const handleChange = useCallback((e) => {
const newEvent = e.target;
const { value, dataset } = newEvent;
if (dataset.testid === 'account-name') {
setAccountName({ accountName: value }, index);
setInvalidPolicyFields({ accountName: true }, index);
setAccountNameState(value);
return;
}
Expand All @@ -30,6 +35,7 @@ const ProvisioningFormAccountDetails = ({ index }) => {
}
setIsWholeDollar(true);
setAccountValue({ accountValue: value }, index);
setInvalidPolicyFields({ accountValue: true }, index);
setAccountValueState(value);
}
}, [index, formData]);
Expand All @@ -39,20 +45,33 @@ const ProvisioningFormAccountDetails = ({ index }) => {
<div className="mb-1">
<h3>{ACCOUNT_DETAIL.TITLE}</h3>
</div>
<Form.Group className="mt-4.5 mb-1">
<Form.Group
className="mt-3.5 mb-1"
isInvalid={isAccountNameDefinedAndFalse}
>
<Form.Control
floatingLabel={ACCOUNT_DETAIL.OPTIONS.displayName}
value={accountNameState}
onChange={handleChange}
data-testid="account-name"
/>
{isAccountNameDefinedAndFalse && (
<Form.Control.Feedback
type="invalid"
>
{ACCOUNT_DETAIL.ERROR.emptyField}
</Form.Control.Feedback>
)}
</Form.Group>
<Form.Group className="mt-4.5">
<Form.Group
className="mt-3.5"
>
<Form.Control
floatingLabel={ACCOUNT_DETAIL.OPTIONS.totalAccountValue.title}
value={accountValueState}
onChange={handleChange}
data-testid="account-value"
isInvalid={isAccountValueDefinedAndFalse || !isWholeDollar}
/>
<Form.Control.Feedback>
{formFeedbackText}
Expand All @@ -61,7 +80,14 @@ const ProvisioningFormAccountDetails = ({ index }) => {
<Form.Control.Feedback
type="invalid"
>
{ALERTS.incorrectDollarAmount}
{ACCOUNT_DETAIL.ERROR.incorrectDollarAmount}
</Form.Control.Feedback>
)}
{isAccountValueDefinedAndFalse && (
<Form.Control.Feedback
type="invalid"
>
{ACCOUNT_DETAIL.ERROR.emptyField}
</Form.Control.Feedback>
)}
</Form.Group>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { useState } from 'react';
import {
Form,
Container,
} from '@edx/paragon';
import { v4 as uuidv4 } from 'uuid';
import { useContextSelector } from 'use-context-selector';
Expand All @@ -12,12 +11,14 @@ import { ProvisioningContext } from '../../ProvisioningContext';

// TODO: Replace URL for hyperlink to somewhere to display catalog content information
const ProvisioningFormCatalog = ({ index }) => {
const { setCustomCatalog, setCatalogQueryCategory } = useProvisioningContext();
const { setCustomCatalog, setCatalogQueryCategory, setInvalidPolicyFields } = useProvisioningContext();
const { CATALOG } = PROVISIONING_PAGE_TEXT.FORM;
const contextData = useContextSelector(ProvisioningContext, v => v[0]);
const { multipleFunds, formData } = contextData;
const { multipleFunds, formData, showInvalidField: { policies } } = contextData;
const isCatalogQueryMetadataDefinedAndFalse = policies[index]?.catalogQueryMetadata === false;
const camelCasedQueries = getCamelCasedConfigAttribute('PREDEFINED_CATALOG_QUERIES');
const [value, setValue] = useState(null);
const customCatalogSelected = value === CATALOG.OPTIONS.custom;
if (multipleFunds === undefined) {
return null;
}
Expand All @@ -44,41 +45,50 @@ const ProvisioningFormCatalog = ({ index }) => {
}, index);
}
setValue(newTabValue);
setInvalidPolicyFields({ catalogQueryMetadata: true }, index);
};

return (
<article className="mt-4.5">
<div>
<h3>{CATALOG.TITLE}</h3>
</div>
<p className="mt-4">{CATALOG.SUB_TITLE}</p>
{multipleFunds && (
<h4>
{extractDefinedCatalogTitle(formData.policies[index])}
</h4>
)}
{multipleFunds === false && (
<Container>
<Form.RadioSet
name="display-catalog-content"
onChange={handleChange}
value={value || formData.policies[index].catalogCategory}
>
{
Object.keys(CATALOG.OPTIONS).map((key) => (
<Form.Radio
value={CATALOG.OPTIONS[key]}
type="radio"
key={uuidv4()}
data-testid={CATALOG.OPTIONS[key]}
data-catalogqueryid={camelCasedQueries[key]}
<Form.Group className="mt-3.5">
<Form.Label className="mb-2.5">{CATALOG.SUB_TITLE}</Form.Label>
<Form.RadioSet
name="display-catalog-content"
onChange={handleChange}
value={value || formData.policies[index].catalogCategory}
>
{
Object.keys(CATALOG.OPTIONS).map((key) => (
<Form.Radio
value={CATALOG.OPTIONS[key]}
type="radio"
key={uuidv4()}
data-testid={CATALOG.OPTIONS[key]}
data-catalogqueryid={camelCasedQueries[key]}
isInvalid={customCatalogSelected ? false : isCatalogQueryMetadataDefinedAndFalse}
>
{CATALOG.OPTIONS[key]}
</Form.Radio>
))
}
</Form.RadioSet>
{!customCatalogSelected && isCatalogQueryMetadataDefinedAndFalse && (
<Form.Control.Feedback
type="invalid"
>
{CATALOG.OPTIONS[key]}
</Form.Radio>
))
}
</Form.RadioSet>
</Container>
{CATALOG.ERROR}
</Form.Control.Feedback>
)}
</Form.Group>
)}
</article>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
Container,
Form,
} from '@edx/paragon';
import { v4 as uuidv4 } from 'uuid';
Expand All @@ -9,9 +8,11 @@ import useProvisioningContext from '../../data/hooks';
import { indexOnlyPropType, selectProvisioningContext } from '../../data/utils';

const ProvisioningFormPerLearnerCap = ({ index }) => {
const { perLearnerCap } = useProvisioningContext();
const { perLearnerCap, setInvalidPolicyFields } = useProvisioningContext();
const { LEARNER_CAP } = PROVISIONING_PAGE_TEXT.FORM;
const [formData] = selectProvisioningContext('formData');
const [formData, showInvalidField] = selectProvisioningContext('formData', 'showInvalidField');
const { policies } = showInvalidField;
const isPerLearnerCapDefinedAndFalse = policies[index]?.perLearnerCap === false;
const [value, setValue] = useState(null);

const handleChange = (e) => {
Expand All @@ -26,14 +27,17 @@ const ProvisioningFormPerLearnerCap = ({ index }) => {
}, index);
}
setValue(newTabValue);
setInvalidPolicyFields({ perLearnerCap: true }, index);
};
return (
<article className="mt-4.5">
<div>
<h3>{LEARNER_CAP.TITLE}</h3>
</div>
<p className="mt-4">{LEARNER_CAP.SUB_TITLE}</p>
<Container>
<Form.Group
className="mt-3.5"
>
<Form.Label className="mb-2.5">{LEARNER_CAP.SUB_TITLE}</Form.Label>
<Form.RadioSet
name={`display-per-learner-cap-${index}`}
onChange={handleChange}
Expand All @@ -46,13 +50,21 @@ const ProvisioningFormPerLearnerCap = ({ index }) => {
type="radio"
key={uuidv4()}
data-testid={LEARNER_CAP.OPTIONS[key]}
isInvalid={isPerLearnerCapDefinedAndFalse}
>
{LEARNER_CAP.OPTIONS[key]}
</Form.Radio>
))
}
</Form.RadioSet>
</Container>
{isPerLearnerCapDefinedAndFalse && (
<Form.Control.Feedback
type="invalid"
>
{LEARNER_CAP.ERROR}
</Form.Control.Feedback>
)}
</Form.Group>
</article>
);
};
Expand Down
Loading

0 comments on commit fc954ad

Please sign in to comment.