diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 59d6eb294b3c..aa6b51940694 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -155,6 +155,7 @@ src/applications/static-pages/health-care-manage-benefits/view-test-and-lab-resu src/applications/facility-locator @department-of-veterans-affairs/vfs-facilities-frontend @department-of-veterans-affairs/va-platform-cop-frontend src/applications/static-pages/facilities @department-of-veterans-affairs/vfs-facilities-frontend @department-of-veterans-affairs/va-platform-cop-frontend src/applications/static-pages/tests/facilities @department-of-veterans-affairs/vfs-facilities-frontend @department-of-veterans-affairs/va-platform-cop-frontend +src/applications/static-pages/situation-updates-banner @department-of-veterans-affairs/vfs-facilities-frontend @department-of-veterans-affairs/va-platform-cop-frontend # Caregiver diff --git a/script/watch.js b/script/watch.js index aee70cf27a21..d2a854d53787 100644 --- a/script/watch.js +++ b/script/watch.js @@ -2,7 +2,7 @@ const argv = require('minimist')(process.argv.slice(2)); const printBuildHelp = require('./build-help'); const { runCommand } = require('./utils'); -// Preset memory options 1gb -> 8gb +// Preset memory options 1gb -> 12gb const memoryOptions = [1024, 2048, 3072, 4096, 5120, 6144, 7168, 8192, 12288]; // Caching the input memory arg diff --git a/src/applications/ivc-champva/10-10D/config/constants.js b/src/applications/ivc-champva/10-10D/config/constants.js index 6febf1de17f4..8c1ea16dc68a 100644 --- a/src/applications/ivc-champva/10-10D/config/constants.js +++ b/src/applications/ivc-champva/10-10D/config/constants.js @@ -51,5 +51,22 @@ export const OPTIONAL_FILES = { 'Proof of Legal Separation from Marriage Or Legal Union to Other', }; +// The backend needs this list so that it can properly match the attachmentId +// on a per applicant basis to the temporary cache files that have been uploaded +// See: https://github.com/department-of-veterans-affairs/va.gov-team/issues/96358 +export const FILE_UPLOAD_ORDER = [ + 'applicantBirthCertOrSocialSecCard', + 'applicantAdoptionPapers', + 'applicantStepMarriageCert', + 'applicantSchoolCert', + 'applicantHelplessCert', + 'applicantRemarriageCert', + 'applicantMedicarePartAPartBCard', + 'applicantMedicarePartDCard', + 'applicantMedicareIneligibleProof', + 'applicantOhiCard', + 'applicantOtherInsuranceCertification', +]; + export const ADDITIONAL_FILES_HINT = 'Depending on your response, you may need to submit additional documents with this application.'; diff --git a/src/applications/ivc-champva/10-10D/config/submitTransformer.js b/src/applications/ivc-champva/10-10D/config/submitTransformer.js index c1af3461484c..795663751ec4 100644 --- a/src/applications/ivc-champva/10-10D/config/submitTransformer.js +++ b/src/applications/ivc-champva/10-10D/config/submitTransformer.js @@ -1,5 +1,5 @@ import { transformForSubmit as formsSystemTransformForSubmit } from 'platform/forms-system/src/js/helpers'; -import { REQUIRED_FILES, OPTIONAL_FILES } from './constants'; +import { FILE_UPLOAD_ORDER } from './constants'; import { adjustYearString, concatStreets, @@ -53,13 +53,9 @@ function transformApplicants(applicants) { ), // Grab any file upload properties from this applicant and combine into a // supporting documents array: - applicantSupportingDocuments: Object.keys({ - ...REQUIRED_FILES, - ...OPTIONAL_FILES, - }) - .filter(k => k.includes('applicant')) // Ignore sponsor files - .map(f => app?.[f]) // Grab the upload obj from top-level in applicant - .filter(el => el), // Drop any undefineds/nulls + applicantSupportingDocuments: FILE_UPLOAD_ORDER.map( + property => app?.[property], + ).filter(el => el), // Drop any undefineds/nulls }; transformedApp = adjustYearString(transformedApp); transformedApp.applicantAddress = concatStreets( diff --git a/src/applications/ivc-champva/10-10D/tests/unit/components/File/MissingFileOverview.unit.spec.js b/src/applications/ivc-champva/10-10D/tests/unit/components/File/MissingFileOverview.unit.spec.js index 71817250ce17..1f6a8caf8985 100644 --- a/src/applications/ivc-champva/10-10D/tests/unit/components/File/MissingFileOverview.unit.spec.js +++ b/src/applications/ivc-champva/10-10D/tests/unit/components/File/MissingFileOverview.unit.spec.js @@ -4,7 +4,10 @@ import { Provider } from 'react-redux'; import { expect } from 'chai'; import SupportingDocumentsPage from '../../../../pages/SupportingDocumentsPage'; import { MissingFileConsentPage } from '../../../../pages/MissingFileConsentPage'; -import { REQUIRED_FILES } from '../../../../config/constants'; +import { + REQUIRED_FILES, + FILE_UPLOAD_ORDER, +} from '../../../../config/constants'; import { testComponentRender, getProps, @@ -14,10 +17,52 @@ import { checkFlags, } from '../../../../../shared/components/fileUploads/MissingFileOverview'; import MissingFileList from '../../../../../shared/components/fileUploads/MissingFileList'; +import { getAllPages } from '../../../../../shared/tests/helpers'; +import SupportingDocsVerification from '../../../../../shared/components/fileUploads/supportingDocsVerification'; import formConfig from '../../../../config/form'; import mockData from '../../../e2e/fixtures/data/test-data.json'; +describe('FILE_UPLOAD_ORDER constant', () => { + it('should match order of file upload fields present in formConfig', () => { + /* + NOTE: FILE_UPLOAD_ORDER must be manually updated if the order + of file uploads in `formConfig` ever changes. This test serves to + make sure that happens. The backend relies on this list to properly + map metadata to the tmp files generated when users upload docs. + */ + + const verifier = new SupportingDocsVerification([]); + // We want this `FILE_UPLOAD_ORDER` to match what we pull from formConfig. + // This helper produces a list like: + // ['applicantBirthCertOrSocialSecCard','applicantAdoptionPapers', ...] + const generatedArr = verifier + .getApplicantFileKeyNames(getAllPages(formConfig)) + .map(el => el.name); + + let orderIsSame = true; + generatedArr.forEach((el, idx) => { + if (FILE_UPLOAD_ORDER[idx] !== el) { + orderIsSame = false; + } + }); + + expect( + orderIsSame, + `Expected FILE_UPLOAD_ORDER array: + + ${FILE_UPLOAD_ORDER.join('\n\t')} + + to have same order as defined in formConfig: + + ${generatedArr.join('\n\t')} + + Please verify that FILE_UPLOAD_ORDER matches the order of file upload properties defined in formConfig. + `, + ).to.be.true; + }); +}); + describe('hasReq', () => { const data = { applicants: [ diff --git a/src/applications/letters/components/DownloadLetterLink.jsx b/src/applications/letters/components/DownloadLetterLink.jsx index 2d0239771c95..d68acc723799 100644 --- a/src/applications/letters/components/DownloadLetterLink.jsx +++ b/src/applications/letters/components/DownloadLetterLink.jsx @@ -30,7 +30,7 @@ export class DownloadLetterLink extends React.Component { render() { let buttonText; - let buttonDisabled; + let buttonDisabled; // false causes MS Voice Access to ignore buttons let message; switch (this.props.downloadStatus) { case DOWNLOAD_STATUSES.downloading: @@ -39,7 +39,7 @@ export class DownloadLetterLink extends React.Component { break; case DOWNLOAD_STATUSES.success: buttonText = `${this.props.letterName} (PDF)`; - buttonDisabled = false; + buttonDisabled = undefined; message = (

