From 9be44a1a17497c8f505cc49e45f99bef7f1108a6 Mon Sep 17 00:00:00 2001 From: Michael Baghel Date: Wed, 30 Jun 2021 15:51:08 +0400 Subject: [PATCH 1/5] Remove some unused props and imports. --- .../components/InputContainer/index.jsx | 6 +---- .../components/SearchAndSubmit/index.jsx | 23 +++++++++++-------- .../components/SearchContainer/index.jsx | 4 ---- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/routes/CreateNewTeam/components/InputContainer/index.jsx b/src/routes/CreateNewTeam/components/InputContainer/index.jsx index af03d23c..f31cc862 100644 --- a/src/routes/CreateNewTeam/components/InputContainer/index.jsx +++ b/src/routes/CreateNewTeam/components/InputContainer/index.jsx @@ -5,14 +5,10 @@ * input pages. Contains logic and supporting * components for selecting for roles. */ -import React, { useCallback } from "react"; +import React from "react"; import PT from "prop-types"; import AddedRolesAccordion from "../AddedRolesAccordion"; import Completeness from "../Completeness"; -import SearchCard from "../SearchCard"; -import ResultCard from "../ResultCard"; -import NoMatchingProfilesResultCard from "../NoMatchingProfilesResultCard"; -import { isCustomRole } from "utils/helpers"; import "./styles.module.scss"; function InputContainer({ diff --git a/src/routes/CreateNewTeam/components/SearchAndSubmit/index.jsx b/src/routes/CreateNewTeam/components/SearchAndSubmit/index.jsx index 339d725a..be76462b 100644 --- a/src/routes/CreateNewTeam/components/SearchAndSubmit/index.jsx +++ b/src/routes/CreateNewTeam/components/SearchAndSubmit/index.jsx @@ -4,7 +4,12 @@ import React, { useCallback, useState, useEffect } from "react"; import { useDispatch, useSelector } from "react-redux"; import { searchRoles } from "services/teams"; import { isCustomRole, setCurrentStage } from "utils/helpers"; -import { clearMatchingRole, saveMatchingRole, addRoleSearchId, addSearchedRole } from "../../actions"; +import { + clearMatchingRole, + saveMatchingRole, + addRoleSearchId, + addSearchedRole, +} from "../../actions"; import InputContainer from "../InputContainer"; import SearchContainer from "../SearchContainer"; import SubmitContainer from "../SubmitContainer"; @@ -14,19 +19,19 @@ function SearchAndSubmit(props) { const [searchState, setSearchState] = useState(null); - const { matchingRole } = useSelector( - (state) => state.searchedRoles - ); + const { matchingRole } = useSelector((state) => state.searchedRoles); - useEffect(()=> { - const isFromInputPage = searchObject.role || searchObject.skills && searchObject.skills.length - || searchObject.jobDescription + useEffect(() => { + const isFromInputPage = + searchObject.role || + (searchObject.skills && searchObject.skills.length) || + searchObject.jobDescription; // refresh in search page directly if (matchingRole && !isFromInputPage) { setCurrentStage(2, stages, setStages); setSearchState("done"); } - }, []) + }, []); const dispatch = useDispatch(); @@ -52,8 +57,6 @@ function SearchAndSubmit(props) { } else if (searchId) { dispatch(addRoleSearchId(searchId)); } - // setMatchingRole(res.data); - dispatch(saveMatchingRole(res.data)); }) .catch((err) => { diff --git a/src/routes/CreateNewTeam/components/SearchContainer/index.jsx b/src/routes/CreateNewTeam/components/SearchContainer/index.jsx index 141cdba1..b08b0913 100644 --- a/src/routes/CreateNewTeam/components/SearchContainer/index.jsx +++ b/src/routes/CreateNewTeam/components/SearchContainer/index.jsx @@ -17,8 +17,6 @@ import "./styles.module.scss"; function SearchContainer({ stages, - isCompletenessDisabled, - toRender, onClick, search, completenessStyle, @@ -66,10 +64,8 @@ function SearchContainer({ SearchContainer.propTypes = { stages: PT.array, - isCompletenessDisabled: PT.bool, onClick: PT.func, search: PT.func, - toRender: PT.func, completenessStyle: PT.string, navigate: PT.func, addedRoles: PT.array, From a01c56b70c27b95bccd03e38cbe10f69a5fe18b1 Mon Sep 17 00:00:00 2001 From: Michael Baghel Date: Wed, 30 Jun 2021 16:20:39 +0400 Subject: [PATCH 2/5] Require login at Continue stage of create new team. Remove more unused code. --- .../components/InputContainer/index.jsx | 2 +- .../components/SearchAndSubmit/index.jsx | 2 -- .../components/SearchContainer/index.jsx | 25 +++++++++++++------ .../components/SubmitContainer/index.jsx | 6 ++--- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/routes/CreateNewTeam/components/InputContainer/index.jsx b/src/routes/CreateNewTeam/components/InputContainer/index.jsx index f31cc862..8ae24bce 100644 --- a/src/routes/CreateNewTeam/components/InputContainer/index.jsx +++ b/src/routes/CreateNewTeam/components/InputContainer/index.jsx @@ -28,7 +28,7 @@ function InputContainer({ isDisabled={isCompletenessDisabled} onClick={search} extraStyleName={completenessStyle} - buttonLabel={"Search"} + buttonLabel="Search" stages={stages} percentage="26" /> diff --git a/src/routes/CreateNewTeam/components/SearchAndSubmit/index.jsx b/src/routes/CreateNewTeam/components/SearchAndSubmit/index.jsx index be76462b..bfefd418 100644 --- a/src/routes/CreateNewTeam/components/SearchAndSubmit/index.jsx +++ b/src/routes/CreateNewTeam/components/SearchAndSubmit/index.jsx @@ -81,8 +81,6 @@ function SearchAndSubmit(props) { { + setAddAnotherOpen(false); navigate("../result"); }, [navigate]); + const addAnother = useCallback(() => { + navigate("/taas/createnewteam"); + }, [navigate]); + const renderLeftSide = () => { if (searchState === "searching") return ; if (!isCustomRole(matchingRole)) return ; @@ -51,21 +57,26 @@ function SearchContainer({ searchState === "searching" || (searchState === "done" && (!addedRoles || !addedRoles.length)) } - onClick={searchState ? onSubmit : onClick ? onClick : search} + onClick={() => setAddAnotherOpen(true)} extraStyleName={completenessStyle} - buttonLabel={searchState ? "Submit Request" : "Search"} + buttonLabel="Submit Request" stages={stages} percentage={getPercentage()} /> + setAddAnotherOpen(false)} + submitDone={true} + onContinueClick={onSubmit} + addAnother={addAnother} + /> ); } SearchContainer.propTypes = { stages: PT.array, - onClick: PT.func, - search: PT.func, completenessStyle: PT.string, navigate: PT.func, addedRoles: PT.array, diff --git a/src/routes/CreateNewTeam/components/SubmitContainer/index.jsx b/src/routes/CreateNewTeam/components/SubmitContainer/index.jsx index f739dcac..c682ac62 100644 --- a/src/routes/CreateNewTeam/components/SubmitContainer/index.jsx +++ b/src/routes/CreateNewTeam/components/SubmitContainer/index.jsx @@ -35,8 +35,8 @@ function SubmitContainer({ matchingRole, addedRoles, }) { - const [addAnotherOpen, setAddAnotherOpen] = useState(true); - const [teamDetailsOpen, setTeamDetailsOpen] = useState(false); + const [addAnotherOpen, setAddAnotherOpen] = useState(false); + const [teamDetailsOpen, setTeamDetailsOpen] = useState(true); const [teamObject, setTeamObject] = useState(null); const [requestLoading, setRequestLoading] = useState(false); @@ -99,7 +99,7 @@ function SubmitContainer({ dispatch(clearSearchedRoles()); // Backend api create project has sync issue, so delay 2 seconds navigate("/taas/myteams"); - }, 2000) + }, 2000); }) .catch((err) => { setRequestLoading(false); From b47476b15f77dad60e444a6987fb27909babeb50 Mon Sep 17 00:00:00 2001 From: Michael Baghel Date: Thu, 1 Jul 2021 11:59:14 +0400 Subject: [PATCH 3/5] Allow user to add custom role to added roles list from No Matching Profiles card. --- src/routes/CreateNewTeam/actions/index.js | 4 +- .../NoMatchingProfilesResultCard/index.jsx | 50 +++++++++++++++++-- .../styles.module.scss | 22 +++++++- .../components/SubmitContainer/index.jsx | 4 +- 4 files changed, 69 insertions(+), 11 deletions(-) diff --git a/src/routes/CreateNewTeam/actions/index.js b/src/routes/CreateNewTeam/actions/index.js index b90aae4e..0a224017 100644 --- a/src/routes/CreateNewTeam/actions/index.js +++ b/src/routes/CreateNewTeam/actions/index.js @@ -32,7 +32,7 @@ const addMatchingRole = (matchingRole) => ({ payload: matchingRole, }); -const deleteMatchingRole = (matchingRole) => ({ +const deleteMatchingRole = () => ({ type: ACTION_TYPE.DELETE_MATCHING_ROLE, }); @@ -61,7 +61,7 @@ export const saveMatchingRole = (matchingRole) => (dispatch, getState) => { updateLocalStorage(getState().searchedRoles); }; -export const clearMatchingRole = (matchingRole) => (dispatch, getState) => { +export const clearMatchingRole = () => (dispatch, getState) => { dispatch(deleteMatchingRole()); updateLocalStorage(getState().searchedRoles); }; diff --git a/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/index.jsx b/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/index.jsx index 4daee929..2085ab7c 100644 --- a/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/index.jsx +++ b/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/index.jsx @@ -2,9 +2,11 @@ * No Matching Profiles Result Card * Card that appears when there are no matching profiles after searching. */ -import React from "react"; +import React, { useCallback, useMemo } from "react"; import { Link } from "@reach/router"; import PT from "prop-types"; +import { useDispatch, useSelector } from "react-redux"; +import { addSearchedRole } from "../../actions"; import "./styles.module.scss"; import IconEarthX from "../../../../assets/images/icon-earth-x.svg"; import Curve from "../../../../assets/images/curve.svg"; @@ -12,6 +14,30 @@ import Button from "components/Button"; import { formatMoney } from "utils/format"; function NoMatchingProfilesResultCard({ role }) { + const { addedRoles } = useSelector((state) => state.searchedRoles); + + const alreadyAdded = useMemo(() => { + if ( + addedRoles.find( + (addedRole) => addedRole.searchId === role.roleSearchRequestId + ) + ) { + return true; + } + return false; + }, [addedRoles, role]); + + const dispatch = useDispatch(); + + const addRole = useCallback(() => { + const searchId = role.roleSearchRequestId; + let name = "Custom Role"; + if (role.jobTitle && role.jobTitle.length) { + name = role.jobTitle; + } + dispatch(addSearchedRole({ searchId, name })); + }, [dispatch, role]); + return (
@@ -21,6 +47,11 @@ function NoMatchingProfilesResultCard({ role }) {
+

+ {role.jobTitle && role.jobTitle.length + ? role.jobTitle + : "Custom Role"} +

We will be looking internally for members matching your requirements and be back at them in about 2 weeks. @@ -38,11 +69,20 @@ function NoMatchingProfilesResultCard({ role }) {

/Week

)} - - + + - +
); diff --git a/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/styles.module.scss b/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/styles.module.scss index 7334e07f..b39ab76b 100644 --- a/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/styles.module.scss +++ b/src/routes/CreateNewTeam/components/NoMatchingProfilesResultCard/styles.module.scss @@ -13,7 +13,7 @@ justify-content: flex-start; align-items: center; padding: 30px 0 60px 0; - margin-bottom: 30px; + margin-bottom: 14px; color: #fff; background-image: linear-gradient(225deg, #555555 0%, #2A2A2A 100%); position: relative; @@ -41,6 +41,17 @@ flex-direction: column; align-items: center; padding-bottom: 61px; + .job-title { + @include font-barlow; + font-size: 22px; + margin-bottom: 18px; + font-weight: 600; + text-align: center; + text-transform: uppercase; + // position text over bottom of header image + position: relative; + z-index: 100; + } p.info-txt { @include font-roboto; font-size: 14px; @@ -82,8 +93,15 @@ } } - .button { + .button-group { margin-top: 62px; + display: flex; + flex-direction: row; + align-items: center; + + .left { + margin-right: 30px; + } } } diff --git a/src/routes/CreateNewTeam/components/SubmitContainer/index.jsx b/src/routes/CreateNewTeam/components/SubmitContainer/index.jsx index c682ac62..b7bfdc40 100644 --- a/src/routes/CreateNewTeam/components/SubmitContainer/index.jsx +++ b/src/routes/CreateNewTeam/components/SubmitContainer/index.jsx @@ -26,7 +26,7 @@ import "./styles.module.scss"; import { isCustomRole, setCurrentStage } from "utils/helpers"; import { clearSearchedRoles } from "../../actions"; import { postTeamRequest } from "services/teams"; -import SuccessCard from "../SuccessCard"; +import NoMatchingProfilesResultCard from "../NoMatchingProfilesResultCard"; function SubmitContainer({ stages, @@ -112,7 +112,7 @@ function SubmitContainer({ {!isCustomRole(matchingRole) ? ( ) : ( - + )}
From 1313b50c6dd56720d6eb7c7e4bf64c453a28b092 Mon Sep 17 00:00:00 2001 From: Michael Baghel Date: Thu, 1 Jul 2021 17:33:33 +0400 Subject: [PATCH 4/5] 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. --- .../components/AddedRolesAccordion/index.jsx | 14 ++- .../AddedRolesAccordion/styles.module.scss | 17 +++- .../components/BaseCreateModal/index.jsx | 5 +- .../components/TeamDetailsModal/index.jsx | 97 +++++-------------- .../TeamDetailsModal/utils/validator.js | 60 ++++++++++++ 5 files changed, 117 insertions(+), 76 deletions(-) create mode 100644 src/routes/CreateNewTeam/components/TeamDetailsModal/utils/validator.js diff --git a/src/routes/CreateNewTeam/components/AddedRolesAccordion/index.jsx b/src/routes/CreateNewTeam/components/AddedRolesAccordion/index.jsx index ece5e080..1618b2b3 100644 --- a/src/routes/CreateNewTeam/components/AddedRolesAccordion/index.jsx +++ b/src/routes/CreateNewTeam/components/AddedRolesAccordion/index.jsx @@ -8,11 +8,16 @@ import React, { useState } from "react"; import PT from "prop-types"; import cn from "classnames"; +import { useDispatch } from "react-redux"; +import { deleteSearchedRole } from "../../actions"; import "./styles.module.scss"; +import IconCrossLight from "../../../../assets/images/icon-cross-light.svg"; function AddedRolesAccordion({ addedRoles }) { const [isOpen, setIsOpen] = useState(false); + const dispatch = useDispatch(); + return addedRoles.length ? (
{isOpen && (
- {addedRoles.map(({ name }) => ( -
{name}
+ {addedRoles.map(({ name, searchId: id }) => ( +
+ {name} + +
))}
)} diff --git a/src/routes/CreateNewTeam/components/AddedRolesAccordion/styles.module.scss b/src/routes/CreateNewTeam/components/AddedRolesAccordion/styles.module.scss index 6478f1f0..67f7c05e 100644 --- a/src/routes/CreateNewTeam/components/AddedRolesAccordion/styles.module.scss +++ b/src/routes/CreateNewTeam/components/AddedRolesAccordion/styles.module.scss @@ -55,11 +55,12 @@ .panel { padding: 12px 18px 14px 10px; .role-name { - height: 40px; + position: relative; width: 100%; background-color: #F4F4F4; border-radius: 6px; padding: 10px; + padding-right: 30px; @include font-barlow; font-size: 16px; line-height: 20px; @@ -68,5 +69,19 @@ &:not(:first-child) { margin-top: 5px; } + + >button { + outline: none; + border: none; + background: none; + position: absolute; + top: 12px; + right: 4px; + &:hover { + g { + stroke: red; + } + } + } } } \ No newline at end of file diff --git a/src/routes/CreateNewTeam/components/BaseCreateModal/index.jsx b/src/routes/CreateNewTeam/components/BaseCreateModal/index.jsx index d7fcecbb..17528b3e 100644 --- a/src/routes/CreateNewTeam/components/BaseCreateModal/index.jsx +++ b/src/routes/CreateNewTeam/components/BaseCreateModal/index.jsx @@ -35,6 +35,7 @@ function BaseCreateModal({ loadingMessage, maxWidth = "680px", darkHeader, + disableFocusTrap, children, }) { return ( @@ -51,8 +52,9 @@ function BaseCreateModal({ modalContainer: containerStyle, closeButton: closeButtonStyle, }} + focusTrapped={!disableFocusTrap} > -
+
{isLoading ? (
@@ -86,6 +88,7 @@ BaseCreateModal.propTypes = { loadingMessage: PT.string, maxWidth: PT.string, darkHeader: PT.bool, + disableFocusTrap: PT.bool, children: PT.node, }; diff --git a/src/routes/CreateNewTeam/components/TeamDetailsModal/index.jsx b/src/routes/CreateNewTeam/components/TeamDetailsModal/index.jsx index 16215341..ef97a714 100644 --- a/src/routes/CreateNewTeam/components/TeamDetailsModal/index.jsx +++ b/src/routes/CreateNewTeam/components/TeamDetailsModal/index.jsx @@ -3,7 +3,7 @@ * Popup form to enter details about the * team request before submitting. */ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import PT from "prop-types"; import { Form, Field, useField } from "react-final-form"; import { useDispatch } from "react-redux"; @@ -18,6 +18,7 @@ import { deleteSearchedRole } from "../../actions"; import IconCrossLight from "../../../../assets/images/icon-cross-light.svg"; import "./styles.module.scss"; import NumberInput from "components/NumberInput"; +import validator from "./utils/validator"; const Error = ({ name }) => { const { @@ -28,13 +29,23 @@ const Error = ({ name }) => { function TeamDetailsModal({ open, onClose, submitForm, addedRoles }) { const [showDescription, setShowDescription] = useState(false); - const [startMonthVisible, setStartMonthVisible] = useState(() => { - const roles = {}; - addedRoles.forEach(({ searchId }) => { - roles[searchId] = false; - }); - return roles; - }); + const [startMonthVisible, setStartMonthVisible] = useState({}); + + // Ensure role is removed from form state when it is removed from redux store + let getFormState; + let clearFormField; + useEffect(() => { + const values = getFormState().values; + for (let fieldName of Object.keys(values)) { + if (fieldName === "teamName" || fieldName === "teamDescription") { + continue; + } + if (addedRoles.findIndex((role) => role.searchId === fieldName)) { + clearFormField(fieldName); + setStartMonthVisible((state) => ({ ...state, [fieldName]: false })); + } + } + }, [getFormState, addedRoles, clearFormField]); const dispatch = useDispatch(); @@ -42,66 +53,6 @@ function TeamDetailsModal({ open, onClose, submitForm, addedRoles }) { setShowDescription((prevState) => !prevState); }; - const validateName = (name) => { - if (!name || name.trim().length === 0) { - return "Please enter a team name."; - } - return undefined; - }; - - const validateNumber = (number) => { - const converted = Number(number); - - if ( - Number.isNaN(converted) || - converted !== Math.floor(converted) || - converted < 1 - ) { - return "Please enter a positive integer"; - } - return undefined; - }; - - const validateMonth = (monthString) => { - const then = new Date(monthString); - const now = new Date(); - const thenYear = then.getFullYear(); - const nowYear = now.getFullYear(); - const thenMonth = then.getMonth(); - const nowMonth = now.getMonth(); - - if (thenYear < nowYear || (thenYear === nowYear && thenMonth < nowMonth)) { - return "Start month may not be before current month"; - } - return undefined; - }; - - const validateRole = (role) => { - const roleErrors = {}; - roleErrors.numberOfResources = validateNumber(role.numberOfResources); - roleErrors.durationWeeks = validateNumber(role.durationWeeks); - if (role.startMonth) { - roleErrors.startMonth = validateMonth(role.startMonth); - } - - return roleErrors; - }; - - const validator = (values) => { - const errors = {}; - - errors.teamName = validateName(values.teamName); - - for (const key of Object.keys(values)) { - if (key === "teamDescription" || key === "teamName") continue; - errors[key] = validateRole(values[key]); - } - - return errors; - }; - - const validateRequired = value => (value ? undefined : 'Please enter a positive integer') - return (
undefined); }, }} - initialValues={{ teamName: "" }} validate={validator} > {({ @@ -118,8 +68,11 @@ function TeamDetailsModal({ open, onClose, submitForm, addedRoles }) { hasValidationErrors, form: { mutators: { clearField }, + getState, }, }) => { + getFormState = getState; + clearFormField = clearField; return ( } + disableFocusTrap >
{name} - + {({ input, meta }) => ( - + {({ input, meta }) => ( { - clearField(id); dispatch(deleteSearchedRole(id)); }} > diff --git a/src/routes/CreateNewTeam/components/TeamDetailsModal/utils/validator.js b/src/routes/CreateNewTeam/components/TeamDetailsModal/utils/validator.js new file mode 100644 index 00000000..ac4f7646 --- /dev/null +++ b/src/routes/CreateNewTeam/components/TeamDetailsModal/utils/validator.js @@ -0,0 +1,60 @@ +const validateName = (name) => { + if (!name || name.trim().length === 0) { + return "Please enter a team name."; + } + return undefined; +}; + +const validateNumber = (number) => { + const converted = Number(number); + + if ( + !number || + Number.isNaN(converted) || + converted !== Math.floor(converted) || + converted < 1 + ) { + return "Please enter a positive integer"; + } + return undefined; +}; + +const validateMonth = (monthString) => { + const then = new Date(monthString); + const now = new Date(); + const thenYear = then.getFullYear(); + const nowYear = now.getFullYear(); + const thenMonth = then.getMonth(); + const nowMonth = now.getMonth(); + + if (thenYear < nowYear || (thenYear === nowYear && thenMonth < nowMonth)) { + return "Start month may not be before current month"; + } + return undefined; +}; + +const validateRole = (role) => { + const roleErrors = {}; + roleErrors.numberOfResources = validateNumber(role.numberOfResources); + roleErrors.durationWeeks = validateNumber(role.durationWeeks); + if (role.startMonth) { + roleErrors.startMonth = validateMonth(role.startMonth); + } + + return roleErrors; +}; + +const validator = (values) => { + const errors = {}; + + errors.teamName = validateName(values.teamName); + + for (const key of Object.keys(values)) { + if (key === "teamDescription" || key === "teamName") continue; + errors[key] = validateRole(values[key]); + } + + return errors; +}; + +export default validator; From 8f8971cf04515ef088aaa69a2fa556b68de35bda Mon Sep 17 00:00:00 2001 From: Michael Baghel Date: Thu, 1 Jul 2021 19:36:22 +0400 Subject: [PATCH 5/5] fix: only clear form data for removed search role --- src/routes/CreateNewTeam/components/TeamDetailsModal/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/CreateNewTeam/components/TeamDetailsModal/index.jsx b/src/routes/CreateNewTeam/components/TeamDetailsModal/index.jsx index ef97a714..b2859592 100644 --- a/src/routes/CreateNewTeam/components/TeamDetailsModal/index.jsx +++ b/src/routes/CreateNewTeam/components/TeamDetailsModal/index.jsx @@ -40,7 +40,7 @@ function TeamDetailsModal({ open, onClose, submitForm, addedRoles }) { if (fieldName === "teamName" || fieldName === "teamDescription") { continue; } - if (addedRoles.findIndex((role) => role.searchId === fieldName)) { + if (addedRoles.findIndex((role) => role.searchId === fieldName) === -1) { clearFormField(fieldName); setStartMonthVisible((state) => ({ ...state, [fieldName]: false })); }