Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 1313b50

Browse files
committed
Allow user to remove searched role from added roles accordion.
Ensure team details popup form is updated when searched role removed. Move pure functions for validation of team details popup to their own file. Disable focus trap for team details popup.
1 parent b47476b commit 1313b50

File tree

5 files changed

+117
-76
lines changed

5 files changed

+117
-76
lines changed

src/routes/CreateNewTeam/components/AddedRolesAccordion/index.jsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,16 @@
88
import React, { useState } from "react";
99
import PT from "prop-types";
1010
import cn from "classnames";
11+
import { useDispatch } from "react-redux";
12+
import { deleteSearchedRole } from "../../actions";
1113
import "./styles.module.scss";
14+
import IconCrossLight from "../../../../assets/images/icon-cross-light.svg";
1215

1316
function AddedRolesAccordion({ addedRoles }) {
1417
const [isOpen, setIsOpen] = useState(false);
1518

19+
const dispatch = useDispatch();
20+
1621
return addedRoles.length ? (
1722
<div styleName="accordion">
1823
<button onClick={() => setIsOpen(!isOpen)} styleName="button">
@@ -27,8 +32,13 @@ function AddedRolesAccordion({ addedRoles }) {
2732
</button>
2833
{isOpen && (
2934
<div styleName="panel">
30-
{addedRoles.map(({ name }) => (
31-
<div styleName="role-name">{name}</div>
35+
{addedRoles.map(({ name, searchId: id }) => (
36+
<div key={id} styleName="role-name">
37+
{name}
38+
<button onClick={() => dispatch(deleteSearchedRole(id))}>
39+
<IconCrossLight height="14px" width="14px" />
40+
</button>
41+
</div>
3242
))}
3343
</div>
3444
)}

src/routes/CreateNewTeam/components/AddedRolesAccordion/styles.module.scss

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,12 @@
5555
.panel {
5656
padding: 12px 18px 14px 10px;
5757
.role-name {
58-
height: 40px;
58+
position: relative;
5959
width: 100%;
6060
background-color: #F4F4F4;
6161
border-radius: 6px;
6262
padding: 10px;
63+
padding-right: 30px;
6364
@include font-barlow;
6465
font-size: 16px;
6566
line-height: 20px;
@@ -68,5 +69,19 @@
6869
&:not(:first-child) {
6970
margin-top: 5px;
7071
}
72+
73+
>button {
74+
outline: none;
75+
border: none;
76+
background: none;
77+
position: absolute;
78+
top: 12px;
79+
right: 4px;
80+
&:hover {
81+
g {
82+
stroke: red;
83+
}
84+
}
85+
}
7186
}
7287
}

src/routes/CreateNewTeam/components/BaseCreateModal/index.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ function BaseCreateModal({
3535
loadingMessage,
3636
maxWidth = "680px",
3737
darkHeader,
38+
disableFocusTrap,
3839
children,
3940
}) {
4041
return (
@@ -51,8 +52,9 @@ function BaseCreateModal({
5152
modalContainer: containerStyle,
5253
closeButton: closeButtonStyle,
5354
}}
55+
focusTrapped={!disableFocusTrap}
5456
>
55-
<div styleName="modal-body">
57+
<div styleName="modal-body" tabIndex="-1">
5658
{isLoading ? (
5759
<div styleName={cn("modal-header", { "dark-header": darkHeader })}>
5860
<CenteredSpinner />
@@ -86,6 +88,7 @@ BaseCreateModal.propTypes = {
8688
loadingMessage: PT.string,
8789
maxWidth: PT.string,
8890
darkHeader: PT.bool,
91+
disableFocusTrap: PT.bool,
8992
children: PT.node,
9093
};
9194

src/routes/CreateNewTeam/components/TeamDetailsModal/index.jsx

Lines changed: 25 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Popup form to enter details about the
44
* team request before submitting.
55
*/
6-
import React, { useState } from "react";
6+
import React, { useEffect, useState } from "react";
77
import PT from "prop-types";
88
import { Form, Field, useField } from "react-final-form";
99
import { useDispatch } from "react-redux";
@@ -18,6 +18,7 @@ import { deleteSearchedRole } from "../../actions";
1818
import IconCrossLight from "../../../../assets/images/icon-cross-light.svg";
1919
import "./styles.module.scss";
2020
import NumberInput from "components/NumberInput";
21+
import validator from "./utils/validator";
2122

2223
const Error = ({ name }) => {
2324
const {
@@ -28,80 +29,30 @@ const Error = ({ name }) => {
2829

2930
function TeamDetailsModal({ open, onClose, submitForm, addedRoles }) {
3031
const [showDescription, setShowDescription] = useState(false);
31-
const [startMonthVisible, setStartMonthVisible] = useState(() => {
32-
const roles = {};
33-
addedRoles.forEach(({ searchId }) => {
34-
roles[searchId] = false;
35-
});
36-
return roles;
37-
});
32+
const [startMonthVisible, setStartMonthVisible] = useState({});
33+
34+
// Ensure role is removed from form state when it is removed from redux store
35+
let getFormState;
36+
let clearFormField;
37+
useEffect(() => {
38+
const values = getFormState().values;
39+
for (let fieldName of Object.keys(values)) {
40+
if (fieldName === "teamName" || fieldName === "teamDescription") {
41+
continue;
42+
}
43+
if (addedRoles.findIndex((role) => role.searchId === fieldName)) {
44+
clearFormField(fieldName);
45+
setStartMonthVisible((state) => ({ ...state, [fieldName]: false }));
46+
}
47+
}
48+
}, [getFormState, addedRoles, clearFormField]);
3849

3950
const dispatch = useDispatch();
4051

4152
const toggleDescription = () => {
4253
setShowDescription((prevState) => !prevState);
4354
};
4455

45-
const validateName = (name) => {
46-
if (!name || name.trim().length === 0) {
47-
return "Please enter a team name.";
48-
}
49-
return undefined;
50-
};
51-
52-
const validateNumber = (number) => {
53-
const converted = Number(number);
54-
55-
if (
56-
Number.isNaN(converted) ||
57-
converted !== Math.floor(converted) ||
58-
converted < 1
59-
) {
60-
return "Please enter a positive integer";
61-
}
62-
return undefined;
63-
};
64-
65-
const validateMonth = (monthString) => {
66-
const then = new Date(monthString);
67-
const now = new Date();
68-
const thenYear = then.getFullYear();
69-
const nowYear = now.getFullYear();
70-
const thenMonth = then.getMonth();
71-
const nowMonth = now.getMonth();
72-
73-
if (thenYear < nowYear || (thenYear === nowYear && thenMonth < nowMonth)) {
74-
return "Start month may not be before current month";
75-
}
76-
return undefined;
77-
};
78-
79-
const validateRole = (role) => {
80-
const roleErrors = {};
81-
roleErrors.numberOfResources = validateNumber(role.numberOfResources);
82-
roleErrors.durationWeeks = validateNumber(role.durationWeeks);
83-
if (role.startMonth) {
84-
roleErrors.startMonth = validateMonth(role.startMonth);
85-
}
86-
87-
return roleErrors;
88-
};
89-
90-
const validator = (values) => {
91-
const errors = {};
92-
93-
errors.teamName = validateName(values.teamName);
94-
95-
for (const key of Object.keys(values)) {
96-
if (key === "teamDescription" || key === "teamName") continue;
97-
errors[key] = validateRole(values[key]);
98-
}
99-
100-
return errors;
101-
};
102-
103-
const validateRequired = value => (value ? undefined : 'Please enter a positive integer')
104-
10556
return (
10657
<Form
10758
onSubmit={submitForm}
@@ -110,16 +61,18 @@ function TeamDetailsModal({ open, onClose, submitForm, addedRoles }) {
11061
changeValue(state, fieldName, () => undefined);
11162
},
11263
}}
113-
initialValues={{ teamName: "" }}
11464
validate={validator}
11565
>
11666
{({
11767
handleSubmit,
11868
hasValidationErrors,
11969
form: {
12070
mutators: { clearField },
71+
getState,
12172
},
12273
}) => {
74+
getFormState = getState;
75+
clearFormField = clearField;
12376
return (
12477
<BaseCreateModal
12578
open={open}
@@ -136,6 +89,7 @@ function TeamDetailsModal({ open, onClose, submitForm, addedRoles }) {
13689
Submit
13790
</Button>
13891
}
92+
disableFocusTrap
13993
>
14094
<div styleName="modal-body">
14195
<FormField
@@ -183,7 +137,7 @@ function TeamDetailsModal({ open, onClose, submitForm, addedRoles }) {
183137
<tr styleName="role-row" key={id}>
184138
<td>{name}</td>
185139
<td>
186-
<Field validate={validateRequired} name={`${id}.numberOfResources`} initialValue="3">
140+
<Field name={`${id}.numberOfResources`} initialValue="3">
187141
{({ input, meta }) => (
188142
<NumberInput
189143
name={input.name}
@@ -199,7 +153,7 @@ function TeamDetailsModal({ open, onClose, submitForm, addedRoles }) {
199153
<Error name={`${id}.numberOfResources`} />
200154
</td>
201155
<td>
202-
<Field validate={validateRequired} name={`${id}.durationWeeks`} initialValue="20">
156+
<Field name={`${id}.durationWeeks`} initialValue="20">
203157
{({ input, meta }) => (
204158
<NumberInput
205159
name={input.name}
@@ -257,7 +211,6 @@ function TeamDetailsModal({ open, onClose, submitForm, addedRoles }) {
257211
<button
258212
styleName="delete-role"
259213
onClick={() => {
260-
clearField(id);
261214
dispatch(deleteSearchedRole(id));
262215
}}
263216
>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
const validateName = (name) => {
2+
if (!name || name.trim().length === 0) {
3+
return "Please enter a team name.";
4+
}
5+
return undefined;
6+
};
7+
8+
const validateNumber = (number) => {
9+
const converted = Number(number);
10+
11+
if (
12+
!number ||
13+
Number.isNaN(converted) ||
14+
converted !== Math.floor(converted) ||
15+
converted < 1
16+
) {
17+
return "Please enter a positive integer";
18+
}
19+
return undefined;
20+
};
21+
22+
const validateMonth = (monthString) => {
23+
const then = new Date(monthString);
24+
const now = new Date();
25+
const thenYear = then.getFullYear();
26+
const nowYear = now.getFullYear();
27+
const thenMonth = then.getMonth();
28+
const nowMonth = now.getMonth();
29+
30+
if (thenYear < nowYear || (thenYear === nowYear && thenMonth < nowMonth)) {
31+
return "Start month may not be before current month";
32+
}
33+
return undefined;
34+
};
35+
36+
const validateRole = (role) => {
37+
const roleErrors = {};
38+
roleErrors.numberOfResources = validateNumber(role.numberOfResources);
39+
roleErrors.durationWeeks = validateNumber(role.durationWeeks);
40+
if (role.startMonth) {
41+
roleErrors.startMonth = validateMonth(role.startMonth);
42+
}
43+
44+
return roleErrors;
45+
};
46+
47+
const validator = (values) => {
48+
const errors = {};
49+
50+
errors.teamName = validateName(values.teamName);
51+
52+
for (const key of Object.keys(values)) {
53+
if (key === "teamDescription" || key === "teamName") continue;
54+
errors[key] = validateRole(values[key]);
55+
}
56+
57+
return errors;
58+
};
59+
60+
export default validator;

0 commit comments

Comments
 (0)