Your letter has successfully downloaded.

@@ -52,7 +52,7 @@ export class DownloadLetterLink extends React.Component { break; case DOWNLOAD_STATUSES.failure: buttonText = 'Retry download'; - buttonDisabled = false; + buttonDisabled = undefined; message = (

Your letter didn’t download.

@@ -65,7 +65,7 @@ export class DownloadLetterLink extends React.Component { break; default: buttonText = `${this.props.letterName} (PDF)`; - buttonDisabled = false; + buttonDisabled = undefined; } return ( diff --git a/src/applications/letters/components/NoAddressBanner.jsx b/src/applications/letters/components/NoAddressBanner.jsx index 41ac9e80e726..6b22a5f9b741 100644 --- a/src/applications/letters/components/NoAddressBanner.jsx +++ b/src/applications/letters/components/NoAddressBanner.jsx @@ -3,7 +3,7 @@ import React from 'react'; export default function NoAddressBanner() { return ( <> - +

We don’t have a valid address on file for you

You’ll need to{' '} diff --git a/src/applications/letters/containers/AddressSection.jsx b/src/applications/letters/containers/AddressSection.jsx index 3b58d0ef8156..55efec3652be 100644 --- a/src/applications/letters/containers/AddressSection.jsx +++ b/src/applications/letters/containers/AddressSection.jsx @@ -39,8 +39,8 @@ export function AddressSection({ address }) { navigateToLetterList(navigate)} />
@@ -55,9 +55,7 @@ export function AddressSection({ address }) { return ( <> -
- {emptyAddress ? : addressContent} -
+ {emptyAddress ? : addressContent} {viewLettersButton} ); diff --git a/src/applications/letters/containers/LetterList.jsx b/src/applications/letters/containers/LetterList.jsx index c47cc7202480..261a0df3c6ae 100644 --- a/src/applications/letters/containers/LetterList.jsx +++ b/src/applications/letters/containers/LetterList.jsx @@ -95,7 +95,7 @@ export class LetterList extends React.Component { } return ( -
+

To see an explanation about each letter, click on the (+) to expand the box. After you expand the box, you’ll be given the option to diff --git a/src/applications/letters/sass/letters.scss b/src/applications/letters/sass/letters.scss index fd1dc9169bd7..39b6a24544d9 100644 --- a/src/applications/letters/sass/letters.scss +++ b/src/applications/letters/sass/letters.scss @@ -149,6 +149,7 @@ } .form-expanding-group { + margin-bottom: 1.5em; margin-left: initial; margin-top: 1.5em; } diff --git a/src/applications/letters/tests/01-authed.cypress.spec.js b/src/applications/letters/tests/01-authed.cypress.spec.js index 69d1f39994a1..6436019679e7 100644 --- a/src/applications/letters/tests/01-authed.cypress.spec.js +++ b/src/applications/letters/tests/01-authed.cypress.spec.js @@ -35,6 +35,11 @@ describe('Authed Letter Test', () => { cy.axeCheck(); + cy.get('[data-cy="view-letters-button"]') + .shadow() + .find('button.usa-button') + .should('not.have.attr', 'aria-disabled'); // Check for MS Voice Access usability + cy.get('[data-cy="view-letters-button"]') .click() .then(() => { diff --git a/src/applications/letters/tests/containers/AddressSection.unit.spec.jsx b/src/applications/letters/tests/containers/AddressSection.unit.spec.jsx index c809bcb412f9..903051919216 100644 --- a/src/applications/letters/tests/containers/AddressSection.unit.spec.jsx +++ b/src/applications/letters/tests/containers/AddressSection.unit.spec.jsx @@ -45,7 +45,7 @@ describe('', () => { stub.restore(); }); - it('should enable the View Letters button with default props', () => { + it('should enable the View letters button with default props', () => { const { container } = render( @@ -53,12 +53,12 @@ describe('', () => { ); expect($('va-button', container).getAttribute('text')).to.eq( - 'View Letters', + 'View letters', ); - expect($('va-button', container).getAttribute('disabled')).to.eq('false'); + expect($('va-button', container).getAttribute('disabled')).to.be.null; }); - it('should render an empty address warning on the view screen and disable the View Letters button', () => { + it('should render an empty address warning on the view screen and disable the View letters button', () => { const { container, getByText } = render( @@ -67,7 +67,7 @@ describe('', () => { expect(getByText('We don’t have a valid address on file for you').exist); expect($('va-button', container).getAttribute('text')).to.eq( - 'View Letters', + 'View letters', ); expect($('va-button', container).getAttribute('disabled')).to.eq('true'); }); diff --git a/src/applications/static-pages/situation-updates-banner/createSituationUpdatesBanner.jsx b/src/applications/static-pages/situation-updates-banner/createSituationUpdatesBanner.jsx new file mode 100644 index 000000000000..326c302b9e3c --- /dev/null +++ b/src/applications/static-pages/situation-updates-banner/createSituationUpdatesBanner.jsx @@ -0,0 +1,54 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Provider } from 'react-redux'; +import { useFeatureToggle } from '~/platform/utilities/feature-toggles'; +import SituationUpdateBanner from './situationUpdateBanner'; + +export const BannerContainer = () => { + const { + TOGGLE_NAMES, + useToggleValue, + useToggleLoadingValue, + } = useFeatureToggle(); + + const alternativeBannersEnabled = useToggleValue( + TOGGLE_NAMES.bannerUseAlternativeBanners, + ); + const isLoadingFeatureFlags = useToggleLoadingValue(); + + if (isLoadingFeatureFlags || !alternativeBannersEnabled) { + return null; + } + + const defaultProps = { + id: '1', + bundle: 'situation-updates', + headline: 'Situation update', + alertType: 'warning', + content: + "We're having issues at this location. Please avoid this facility until further notice.", + context: 'global', + showClose: true, + operatingStatusCTA: false, + emailUpdatesButton: false, + findFacilitiesCTA: false, + limitSubpageInheritance: false, + }; + + return ; +}; + +export default async function createSituationUpdatesBanner(store, widgetType) { + const bannerWidget = document.querySelector( + `[data-widget-type="${widgetType}"]`, + ); + + if (bannerWidget) { + ReactDOM.render( + + + , + bannerWidget, + ); + } +} diff --git a/src/applications/static-pages/situation-updates-banner/situationUpdateBanner.jsx b/src/applications/static-pages/situation-updates-banner/situationUpdateBanner.jsx new file mode 100644 index 000000000000..e1dd008d86d0 --- /dev/null +++ b/src/applications/static-pages/situation-updates-banner/situationUpdateBanner.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export default function SituationUpdateBanner({ + id, + alertType, + headline, + showClose, + content, +}) { + return ( + +

{content}

+ + ); +} + +SituationUpdateBanner.propTypes = { + alertType: PropTypes.string.isRequired, + content: PropTypes.node.isRequired, + headline: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, + showClose: PropTypes.bool, +}; diff --git a/src/applications/static-pages/situation-updates-banner/tests/createSituationUpdatesBanner.unit.spec.js b/src/applications/static-pages/situation-updates-banner/tests/createSituationUpdatesBanner.unit.spec.js new file mode 100644 index 000000000000..7723cb5a2548 --- /dev/null +++ b/src/applications/static-pages/situation-updates-banner/tests/createSituationUpdatesBanner.unit.spec.js @@ -0,0 +1,75 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import ReactDOM from 'react-dom'; +import { Provider } from 'react-redux'; + +import createSituationUpdatesBanner, { + BannerContainer, +} from '../createSituationUpdatesBanner'; +import widgetTypes from '../../widgetTypes'; + +describe('createSituationUpdatesBanner', () => { + let sandbox; + let store; + let widgetContainer; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + store = { + getState: () => ({ + featureToggles: { + loading: false, + bannerUseAlternativeBanners: true, + }, + }), + subscribe: () => {}, + dispatch: sinon.spy(), + }; + + widgetContainer = document.createElement('div'); + widgetContainer.setAttribute( + 'data-widget-type', + widgetTypes.SITUATION_UPDATES_BANNER, + ); + document.body.appendChild(widgetContainer); + + sandbox.stub(ReactDOM, 'render'); + }); + + afterEach(() => { + sandbox.restore(); + if (widgetContainer && widgetContainer.parentNode) { + widgetContainer.parentNode.removeChild(widgetContainer); + } + }); + + it('should not render banner when widget container is not found', async () => { + document.body.removeChild(widgetContainer); + await createSituationUpdatesBanner( + store, + widgetTypes.SITUATION_UPDATES_BANNER, + ); + expect(ReactDOM.render.called).to.be.false; + }); + + it('should render banner with default props when widget container exists', async () => { + await createSituationUpdatesBanner( + store, + widgetTypes.SITUATION_UPDATES_BANNER, + ); + + expect(ReactDOM.render.calledOnce).to.be.true; + + const renderCall = ReactDOM.render.getCall(0); + const [element, container] = renderCall.args; + + expect(container).to.equal(widgetContainer); + + // Check if rendered element is wrapped in Provider + expect(element.type).to.equal(Provider); + + // Check if SituationUpdateBanner is rendered with correct props + const situationBanner = element.props.children; + expect(situationBanner.type).to.equal(BannerContainer); + }); +}); diff --git a/src/applications/static-pages/static-pages-entry.js b/src/applications/static-pages/static-pages-entry.js index 4b1c2ff24c75..244d4cd1c494 100644 --- a/src/applications/static-pages/static-pages-entry.js +++ b/src/applications/static-pages/static-pages-entry.js @@ -64,6 +64,7 @@ import createPost911GiBillStatusWidget, { post911GIBillStatusReducer, } from '../post-911-gib-status/createPost911GiBillStatusWidget'; import createResourcesAndSupportSearchWidget from './widget-creators/resources-and-support-search'; +import createSituationUpdatesBanner from './situation-updates-banner/createSituationUpdatesBanner'; import createThirdPartyApps, { thirdPartyAppsReducer, } from '../third-party-app-directory/createThirdPartyApps'; @@ -200,6 +201,7 @@ createScheduleViewVAAppointmentsPage( widgetTypes.SCHEDULE_VIEW_VA_APPOINTMENTS_PAGE, ); createSecureMessagingPage(store, widgetTypes.SECURE_MESSAGING_PAGE); +createSituationUpdatesBanner(store, widgetTypes.SITUATION_UPDATES_BANNER); createViewTestAndLabResultsPage( store, widgetTypes.VIEW_TEST_AND_LAB_RESULTS_PAGE, diff --git a/src/applications/static-pages/widgetTypes.js b/src/applications/static-pages/widgetTypes.js index 287c49be0c58..c128249c0661 100644 --- a/src/applications/static-pages/widgetTypes.js +++ b/src/applications/static-pages/widgetTypes.js @@ -74,6 +74,7 @@ export default { SCO_EVENTS: 'sco-events', SECURE_MESSAGING_PAGE: 'secure-messaging-page', SIDE_NAV: 'side-nav', + SITUATION_UPDATES_BANNER: 'situation-updates-banner', SUPPLEMENTAL_CLAIM: 'supplemental_claim', THIRD_PARTY_APP_DIRECTORY: 'third-party-app-directory', VET_CENTER_HOURS: 'vet-center-hours', diff --git a/src/platform/utilities/feature-toggles/featureFlagNames.json b/src/platform/utilities/feature-toggles/featureFlagNames.json index 83aa9982f3ff..28839ae11a87 100644 --- a/src/platform/utilities/feature-toggles/featureFlagNames.json +++ b/src/platform/utilities/feature-toggles/featureFlagNames.json @@ -7,6 +7,7 @@ "askVaIntroductionPageFeature": "ask_va_introduction_page_feature", "authExpVbaDowntimeMessage": "auth_exp_vba_downtime_message", "avsEnabled": "avs_enabled", + "bannerUseAlternativeBanners": "banner_use_alternative_banners", "bcasLettersUseLighthouse": "bcas_letters_use_lighthouse", "benefitsDocumentsUseLighthouse": "benefits_documents_use_lighthouse", "burialFormEnabled": "burial_form_enabled",