@@ -23,12 +25,14 @@ function NoMatchingProfilesResultCard() {
We will be looking internally for members matching your requirements
and be back at them in about 2 weeks.
-
-
Niche Rate
-
$1,200
-
/Week
-
-
+ {role && (
+
+
{role.name} Rate
+
{formatMoney(role.rates[0].global)}
+
/Week
+
+ )}
+
@@ -38,4 +42,8 @@ function NoMatchingProfilesResultCard() {
);
}
+NoMatchingProfilesResultCard.propTypes = {
+ role: PT.object,
+};
+
export default NoMatchingProfilesResultCard;
diff --git a/src/routes/CreateNewTeam/components/ResultCard/index.jsx b/src/routes/CreateNewTeam/components/ResultCard/index.jsx
index c6920c22..97e44dbf 100644
--- a/src/routes/CreateNewTeam/components/ResultCard/index.jsx
+++ b/src/routes/CreateNewTeam/components/ResultCard/index.jsx
@@ -15,7 +15,6 @@ import IconTeamMeetingChat from "../../../../assets/images/icon-team-meeting-cha
import Curve from "../../../../assets/images/curve.svg";
import CircularProgressBar from "../CircularProgressBar";
import Button from "components/Button";
-import { MATCHING_RATE } from "constants";
import { formatMoney } from "utils/format";
function formatRate(value) {
@@ -23,10 +22,15 @@ function formatRate(value) {
return formatMoney(value);
}
+function formatPercent(value) {
+ return `${Math.round(value * 100)}%`;
+}
+
function ResultCard({ role }) {
const {
numberOfMembersAvailable,
isExternalMember,
+ skillsMatch,
rates: [rates],
} = role;
const [userHandle, setUserHandle] = useState(null);
@@ -34,7 +38,7 @@ function ResultCard({ role }) {
useEffect(() => {
getAuthUserProfile().then((res) => {
- setUserHandle(res.handle || null);
+ setUserHandle(res?.handle || null);
});
}, []);
@@ -44,8 +48,8 @@ function ResultCard({ role }) {
We have matching profiles
- We have qualified candidates who match {MATCHING_RATE}% or more of
- your job requirements.
+ We have qualified candidates who match {formatPercent(skillsMatch)}
+ {skillsMatch < 1 ? " or more " : " "} of your job requirements.
@@ -217,12 +221,12 @@ function ResultCard({ role }) {
-
{MATCHING_RATE}%
-
Matching rate
+
{formatPercent(skillsMatch)}
+
Skills Match
}
/>
diff --git a/src/routes/CreateNewTeam/components/RoleDetailsModal/index.jsx b/src/routes/CreateNewTeam/components/RoleDetailsModal/index.jsx
index a10f2de7..206e4ee5 100644
--- a/src/routes/CreateNewTeam/components/RoleDetailsModal/index.jsx
+++ b/src/routes/CreateNewTeam/components/RoleDetailsModal/index.jsx
@@ -48,7 +48,7 @@ function RoleDetailsModal({ roleId, open, onClose }) {
[role, imgError]
);
- const skills = role ? role.listOfSkills : [];
+ const skills = role && role.listOfSkills ? role.listOfSkills : [];
const hideSkills = () => {
onClose();
diff --git a/src/routes/CreateNewTeam/components/RoleDetailsModal/styles.module.scss b/src/routes/CreateNewTeam/components/RoleDetailsModal/styles.module.scss
index e7470ce5..d8ad2bf7 100644
--- a/src/routes/CreateNewTeam/components/RoleDetailsModal/styles.module.scss
+++ b/src/routes/CreateNewTeam/components/RoleDetailsModal/styles.module.scss
@@ -14,12 +14,23 @@
}
.markdown-container {
- // not adds specificity to override style
- p:not(table) {
- @include font-roboto;
- color: #2a2a2a;
- font-size: 16px;
- line-height: 26px;
+ :global {
+ // resets styles in markdown-viewer
+ .tui-editor-contents {
+ @include font-roboto;
+ color: #2a2a2a;
+ font-size: 16px;
+ line-height: 26px;
+ ul {
+ list-style: initial;
+ >li {
+ margin-bottom: 10px;
+ &::before {
+ background: none;
+ }
+ }
+ }
+ }
}
}
diff --git a/src/routes/CreateNewTeam/components/SearchAndSubmit/index.jsx b/src/routes/CreateNewTeam/components/SearchAndSubmit/index.jsx
index 9f27e3ba..b801e2d1 100644
--- a/src/routes/CreateNewTeam/components/SearchAndSubmit/index.jsx
+++ b/src/routes/CreateNewTeam/components/SearchAndSubmit/index.jsx
@@ -1,13 +1,23 @@
import { Router } from "@reach/router";
import React from "react";
+import { useSelector } from "react-redux";
import SearchContainer from "../SearchContainer";
import SubmitContainer from "../SubmitContainer";
function SearchAndSubmit(props) {
+ const { addedRoles, previousSearchId } = useSelector(
+ (state) => state.searchedRoles
+ );
+
return (
-
-
+
+
);
}
diff --git a/src/routes/CreateNewTeam/components/SearchContainer/index.jsx b/src/routes/CreateNewTeam/components/SearchContainer/index.jsx
index 4cf96c6f..d31b2761 100644
--- a/src/routes/CreateNewTeam/components/SearchContainer/index.jsx
+++ b/src/routes/CreateNewTeam/components/SearchContainer/index.jsx
@@ -8,36 +8,17 @@
import React, { useCallback, useState } from "react";
import PT from "prop-types";
import _ from "lodash";
-import { useDispatch, useSelector } from "react-redux";
+import { useDispatch } from "react-redux";
import AddedRolesAccordion from "../AddedRolesAccordion";
import Completeness from "../Completeness";
import SearchCard from "../SearchCard";
import ResultCard from "../ResultCard";
import NoMatchingProfilesResultCard from "../NoMatchingProfilesResultCard";
import { searchRoles } from "services/teams";
-import { setCurrentStage } from "utils/helpers";
+import { isCustomRole, setCurrentStage } from "utils/helpers";
import { addRoleSearchId, addSearchedRole } from "../../actions";
import "./styles.module.scss";
-/**
- * Converts an array of role search objects to two data
- * lists which can be set as sessionStorage items
- *
- * @param {object[]} arrayOfObjects array of role objects
- */
-const storeStrings = (arrayOfObjects) => {
- const objectOfArrays = arrayOfObjects.reduce(
- (acc, curr) => ({
- searchId: [...acc.searchId, curr.searchId],
- name: [...acc.name, curr.name],
- }),
- { searchId: [], name: [] }
- );
-
- sessionStorage.setItem("searchIds", objectOfArrays.searchId.join(","));
- sessionStorage.setItem("roleNames", objectOfArrays.name.join(","));
-};
-
function SearchContainer({
stages,
setStages,
@@ -45,24 +26,18 @@ function SearchContainer({
toRender,
searchObject,
completenessStyle,
- reloadRolesPage,
navigate,
+ addedRoles,
+ previousSearchId,
}) {
- const { addedRoles, previousSearchId } = useSelector(
- (state) => state.searchedRoles
- );
-
const [searchState, setSearchState] = useState(null);
const [matchingRole, setMatchingRole] = useState(null);
- const [addAnotherModalOpen, setAddAnotherModalOpen] = useState(false);
- const [submitDone, setSubmitDone] = useState(true);
const dispatch = useDispatch();
const onSubmit = useCallback(() => {
- storeStrings(addedRoles);
navigate("result", { state: { matchingRole } });
- }, [addedRoles, navigate, matchingRole]);
+ }, [navigate, matchingRole]);
const search = () => {
setCurrentStage(1, stages, setStages);
@@ -76,12 +51,12 @@ function SearchContainer({
.then((res) => {
const name = _.get(res, "data.name");
const searchId = _.get(res, "data.roleSearchRequestId");
- if (name && !name.toLowerCase().includes("niche")) {
- setMatchingRole(res.data);
+ if (name && !isCustomRole({ name })) {
dispatch(addSearchedRole({ searchId, name }));
} else if (searchId) {
dispatch(addRoleSearchId(searchId));
}
+ setMatchingRole(res.data);
})
.catch((err) => {
console.error(err);
@@ -95,8 +70,8 @@ function SearchContainer({
const renderLeftSide = () => {
if (!searchState) return toRender;
if (searchState === "searching") return ;
- if (matchingRole) return ;
- return ;
+ if (!isCustomRole(matchingRole)) return ;
+ return ;
};
const getPercentage = useCallback(() => {
@@ -135,8 +110,9 @@ SearchContainer.propTypes = {
searchObject: PT.object,
toRender: PT.node,
completenessStyle: PT.string,
- reloadRolesPage: PT.func,
navigate: PT.func,
+ addedRoles: PT.array,
+ previousSearchId: PT.string,
};
export default SearchContainer;
diff --git a/src/routes/CreateNewTeam/components/SubmitContainer/index.jsx b/src/routes/CreateNewTeam/components/SubmitContainer/index.jsx
index a8223a29..e2922708 100644
--- a/src/routes/CreateNewTeam/components/SubmitContainer/index.jsx
+++ b/src/routes/CreateNewTeam/components/SubmitContainer/index.jsx
@@ -4,9 +4,14 @@
* Requires authentication to complete submission process
* and contains a series of popups to lead user through the flow.
*/
-import React, { useCallback, useEffect, useState } from "react";
+import React, {
+ useCallback,
+ useEffect,
+ useLayoutEffect,
+ useState,
+} from "react";
import PT from "prop-types";
-import { useDispatch, useSelector } from "react-redux";
+import { useDispatch } from "react-redux";
import _ from "lodash";
import { toastr } from "react-redux-toastr";
import { navigate } from "@reach/router";
@@ -19,46 +24,19 @@ import ConfirmationModal from "../ConfirmationModal";
import withAuthentication from "../../../../hoc/withAuthentication";
import "./styles.module.scss";
import { setCurrentStage } from "utils/helpers";
-import { clearSearchedRoles, replaceSearchedRoles } from "../../actions";
+import { clearSearchedRoles } from "../../actions";
import { postTeamRequest } from "services/teams";
import SuccessCard from "../SuccessCard";
-const retrieveRoles = () => {
- const searchIdString = sessionStorage.getItem("searchIds");
- const nameString = sessionStorage.getItem("roleNames");
-
- if (!searchIdString || !nameString) return [];
- const searchIds = searchIdString.split(",");
- const names = nameString.split(",");
- if (searchIds.length !== names.length) return [];
-
- const roles = [];
- for (let i = 0; i < searchIds.length; i++) {
- roles.push({
- searchId: searchIds[i],
- name: names[i],
- });
- }
-
- return roles;
-};
-
-const clearSessionKeys = () => {
- sessionStorage.removeItem("searchIds");
- sessionStorage.removeItem("roleNames");
-};
-
function SubmitContainer({
stages,
setStages,
completenessStyle,
- reloadRolesPage,
location,
+ addedRoles,
}) {
const matchingRole = location?.state?.matchingRole;
- const { addedRoles } = useSelector((state) => state.searchedRoles);
-
const [addAnotherOpen, setAddAnotherOpen] = useState(true);
const [teamDetailsOpen, setTeamDetailsOpen] = useState(false);
const [teamObject, setTeamObject] = useState(null);
@@ -68,11 +46,17 @@ function SubmitContainer({
useEffect(() => {
setCurrentStage(2, stages, setStages);
- const storedRoles = retrieveRoles();
- if (storedRoles) {
- if (!addedRoles || storedRoles.length > addedRoles.length) {
- dispatch(replaceSearchedRoles(storedRoles));
- }
+ if (!addedRoles || addedRoles.length === 0) {
+ navigate("/taas/myteams/createnewteam");
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ // redirects user if they enter the page URL directly
+ // without adding any roles.
+ useLayoutEffect(() => {
+ if (!addedRoles || addedRoles.length === 0) {
+ navigate("/taas/myteams/createnewteam");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -83,11 +67,7 @@ function SubmitContainer({
};
const addAnother = () => {
- if (reloadRolesPage) {
- setCurrentStage(0, stages, setStages);
- reloadRolesPage();
- }
- navigate("/taas/myteams/createnewteam/role");
+ navigate("/taas/myteams/createnewteam");
};
const assembleTeam = (formData) => {
@@ -98,13 +78,14 @@ function SubmitContainer({
if (key === "teamName" || key === "teamDescription") {
continue;
}
- const position = _.pick(
- formData[key],
- "numberOfResources",
- "durationWeeks",
- "startMonth"
+ const position = _.mapValues(formData[key], (val, key) =>
+ key === "startMonth" ? val : parseInt(val, 10)
);
+ if (position.startMonth === null) {
+ delete position.startMonth;
+ }
+
position.roleSearchRequestId = key;
position.roleName = addedRoles.find((role) => role.searchId === key).name;
@@ -121,7 +102,6 @@ function SubmitContainer({
postTeamRequest(teamObject)
.then((res) => {
const projectId = _.get(res, ["data", "projectId"]);
- clearSessionKeys();
dispatch(clearSearchedRoles());
navigate(`/taas/myteams/${projectId}`);
})
@@ -171,8 +151,8 @@ SubmitContainer.propTypes = {
stages: PT.array,
setStages: PT.func,
completenessStyle: PT.string,
- reloadRolesPage: PT.bool,
location: PT.object,
+ addedRoles: PT.array,
};
export default withAuthentication(SubmitContainer);
diff --git a/src/routes/CreateNewTeam/components/SuccessCard/index.jsx b/src/routes/CreateNewTeam/components/SuccessCard/index.jsx
index 1aa05fa8..41feb1b4 100644
--- a/src/routes/CreateNewTeam/components/SuccessCard/index.jsx
+++ b/src/routes/CreateNewTeam/components/SuccessCard/index.jsx
@@ -8,7 +8,6 @@ import React from "react";
import { Link } from "@reach/router";
import IconEarthCheck from "../../../../assets/images/icon-earth-check.svg";
import Curve from "../../../../assets/images/curve.svg";
-import { MATCHING_RATE } from "constants";
import "./styles.module.scss";
import Button from "components/Button";
@@ -18,10 +17,7 @@ function SuccessCard() {
We have matching profiles
-
- We have qualified candidates who match {MATCHING_RATE}% or more of
- your job requirements.
-
+
We have qualified candidates who match your job requirements.
@@ -30,7 +26,7 @@ function SuccessCard() {
Please use the button to the right to submit your request, or the
button below to search for additional roles.
-
+
diff --git a/src/routes/CreateNewTeam/components/TeamDetailsModal/index.jsx b/src/routes/CreateNewTeam/components/TeamDetailsModal/index.jsx
index 716d375d..6c599e67 100644
--- a/src/routes/CreateNewTeam/components/TeamDetailsModal/index.jsx
+++ b/src/routes/CreateNewTeam/components/TeamDetailsModal/index.jsx
@@ -11,13 +11,15 @@ import BaseCreateModal from "../BaseCreateModal";
import { FORM_FIELD_TYPE } from "constants/";
import { formatPlural } from "utils/format";
import Button from "components/Button";
+import MonthPicker from "components/MonthPicker";
+import InformationTooltip from "components/InformationTooltip";
import "./styles.module.scss";
const Error = ({ name }) => {
const {
- meta: { touched, error },
- } = useField(name, { subscription: { touched: true, error: true } });
- return touched && error ? {error} : null;
+ meta: { dirty, error },
+ } = useField(name, { subscription: { dirty: true, error: true } });
+ return dirty && error ? {error} : null;
};
function TeamDetailsModal({ open, onClose, submitForm, addedRoles }) {
@@ -95,10 +97,21 @@ function TeamDetailsModal({ open, onClose, submitForm, addedRoles }) {
return (
>
diff --git a/src/routes/CreateNewTeam/pages/InputSkills/components/SkillItem/index.jsx b/src/routes/CreateNewTeam/pages/InputSkills/components/SkillItem/index.jsx
index ec94cea5..66a819e7 100644
--- a/src/routes/CreateNewTeam/pages/InputSkills/components/SkillItem/index.jsx
+++ b/src/routes/CreateNewTeam/pages/InputSkills/components/SkillItem/index.jsx
@@ -5,7 +5,7 @@
*/
import React from "react";
import PT from "prop-types";
-import IconQuestionCircle from "../../../../../../assets/images/icon-question-circle.svg";
+import IconSkill from "../../../../../../assets/images/icon-skill.svg";
import "./styles.module.scss";
import cn from "classnames";
@@ -28,7 +28,7 @@ function SkillItem({ id, name, onClick, isSelected }) {
styleName="image"
/>
) : (
-
+
)}
{name}
diff --git a/src/routes/CreateNewTeam/pages/SelectRole/index.jsx b/src/routes/CreateNewTeam/pages/SelectRole/index.jsx
index 5488b60b..88c84e07 100644
--- a/src/routes/CreateNewTeam/pages/SelectRole/index.jsx
+++ b/src/routes/CreateNewTeam/pages/SelectRole/index.jsx
@@ -11,6 +11,11 @@ import { getRoles } from "services/roles";
import LoadingIndicator from "components/LoadingIndicator";
import RoleDetailsModal from "../../components/RoleDetailsModal";
import SearchAndSubmit from "../../components/SearchAndSubmit";
+import { isCustomRole } from "utils/helpers";
+
+// Remove custom roles from role list
+const removeCustomRoles = (roles) =>
+ roles.filter((role) => !isCustomRole(role));
function SelectRole() {
const [stages, setStages] = useState([
@@ -33,12 +38,6 @@ function SelectRole() {
setRoleDetailsModalOpen(true);
}, []);
- const resetState = () => {
- setSelectedRoleId(null);
- setRoleDetailsModalOpen(false);
- setRoleDetailsModalId(null);
- };
-
if (!roles) {
return ;
}
@@ -50,11 +49,10 @@ function SelectRole() {
isCompletenessDisabled={!selectedRoleId}
searchObject={{ roleId: selectedRoleId }}
completenessStyle="role-selection"
- reloadRolesPage={resetState}
toRender={
<>
{
+ const defaultState = {
+ previousSearchId: undefined,
+ addedRoles: [],
+ };
+ try {
+ const state = localStorage.getItem("rolesState");
+ if (state === null) {
+ return defaultState;
+ }
+ return JSON.parse(state);
+ } catch {
+ return defaultState;
+ }
};
+const initialState = loadState();
+
const reducer = (state = initialState, action) => {
switch (action.type) {
case ACTION_TYPE.CLEAR_SEARCHED_ROLES:
diff --git a/src/styles/main.vendor.scss b/src/styles/main.vendor.scss
index a2a998d4..e0608194 100644
--- a/src/styles/main.vendor.scss
+++ b/src/styles/main.vendor.scss
@@ -4,6 +4,7 @@
@import "~react-redux-toastr/src/styles/index";
@import "~react-responsive-modal/styles";
@import "~react-loader-spinner/dist/loader/css/react-spinner-loader.css";
+@import "~react-datepicker/dist/react-datepicker.css";
// toast-ui.editor styles
@import "~codemirror/lib/codemirror.css";
diff --git a/src/utils/helpers.js b/src/utils/helpers.js
index 5de1d653..4a92c84e 100644
--- a/src/utils/helpers.js
+++ b/src/utils/helpers.js
@@ -5,6 +5,7 @@
* If there are multiple methods which could be grouped into a separate file by their meaning they should be extracted from here to not make this file too big.
*/
import _ from "lodash";
+import { CUSTOM_ROLE_NAMES } from "constants/";
/**
* Delay code for some milliseconds using promise.
@@ -62,3 +63,11 @@ export const setCurrentStage = (currentStepIdx, stages, setStagesCallback) => {
.map((s) => ({ ...s, completed: false, isCurrent: false })),
]);
};
+
+/**
+ * Checks if role is custom/niche
+ * @param {Object} role role to check
+ * @returns {boolean} whether the role is custom/niche
+ */
+export const isCustomRole = (role) =>
+ !role.name || CUSTOM_ROLE_NAMES.includes(role.name.toLowerCase());