From 43075f4034dfcec3ca735820a0e2d0d9b551965e Mon Sep 17 00:00:00 2001 From: William Phelps Date: Fri, 15 Nov 2024 08:02:45 -0800 Subject: [PATCH 1/4] Create new-conditions prototype form --- .../user-testing/new-conditions/app-entry.jsx | 15 + .../components/Autocomplete.jsx | 221 +++++++++ .../new-conditions/config/form.js | 78 ++++ .../user-testing/new-conditions/constants.js | 2 + .../new-conditions/containers/App.jsx | 12 + .../containers/ConfirmationPage.jsx | 100 ++++ .../containers/IntroductionPage.jsx | 55 +++ .../content/conditionOptions.js | 440 ++++++++++++++++++ .../new-conditions/content/newConditions.jsx | 36 ++ .../user-testing/new-conditions/manifest.json | 7 + .../new-conditions/pages/chooseDemo.js | 28 ++ .../condition.js | 105 +++++ .../newConditionsJustConditionsPages/date.js | 33 ++ .../newConditionsJustConditionsPages/index.js | 79 ++++ .../newConditionsJustConditionsPages/intro.js | 17 + .../sideOfBody.js | 39 ++ .../summary.js | 33 ++ .../newConditionsJustConditionsPages/utils.js | 56 +++ .../pages/newConditionsPages/cause.js | 48 ++ .../pages/newConditionsPages/causeFollowUp.js | 158 +++++++ .../pages/newConditionsPages/condition.js | 105 +++++ .../pages/newConditionsPages/date.js | 33 ++ .../pages/newConditionsPages/index.js | 91 ++++ .../pages/newConditionsPages/intro.js | 17 + .../pages/newConditionsPages/sideOfBody.js | 39 ++ .../pages/newConditionsPages/summary.js | 33 ++ .../pages/newConditionsPages/utils.js | 79 ++++ .../new-conditions/reducers/index.js | 6 + .../user-testing/new-conditions/routes.jsx | 12 + .../new-conditions/sass/new-conditions.scss | 52 +++ .../containers/ConfirmationPage.unit.spec.jsx | 45 ++ .../containers/IntroductionPage.unit.spec.jsx | 68 +++ .../tests/fixtures/data/minimal-test.json | 9 + .../fixtures/mocks/local-mock-responses.js | 8 + .../tests/fixtures/mocks/user.json | 56 +++ .../tests/new-conditions.cypress.spec.js | 37 ++ src/platform/forms/constants.js | 8 + 37 files changed, 2260 insertions(+) create mode 100644 src/applications/user-testing/new-conditions/app-entry.jsx create mode 100644 src/applications/user-testing/new-conditions/components/Autocomplete.jsx create mode 100644 src/applications/user-testing/new-conditions/config/form.js create mode 100644 src/applications/user-testing/new-conditions/constants.js create mode 100644 src/applications/user-testing/new-conditions/containers/App.jsx create mode 100644 src/applications/user-testing/new-conditions/containers/ConfirmationPage.jsx create mode 100644 src/applications/user-testing/new-conditions/containers/IntroductionPage.jsx create mode 100644 src/applications/user-testing/new-conditions/content/conditionOptions.js create mode 100644 src/applications/user-testing/new-conditions/content/newConditions.jsx create mode 100644 src/applications/user-testing/new-conditions/manifest.json create mode 100644 src/applications/user-testing/new-conditions/pages/chooseDemo.js create mode 100644 src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/condition.js create mode 100644 src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/date.js create mode 100644 src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/index.js create mode 100644 src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/intro.js create mode 100644 src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/sideOfBody.js create mode 100644 src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/summary.js create mode 100644 src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/utils.js create mode 100644 src/applications/user-testing/new-conditions/pages/newConditionsPages/cause.js create mode 100644 src/applications/user-testing/new-conditions/pages/newConditionsPages/causeFollowUp.js create mode 100644 src/applications/user-testing/new-conditions/pages/newConditionsPages/condition.js create mode 100644 src/applications/user-testing/new-conditions/pages/newConditionsPages/date.js create mode 100644 src/applications/user-testing/new-conditions/pages/newConditionsPages/index.js create mode 100644 src/applications/user-testing/new-conditions/pages/newConditionsPages/intro.js create mode 100644 src/applications/user-testing/new-conditions/pages/newConditionsPages/sideOfBody.js create mode 100644 src/applications/user-testing/new-conditions/pages/newConditionsPages/summary.js create mode 100644 src/applications/user-testing/new-conditions/pages/newConditionsPages/utils.js create mode 100644 src/applications/user-testing/new-conditions/reducers/index.js create mode 100644 src/applications/user-testing/new-conditions/routes.jsx create mode 100644 src/applications/user-testing/new-conditions/sass/new-conditions.scss create mode 100644 src/applications/user-testing/new-conditions/tests/containers/ConfirmationPage.unit.spec.jsx create mode 100644 src/applications/user-testing/new-conditions/tests/containers/IntroductionPage.unit.spec.jsx create mode 100644 src/applications/user-testing/new-conditions/tests/fixtures/data/minimal-test.json create mode 100644 src/applications/user-testing/new-conditions/tests/fixtures/mocks/local-mock-responses.js create mode 100644 src/applications/user-testing/new-conditions/tests/fixtures/mocks/user.json create mode 100644 src/applications/user-testing/new-conditions/tests/new-conditions.cypress.spec.js diff --git a/src/applications/user-testing/new-conditions/app-entry.jsx b/src/applications/user-testing/new-conditions/app-entry.jsx new file mode 100644 index 000000000000..245e2a7d7155 --- /dev/null +++ b/src/applications/user-testing/new-conditions/app-entry.jsx @@ -0,0 +1,15 @@ +import '@department-of-veterans-affairs/platform-polyfills'; +import './sass/new-conditions.scss'; + +import { startAppFromIndex } from '@department-of-veterans-affairs/platform-startup/exports'; + +import routes from './routes'; +import reducer from './reducers'; +import manifest from './manifest.json'; + +startAppFromIndex({ + entryName: manifest.entryName, + url: manifest.rootUrl, + reducer, + routes, +}); diff --git a/src/applications/user-testing/new-conditions/components/Autocomplete.jsx b/src/applications/user-testing/new-conditions/components/Autocomplete.jsx new file mode 100644 index 000000000000..9f085ab5ad18 --- /dev/null +++ b/src/applications/user-testing/new-conditions/components/Autocomplete.jsx @@ -0,0 +1,221 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import PropTypes from 'prop-types'; +import { VaTextInput } from '@department-of-veterans-affairs/component-library/dist/react-bindings'; +import debounce from 'lodash/debounce'; +import { fullStringSimilaritySearch } from 'platform/forms-system/src/js/utilities/addDisabilitiesStringSearch'; + +const INSTRUCTIONS = + 'When autocomplete results are available use up and down arrows to review and enter to select. Touch device users, explore by touch or with swipe gestures.'; + +const createFreeTextItem = val => `Enter your condition as "${val}"`; + +const Autocomplete = ({ + availableResults, + debounceDelay, + formData, + id, + label, + onChange, +}) => { + const [value, setValue] = useState(formData); + const [results, setResults] = useState([]); + const [activeIndex, setActiveIndex] = useState(null); + const [ariaLiveText, setAriaLiveText] = useState(''); + + const containerRef = useRef(null); + const inputRef = useRef(null); + const resultsRef = useRef([]); + + // Delays screen reader result count reading to avoid interruption by input content reading + const debouncedSetAriaLiveText = useRef( + debounce((resultCount, freeTextResult) => { + const makePlural = resultCount > 1 ? 's' : ''; + + setAriaLiveText( + `${resultCount} result${makePlural}. ${freeTextResult}, (1 of ${resultCount})`, + ); + }, 700), + ).current; + + const debouncedSearch = useRef( + debounce(async inputValue => { + const freeTextResult = createFreeTextItem(inputValue); + const searchResults = fullStringSimilaritySearch( + inputValue, + availableResults, + ); + const updatedResults = [freeTextResult, ...searchResults]; + setResults(updatedResults); + setActiveIndex(0); + + debouncedSetAriaLiveText(updatedResults.length, freeTextResult); + }, debounceDelay), + ).current; + + const closeList = useCallback( + () => { + debouncedSearch.cancel(); + debouncedSetAriaLiveText.cancel(); + setResults([]); + setActiveIndex(null); + }, + [debouncedSearch, debouncedSetAriaLiveText], + ); + + const handleInputChange = inputValue => { + setValue(inputValue); + onChange(inputValue); + + if (!inputValue) { + closeList(); + setAriaLiveText('Input is empty. Please enter a condition.'); + return; + } + + debouncedSearch(inputValue); + }; + + const activateScrollToAndFocus = index => { + setActiveIndex(index); + + const activeResult = resultsRef.current[index]; + activeResult?.scrollIntoView({ + block: 'nearest', + }); + activeResult?.focus(); + }; + + const focusOnInput = () => + inputRef.current.shadowRoot.querySelector('input').focus(); + + const navigateList = (e, adjustment) => { + e.preventDefault(); + const newIndex = activeIndex + adjustment; + if (newIndex > results.length - 1) { + return; + } + + if (newIndex < 1) { + activateScrollToAndFocus(0); + + focusOnInput(); + } else { + activateScrollToAndFocus(newIndex); + } + }; + + const selectResult = result => { + const newValue = result === createFreeTextItem(value) ? value : result; + setValue(newValue); + onChange(newValue); + setAriaLiveText(`${newValue} is selected`); + closeList(); + + focusOnInput(); + }; + + const handleKeyDown = e => { + if (results.length > 0) { + if (e.key === 'ArrowDown') { + navigateList(e, 1); + } else if (e.key === 'ArrowUp') { + navigateList(e, -1); + } else if (e.key === 'Enter') { + selectResult(results[activeIndex]); + } else if (e.key === 'Escape') { + closeList(); + focusOnInput(); + } else if (e.key === 'Tab') { + closeList(); + } else { + focusOnInput(); + } + } + }; + + useEffect( + () => { + const handleClickOutside = event => { + if ( + containerRef.current && + !containerRef.current.contains(event.target) + ) { + closeList(); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, + [closeList], + ); + + const handleFocus = () => { + if (value && results.length === 0) { + debouncedSearch(value); + } + }; + + return ( +
+ handleInputChange(e.target.value)} + onKeyDown={handleKeyDown} + /> + {results.length > 0 && ( +
    + {results.map((result, index) => ( +
  • { + resultsRef.current[index] = el; + }} + role="option" + tabIndex={-1} + onClick={() => selectResult(result)} + onKeyDown={handleKeyDown} // Keydown is handled on the input; this is never fired and prevents eslint error + onMouseEnter={() => activateScrollToAndFocus(index)} + > + {result} +
  • + ))} +
+ )} +

+ {ariaLiveText} +

