diff --git a/.circleci/config.yml b/.circleci/config.yml index 6e0a432989..7b50d76bd5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -291,7 +291,7 @@ parameters: default: "al/ttahub-2570/flat-resource-sql" type: string sandbox_git_branch: # change to feature branch to test deployment - default: "mb/TTAHUB/frontend-for-tr-dashboard" + default: "jp/2793/reopen-frontend" type: string prod_new_relic_app_id: default: "877570491" diff --git a/frontend/package.json b/frontend/package.json index 79ce687a4c..b1c2c76ef5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,7 +11,7 @@ "@hookform/error-message": "^0.0.5", "@react-hook/resize-observer": "^1.2.6", "@trussworks/react-uswds": "4.1.1", - "@ttahub/common": "2.0.18", + "@ttahub/common": "^2.1.3", "@use-it/interval": "^1.0.0", "async": "^3.2.3", "browserslist": "^4.16.5", diff --git a/frontend/src/components/GoalCards/GoalCard.js b/frontend/src/components/GoalCards/GoalCard.js index 978756f2f4..c26fcf9baf 100644 --- a/frontend/src/components/GoalCards/GoalCard.js +++ b/frontend/src/components/GoalCards/GoalCard.js @@ -55,6 +55,7 @@ function GoalCard({ recipientId, regionId, showCloseSuspendGoalModal, + showReopenGoalModal, performGoalStatusUpdate, handleGoalCheckboxSelect, isChecked, @@ -77,6 +78,7 @@ function GoalCard({ createdVia, collaborators, onAR, + isReopenedGoal, } = goal; const sortedObjectives = [...objectives, ...(sessionObjectives || [])]; @@ -88,7 +90,7 @@ function GoalCard({ const lastTTA = useMemo(() => objectives.reduce((prev, curr) => (new Date(prev) > new Date(curr.endDate) ? prev : curr.endDate), ''), [objectives]); const history = useHistory(); - const goalNumbers = goal.goalNumbers.join(', '); + const goalNumbers = `${goal.goalNumbers.join(', ')}${isReopenedGoal ? '-R' : ''}`; const { user } = useContext(UserContext); const { setIsAppLoading } = useContext(AppLoadingContext); @@ -110,6 +112,12 @@ function GoalCard({ const contextMenuLabel = `Actions for goal ${id}`; const menuItems = [ + ...(goalStatus === 'Closed' ? [{ + label: 'Reopen', + onClick: () => { + showReopenGoalModal(id); + }, + }] : []), { label: goalStatus === 'Closed' ? 'View' : 'Edit', onClick: () => { @@ -270,6 +278,7 @@ GoalCard.propTypes = { recipientId: PropTypes.string.isRequired, regionId: PropTypes.string.isRequired, showCloseSuspendGoalModal: PropTypes.func.isRequired, + showReopenGoalModal: PropTypes.func.isRequired, performGoalStatusUpdate: PropTypes.func.isRequired, handleGoalCheckboxSelect: PropTypes.func.isRequired, isChecked: PropTypes.bool.isRequired, diff --git a/frontend/src/components/GoalCards/GoalCards.js b/frontend/src/components/GoalCards/GoalCards.js index 3ffacdfd96..05dcc948b6 100644 --- a/frontend/src/components/GoalCards/GoalCards.js +++ b/frontend/src/components/GoalCards/GoalCards.js @@ -10,7 +10,8 @@ import GoalsCardsHeader from './GoalsCardsHeader'; import Container from '../Container'; import GoalCard from './GoalCard'; import CloseSuspendReasonModal from '../CloseSuspendReasonModal'; -import { updateGoalStatus } from '../../fetchers/goals'; +import { reopenGoal, updateGoalStatus } from '../../fetchers/goals'; +import ReopenReasonModal from '../ReopenReasonModal'; function GoalCards({ recipientId, @@ -46,6 +47,11 @@ function GoalCards({ const [resetModalValues, setResetModalValues] = useState(false); const closeSuspendModalRef = useRef(); + // Reopen reason modal. + const [reopenGoalId, setReopenGoalId] = useState(null); + const [resetReopenModalValues, setResetReopenModalValues] = useState(false); + const reopenModalRef = useRef(); + const showCloseSuspendGoalModal = (status, goalIds, oldGoalStatus) => { setCloseSuspendGoalIds(goalIds); setCloseSuspendStatus(status); @@ -54,6 +60,27 @@ function GoalCards({ closeSuspendModalRef.current.toggleModal(true); }; + const showReopenGoalModal = (goalId) => { + setReopenGoalId(goalId); + setResetReopenModalValues(!resetReopenModalValues); + reopenModalRef.current.toggleModal(true); + }; + + const onSubmitReopenGoal = async (goalId, reopenReason, reopenContext) => { + const updatedGoal = await reopenGoal(goalId, reopenReason, reopenContext); + + const newGoals = goals.map((g) => (g.id === updatedGoal.id ? { + ...g, + goalStatus: 'In Progress', + previousStatus: 'Closed', + isReopenedGoal: true, + } : g)); + + setGoals(newGoals); + + reopenModalRef.current.toggleModal(false); + }; + const performGoalStatusUpdate = async ( goalIds, newGoalStatus, @@ -182,6 +209,13 @@ function GoalCards({ resetValues={resetModalValues} oldGoalStatus={closeSuspendOldStatus} /> + { const newPrompts = grantsToMultiValue(selectedGrants, { ...prompts }); @@ -172,6 +174,7 @@ export default function GoalForm({ setSource(grantsToMultiValue(selectedGoalGrants, goal.source, '')); setCreatedVia(goal.createdVia || ''); setGoalCollaborators(goal.collaborators || []); + setIsReopenedGoal(goal.isReopenedGoal || false); // this is a lot of work to avoid two loops through the goal.objectives // but I'm sure you'll agree its totally worth it @@ -1099,6 +1102,7 @@ export default function GoalForm({ createdVia={createdVia} collaborators={goalCollaborators} goalTemplateId={goalTemplateId} + isReopenedGoal={isReopenedGoal} /> )} diff --git a/frontend/src/components/ReopenReasonModal.js b/frontend/src/components/ReopenReasonModal.js new file mode 100644 index 0000000000..459dd7c2f4 --- /dev/null +++ b/frontend/src/components/ReopenReasonModal.js @@ -0,0 +1,113 @@ +/* eslint-disable react/jsx-props-no-spreading */ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { REOPEN_REASONS, GOAL_STATUS } from '@ttahub/common'; +import { + Form, FormGroup, ErrorMessage, Label, Fieldset, Radio, Textarea, +} from '@trussworks/react-uswds'; +import Modal from './Modal'; + +const ReopenReasonModal = ({ + modalRef, goalId, onSubmit, resetValues, +}) => { + const [reopenReason, setReopenReason] = useState(''); + const [reopenContext, setReopenContext] = useState(''); + const [showValidationError, setShowValidationError] = useState(false); + + useEffect(() => { + // Every time we show the modal reset the form. + setReopenReason(''); + setReopenContext(''); + setShowValidationError(false); + }, [resetValues]); + + const reasonChanged = (e) => { + setReopenReason(e.target.value); + setShowValidationError(false); + }; + + const reasonRadioOptions = Object.values(REOPEN_REASONS[GOAL_STATUS.CLOSED]); + + const generateReasonRadioButtons = () => reasonRadioOptions.map((r) => ( + + )); + const contextChanged = (e) => { + setReopenContext(e.target.value); + }; + + const validateSubmit = () => { + if (!reopenReason) { + setShowValidationError(true); + } else { + onSubmit(goalId, reopenReason, reopenContext); + } + }; + + return ( +
+ +
+ + + {showValidationError + ? 'Please select a reason for reopening this goal.' : null} + +
+ { + generateReasonRadioButtons() + } +
+
+ +
+ +