From 213f2a8c88977da9ff653f66bb06f11443e7e9cb Mon Sep 17 00:00:00 2001 From: nicholashibbits1 <172406954+nicholashibbits1@users.noreply.github.com> Date: Tue, 3 Dec 2024 10:29:07 -0600 Subject: [PATCH] Edm 363 update lce UI (#33304) * Align header for mobile | display result count beside to keyword suggestion * Align cancel icon styles with search input | reduce number of typeahed suggestions dipslayed * touchup styles | change license state if it differs from name selection * slot conditional alert into dropdown label * set state filter field to all when switching to certification category type * filter typeahead suggestions based on filter options * update filter options based on typeahead suggestion selected * rename functions more clearly | correct state managment for filters when clearing input * cleanup * update results to va-card component * enable routing for new lce result info page * align breadcrumbs with result details url * cleanup * delete unit test for unused component * update unit test for helper function --- src/applications/gi/components/Dropdown.jsx | 3 + .../gi/components/GiBillBreadcrumbs.jsx | 11 +- .../gi/components/LcKeywordSearch.jsx | 67 +++++--- .../LicenseCertificationResultInfo.jsx | 72 --------- .../components/LicenseCertificationSearch.jsx | 2 +- .../LicenseCertificationSearchForm.jsx | 147 ++++++++++++++---- .../LicenseCertificationSearchResult.jsx | 32 +--- .../LicenseCertificationSearchResults.jsx | 42 ++--- src/applications/gi/routes.jsx | 9 ++ src/applications/gi/sass/gi.scss | 31 ++-- ...censeCertificationResultInfo.unit.spec.jsx | 30 ---- .../gi/tests/utils/helpers.unit.spec.js | 14 +- src/applications/gi/utils/helpers.js | 2 +- 13 files changed, 240 insertions(+), 222 deletions(-) delete mode 100644 src/applications/gi/components/LicenseCertificationResultInfo.jsx delete mode 100644 src/applications/gi/tests/components/LicenseCertificationResultInfo.unit.spec.jsx diff --git a/src/applications/gi/components/Dropdown.jsx b/src/applications/gi/components/Dropdown.jsx index c7dfae991769..398d6205d730 100644 --- a/src/applications/gi/components/Dropdown.jsx +++ b/src/applications/gi/components/Dropdown.jsx @@ -18,6 +18,7 @@ const Dropdown = ({ value, visible, required, + children, }) => { if (!visible) { return null; @@ -28,6 +29,7 @@ const Dropdown = ({ ); @@ -91,6 +93,7 @@ Dropdown.propTypes = { visible: PropTypes.bool, onFocus: PropTypes.func, required: PropTypes.bool, + children: PropTypes.node, }; Dropdown.defaultProps = { diff --git a/src/applications/gi/components/GiBillBreadcrumbs.jsx b/src/applications/gi/components/GiBillBreadcrumbs.jsx index 2687bff5d3d3..1cca2ce8843d 100644 --- a/src/applications/gi/components/GiBillBreadcrumbs.jsx +++ b/src/applications/gi/components/GiBillBreadcrumbs.jsx @@ -11,6 +11,7 @@ const GiBillBreadcrumbs = () => { const compareMatch = useRouteMatch('/compare'); const lcMatch = useRouteMatch('/lc-search'); const lcResultsMatch = useRouteMatch('/lc-search/results'); + const lcResultInfoMatch = useRouteMatch('/lc-search/:type/:id'); const crumbLiEnding = giDocumentTitle(); const formatedProgramType = formatProgramType( ProgramsTypeMatch?.params?.programType, @@ -55,7 +56,7 @@ const GiBillBreadcrumbs = () => { if (lcMatch) { crumbs.push({ href: '/education/gi-bill-comparison-tool/lc-search', - label: 'Licensces and Certifications', + label: 'Licensces & Certifications', }); } if (lcResultsMatch) { @@ -64,6 +65,14 @@ const GiBillBreadcrumbs = () => { label: 'Search Results', }); } + if (lcResultInfoMatch) { + crumbs.push({ + href: `/education/gi-bill-comparison-tool/lc-search/results/${ + lcResultInfoMatch.params.type + }/${lcResultInfoMatch.params.id}`, + label: 'Result Details', + }); + } return (
diff --git a/src/applications/gi/components/LcKeywordSearch.jsx b/src/applications/gi/components/LcKeywordSearch.jsx index 13c00ed87693..01824c404641 100644 --- a/src/applications/gi/components/LcKeywordSearch.jsx +++ b/src/applications/gi/components/LcKeywordSearch.jsx @@ -9,6 +9,7 @@ export default function LcKeywordSearch({ suggestions, onSelection, onUpdateAutocompleteSearchTerm, + handleClearInput, }) { const handleChange = e => { const { value } = e.target; @@ -16,15 +17,11 @@ export default function LcKeywordSearch({ }; const handleSuggestionSelected = selected => { - onUpdateAutocompleteSearchTerm(selected.label); + const { name, type } = selected; - if (onSelection) { - onSelection(selected); - } - }; + onUpdateAutocompleteSearchTerm(name); - const handleClearInput = () => { - onUpdateAutocompleteSearchTerm(''); + onSelection({ type, state: type === 'license' ? 'FL' : 'all' }); // remove hardcoded state }; return ( @@ -56,6 +53,11 @@ export default function LcKeywordSearch({
)} @@ -83,21 +85,38 @@ export default function LcKeywordSearch({ id="lcKeywordSearch" style={{ maxWidth: '30rem' }} > - {suggestions.map((item, index) => ( -
- {item.name} -
- ))} + {suggestions + .map((item, index) => ( +
+ {index !== 0 ? ( + item.name + ) : ( +
+ + {item.name} + + + {`(${ + suggestions.length > 1 + ? suggestions.length + : 'No' + } results)`} + +
+ )} +
+ )) + .slice(0, 6)}
)}
@@ -108,8 +127,8 @@ export default function LcKeywordSearch({ } LcKeywordSearch.propTypes = { + onSelection: PropTypes.func.isRequired, inputValue: PropTypes.string, suggestions: PropTypes.array, - onSelection: PropTypes.func, onUpdateAutocompleteSearchTerm: PropTypes.func, }; diff --git a/src/applications/gi/components/LicenseCertificationResultInfo.jsx b/src/applications/gi/components/LicenseCertificationResultInfo.jsx deleted file mode 100644 index 14de312c61eb..000000000000 --- a/src/applications/gi/components/LicenseCertificationResultInfo.jsx +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; - -export default function LicenseCertificationResultInfo({ resultInfo }) { - const { institution, tests } = resultInfo; - - return ( - <> -
- - - Test - Fee - - {tests.map((test, i) => { - return ( - - {test.name} - - {test.fee.toLocaleString('en-US', { - currency: 'USD', - maximumFractionDigits: 0, - minimumFractionDigits: 0, - style: 'currency', - })} - - - ); - })} - -
-
- - -

{institution.name}

-
- - -

{institution.phone}

-
-
-
- Physical address and mailing address are the same -

- {institution.physicalStreet} -
- {institution.physicalCity}, {` `} - {institution.physicalState} {` `} - {institution.physicalZip} -
- {institution.physicalCountry} -

-
-
-

- - Print and fill out form Request for Reimbursement of Licensing or - Certification Test Fees - -

- - Link to VA Form 22-0803 - -
- - ); -} diff --git a/src/applications/gi/components/LicenseCertificationSearch.jsx b/src/applications/gi/components/LicenseCertificationSearch.jsx index e6e5bb84a0ca..a5183ebd5c98 100644 --- a/src/applications/gi/components/LicenseCertificationSearch.jsx +++ b/src/applications/gi/components/LicenseCertificationSearch.jsx @@ -27,7 +27,7 @@ function LicenseCertificationSearch({
-

+

Licenses and Certifications

diff --git a/src/applications/gi/components/LicenseCertificationSearchForm.jsx b/src/applications/gi/components/LicenseCertificationSearchForm.jsx index 32b9b465968e..46c5a16318d0 100644 --- a/src/applications/gi/components/LicenseCertificationSearchForm.jsx +++ b/src/applications/gi/components/LicenseCertificationSearchForm.jsx @@ -2,13 +2,17 @@ import React, { useEffect, useState } from 'react'; import ADDRESS_DATA from 'platform/forms/address/data'; import PropTypes from 'prop-types'; import Dropdown from './Dropdown'; -import { updateLcFilterDropdowns } from '../utils/helpers'; +import { handleUpdateLcFilterDropdowns } from '../utils/helpers'; import LcKeywordSearch from './LcKeywordSearch'; function capitalizeFirstLetter(string) { return string.charAt(0).toUpperCase() + string.slice(1); } +const mappedStates = Object.entries(ADDRESS_DATA.states).map(state => { + return { optionValue: state[0], optionLabel: state[1] }; +}); + export const dropdownSchema = [ { label: 'category', @@ -29,29 +33,40 @@ export const dropdownSchema = [ }, { label: 'state', - options: [ - { optionValue: 'all', optionLabel: 'All' }, - ...Object.entries(ADDRESS_DATA.states).map(state => { - return { optionValue: state[0], optionLabel: state[1] }; - }), - ], + options: [{ optionValue: 'all', optionLabel: 'All' }, ...mappedStates], alt: 'state', current: { optionValue: 'all', optionLabel: 'All' }, }, ]; -const filterSuggestions = (suggestions, value) => { - // add filter options as arg +const filterSuggestions = (suggestions, value, filters) => { + const { type } = filters; // destructure state once it's added to the response + if (!value) { return []; } return suggestions.filter(suggestion => { - // TODO add logic to account for filterOptions + if (type !== suggestion.type && type !== 'all') return false; + // TODO add logic to account for state + return suggestion.name.toLowerCase().includes(value.toLowerCase()); }); }; +const resetStateDropdown = dropdowns => { + return dropdowns.map(dropdown => { + return dropdown.label === 'state' + ? { + ...dropdown, + current: dropdown.options.find( + option => option.optionValue === 'all', + ), + } + : dropdown; + }); +}; + export default function LicenseCertificationSearchForm({ suggestions, handleSearch, @@ -59,22 +74,25 @@ export default function LicenseCertificationSearchForm({ const [dropdowns, setDropdowns] = useState(dropdownSchema); const [name, setName] = useState(''); const [filteredSuggestions, setFilteredSuggestions] = useState(suggestions); + const [showStateAlert, setShowStateAlert] = useState(false); useEffect( () => { - const newSuggestions = filterSuggestions(suggestions, name); + const newSuggestions = filterSuggestions(suggestions, name, { + type: dropdowns[0].current.optionValue, + }); if (name.trim() !== '') { newSuggestions.unshift({ name, - link: 'lce/', // verify link - type: 'all', // verify type + link: 'lce/', + type: 'all', }); } setFilteredSuggestions(newSuggestions); }, - [name], + [name, suggestions, dropdowns], ); const handleReset = () => { @@ -83,12 +101,53 @@ export default function LicenseCertificationSearchForm({ }; const handleChange = e => { - const newDropdowns = updateLcFilterDropdowns(dropdowns, e.target); - setDropdowns(newDropdowns); + const newDropdowns = handleUpdateLcFilterDropdowns(dropdowns, e.target); + + if (newDropdowns[0].current.optionValue === 'certification') { + setDropdowns(resetStateDropdown); + } + + setDropdowns(current => { + // update url params + return handleUpdateLcFilterDropdowns(current, e.target); + }); }; const onUpdateAutocompleteSearchTerm = value => { - setName(value); + setName(value); // update url params + }; + + const onSelection = selection => { + const { type, state } = selection; // TODO ensure state is added to response object coming from BE + + const updateDropdowns = dropdowns.map(dropdown => { + if (dropdown.label === 'state') { + return { + ...dropdown, + current: dropdown.options.find(option => { + return option.optionValue === state; + }), + }; + } + + return { + ...dropdown, + current: dropdown.options.find(option => { + return option.optionValue === type; + }), + }; + }); + + if (type === 'license' && dropdowns[1].current.optionValue !== state) { + setShowStateAlert(true); + } + + setDropdowns(updateDropdowns); + }; + + const handleClearInput = () => { + onUpdateAutocompleteSearchTerm(''); + setShowStateAlert(false); }; return ( @@ -105,28 +164,50 @@ export default function LicenseCertificationSearchForm({ selectClassName="lc-dropdown-filter" required={dropdowns[0].label === 'category'} /> + + + {showStateAlert && name.length > 0 ? ( + + The state of {dropdowns[1].current.optionLabel} has been selected + becuase the {name} license is specific to it. + + ) : ( + <> + (Note: Certifications are nationwide. Selecting a state from this + dropdown will only impact licenses and prep courses) + + )} +

- {dropdowns[0].current.optionLabel !== 'Prep Course' && ( - - )}
@@ -143,6 +224,6 @@ export default function LicenseCertificationSearchForm({ } LicenseCertificationSearchForm.propTypes = { - suggestions: PropTypes.array, handleSearch: PropTypes.func.isRequired, + suggestions: PropTypes.array, }; diff --git a/src/applications/gi/containers/LicenseCertificationSearchResult.jsx b/src/applications/gi/containers/LicenseCertificationSearchResult.jsx index db83b52f2c72..05740b8cfaf5 100644 --- a/src/applications/gi/containers/LicenseCertificationSearchResult.jsx +++ b/src/applications/gi/containers/LicenseCertificationSearchResult.jsx @@ -1,34 +1,16 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import LicenseCertificationResultInfo from '../components/LicenseCertificationResultInfo'; import { fetchLcResult } from '../actions'; -function LicenseCertificationSearchResult({ - result, - hasFetchedResult, - dispatchFetchLcResult, - resultInfo, -}) { - const { link, type, name } = result; - - const handleClick = () => { - dispatchFetchLcResult(link); - }; - +function LicenseCertificationSearchResult() { return ( - handleClick()} - > - {hasFetchedResult ? ( - - ) : ( -

Loading

- )} -
+
+
+

Name

+
Tab view for results
+
+
); } diff --git a/src/applications/gi/containers/LicenseCertificationSearchResults.jsx b/src/applications/gi/containers/LicenseCertificationSearchResults.jsx index 3c47095c10a2..3459fc7cba64 100644 --- a/src/applications/gi/containers/LicenseCertificationSearchResults.jsx +++ b/src/applications/gi/containers/LicenseCertificationSearchResults.jsx @@ -3,7 +3,6 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { VaPagination } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; import { useLocation } from 'react-router-dom'; -import LicenseCertificationSearchResult from './LicenseCertificationSearchResult'; import { fetchLicenseCertificationResults } from '../actions'; function LicenseCertificationSearchResults({ @@ -16,21 +15,21 @@ function LicenseCertificationSearchResults({ const location = useLocation(); const searchParams = new URLSearchParams(location.search); const name = searchParams.get('name'); - const type = searchParams.get('type'); + // const type = searchParams.get('type'); const itemsPerPage = 5; const totalPages = Math.ceil(lcResults.length / itemsPerPage); - const currentResults = lcResults.slice( - (currentPage - 1) * itemsPerPage, - currentPage * itemsPerPage, - ); + // const currentResults = lcResults.slice( + // (currentPage - 1) * itemsPerPage, + // currentPage * itemsPerPage, + // ); useEffect( () => { - dispatchFetchLicenseCertificationResults(name, type); + dispatchFetchLicenseCertificationResults(); }, - [dispatchFetchLicenseCertificationResults, name, type], + [dispatchFetchLicenseCertificationResults], ); const handlePageChange = page => { @@ -60,16 +59,23 @@ function LicenseCertificationSearchResults({

- - {currentResults.map((result, index) => { - return ( - - ); - })} - + {lcResults.map((result, index) => { + return ( +
+ +

{result.name}

+

+ {result.type} +

+ +
+
+ ); + })}
{ @@ -25,15 +26,23 @@ export const buildRoutes = () => { render={({ match }) => } /> } /> ( )} /> + ( + + )} + /> } diff --git a/src/applications/gi/sass/gi.scss b/src/applications/gi/sass/gi.scss index acabc8cc7810..b3c6d9453f2f 100644 --- a/src/applications/gi/sass/gi.scss +++ b/src/applications/gi/sass/gi.scss @@ -296,15 +296,6 @@ } } -.lc-dropdown-filter { - max-width: 30rem; -} - -.lc-filter-options { - padding: 0.5rem; - margin: 0; -} - .provider-info-container > span { gap: 1rem; } @@ -318,12 +309,32 @@ padding-left: 0; } -.lc-name-search-input { +.lc-dropdown-filter { max-width: 30rem; } + +.lc-clear { + border: 1px solid $color-gray-dark; + border-left: none; +} + +.lc-card-subheader { + text-transform: capitalize; +} + +.license-alert { + max-width: 30rem; +} + +.reset-search { + background-color: $color-white; + color: $color-primary; +} + .inst-remove-btn::part(button) { background-color: transparent; } + .search-container { va-text-input::part(input) { width: 100%; diff --git a/src/applications/gi/tests/components/LicenseCertificationResultInfo.unit.spec.jsx b/src/applications/gi/tests/components/LicenseCertificationResultInfo.unit.spec.jsx deleted file mode 100644 index d32bd93bac4b..000000000000 --- a/src/applications/gi/tests/components/LicenseCertificationResultInfo.unit.spec.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import { expect } from 'chai'; -import { shallow } from 'enzyme'; -import LicenseCertificationResultInfo from '../../components/LicenseCertificationResultInfo'; - -describe('', () => { - it('should render without crashing', () => { - const resultInfo = { - institution: { - name: 'Sample Institution', - phone: '123-456-7890', - physicalStreet: '123 Main St', - physicalCity: 'Sample City', - physicalState: 'CA', - physicalZip: '90210', - physicalCountry: 'USA', - }, - tests: [ - { name: 'Sample Test 1', fee: 200 }, - { name: 'Sample Test 2', fee: 300 }, - ], - }; - - const wrapper = shallow( - , - ); - expect(wrapper.exists()).to.be.ok; - wrapper.unmount(); - }); -}); diff --git a/src/applications/gi/tests/utils/helpers.unit.spec.js b/src/applications/gi/tests/utils/helpers.unit.spec.js index e12e10f20127..3f471ac7e4a1 100644 --- a/src/applications/gi/tests/utils/helpers.unit.spec.js +++ b/src/applications/gi/tests/utils/helpers.unit.spec.js @@ -27,7 +27,7 @@ import { formatProgramType, isReviewInstance, isSmallScreenLogic, - updateLcFilterDropdowns, + handleUpdateLcFilterDropdowns, } from '../../utils/helpers'; describe('GIBCT helpers:', () => { @@ -567,13 +567,13 @@ describe('GIBCT helpers:', () => { }); }); - describe('updateLcFilterDropdowns', () => { + describe('handleUpdateLcFilterDropdowns', () => { it('should update the correct dropdown with the selected option based on the target id and value', () => { const dropdowns = [ { label: 'category', options: [ - { optionValue: '', optionLabel: '-Select-' }, + { optionValue: 'all', optionLabel: 'All' }, { optionValue: 'licenses', optionLabel: 'License' }, { optionValue: 'certifications', optionLabel: 'Certification' }, { optionValue: 'preps', optionLabel: 'Prep Course' }, @@ -584,7 +584,7 @@ describe('GIBCT helpers:', () => { { label: 'state', options: [ - { optionValue: 'All', optionLabel: 'All' }, + { optionValue: 'all', optionLabel: 'All' }, { optionValue: 'CA', optionLabel: 'California' }, { optionValue: 'TX', optionLabel: 'Texas' }, ], @@ -603,7 +603,7 @@ describe('GIBCT helpers:', () => { { label: 'category', options: [ - { optionValue: '', optionLabel: '-Select-' }, + { optionValue: 'all', optionLabel: 'All' }, { optionValue: 'licenses', optionLabel: 'License' }, { optionValue: 'certifications', optionLabel: 'Certification' }, { optionValue: 'preps', optionLabel: 'Prep Course' }, @@ -614,7 +614,7 @@ describe('GIBCT helpers:', () => { { label: 'state', options: [ - { optionValue: 'All', optionLabel: 'All' }, + { optionValue: 'all', optionLabel: 'All' }, { optionValue: 'CA', optionLabel: 'California' }, { optionValue: 'TX', optionLabel: 'Texas' }, ], @@ -623,7 +623,7 @@ describe('GIBCT helpers:', () => { }, ]; - const result = updateLcFilterDropdowns(dropdowns, target); + const result = handleUpdateLcFilterDropdowns(dropdowns, target); expect(result).to.deep.equal(expectedResult); }); diff --git a/src/applications/gi/utils/helpers.js b/src/applications/gi/utils/helpers.js index 4acd6b2354e2..f673472e9a87 100644 --- a/src/applications/gi/utils/helpers.js +++ b/src/applications/gi/utils/helpers.js @@ -528,7 +528,7 @@ export const getGIBillHeaderText = (automatedTest = false) => { : 'Learn about and compare your GI Bill benefits at approved schools and employers.'; }; -export const updateLcFilterDropdowns = (dropdowns, target) => { +export const handleUpdateLcFilterDropdowns = (dropdowns, target) => { const updatedFieldIndex = dropdowns.findIndex(dropdown => { return dropdown.label === target.id; });