+
+ ); +}; + +Autocomplete.propTypes = { + availableResults: PropTypes.array, + debounceDelay: PropTypes.number, + formData: PropTypes.string, + id: PropTypes.string, + label: PropTypes.string, + onChange: PropTypes.func, +}; + +export default Autocomplete; diff --git a/src/applications/user-testing/new-conditions/config/form.js b/src/applications/user-testing/new-conditions/config/form.js new file mode 100644 index 000000000000..3d7d151e30b6 --- /dev/null +++ b/src/applications/user-testing/new-conditions/config/form.js @@ -0,0 +1,78 @@ +import { VA_FORM_IDS } from 'platform/forms/constants'; +import CallVBACenter from 'platform/static-data/CallVBACenter'; +import React from 'react'; +import { SUBTITLE, TITLE } from '../constants'; +import ConfirmationPage from '../containers/ConfirmationPage'; +import IntroductionPage from '../containers/IntroductionPage'; +import manifest from '../manifest.json'; + +import chooseDemo from '../pages/chooseDemo'; +import newConditionsJustConditionsPages from '../pages/newConditionsJustConditionsPages'; +import newConditionsPages from '../pages/newConditionsPages'; + +const FormFooter = () => ( +
+
+ +
+
+

+ For help filling out this form, or if the form isn’t working + right, please +

+
+
+
+
+
+); + +/** @type {FormConfig} */ +const formConfig = { + rootUrl: manifest.rootUrl, + urlPrefix: '/', + submitUrl: '/v0/api', + submit: () => + Promise.resolve({ attributes: { confirmationNumber: '123123123' } }), + trackingPrefix: 'new-conditions', + introduction: IntroductionPage, + confirmation: ConfirmationPage, + footerContent: FormFooter, + // dev: { + // showNavLinks: true, + // collapsibleNavLinks: true, + // }, + formId: VA_FORM_IDS.FORM_21_526EZ, + saveInProgress: { + messages: { + inProgress: + 'Your disability compensation application (21-526EZ) is in progress.', + expired: + 'Your saved disability compensation application (21-526EZ) has expired. If you want to apply for disability compensation, please start a new application.', + saved: 'Your disability compensation application has been saved.', + }, + }, + version: 0, + prefillEnabled: true, + savedFormMessages: { + notFound: 'Please start over to apply for disability compensation.', + noAuth: + 'Please sign in again to continue your application for disability compensation.', + }, + title: TITLE, + subTitle: SUBTITLE, + v3SegmentedProgressBar: true, + defaultDefinitions: {}, + chapters: { + newConditionsChapter: { + title: 'Conditions', + pages: { + chooseDemo, + ...newConditionsPages, + ...newConditionsJustConditionsPages, + }, + }, + }, +}; + +export default formConfig; diff --git a/src/applications/user-testing/new-conditions/constants.js b/src/applications/user-testing/new-conditions/constants.js new file mode 100644 index 000000000000..97bc678a1a49 --- /dev/null +++ b/src/applications/user-testing/new-conditions/constants.js @@ -0,0 +1,2 @@ +export const TITLE = 'File for disability compensation with VA Form 21-526EZ'; +export const SUBTITLE = 'VA Form 21-526EZ'; diff --git a/src/applications/user-testing/new-conditions/containers/App.jsx b/src/applications/user-testing/new-conditions/containers/App.jsx new file mode 100644 index 000000000000..797306ee1e20 --- /dev/null +++ b/src/applications/user-testing/new-conditions/containers/App.jsx @@ -0,0 +1,12 @@ +import React from 'react'; + +import RoutedSavableApp from 'platform/forms/save-in-progress/RoutedSavableApp'; +import formConfig from '../config/form'; + +export default function App({ location, children }) { + return ( + + {children} + + ); +} diff --git a/src/applications/user-testing/new-conditions/containers/ConfirmationPage.jsx b/src/applications/user-testing/new-conditions/containers/ConfirmationPage.jsx new file mode 100644 index 000000000000..cd485448d364 --- /dev/null +++ b/src/applications/user-testing/new-conditions/containers/ConfirmationPage.jsx @@ -0,0 +1,100 @@ +import React, { useEffect, useRef } from 'react'; +import PropTypes from 'prop-types'; +import { format, isValid } from 'date-fns'; +import { useSelector } from 'react-redux'; +import { scrollTo, waitForRenderThenFocus } from 'platform/utilities/ui'; + +export const ConfirmationPage = () => { + const alertRef = useRef(null); + const form = useSelector(state => state.form || {}); + const { submission, formId, data = {} } = form; + const { fullName } = data; + const submitDate = submission?.timestamp; + const confirmationNumber = submission?.response?.confirmationNumber; + + useEffect( + () => { + if (alertRef?.current) { + scrollTo('topScrollElement'); + waitForRenderThenFocus('h2', alertRef.current); + } + }, + [alertRef], + ); + + return ( +
+
+ VA logo +
+ + +

Your application has been submitted

+
+ +

We may contact you for more information or documents.

+

Please print this page for your records.

+
+

+ File for disability compensation Claim{' '} + (Form {formId}) +

+ {fullName ? ( + + for {fullName.first} {fullName.middle} {fullName.last} + {fullName.suffix ? `, ${fullName.suffix}` : null} + + ) : null} + + {confirmationNumber ? ( + <> +

Confirmation number

+

{confirmationNumber}

+ + ) : null} + + {isValid(submitDate) ? ( +

+ Date submitted +
+ {format(submitDate, 'MMMM d, yyyy')} +

+ ) : null} + + +
+ + Go back to VA.gov + + + {/*
+

Need help?

+ +
*/} +
+ ); +}; + +ConfirmationPage.propTypes = { + form: PropTypes.shape({ + data: PropTypes.shape({ + fullName: { + first: PropTypes.string, + middle: PropTypes.string, + last: PropTypes.string, + suffix: PropTypes.string, + }, + }), + formId: PropTypes.string, + submission: PropTypes.shape({ + timestamp: PropTypes.string, + }), + }), + name: PropTypes.string, +}; + +export default ConfirmationPage; diff --git a/src/applications/user-testing/new-conditions/containers/IntroductionPage.jsx b/src/applications/user-testing/new-conditions/containers/IntroductionPage.jsx new file mode 100644 index 000000000000..0c4fa9deb1b4 --- /dev/null +++ b/src/applications/user-testing/new-conditions/containers/IntroductionPage.jsx @@ -0,0 +1,55 @@ +import React, { useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { focusElement, scrollToTop } from 'platform/utilities/ui'; +import FormTitle from 'platform/forms-system/src/js/components/FormTitle'; +import SaveInProgressIntro from 'platform/forms/save-in-progress/SaveInProgressIntro'; + +import { TITLE, SUBTITLE } from '../constants'; + +const OMB_RES_BURDEN = 25; +const OMB_NUMBER = '2900-0747'; +const OMB_EXP_DATE = '11/30/2025'; + +export const IntroductionPage = props => { + const { route } = props; + const { formConfig, pageList } = route; + + useEffect(() => { + scrollToTop(); + focusElement('h1'); + }, []); + + return ( +
+ + +

+ +

+ ); +}; + +IntroductionPage.propTypes = { + route: PropTypes.shape({ + formConfig: PropTypes.shape({ + prefillEnabled: PropTypes.bool.isRequired, + savedFormMessages: PropTypes.object.isRequired, + }).isRequired, + pageList: PropTypes.arrayOf(PropTypes.object).isRequired, + }).isRequired, + location: PropTypes.shape({ + basename: PropTypes.string, + }), +}; + +export default IntroductionPage; diff --git a/src/applications/user-testing/new-conditions/content/conditionOptions.js b/src/applications/user-testing/new-conditions/content/conditionOptions.js new file mode 100644 index 000000000000..83656a964e81 --- /dev/null +++ b/src/applications/user-testing/new-conditions/content/conditionOptions.js @@ -0,0 +1,440 @@ +export const conditionObjects = [ + { option: 'ACL tear (anterior cruciate ligament tear)', sideOfBody: true }, + { option: 'acne' }, + { option: 'adjustment disorder' }, + { option: 'agoraphobia' }, + { option: 'alopecia' }, + { option: 'ALS (amyotrophic lateral sclerosis)' }, + { option: 'anal cancer' }, + { option: 'anal or perianal fistula (fistula-in-ano)' }, + { option: 'ankle replacement (ankle arthroplasty)', sideOfBody: true }, + { option: 'ankle sprain', sideOfBody: true }, + { option: 'ankylosis in ankle', sideOfBody: true }, + { option: 'ankylosis in elbow', sideOfBody: true }, + { option: 'ankylosis in hand or fingers', sideOfBody: true }, + { option: 'ankylosis in hip', sideOfBody: true }, + { option: 'ankylosis in knee', sideOfBody: true }, + { option: 'ankylosis in shoulder', sideOfBody: true }, + { option: 'ankylosis in wrist', sideOfBody: true }, + { option: 'anorexia (type of eating disorder)' }, + { option: 'aortic insufficiency (aortic regurgitation)' }, + { option: 'aortic valve disease' }, + { option: 'arrhythmia (irregular heartbeat)' }, + { option: 'asthma' }, + { + option: 'astragalectomy or talectomy (removal of talus bone in ankle)', + sideOfBody: true, + }, + { option: "athlete's foot (tinea pedis)" }, + { option: 'atrial fibrillation (A-fib)' }, + { option: 'basal cell carcinoma of the skin, extremities or trunk' }, + { option: 'basal cell carcinoma of the skin, head or neck' }, + { option: 'bipolar disorder' }, + { option: 'biventricular hypertrophy' }, + { option: 'bladder cancer' }, + { option: 'bladder stones' }, + { option: 'bone cancer, including osteosarcoma, extremities or trunk' }, + { option: 'bone cancer, including osteosarcoma, head or neck' }, + { option: 'Boutonniere deformity in fingers', sideOfBody: true }, + { option: 'brain cancer, including glioblastoma' }, + { option: 'breast cancer', sideOfBody: true }, + { option: 'bronchiectasis' }, + { option: 'bulimia (type of eating disorder)' }, + { option: 'bunions (hallux valgus)', sideOfBody: true }, + { option: 'bursitis in ankle', sideOfBody: true }, + { option: 'bursitis in elbow', sideOfBody: true }, + { option: 'bursitis in foot', sideOfBody: true }, + { option: 'bursitis in hand or fingers', sideOfBody: true }, + { option: 'bursitis in hip', sideOfBody: true }, + { option: 'bursitis in knee', sideOfBody: true }, + { option: 'bursitis in shoulder', sideOfBody: true }, + { option: 'bursitis in wrist', sideOfBody: true }, + { option: 'carpal tunnel syndrome', sideOfBody: true }, + { option: 'cervical cancer' }, + { option: 'cervical spinal stenosis (narrowing of spinal canal in neck)' }, + { option: 'chloracne' }, + { option: 'chronic bronchitis' }, + { option: 'chronic conjunctivitis (pink eye)' }, + { option: 'chronic fatigue syndrome' }, + { option: 'chronic kidney disease (CKD)' }, + { option: 'chronic laryngitis' }, + { option: 'chronic nephritis (inflammation of kidneys)' }, + { option: 'chronic obstructive pulmonary disease (COPD)' }, + { option: "chronic otitis externa (swimmer's ear)", sideOfBody: true }, + { + option: 'chronic otitis media (chronic middle ear infection)', + sideOfBody: true, + }, + { option: 'chronic rhinitis, allergic or non-allergic' }, + { option: 'chronic sinusitis' }, + { + option: 'chronic suppurative otitis media (CSOM) or mastoiditis', + sideOfBody: true, + }, + { option: 'chronic urticaria (chronic hives)' }, + { option: 'cirrhosis (scarring of liver)' }, + { option: 'claw foot (pes cavus)', sideOfBody: true }, + { option: 'colorectal cancer or colon cancer' }, + { option: 'complete loss of sense of smell (anosmia)' }, + { option: 'complete loss of sense of taste (ageusia)' }, + { option: 'congestive heart failure (CHF)' }, + { option: 'constrictive bronchiolitis (obliterative bronchiolitis)' }, + { option: 'coronary artery disease (CAD or arteriosclerotic heart disease)' }, + { option: 'costochondritis' }, + { + option: + 'Cranial nerve paralysis or cranial neuritis (inflammation of cranial nerves)', + }, + { option: 'cranial neuralgia (cranial nerve pain)' }, + { option: "Crohn's disease" }, + { option: 'cyclothymic disorder (mild bipolar disorder)' }, + { + option: "De Quervain's tenosynovitis (De Quervain's syndrome)", + sideOfBody: true, + }, + { option: 'decompression illness' }, + { option: 'deep vein thrombosis (DVT) in leg', sideOfBody: true }, + { + option: 'degenerative arthritis (osteoarthritis) in ankle', + sideOfBody: true, + }, + { + option: 'degenerative arthritis (osteoarthritis) in elbow', + sideOfBody: true, + }, + { + option: 'degenerative arthritis (osteoarthritis) in foot', + sideOfBody: true, + }, + { + option: 'degenerative arthritis (osteoarthritis) in hand or fingers', + sideOfBody: true, + }, + { + option: 'degenerative arthritis (osteoarthritis) in hip', + sideOfBody: true, + }, + { + option: 'degenerative arthritis (osteoarthritis) in knee', + sideOfBody: true, + }, + { + option: 'degenerative arthritis (osteoarthritis) in shoulder', + sideOfBody: true, + }, + { + option: 'degenerative arthritis (osteoarthritis) in wrist', + sideOfBody: true, + }, + { option: 'depression (major depressive disorder)' }, + { option: 'deviated septum' }, + { option: 'diabetes insipidus' }, + { option: 'diabetes, type 1 or type 2' }, + { option: 'diabetic nephropathy (diabetic kidney disease)' }, + { + option: 'diabetic peripheral neuropathy lower extremities', + sideOfBody: true, + }, + { + option: 'diabetic peripheral neuropathy upper extremities', + sideOfBody: true, + }, + { option: 'diabetic retinopathy' }, + { option: 'dry eye syndrome' }, + { + option: + "Dupuytren's contracture (abnormal thickening of tissues in palm of hand)", + sideOfBody: true, + }, + { + option: + 'dysphagia (difficulty swallowing), associated with neurological disorder', + }, + { + option: + 'dysphagia (difficulty swallowing), not associated with neurological disorder', + }, + { option: 'ear cancer' }, + { option: 'eczema (atopic dermatitis)' }, + { option: 'elbow replacement (elbow arthroplasty)', sideOfBody: true }, + { option: 'emphysema' }, + { option: 'endometriosis' }, + { option: 'enlarged prostate (benign prostatic hyperplasia or BPH)' }, + { option: 'epididymitis' }, + { option: 'epiphora (watery eyes)' }, + { option: 'erectile dysfunction (ED)' }, + { option: 'esophageal cancer' }, + { option: 'esophageal stricture (narrowing of esophagus)' }, + { option: 'eye cancer, including melanoma of the eye' }, + { option: 'fecal incontinence (loss of bowel control)' }, + { option: 'female sexual arousal disorder (FSAD)' }, + { option: 'femoral hernia (hernia in thigh area)' }, + { option: 'fibromyalgia' }, + { option: 'flatfoot (pes planus)', sideOfBody: true }, + { option: 'foot fracture (tarsal or metatarsal fracture)', sideOfBody: true }, + { option: 'frostbite in foot', sideOfBody: true }, + { option: 'frostbite in hand or fingers', sideOfBody: true }, + { option: 'gallstones (cholelithiasis)' }, + { option: "gamekeeper's thumb", sideOfBody: true }, + { option: 'ganglion cyst in hand or fingers', sideOfBody: true }, + { option: 'ganglion cyst in wrist', sideOfBody: true }, + { option: 'generalized anxiety disorder (GAD)' }, + { option: 'GERD (gastroesophageal reflux disease)' }, + { option: 'glaucoma' }, + { option: "golfer's elbow (medial epicondylitis)", sideOfBody: true }, + { option: 'gonococcal arthritis' }, + { option: 'gout in foot', sideOfBody: true }, + { option: 'gout in wrist', sideOfBody: true }, + { option: 'granulomatous disease' }, + { + option: 'greater trochanteric pain syndrome (lateral hip pain)', + sideOfBody: true, + }, + { option: 'hallux rigidus (big toe arthritis)', sideOfBody: true }, + { option: 'hammer toe', sideOfBody: true }, + { option: "Hansen's disease (leprosy)" }, + { option: 'hearing loss' }, + { option: 'heart attack (myocardial infarction)' }, + { option: 'heart bypass surgery (coronary artery bypass graft)' }, + { option: 'hemorrhoids' }, + { option: 'hepatitis B' }, + { option: 'hepatitis C' }, + { option: 'herpes (herpes simplex virus or HSV)' }, + { option: 'hiatal hernia' }, + { + option: 'hip impingement (femoroacetabular impingement or FAI)', + sideOfBody: true, + }, + { option: 'hip replacement (hip arthroplasty)', sideOfBody: true }, + { option: 'hip sprain', sideOfBody: true }, + { option: 'HIV-related illness' }, + { option: 'Hodgkin lymphoma' }, + { option: 'hyperhidrosis (excessive sweating)' }, + { option: 'hypertension (high blood pressure)' }, + { option: 'hyperthyroidism (overactive thyroid)' }, + { option: 'hypopharyngeal cancer' }, + { option: 'hypothyroidism (underactive thyroid)' }, + { option: 'hysterectomy (removal of uterus)' }, + { + option: 'hysterectomy and ovariectomy (removal of uterus and both ovaries)', + }, + { option: 'illness anxiety disorder (hypochondria)' }, + { option: 'immersion foot', sideOfBody: true }, + { option: 'inguinal hernia (hernia in groin)' }, + { option: 'interstitial lung disease (ILD), including asbestosis' }, + { option: 'intervertebral disc syndrome (IVDS), back' }, + { option: 'intervertebral disc syndrome (IVDS), neck' }, + { option: 'iron deficiency anemia' }, + { option: 'irritable bowel syndrome (IBS)' }, + { option: 'jaw cancer' }, + { option: 'keratinization skin disorders' }, + { option: 'kidney cancer (renal cancer)', sideOfBody: true }, + { option: 'kidney removal (nephrectomy)' }, + { option: 'kidney stones (nephrolithiasis)' }, + { option: 'kidney transplant' }, + { option: 'knee hyperextension (genu recurvatum)', sideOfBody: true }, + { option: 'knee instability', sideOfBody: true }, + { option: 'knee or patellar dislocation', sideOfBody: true }, + { option: 'knee or patellar fracture', sideOfBody: true }, + { option: 'knee replacement (knee arthroplasty)', sideOfBody: true }, + { option: 'knee strain or sprain', sideOfBody: true }, + { option: 'labyrinthitis (inner ear inflammation)', sideOfBody: true }, + { option: 'laryngeal cancer (cancer of larynx)' }, + { option: 'left ventricular hypertrophy (LVH)' }, + { option: 'leukemia' }, + { option: 'liver cancer, including hepatocellular carcinoma (HCC)' }, + { option: 'loss of both eyes' }, + { option: 'loss of teeth due to bone loss' }, + { option: 'lower back sprain (lumbosacral sprain)' }, + { option: 'lower back strain (lumbosacral strain)' }, + { + option: 'lumbar spinal stenosis (narrowing of spinal canal in lower back)', + }, + { option: 'lung cancer' }, + { option: 'lupus' }, + { option: 'lymphatic filariasis' }, + { option: 'malaria' }, + { option: 'malignant tumor in spinal cord' }, + { option: 'mallet finger', sideOfBody: true }, + { option: 'melanoma of the skin' }, + { option: "Meniere's disease" }, + { option: 'meniscectomy (removal of meniscus)', sideOfBody: true }, + { option: 'meniscus tear', sideOfBody: true }, + { option: 'metatarsalgia (pain in ball of foot)', sideOfBody: true }, + { option: 'middle back sprain (thoracic sprain)' }, + { option: 'middle back strain (thoracic strain)' }, + { option: 'migraines (headaches)' }, + { option: 'mitral regurgitation' }, + { option: 'mitral valve prolapse (Barlow syndrome)' }, + { option: "Morton's neuroma", sideOfBody: true }, + { option: 'multiple myeloma, extremities or trunk' }, + { option: 'multiple myeloma, head or neck' }, + { option: 'multiple sclerosis (MS)' }, + { option: 'muscle hernia' }, + { option: 'myasthenia gravis' }, + { option: 'nasopharyngeal cancer' }, + { option: 'neck sprain (cervical sprain)' }, + { option: 'neck strain (cervical strain)' }, + { option: 'nerve-related soft-tissue sarcoma, extremities or trunk' }, + { option: 'nerve-related soft-tissue sarcoma, head or neck' }, + { option: 'neurogenic bladder' }, + { + option: 'non-diabetic peripheral neuropathy lower extremities', + sideOfBody: true, + }, + { + option: 'non-diabetic peripheral neuropathy upper extremities', + sideOfBody: true, + }, + { option: 'non-Hodgkin lymphoma' }, + { option: 'obsessive compulsive disorder (OCD)' }, + { option: 'oral cavity squamous cell carcinoma' }, + { option: 'orchitis (inflammation of testicles)' }, + { option: 'oropharyngeal cancer' }, + { option: 'osteomyelitis' }, + { option: 'ovarian adhesions' }, + { option: 'ovarian cancer', sideOfBody: true }, + { option: 'ovarian cysts' }, + { option: 'ovariectomy (removal of one or both ovaries)' }, + { option: 'pancreatic cancer' }, + { option: 'panic disorder' }, + { option: 'paranasal sinus or nasal cavity cancer' }, + { option: "Parkinson's disease" }, + { option: 'patellar or quadriceps tendon rupture', sideOfBody: true }, + { option: 'patellofemoral pain syndrome', sideOfBody: true }, + { option: 'PCL tear (posterior cruciate ligament tear)', sideOfBody: true }, + { option: 'penile cancer' }, + { + option: 'perforated eardrum (perforated tympanic membrane)', + sideOfBody: true, + }, + { option: 'persistent depressive disorder (dysthymic disorder)' }, + { option: 'pharyngeal cancer (throat cancer)' }, + { option: 'plantar fasciitis', sideOfBody: true }, + { option: 'plantar warts (foot warts)', sideOfBody: true }, + { option: 'pleurisy (pleuritis)' }, + { option: 'pneumococcal arthritis' }, + { option: 'polycystic ovary syndrome (PCOS)' }, + { option: 'post-traumatic arthritis in ankle', sideOfBody: true }, + { option: 'post-traumatic arthritis in elbow', sideOfBody: true }, + { option: 'post-traumatic arthritis in foot', sideOfBody: true }, + { option: 'post-traumatic arthritis in hand or fingers', sideOfBody: true }, + { option: 'post-traumatic arthritis in hip', sideOfBody: true }, + { option: 'post-traumatic arthritis in knee', sideOfBody: true }, + { option: 'post-traumatic arthritis in shoulder', sideOfBody: true }, + { option: 'post-traumatic arthritis in wrist', sideOfBody: true }, + { option: 'poststreptococcal arthritis' }, + { option: 'premature ventricular contractions' }, + { option: 'proctitis (inflammation of rectum)' }, + { option: 'prostate cancer' }, + { option: 'prostatitis (inflammation of prostate)' }, + { + option: + 'pseudofolliculitis barbae (razor bumps, shave bumps, or ingrown hairs)', + }, + { option: 'psoriasis' }, + { option: 'PTSD (post-traumatic stress disorder)' }, + { option: 'pulmonary fibrosis' }, + { option: 'pulmonary granuloma (lung nodule)' }, + { option: 'radiculopathy lower extremities', sideOfBody: true }, + { option: 'radiculopathy upper extremities', sideOfBody: true }, + { option: "Raynaud's disease" }, + { option: 'rheumatoid arthritis' }, + { option: 'rib fracture', sideOfBody: true }, + { option: 'rib removal' }, + { option: 'right ventricular hypertrophy (RVH)' }, + { option: 'ringworm (dermatophytosis)' }, + { option: 'rotator cuff tear', sideOfBody: true }, + { option: 'salivary gland cancer' }, + { option: 'sarcoidosis' }, + { option: 'scars, extremities or trunk' }, + { option: 'scars, head, face or neck' }, + { option: 'schizophrenia' }, + { option: 'shin splints', sideOfBody: true }, + { option: 'shoulder dislocation', sideOfBody: true }, + { option: 'shoulder impingement syndrome', sideOfBody: true }, + { option: 'shoulder replacement (shoulder arthroplasty)', sideOfBody: true }, + { option: 'shoulder strain', sideOfBody: true }, + { option: 'sickle cell anemia' }, + { + option: 'SLAP tear (superior labral anterior to posterior tear)', + sideOfBody: true, + }, + { option: 'sleep apnea' }, + { option: 'small intestine cancer' }, + { + option: + 'soft-tissue sarcoma of muscle, fat, or fibrous connective tissue, extremities or trunk', + }, + { + option: + 'soft-tissue sarcoma of muscle, fat, or fibrous connective tissue, head or neck', + }, + { option: 'somatic symptom disorder (SSD)' }, + { option: 'spinal arthritis, back' }, + { option: 'spinal arthritis, neck' }, + { option: 'spondylolisthesis, back' }, + { option: 'spondylolisthesis, neck' }, + { option: 'squamous cell carcinoma of the skin, extremities or trunk' }, + { option: 'squamous cell carcinoma of the skin, head or neck' }, + { option: 'stomach cancer' }, + { option: 'stress fracture in leg', sideOfBody: true }, + { option: 'syphilitic arthritis' }, + { option: 'tachycardia' }, + { option: 'tailbone (coccyx) removal' }, + { option: 'temporomandibular disorder, including TMJ' }, + { option: 'tendinosis in hand or fingers', sideOfBody: true }, + { option: 'tendonitis (tendinitis) in ankle', sideOfBody: true }, + { option: 'tendonitis (tendinitis) in elbow', sideOfBody: true }, + { option: 'tendonitis (tendinitis) in foot', sideOfBody: true }, + { option: 'tendonitis (tendinitis) in hand or fingers', sideOfBody: true }, + { option: 'tendonitis (tendinitis) in hip', sideOfBody: true }, + { option: 'tendonitis (tendinitis) in knee', sideOfBody: true }, + { option: 'tendonitis (tendinitis) in shoulder', sideOfBody: true }, + { option: 'tendonitis (tendinitis) in wrist', sideOfBody: true }, + { option: 'tennis elbow (lateral epicondylitis)', sideOfBody: true }, + { option: 'tenosynovitis in hand or fingers', sideOfBody: true }, + { option: 'tenosynovitis in wrist', sideOfBody: true }, + { option: 'testicular cancer', sideOfBody: true }, + { option: 'thyroid cancer' }, + { option: 'tibia or fibula fracture', sideOfBody: true }, + { option: 'Tietze syndrome' }, + { option: 'tinnitus (ringing or hissing in ears)' }, + { option: 'tongue cancer' }, + { option: 'trachea cancer (cancer of windpipe)' }, + { option: 'traumatic brain injury (TBI)' }, + { option: 'trigger finger', sideOfBody: true }, + { option: 'typhoid arthritis' }, + { option: 'ulcerative colitis' }, + { option: 'ureteral cancer (ureter cancer)' }, + { option: 'urethritis' }, + { option: 'urinary incontinence (loss of bladder control)' }, + { option: 'uterine cancer' }, + { option: 'vaginal cancer' }, + { option: 'varicocele', sideOfBody: true }, + { option: 'varicose veins in leg', sideOfBody: true }, + { + option: + 'vascular soft-tissue sarcoma, including angiosarcoma, extremities or trunk', + }, + { + option: + 'vascular soft-tissue sarcoma, including angiosarcoma, head or neck', + }, + { option: 'ventral hernia (hernia in abdomen)' }, + { + option: + 'visual impairment, including blurry vision, blindness and double vision', + }, + { option: 'vitiligo' }, + { option: 'vulvar cancer' }, + { option: 'weak foot', sideOfBody: true }, + { option: 'Wolff-Parkinson-White syndrome' }, + { option: 'wrist fracture', sideOfBody: true }, + { option: 'wrist replacement (wrist arthroplasty)', sideOfBody: true }, + { option: 'wrist sprain', sideOfBody: true }, +]; + +export const conditionOptions = conditionObjects.map( + condition => condition.option, +); diff --git a/src/applications/user-testing/new-conditions/content/newConditions.jsx b/src/applications/user-testing/new-conditions/content/newConditions.jsx new file mode 100644 index 000000000000..ebd8a8446f2d --- /dev/null +++ b/src/applications/user-testing/new-conditions/content/newConditions.jsx @@ -0,0 +1,36 @@ +import React from 'react'; + +export const conditionInstructions = ( + <> +

Add a condition below. You can add more conditions later.

+

If your condition isn’t listed

+

+ You can claim a condition that isn’t listed. Enter your condition, + diagnosis or short description of your symptoms. +

+

Examples of conditions

+ +

Add a new condition

+ +); + +export const ServiceConnectedDisabilityDescription = () => ( + +

+ To be eligible for service-connected disability benefits, you’ll need to + show that your disability was caused by an event, injury, or disease + during your military service. You’ll need to show your condition is linked + to your service by submitting evidence, such as medical reports or lay + statements, with your claim. We may ask you to have a claim exam if you + don’t submit evidence or if we need more information to decide your claim. +

+
+); diff --git a/src/applications/user-testing/new-conditions/manifest.json b/src/applications/user-testing/new-conditions/manifest.json new file mode 100644 index 000000000000..3b8fd5a10132 --- /dev/null +++ b/src/applications/user-testing/new-conditions/manifest.json @@ -0,0 +1,7 @@ +{ + "appName": "File for disability compensation", + "entryFile": "./app-entry.jsx", + "entryName": "new-conditions", + "rootUrl": "/user-testing", + "productId": "0db1a222-9d73-4c26-9966-75283e6510a1" +} diff --git a/src/applications/user-testing/new-conditions/pages/chooseDemo.js b/src/applications/user-testing/new-conditions/pages/chooseDemo.js new file mode 100644 index 000000000000..7e36567d51ef --- /dev/null +++ b/src/applications/user-testing/new-conditions/pages/chooseDemo.js @@ -0,0 +1,28 @@ +import { + radioSchema, + radioUI, +} from 'platform/forms-system/src/js/web-component-patterns'; + +const demoOptions = { + newConditions: 'Owl', + newConditionsJustConditions: 'Fox', +}; + +/** @type {PageSchema} */ +export default { + title: 'Choose path', + path: 'choose-demo', + uiSchema: { + demo: radioUI({ + title: 'Which demo would you like to go through first?', + labels: demoOptions, + }), + }, + schema: { + type: 'object', + properties: { + demo: radioSchema(Object.keys(demoOptions)), + }, + required: ['demo'], + }, +}; diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/condition.js b/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/condition.js new file mode 100644 index 000000000000..d853f5530806 --- /dev/null +++ b/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/condition.js @@ -0,0 +1,105 @@ +import React from 'react'; +import { + titleUI, + withAlertOrDescription, +} from 'platform/forms-system/src/js/web-component-patterns'; + +import Autocomplete from '../../components/Autocomplete'; +import { conditionOptions } from '../../content/conditionOptions'; +import { conditionInstructions } from '../../content/newConditions'; +import { arrayBuilderOptions, createTitle } from './utils'; + +const missingConditionMessage = + 'Enter a condition, diagnosis, or short description of your symptoms'; + +const NULL_CONDITION_STRING = 'Unknown Condition'; + +const regexNonWord = /[^\w]/g; +const sippableId = str => + (str || 'blank').replace(regexNonWord, '').toLowerCase(); + +const validateLength = (err, fieldData) => { + if (fieldData.length > 255) { + err.addError('This needs to be less than 256 characters'); + } +}; + +const validateNotMissing = (err, fieldData) => { + const isMissingCondition = + !fieldData?.trim() || + fieldData.toLowerCase() === NULL_CONDITION_STRING.toLowerCase(); + + if (isMissingCondition) { + err.addError(missingConditionMessage); + } +}; + +const validateNotDuplicate = (err, fieldData, formData) => { + const currentList = + formData?.newConditions?.map(condition => + condition.condition?.toLowerCase(), + ) || []; + const itemLowerCased = fieldData?.toLowerCase() || ''; + const itemSippableId = sippableId(fieldData || ''); + const itemCount = currentList.filter( + item => item === itemLowerCased || sippableId(item) === itemSippableId, + ); + + if (itemCount.length > 1) { + err.addError('You’ve already added this condition to your claim'); + } +}; + +export const validateCondition = (err, fieldData = '', formData = {}) => { + validateLength(err, fieldData); + validateNotMissing(err, fieldData); + validateNotDuplicate(err, fieldData, formData); +}; + +/** @returns {PageSchema} */ +const conditionPage = { + uiSchema: { + ...titleUI( + () => + createTitle( + 'Tell us the new condition you want to claim', + `Edit condition`, + ), + withAlertOrDescription({ + nounSingular: arrayBuilderOptions.nounSingular, + description: conditionInstructions, + hasMultipleItemPages: true, + }), + ), + condition: { + 'ui:title': 'Enter your condition', + 'ui:field': data => ( + + ), + 'ui:errorMessages': { + required: missingConditionMessage, + }, + 'ui:validations': [validateCondition], + 'ui:options': { + useAllFormData: true, + }, + }, + }, + schema: { + type: 'object', + properties: { + condition: { + type: 'string', + }, + }, + required: ['condition'], + }, +}; + +export default conditionPage; diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/date.js b/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/date.js new file mode 100644 index 000000000000..5dfbca22c7c3 --- /dev/null +++ b/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/date.js @@ -0,0 +1,33 @@ +import { + arrayBuilderItemSubsequentPageTitleUI, + textUI, +} from 'platform/forms-system/src/js/web-component-patterns'; + +import { createItemName } from './utils'; + +/** @returns {PageSchema} */ +const datePage = { + uiSchema: { + ...arrayBuilderItemSubsequentPageTitleUI( + ({ formData }) => `Date of ${createItemName(formData)}`, + ), + date: textUI({ + title: + 'What is the approximate date this condition began? If you’re having trouble remembering the exact date you can provide a year.', + hint: 'For example: January 2004 or 2004', + charcount: true, + }), + }, + schema: { + type: 'object', + properties: { + date: { + type: 'string', + maxLength: 25, + }, + }, + required: ['date'], + }, +}; + +export default datePage; diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/index.js b/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/index.js new file mode 100644 index 000000000000..7939ad64f30c --- /dev/null +++ b/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/index.js @@ -0,0 +1,79 @@ +import { stringifyUrlParams } from '@department-of-veterans-affairs/platform-forms-system/helpers'; +import { arrayBuilderPages } from 'platform/forms-system/src/js/patterns/array-builder'; +import { getArrayIndexFromPathName } from 'platform/forms-system/src/js/patterns/array-builder/helpers'; + +import conditionPage from './condition'; +import datePage from './date'; +import introPage from './intro'; +import sideOfBodyPage from './sideOfBody'; +import summaryPage from './summary'; +import { arrayBuilderOptions, hasSideOfBody } from './utils'; + +const newConditionsJustConditionsPages = arrayBuilderPages( + arrayBuilderOptions, + (pageBuilder, helpers) => ({ + newConditionsJustConditionsIntro: pageBuilder.introPage({ + title: 'New conditions intro', + path: 'new-conditions-just-conditions-intro', + depends: formData => formData.demo === 'newConditionsJustConditions', + uiSchema: introPage.uiSchema, + schema: introPage.schema, + }), + newConditionsJustConditionsSummary: pageBuilder.summaryPage({ + title: 'Review your new conditions', + path: 'new-conditions-just-conditions-summary', + depends: formData => formData.demo === 'newConditionsJustConditions', + uiSchema: summaryPage.uiSchema, + schema: summaryPage.schema, + }), + newConditionsJustConditionsCondition: pageBuilder.itemPage({ + title: 'Claim a new condition', + path: 'new-conditions-just-conditions/:index/condition', + depends: formData => formData.demo === 'newConditionsJustConditions', + uiSchema: conditionPage.uiSchema, + schema: conditionPage.schema, + onNavForward: props => { + const { formData, pathname, urlParams, goPath } = props; + const index = getArrayIndexFromPathName(pathname); + const urlParamsString = stringifyUrlParams(urlParams) || ''; + + // TODO: This fixed bug where side of body was not being cleared when condition was edited which could result in 'Asthma, right' + // However, with this fix, when user doesn't change condition, side of body is cleared which could confuse users + formData.sideOfBody = undefined; + + return hasSideOfBody(formData, index) + ? helpers.navForwardKeepUrlParams(props) + : goPath( + `new-conditions-just-conditions/${index}/date${urlParamsString}`, + ); + }, + }), + newConditionsJustConditionsSideOfBody: pageBuilder.itemPage({ + title: 'Side of body of new condition', + path: 'new-conditions-just-conditions/:index/side-of-body', + depends: formData => formData.demo === 'newConditionsJustConditions', + uiSchema: sideOfBodyPage.uiSchema, + schema: sideOfBodyPage.schema, + }), + newConditionsJustConditionsDate: pageBuilder.itemPage({ + title: 'Date of new condition', + path: 'new-conditions-just-conditions/:index/date', + depends: formData => formData.demo === 'newConditionsJustConditions', + uiSchema: datePage.uiSchema, + schema: datePage.schema, + onNavBack: props => { + const { formData, pathname, urlParams, goPath } = props; + const index = getArrayIndexFromPathName(pathname); + const urlParamsString = stringifyUrlParams(urlParams) || ''; + + return hasSideOfBody(formData, index) + ? helpers.navBackKeepUrlParams(props) + : goPath( + `new-conditions-just-conditions/${index}/condition${urlParamsString}`, + ); + }, + }), + }), +); + +export default newConditionsJustConditionsPages; diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/intro.js b/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/intro.js new file mode 100644 index 000000000000..035320fd81b0 --- /dev/null +++ b/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/intro.js @@ -0,0 +1,17 @@ +import { titleUI } from 'platform/forms-system/src/js/web-component-patterns'; + +/** @returns {PageSchema} */ +const introPage = { + uiSchema: { + ...titleUI( + 'New conditions', + 'In the next few screens, we’ll ask you about the new condition or conditions you’re filing for compensation.', + ), + }, + schema: { + type: 'object', + properties: {}, + }, +}; + +export default introPage; diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/sideOfBody.js b/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/sideOfBody.js new file mode 100644 index 000000000000..1bb251fde7fb --- /dev/null +++ b/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/sideOfBody.js @@ -0,0 +1,39 @@ +import { + radioSchema, + radioUI, + titleUI, +} from 'platform/forms-system/src/js/web-component-patterns'; + +import { createTitle } from './utils'; + +const sideOfBodyOptions = { + RIGHT: 'Right', + LEFT: 'Left', + BILATERAL: 'Bilateral (both sides)', +}; + +/** @returns {PageSchema} */ +const sideOfBodyPage = { + uiSchema: { + ...titleUI(({ formData }) => { + const condition = formData.condition || 'condition'; + + return createTitle( + `Where is your ${condition}?`, + `Edit side of body for ${condition}`, + ); + }), + sideOfBody: radioUI({ + title: 'Side of body', + labels: sideOfBodyOptions, + }), + }, + schema: { + type: 'object', + properties: { + sideOfBody: radioSchema(Object.keys(sideOfBodyOptions)), + }, + }, +}; + +export default sideOfBodyPage; diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/summary.js b/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/summary.js new file mode 100644 index 000000000000..ef8bf9a1b7ea --- /dev/null +++ b/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/summary.js @@ -0,0 +1,33 @@ +import { + arrayBuilderYesNoSchema, + arrayBuilderYesNoUI, +} from 'platform/forms-system/src/js/web-component-patterns'; + +import { arrayBuilderOptions } from './utils'; + +/** + * This page is skipped on the first loop for required flow + * Cards are populated on this page above the uiSchema if items are present + * + * @returns {PageSchema} + */ +const summaryPage = { + uiSchema: { + 'view:hasConditions': arrayBuilderYesNoUI( + arrayBuilderOptions, + {}, + { + hint: ' ', // Because there is maxItems: 100 if this empty string is not present the hint will count down from 100 which is a confusing user experience + }, + ), + }, + schema: { + type: 'object', + properties: { + 'view:hasConditions': arrayBuilderYesNoSchema, + }, + required: ['view:hasConditions'], + }, +}; + +export default summaryPage; diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/utils.js b/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/utils.js new file mode 100644 index 000000000000..3468a4f21f97 --- /dev/null +++ b/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/utils.js @@ -0,0 +1,56 @@ +import { getArrayUrlSearchParams } from 'platform/forms-system/src/js/patterns/array-builder/helpers'; + +import { conditionObjects } from '../../content/conditionOptions'; + +export const createTitle = (defaultTitle, editTitle) => { + const search = getArrayUrlSearchParams(); + const isEdit = search.get('edit'); + + if (isEdit) { + return editTitle; + } + return defaultTitle; +}; + +export const hasSideOfBody = (formData, index) => { + const condition = formData?.newConditions + ? formData.newConditions[index]?.condition + : formData.condition; + + const conditionObject = conditionObjects.find( + conditionObj => conditionObj.option === condition, + ); + + return conditionObject ? conditionObject.sideOfBody : false; +}; + +// Different than lodash _capitalize because does not make rest of string lowercase which would break acronyms +const capitalizeFirstLetter = string => { + return string?.charAt(0).toUpperCase() + string?.slice(1); +}; + +export const createItemName = (item, capFirstLetter = false) => { + const condition = capFirstLetter + ? capitalizeFirstLetter(item?.condition) + : item?.condition || 'condition'; + + if (item?.sideOfBody) { + return `${condition}, ${item?.sideOfBody.toLowerCase()}`; + } + + return condition; +}; + +/** @type {ArrayBuilderOptions} */ +export const arrayBuilderOptions = { + arrayPath: 'newConditions', + nounSingular: 'condition', + nounPlural: 'conditions', + required: true, + isItemIncomplete: item => !item?.condition || !item?.date, + maxItems: 100, + text: { + getItemName: item => createItemName(item, true), + cardDescription: item => `Date began: ${item?.date}`, + }, +}; diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsPages/cause.js b/src/applications/user-testing/new-conditions/pages/newConditionsPages/cause.js new file mode 100644 index 000000000000..b66491d50dc8 --- /dev/null +++ b/src/applications/user-testing/new-conditions/pages/newConditionsPages/cause.js @@ -0,0 +1,48 @@ +import { + arrayBuilderItemSubsequentPageTitleUI, + radioSchema, + radioUI, +} from 'platform/forms-system/src/js/web-component-patterns'; + +import { ServiceConnectedDisabilityDescription } from '../../content/newConditions'; +import { createItemName } from './utils'; + +const causeOptions = { + NEW: + 'My condition was caused by an injury or exposure during my military service.', + SECONDARY: + 'My condition was caused by another service-connected disability I already have. (For example, I have a limp that caused lower-back problems.)', + WORSENED: + 'My condition existed before I served in the military, but it got worse because of my military service.', + VA: + 'My condition was caused by an injury or event that happened when I was receiving VA care.', +}; + +/** @returns {PageSchema} */ +const causePage = { + uiSchema: { + ...arrayBuilderItemSubsequentPageTitleUI( + ({ formData }) => `Cause of ${createItemName(formData)}`, + ), + cause: radioUI({ + title: 'What caused your condition?', + labels: causeOptions, + }), + 'view:serviceConnectedDisabilityDescription': { + 'ui:description': ServiceConnectedDisabilityDescription, + }, + }, + schema: { + type: 'object', + properties: { + cause: radioSchema(Object.keys(causeOptions)), + 'view:serviceConnectedDisabilityDescription': { + type: 'object', + properties: {}, + }, + }, + required: ['cause'], + }, +}; + +export default causePage; diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsPages/causeFollowUp.js b/src/applications/user-testing/new-conditions/pages/newConditionsPages/causeFollowUp.js new file mode 100644 index 000000000000..fefbbbcfc897 --- /dev/null +++ b/src/applications/user-testing/new-conditions/pages/newConditionsPages/causeFollowUp.js @@ -0,0 +1,158 @@ +import { + arrayBuilderItemSubsequentPageTitleUI, + selectSchema, + selectUI, + textareaUI, + textUI, +} from 'platform/forms-system/src/js/web-component-patterns'; + +import { conditionOptions } from '../../content/conditionOptions'; +import { createItemName } from './utils'; + +const createCauseFollowUpTitles = formData => { + const causeTitle = { + NEW: `Details of injury or exposure that caused ${createItemName( + formData, + )}`, + SECONDARY: `Details of the service-connected disability that caused ${createItemName( + formData, + )}`, + WORSENED: `Details of the injury or exposure that worsened ${createItemName( + formData, + )}`, + VA: `Details of the injury or event in VA care for ${createItemName( + formData, + )}`, + }; + + return causeTitle[formData.cause]; +}; + +// TODO: Fix formData so that shape is consistent +// formData changes shape between add and edit which results in the need for the conditional below +const createCauseFollowUpConditional = (formData, index, causeType) => { + const cause = formData?.newConditions + ? formData.newConditions[index]?.cause + : formData.cause; + return cause !== causeType; +}; + +// TODO: Fix causedByCondition functionality on edit +// formData on add { "newConditions": [{ "condition": "migraines (headaches)"... }] } +// formData on edit { "condition": "migraines (headaches)"... } - does not include ratedDisabilities or other newConditions +// TODO: If causedByCondition is 'asthma' asthma is updated to 'emphysema' ensure 'asthma' is cleared as potential cause +const getOtherConditions = (formData, currentIndex) => { + const ratedDisabilities = + formData?.ratedDisabilities?.map(disability => disability.name) || []; + + const otherNewConditions = + formData?.newConditions + ?.filter((_, index) => index !== currentIndex) + ?.map(condition => createItemName(condition)) || []; + + return [...ratedDisabilities, ...otherNewConditions]; +}; + +/** @returns {PageSchema} */ +const causeFollowUpPage = { + uiSchema: { + ...arrayBuilderItemSubsequentPageTitleUI(({ formData }) => + createCauseFollowUpTitles(formData), + ), + primaryDescription: textareaUI({ + title: + 'Briefly describe the injury or exposure that caused your condition. For example, I operated loud machinery while in the service, and this caused me to lose my hearing.', + hideIf: (formData, index) => + createCauseFollowUpConditional(formData, index, 'NEW'), + required: (formData, index) => + !createCauseFollowUpConditional(formData, index, 'NEW'), + charcount: true, + }), + causedByCondition: selectUI({ + title: + 'Choose the service-connected disability that caused the new condition that you’re claiming here.', + updateSchema: (formData, _schema, _uiSchema, index) => { + return selectSchema(getOtherConditions(formData, index)); + }, + hideIf: (formData, index) => + createCauseFollowUpConditional(formData, index, 'SECONDARY'), + required: (formData, index) => + !createCauseFollowUpConditional(formData, index, 'SECONDARY'), + }), + causedByConditionDescription: textareaUI({ + title: 'Briefly describe how this disability caused your new condition.', + hideIf: (formData, index) => + createCauseFollowUpConditional(formData, index, 'SECONDARY'), + required: (formData, index) => + !createCauseFollowUpConditional(formData, index, 'SECONDARY'), + charcount: true, + }), + worsenedDescription: textUI({ + title: + 'Briefly describe the injury or exposure during your military service that caused your existing disability to get worse.', + hideIf: (formData, index) => + createCauseFollowUpConditional(formData, index, 'WORSENED'), + required: (formData, index) => + !createCauseFollowUpConditional(formData, index, 'WORSENED'), + charcount: true, + }), + worsenedEffects: textareaUI({ + title: + 'Tell us how the disability affected you before your service, and how it affects you now after your service.', + hideIf: (formData, index) => + createCauseFollowUpConditional(formData, index, 'WORSENED'), + required: (formData, index) => + !createCauseFollowUpConditional(formData, index, 'WORSENED'), + charcount: true, + }), + vaMistreatmentDescription: textareaUI({ + title: + 'Briefly describe the injury or event while you were under VA care that caused your disability.', + hideIf: (formData, index) => + createCauseFollowUpConditional(formData, index, 'VA'), + required: (formData, index) => + !createCauseFollowUpConditional(formData, index, 'VA'), + charcount: true, + }), + vaMistreatmentLocation: textUI({ + title: 'Tell us where this happened.', + hideIf: (formData, index) => + createCauseFollowUpConditional(formData, index, 'VA'), + required: (formData, index) => + !createCauseFollowUpConditional(formData, index, 'VA'), + charcount: true, + }), + }, + schema: { + type: 'object', + properties: { + primaryDescription: { + type: 'string', + maxLength: 400, + }, + causedByCondition: selectSchema(conditionOptions), + causedByConditionDescription: { + type: 'string', + maxLength: 400, + }, + worsenedDescription: { + type: 'string', + maxLength: 50, + }, + worsenedEffects: { + type: 'string', + maxLength: 350, + }, + vaMistreatmentDescription: { + type: 'string', + maxLength: 350, + }, + vaMistreatmentLocation: { + type: 'string', + maxLength: 25, + }, + }, + }, +}; + +export default causeFollowUpPage; diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsPages/condition.js b/src/applications/user-testing/new-conditions/pages/newConditionsPages/condition.js new file mode 100644 index 000000000000..d853f5530806 --- /dev/null +++ b/src/applications/user-testing/new-conditions/pages/newConditionsPages/condition.js @@ -0,0 +1,105 @@ +import React from 'react'; +import { + titleUI, + withAlertOrDescription, +} from 'platform/forms-system/src/js/web-component-patterns'; + +import Autocomplete from '../../components/Autocomplete'; +import { conditionOptions } from '../../content/conditionOptions'; +import { conditionInstructions } from '../../content/newConditions'; +import { arrayBuilderOptions, createTitle } from './utils'; + +const missingConditionMessage = + 'Enter a condition, diagnosis, or short description of your symptoms'; + +const NULL_CONDITION_STRING = 'Unknown Condition'; + +const regexNonWord = /[^\w]/g; +const sippableId = str => + (str || 'blank').replace(regexNonWord, '').toLowerCase(); + +const validateLength = (err, fieldData) => { + if (fieldData.length > 255) { + err.addError('This needs to be less than 256 characters'); + } +}; + +const validateNotMissing = (err, fieldData) => { + const isMissingCondition = + !fieldData?.trim() || + fieldData.toLowerCase() === NULL_CONDITION_STRING.toLowerCase(); + + if (isMissingCondition) { + err.addError(missingConditionMessage); + } +}; + +const validateNotDuplicate = (err, fieldData, formData) => { + const currentList = + formData?.newConditions?.map(condition => + condition.condition?.toLowerCase(), + ) || []; + const itemLowerCased = fieldData?.toLowerCase() || ''; + const itemSippableId = sippableId(fieldData || ''); + const itemCount = currentList.filter( + item => item === itemLowerCased || sippableId(item) === itemSippableId, + ); + + if (itemCount.length > 1) { + err.addError('You’ve already added this condition to your claim'); + } +}; + +export const validateCondition = (err, fieldData = '', formData = {}) => { + validateLength(err, fieldData); + validateNotMissing(err, fieldData); + validateNotDuplicate(err, fieldData, formData); +}; + +/** @returns {PageSchema} */ +const conditionPage = { + uiSchema: { + ...titleUI( + () => + createTitle( + 'Tell us the new condition you want to claim', + `Edit condition`, + ), + withAlertOrDescription({ + nounSingular: arrayBuilderOptions.nounSingular, + description: conditionInstructions, + hasMultipleItemPages: true, + }), + ), + condition: { + 'ui:title': 'Enter your condition', + 'ui:field': data => ( + + ), + 'ui:errorMessages': { + required: missingConditionMessage, + }, + 'ui:validations': [validateCondition], + 'ui:options': { + useAllFormData: true, + }, + }, + }, + schema: { + type: 'object', + properties: { + condition: { + type: 'string', + }, + }, + required: ['condition'], + }, +}; + +export default conditionPage; diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsPages/date.js b/src/applications/user-testing/new-conditions/pages/newConditionsPages/date.js new file mode 100644 index 000000000000..5dfbca22c7c3 --- /dev/null +++ b/src/applications/user-testing/new-conditions/pages/newConditionsPages/date.js @@ -0,0 +1,33 @@ +import { + arrayBuilderItemSubsequentPageTitleUI, + textUI, +} from 'platform/forms-system/src/js/web-component-patterns'; + +import { createItemName } from './utils'; + +/** @returns {PageSchema} */ +const datePage = { + uiSchema: { + ...arrayBuilderItemSubsequentPageTitleUI( + ({ formData }) => `Date of ${createItemName(formData)}`, + ), + date: textUI({ + title: + 'What is the approximate date this condition began? If you’re having trouble remembering the exact date you can provide a year.', + hint: 'For example: January 2004 or 2004', + charcount: true, + }), + }, + schema: { + type: 'object', + properties: { + date: { + type: 'string', + maxLength: 25, + }, + }, + required: ['date'], + }, +}; + +export default datePage; diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsPages/index.js b/src/applications/user-testing/new-conditions/pages/newConditionsPages/index.js new file mode 100644 index 000000000000..92471226126a --- /dev/null +++ b/src/applications/user-testing/new-conditions/pages/newConditionsPages/index.js @@ -0,0 +1,91 @@ +import { stringifyUrlParams } from '@department-of-veterans-affairs/platform-forms-system/helpers'; +import { arrayBuilderPages } from 'platform/forms-system/src/js/patterns/array-builder'; +import { getArrayIndexFromPathName } from 'platform/forms-system/src/js/patterns/array-builder/helpers'; + +import causePage from './cause'; +import causeFollowUpPage from './causeFollowUp'; +import conditionPage from './condition'; +import datePage from './date'; +import introPage from './intro'; +import sideOfBodyPage from './sideOfBody'; +import summaryPage from './summary'; +import { arrayBuilderOptions, hasSideOfBody } from './utils'; + +const newConditionsPages = arrayBuilderPages( + arrayBuilderOptions, + (pageBuilder, helpers) => ({ + newConditionsIntro: pageBuilder.introPage({ + title: 'New conditions intro', + path: 'new-conditions-intro', + depends: formData => formData.demo === 'newConditions', + uiSchema: introPage.uiSchema, + schema: introPage.schema, + }), + newConditionsSummary: pageBuilder.summaryPage({ + title: 'Review your new conditions', + path: 'new-conditions-summary', + depends: formData => formData.demo === 'newConditions', + uiSchema: summaryPage.uiSchema, + schema: summaryPage.schema, + }), + newConditionsCondition: pageBuilder.itemPage({ + title: 'Claim a new condition', + path: 'new-conditions/:index/condition', + depends: formData => formData.demo === 'newConditions', + uiSchema: conditionPage.uiSchema, + schema: conditionPage.schema, + onNavForward: props => { + const { formData, pathname, urlParams, goPath } = props; + const index = getArrayIndexFromPathName(pathname); + const urlParamsString = stringifyUrlParams(urlParams) || ''; + + // TODO: This fixed bug where side of body was not being cleared when condition was edited which could result in 'Asthma, right' + // However, with this fix, when user doesn't change condition, side of body is cleared which could confuse users + formData.sideOfBody = undefined; + + return hasSideOfBody(formData, index) + ? helpers.navForwardKeepUrlParams(props) + : goPath(`new-conditions/${index}/date${urlParamsString}`); + }, + }), + newConditionsSideOfBody: pageBuilder.itemPage({ + title: 'Side of body of new condition', + path: 'new-conditions/:index/side-of-body', + depends: formData => formData.demo === 'newConditions', + uiSchema: sideOfBodyPage.uiSchema, + schema: sideOfBodyPage.schema, + }), + newConditionsDate: pageBuilder.itemPage({ + title: 'Date of new condition', + path: 'new-conditions/:index/date', + depends: formData => formData.demo === 'newConditions', + uiSchema: datePage.uiSchema, + schema: datePage.schema, + onNavBack: props => { + const { formData, pathname, urlParams, goPath } = props; + const index = getArrayIndexFromPathName(pathname); + const urlParamsString = stringifyUrlParams(urlParams) || ''; + + return hasSideOfBody(formData, index) + ? helpers.navBackKeepUrlParams(props) + : goPath(`new-conditions/${index}/condition${urlParamsString}`); + }, + }), + newConditionsCause: pageBuilder.itemPage({ + title: 'Cause of new condition', + path: 'new-conditions/:index/cause', + depends: formData => formData.demo === 'newConditions', + uiSchema: causePage.uiSchema, + schema: causePage.schema, + }), + newConditionsCauseFollowUp: pageBuilder.itemPage({ + title: 'Cause follow up for new condition', + path: 'new-conditions/:index/cause-follow-up', + depends: formData => formData.demo === 'newConditions', + uiSchema: causeFollowUpPage.uiSchema, + schema: causeFollowUpPage.schema, + }), + }), +); + +export default newConditionsPages; diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsPages/intro.js b/src/applications/user-testing/new-conditions/pages/newConditionsPages/intro.js new file mode 100644 index 000000000000..035320fd81b0 --- /dev/null +++ b/src/applications/user-testing/new-conditions/pages/newConditionsPages/intro.js @@ -0,0 +1,17 @@ +import { titleUI } from 'platform/forms-system/src/js/web-component-patterns'; + +/** @returns {PageSchema} */ +const introPage = { + uiSchema: { + ...titleUI( + 'New conditions', + 'In the next few screens, we’ll ask you about the new condition or conditions you’re filing for compensation.', + ), + }, + schema: { + type: 'object', + properties: {}, + }, +}; + +export default introPage; diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsPages/sideOfBody.js b/src/applications/user-testing/new-conditions/pages/newConditionsPages/sideOfBody.js new file mode 100644 index 000000000000..1bb251fde7fb --- /dev/null +++ b/src/applications/user-testing/new-conditions/pages/newConditionsPages/sideOfBody.js @@ -0,0 +1,39 @@ +import { + radioSchema, + radioUI, + titleUI, +} from 'platform/forms-system/src/js/web-component-patterns'; + +import { createTitle } from './utils'; + +const sideOfBodyOptions = { + RIGHT: 'Right', + LEFT: 'Left', + BILATERAL: 'Bilateral (both sides)', +}; + +/** @returns {PageSchema} */ +const sideOfBodyPage = { + uiSchema: { + ...titleUI(({ formData }) => { + const condition = formData.condition || 'condition'; + + return createTitle( + `Where is your ${condition}?`, + `Edit side of body for ${condition}`, + ); + }), + sideOfBody: radioUI({ + title: 'Side of body', + labels: sideOfBodyOptions, + }), + }, + schema: { + type: 'object', + properties: { + sideOfBody: radioSchema(Object.keys(sideOfBodyOptions)), + }, + }, +}; + +export default sideOfBodyPage; diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsPages/summary.js b/src/applications/user-testing/new-conditions/pages/newConditionsPages/summary.js new file mode 100644 index 000000000000..ef8bf9a1b7ea --- /dev/null +++ b/src/applications/user-testing/new-conditions/pages/newConditionsPages/summary.js @@ -0,0 +1,33 @@ +import { + arrayBuilderYesNoSchema, + arrayBuilderYesNoUI, +} from 'platform/forms-system/src/js/web-component-patterns'; + +import { arrayBuilderOptions } from './utils'; + +/** + * This page is skipped on the first loop for required flow + * Cards are populated on this page above the uiSchema if items are present + * + * @returns {PageSchema} + */ +const summaryPage = { + uiSchema: { + 'view:hasConditions': arrayBuilderYesNoUI( + arrayBuilderOptions, + {}, + { + hint: ' ', // Because there is maxItems: 100 if this empty string is not present the hint will count down from 100 which is a confusing user experience + }, + ), + }, + schema: { + type: 'object', + properties: { + 'view:hasConditions': arrayBuilderYesNoSchema, + }, + required: ['view:hasConditions'], + }, +}; + +export default summaryPage; diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsPages/utils.js b/src/applications/user-testing/new-conditions/pages/newConditionsPages/utils.js new file mode 100644 index 000000000000..63a0ba9509bb --- /dev/null +++ b/src/applications/user-testing/new-conditions/pages/newConditionsPages/utils.js @@ -0,0 +1,79 @@ +import { getArrayUrlSearchParams } from 'platform/forms-system/src/js/patterns/array-builder/helpers'; + +import { conditionObjects } from '../../content/conditionOptions'; + +export const createTitle = (defaultTitle, editTitle) => { + const search = getArrayUrlSearchParams(); + const isEdit = search.get('edit'); + + if (isEdit) { + return editTitle; + } + return defaultTitle; +}; + +export const hasSideOfBody = (formData, index) => { + const condition = formData?.newConditions + ? formData.newConditions[index]?.condition + : formData.condition; + + const conditionObject = conditionObjects.find( + conditionObj => conditionObj.option === condition, + ); + + return conditionObject ? conditionObject.sideOfBody : false; +}; + +const createCauseDescriptions = item => { + return { + NEW: 'Caused by an injury or exposure during my service.', + SECONDARY: `Caused by ${item?.causedByCondition}.`, + WORSENED: + 'Existed before I served in the military, but got worse because of my military service.', + VA: + 'Caused by an injury or event that happened when I was receiving VA care.', + }; +}; + +const causeFollowUpChecks = { + NEW: item => !item?.primaryDescription, + SECONDARY: item => + !item?.causedByCondition || !item?.causedByConditionDescription, + WORSENED: item => !item?.worsenedDescription || !item?.worsenedEffects, + VA: item => !item?.vaMistreatmentDescription || !item?.vaMistreatmentLocation, +}; + +// Different than lodash _capitalize because does not make rest of string lowercase which would break acronyms +const capitalizeFirstLetter = string => { + return string?.charAt(0).toUpperCase() + string?.slice(1); +}; + +export const createItemName = (item, capFirstLetter = false) => { + const condition = capFirstLetter + ? capitalizeFirstLetter(item?.condition) + : item?.condition || 'condition'; + + if (item?.sideOfBody) { + return `${condition}, ${item?.sideOfBody.toLowerCase()}`; + } + + return condition; +}; + +/** @type {ArrayBuilderOptions} */ +export const arrayBuilderOptions = { + arrayPath: 'newConditions', + nounSingular: 'condition', + nounPlural: 'conditions', + required: true, + isItemIncomplete: item => + !item?.condition || + !item?.date || + !item?.cause || + (causeFollowUpChecks[item.cause] && causeFollowUpChecks[item.cause](item)), + maxItems: 100, + text: { + getItemName: item => createItemName(item, true), + cardDescription: item => createCauseDescriptions(item)[(item?.cause)], + }, +}; diff --git a/src/applications/user-testing/new-conditions/reducers/index.js b/src/applications/user-testing/new-conditions/reducers/index.js new file mode 100644 index 000000000000..fa616ca0da4b --- /dev/null +++ b/src/applications/user-testing/new-conditions/reducers/index.js @@ -0,0 +1,6 @@ +import { createSaveInProgressFormReducer } from 'platform/forms/save-in-progress/reducers'; +import formConfig from '../config/form'; + +export default { + form: createSaveInProgressFormReducer(formConfig), +}; diff --git a/src/applications/user-testing/new-conditions/routes.jsx b/src/applications/user-testing/new-conditions/routes.jsx new file mode 100644 index 000000000000..8b56e82cbf9a --- /dev/null +++ b/src/applications/user-testing/new-conditions/routes.jsx @@ -0,0 +1,12 @@ +import { createRoutesWithSaveInProgress } from 'platform/forms/save-in-progress/helpers'; +import formConfig from './config/form'; +import App from './containers/App'; + +const route = { + path: '/', + component: App, + indexRoute: { onEnter: (nextState, replace) => replace('/introduction') }, + childRoutes: createRoutesWithSaveInProgress(formConfig), +}; + +export default route; diff --git a/src/applications/user-testing/new-conditions/sass/new-conditions.scss b/src/applications/user-testing/new-conditions/sass/new-conditions.scss new file mode 100644 index 000000000000..1d4590179b05 --- /dev/null +++ b/src/applications/user-testing/new-conditions/sass/new-conditions.scss @@ -0,0 +1,52 @@ +@import "~@department-of-veterans-affairs/css-library/dist/stylesheets/modules/m-process-list"; +@import "~@department-of-veterans-affairs/css-library/dist/stylesheets/modules/m-form-process"; +@import "../../../../platform/forms/sass/m-schemaform"; +@import "~@department-of-veterans-affairs/css-library/dist/stylesheets/modules/m-modal"; +@import "~@department-of-veterans-affairs/css-library/dist/stylesheets/modules/m-omb-info"; +@import "../../../../platform/forms/sass/m-form-confirmation"; + +@import "~@department-of-veterans-affairs/css-library/dist/stylesheets/functions"; +@import "~@department-of-veterans-affairs/css-library/dist/stylesheets/modules/m-process-list"; +@import "~@department-of-veterans-affairs/css-library/dist/stylesheets/modules/m-form-process"; +@import "../../../../platform/forms/sass/m-schemaform"; +@import "../../../../platform/forms/sass/m-form-confirmation"; + +/* addDisabilities autocomplete styles */ +.cc-autocomplete { + position: relative; + + &__list { + position: absolute; + width: 100%; + max-width: 30rem; + background-color: var(--vads-color-white); + padding: 0; + margin: 0; + overflow-y: auto; + z-index: 100; + max-height: 20rem; + border: 1px solid var(--vads-color-black); + } + + &__option { + display: block; + padding: units(1); + margin: 0; + cursor: pointer; + + &--active { + background-color: var(--vads-color-primary); + border-color: var(--vads-color-primary); + color: var(--vads-color-white); + } + + &:first-child { + border-bottom: 1px solid var(--vads-color-gray-warm-light); + } + } + + // Adjusts spacing on label inside web component text input. + va-text-input::part(label) { + margin-top: 0; + } +} diff --git a/src/applications/user-testing/new-conditions/tests/containers/ConfirmationPage.unit.spec.jsx b/src/applications/user-testing/new-conditions/tests/containers/ConfirmationPage.unit.spec.jsx new file mode 100644 index 000000000000..5bfb439956cb --- /dev/null +++ b/src/applications/user-testing/new-conditions/tests/containers/ConfirmationPage.unit.spec.jsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { render } from '@testing-library/react'; +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { expect } from 'chai'; +import formConfig from '../../config/form'; +import ConfirmationPage from '../../containers/ConfirmationPage'; + +const storeBase = { + form: { + formId: formConfig.formId, + submission: { + response: { + confirmationNumber: '123456', + }, + timestamp: Date.now(), + }, + data: { + fullName: { + first: 'John', + middle: '', + last: 'Doe', + }, + }, + }, +}; + +describe('Confirmation page', () => { + const middleware = [thunk]; + const mockStore = configureStore(middleware); + + it('it should show status success and the correct name of person', () => { + const { container, getByText } = render( + + + , + ); + expect(container.querySelector('va-alert')).to.have.attr( + 'status', + 'success', + ); + getByText(/John Doe/); + }); +}); diff --git a/src/applications/user-testing/new-conditions/tests/containers/IntroductionPage.unit.spec.jsx b/src/applications/user-testing/new-conditions/tests/containers/IntroductionPage.unit.spec.jsx new file mode 100644 index 000000000000..96236de1f574 --- /dev/null +++ b/src/applications/user-testing/new-conditions/tests/containers/IntroductionPage.unit.spec.jsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { render } from '@testing-library/react'; +import { expect } from 'chai'; +import formConfig from '../../config/form'; +import IntroductionPage from '../../containers/IntroductionPage'; + +const props = { + route: { + path: 'introduction', + pageList: [], + formConfig, + }, + userLoggedIn: false, + userIdVerified: true, +}; + +const mockStore = { + getState: () => ({ + user: { + login: { + currentlyLoggedIn: false, + }, + profile: { + savedForms: [], + prefillsAvailable: [], + loa: { + current: 3, + highest: 3, + }, + verified: true, + dob: '2000-01-01', + claims: { + appeals: false, + }, + }, + }, + form: { + formId: formConfig.formId, + loadedStatus: 'success', + savedStatus: '', + loadedData: { + metadata: {}, + }, + data: {}, + }, + scheduledDowntime: { + globalDowntime: null, + isReady: true, + isPending: false, + serviceMap: { get() {} }, + dismissedDowntimeWarnings: [], + }, + }), + subscribe: () => {}, + dispatch: () => {}, +}; + +describe('IntroductionPage', () => { + it('should render', () => { + const { container } = render( + + + , + ); + expect(container).to.exist; + }); +}); diff --git a/src/applications/user-testing/new-conditions/tests/fixtures/data/minimal-test.json b/src/applications/user-testing/new-conditions/tests/fixtures/data/minimal-test.json new file mode 100644 index 000000000000..b57dee5b87cf --- /dev/null +++ b/src/applications/user-testing/new-conditions/tests/fixtures/data/minimal-test.json @@ -0,0 +1,9 @@ +{ + "data": { + "fullName": { + "first": "John", + "last": "Doe" + }, + "dateOfBirth": "1980-01-01" + } +} diff --git a/src/applications/user-testing/new-conditions/tests/fixtures/mocks/local-mock-responses.js b/src/applications/user-testing/new-conditions/tests/fixtures/mocks/local-mock-responses.js new file mode 100644 index 000000000000..e6c6e27268da --- /dev/null +++ b/src/applications/user-testing/new-conditions/tests/fixtures/mocks/local-mock-responses.js @@ -0,0 +1,8 @@ +// yarn mock-api --responses ./src/applications/{application}/tests/e2e/fixtures/mocks/local-mock-responses.js +const mockUser = require('./user.json'); + +const responses = { + 'GET /v0/user': mockUser, +}; + +module.exports = responses; diff --git a/src/applications/user-testing/new-conditions/tests/fixtures/mocks/user.json b/src/applications/user-testing/new-conditions/tests/fixtures/mocks/user.json new file mode 100644 index 000000000000..c707324756e1 --- /dev/null +++ b/src/applications/user-testing/new-conditions/tests/fixtures/mocks/user.json @@ -0,0 +1,56 @@ +{ + "data": { + "attributes": { + "profile": { + "sign_in": { + "service_name": "idme" + }, + "email": "john.doe@example.com", + "loa": { "current": 3 }, + "first_name": "John", + "middle_name": "", + "last_name": "Doe", + "gender": "M", + "birth_date": "1985-01-01", + "verified": true + }, + "veteran_status": { + "status": "OK", + "is_veteran": true, + "served_in_military": true + }, + "in_progress_forms": [], + "prefills_available": [], + "services": [ + "facilities", + "hca", + "edu-benefits", + "evss-claims", + "form526", + "user-profile", + "health-records", + "rx", + "messaging" + ], + "va_profile": { + "status": "OK", + "birth_date": "19850101", + "family_name": "Doe", + "gender": "M", + "given_names": ["John", ""], + "active_status": "active", + "facilities": [ + { + "facility_id": "983", + "is_cerner": false + }, + { + "facility_id": "984", + "is_cerner": false + } + ] + } + } + }, + "meta": { "errors": null } +} diff --git a/src/applications/user-testing/new-conditions/tests/new-conditions.cypress.spec.js b/src/applications/user-testing/new-conditions/tests/new-conditions.cypress.spec.js new file mode 100644 index 000000000000..906270a8aace --- /dev/null +++ b/src/applications/user-testing/new-conditions/tests/new-conditions.cypress.spec.js @@ -0,0 +1,37 @@ +import path from 'path'; +import testForm from 'platform/testing/e2e/cypress/support/form-tester'; +import { createTestConfig } from 'platform/testing/e2e/cypress/support/form-tester/utilities'; +import mockUser from './fixtures/mocks/user.json'; +import formConfig from '../config/form'; +import manifest from '../manifest.json'; + +const testConfig = createTestConfig( + { + dataPrefix: 'data', + dataDir: path.join(__dirname, 'fixtures', 'data'), + dataSets: ['minimal-test'], + pageHooks: { + introduction: ({ afterHook }) => { + afterHook(() => { + cy.findAllByText(/^start/i, { selector: 'a[href="#start"]' }) + .last() + .click({ force: true }); + }); + }, + }, + + setupPerTest: () => { + cy.intercept('GET', '/v0/user', mockUser); + cy.intercept('POST', formConfig.submitUrl, { status: 200 }); + cy.login(mockUser); + }, + + // Skip tests in CI until the form is released. + // Remove this setting when the form has a content page in production. + skip: Cypress.env('CI'), + }, + manifest, + formConfig, +); + +testForm(testConfig); diff --git a/src/platform/forms/constants.js b/src/platform/forms/constants.js index 797e104f6513..1c0daaae1ccf 100644 --- a/src/platform/forms/constants.js +++ b/src/platform/forms/constants.js @@ -155,6 +155,7 @@ export const getAllFormLinks = getAppUrlImpl => { [VA_FORM_IDS.FORM_WELCOME_VA_SETUP_REVIEW_INFORMATION]: `${tryGetAppUrl( 'welcome-va-setup-review-information', )}/`, + [VA_FORM_IDS.FORM_21_526EZ]: `${tryGetAppUrl('21-526EZ')}/`, }; }; @@ -465,6 +466,13 @@ export const MY_VA_SIP_FORMS = [ description: 'welcome va setup review information form', trackingPrefix: 'welcome-va-setup-review-information-', }, + { + id: VA_FORM_IDS.FORM_21_526EZ, + benefit: 'disability compensation', + title: 'File for disability compensation', + description: 'disability compensation', + trackingPrefix: 'new-conditions', + }, ]; export const FORM_BENEFITS = MY_VA_SIP_FORMS.reduce((acc, form) => { From 12fd67203a528fc33df087b13264366f173a97e5 Mon Sep 17 00:00:00 2001 From: William Phelps Date: Fri, 15 Nov 2024 08:14:18 -0800 Subject: [PATCH 2/4] Refine titles, choose your demo, and remove testing content --- .../user-testing/new-conditions/constants.js | 4 +- .../new-conditions/pages/chooseDemo.js | 4 +- .../containers/ConfirmationPage.unit.spec.jsx | 45 ------------ .../containers/IntroductionPage.unit.spec.jsx | 68 ------------------- .../tests/fixtures/data/minimal-test.json | 9 --- .../fixtures/mocks/local-mock-responses.js | 8 --- .../tests/fixtures/mocks/user.json | 56 --------------- .../tests/new-conditions.cypress.spec.js | 37 ---------- 8 files changed, 4 insertions(+), 227 deletions(-) delete mode 100644 src/applications/user-testing/new-conditions/tests/containers/ConfirmationPage.unit.spec.jsx delete mode 100644 src/applications/user-testing/new-conditions/tests/containers/IntroductionPage.unit.spec.jsx delete mode 100644 src/applications/user-testing/new-conditions/tests/fixtures/data/minimal-test.json delete mode 100644 src/applications/user-testing/new-conditions/tests/fixtures/mocks/local-mock-responses.js delete mode 100644 src/applications/user-testing/new-conditions/tests/fixtures/mocks/user.json delete mode 100644 src/applications/user-testing/new-conditions/tests/new-conditions.cypress.spec.js diff --git a/src/applications/user-testing/new-conditions/constants.js b/src/applications/user-testing/new-conditions/constants.js index 97bc678a1a49..877ed5a742c4 100644 --- a/src/applications/user-testing/new-conditions/constants.js +++ b/src/applications/user-testing/new-conditions/constants.js @@ -1,2 +1,2 @@ -export const TITLE = 'File for disability compensation with VA Form 21-526EZ'; -export const SUBTITLE = 'VA Form 21-526EZ'; +export const TITLE = 'Demo: File for disability compensation'; +export const SUBTITLE = 'Demo of VA Form 21-526EZ'; diff --git a/src/applications/user-testing/new-conditions/pages/chooseDemo.js b/src/applications/user-testing/new-conditions/pages/chooseDemo.js index 7e36567d51ef..b5871ab60a03 100644 --- a/src/applications/user-testing/new-conditions/pages/chooseDemo.js +++ b/src/applications/user-testing/new-conditions/pages/chooseDemo.js @@ -10,11 +10,11 @@ const demoOptions = { /** @type {PageSchema} */ export default { - title: 'Choose path', + title: 'Choose a demo', path: 'choose-demo', uiSchema: { demo: radioUI({ - title: 'Which demo would you like to go through first?', + title: 'Choose a demo', labels: demoOptions, }), }, diff --git a/src/applications/user-testing/new-conditions/tests/containers/ConfirmationPage.unit.spec.jsx b/src/applications/user-testing/new-conditions/tests/containers/ConfirmationPage.unit.spec.jsx deleted file mode 100644 index 5bfb439956cb..000000000000 --- a/src/applications/user-testing/new-conditions/tests/containers/ConfirmationPage.unit.spec.jsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import { Provider } from 'react-redux'; -import { render } from '@testing-library/react'; -import configureStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import { expect } from 'chai'; -import formConfig from '../../config/form'; -import ConfirmationPage from '../../containers/ConfirmationPage'; - -const storeBase = { - form: { - formId: formConfig.formId, - submission: { - response: { - confirmationNumber: '123456', - }, - timestamp: Date.now(), - }, - data: { - fullName: { - first: 'John', - middle: '', - last: 'Doe', - }, - }, - }, -}; - -describe('Confirmation page', () => { - const middleware = [thunk]; - const mockStore = configureStore(middleware); - - it('it should show status success and the correct name of person', () => { - const { container, getByText } = render( - - - , - ); - expect(container.querySelector('va-alert')).to.have.attr( - 'status', - 'success', - ); - getByText(/John Doe/); - }); -}); diff --git a/src/applications/user-testing/new-conditions/tests/containers/IntroductionPage.unit.spec.jsx b/src/applications/user-testing/new-conditions/tests/containers/IntroductionPage.unit.spec.jsx deleted file mode 100644 index 96236de1f574..000000000000 --- a/src/applications/user-testing/new-conditions/tests/containers/IntroductionPage.unit.spec.jsx +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; -import { Provider } from 'react-redux'; -import { render } from '@testing-library/react'; -import { expect } from 'chai'; -import formConfig from '../../config/form'; -import IntroductionPage from '../../containers/IntroductionPage'; - -const props = { - route: { - path: 'introduction', - pageList: [], - formConfig, - }, - userLoggedIn: false, - userIdVerified: true, -}; - -const mockStore = { - getState: () => ({ - user: { - login: { - currentlyLoggedIn: false, - }, - profile: { - savedForms: [], - prefillsAvailable: [], - loa: { - current: 3, - highest: 3, - }, - verified: true, - dob: '2000-01-01', - claims: { - appeals: false, - }, - }, - }, - form: { - formId: formConfig.formId, - loadedStatus: 'success', - savedStatus: '', - loadedData: { - metadata: {}, - }, - data: {}, - }, - scheduledDowntime: { - globalDowntime: null, - isReady: true, - isPending: false, - serviceMap: { get() {} }, - dismissedDowntimeWarnings: [], - }, - }), - subscribe: () => {}, - dispatch: () => {}, -}; - -describe('IntroductionPage', () => { - it('should render', () => { - const { container } = render( - - - , - ); - expect(container).to.exist; - }); -}); diff --git a/src/applications/user-testing/new-conditions/tests/fixtures/data/minimal-test.json b/src/applications/user-testing/new-conditions/tests/fixtures/data/minimal-test.json deleted file mode 100644 index b57dee5b87cf..000000000000 --- a/src/applications/user-testing/new-conditions/tests/fixtures/data/minimal-test.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "data": { - "fullName": { - "first": "John", - "last": "Doe" - }, - "dateOfBirth": "1980-01-01" - } -} diff --git a/src/applications/user-testing/new-conditions/tests/fixtures/mocks/local-mock-responses.js b/src/applications/user-testing/new-conditions/tests/fixtures/mocks/local-mock-responses.js deleted file mode 100644 index e6c6e27268da..000000000000 --- a/src/applications/user-testing/new-conditions/tests/fixtures/mocks/local-mock-responses.js +++ /dev/null @@ -1,8 +0,0 @@ -// yarn mock-api --responses ./src/applications/{application}/tests/e2e/fixtures/mocks/local-mock-responses.js -const mockUser = require('./user.json'); - -const responses = { - 'GET /v0/user': mockUser, -}; - -module.exports = responses; diff --git a/src/applications/user-testing/new-conditions/tests/fixtures/mocks/user.json b/src/applications/user-testing/new-conditions/tests/fixtures/mocks/user.json deleted file mode 100644 index c707324756e1..000000000000 --- a/src/applications/user-testing/new-conditions/tests/fixtures/mocks/user.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "data": { - "attributes": { - "profile": { - "sign_in": { - "service_name": "idme" - }, - "email": "john.doe@example.com", - "loa": { "current": 3 }, - "first_name": "John", - "middle_name": "", - "last_name": "Doe", - "gender": "M", - "birth_date": "1985-01-01", - "verified": true - }, - "veteran_status": { - "status": "OK", - "is_veteran": true, - "served_in_military": true - }, - "in_progress_forms": [], - "prefills_available": [], - "services": [ - "facilities", - "hca", - "edu-benefits", - "evss-claims", - "form526", - "user-profile", - "health-records", - "rx", - "messaging" - ], - "va_profile": { - "status": "OK", - "birth_date": "19850101", - "family_name": "Doe", - "gender": "M", - "given_names": ["John", ""], - "active_status": "active", - "facilities": [ - { - "facility_id": "983", - "is_cerner": false - }, - { - "facility_id": "984", - "is_cerner": false - } - ] - } - } - }, - "meta": { "errors": null } -} diff --git a/src/applications/user-testing/new-conditions/tests/new-conditions.cypress.spec.js b/src/applications/user-testing/new-conditions/tests/new-conditions.cypress.spec.js deleted file mode 100644 index 906270a8aace..000000000000 --- a/src/applications/user-testing/new-conditions/tests/new-conditions.cypress.spec.js +++ /dev/null @@ -1,37 +0,0 @@ -import path from 'path'; -import testForm from 'platform/testing/e2e/cypress/support/form-tester'; -import { createTestConfig } from 'platform/testing/e2e/cypress/support/form-tester/utilities'; -import mockUser from './fixtures/mocks/user.json'; -import formConfig from '../config/form'; -import manifest from '../manifest.json'; - -const testConfig = createTestConfig( - { - dataPrefix: 'data', - dataDir: path.join(__dirname, 'fixtures', 'data'), - dataSets: ['minimal-test'], - pageHooks: { - introduction: ({ afterHook }) => { - afterHook(() => { - cy.findAllByText(/^start/i, { selector: 'a[href="#start"]' }) - .last() - .click({ force: true }); - }); - }, - }, - - setupPerTest: () => { - cy.intercept('GET', '/v0/user', mockUser); - cy.intercept('POST', formConfig.submitUrl, { status: 200 }); - cy.login(mockUser); - }, - - // Skip tests in CI until the form is released. - // Remove this setting when the form has a content page in production. - skip: Cypress.env('CI'), - }, - manifest, - formConfig, -); - -testForm(testConfig); From c1160e66199424569a21feb5df7e7b338642e054 Mon Sep 17 00:00:00 2001 From: William Phelps Date: Fri, 15 Nov 2024 12:34:16 -0800 Subject: [PATCH 3/4] Update folder urls to include demo name and update folders and form key to align with demo name --- .../new-conditions/config/form.js | 8 +-- .../user-testing/new-conditions/constants.js | 4 +- .../new-conditions/pages/chooseDemo.js | 7 ++- .../cause.js | 0 .../causeFollowUp.js | 10 ++-- .../condition.js | 2 +- .../date.js | 0 .../index.js | 51 ++++++++++--------- .../intro.js | 0 .../sideOfBody.js | 0 .../summary.js | 0 .../utils.js | 6 +-- .../condition.js | 2 +- .../date.js | 0 .../index.js | 37 +++++++------- .../intro.js | 0 .../sideOfBody.js | 0 .../summary.js | 0 .../utils.js | 6 +-- 19 files changed, 71 insertions(+), 62 deletions(-) rename src/applications/user-testing/new-conditions/pages/{newConditionsPages => conditionByConditionPages}/cause.js (100%) rename src/applications/user-testing/new-conditions/pages/{newConditionsPages => conditionByConditionPages}/causeFollowUp.js (95%) rename src/applications/user-testing/new-conditions/pages/{newConditionsPages => conditionByConditionPages}/condition.js (98%) rename src/applications/user-testing/new-conditions/pages/{newConditionsJustConditionsPages => conditionByConditionPages}/date.js (100%) rename src/applications/user-testing/new-conditions/pages/{newConditionsPages => conditionByConditionPages}/index.js (60%) rename src/applications/user-testing/new-conditions/pages/{newConditionsJustConditionsPages => conditionByConditionPages}/intro.js (100%) rename src/applications/user-testing/new-conditions/pages/{newConditionsJustConditionsPages => conditionByConditionPages}/sideOfBody.js (100%) rename src/applications/user-testing/new-conditions/pages/{newConditionsJustConditionsPages => conditionByConditionPages}/summary.js (100%) rename src/applications/user-testing/new-conditions/pages/{newConditionsPages => conditionByConditionPages}/utils.js (94%) rename src/applications/user-testing/new-conditions/pages/{newConditionsJustConditionsPages => conditionsFirstPages}/condition.js (98%) rename src/applications/user-testing/new-conditions/pages/{newConditionsPages => conditionsFirstPages}/date.js (100%) rename src/applications/user-testing/new-conditions/pages/{newConditionsJustConditionsPages => conditionsFirstPages}/index.js (65%) rename src/applications/user-testing/new-conditions/pages/{newConditionsPages => conditionsFirstPages}/intro.js (100%) rename src/applications/user-testing/new-conditions/pages/{newConditionsPages => conditionsFirstPages}/sideOfBody.js (100%) rename src/applications/user-testing/new-conditions/pages/{newConditionsPages => conditionsFirstPages}/summary.js (100%) rename src/applications/user-testing/new-conditions/pages/{newConditionsJustConditionsPages => conditionsFirstPages}/utils.js (92%) diff --git a/src/applications/user-testing/new-conditions/config/form.js b/src/applications/user-testing/new-conditions/config/form.js index 3d7d151e30b6..712c530e13d4 100644 --- a/src/applications/user-testing/new-conditions/config/form.js +++ b/src/applications/user-testing/new-conditions/config/form.js @@ -7,8 +7,8 @@ import IntroductionPage from '../containers/IntroductionPage'; import manifest from '../manifest.json'; import chooseDemo from '../pages/chooseDemo'; -import newConditionsJustConditionsPages from '../pages/newConditionsJustConditionsPages'; -import newConditionsPages from '../pages/newConditionsPages'; +import conditionByConditionPages from '../pages/conditionByConditionPages'; +import conditionsFirstPages from '../pages/conditionsFirstPages'; const FormFooter = () => (
@@ -68,8 +68,8 @@ const formConfig = { title: 'Conditions', pages: { chooseDemo, - ...newConditionsPages, - ...newConditionsJustConditionsPages, + ...conditionByConditionPages, + ...conditionsFirstPages, }, }, }, diff --git a/src/applications/user-testing/new-conditions/constants.js b/src/applications/user-testing/new-conditions/constants.js index 877ed5a742c4..7719548817e0 100644 --- a/src/applications/user-testing/new-conditions/constants.js +++ b/src/applications/user-testing/new-conditions/constants.js @@ -1,2 +1,4 @@ -export const TITLE = 'Demo: File for disability compensation'; +export const TITLE = 'Demo: File for disability compensation - New conditions'; export const SUBTITLE = 'Demo of VA Form 21-526EZ'; +export const CONDITION_BY_CONDITION = 'ape'; +export const CONDITIONS_FIRST = 'elk'; diff --git a/src/applications/user-testing/new-conditions/pages/chooseDemo.js b/src/applications/user-testing/new-conditions/pages/chooseDemo.js index b5871ab60a03..cdba25c4e1cd 100644 --- a/src/applications/user-testing/new-conditions/pages/chooseDemo.js +++ b/src/applications/user-testing/new-conditions/pages/chooseDemo.js @@ -2,10 +2,13 @@ import { radioSchema, radioUI, } from 'platform/forms-system/src/js/web-component-patterns'; +import capitalize from 'lodash/capitalize'; + +import { CONDITION_BY_CONDITION, CONDITIONS_FIRST } from '../constants'; const demoOptions = { - newConditions: 'Owl', - newConditionsJustConditions: 'Fox', + CONDITION_BY_CONDITION: capitalize(CONDITION_BY_CONDITION), + CONDITIONS_FIRST: capitalize(CONDITIONS_FIRST), }; /** @type {PageSchema} */ diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsPages/cause.js b/src/applications/user-testing/new-conditions/pages/conditionByConditionPages/cause.js similarity index 100% rename from src/applications/user-testing/new-conditions/pages/newConditionsPages/cause.js rename to src/applications/user-testing/new-conditions/pages/conditionByConditionPages/cause.js diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsPages/causeFollowUp.js b/src/applications/user-testing/new-conditions/pages/conditionByConditionPages/causeFollowUp.js similarity index 95% rename from src/applications/user-testing/new-conditions/pages/newConditionsPages/causeFollowUp.js rename to src/applications/user-testing/new-conditions/pages/conditionByConditionPages/causeFollowUp.js index fefbbbcfc897..767654927f4a 100644 --- a/src/applications/user-testing/new-conditions/pages/newConditionsPages/causeFollowUp.js +++ b/src/applications/user-testing/new-conditions/pages/conditionByConditionPages/causeFollowUp.js @@ -31,22 +31,22 @@ const createCauseFollowUpTitles = formData => { // TODO: Fix formData so that shape is consistent // formData changes shape between add and edit which results in the need for the conditional below const createCauseFollowUpConditional = (formData, index, causeType) => { - const cause = formData?.newConditions - ? formData.newConditions[index]?.cause + const cause = formData?.conditionByCondition + ? formData.conditionByCondition[index]?.cause : formData.cause; return cause !== causeType; }; // TODO: Fix causedByCondition functionality on edit -// formData on add { "newConditions": [{ "condition": "migraines (headaches)"... }] } -// formData on edit { "condition": "migraines (headaches)"... } - does not include ratedDisabilities or other newConditions +// formData on add { "conditionByCondition": [{ "condition": "migraines (headaches)"... }] } +// formData on edit { "condition": "migraines (headaches)"... } - does not include ratedDisabilities or other new conditions // TODO: If causedByCondition is 'asthma' asthma is updated to 'emphysema' ensure 'asthma' is cleared as potential cause const getOtherConditions = (formData, currentIndex) => { const ratedDisabilities = formData?.ratedDisabilities?.map(disability => disability.name) || []; const otherNewConditions = - formData?.newConditions + formData?.conditionByCondition ?.filter((_, index) => index !== currentIndex) ?.map(condition => createItemName(condition)) || []; diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsPages/condition.js b/src/applications/user-testing/new-conditions/pages/conditionByConditionPages/condition.js similarity index 98% rename from src/applications/user-testing/new-conditions/pages/newConditionsPages/condition.js rename to src/applications/user-testing/new-conditions/pages/conditionByConditionPages/condition.js index d853f5530806..c8c9623a1603 100644 --- a/src/applications/user-testing/new-conditions/pages/newConditionsPages/condition.js +++ b/src/applications/user-testing/new-conditions/pages/conditionByConditionPages/condition.js @@ -36,7 +36,7 @@ const validateNotMissing = (err, fieldData) => { const validateNotDuplicate = (err, fieldData, formData) => { const currentList = - formData?.newConditions?.map(condition => + formData?.conditionByCondition?.map(condition => condition.condition?.toLowerCase(), ) || []; const itemLowerCased = fieldData?.toLowerCase() || ''; diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/date.js b/src/applications/user-testing/new-conditions/pages/conditionByConditionPages/date.js similarity index 100% rename from src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/date.js rename to src/applications/user-testing/new-conditions/pages/conditionByConditionPages/date.js diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsPages/index.js b/src/applications/user-testing/new-conditions/pages/conditionByConditionPages/index.js similarity index 60% rename from src/applications/user-testing/new-conditions/pages/newConditionsPages/index.js rename to src/applications/user-testing/new-conditions/pages/conditionByConditionPages/index.js index 92471226126a..2e23a9f7c37d 100644 --- a/src/applications/user-testing/new-conditions/pages/newConditionsPages/index.js +++ b/src/applications/user-testing/new-conditions/pages/conditionByConditionPages/index.js @@ -10,28 +10,29 @@ import introPage from './intro'; import sideOfBodyPage from './sideOfBody'; import summaryPage from './summary'; import { arrayBuilderOptions, hasSideOfBody } from './utils'; +import { CONDITION_BY_CONDITION } from '../../constants'; -const newConditionsPages = arrayBuilderPages( +const conditionByConditionPages = arrayBuilderPages( arrayBuilderOptions, (pageBuilder, helpers) => ({ - newConditionsIntro: pageBuilder.introPage({ + conditionByConditionIntro: pageBuilder.introPage({ title: 'New conditions intro', - path: 'new-conditions-intro', - depends: formData => formData.demo === 'newConditions', + path: `new-conditions-${CONDITION_BY_CONDITION}-intro`, + depends: formData => formData.demo === 'CONDITION_BY_CONDITION', uiSchema: introPage.uiSchema, schema: introPage.schema, }), - newConditionsSummary: pageBuilder.summaryPage({ + conditionByConditionSummary: pageBuilder.summaryPage({ title: 'Review your new conditions', - path: 'new-conditions-summary', - depends: formData => formData.demo === 'newConditions', + path: `new-conditions-${CONDITION_BY_CONDITION}-summary`, + depends: formData => formData.demo === 'CONDITION_BY_CONDITION', uiSchema: summaryPage.uiSchema, schema: summaryPage.schema, }), - newConditionsCondition: pageBuilder.itemPage({ + conditionByConditionCondition: pageBuilder.itemPage({ title: 'Claim a new condition', - path: 'new-conditions/:index/condition', - depends: formData => formData.demo === 'newConditions', + path: `new-conditions-${CONDITION_BY_CONDITION}/:index/condition`, + depends: formData => formData.demo === 'CONDITION_BY_CONDITION', uiSchema: conditionPage.uiSchema, schema: conditionPage.schema, onNavForward: props => { @@ -45,20 +46,22 @@ const newConditionsPages = arrayBuilderPages( return hasSideOfBody(formData, index) ? helpers.navForwardKeepUrlParams(props) - : goPath(`new-conditions/${index}/date${urlParamsString}`); + : goPath( + `new-conditions-${CONDITION_BY_CONDITION}/${index}/date${urlParamsString}`, + ); }, }), - newConditionsSideOfBody: pageBuilder.itemPage({ + conditionByConditionSideOfBody: pageBuilder.itemPage({ title: 'Side of body of new condition', - path: 'new-conditions/:index/side-of-body', - depends: formData => formData.demo === 'newConditions', + path: `new-conditions-${CONDITION_BY_CONDITION}/:index/side-of-body`, + depends: formData => formData.demo === 'CONDITION_BY_CONDITION', uiSchema: sideOfBodyPage.uiSchema, schema: sideOfBodyPage.schema, }), - newConditionsDate: pageBuilder.itemPage({ + conditionByConditionDate: pageBuilder.itemPage({ title: 'Date of new condition', - path: 'new-conditions/:index/date', - depends: formData => formData.demo === 'newConditions', + path: `new-conditions-${CONDITION_BY_CONDITION}/:index/date`, + depends: formData => formData.demo === 'CONDITION_BY_CONDITION', uiSchema: datePage.uiSchema, schema: datePage.schema, onNavBack: props => { @@ -71,21 +74,21 @@ const newConditionsPages = arrayBuilderPages( : goPath(`new-conditions/${index}/condition${urlParamsString}`); }, }), - newConditionsCause: pageBuilder.itemPage({ + conditionByConditionCause: pageBuilder.itemPage({ title: 'Cause of new condition', - path: 'new-conditions/:index/cause', - depends: formData => formData.demo === 'newConditions', + path: `new-conditions-${CONDITION_BY_CONDITION}/:index/cause`, + depends: formData => formData.demo === 'CONDITION_BY_CONDITION', uiSchema: causePage.uiSchema, schema: causePage.schema, }), - newConditionsCauseFollowUp: pageBuilder.itemPage({ + conditionByConditionCauseFollowUp: pageBuilder.itemPage({ title: 'Cause follow up for new condition', - path: 'new-conditions/:index/cause-follow-up', - depends: formData => formData.demo === 'newConditions', + path: `new-conditions-${CONDITION_BY_CONDITION}/:index/cause-follow-up`, + depends: formData => formData.demo === 'CONDITION_BY_CONDITION', uiSchema: causeFollowUpPage.uiSchema, schema: causeFollowUpPage.schema, }), }), ); -export default newConditionsPages; +export default conditionByConditionPages; diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/intro.js b/src/applications/user-testing/new-conditions/pages/conditionByConditionPages/intro.js similarity index 100% rename from src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/intro.js rename to src/applications/user-testing/new-conditions/pages/conditionByConditionPages/intro.js diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/sideOfBody.js b/src/applications/user-testing/new-conditions/pages/conditionByConditionPages/sideOfBody.js similarity index 100% rename from src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/sideOfBody.js rename to src/applications/user-testing/new-conditions/pages/conditionByConditionPages/sideOfBody.js diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/summary.js b/src/applications/user-testing/new-conditions/pages/conditionByConditionPages/summary.js similarity index 100% rename from src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/summary.js rename to src/applications/user-testing/new-conditions/pages/conditionByConditionPages/summary.js diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsPages/utils.js b/src/applications/user-testing/new-conditions/pages/conditionByConditionPages/utils.js similarity index 94% rename from src/applications/user-testing/new-conditions/pages/newConditionsPages/utils.js rename to src/applications/user-testing/new-conditions/pages/conditionByConditionPages/utils.js index 63a0ba9509bb..ed838dba8f4d 100644 --- a/src/applications/user-testing/new-conditions/pages/newConditionsPages/utils.js +++ b/src/applications/user-testing/new-conditions/pages/conditionByConditionPages/utils.js @@ -13,8 +13,8 @@ export const createTitle = (defaultTitle, editTitle) => { }; export const hasSideOfBody = (formData, index) => { - const condition = formData?.newConditions - ? formData.newConditions[index]?.condition + const condition = formData?.conditionByCondition + ? formData.conditionByCondition[index]?.condition : formData.condition; const conditionObject = conditionObjects.find( @@ -62,7 +62,7 @@ export const createItemName = (item, capFirstLetter = false) => { /** @type {ArrayBuilderOptions} */ export const arrayBuilderOptions = { - arrayPath: 'newConditions', + arrayPath: 'conditionByCondition', nounSingular: 'condition', nounPlural: 'conditions', required: true, diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/condition.js b/src/applications/user-testing/new-conditions/pages/conditionsFirstPages/condition.js similarity index 98% rename from src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/condition.js rename to src/applications/user-testing/new-conditions/pages/conditionsFirstPages/condition.js index d853f5530806..c2d030f067f3 100644 --- a/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/condition.js +++ b/src/applications/user-testing/new-conditions/pages/conditionsFirstPages/condition.js @@ -36,7 +36,7 @@ const validateNotMissing = (err, fieldData) => { const validateNotDuplicate = (err, fieldData, formData) => { const currentList = - formData?.newConditions?.map(condition => + formData?.conditionsFirst?.map(condition => condition.condition?.toLowerCase(), ) || []; const itemLowerCased = fieldData?.toLowerCase() || ''; diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsPages/date.js b/src/applications/user-testing/new-conditions/pages/conditionsFirstPages/date.js similarity index 100% rename from src/applications/user-testing/new-conditions/pages/newConditionsPages/date.js rename to src/applications/user-testing/new-conditions/pages/conditionsFirstPages/date.js diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/index.js b/src/applications/user-testing/new-conditions/pages/conditionsFirstPages/index.js similarity index 65% rename from src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/index.js rename to src/applications/user-testing/new-conditions/pages/conditionsFirstPages/index.js index 7939ad64f30c..636238c669ed 100644 --- a/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/index.js +++ b/src/applications/user-testing/new-conditions/pages/conditionsFirstPages/index.js @@ -8,28 +8,29 @@ import introPage from './intro'; import sideOfBodyPage from './sideOfBody'; import summaryPage from './summary'; import { arrayBuilderOptions, hasSideOfBody } from './utils'; +import { CONDITIONS_FIRST } from '../../constants'; -const newConditionsJustConditionsPages = arrayBuilderPages( +const conditionsFirstPages = arrayBuilderPages( arrayBuilderOptions, (pageBuilder, helpers) => ({ - newConditionsJustConditionsIntro: pageBuilder.introPage({ + conditionsFirstIntro: pageBuilder.introPage({ title: 'New conditions intro', - path: 'new-conditions-just-conditions-intro', - depends: formData => formData.demo === 'newConditionsJustConditions', + path: `new-conditions-${CONDITIONS_FIRST}-intro`, + depends: formData => formData.demo === 'CONDITIONS_FIRST', uiSchema: introPage.uiSchema, schema: introPage.schema, }), - newConditionsJustConditionsSummary: pageBuilder.summaryPage({ + conditionsFirstSummary: pageBuilder.summaryPage({ title: 'Review your new conditions', - path: 'new-conditions-just-conditions-summary', - depends: formData => formData.demo === 'newConditionsJustConditions', + path: `new-conditions-${CONDITIONS_FIRST}-summary`, + depends: formData => formData.demo === 'CONDITIONS_FIRST', uiSchema: summaryPage.uiSchema, schema: summaryPage.schema, }), - newConditionsJustConditionsCondition: pageBuilder.itemPage({ + conditionsFirstCondition: pageBuilder.itemPage({ title: 'Claim a new condition', - path: 'new-conditions-just-conditions/:index/condition', - depends: formData => formData.demo === 'newConditionsJustConditions', + path: `new-conditions-${CONDITIONS_FIRST}/:index/condition`, + depends: formData => formData.demo === 'CONDITIONS_FIRST', uiSchema: conditionPage.uiSchema, schema: conditionPage.schema, onNavForward: props => { @@ -44,21 +45,21 @@ const newConditionsJustConditionsPages = arrayBuilderPages( return hasSideOfBody(formData, index) ? helpers.navForwardKeepUrlParams(props) : goPath( - `new-conditions-just-conditions/${index}/date${urlParamsString}`, + `new-conditions-${CONDITIONS_FIRST}/${index}/date${urlParamsString}`, ); }, }), - newConditionsJustConditionsSideOfBody: pageBuilder.itemPage({ + conditionsFirstSideOfBody: pageBuilder.itemPage({ title: 'Side of body of new condition', - path: 'new-conditions-just-conditions/:index/side-of-body', - depends: formData => formData.demo === 'newConditionsJustConditions', + path: `new-conditions-${CONDITIONS_FIRST}/:index/side-of-body`, + depends: formData => formData.demo === 'CONDITIONS_FIRST', uiSchema: sideOfBodyPage.uiSchema, schema: sideOfBodyPage.schema, }), newConditionsJustConditionsDate: pageBuilder.itemPage({ title: 'Date of new condition', - path: 'new-conditions-just-conditions/:index/date', - depends: formData => formData.demo === 'newConditionsJustConditions', + path: `new-conditions-${CONDITIONS_FIRST}/:index/date`, + depends: formData => formData.demo === 'CONDITIONS_FIRST', uiSchema: datePage.uiSchema, schema: datePage.schema, onNavBack: props => { @@ -69,11 +70,11 @@ const newConditionsJustConditionsPages = arrayBuilderPages( return hasSideOfBody(formData, index) ? helpers.navBackKeepUrlParams(props) : goPath( - `new-conditions-just-conditions/${index}/condition${urlParamsString}`, + `new-conditions-${CONDITIONS_FIRST}/${index}/condition${urlParamsString}`, ); }, }), }), ); -export default newConditionsJustConditionsPages; +export default conditionsFirstPages; diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsPages/intro.js b/src/applications/user-testing/new-conditions/pages/conditionsFirstPages/intro.js similarity index 100% rename from src/applications/user-testing/new-conditions/pages/newConditionsPages/intro.js rename to src/applications/user-testing/new-conditions/pages/conditionsFirstPages/intro.js diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsPages/sideOfBody.js b/src/applications/user-testing/new-conditions/pages/conditionsFirstPages/sideOfBody.js similarity index 100% rename from src/applications/user-testing/new-conditions/pages/newConditionsPages/sideOfBody.js rename to src/applications/user-testing/new-conditions/pages/conditionsFirstPages/sideOfBody.js diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsPages/summary.js b/src/applications/user-testing/new-conditions/pages/conditionsFirstPages/summary.js similarity index 100% rename from src/applications/user-testing/new-conditions/pages/newConditionsPages/summary.js rename to src/applications/user-testing/new-conditions/pages/conditionsFirstPages/summary.js diff --git a/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/utils.js b/src/applications/user-testing/new-conditions/pages/conditionsFirstPages/utils.js similarity index 92% rename from src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/utils.js rename to src/applications/user-testing/new-conditions/pages/conditionsFirstPages/utils.js index 3468a4f21f97..2edfa89e3872 100644 --- a/src/applications/user-testing/new-conditions/pages/newConditionsJustConditionsPages/utils.js +++ b/src/applications/user-testing/new-conditions/pages/conditionsFirstPages/utils.js @@ -13,8 +13,8 @@ export const createTitle = (defaultTitle, editTitle) => { }; export const hasSideOfBody = (formData, index) => { - const condition = formData?.newConditions - ? formData.newConditions[index]?.condition + const condition = formData?.conditionsFirst + ? formData.conditionsFirst[index]?.condition : formData.condition; const conditionObject = conditionObjects.find( @@ -43,7 +43,7 @@ export const createItemName = (item, capFirstLetter = false) => { /** @type {ArrayBuilderOptions} */ export const arrayBuilderOptions = { - arrayPath: 'newConditions', + arrayPath: 'conditionsFirst', nounSingular: 'condition', nounPlural: 'conditions', required: true, From cd03b32040b993bc431c954bbe8c8fed6172533d Mon Sep 17 00:00:00 2001 From: William Phelps Date: Fri, 15 Nov 2024 12:35:17 -0800 Subject: [PATCH 4/4] Update appName to include "Demo" and "New conditions" update url to "/user-testing/new-conditions" --- src/applications/user-testing/new-conditions/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/applications/user-testing/new-conditions/manifest.json b/src/applications/user-testing/new-conditions/manifest.json index 3b8fd5a10132..9f3d67d20d82 100644 --- a/src/applications/user-testing/new-conditions/manifest.json +++ b/src/applications/user-testing/new-conditions/manifest.json @@ -1,7 +1,7 @@ { - "appName": "File for disability compensation", + "appName": "Demo: File for disability compensation - New conditions", "entryFile": "./app-entry.jsx", "entryName": "new-conditions", - "rootUrl": "/user-testing", + "rootUrl": "/user-testing/new-conditions", "productId": "0db1a222-9d73-4c26-9966-75283e6510a1" }