diff --git a/app/views/queue/index.html.erb b/app/views/queue/index.html.erb index 88b5f34ca42..e0ba7a1038a 100644 --- a/app/views/queue/index.html.erb +++ b/app/views/queue/index.html.erb @@ -52,7 +52,8 @@ split_appeal_workflow: FeatureToggle.enabled?(:split_appeal_workflow, user: current_user), cavc_remand_granted_substitute_appellant: FeatureToggle.enabled?(:cavc_remand_granted_substitute_appellant, user: current_user), cavc_dashboard_workflow: FeatureToggle.enabled?(:cavc_dashboard_workflow, user: current_user), - cc_appeal_workflow: FeatureToggle.enabled?(:cc_appeal_workflow, user: current_user) + cc_appeal_workflow: FeatureToggle.enabled?(:cc_appeal_workflow, user: current_user), + cc_vacatur_visibility: FeatureToggle.enabled?(:cc_vacatur_visibility, user: current_user) } }) %> <% end %> diff --git a/client/COPY.json b/client/COPY.json index dafc32b9699..9bd85705be1 100644 --- a/client/COPY.json +++ b/client/COPY.json @@ -386,6 +386,12 @@ "MTV_CHECKOUT_RETURN_TO_JUDGE_MODAL_INSTRUCTIONS_LABEL": "Provide instructions and context for this action", "MTV_CHECKOUT_RETURN_TO_JUDGE_SUCCESS_TITLE": "%s's Motion to Vacate has been returned to %s", "MTV_CHECKOUT_RETURN_TO_JUDGE_SUCCESS_DETAILS": "If you made a mistake, please email your judge to resolve the issue.", + + "MTV_TASK_INSTRUCTIONS": "**Motion To Vacate:** \n", + "MTV_TASK_INSTRUCTIONS_TYPE": "**Type:** ", + "MTV_TASK_INSTRUCTIONS_DETAIL": "**Detail:** ", + "MTV_TASK_INSTRUCTIONS_HYPERLINK": "**Hyperlink:** ", + "VACATE_AND_DE_NOVO_TASK_LABEL": "Vacate and De Novo", "VACATE_AND_READJUDICATION_TASK_LABEL": "Vacate and Readjudication", "STRAIGHT_VACATE_TASK_LABEL": "Straight Vacate", diff --git a/client/app/queue/mtv/AddressMotionToVacateView.jsx b/client/app/queue/mtv/AddressMotionToVacateView.jsx index 825fd14c2d5..8cf14c6e30a 100644 --- a/client/app/queue/mtv/AddressMotionToVacateView.jsx +++ b/client/app/queue/mtv/AddressMotionToVacateView.jsx @@ -18,6 +18,9 @@ export const AddressMotionToVacateView = () => { const task = useSelector((state) => taskById(state, { taskId })); const appeal = useSelector((state) => appealWithDetailSelector(state, { appealId })); + const vacateTypeFeatureToggle = useSelector( + (state) => state.ui.featureToggles.cc_vacatur_visibility + ); const { selected, options } = taskActionData({ task, match }); @@ -42,6 +45,7 @@ export const AddressMotionToVacateView = () => { task={task} attorneys={attyOptions} selectedAttorney={selected} + vacateTypeFeatureToggle = {vacateTypeFeatureToggle} appeal={appeal} onSubmit={handleSubmit} returnToLitSupportLink={`${match.url}/${JUDGE_RETURN_TO_LIT_SUPPORT.value}`} diff --git a/client/app/queue/mtv/MTVJudgeDisposition.jsx b/client/app/queue/mtv/MTVJudgeDisposition.jsx index eddde279cc9..ec89ec4d81a 100644 --- a/client/app/queue/mtv/MTVJudgeDisposition.jsx +++ b/client/app/queue/mtv/MTVJudgeDisposition.jsx @@ -13,9 +13,13 @@ import { JUDGE_ADDRESS_MTV_VACATE_TYPE_LABEL, JUDGE_ADDRESS_MTV_HYPERLINK_LABEL, JUDGE_ADDRESS_MTV_DISPOSITION_NOTES_LABEL, - JUDGE_ADDRESS_MTV_ASSIGN_ATTORNEY_LABEL + JUDGE_ADDRESS_MTV_ASSIGN_ATTORNEY_LABEL, + MTV_TASK_INSTRUCTIONS, + MTV_TASK_INSTRUCTIONS_TYPE, + MTV_TASK_INSTRUCTIONS_DETAIL, + MTV_TASK_INSTRUCTIONS_HYPERLINK } from '../../../COPY'; -import { DISPOSITION_TEXT, VACATE_TYPE_OPTIONS } from '../../../constants/MOTION_TO_VACATE'; +import { DISPOSITION_TIMELINE_TEXT, VACATE_TYPE_OPTIONS } from '../../../constants/MOTION_TO_VACATE'; import { JUDGE_RETURN_TO_LIT_SUPPORT } from '../../../constants/TASK_ACTIONS'; import SearchableDropdown from '../../components/SearchableDropdown'; import AppSegment from '@department-of-veterans-affairs/caseflow-frontend-toolkit/components/AppSegment'; @@ -28,6 +32,7 @@ import StringUtil from '../../util/StringUtil'; import { ReturnToLitSupportAlert } from './ReturnToLitSupportAlert'; import { grantTypes, dispositionStrings } from './mtvConstants'; import { sprintf } from 'sprintf-js'; +import { isEmpty } from 'lodash'; const vacateTypeText = (val) => { const opt = VACATE_TYPE_OPTIONS.find((i) => i.value === val); @@ -35,19 +40,33 @@ const vacateTypeText = (val) => { return opt && opt.displayText; }; -const formatInstructions = ({ disposition, vacateType, hyperlink, instructions }) => { - const parts = [`I am proceeding with a ${DISPOSITION_TEXT[disposition]}.`]; +const formatInstructions = ({ vacateTypeFeatureToggle, disposition, vacateType, hyperlink, instructions }) => { + const parts = [`${MTV_TASK_INSTRUCTIONS}${DISPOSITION_TIMELINE_TEXT[disposition]}\n`]; switch (disposition) { case 'granted': case 'partially_granted': - parts.push(`This will be a ${vacateTypeText(vacateType)}`); - parts.push(instructions); + if (!vacateTypeFeatureToggle) { + parts.push(MTV_TASK_INSTRUCTIONS_TYPE); + parts.push(`${vacateTypeText(vacateType)}\n`); + + } + if (isEmpty(instructions) === false) { + parts.push(MTV_TASK_INSTRUCTIONS_DETAIL); + parts.push(`${instructions}\n`); + } break; + case 'denied': + case 'dismissed': default: - parts.push(instructions); - parts.push('\nHere is the hyperlink to the signed denial document'); - parts.push(hyperlink); + if (isEmpty(instructions) === false) { + parts.push(MTV_TASK_INSTRUCTIONS_DETAIL); + parts.push(`${instructions}\n`); + } + if (hyperlink !== null) { + parts.push(MTV_TASK_INSTRUCTIONS_HYPERLINK); + parts.push(`${hyperlink}\n`); + } break; } @@ -62,10 +81,12 @@ const styles = { }; export const MTVJudgeDisposition = ({ + attorneys, selectedAttorney, task, appeal, + vacateTypeFeatureToggle, onSubmit = () => null, submitting = false, returnToLitSupportLink = JUDGE_RETURN_TO_LIT_SUPPORT.value @@ -81,6 +102,7 @@ export const MTVJudgeDisposition = ({ const handleSubmit = () => { const formattedInstructions = formatInstructions({ + vacateTypeFeatureToggle, disposition, vacateType, hyperlink, @@ -180,7 +202,8 @@ export const MTVJudgeDisposition = ({ setInstructions(val)} value={instructions} className={['mtv-decision-instructions']} @@ -227,6 +250,7 @@ MTVJudgeDisposition.propTypes = { onSubmit: PropTypes.func.isRequired, submitting: PropTypes.bool, task: PropTypes.object.isRequired, + vacateTypeFeatureToggle: PropTypes.bool, appeal: PropTypes.object.isRequired, attorneys: PropTypes.array.isRequired, selectedAttorney: PropTypes.object, diff --git a/client/constants/MOTION_TO_VACATE.json b/client/constants/MOTION_TO_VACATE.json index eb7d4c76893..e5b1d8e44c0 100644 --- a/client/constants/MOTION_TO_VACATE.json +++ b/client/constants/MOTION_TO_VACATE.json @@ -39,6 +39,14 @@ "denied": "denial of all issues for vacatur", "dismissed": "dismissal" }, + + "DISPOSITION_TIMELINE_TEXT": { + "granted": "Full vacatur", + "partially_granted": "Partial vacatur", + "denied": "Deny all issues for vacatur", + "dismissed": "Dismiss all issues for vacatur" + }, + "DISPOSITION_RECOMMENDATIONS": { "granted": "I recommend granting a vacatur.", "partially_granted": "I recommend granting a partial vacatur.", diff --git a/client/test/app/queue/mtv/MTVJudgeDisposition.test.js b/client/test/app/queue/mtv/MTVJudgeDisposition.test.js index 42025286c26..306965c3da0 100644 --- a/client/test/app/queue/mtv/MTVJudgeDisposition.test.js +++ b/client/test/app/queue/mtv/MTVJudgeDisposition.test.js @@ -15,40 +15,99 @@ const task = generateAmaTask({ type: 'VacateMotionMailTask', instructions: ['Lorem ipsum dolor sit amet, consectetur adipiscing'], }); +let linkField = /Insert Caseflow Reader document hyperlink to/; +let instructionsField = /Provide context and instructions on which issues should be/; + +const selectRadioField = (radioSelection) => { + const radioFieldToSelect = screen.getByLabelText(radioSelection); + + userEvent.click(radioFieldToSelect); +}; + +const enterAdditionalContext = (text, selectedField) => { + const textField = screen.getByText(selectedField); + + userEvent.type(textField, text); +}; + +const selectDisposition = async (disposition = 'grant all') => { + userEvent.click( + screen.getByLabelText(new RegExp(disposition, 'i')) + ); + + if ((/grant/i).test(disposition)) { + await waitFor(() => { + expect( + screen.getByText(/what type of vacate/i) + ).toBeInTheDocument(); + }); + } else { + await waitFor(() => { + expect( + screen.getByLabelText(/insert caseflow reader document hyperlink/i) + ).toBeInTheDocument(); + }); + } +}; + +const fillForm = async (disposition, vacateType, vacateIssues, hyperlink, instructions) => { + userEvent.click( + screen.getByLabelText(new RegExp(disposition, 'i')) + ); + + if ((/grant all/i).test(disposition)) { + await waitFor(() => { + expect( + screen.getByText(/what type of vacate/i) + ).toBeInTheDocument(); + }); + + selectRadioField(vacateType); + + } else if ((/grant partial/i).test(disposition)) { + await waitFor(() => { + expect( + screen.getByText(/which issues would you like to vacate/i) + ).toBeInTheDocument(); + }); + + selectRadioField(vacateType); + selectRadioField(vacateIssues); + + } else { + await waitFor(() => { + expect( + screen.getByLabelText(/insert caseflow reader document hyperlink/i) + ).toBeInTheDocument(); + }); + + enterAdditionalContext(hyperlink, linkField); + + } + + enterAdditionalContext(instructions, instructionsField); + + await userEvent.click( + screen.getByText('Submit') + ); +}; describe('MTVJudgeDisposition', () => { + const onSubmit = jest.fn(); + const defaults = { appeal: amaAppeal, attorneys: generateAttorneys(5), task, onSubmit, }; + const setup = (props) => render(, { wrapper: BrowserRouter, }); - const selectDisposition = async (disposition = 'grant all') => { - await userEvent.click( - screen.getByLabelText(new RegExp(disposition, 'i')) - ); - - if ((/grant/i).test(disposition)) { - await waitFor(() => { - expect( - screen.getByText(/what type of vacate/i) - ).toBeInTheDocument(); - }); - } else { - await waitFor(() => { - expect( - screen.getByLabelText(/insert caseflow reader document hyperlink/i) - ).toBeInTheDocument(); - }); - } - }; - describe('default view', () => { it('renders correctly', () => { const { container } = setup(); @@ -67,7 +126,7 @@ describe('MTVJudgeDisposition', () => { describe.each(DISPOSITION_OPTIONS.map((item) => [item.value, item]))( 'with %s disposition selected', - (disposition, { displayText: label }) => { + ({ displayText: label }) => { it('renders correctly', async () => { const { container } = setup(); @@ -87,4 +146,237 @@ describe('MTVJudgeDisposition', () => { }); } ); + + describe('Case timeline instructions, feature toggle enabled', () => { + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('grant or partial grant instructions sent', () => { + it('sends the correct instructions based on grant all disposition', async () => { + const disposition = 'grant all'; + let vacateType = 'Vacate and De Novo (2 documents)'; + let vacateIssues; + let hyperlink; + const instructions = 'instructions from judge'; + + setup({ vacateTypeFeatureToggle: false }); + + await fillForm( + disposition, + vacateType, + vacateIssues, + hyperlink, + instructions + ); + + expect(onSubmit.mock.calls[0][0].instructions).toMatch( + '**Motion To Vacate:** ' + + '\nFull vacatur' + + '\n' + + '\n**Type:** ' + + '\nVacate and De Novo (2 documents)' + + '\n' + + '\n**Detail:** ' + + '\ninstructions from judge' + + '\n' + ); + }); + + it('sends the correct instructions based on partially granted disposition', async () => { + const disposition = 'Grant partial vacatur'; + const vacateType = 'Straight Vacate (1 document)'; + const vacateIssues = '1. This is a description of the decision'; + let hyperlink; + const instructions = 'some instructions from judge'; + + setup({ vacateTypeFeatureToggle: false }); + + await fillForm( + disposition, + vacateType, + vacateIssues, + hyperlink, + instructions + ); + + expect(onSubmit.mock.calls[0][0].instructions).toMatch('**Motion To Vacate:** ' + + '\nPartial vacatur' + + '\n' + + '\n**Type:** ' + + '\nStraight Vacate (1 document)' + + '\n' + + '\n**Detail:** ' + + '\nsome instructions from judge' + + '\n' + ); + }); + }); + + describe('deny or dismiss instructions sent', () => { + it('sends the correct instructions based on denied disposition', async () => { + const disposition = 'deny'; + let vacateType; + let vacateIssues; + const hyperlink = 'www.caseflow.com'; + const instructions = 'testing'; + + setup(); + + await fillForm( + disposition, + vacateType, + vacateIssues, + hyperlink, + instructions + ); + + expect(onSubmit.mock.calls[0][0].instructions).toMatch( + '**Motion To Vacate:** \n' + + 'Deny all issues for vacatur\n\n' + + '**Detail:** \ntesting\n\n' + + '**Hyperlink:** \nwww.caseflow.com\n' + ); + }); + + it('sends the correct instructions based on dismissed disposition', async () => { + const disposition = 'dismiss'; + let vacateType; + let vacateIssues; + const hyperlink = 'www.google.com'; + const instructions = 'new instructions from judge'; + + setup(); + + await fillForm( + disposition, + vacateType, + vacateIssues, + hyperlink, + instructions + ); + + expect(onSubmit.mock.calls[0][0].instructions).toMatch( + '**Motion To Vacate:** \n' + + 'Dismiss all issues for vacatur\n\n**Detail:** \nnew instructions from judge\n\n' + + '**Hyperlink:** \nwww.google.com\n' + ); + }); + }); + }); + + describe('Case timeline instructions, feature toggle disabled', () => { + + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('grant or partial grant instructions sent', () => { + it('sends the correct instructions based on grant all disposition', async () => { + + const disposition = 'grant all'; + let vacateType = 'Vacate and De Novo (2 documents)'; + let vacateIssues; + let hyperlink; + const instructions = 'instructions from judge'; + + setup({ vacateTypeFeatureToggle: true }); + + await fillForm( + disposition, + vacateType, + vacateIssues, + hyperlink, + instructions + ); + + expect(onSubmit.mock.calls[0][0].instructions).toMatch( + '**Motion To Vacate:** ' + + '\nFull vacatur' + + '\n' + + '\n**Detail:** ' + + '\ninstructions from judge' + + '\n' + ); + + }); + + it('sends the correct instructions based on partially granted disposition', async () => { + const disposition = 'Grant partial vacatur'; + let vacateType = 'Vacate and De Novo (2 documents)'; + let vacateIssues = '1. This is a description of the decision'; + let hyperlink; + const instructions = 'some instructions from judge'; + + setup({ vacateTypeFeatureToggle: true }); + await fillForm( + disposition, + vacateType, + vacateIssues, + hyperlink, + instructions + ); + + expect(onSubmit.mock.calls[0][0].instructions).toMatch( + '**Motion To Vacate:** ' + + '\nPartial vacatur' + + '\n' + + '\n**Detail:** ' + + '\nsome instructions from judge' + + '\n' + ); + }); + }); + + describe('deny or dismiss instructions sent', () => { + it('sends the correct instructions based on denied disposition', async () => { + const disposition = 'deny'; + let vacateType; + let vacateIssues; + const hyperlink = 'www.caseflow.com'; + const instructions = 'testing'; + + setup({ vacateTypeFeatureToggle: true }); + + await fillForm( + disposition, + vacateType, + vacateIssues, + hyperlink, + instructions + ); + + expect(onSubmit.mock.calls[0][0].instructions).toMatch( + '**Motion To Vacate:** \n' + + 'Deny all issues for vacatur\n\n' + + '**Detail:** \ntesting\n\n' + + '**Hyperlink:** \nwww.caseflow.com\n' + ); + }); + + it('sends the correct instructions based on dismissed disposition', async () => { + const disposition = 'dismiss'; + let vacateType; + let vacateIssues; + const hyperlink = 'www.google.com'; + const instructions = 'new instructions from judge'; + + setup({ vacateTypeFeatureToggle: true }); + + await fillForm( + disposition, + vacateType, + vacateIssues, + hyperlink, + instructions + ); + + expect(onSubmit.mock.calls[0][0].instructions).toMatch( + '**Motion To Vacate:** \n' + + 'Dismiss all issues for vacatur\n\n**Detail:** \nnew instructions from judge\n\n' + + '**Hyperlink:** \nwww.google.com\n' + ); + }); + }); + }); }); diff --git a/client/test/app/queue/mtv/__snapshots__/MTVJudgeDisposition.test.js.snap b/client/test/app/queue/mtv/__snapshots__/MTVJudgeDisposition.test.js.snap index 94920f806ba..01fe1fb42f9 100644 --- a/client/test/app/queue/mtv/__snapshots__/MTVJudgeDisposition.test.js.snap +++ b/client/test/app/queue/mtv/__snapshots__/MTVJudgeDisposition.test.js.snap @@ -422,31 +422,77 @@ exports[`MTVJudgeDisposition with denied disposition selected renders correctly - + +
+
+ + +
+
+ + +
+
+ + +
+
+
@@ -456,7 +502,7 @@ exports[`MTVJudgeDisposition with denied disposition selected renders correctly > - Provide context and instructions on which issues should be denied + Provide context and instructions on which issues should be granted @@ -470,14 +516,114 @@ exports[`MTVJudgeDisposition with denied disposition selected renders correctly name="instructions" />
+
+
- + +
+
+ + +
+
+ + +
+
+ + +
+
+
@@ -719,7 +911,7 @@ exports[`MTVJudgeDisposition with dismissed disposition selected renders correct > - Provide context and instructions on which issues should be dismissed + Provide context and instructions on which issues should be granted @@ -733,14 +925,114 @@ exports[`MTVJudgeDisposition with dismissed disposition selected renders correct name="instructions" />
+
+
-
- - - - Which issues would you like to vacate? - - - -
- - -
-
- - -
-
@@ -1478,7 +1729,7 @@ exports[`MTVJudgeDisposition with partially_granted disposition selected renders > - Provide context and instructions on which issues should be partially_granted + Provide context and instructions on which issues should be granted diff --git a/scripts/enable_features_dev.rb b/scripts/enable_features_dev.rb index 5a0a2ddf6c3..f6053eedb20 100644 --- a/scripts/enable_features_dev.rb +++ b/scripts/enable_features_dev.rb @@ -54,11 +54,12 @@ def call # - they make significantly drastic changes in Dev/Demo compared to Production # - the work around the feature has been paused # - the flag is only being used to disable functionality -disabled_flags = [ - "legacy_das_deprecation", - "cavc_dashboard_workflow", - "poa_auto_refresh", - "interface_version_2" +disabled_flags = %w[ + legacy_das_deprecation + cavc_dashboard_workflow + poa_auto_refresh + interface_version_2 + cc_vacatur_visibility ] all_features = AllFeatureToggles.new.call.flatten.uniq diff --git a/spec/feature/queue/motion_to_vacate_spec.rb b/spec/feature/queue/motion_to_vacate_spec.rb index c07feb9b71d..1841031eaf7 100644 --- a/spec/feature/queue/motion_to_vacate_spec.rb +++ b/spec/feature/queue/motion_to_vacate_spec.rb @@ -435,6 +435,7 @@ def submit_and_fetch_task(judge) expect(new_task.available_actions(motions_attorney)).to include( Constants.TASK_ACTIONS.LIT_SUPPORT_PULAC_CERULLO.to_h ) + expect(new_task.instructions.join("")).to eq(instructions) end end diff --git a/spec/support/queue_helpers.rb b/spec/support/queue_helpers.rb index bfd61ed4c25..39d1cbabb0c 100644 --- a/spec/support/queue_helpers.rb +++ b/spec/support/queue_helpers.rb @@ -9,6 +9,10 @@ def disposition_text mtv_const.DISPOSITION_TEXT.to_h end + def disposition_timeline_text + mtv_const.DISPOSITION_TIMELINE_TEXT.to_h + end + def recommendation_text mtv_const.DISPOSITION_RECOMMENDATIONS.to_h end @@ -32,14 +36,26 @@ def format_mtv_attorney_instructions(notes:, disposition:, hyperlinks: []) end def format_mtv_judge_instructions(notes:, disposition:, vacate_type: nil, hyperlink: nil) - parts = ["I am proceeding with a #{disposition_text[disposition.to_sym]}."] - - parts += case disposition - when "granted", "partial" - ["This will be a #{vacate_types[vacate_type.to_sym]}", notes] - else - [notes, "\nHere is the hyperlink to the signed denial document", hyperlink] - end + parts = ["**Motion To Vacate:** \n#{disposition_timeline_text[disposition.to_sym]}\n"] + + case disposition + when "granted", "partially_granted" + parts += ["**Type:** "] + parts += ["#{vacate_types[vacate_type.to_sym]}\n"] + if !notes.empty? + parts += ["**Detail:** "] + parts += ["#{notes}\n"] + end + when "denied", "dismissed" + if !notes.empty? + parts += ["**Detail:** "] + parts += ["#{notes}\n"] + end + if hyperlink.present? + parts += ["**Hyperlink:** "] + parts += ["#{hyperlink}\n"] + end + end parts.join("\n") end