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/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/InputContainer/index.jsx b/src/routes/CreateNewTeam/components/InputContainer/index.jsx index f5d69e62..cebc1551 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({ @@ -33,7 +29,7 @@ function InputContainer({ isDisabled={isCompletenessDisabled} onClick={onClick ? onClick: search} extraStyleName={completenessStyle} - buttonLabel={"Search"} + buttonLabel="Search" stages={stages} percentage="26" /> 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/SearchAndSubmit/index.jsx b/src/routes/CreateNewTeam/components/SearchAndSubmit/index.jsx index 339d725a..bfefd418 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) => { @@ -78,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 ; @@ -53,23 +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, - isCompletenessDisabled: PT.bool, - onClick: PT.func, - search: PT.func, - toRender: 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..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, @@ -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); @@ -112,7 +112,7 @@ function SubmitContainer({ {!isCustomRole(matchingRole) ? ( ) : ( - + )}
diff --git a/src/routes/CreateNewTeam/components/TeamDetailsModal/index.jsx b/src/routes/CreateNewTeam/components/TeamDetailsModal/index.jsx index 16215341..b2859592 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) === -1) { + 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;