Skip to content

Commit

Permalink
Merge pull request #352 from adhocteam/main
Browse files Browse the repository at this point in the history
Add update remove objectives
  • Loading branch information
rahearn authored Mar 8, 2021
2 parents ced9201 + e7db658 commit c9b73fc
Show file tree
Hide file tree
Showing 25 changed files with 1,007 additions and 151 deletions.
3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"simplebar-react": "^2.3.0",
"url-join": "^4.0.1",
"use-deep-compare-effect": "^1.6.1",
"uswds": "^2.9.0"
"uswds": "^2.9.0",
"uuid": "^8.3.2"
},
"engines": {
"node": "14.16.0"
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/ContextMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ function ContextMenu({
aria-haspopup
onClick={() => updateShown((previous) => !previous)}
aria-label={label}
type="button"
>
<FontAwesomeIcon color="black" icon={faEllipsisH} />
</Button>
Expand Down
15 changes: 9 additions & 6 deletions frontend/src/components/FormItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import './FormItem.css';
const labelPropTypes = {
label: PropTypes.node.isRequired,
children: PropTypes.node.isRequired,
className: PropTypes.string.isRequired,
};

function FieldSetWrapper({ label, children }) {
function FieldSetWrapper({ label, children, className }) {
return (
<Fieldset unstyled>
<Fieldset unstyled className={className}>
<legend>{label}</legend>
{children}
</Fieldset>
Expand All @@ -24,9 +25,9 @@ function FieldSetWrapper({ label, children }) {

FieldSetWrapper.propTypes = labelPropTypes;

function LabelWrapper({ label, children }) {
function LabelWrapper({ label, children, className }) {
return (
<Label>
<Label className={className}>
{label}
{children}
</Label>
Expand All @@ -36,7 +37,7 @@ function LabelWrapper({ label, children }) {
LabelWrapper.propTypes = labelPropTypes;

function FormItem({
label, children, required, name, fieldSetWrapper,
label, children, required, name, fieldSetWrapper, className,
}) {
const { formState: { errors } } = useFormContext();
const fieldErrors = errors[name];
Expand All @@ -51,7 +52,7 @@ function FormItem({

return (
<FormGroup error={fieldErrors}>
<LabelType label={labelWithRequiredTag}>
<LabelType label={labelWithRequiredTag} className={className}>
<ReactHookFormError
errors={errors}
name={name}
Expand All @@ -69,11 +70,13 @@ FormItem.propTypes = {
name: PropTypes.string.isRequired,
fieldSetWrapper: PropTypes.bool,
required: PropTypes.bool,
className: PropTypes.string,
};

FormItem.defaultProps = {
required: true,
fieldSetWrapper: false,
className: '',
};

export default FormItem;
23 changes: 20 additions & 3 deletions frontend/src/components/MultiSelect.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ function MultiSelect({
labelProperty,
valueProperty,
simple,
rules,
multiSelectOptions,
onItemSelected,
components: componentReplacements,
}) {
/*
Expand All @@ -95,7 +97,11 @@ function MultiSelect({
value: v, label: v,
}));
}
return value.map((item) => ({ label: item[labelProperty], value: item[valueProperty] }));
return value.map((item) => ({
...item,
label: item[labelProperty],
value: item[valueProperty],
}));
};

/*
Expand All @@ -109,7 +115,11 @@ function MultiSelect({
controllerOnChange(event.map((v) => v.value));
} else {
controllerOnChange(
event.map((item) => ({ [labelProperty]: item.label, [valueProperty]: item.value })),
event.map((item) => ({
...item,
[labelProperty]: item.label,
[valueProperty]: item.value,
})),
);
}
};
Expand All @@ -124,7 +134,9 @@ function MultiSelect({
id={name}
value={values}
onChange={(event) => {
if (event) {
if (onItemSelected) {
onItemSelected(event);
} else if (event) {
onChange(event, controllerOnChange);
} else {
controllerOnChange([]);
Expand All @@ -151,6 +163,7 @@ function MultiSelect({
}
return true;
},
...rules,
}}
name={name}
/>
Expand Down Expand Up @@ -182,13 +195,15 @@ MultiSelect.propTypes = {
// eslint-disable-next-line react/forbid-prop-types
control: PropTypes.object.isRequired,
components: PropTypes.shape({}),
onItemSelected: PropTypes.func,
multiSelectOptions: PropTypes.shape({
isClearable: PropTypes.bool,
closeMenuOnSelect: PropTypes.bool,
controlShouldRenderValue: PropTypes.bool,
hideSelectedOptions: PropTypes.bool,
}),
disabled: PropTypes.bool,
rules: PropTypes.shape({}),
required: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
};

Expand All @@ -200,6 +215,8 @@ MultiSelect.defaultProps = {
valueProperty: 'value',
multiSelectOptions: {},
components: {},
rules: {},
onItemSelected: null,
};

export default MultiSelect;
32 changes: 21 additions & 11 deletions frontend/src/components/Navigator/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,22 +65,21 @@ function Navigator({
const { isDirty, errors, isValid } = formState;
const hasErrors = Object.keys(errors).length > 0;

useEffect(() => {
if (showValidationErrors && !page.review) {
trigger();
}
}, [page.review, trigger, showValidationErrors]);

const newNavigatorState = () => {
if (page.review) {
return pageState;
}

const currentPageState = pageState[page.position];
const isComplete = page.isPageComplete ? page.isPageComplete(getValues()) : isValid;

const newPageState = { ...pageState };
if (isValid) {
if (isComplete) {
newPageState[page.position] = COMPLETE;
} else if (currentPageState === COMPLETE) {
newPageState[page.position] = IN_PROGRESS;
} else {
newPageState[page.position] = isDirty ? IN_PROGRESS : pageState[page.position];
newPageState[page.position] = isDirty ? IN_PROGRESS : currentPageState;
}
return newPageState;
};
Expand All @@ -105,9 +104,10 @@ function Navigator({
};

const onUpdatePage = async (index) => {
const newIndex = index === page.position ? null : index;
await onSaveForm();
updatePage(newIndex);
if (index !== page.position) {
updatePage(index);
}
};

const onContinue = () => {
Expand All @@ -124,6 +124,14 @@ function Navigator({
reset(formData);
}, [currentPage, reset, formData]);

useEffect(() => {
if (showValidationErrors && !page.review) {
setTimeout(() => {
trigger();
});
}
}, [page.path, page.review, trigger, showValidationErrors]);

const navigatorPages = pages.map((p) => {
const current = p.position === page.position;

Expand All @@ -135,7 +143,9 @@ function Navigator({
const state = p.review ? formData.status : stateOfPage;
return {
label: p.label,
onNavigation: () => onUpdatePage(p.position),
onNavigation: () => {
onUpdatePage(p.position);
},
state,
current,
review: p.review,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import fetchMock from 'fetch-mock';
import React from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import join from 'url-join';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';

import goalsObjectives from '../goalsObjectives';

Expand Down Expand Up @@ -40,6 +42,21 @@ const renderGoals = (grantIds, activityRecipientType, initialData, goals = []) =
);
};

// eslint-disable-next-line react/prop-types
const RenderReview = ({ goals }) => {
const history = createMemoryHistory();
const hookForm = useForm({
defaultValues: { goals },
});
return (
<Router history={history}>
<FormProvider {...hookForm}>
{goalsObjectives.reviewSection()}
</FormProvider>
</Router>
);
};

describe('goals objectives', () => {
afterEach(() => fetchMock.restore());
describe('when activity recipient type is "grantee"', () => {
Expand All @@ -63,4 +80,25 @@ describe('goals objectives', () => {
expect(screen.queryByText('Goals and objectives')).toBeNull();
});
});

describe('review page', () => {
it('displays goals with no objectives', async () => {
render(<RenderReview goals={[{ id: 1, name: 'goal' }]} />);
const goal = await screen.findByText('goal');
expect(goal).toBeVisible();
});

it('displays goals with objectives', async () => {
render(<RenderReview goals={[{
id: 1,
name: 'goal',
objectives: [{
id: 1, title: 'title', ttaProvided: 'ttaProvided', status: 'Not Started',
}],
}]}
/>);
const objective = await screen.findByText('title');
expect(objective).toBeVisible();
});
});
});
5 changes: 1 addition & 4 deletions frontend/src/pages/ActivityReport/Pages/components/Goal.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,5 @@

.smart-hub--goal-content {
margin-left: 1rem;
}

.usa-form .smart-hub--button {
margin-top: 0;
width: 100%;
}
88 changes: 69 additions & 19 deletions frontend/src/pages/ActivityReport/Pages/components/Goal.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,81 @@ import { Button } from '@trussworks/react-uswds';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTrash } from '@fortawesome/free-solid-svg-icons';

import Objective from './Objective';
import './Goal.css';

const Goals = ({ id, name, onRemove }) => (
<div className="smart-hub--goal">
<div className="smart-hub--goal-content">
<p className="margin-top-0">
<span className="text-bold">Goal: </span>
{ name }
</p>
<Button onClick={(e) => { e.preventDefault(); }} outline className="smart-hub--button">
Add new Objective
</Button>
</div>
<div className="margin-left-auto margin-top-2">
<Button onClick={(e) => { e.preventDefault(); onRemove(id); }} unstyled className="smart-hub--button" aria-label="remove goal">
<FontAwesomeIcon color="gray" icon={faTrash} />
</Button>
const Goals = ({
name, onRemoveGoal, goalIndex, objectives, onUpdateObjectives, createObjective,
}) => {
const onRemoveObjective = (index) => {
const newObjectives = objectives.filter((o, objectiveIndex) => index !== objectiveIndex);
onUpdateObjectives(newObjectives);
};

const onUpdateObjective = (index, newObjective) => {
const newObjectives = [...objectives];
newObjectives[index] = newObjective;
onUpdateObjectives(newObjectives);
};
const singleObjective = objectives.length === 1;

return (
<div className="smart-hub--goal">
<div className="smart-hub--goal-content">
<div className="display-flex flex-align-start">
<p className="margin-top-0">
<span className="text-bold">Goal: </span>
{ name }
</p>

<div className="margin-left-auto">
<Button type="button" onClick={onRemoveGoal} unstyled className="smart-hub--button" aria-label={`remove goal ${goalIndex + 1}`}>
<FontAwesomeIcon color="gray" icon={faTrash} />
</Button>
</div>
</div>
<div>
{objectives.map((objective, objectiveIndex) => (
<div className="margin-top-1" key={objective.id}>
<Objective
goalIndex={goalIndex}
objectiveIndex={objectiveIndex}
objective={objective}
onRemove={() => { if (!singleObjective) { onRemoveObjective(objectiveIndex); } }}
onUpdate={(newObjective) => onUpdateObjective(objectiveIndex, newObjective)}
/>
</div>
))}
</div>
<Button
type="button"
onClick={() => {
onUpdateObjectives([...objectives, createObjective()]);
}}
outline
aria-label={`add objective to goal ${goalIndex + 1}`}
>
Add New Objective
</Button>
</div>

</div>
</div>
);
);
};

Goals.propTypes = {
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
objectives: PropTypes.arrayOf(PropTypes.shape({
title: PropTypes.string,
ttaProvided: PropTypes.string,
status: PropTypes.string,
new: PropTypes.bool,
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
})).isRequired,
createObjective: PropTypes.func.isRequired,
name: PropTypes.string.isRequired,
onRemove: PropTypes.func.isRequired,
goalIndex: PropTypes.number.isRequired,
onRemoveGoal: PropTypes.func.isRequired,
onUpdateObjectives: PropTypes.func.isRequired,
};

export default Goals;
Loading

0 comments on commit c9b73fc

Please sign in to comment.