From 3a534742f2f17916e14f548a0a476a42b96ad7d8 Mon Sep 17 00:00:00 2001
From: Michael Baghel
Date: Wed, 14 Apr 2021 19:45:26 +0400
Subject: [PATCH 01/20] changed buttons and headings in candidates page
---
src/constants/index.js | 8 +--
.../InterviewDetailsPopup/index.jsx | 8 +++
.../components/PositionCandidates/index.jsx | 63 +++++++++++--------
.../PositionCandidates/styles.module.scss | 9 +--
4 files changed, 55 insertions(+), 33 deletions(-)
create mode 100644 src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
diff --git a/src/constants/index.js b/src/constants/index.js
index 6afdbdf5..70614c8e 100644
--- a/src/constants/index.js
+++ b/src/constants/index.js
@@ -124,14 +124,14 @@ export const CANDIDATE_STATUS_FILTERS = [
},
{
key: CANDIDATE_STATUS_FILTER_KEY.INTERESTED,
- buttonText: "Interested",
- title: "Interested Candidates",
+ buttonText: "Interviews",
+ title: "Interviews",
statuses: [CANDIDATE_STATUS.SHORTLIST, CANDIDATE_STATUS.INTERVIEW],
},
{
key: CANDIDATE_STATUS_FILTER_KEY.NOT_INTERESTED,
- buttonText: "Not Interested",
- title: "Not Interested Candidates",
+ buttonText: "Declined",
+ title: "Declined",
statuses: [CANDIDATE_STATUS.REJECTED, CANDIDATE_STATUS.TOPCODER_REJECTED],
},
];
diff --git a/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx b/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
new file mode 100644
index 00000000..f8e948ce
--- /dev/null
+++ b/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
@@ -0,0 +1,8 @@
+import React from "react";
+import BaseModal from "components/BaseModal";
+
+function InterviewDetailsPopup() {
+ return ;
+}
+
+export default InterviewDetailsPopup;
diff --git a/src/routes/PositionDetails/components/PositionCandidates/index.jsx b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
index a6d87006..5359bd15 100644
--- a/src/routes/PositionDetails/components/PositionCandidates/index.jsx
+++ b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
@@ -26,6 +26,7 @@ import { toastr } from "react-redux-toastr";
import { getJobById } from "services/jobs";
import { PERMISSIONS } from "constants/permissions";
import { hasPermission } from "utils/permissions";
+import ActionsMenu from "components/ActionsMenu";
/**
* Generates a function to sort candidates
@@ -61,9 +62,10 @@ const populateSkillsMatched = (position, candidate) => ({
const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
const { candidates } = position;
const [sortBy, setSortBy] = useState(CANDIDATES_SORT_BY.SKILL_MATCHED);
- const statusFilter = useMemo(() =>
- _.find(CANDIDATE_STATUS_FILTERS, { key: statusFilterKey })
- , [statusFilterKey]);
+ const statusFilter = useMemo(
+ () => _.find(CANDIDATE_STATUS_FILTERS, { key: statusFilterKey }),
+ [statusFilterKey]
+ );
const filteredCandidates = useMemo(
() =>
@@ -161,9 +163,7 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
/>
{filteredCandidates.length === 0 && (
-
- No {statusFilter.title}
-
+ No {statusFilter.title}
)}
{filteredCandidates.length > 0 && (
@@ -196,27 +196,40 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
)}
- {statusFilterKey === CANDIDATE_STATUS_FILTER_KEY.TO_REVIEW && hasPermission(PERMISSIONS.UPDATE_JOB_CANDIDATE) && (
- <>
- Interested in this candidate?
+ {statusFilterKey === CANDIDATE_STATUS_FILTER_KEY.TO_REVIEW &&
+ hasPermission(PERMISSIONS.UPDATE_JOB_CANDIDATE) && (
-
markCandidateRejected(candidate.id)}
- disabled={candidate.updating}
- >
- No
-
-
markCandidateShortlisted(candidate.id)}
- disabled={candidate.updating}
- >
- Yes
-
+
{
+ alert("TODO: Interview Scheduled!!!");
+ },
+ },
+ {
+ label: "Decline Candidate",
+ action: () => {
+ markCandidateRejected(candidate.id);
+ },
+ },
+ {
+ label: "Select Candidate",
+ action: () => {
+ markCandidateShortlisted(candidate.id);
+ },
+ },
+ ]}
+ />
- >
- )}
+ )}
+ {statusFilterKey === CANDIDATE_STATUS_FILTER_KEY.INTERESTED &&
+ hasPermission(PERMISSIONS.UPDATE_JOB_CANDIDATE) && (
+
+ Schedule Another Interview
+ View Previous Interviews
+
+ )}
))}
diff --git a/src/routes/PositionDetails/components/PositionCandidates/styles.module.scss b/src/routes/PositionDetails/components/PositionCandidates/styles.module.scss
index 50fb7e2d..032c4920 100644
--- a/src/routes/PositionDetails/components/PositionCandidates/styles.module.scss
+++ b/src/routes/PositionDetails/components/PositionCandidates/styles.module.scss
@@ -55,17 +55,18 @@
.cell-action {
justify-content: center;
- align-items: flex-start;
+ align-items: flex-end;
flex-direction: column;
width: 20%;
}
.actions {
display: flex;
- margin-top: 16px;
-
+ flex-direction: column;
+ align-items: stretch;
> * + * {
- margin-left: 10px;
+ margin-top: 10px;
+ justify-content: center;
}
}
From 3011615028fffb8334a51e6928533cefd7a27e62 Mon Sep 17 00:00:00 2001
From: Michael Baghel
Date: Thu, 15 Apr 2021 13:40:38 +0400
Subject: [PATCH 02/20] Added helper to create fake interviews. Injected fake
interviews in loadPosition action
---
package-lock.json | 5 +++
package.json | 1 +
src/routes/PositionDetails/actions/index.js | 8 ++++
src/utils/helpers.js | 42 +++++++++++++++++++++
4 files changed, 56 insertions(+)
diff --git a/package-lock.json b/package-lock.json
index 84f77334..b40da023 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6988,6 +6988,11 @@
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
"dev": true
},
+ "faker": {
+ "version": "5.5.3",
+ "resolved": "https://registry.npmjs.org/faker/-/faker-5.5.3.tgz",
+ "integrity": "sha512-wLTv2a28wjUyWkbnX7u/ABZBkUkIF2fCd73V6P2oFqEGEktDfzWx4UxrSqtPRw0xPRAcjeAOIiJWqZm3pP4u3g=="
+ },
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
diff --git a/package.json b/package.json
index cf19d263..4dc1d869 100644
--- a/package.json
+++ b/package.json
@@ -60,6 +60,7 @@
"axios": "^0.21.0",
"classnames": "^2.2.6",
"express": "^4.17.1",
+ "faker": "^5.5.3",
"final-form": "^4.20.1",
"immutability-helper": "^3.1.1",
"lodash": "^4.17.20",
diff --git a/src/routes/PositionDetails/actions/index.js b/src/routes/PositionDetails/actions/index.js
index e7fdf31a..375f264e 100644
--- a/src/routes/PositionDetails/actions/index.js
+++ b/src/routes/PositionDetails/actions/index.js
@@ -1,8 +1,10 @@
/**
* Position Details page actions
*/
+import _ from "lodash";
import { getPositionDetails, patchPositionCandidate } from "services/teams";
import { ACTION_TYPE } from "constants";
+import { getFakeInterviews } from "utils/helpers";
/**
* Load Team Position details (team job)
@@ -17,6 +19,12 @@ export const loadPosition = (teamId, positionId) => ({
payload: async () => {
const response = await getPositionDetails(teamId, positionId);
+ // inject mock interview data to candidates list
+ for (const candidate of response.data.candidates) {
+ const fakeInterviews = getFakeInterviews(candidate);
+ _.set(candidate, "interviews", fakeInterviews);
+ }
+
return response.data;
},
meta: {
diff --git a/src/utils/helpers.js b/src/utils/helpers.js
index 53f24cd2..474c8597 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 faker from "faker";
/**
* Delay code for some milliseconds using promise.
@@ -38,3 +39,44 @@ export const getSelectOptionByValue = (value, selectOptions) => {
return option;
};
+
+/**
+ * Generates a pseudorandom integer between two numbers (inclusive)
+ *
+ * @param {number} lowNum an integer to use as minimun
+ * @param {number} highNum an integer to use as maximum
+ * @returns {number} a psuedorandom number between low and high
+ */
+export const rollDice = (lowNum, highNum) => {
+ const diffPlusOne = highNum - lowNum + 1;
+ return Math.floor(Math.random() * diffPlusOne);
+};
+
+export const getFakeInterviews = (candidate) => {
+ // decide how many interviews to return
+ const numInterviews = rollDice(0, 3);
+
+ const interviews = [];
+ for (let i = 0; i < numInterviews; i++) {
+ const numEmails = rollDice(1, 5);
+ const emails = _.times(numEmails, faker.internet.email);
+
+ const interview = {
+ id: faker.datatype.uuid(),
+ googleCalendarId: "",
+ attendeesList: emails,
+ startTimeStamp: faker.date.recent(),
+ custommessage: "",
+ xaiTemplate: "",
+ jobCandidates: candidate.id,
+ round: i + 1,
+ status: "Completed",
+ createdBy: faker.datatype.uuid(),
+ updatedBy: faker.datatype.uuid(),
+ createdAt: faker.date.past(),
+ updatedAt: faker.date.past(),
+ };
+ interviews.push(interview);
+ }
+ return interviews;
+};
From add1f0378aee01e8ea63c9115ccacd42cbe1c02e Mon Sep 17 00:00:00 2001
From: Michael Baghel
Date: Thu, 15 Apr 2021 16:11:32 +0400
Subject: [PATCH 03/20] Added lastest interview info to table
---
.../components/LatestInterview/index.jsx | 26 +++++++++++++++++++
.../LatestInterview/styles.module.scss | 8 ++++++
.../components/PositionCandidates/index.jsx | 12 ++++++++-
.../PositionCandidates/styles.module.scss | 11 ++++++--
4 files changed, 54 insertions(+), 3 deletions(-)
create mode 100644 src/routes/PositionDetails/components/LatestInterview/index.jsx
create mode 100644 src/routes/PositionDetails/components/LatestInterview/styles.module.scss
diff --git a/src/routes/PositionDetails/components/LatestInterview/index.jsx b/src/routes/PositionDetails/components/LatestInterview/index.jsx
new file mode 100644
index 00000000..b45d7d5d
--- /dev/null
+++ b/src/routes/PositionDetails/components/LatestInterview/index.jsx
@@ -0,0 +1,26 @@
+import React from "react";
+import PT from "prop-types";
+import "./styles.module.scss";
+import { formatDate } from "utils/format";
+
+function LatestInterview({ interviews }) {
+ if (!interviews.length) {
+ return
;
+ }
+
+ const latestInterview = interviews[interviews.length - 1];
+
+ return (
+ <>
+ Interview Round {latestInterview.round}
+ {latestInterview.status}
+ {formatDate(latestInterview.startTimeStamp)}
+ >
+ );
+}
+
+LatestInterview.propTypes = {
+ interviews: PT.array,
+};
+
+export default LatestInterview;
diff --git a/src/routes/PositionDetails/components/LatestInterview/styles.module.scss b/src/routes/PositionDetails/components/LatestInterview/styles.module.scss
new file mode 100644
index 00000000..ab6df681
--- /dev/null
+++ b/src/routes/PositionDetails/components/LatestInterview/styles.module.scss
@@ -0,0 +1,8 @@
+.small {
+ font-size: 12px;
+}
+
+.strong {
+ font-weight: bold;
+ margin-bottom: 8px;
+}
\ No newline at end of file
diff --git a/src/routes/PositionDetails/components/PositionCandidates/index.jsx b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
index 5359bd15..844e35c2 100644
--- a/src/routes/PositionDetails/components/PositionCandidates/index.jsx
+++ b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
@@ -27,6 +27,7 @@ import { getJobById } from "services/jobs";
import { PERMISSIONS } from "constants/permissions";
import { hasPermission } from "utils/permissions";
import ActionsMenu from "components/ActionsMenu";
+import LatestInterview from "../LatestInterview";
/**
* Generates a function to sort candidates
@@ -195,6 +196,11 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
)}
+ {statusFilterKey === CANDIDATE_STATUS_FILTER_KEY.INTERESTED && (
+
+
+
+ )}
{statusFilterKey === CANDIDATE_STATUS_FILTER_KEY.TO_REVIEW &&
hasPermission(PERMISSIONS.UPDATE_JOB_CANDIDATE) && (
@@ -227,7 +233,11 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
hasPermission(PERMISSIONS.UPDATE_JOB_CANDIDATE) && (
Schedule Another Interview
- View Previous Interviews
+ {candidate.interviews.length > 0 && (
+
+ View Previous Interviews
+
+ )}
)}
diff --git a/src/routes/PositionDetails/components/PositionCandidates/styles.module.scss b/src/routes/PositionDetails/components/PositionCandidates/styles.module.scss
index 032c4920..11babadb 100644
--- a/src/routes/PositionDetails/components/PositionCandidates/styles.module.scss
+++ b/src/routes/PositionDetails/components/PositionCandidates/styles.module.scss
@@ -50,14 +50,21 @@
justify-content: center;
align-items: flex-start;
flex-direction: column;
- width: 50%;
+ width: 30%;
+}
+
+.cell-prev-interviews {
+ justify-content: center;
+ align-items: flex-start;
+ flex-direction: column;
+ width: 15%;
}
.cell-action {
justify-content: center;
align-items: flex-end;
flex-direction: column;
- width: 20%;
+ width: 25%;
}
.actions {
From 59d3c31504609fd7076a9b84c85446cdce6865d9 Mon Sep 17 00:00:00 2001
From: Michael Baghel
Date: Fri, 16 Apr 2021 17:38:42 +0400
Subject: [PATCH 04/20] working previous interviews popup w/ basic accordion
---
src/components/Accordion/index.jsx | 32 +++
src/components/Accordion/styles.module.scss | 53 ++++
src/components/SimpleModal/index.jsx | 73 ++++++
src/components/SimpleModal/styles.module.scss | 28 +++
.../components/LatestInterview/index.jsx | 2 +-
.../components/PositionCandidates/index.jsx | 237 ++++++++++--------
.../components/PrevInterviewItem/index.jsx | 30 +++
.../PreviousInterviewsPopup/index.jsx | 54 ++++
.../styles.module.scss | 6 +
src/utils/helpers.js | 8 +-
10 files changed, 409 insertions(+), 114 deletions(-)
create mode 100644 src/components/Accordion/index.jsx
create mode 100644 src/components/Accordion/styles.module.scss
create mode 100644 src/components/SimpleModal/index.jsx
create mode 100644 src/components/SimpleModal/styles.module.scss
create mode 100644 src/routes/PositionDetails/components/PrevInterviewItem/index.jsx
create mode 100644 src/routes/PositionDetails/components/PreviousInterviewsPopup/index.jsx
create mode 100644 src/routes/PositionDetails/components/PreviousInterviewsPopup/styles.module.scss
diff --git a/src/components/Accordion/index.jsx b/src/components/Accordion/index.jsx
new file mode 100644
index 00000000..6a9e00c1
--- /dev/null
+++ b/src/components/Accordion/index.jsx
@@ -0,0 +1,32 @@
+import React, { useState } from "react";
+import PT from "prop-types";
+import "./styles.module.scss";
+
+function Accordion(props) {
+ const { title, sidebar, subhead, children } = props;
+ const [isOpen, setIsOpen] = useState(false);
+
+ return (
+
+
setIsOpen(!isOpen)} styleName="accordion">
+
+
+ {isOpen &&
{children}
}
+
+ );
+}
+
+Accordion.propTypes = {
+ title: PT.string,
+ sidebar: PT.string,
+ subhead: PT.string,
+ children: PT.node,
+};
+
+export default Accordion;
diff --git a/src/components/Accordion/styles.module.scss b/src/components/Accordion/styles.module.scss
new file mode 100644
index 00000000..c658021d
--- /dev/null
+++ b/src/components/Accordion/styles.module.scss
@@ -0,0 +1,53 @@
+@import "styles/include";
+
+.accordion {
+ cursor: pointer;
+ width: 100%;
+ border: none;
+ outline: none;
+ background-color: #fff;
+ color: #2a2a2a;
+ display: flex;
+ text-align: left;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+ padding: 15px 0 10px 0;
+
+ p {
+ @include font-roboto;
+ font-size: 14px;
+ }
+}
+
+.accordion:before {
+ display: inline-block;
+ content: '';
+ height: 10px;
+ width: 10px;
+ margin-right: 12px;
+ border-bottom: 2px solid #137D60;
+ border-right: 2px solid #137D60;
+ transform: rotate(-45deg);
+}
+
+.heading {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+}
+
+.title {
+ @include font-barlow;
+ font-weight: 600;
+ font-size: 20px;
+ margin: 0;
+ padding: 0;
+ text-transform: uppercase;
+}
+
+.panel {
+ display: block;
+}
\ No newline at end of file
diff --git a/src/components/SimpleModal/index.jsx b/src/components/SimpleModal/index.jsx
new file mode 100644
index 00000000..1180d415
--- /dev/null
+++ b/src/components/SimpleModal/index.jsx
@@ -0,0 +1,73 @@
+/**
+ * SimpleModal
+ *
+ * Wraps the react-responsive-modal
+ * and adds the app's style
+ *
+ * The same as the BaseModal, but with only a single close button
+ * Supports title
+ * children are displayed as modal content
+ */
+
+import React from "react";
+import PT from "prop-types";
+import { Modal } from "react-responsive-modal";
+import Button from "../Button";
+import IconCross from "../../assets/images/icon-cross-light.svg";
+import "./styles.module.scss";
+
+const modalStyle = {
+ borderRadius: "8px",
+ padding: "32px 32px 22px 32px",
+ maxWidth: "640px",
+ width: "100%",
+ margin: 0,
+};
+
+const containerStyle = {
+ padding: "10px",
+};
+
+export const SimpleModal = ({
+ open,
+ onClose,
+ extraModalStyle,
+ title,
+ children,
+ disabled,
+}) => (
+ }
+ styles={{
+ modal: { ...modalStyle, ...extraModalStyle },
+ modalContainer: containerStyle,
+ }}
+ center={true}
+ >
+ {title && {title} }
+ {children}
+
+
+ Close
+
+
+
+);
+
+SimpleModal.propTypes = {
+ open: PT.bool.isRequired,
+ onClose: PT.func.isRequired,
+ children: PT.node,
+ title: PT.string,
+ disabled: PT.bool,
+ extraModalStyle: PT.object,
+};
+
+export default SimpleModal;
diff --git a/src/components/SimpleModal/styles.module.scss b/src/components/SimpleModal/styles.module.scss
new file mode 100644
index 00000000..e1c1d36c
--- /dev/null
+++ b/src/components/SimpleModal/styles.module.scss
@@ -0,0 +1,28 @@
+@import "styles/include";
+
+.title {
+ @include font-barlow-condensed;
+ font-weight: normal;
+ font-size: 34px;
+ line-height: 40px;
+ margin: 0 0 24px 0;
+ overflow-wrap: anywhere;
+ padding: 0;
+ text-transform: uppercase;
+}
+
+.content {
+ @include font-roboto;
+ line-height: 140%;
+}
+
+.button-group {
+ margin: 24px 0 0 0;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ button {
+ margin-right: 10px;
+ margin-bottom: 10px;
+ }
+}
diff --git a/src/routes/PositionDetails/components/LatestInterview/index.jsx b/src/routes/PositionDetails/components/LatestInterview/index.jsx
index b45d7d5d..81f1d49d 100644
--- a/src/routes/PositionDetails/components/LatestInterview/index.jsx
+++ b/src/routes/PositionDetails/components/LatestInterview/index.jsx
@@ -14,7 +14,7 @@ function LatestInterview({ interviews }) {
<>
Interview Round {latestInterview.round}
{latestInterview.status}
- {formatDate(latestInterview.startTimeStamp)}
+ {formatDate(latestInterview.startTimestamp)}
>
);
}
diff --git a/src/routes/PositionDetails/components/PositionCandidates/index.jsx b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
index 844e35c2..9fe53539 100644
--- a/src/routes/PositionDetails/components/PositionCandidates/index.jsx
+++ b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
@@ -28,6 +28,7 @@ import { PERMISSIONS } from "constants/permissions";
import { hasPermission } from "utils/permissions";
import ActionsMenu from "components/ActionsMenu";
import LatestInterview from "../LatestInterview";
+import PreviousInterviewsPopup from "../PreviousInterviewsPopup";
/**
* Generates a function to sort candidates
@@ -61,6 +62,14 @@ const populateSkillsMatched = (position, candidate) => ({
});
const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
+ const [prevInterviewsOpen, setPrevInterviewsOpen] = useState(false);
+ const [selectedCandidate, setSelectedCandidate] = useState(null);
+
+ const openPrevInterviewsPopup = (candidate) => {
+ setSelectedCandidate(candidate);
+ setPrevInterviewsOpen(true);
+ };
+
const { candidates } = position;
const [sortBy, setSortBy] = useState(CANDIDATES_SORT_BY.SKILL_MATCHED);
const statusFilter = useMemo(
@@ -150,123 +159,133 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
);
return (
-
-
- }
- />
+ <>
+
+
+ }
+ />
- {filteredCandidates.length === 0 && (
-
No {statusFilter.title}
- )}
- {filteredCandidates.length > 0 && (
-
- {pageCandidates.map((candidate) => (
-
-
-
-
-
- {statusFilterKey === CANDIDATE_STATUS_FILTER_KEY.INTERESTED && (
-
-
+ {filteredCandidates.length === 0 && (
+
No {statusFilter.title}
+ )}
+ {filteredCandidates.length > 0 && (
+
+ {pageCandidates.map((candidate) => (
+
+
+
+
+
- )}
-
- {statusFilterKey === CANDIDATE_STATUS_FILTER_KEY.TO_REVIEW &&
- hasPermission(PERMISSIONS.UPDATE_JOB_CANDIDATE) && (
-
-
{
- alert("TODO: Interview Scheduled!!!");
+ {statusFilterKey === CANDIDATE_STATUS_FILTER_KEY.INTERESTED && (
+
+
+
+ )}
+
+ {statusFilterKey === CANDIDATE_STATUS_FILTER_KEY.TO_REVIEW &&
+ hasPermission(PERMISSIONS.UPDATE_JOB_CANDIDATE) && (
+
+
{
+ alert("TODO: Interview Scheduled!!!");
+ },
},
- },
- {
- label: "Decline Candidate",
- action: () => {
- markCandidateRejected(candidate.id);
+ {
+ label: "Decline Candidate",
+ action: () => {
+ markCandidateRejected(candidate.id);
+ },
},
- },
- {
- label: "Select Candidate",
- action: () => {
- markCandidateShortlisted(candidate.id);
+ {
+ label: "Select Candidate",
+ action: () => {
+ markCandidateShortlisted(candidate.id);
+ },
},
- },
- ]}
- />
-
- )}
- {statusFilterKey === CANDIDATE_STATUS_FILTER_KEY.INTERESTED &&
- hasPermission(PERMISSIONS.UPDATE_JOB_CANDIDATE) && (
-
- Schedule Another Interview
- {candidate.interviews.length > 0 && (
-
- View Previous Interviews
-
- )}
-
- )}
+ ]}
+ />
+
+ )}
+ {statusFilterKey === CANDIDATE_STATUS_FILTER_KEY.INTERESTED &&
+ hasPermission(PERMISSIONS.UPDATE_JOB_CANDIDATE) && (
+
+ Schedule Another Interview
+ {candidate.interviews.length > 0 && (
+ openPrevInterviewsPopup(candidate)}
+ >
+ View Previous Interviews
+
+ )}
+
+ )}
+
-
- ))}
-
- )}
-
-
-
- Show More
-
- {filteredCandidates.length > 0 && (
-
+ ))}
+
)}
+
+
+
+ Show More
+
+ {filteredCandidates.length > 0 && (
+
+ )}
+
-
+
setPrevInterviewsOpen(false)}
+ candidate={selectedCandidate}
+ />
+ >
);
};
diff --git a/src/routes/PositionDetails/components/PrevInterviewItem/index.jsx b/src/routes/PositionDetails/components/PrevInterviewItem/index.jsx
new file mode 100644
index 00000000..dd42fa0a
--- /dev/null
+++ b/src/routes/PositionDetails/components/PrevInterviewItem/index.jsx
@@ -0,0 +1,30 @@
+import React from "react";
+import PT from "prop-types";
+import { formatDate } from "utils/format";
+import Accordion from "components/Accordion";
+
+function PrevInterviewItem(props) {
+ const { date, round, emails } = props;
+
+ return (
+
+
+ {emails.map((email) => (
+ {email}
+ ))}
+
+
+ );
+}
+
+PrevInterviewItem.propTypes = {
+ date: PT.string.isRequired,
+ round: PT.number.isRequired,
+ emails: PT.arrayOf(PT.string).isRequired,
+};
+
+export default PrevInterviewItem;
diff --git a/src/routes/PositionDetails/components/PreviousInterviewsPopup/index.jsx b/src/routes/PositionDetails/components/PreviousInterviewsPopup/index.jsx
new file mode 100644
index 00000000..5f00b0b1
--- /dev/null
+++ b/src/routes/PositionDetails/components/PreviousInterviewsPopup/index.jsx
@@ -0,0 +1,54 @@
+import React from "react";
+import PT from "prop-types";
+import SimpleModal from "components/SimpleModal";
+import User from "components/User";
+import "./styles.module.scss";
+import PrevInterviewItem from "../PrevInterviewItem";
+
+function PreviousInterviewsPopup(props) {
+ const { candidate, open, onClose } = props;
+
+ const showPrevInterviews = (interviews) => {
+ return interviews.map((interview) => (
+
+ ));
+ };
+
+ return (
+
+ {candidate === null ? (
+ ""
+ ) : (
+ <>
+
+ {candidate === null ? (
+ ""
+ ) : (
+
+ )}
+
+ {showPrevInterviews(candidate.interviews)}
+ >
+ )}
+
+ );
+}
+
+PreviousInterviewsPopup.propTypes = {
+ candidate: PT.object,
+ open: PT.bool,
+ onClose: PT.func,
+};
+
+export default PreviousInterviewsPopup;
diff --git a/src/routes/PositionDetails/components/PreviousInterviewsPopup/styles.module.scss b/src/routes/PositionDetails/components/PreviousInterviewsPopup/styles.module.scss
new file mode 100644
index 00000000..915099c3
--- /dev/null
+++ b/src/routes/PositionDetails/components/PreviousInterviewsPopup/styles.module.scss
@@ -0,0 +1,6 @@
+.user {
+ font-size: 14px;
+ color: #0D61BF;
+ padding-bottom: 25px;
+ border-bottom: 1px solid #E9E9E9;
+}
\ No newline at end of file
diff --git a/src/utils/helpers.js b/src/utils/helpers.js
index 474c8597..cf321f42 100644
--- a/src/utils/helpers.js
+++ b/src/utils/helpers.js
@@ -49,23 +49,23 @@ export const getSelectOptionByValue = (value, selectOptions) => {
*/
export const rollDice = (lowNum, highNum) => {
const diffPlusOne = highNum - lowNum + 1;
- return Math.floor(Math.random() * diffPlusOne);
+ return Math.floor(Math.random() * diffPlusOne + lowNum);
};
export const getFakeInterviews = (candidate) => {
// decide how many interviews to return
- const numInterviews = rollDice(0, 3);
+ const numInterviews = rollDice(1, 3);
const interviews = [];
for (let i = 0; i < numInterviews; i++) {
const numEmails = rollDice(1, 5);
- const emails = _.times(numEmails, faker.internet.email);
+ const emails = _.times(numEmails, faker.internet.exampleEmail);
const interview = {
id: faker.datatype.uuid(),
googleCalendarId: "",
attendeesList: emails,
- startTimeStamp: faker.date.recent(),
+ startTimestamp: faker.date.recent(),
custommessage: "",
xaiTemplate: "",
jobCandidates: candidate.id,
From 48573a95218fa6581caa4a894f7576a2f55fb112 Mon Sep 17 00:00:00 2001
From: Michael Baghel
Date: Sat, 17 Apr 2021 12:10:33 +0400
Subject: [PATCH 05/20] finished previous interviews popup
---
src/components/Accordion/index.jsx | 14 +++++++--
src/components/Accordion/styles.module.scss | 31 ++++++++++++++-----
.../components/PrevInterviewItem/index.jsx | 3 +-
.../PrevInterviewItem/styles.module.scss | 3 ++
4 files changed, 41 insertions(+), 10 deletions(-)
create mode 100644 src/routes/PositionDetails/components/PrevInterviewItem/styles.module.scss
diff --git a/src/components/Accordion/index.jsx b/src/components/Accordion/index.jsx
index 6a9e00c1..4e0d29e6 100644
--- a/src/components/Accordion/index.jsx
+++ b/src/components/Accordion/index.jsx
@@ -1,3 +1,8 @@
+/**
+ * An expandable item which can be used
+ * repeatadly to form an accordion style display
+ */
+
import React, { useState } from "react";
import PT from "prop-types";
import "./styles.module.scss";
@@ -7,8 +12,13 @@ function Accordion(props) {
const [isOpen, setIsOpen] = useState(false);
return (
-
-
setIsOpen(!isOpen)} styleName="accordion">
+
+
setIsOpen(!isOpen)} styleName="button">
+ {isOpen ? (
+
+ ) : (
+
+ )}
{title}
diff --git a/src/components/Accordion/styles.module.scss b/src/components/Accordion/styles.module.scss
index c658021d..ffa9a997 100644
--- a/src/components/Accordion/styles.module.scss
+++ b/src/components/Accordion/styles.module.scss
@@ -1,6 +1,11 @@
@import "styles/include";
.accordion {
+ padding-bottom: 10px;
+ border-bottom: 1px solid #e9e9e9;
+}
+
+.button {
cursor: pointer;
width: 100%;
border: none;
@@ -20,14 +25,25 @@
}
}
-.accordion:before {
+.down-arrow {
+ display: inline-block;
+ content: '';
+ height: 13px;
+ width: 13px;
+ margin-right: 16px;
+ border-bottom: 3px solid #137D60;
+ border-right: 3px solid #137D60;
+ transform: rotate(45deg);
+}
+
+.right-arrow {
display: inline-block;
content: '';
- height: 10px;
- width: 10px;
- margin-right: 12px;
- border-bottom: 2px solid #137D60;
- border-right: 2px solid #137D60;
+ height: 13px;
+ width: 13px;
+ margin-right: 16px;
+ border-bottom: 3px solid #137D60;
+ border-right: 3px solid #137D60;
transform: rotate(-45deg);
}
@@ -49,5 +65,6 @@
}
.panel {
- display: block;
+ padding-left: 28px;
+ font-size: 14px;
}
\ No newline at end of file
diff --git a/src/routes/PositionDetails/components/PrevInterviewItem/index.jsx b/src/routes/PositionDetails/components/PrevInterviewItem/index.jsx
index dd42fa0a..3dae1069 100644
--- a/src/routes/PositionDetails/components/PrevInterviewItem/index.jsx
+++ b/src/routes/PositionDetails/components/PrevInterviewItem/index.jsx
@@ -2,6 +2,7 @@ import React from "react";
import PT from "prop-types";
import { formatDate } from "utils/format";
import Accordion from "components/Accordion";
+import "./styles.module.scss";
function PrevInterviewItem(props) {
const { date, round, emails } = props;
@@ -14,7 +15,7 @@ function PrevInterviewItem(props) {
>
{emails.map((email) => (
- {email}
+ {email}
))}
diff --git a/src/routes/PositionDetails/components/PrevInterviewItem/styles.module.scss b/src/routes/PositionDetails/components/PrevInterviewItem/styles.module.scss
new file mode 100644
index 00000000..fc9df5f8
--- /dev/null
+++ b/src/routes/PositionDetails/components/PrevInterviewItem/styles.module.scss
@@ -0,0 +1,3 @@
+.email {
+ margin-bottom: 8px;
+}
\ No newline at end of file
From 4e7536aa1642e90d3abf2e7182b177ed01e331c1 Mon Sep 17 00:00:00 2001
From: Michael Baghel
Date: Sun, 18 Apr 2021 15:42:21 +0400
Subject: [PATCH 06/20] Finished appearance of interview details popup
---
src/components/Radio/index.jsx | 38 ++++++
src/components/Radio/styles.module.scss | 59 +++++++++
.../InterviewDetailsPopup/index.jsx | 116 +++++++++++++++++-
.../InterviewDetailsPopup/styles.module.scss | 39 ++++++
.../components/PositionCandidates/index.jsx | 25 +++-
.../PreviousInterviewsPopup/index.jsx | 6 +-
6 files changed, 274 insertions(+), 9 deletions(-)
create mode 100644 src/components/Radio/index.jsx
create mode 100644 src/components/Radio/styles.module.scss
create mode 100644 src/routes/PositionDetails/components/InterviewDetailsPopup/styles.module.scss
diff --git a/src/components/Radio/index.jsx b/src/components/Radio/index.jsx
new file mode 100644
index 00000000..f3d38639
--- /dev/null
+++ b/src/components/Radio/index.jsx
@@ -0,0 +1,38 @@
+import React from "react";
+import PT from "prop-types";
+import cn from "classnames";
+import "./styles.module.scss";
+
+function Radio(props) {
+ return (
+
+
+
+ {props.label}
+
+ );
+}
+
+Radio.propTypes = {
+ onChange: PT.func,
+ onBlur: PT.func,
+ onFocus: PT.func,
+ value: PT.string.isRequired,
+ disabled: PT.bool,
+ type: PT.string.isRequired,
+ label: PT.string,
+ checked: PT.bool,
+ horizontal: PT.bool,
+};
+
+export default Radio;
diff --git a/src/components/Radio/styles.module.scss b/src/components/Radio/styles.module.scss
new file mode 100644
index 00000000..4e4001e0
--- /dev/null
+++ b/src/components/Radio/styles.module.scss
@@ -0,0 +1,59 @@
+.radio-input {
+ width: auto;
+ height: auto;
+ position: absolute;
+ opacity: 0;
+ cursor: pointer;
+}
+
+.container {
+ display: block;
+ position: relative;
+ padding-left: 35px;
+ margin-bottom: 24px;
+ cursor: pointer;
+ font-size: 14px;
+ user-select: none;
+
+ &.horizontal {
+ margin-bottom: 0;
+ display: inline-block;
+ margin-left: 24px;
+ }
+}
+
+.custom {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 24px;
+ width: 24px;
+ background-color: #fff;
+ border-radius: 50%;
+ border: 1px solid #AAA;
+}
+
+.radio-input:checked ~ .custom {
+ background-color: #0AB88A;
+ box-shadow: inset 0 1px 2px 0 rgba(0, 0, 0, 0.29);
+}
+
+.custom:after {
+ content: "";
+ position: absolute;
+ display: none;
+}
+
+.radio-input:checked ~ .custom:after {
+ display: block;
+}
+
+.container .custom:after {
+ top: 5px;
+ left: 5px;
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.35);
+ background: white;
+}
\ No newline at end of file
diff --git a/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx b/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
index f8e948ce..ecdc0c48 100644
--- a/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
+++ b/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
@@ -1,8 +1,118 @@
-import React from "react";
+import React, { useEffect, useState } from "react";
+import { getAuthUserProfile } from "@topcoder/micro-frontends-navbar-app";
+import { Form } from "react-final-form";
+import Radio from "components/Radio";
+import User from "components/User";
import BaseModal from "components/BaseModal";
+import FormField from "components/FormField";
+import Button from "components/Button";
+import { FORM_FIELD_TYPE } from "constants";
+import "./styles.module.scss";
-function InterviewDetailsPopup() {
- return ;
+function InterviewDetailsPopup({ open, onClose, candidate }) {
+ const [isLoading, setIsLoading] = useState(true);
+ const [myEmail, setMyEmail] = useState("");
+
+ useEffect(() => {
+ getAuthUserProfile().then((res) => {
+ setMyEmail(res.email || "");
+ setIsLoading(false);
+ });
+ }, []);
+
+ return isLoading ? null : (
+
+ );
}
export default InterviewDetailsPopup;
diff --git a/src/routes/PositionDetails/components/InterviewDetailsPopup/styles.module.scss b/src/routes/PositionDetails/components/InterviewDetailsPopup/styles.module.scss
new file mode 100644
index 00000000..ce23d6c9
--- /dev/null
+++ b/src/routes/PositionDetails/components/InterviewDetailsPopup/styles.module.scss
@@ -0,0 +1,39 @@
+@import "styles/include";
+
+.user {
+ font-size: 14px;
+ color: #0D61BF;
+}
+
+.top {
+ padding-bottom: 25px;
+ border-bottom: 1px solid #E9E9E9;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.center {
+ padding: 25px 0;
+ border-bottom: 1px solid #E9E9E9;
+}
+
+.center-header {
+ @include font-barlow;
+ font-weight: 600;
+ font-size: 20px;
+ margin: 0 0 10px 0;
+ padding: 0;
+ text-transform: uppercase;
+}
+
+.modal-text {
+ @include font-roboto;
+ font-size: 14px;
+}
+
+.bottom {
+ padding-top: 25px;
+ padding-bottom: 8px;
+}
\ No newline at end of file
diff --git a/src/routes/PositionDetails/components/PositionCandidates/index.jsx b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
index 9fe53539..4d5f8f27 100644
--- a/src/routes/PositionDetails/components/PositionCandidates/index.jsx
+++ b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
@@ -28,6 +28,7 @@ import { PERMISSIONS } from "constants/permissions";
import { hasPermission } from "utils/permissions";
import ActionsMenu from "components/ActionsMenu";
import LatestInterview from "../LatestInterview";
+import InterviewDetailsPopup from "../InterviewDetailsPopup";
import PreviousInterviewsPopup from "../PreviousInterviewsPopup";
/**
@@ -62,9 +63,15 @@ const populateSkillsMatched = (position, candidate) => ({
});
const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
+ const [interviewDetailsOpen, setInterviewDetailsOpen] = useState(false);
const [prevInterviewsOpen, setPrevInterviewsOpen] = useState(false);
const [selectedCandidate, setSelectedCandidate] = useState(null);
+ const openInterviewDetailsPopup = (candidate) => {
+ setSelectedCandidate(candidate);
+ setInterviewDetailsOpen(true);
+ };
+
const openPrevInterviewsPopup = (candidate) => {
setSelectedCandidate(candidate);
setPrevInterviewsOpen(true);
@@ -220,14 +227,11 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
{
label: "Schedule Interview",
action: () => {
- alert("TODO: Interview Scheduled!!!");
+ openInterviewDetailsPopup(candidate);
},
},
{
- label: "Decline Candidate",
- action: () => {
- markCandidateRejected(candidate.id);
- },
+ separator: true,
},
{
label: "Select Candidate",
@@ -235,6 +239,12 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
markCandidateShortlisted(candidate.id);
},
},
+ {
+ label: "Decline Candidate",
+ action: () => {
+ markCandidateRejected(candidate.id);
+ },
+ },
]}
/>
@@ -285,6 +295,11 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
onClose={() => setPrevInterviewsOpen(false)}
candidate={selectedCandidate}
/>
+
setInterviewDetailsOpen(false)}
+ candidate={selectedCandidate}
+ />
>
);
};
diff --git a/src/routes/PositionDetails/components/PreviousInterviewsPopup/index.jsx b/src/routes/PositionDetails/components/PreviousInterviewsPopup/index.jsx
index 5f00b0b1..b3da61df 100644
--- a/src/routes/PositionDetails/components/PreviousInterviewsPopup/index.jsx
+++ b/src/routes/PositionDetails/components/PreviousInterviewsPopup/index.jsx
@@ -9,7 +9,11 @@ function PreviousInterviewsPopup(props) {
const { candidate, open, onClose } = props;
const showPrevInterviews = (interviews) => {
- return interviews.map((interview) => (
+ const sortedInterviews = interviews
+ .slice()
+ .sort((a, b) => a.round - b.round);
+
+ return sortedInterviews.map((interview) => (
Date: Mon, 19 Apr 2021 16:21:32 +0400
Subject: [PATCH 07/20] Working Interview Details Popup, but without validation
or closing+resetting on submit
---
package-lock.json | 23 +++
package.json | 2 +
src/components/RadioFieldGroup/index.jsx | 46 +++++
src/constants/index.js | 4 +
src/routes/PositionDetails/actions/index.js | 18 +-
.../InterviewDetailsPopup/index.jsx | 166 +++++++++++++++++-
.../InterviewDetailsPopup/styles.module.scss | 35 ++++
.../components/PositionCandidates/index.jsx | 6 +-
src/routes/PositionDetails/reducers/index.js | 43 +++++
src/services/teams.js | 39 ++++
src/utils/helpers.js | 30 ++--
11 files changed, 395 insertions(+), 17 deletions(-)
create mode 100644 src/components/RadioFieldGroup/index.jsx
diff --git a/package-lock.json b/package-lock.json
index b40da023..a32ecd62 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7118,6 +7118,11 @@
"@babel/runtime": "^7.10.0"
}
},
+ "final-form-arrays": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/final-form-arrays/-/final-form-arrays-3.0.2.tgz",
+ "integrity": "sha512-TfO8aZNz3RrsZCDx8GHMQcyztDNpGxSSi9w4wpSNKlmv2PfFWVVM8P7Yj5tj4n0OWax+x5YwTLhT5BnqSlCi+w=="
+ },
"finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
@@ -14477,6 +14482,24 @@
}
}
},
+ "react-final-form-arrays": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/react-final-form-arrays/-/react-final-form-arrays-3.1.3.tgz",
+ "integrity": "sha512-dzBiLfbr9l1YRExARBpJ8uA/djBenCvFrbrsXjd362joDl3vT+WhmMKKr6HDQMJffjA8T4gZ3n5+G9M59yZfuQ==",
+ "requires": {
+ "@babel/runtime": "^7.12.1"
+ },
+ "dependencies": {
+ "@babel/runtime": {
+ "version": "7.13.10",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz",
+ "integrity": "sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==",
+ "requires": {
+ "regenerator-runtime": "^0.13.4"
+ }
+ }
+ }
+ },
"react-input-autosize": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-3.0.0.tgz",
diff --git a/package.json b/package.json
index 4dc1d869..31bd4caf 100644
--- a/package.json
+++ b/package.json
@@ -62,6 +62,7 @@
"express": "^4.17.1",
"faker": "^5.5.3",
"final-form": "^4.20.1",
+ "final-form-arrays": "^3.0.2",
"immutability-helper": "^3.1.1",
"lodash": "^4.17.20",
"moment": "^2.29.1",
@@ -72,6 +73,7 @@
"react-datepicker": "^3.4.1",
"react-dom": "^16.12.0",
"react-final-form": "^6.5.2",
+ "react-final-form-arrays": "^3.1.3",
"react-loader-spinner": "^4.0.0",
"react-outside-click-handler": "^1.3.0",
"react-popper": "^2.2.3",
diff --git a/src/components/RadioFieldGroup/index.jsx b/src/components/RadioFieldGroup/index.jsx
new file mode 100644
index 00000000..3dde6f14
--- /dev/null
+++ b/src/components/RadioFieldGroup/index.jsx
@@ -0,0 +1,46 @@
+/**
+ * Component that takes a configuration object
+ * and returns a group of react-final-form radio fields
+ */
+
+import React from "react";
+import PT from "prop-types";
+import Radio from "components/Radio";
+import { Field } from "react-final-form";
+
+function RadioFieldGroup({ name, isHorizontal, radios }) {
+ return (
+
+ {radios.map((radio) => (
+
+ {({ input }) => (
+
+ )}
+
+ ))}
+
+ );
+}
+
+RadioFieldGroup.propTypes = {
+ name: PT.string.isRequired,
+ isHorizontal: PT.bool,
+ radios: PT.arrayOf(
+ PT.shape({
+ label: PT.string.isRequired,
+ value: PT.any.isRequired,
+ })
+ ),
+};
+
+export default RadioFieldGroup;
diff --git a/src/constants/index.js b/src/constants/index.js
index 70614c8e..a46a7e77 100644
--- a/src/constants/index.js
+++ b/src/constants/index.js
@@ -171,6 +171,10 @@ export const ACTION_TYPE = {
UPDATE_CANDIDATE_SUCCESS: "UPDATE_CANDIDATE_SUCCESS",
UPDATE_CANDIDATE_ERROR: "UPDATE_CANDIDATE_ERROR",
+ ADD_INTERVIEW: "ADD_INTERVIEW",
+ ADD_INTERVIEW_PENDING: "ADD_INTERVIEW_PENDING",
+ ADD_INTERVIEW_SUCCESS: "ADD_INTERVIEW_SUCCESS",
+ ADD_INTERVIEW_ERROR: "ADD_INTERVIEW_ERROR",
/*
withAuthentication
*/
diff --git a/src/routes/PositionDetails/actions/index.js b/src/routes/PositionDetails/actions/index.js
index 375f264e..b0566318 100644
--- a/src/routes/PositionDetails/actions/index.js
+++ b/src/routes/PositionDetails/actions/index.js
@@ -2,7 +2,11 @@
* Position Details page actions
*/
import _ from "lodash";
-import { getPositionDetails, patchPositionCandidate } from "services/teams";
+import {
+ getPositionDetails,
+ patchPositionCandidate,
+ patchCandidateInterview,
+} from "services/teams";
import { ACTION_TYPE } from "constants";
import { getFakeInterviews } from "utils/helpers";
@@ -56,6 +60,18 @@ export const updateCandidate = (candidateId, partialCandidateData) => ({
},
});
+export const addInterview = (candidateId, formData) => ({
+ type: ACTION_TYPE.ADD_INTERVIEW,
+ payload: async () => {
+ const response = await patchCandidateInterview(candidateId, formData);
+
+ return response.data;
+ },
+ meta: {
+ candidateId,
+ },
+});
+
/**
* Reset position state
*/
diff --git a/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx b/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
index ecdc0c48..ace8778e 100644
--- a/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
+++ b/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
@@ -1,27 +1,187 @@
-import React, { useEffect, useState } from "react";
+import React, { useEffect, useState, useCallback } from "react";
import { getAuthUserProfile } from "@topcoder/micro-frontends-navbar-app";
import { Form } from "react-final-form";
-import Radio from "components/Radio";
+import arrayMutators from "final-form-arrays";
+import { FieldArray } from "react-final-form-arrays";
+import { useDispatch } from "react-redux";
+import { addInterview } from "../../actions";
import User from "components/User";
import BaseModal from "components/BaseModal";
import FormField from "components/FormField";
import Button from "components/Button";
import { FORM_FIELD_TYPE } from "constants";
import "./styles.module.scss";
+import RadioFieldGroup from "components/RadioFieldGroup";
function InterviewDetailsPopup({ open, onClose, candidate }) {
const [isLoading, setIsLoading] = useState(true);
const [myEmail, setMyEmail] = useState("");
+ const [myId, setMyId] = useState("");
+ const dispatch = useDispatch();
useEffect(() => {
getAuthUserProfile().then((res) => {
setMyEmail(res.email || "");
+ setMyId(res.userId);
setIsLoading(false);
});
}, []);
+ const onSubmitCallback = useCallback(
+ (formData) => {
+ const secondaryEmails =
+ formData.emails?.filter(
+ (email) => typeof email === "string" && email.length > 0
+ ) || [];
+ const interviewData = {
+ xaiTemplate: formData.time,
+ attendeesList: [formData.myemail, formData.email2, ...secondaryEmails],
+ round: candidate.interviews.length + 1,
+ createdBy: myId,
+ };
+
+ console.log(interviewData);
+
+ dispatch(addInterview(candidate.id, interviewData));
+ },
+ [dispatch, candidate]
+ );
+
return isLoading ? null : (
+ /*
+ */
);
}
diff --git a/src/routes/PositionDetails/components/InterviewDetailsPopup/styles.module.scss b/src/routes/PositionDetails/components/InterviewDetailsPopup/styles.module.scss
index ce23d6c9..bdd9262d 100644
--- a/src/routes/PositionDetails/components/InterviewDetailsPopup/styles.module.scss
+++ b/src/routes/PositionDetails/components/InterviewDetailsPopup/styles.module.scss
@@ -36,4 +36,39 @@
.bottom {
padding-top: 25px;
padding-bottom: 8px;
+}
+
+.add-more {
+ outline: none;
+ background: #fff;
+ margin: 10px 0 0 0;
+ padding: 0;
+ color: blue;
+ border: none;
+ border-radius: 0;
+
+ &:hover {
+ text-decoration: underline;
+ }
+}
+
+.array-item {
+ display: flex;
+ flex-direction: row;
+ align-items: stretch;
+ justify-content: space-between;
+}
+
+.array-input {
+ width: 95%
+}
+
+.remove-item {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ margin-bottom: 10px;
+ font-size: 33px;
+ color: #EF476F;
+ cursor: pointer;
}
\ No newline at end of file
diff --git a/src/routes/PositionDetails/components/PositionCandidates/index.jsx b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
index 4d5f8f27..00055bbf 100644
--- a/src/routes/PositionDetails/components/PositionCandidates/index.jsx
+++ b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
@@ -252,7 +252,11 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
{statusFilterKey === CANDIDATE_STATUS_FILTER_KEY.INTERESTED &&
hasPermission(PERMISSIONS.UPDATE_JOB_CANDIDATE) && (
-
Schedule Another Interview
+
openInterviewDetailsPopup(candidate)}
+ >
+ Schedule Another Interview
+
{candidate.interviews.length > 0 && (
{
});
};
+const patchInterviewInState = (state, candidateId, interviewData) => {
+ const candidateIndex = _.findIndex(_.get(state, "position.candidates"), {
+ id: candidateId,
+ });
+
+ if (candidateIndex === -1) {
+ return state;
+ }
+
+ const updatedCandidate = update(state.position.candidates[candidateIndex], {
+ status: { $set: "interview" },
+ interviews: { $push: [interviewData] },
+ });
+
+ return update(state, {
+ loading: { $set: false },
+ error: { $set: undefined },
+ position: {
+ candidates: {
+ $splice: [[candidateIndex, 1, updatedCandidate]],
+ },
+ },
+ });
+};
+
const reducer = (state = initialState, action) => {
switch (action.type) {
case ACTION_TYPE.RESET_POSITION_STATE:
@@ -87,6 +112,24 @@ const reducer = (state = initialState, action) => {
error: action.payload,
});
+ case ACTION_TYPE.ADD_INTERVIEW_PENDING:
+ return {
+ ...state,
+ loading: true,
+ error: undefined,
+ };
+
+ case ACTION_TYPE.ADD_INTERVIEW_SUCCESS:
+ return patchInterviewInState(state, action.meta.candidateId, {
+ ...action.payload,
+ });
+
+ case ACTION_TYPE.ADD_INTERVIEW_ERROR:
+ return {
+ ...state,
+ loading: false,
+ error: action.payload,
+ };
default:
return state;
}
diff --git a/src/services/teams.js b/src/services/teams.js
index b8053cb6..5c1d12e0 100644
--- a/src/services/teams.js
+++ b/src/services/teams.js
@@ -3,6 +3,7 @@
*/
import { axiosInstance as axios } from "./requestInterceptor";
import config from "../../config";
+import { generateInterview } from "utils/helpers";
/**
* Get my teams.
@@ -59,6 +60,44 @@ export const patchPositionCandidate = (candidateId, partialCandidateData) => {
);
};
+/**
+ * Patch New Candidate Interview
+ *
+ * @param {string} candidateId interview candidate id
+ * @param {object} interviewData emails and interview length
+ * @returns {Promise} interview object
+ */
+export const patchCandidateInterview = (candidateId, interviewData) => {
+ // endpoint not currently implemented so response is mocked
+ /* return axios.patch(
+ `${config.API.V5}/jobCandidates/${candidateId}/requestInterview`,
+ interviewData
+ ); */
+
+ const { attendeesList, xaiTemplate, createdBy, round } = interviewData;
+
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ resolve({
+ data: generateInterview({
+ attendeesList,
+ xaiTemplate,
+ jobCandidates: candidateId,
+ updatedBy: "",
+ updatedAt: "",
+ startTimestamp: new Date(
+ Date.now() + 1000 * 60 * 60 * 24 * 3
+ ).toString(), // returns the timestamp 3 days from now
+ createdAt: Date(),
+ createdBy,
+ status: "Scheduling",
+ round,
+ }),
+ });
+ }, 2000);
+ });
+};
+
/**
* Get Team Members
*
diff --git a/src/utils/helpers.js b/src/utils/helpers.js
index cf321f42..5c9c251e 100644
--- a/src/utils/helpers.js
+++ b/src/utils/helpers.js
@@ -52,6 +52,22 @@ export const rollDice = (lowNum, highNum) => {
return Math.floor(Math.random() * diffPlusOne + lowNum);
};
+export const generateInterview = (data) => ({
+ id: data.id || faker.datatype.uuid(),
+ googleCalendarId: data.googleCalendarId || "",
+ attendeesList: data.attendeesList || [],
+ startTimestamp: data.startTimestamp || faker.date.recent(),
+ custommessage: data.custommessage || "",
+ xaiTemplate: data.xaiTemplate || "30-min-interview",
+ jobCandidates: data.jobCandidates || faker.datatype.uuid(),
+ round: data.round,
+ status: data.status || "Completed",
+ createdBy: data.createdBy || faker.datatype.uuid(),
+ updatedBy: data.updatedBy || faker.datatype.uuid(),
+ createdAt: data.createdAt || faker.date.past(),
+ updatedAt: data.updatedAt || faker.date.past(),
+})
+
export const getFakeInterviews = (candidate) => {
// decide how many interviews to return
const numInterviews = rollDice(1, 3);
@@ -61,21 +77,11 @@ export const getFakeInterviews = (candidate) => {
const numEmails = rollDice(1, 5);
const emails = _.times(numEmails, faker.internet.exampleEmail);
- const interview = {
- id: faker.datatype.uuid(),
- googleCalendarId: "",
+ const interview = generateInterview({
attendeesList: emails,
- startTimestamp: faker.date.recent(),
- custommessage: "",
- xaiTemplate: "",
jobCandidates: candidate.id,
round: i + 1,
- status: "Completed",
- createdBy: faker.datatype.uuid(),
- updatedBy: faker.datatype.uuid(),
- createdAt: faker.date.past(),
- updatedAt: faker.date.past(),
- };
+ });
interviews.push(interview);
}
return interviews;
From 793d7680a3b49739019d3b365aaf2e4610315664 Mon Sep 17 00:00:00 2001
From: Michael Baghel
Date: Mon, 19 Apr 2021 18:23:46 +0400
Subject: [PATCH 08/20] Added Interview Confirmation Popup. Started validation
for Details popup
---
.../InterviewConfirmPopup/index.jsx | 49 +++++++++++++++++++
.../InterviewConfirmPopup/styles.module.scss | 17 +++++++
.../InterviewDetailsPopup/index.jsx | 34 ++++++++++---
.../components/PositionCandidates/index.jsx | 6 +++
src/utils/helpers.js | 8 ++-
5 files changed, 105 insertions(+), 9 deletions(-)
create mode 100644 src/routes/PositionDetails/components/InterviewConfirmPopup/index.jsx
create mode 100644 src/routes/PositionDetails/components/InterviewConfirmPopup/styles.module.scss
diff --git a/src/routes/PositionDetails/components/InterviewConfirmPopup/index.jsx b/src/routes/PositionDetails/components/InterviewConfirmPopup/index.jsx
new file mode 100644
index 00000000..42d5b2bf
--- /dev/null
+++ b/src/routes/PositionDetails/components/InterviewConfirmPopup/index.jsx
@@ -0,0 +1,49 @@
+import React from "react";
+import PT from "prop-types";
+import SimpleModal from "components/SimpleModal";
+import "./styles.module.scss";
+
+function InterviewConfirmPopup({ open, onClose }) {
+ return (
+
+
+
+ Please check your email and look for emails from
+ scheduler@topcoder.com .
+
+
+ All interviewers will be able to select availability from there and
+ the system will help you select and book the time.
+
+
+ You may manually select your available times from the email, or click
+ “View More Times” to see
+ expanded options, Additionally, you may click
+ “Overlay My Calendar” to
+ integrate with your calendar and allow the system to schedule based on
+ your calendar availability.
+
+
+ If you have any issues with scheduling, please contact
+ talent@topcoder.com .
+
+
+
+
+ );
+}
+
+InterviewConfirmPopup.propTypes = {
+ open: PT.bool,
+ onClose: PT.func,
+};
+
+export default InterviewConfirmPopup;
diff --git a/src/routes/PositionDetails/components/InterviewConfirmPopup/styles.module.scss b/src/routes/PositionDetails/components/InterviewConfirmPopup/styles.module.scss
new file mode 100644
index 00000000..7d0520e6
--- /dev/null
+++ b/src/routes/PositionDetails/components/InterviewConfirmPopup/styles.module.scss
@@ -0,0 +1,17 @@
+.modal-content {
+ & > p {
+ margin-bottom: 20px;
+ }
+ & * a {
+ color: #0D61BF;
+ }
+}
+
+.highlighted {
+ color: #219174;
+ font-weight: 500;
+}
+
+.video {
+ width: 100%;
+}
\ No newline at end of file
diff --git a/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx b/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
index ace8778e..b38969e6 100644
--- a/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
+++ b/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
@@ -13,6 +13,13 @@ import { FORM_FIELD_TYPE } from "constants";
import "./styles.module.scss";
import RadioFieldGroup from "components/RadioFieldGroup";
+const logValues = (values) => {
+ console.log(values);
+ return {};
+};
+
+// const emailValidator = ()
+
function InterviewDetailsPopup({ open, onClose, candidate }) {
const [isLoading, setIsLoading] = useState(true);
const [myEmail, setMyEmail] = useState("");
@@ -28,7 +35,7 @@ function InterviewDetailsPopup({ open, onClose, candidate }) {
}, []);
const onSubmitCallback = useCallback(
- (formData) => {
+ async (formData) => {
const secondaryEmails =
formData.emails?.filter(
(email) => typeof email === "string" && email.length > 0
@@ -40,11 +47,9 @@ function InterviewDetailsPopup({ open, onClose, candidate }) {
createdBy: myId,
};
- console.log(interviewData);
-
- dispatch(addInterview(candidate.id, interviewData));
+ await dispatch(addInterview(candidate.id, interviewData));
},
- [dispatch, candidate]
+ [dispatch, candidate, myId]
);
return isLoading ? null : (
@@ -57,27 +62,40 @@ function InterviewDetailsPopup({ open, onClose, candidate }) {
mutators={{
...arrayMutators,
}}
+ validate={logValues}
>
{({
handleSubmit,
form: {
mutators: { push },
+ reset,
},
+ submitting,
+ hasValidationErrors,
}) => (
{
+ reset();
+ onClose();
+ }}
title="Schedule an Interview"
button={
{
+ handleSubmit().then(() => {
+ reset();
+ onClose();
+ });
+ }}
size="medium"
isSubmit
- disabled={false}
+ disabled={submitting || hasValidationErrors}
>
Begin scheduling
}
+ disabled={submitting}
>
diff --git a/src/routes/PositionDetails/components/PositionCandidates/index.jsx b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
index 00055bbf..8a68e091 100644
--- a/src/routes/PositionDetails/components/PositionCandidates/index.jsx
+++ b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
@@ -30,6 +30,7 @@ import ActionsMenu from "components/ActionsMenu";
import LatestInterview from "../LatestInterview";
import InterviewDetailsPopup from "../InterviewDetailsPopup";
import PreviousInterviewsPopup from "../PreviousInterviewsPopup";
+import InterviewConfirmPopup from "../InterviewConfirmPopup";
/**
* Generates a function to sort candidates
@@ -66,6 +67,7 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
const [interviewDetailsOpen, setInterviewDetailsOpen] = useState(false);
const [prevInterviewsOpen, setPrevInterviewsOpen] = useState(false);
const [selectedCandidate, setSelectedCandidate] = useState(null);
+ const [interviewConfirmOpen, setInterviewConfirmOpen] = useState(true);
const openInterviewDetailsPopup = (candidate) => {
setSelectedCandidate(candidate);
@@ -304,6 +306,10 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
onClose={() => setInterviewDetailsOpen(false)}
candidate={selectedCandidate}
/>
+ setInterviewConfirmOpen(false)}
+ />
>
);
};
diff --git a/src/utils/helpers.js b/src/utils/helpers.js
index 5c9c251e..6e89f3c0 100644
--- a/src/utils/helpers.js
+++ b/src/utils/helpers.js
@@ -6,6 +6,7 @@
*/
import _ from "lodash";
import faker from "faker";
+import { CANDIDATE_STATUS } from "constants";
/**
* Delay code for some milliseconds using promise.
@@ -66,9 +67,14 @@ export const generateInterview = (data) => ({
updatedBy: data.updatedBy || faker.datatype.uuid(),
createdAt: data.createdAt || faker.date.past(),
updatedAt: data.updatedAt || faker.date.past(),
-})
+});
export const getFakeInterviews = (candidate) => {
+ // If candidate still to review return empty array
+ if (candidate.status === CANDIDATE_STATUS.OPEN) {
+ return [];
+ }
+
// decide how many interviews to return
const numInterviews = rollDice(1, 3);
From a34ef25ed4978d608c217a477a9e164ef0b66f8c Mon Sep 17 00:00:00 2001
From: Michael Baghel
Date: Mon, 19 Apr 2021 21:20:01 +0400
Subject: [PATCH 09/20] Interview details popup working with validation and
triggers confirm popup
---
.../InterviewDetailsPopup/index.jsx | 129 ++++--------------
.../components/PositionCandidates/index.jsx | 3 +-
src/utils/helpers.js | 4 +-
3 files changed, 34 insertions(+), 102 deletions(-)
diff --git a/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx b/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
index b38969e6..aa2e9612 100644
--- a/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
+++ b/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
@@ -13,14 +13,34 @@ import { FORM_FIELD_TYPE } from "constants";
import "./styles.module.scss";
import RadioFieldGroup from "components/RadioFieldGroup";
-const logValues = (values) => {
- console.log(values);
- return {};
+const validateExists = (value) => {
+ return value ? undefined : "Required";
};
-// const emailValidator = ()
+const validateIsEmail = (value) => {
+ if (!value) return undefined;
+ return /\S+@\S+\.\S+/.test(value) ? undefined : "Please enter valid email";
+};
+
+const validator = (values) => {
+ const errors = {};
+
+ errors.myemail =
+ validateExists(values.myemail) || validateIsEmail(values.myemail);
+ errors.email2 =
+ validateExists(values.email2) || validateIsEmail(values.email2);
+
+ errors.emails = [];
+ if (values.emails) {
+ for (const email of values.emails) {
+ errors.emails.push(validateIsEmail(email));
+ }
+ }
+
+ return errors;
+};
-function InterviewDetailsPopup({ open, onClose, candidate }) {
+function InterviewDetailsPopup({ open, onClose, candidate, openNext }) {
const [isLoading, setIsLoading] = useState(true);
const [myEmail, setMyEmail] = useState("");
const [myId, setMyId] = useState("");
@@ -62,7 +82,7 @@ function InterviewDetailsPopup({ open, onClose, candidate }) {
mutators={{
...arrayMutators,
}}
- validate={logValues}
+ validate={validator}
>
{({
handleSubmit,
@@ -86,6 +106,7 @@ function InterviewDetailsPopup({ open, onClose, candidate }) {
handleSubmit().then(() => {
reset();
onClose();
+ openNext();
});
}}
size="medium"
@@ -140,7 +161,7 @@ function InterviewDetailsPopup({ open, onClose, candidate }) {
placeholder: "Email Address",
label: "Email Address",
maxLength: 320,
- isRequired: true,
+ customValidator: true,
}}
/>
@@ -199,97 +221,6 @@ function InterviewDetailsPopup({ open, onClose, candidate }) {
)}
- /*
*/
);
}
diff --git a/src/routes/PositionDetails/components/PositionCandidates/index.jsx b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
index 8a68e091..43729349 100644
--- a/src/routes/PositionDetails/components/PositionCandidates/index.jsx
+++ b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
@@ -67,7 +67,7 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
const [interviewDetailsOpen, setInterviewDetailsOpen] = useState(false);
const [prevInterviewsOpen, setPrevInterviewsOpen] = useState(false);
const [selectedCandidate, setSelectedCandidate] = useState(null);
- const [interviewConfirmOpen, setInterviewConfirmOpen] = useState(true);
+ const [interviewConfirmOpen, setInterviewConfirmOpen] = useState(false);
const openInterviewDetailsPopup = (candidate) => {
setSelectedCandidate(candidate);
@@ -305,6 +305,7 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
open={interviewDetailsOpen}
onClose={() => setInterviewDetailsOpen(false)}
candidate={selectedCandidate}
+ openNext={() => setInterviewConfirmOpen(true)}
/>
{
}
// decide how many interviews to return
- const numInterviews = rollDice(1, 3);
+ const numInterviews = rollDice(0, 3);
const interviews = [];
for (let i = 0; i < numInterviews; i++) {
- const numEmails = rollDice(1, 5);
+ const numEmails = rollDice(2, 6);
const emails = _.times(numEmails, faker.internet.exampleEmail);
const interview = generateInterview({
From d68448853bcf256d554bef84406338cf834ee7e8 Mon Sep 17 00:00:00 2001
From: Michael Baghel
Date: Mon, 19 Apr 2021 22:51:42 +0400
Subject: [PATCH 10/20] Added selectCandidatePopup, started cleanup of other
components
---
.../components/PositionCandidates/index.jsx | 29 +++-
.../PreviousInterviewsPopup/index.jsx | 18 +--
.../components/SelectCandidatePopup/index.jsx | 125 ++++++++++++++++++
.../SelectCandidatePopup/styles.module.scss | 7 +
4 files changed, 164 insertions(+), 15 deletions(-)
create mode 100644 src/routes/PositionDetails/components/SelectCandidatePopup/index.jsx
create mode 100644 src/routes/PositionDetails/components/SelectCandidatePopup/styles.module.scss
diff --git a/src/routes/PositionDetails/components/PositionCandidates/index.jsx b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
index 43729349..a84f42f7 100644
--- a/src/routes/PositionDetails/components/PositionCandidates/index.jsx
+++ b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
@@ -31,6 +31,7 @@ import LatestInterview from "../LatestInterview";
import InterviewDetailsPopup from "../InterviewDetailsPopup";
import PreviousInterviewsPopup from "../PreviousInterviewsPopup";
import InterviewConfirmPopup from "../InterviewConfirmPopup";
+import SelectCandidatePopup from "../SelectCandidatePopup";
/**
* Generates a function to sort candidates
@@ -68,6 +69,8 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
const [prevInterviewsOpen, setPrevInterviewsOpen] = useState(false);
const [selectedCandidate, setSelectedCandidate] = useState(null);
const [interviewConfirmOpen, setInterviewConfirmOpen] = useState(false);
+ const [selectCandidateOpen, setSelectCandidateOpen] = useState(false);
+ const [isReject, setIsReject] = useState(false);
const openInterviewDetailsPopup = (candidate) => {
setSelectedCandidate(candidate);
@@ -79,6 +82,12 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
setPrevInterviewsOpen(true);
};
+ const openSelectCandidatePopup = (candidate, isReject) => {
+ setSelectedCandidate(candidate);
+ isReject ? setIsReject(true) : setIsReject(false);
+ setSelectCandidateOpen(true);
+ };
+
const { candidates } = position;
const [sortBy, setSortBy] = useState(CANDIDATES_SORT_BY.SKILL_MATCHED);
const statusFilter = useMemo(
@@ -133,17 +142,19 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
const markCandidateShortlisted = useCallback(
(candidateId) => {
- updateCandidate(candidateId, {
+ return updateCandidate(candidateId, {
status: CANDIDATE_STATUS.SHORTLIST,
})
.then(() => {
toastr.success("Candidate is marked as interested.");
+ setSelectCandidateOpen(false);
})
.catch((error) => {
toastr.error(
"Failed to mark candidate as interested",
error.toString()
);
+ setSelectCandidateOpen(false);
});
},
[updateCandidate]
@@ -151,17 +162,19 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
const markCandidateRejected = useCallback(
(candidateId) => {
- updateCandidate(candidateId, {
+ return updateCandidate(candidateId, {
status: CANDIDATE_STATUS.REJECTED,
})
.then(() => {
toastr.success("Candidate is marked as not interested.");
+ setSelectCandidateOpen(false);
})
.catch((error) => {
toastr.error(
"Failed to mark candidate as not interested",
error.toString()
);
+ setSelectCandidateOpen(false);
});
},
[updateCandidate]
@@ -238,13 +251,13 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
{
label: "Select Candidate",
action: () => {
- markCandidateShortlisted(candidate.id);
+ openSelectCandidatePopup(candidate);
},
},
{
label: "Decline Candidate",
action: () => {
- markCandidateRejected(candidate.id);
+ openSelectCandidatePopup(candidate, true);
},
},
]}
@@ -311,6 +324,14 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
open={interviewConfirmOpen}
onClose={() => setInterviewConfirmOpen(false)}
/>
+ setSelectCandidateOpen(false)}
+ />
>
);
};
diff --git a/src/routes/PositionDetails/components/PreviousInterviewsPopup/index.jsx b/src/routes/PositionDetails/components/PreviousInterviewsPopup/index.jsx
index b3da61df..343b55e1 100644
--- a/src/routes/PositionDetails/components/PreviousInterviewsPopup/index.jsx
+++ b/src/routes/PositionDetails/components/PreviousInterviewsPopup/index.jsx
@@ -30,17 +30,13 @@ function PreviousInterviewsPopup(props) {
) : (
<>
- {candidate === null ? (
- ""
- ) : (
-
- )}
+
{showPrevInterviews(candidate.interviews)}
>
diff --git a/src/routes/PositionDetails/components/SelectCandidatePopup/index.jsx b/src/routes/PositionDetails/components/SelectCandidatePopup/index.jsx
new file mode 100644
index 00000000..8a1934a9
--- /dev/null
+++ b/src/routes/PositionDetails/components/SelectCandidatePopup/index.jsx
@@ -0,0 +1,125 @@
+import React, { useCallback, useState } from "react";
+import PT from "prop-types";
+import BaseModal from "components/BaseModal";
+import Button from "components/Button";
+import User from "components/User";
+import "./styles.module.scss";
+import CenteredSpinner from "components/CenteredSpinner";
+
+/* function SelectCandidatePopup({
+ candidate,
+ candidateState,
+ shortList,
+ reject,
+ closeModal,
+}) {
+ const selectButton = (
+ shortList()}
+ type={BUTTON_TYPE.PRIMARY}
+ size={BUTTON_SIZE.MEDIUM}
+ >
+ Confirm
+
+ );
+
+ const rejectButton = (
+
+ Confirm
+
+ );
+ return (
+ !!candidateState && (
+
+
+
+ )
+ );
+} */
+
+const SelectCandidatePopup = ({
+ candidate,
+ isReject,
+ open,
+ closeModal,
+ reject,
+ shortList,
+}) => {
+ const [isLoading, setIsLoading] = useState(false);
+
+ const confirmSelection = useCallback(async () => {
+ setIsLoading(true);
+ if (isReject) {
+ await reject(candidate.id);
+ } else {
+ await shortList(candidate.id);
+ }
+ setIsLoading(false);
+ }, [isReject, candidate, reject, shortList]);
+
+ return (
+
+ Confirm
+
+ }
+ disabled={isLoading}
+ >
+ {candidate === null ? (
+ ""
+ ) : (
+ <>
+
+
+
+ {isLoading ? (
+
+ ) : (
+
+ {isReject
+ ? "Are you sure you want to decline the selected candidate?"
+ : "Please confirm your selection of the above candidate"}
+
+ )}
+ >
+ )}
+
+ );
+};
+
+SelectCandidatePopup.propTypes = {
+ candidate: PT.object,
+ open: PT.bool,
+ isReject: PT.bool,
+ shortList: PT.func,
+ reject: PT.func,
+ closeModal: PT.func,
+};
+
+export default SelectCandidatePopup;
diff --git a/src/routes/PositionDetails/components/SelectCandidatePopup/styles.module.scss b/src/routes/PositionDetails/components/SelectCandidatePopup/styles.module.scss
new file mode 100644
index 00000000..af224c4b
--- /dev/null
+++ b/src/routes/PositionDetails/components/SelectCandidatePopup/styles.module.scss
@@ -0,0 +1,7 @@
+.user {
+ font-size: 14px;
+ color: #0D61BF;
+ padding-bottom: 25px;
+ border-bottom: 1px solid #E9E9E9;
+ margin-bottom: 25px;
+}
\ No newline at end of file
From d81af02915306b46ff1e0c82edd7fd90a92942af Mon Sep 17 00:00:00 2001
From: Michael Baghel
Date: Mon, 19 Apr 2021 23:31:07 +0400
Subject: [PATCH 11/20] Documentation and cleanup
---
src/components/Accordion/index.jsx | 2 +
src/components/Radio/index.jsx | 7 +++
src/components/RadioFieldGroup/index.jsx | 2 +
src/routes/PositionDetails/actions/index.js | 11 ++++-
.../InterviewConfirmPopup/index.jsx | 5 +++
.../InterviewDetailsPopup/index.jsx | 10 +++++
.../components/LatestInterview/index.jsx | 5 +++
.../components/PrevInterviewItem/index.jsx | 5 +++
.../PreviousInterviewsPopup/index.jsx | 6 +++
.../components/SelectCandidatePopup/index.jsx | 45 +++----------------
src/routes/PositionDetails/reducers/index.js | 8 ++++
src/services/teams.js | 2 +-
src/utils/helpers.js | 14 +++++-
13 files changed, 78 insertions(+), 44 deletions(-)
diff --git a/src/components/Accordion/index.jsx b/src/components/Accordion/index.jsx
index 4e0d29e6..890639e9 100644
--- a/src/components/Accordion/index.jsx
+++ b/src/components/Accordion/index.jsx
@@ -1,4 +1,6 @@
/**
+ * Accordion
+ *
* An expandable item which can be used
* repeatadly to form an accordion style display
*/
diff --git a/src/components/Radio/index.jsx b/src/components/Radio/index.jsx
index f3d38639..5072d2c7 100644
--- a/src/components/Radio/index.jsx
+++ b/src/components/Radio/index.jsx
@@ -1,3 +1,10 @@
+/**
+ * Radio
+ *
+ * A styled radio button
+ * Used in RadioFieldGroup component
+ */
+
import React from "react";
import PT from "prop-types";
import cn from "classnames";
diff --git a/src/components/RadioFieldGroup/index.jsx b/src/components/RadioFieldGroup/index.jsx
index 3dde6f14..b36608ac 100644
--- a/src/components/RadioFieldGroup/index.jsx
+++ b/src/components/RadioFieldGroup/index.jsx
@@ -1,4 +1,6 @@
/**
+ * RadioFieldGroup
+ *
* Component that takes a configuration object
* and returns a group of react-final-form radio fields
*/
diff --git a/src/routes/PositionDetails/actions/index.js b/src/routes/PositionDetails/actions/index.js
index b0566318..9d34101e 100644
--- a/src/routes/PositionDetails/actions/index.js
+++ b/src/routes/PositionDetails/actions/index.js
@@ -60,10 +60,17 @@ export const updateCandidate = (candidateId, partialCandidateData) => ({
},
});
-export const addInterview = (candidateId, formData) => ({
+/**
+ * Add interview on server and in Redux store
+ *
+ * @param {string} candidateId position candidate id
+ * @param {object} interviewData data to submit in patch request
+ * @returns {object} an interview object
+ */
+export const addInterview = (candidateId, interviewData) => ({
type: ACTION_TYPE.ADD_INTERVIEW,
payload: async () => {
- const response = await patchCandidateInterview(candidateId, formData);
+ const response = await patchCandidateInterview(candidateId, interviewData);
return response.data;
},
diff --git a/src/routes/PositionDetails/components/InterviewConfirmPopup/index.jsx b/src/routes/PositionDetails/components/InterviewConfirmPopup/index.jsx
index 42d5b2bf..fc287fa4 100644
--- a/src/routes/PositionDetails/components/InterviewConfirmPopup/index.jsx
+++ b/src/routes/PositionDetails/components/InterviewConfirmPopup/index.jsx
@@ -1,3 +1,8 @@
+/**
+ * InterviewConfirmPopup
+ *
+ * Popup to Confirm submission of an interview
+ */
import React from "react";
import PT from "prop-types";
import SimpleModal from "components/SimpleModal";
diff --git a/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx b/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
index aa2e9612..ed7df2fa 100644
--- a/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
+++ b/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
@@ -1,3 +1,9 @@
+/**
+ * InterviewDetailsPopup
+ *
+ * Popup that allows user to schedule an interview
+ * Calls addInterview action
+ */
import React, { useEffect, useState, useCallback } from "react";
import { getAuthUserProfile } from "@topcoder/micro-frontends-navbar-app";
import { Form } from "react-final-form";
@@ -13,6 +19,8 @@ import { FORM_FIELD_TYPE } from "constants";
import "./styles.module.scss";
import RadioFieldGroup from "components/RadioFieldGroup";
+/* Validators for Form */
+
const validateExists = (value) => {
return value ? undefined : "Required";
};
@@ -40,6 +48,8 @@ const validator = (values) => {
return errors;
};
+/********************* */
+
function InterviewDetailsPopup({ open, onClose, candidate, openNext }) {
const [isLoading, setIsLoading] = useState(true);
const [myEmail, setMyEmail] = useState("");
diff --git a/src/routes/PositionDetails/components/LatestInterview/index.jsx b/src/routes/PositionDetails/components/LatestInterview/index.jsx
index 81f1d49d..6a74cc2c 100644
--- a/src/routes/PositionDetails/components/LatestInterview/index.jsx
+++ b/src/routes/PositionDetails/components/LatestInterview/index.jsx
@@ -1,3 +1,8 @@
+/**
+ * LastestInterview
+ *
+ * Table item showing candidates latest interview
+ */
import React from "react";
import PT from "prop-types";
import "./styles.module.scss";
diff --git a/src/routes/PositionDetails/components/PrevInterviewItem/index.jsx b/src/routes/PositionDetails/components/PrevInterviewItem/index.jsx
index 3dae1069..727d56a0 100644
--- a/src/routes/PositionDetails/components/PrevInterviewItem/index.jsx
+++ b/src/routes/PositionDetails/components/PrevInterviewItem/index.jsx
@@ -1,3 +1,8 @@
+/**
+ * PrevInterviewItem
+ *
+ * A list item for the PreviousInterviewsPopup
+ */
import React from "react";
import PT from "prop-types";
import { formatDate } from "utils/format";
diff --git a/src/routes/PositionDetails/components/PreviousInterviewsPopup/index.jsx b/src/routes/PositionDetails/components/PreviousInterviewsPopup/index.jsx
index 343b55e1..a7e29271 100644
--- a/src/routes/PositionDetails/components/PreviousInterviewsPopup/index.jsx
+++ b/src/routes/PositionDetails/components/PreviousInterviewsPopup/index.jsx
@@ -1,3 +1,8 @@
+/**
+ * PreviousInterviewsPopup
+ *
+ * Popup listing a user's previous interviews
+ */
import React from "react";
import PT from "prop-types";
import SimpleModal from "components/SimpleModal";
@@ -8,6 +13,7 @@ import PrevInterviewItem from "../PrevInterviewItem";
function PreviousInterviewsPopup(props) {
const { candidate, open, onClose } = props;
+ // sorts interviews and returns list of PrevInterviewItems
const showPrevInterviews = (interviews) => {
const sortedInterviews = interviews
.slice()
diff --git a/src/routes/PositionDetails/components/SelectCandidatePopup/index.jsx b/src/routes/PositionDetails/components/SelectCandidatePopup/index.jsx
index 8a1934a9..3ccae975 100644
--- a/src/routes/PositionDetails/components/SelectCandidatePopup/index.jsx
+++ b/src/routes/PositionDetails/components/SelectCandidatePopup/index.jsx
@@ -1,3 +1,8 @@
+/**
+ * SelectCandidatePopup
+ *
+ * Confirmation popup on selecting or rejecting a candidate
+ */
import React, { useCallback, useState } from "react";
import PT from "prop-types";
import BaseModal from "components/BaseModal";
@@ -6,46 +11,6 @@ import User from "components/User";
import "./styles.module.scss";
import CenteredSpinner from "components/CenteredSpinner";
-/* function SelectCandidatePopup({
- candidate,
- candidateState,
- shortList,
- reject,
- closeModal,
-}) {
- const selectButton = (
- shortList()}
- type={BUTTON_TYPE.PRIMARY}
- size={BUTTON_SIZE.MEDIUM}
- >
- Confirm
-
- );
-
- const rejectButton = (
-
- Confirm
-
- );
- return (
- !!candidateState && (
-
-
-
- )
- );
-} */
-
const SelectCandidatePopup = ({
candidate,
isReject,
diff --git a/src/routes/PositionDetails/reducers/index.js b/src/routes/PositionDetails/reducers/index.js
index c53c158b..16289269 100644
--- a/src/routes/PositionDetails/reducers/index.js
+++ b/src/routes/PositionDetails/reducers/index.js
@@ -42,6 +42,14 @@ const patchCandidateInState = (state, candidateId, partialCandidateData) => {
});
};
+/**
+ * Patch candidate with new interview without mutating state
+ *
+ * @param {object} state state
+ * @param {string} candidateId candidate id
+ * @param {object} interviewData interview object to add to candidate
+ * @returns {object} new state
+ */
const patchInterviewInState = (state, candidateId, interviewData) => {
const candidateIndex = _.findIndex(_.get(state, "position.candidates"), {
id: candidateId,
diff --git a/src/services/teams.js b/src/services/teams.js
index 5c1d12e0..97133c8f 100644
--- a/src/services/teams.js
+++ b/src/services/teams.js
@@ -76,7 +76,7 @@ export const patchCandidateInterview = (candidateId, interviewData) => {
const { attendeesList, xaiTemplate, createdBy, round } = interviewData;
- return new Promise((resolve, reject) => {
+ return new Promise((resolve) => {
setTimeout(() => {
resolve({
data: generateInterview({
diff --git a/src/utils/helpers.js b/src/utils/helpers.js
index de8c476c..6c498471 100644
--- a/src/utils/helpers.js
+++ b/src/utils/helpers.js
@@ -53,6 +53,12 @@ export const rollDice = (lowNum, highNum) => {
return Math.floor(Math.random() * diffPlusOne + lowNum);
};
+/**
+ * Generates a fake interview object
+ *
+ * @param {object} data an optional data object to configure fake interview
+ * @returns {object} a fake interview object
+ */
export const generateInterview = (data) => ({
id: data.id || faker.datatype.uuid(),
googleCalendarId: data.googleCalendarId || "",
@@ -61,7 +67,7 @@ export const generateInterview = (data) => ({
custommessage: data.custommessage || "",
xaiTemplate: data.xaiTemplate || "30-min-interview",
jobCandidates: data.jobCandidates || faker.datatype.uuid(),
- round: data.round,
+ round: data.round || 1,
status: data.status || "Completed",
createdBy: data.createdBy || faker.datatype.uuid(),
updatedBy: data.updatedBy || faker.datatype.uuid(),
@@ -69,6 +75,12 @@ export const generateInterview = (data) => ({
updatedAt: data.updatedAt || faker.date.past(),
});
+/**
+ * Gets fake interviews to attach to jobCandidates
+ *
+ * @param {object} candidate a jobCandidate object
+ * @returns {object[]} an array of fake interviews
+ */
export const getFakeInterviews = (candidate) => {
// If candidate still to review return empty array
if (candidate.status === CANDIDATE_STATUS.OPEN) {
From 5f783a083f87f907d82197b3950494d88cfd8eea Mon Sep 17 00:00:00 2001
From: Cagdas U
Date: Tue, 27 Apr 2021 23:33:51 +0300
Subject: [PATCH 12/20] feat(interview-scheduler): backend integration
1. Remove mocked parts & integrate with the API.
---
src/constants/index.js | 6 ++
src/routes/JobForm/index.jsx | 9 +-
src/routes/PositionDetails/actions/index.js | 8 --
.../InterviewConfirmPopup/index.jsx | 3 +-
.../InterviewDetailsPopup/index.jsx | 91 +++++--------------
.../InterviewDetailsPopup/styles.module.scss | 14 +--
.../components/LatestInterview/index.jsx | 2 +-
.../components/PositionCandidates/index.jsx | 17 ++--
.../PreviousInterviewsPopup/index.jsx | 2 +-
src/services/teams.js | 29 +-----
src/utils/helpers.js | 66 --------------
11 files changed, 61 insertions(+), 186 deletions(-)
diff --git a/src/constants/index.js b/src/constants/index.js
index a46a7e77..431787d9 100644
--- a/src/constants/index.js
+++ b/src/constants/index.js
@@ -298,3 +298,9 @@ export const STATUS_OPTIONS = [
*/
export const DISABLED_DESCRIPTION_MESSAGE =
"You may not edit a Job Description that is currently posted to Topcoder.com. Please contact support@topcoder.com.";
+
+/**
+ * The media URL to be shown on Interview popup
+ */
+export const INTERVIEW_POPUP_MEDIA_URL =
+ "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
diff --git a/src/routes/JobForm/index.jsx b/src/routes/JobForm/index.jsx
index 05bc0a29..880f22e2 100644
--- a/src/routes/JobForm/index.jsx
+++ b/src/routes/JobForm/index.jsx
@@ -8,6 +8,8 @@
import React, { useState, useEffect } from "react";
import PT from "prop-types";
import { toastr } from "react-redux-toastr";
+import _ from "lodash";
+import store from "../../store";
import Page from "components/Page";
import PageHeader from "components/PageHeader";
import { useData } from "hooks/useData";
@@ -61,8 +63,10 @@ const JobForm = ({ teamId, jobId }) => {
// as we are using `PUT` method (not `PATCH`) we have send ALL the fields
// fields which we don't send would become `null` otherwise
- const getRequestData = (values) =>
- _.pick(values, [
+ const getRequestData = (values) => {
+ const externalId = _.get(store.getState(), "authUser.userId");
+ values.externalId = externalId && _.toString(externalId);
+ return _.pick(values, [
"projectId",
"externalId",
"description",
@@ -76,6 +80,7 @@ const JobForm = ({ teamId, jobId }) => {
"skills",
"status",
]);
+ };
useEffect(() => {
if (skills && job && !options) {
diff --git a/src/routes/PositionDetails/actions/index.js b/src/routes/PositionDetails/actions/index.js
index 9d34101e..da23e614 100644
--- a/src/routes/PositionDetails/actions/index.js
+++ b/src/routes/PositionDetails/actions/index.js
@@ -8,7 +8,6 @@ import {
patchCandidateInterview,
} from "services/teams";
import { ACTION_TYPE } from "constants";
-import { getFakeInterviews } from "utils/helpers";
/**
* Load Team Position details (team job)
@@ -22,13 +21,6 @@ export const loadPosition = (teamId, positionId) => ({
type: ACTION_TYPE.LOAD_POSITION,
payload: async () => {
const response = await getPositionDetails(teamId, positionId);
-
- // inject mock interview data to candidates list
- for (const candidate of response.data.candidates) {
- const fakeInterviews = getFakeInterviews(candidate);
- _.set(candidate, "interviews", fakeInterviews);
- }
-
return response.data;
},
meta: {
diff --git a/src/routes/PositionDetails/components/InterviewConfirmPopup/index.jsx b/src/routes/PositionDetails/components/InterviewConfirmPopup/index.jsx
index fc287fa4..c2b6ac79 100644
--- a/src/routes/PositionDetails/components/InterviewConfirmPopup/index.jsx
+++ b/src/routes/PositionDetails/components/InterviewConfirmPopup/index.jsx
@@ -6,6 +6,7 @@
import React from "react";
import PT from "prop-types";
import SimpleModal from "components/SimpleModal";
+import { INTERVIEW_POPUP_MEDIA_URL } from "constants";
import "./styles.module.scss";
function InterviewConfirmPopup({ open, onClose }) {
@@ -38,7 +39,7 @@ function InterviewConfirmPopup({ open, onClose }) {
diff --git a/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx b/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
index ed7df2fa..28dbf1e6 100644
--- a/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
+++ b/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
@@ -4,8 +4,7 @@
* Popup that allows user to schedule an interview
* Calls addInterview action
*/
-import React, { useEffect, useState, useCallback } from "react";
-import { getAuthUserProfile } from "@topcoder/micro-frontends-navbar-app";
+import React, { useCallback } from "react";
import { Form } from "react-final-form";
import arrayMutators from "final-form-arrays";
import { FieldArray } from "react-final-form-arrays";
@@ -21,10 +20,6 @@ import RadioFieldGroup from "components/RadioFieldGroup";
/* Validators for Form */
-const validateExists = (value) => {
- return value ? undefined : "Required";
-};
-
const validateIsEmail = (value) => {
if (!value) return undefined;
return /\S+@\S+\.\S+/.test(value) ? undefined : "Please enter valid email";
@@ -33,11 +28,6 @@ const validateIsEmail = (value) => {
const validator = (values) => {
const errors = {};
- errors.myemail =
- validateExists(values.myemail) || validateIsEmail(values.myemail);
- errors.email2 =
- validateExists(values.email2) || validateIsEmail(values.email2);
-
errors.emails = [];
if (values.emails) {
for (const email of values.emails) {
@@ -51,42 +41,28 @@ const validator = (values) => {
/********************* */
function InterviewDetailsPopup({ open, onClose, candidate, openNext }) {
- const [isLoading, setIsLoading] = useState(true);
- const [myEmail, setMyEmail] = useState("");
- const [myId, setMyId] = useState("");
const dispatch = useDispatch();
- useEffect(() => {
- getAuthUserProfile().then((res) => {
- setMyEmail(res.email || "");
- setMyId(res.userId);
- setIsLoading(false);
- });
- }, []);
-
const onSubmitCallback = useCallback(
async (formData) => {
- const secondaryEmails =
+ const attendeesList =
formData.emails?.filter(
(email) => typeof email === "string" && email.length > 0
) || [];
const interviewData = {
xaiTemplate: formData.time,
- attendeesList: [formData.myemail, formData.email2, ...secondaryEmails],
- round: candidate.interviews.length + 1,
- createdBy: myId,
+ attendeesList,
};
await dispatch(addInterview(candidate.id, interviewData));
},
- [dispatch, candidate, myId]
+ [dispatch, candidate]
);
- return isLoading ? null : (
+ return (
@@ -164,32 +140,6 @@ function InterviewDetailsPopup({ open, onClose, candidate, openNext }) {
Please provide email addresses for all parties you would like
involved with the interview.
-
-
- push("emails")}
- >
- Add more
-
{({ fields }) => {
return fields.map((name, index) => (
@@ -207,18 +157,27 @@ function InterviewDetailsPopup({ open, onClose, candidate, openNext }) {
}}
/>
- fields.remove(index)}
- styleName="remove-item"
- >
- ×
-
+ {index > 0 && (
+ fields.remove(index)}
+ styleName="remove-item"
+ >
+ ×
+
+ )}
));
}}
+ push("emails")}
+ >
+ Add more
+
diff --git a/src/routes/PositionDetails/components/InterviewDetailsPopup/styles.module.scss b/src/routes/PositionDetails/components/InterviewDetailsPopup/styles.module.scss
index bdd9262d..6df0786e 100644
--- a/src/routes/PositionDetails/components/InterviewDetailsPopup/styles.module.scss
+++ b/src/routes/PositionDetails/components/InterviewDetailsPopup/styles.module.scss
@@ -43,7 +43,7 @@
background: #fff;
margin: 10px 0 0 0;
padding: 0;
- color: blue;
+ color: #0D61BF;
border: none;
border-radius: 0;
@@ -60,15 +60,17 @@
}
.array-input {
- width: 95%
+ width: 100%
}
.remove-item {
- display: flex;
- flex-direction: column;
- justify-content: flex-end;
- margin-bottom: 10px;
+ position: absolute;
+ right: 45px;
+ margin-top: 33px;
font-size: 33px;
color: #EF476F;
cursor: pointer;
+ &:focus {
+ outline: none;
+ }
}
\ No newline at end of file
diff --git a/src/routes/PositionDetails/components/LatestInterview/index.jsx b/src/routes/PositionDetails/components/LatestInterview/index.jsx
index 6a74cc2c..de78b88b 100644
--- a/src/routes/PositionDetails/components/LatestInterview/index.jsx
+++ b/src/routes/PositionDetails/components/LatestInterview/index.jsx
@@ -9,7 +9,7 @@ import "./styles.module.scss";
import { formatDate } from "utils/format";
function LatestInterview({ interviews }) {
- if (!interviews.length) {
+ if (!interviews || !interviews.length) {
return
;
}
diff --git a/src/routes/PositionDetails/components/PositionCandidates/index.jsx b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
index a84f42f7..ede24193 100644
--- a/src/routes/PositionDetails/components/PositionCandidates/index.jsx
+++ b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
@@ -272,14 +272,15 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
>
Schedule Another Interview
- {candidate.interviews.length > 0 && (
-
openPrevInterviewsPopup(candidate)}
- >
- View Previous Interviews
-
- )}
+ {candidate.interviews &&
+ candidate.interviews.length > 0 && (
+
openPrevInterviewsPopup(candidate)}
+ >
+ View Previous Interviews
+
+ )}
)}
diff --git a/src/routes/PositionDetails/components/PreviousInterviewsPopup/index.jsx b/src/routes/PositionDetails/components/PreviousInterviewsPopup/index.jsx
index a7e29271..eb6aeed7 100644
--- a/src/routes/PositionDetails/components/PreviousInterviewsPopup/index.jsx
+++ b/src/routes/PositionDetails/components/PreviousInterviewsPopup/index.jsx
@@ -15,7 +15,7 @@ function PreviousInterviewsPopup(props) {
// sorts interviews and returns list of PrevInterviewItems
const showPrevInterviews = (interviews) => {
- const sortedInterviews = interviews
+ const sortedInterviews = (interviews || [])
.slice()
.sort((a, b) => a.round - b.round);
diff --git a/src/services/teams.js b/src/services/teams.js
index 97133c8f..58ae0295 100644
--- a/src/services/teams.js
+++ b/src/services/teams.js
@@ -3,7 +3,6 @@
*/
import { axiosInstance as axios } from "./requestInterceptor";
import config from "../../config";
-import { generateInterview } from "utils/helpers";
/**
* Get my teams.
@@ -68,34 +67,10 @@ export const patchPositionCandidate = (candidateId, partialCandidateData) => {
* @returns {Promise} interview object
*/
export const patchCandidateInterview = (candidateId, interviewData) => {
- // endpoint not currently implemented so response is mocked
- /* return axios.patch(
+ return axios.patch(
`${config.API.V5}/jobCandidates/${candidateId}/requestInterview`,
interviewData
- ); */
-
- const { attendeesList, xaiTemplate, createdBy, round } = interviewData;
-
- return new Promise((resolve) => {
- setTimeout(() => {
- resolve({
- data: generateInterview({
- attendeesList,
- xaiTemplate,
- jobCandidates: candidateId,
- updatedBy: "",
- updatedAt: "",
- startTimestamp: new Date(
- Date.now() + 1000 * 60 * 60 * 24 * 3
- ).toString(), // returns the timestamp 3 days from now
- createdAt: Date(),
- createdBy,
- status: "Scheduling",
- round,
- }),
- });
- }, 2000);
- });
+ );
};
/**
diff --git a/src/utils/helpers.js b/src/utils/helpers.js
index 6c498471..53f24cd2 100644
--- a/src/utils/helpers.js
+++ b/src/utils/helpers.js
@@ -5,8 +5,6 @@
* 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 faker from "faker";
-import { CANDIDATE_STATUS } from "constants";
/**
* Delay code for some milliseconds using promise.
@@ -40,67 +38,3 @@ export const getSelectOptionByValue = (value, selectOptions) => {
return option;
};
-
-/**
- * Generates a pseudorandom integer between two numbers (inclusive)
- *
- * @param {number} lowNum an integer to use as minimun
- * @param {number} highNum an integer to use as maximum
- * @returns {number} a psuedorandom number between low and high
- */
-export const rollDice = (lowNum, highNum) => {
- const diffPlusOne = highNum - lowNum + 1;
- return Math.floor(Math.random() * diffPlusOne + lowNum);
-};
-
-/**
- * Generates a fake interview object
- *
- * @param {object} data an optional data object to configure fake interview
- * @returns {object} a fake interview object
- */
-export const generateInterview = (data) => ({
- id: data.id || faker.datatype.uuid(),
- googleCalendarId: data.googleCalendarId || "",
- attendeesList: data.attendeesList || [],
- startTimestamp: data.startTimestamp || faker.date.recent(),
- custommessage: data.custommessage || "",
- xaiTemplate: data.xaiTemplate || "30-min-interview",
- jobCandidates: data.jobCandidates || faker.datatype.uuid(),
- round: data.round || 1,
- status: data.status || "Completed",
- createdBy: data.createdBy || faker.datatype.uuid(),
- updatedBy: data.updatedBy || faker.datatype.uuid(),
- createdAt: data.createdAt || faker.date.past(),
- updatedAt: data.updatedAt || faker.date.past(),
-});
-
-/**
- * Gets fake interviews to attach to jobCandidates
- *
- * @param {object} candidate a jobCandidate object
- * @returns {object[]} an array of fake interviews
- */
-export const getFakeInterviews = (candidate) => {
- // If candidate still to review return empty array
- if (candidate.status === CANDIDATE_STATUS.OPEN) {
- return [];
- }
-
- // decide how many interviews to return
- const numInterviews = rollDice(0, 3);
-
- const interviews = [];
- for (let i = 0; i < numInterviews; i++) {
- const numEmails = rollDice(2, 6);
- const emails = _.times(numEmails, faker.internet.exampleEmail);
-
- const interview = generateInterview({
- attendeesList: emails,
- jobCandidates: candidate.id,
- round: i + 1,
- });
- interviews.push(interview);
- }
- return interviews;
-};
From 8e34aeaee90262df5ab5d00aacd5b13b3b3dfceb Mon Sep 17 00:00:00 2001
From: Atif
Date: Wed, 28 Apr 2021 20:06:21 +0530
Subject: [PATCH 13/20] Node version 10
---
docker/Dockerfile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 62e860d6..a3565335 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,5 +1,5 @@
# Use the base image with Node.js
-FROM node:latest
+FROM node:10
ARG APPMODE
ARG APPENV
From 757fac692e4eee1318507d90ad0990b55292f714 Mon Sep 17 00:00:00 2001
From: nkumar-topcoder <33625707+nkumar-topcoder@users.noreply.github.com>
Date: Wed, 28 Apr 2021 20:24:05 +0530
Subject: [PATCH 14/20] Update mockDockerfile
---
docker/mockDockerfile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docker/mockDockerfile b/docker/mockDockerfile
index 8e603d4a..bf689d26 100644
--- a/docker/mockDockerfile
+++ b/docker/mockDockerfile
@@ -1,5 +1,5 @@
# Use the base image with Node.js
-FROM node:latest
+FROM node:10
# Copy the current directory into the Docker image
COPY . /taas-app
From cfeec2e85bf72fc115e8880d6fea8b378c803b5d Mon Sep 17 00:00:00 2001
From: urwithat
Date: Thu, 29 Apr 2021 00:41:23 +0530
Subject: [PATCH 15/20] Format change for Interview 30 60
---
.../components/InterviewDetailsPopup/index.jsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx b/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
index 28dbf1e6..363bf95c 100644
--- a/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
+++ b/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
@@ -62,7 +62,7 @@ function InterviewDetailsPopup({ open, onClose, candidate, openNext }) {
return (
From ba37135d76cf9f77da132c4f72dc8e3917d8ab3a Mon Sep 17 00:00:00 2001
From: Cagdas U
Date: Thu, 29 Apr 2021 23:44:20 +0300
Subject: [PATCH 16/20] fix(interview-scheduler): various fixes & improvements
Addresses:
https://github.com/topcoder-platform/taas-app/issues/158
https://github.com/topcoder-platform/taas-app/issues/159
https://github.com/topcoder-platform/taas-app/issues/161
https://github.com/topcoder-platform/taas-app/issues/163
https://github.com/topcoder-platform/taas-app/issues/164
https://github.com/topcoder-platform/taas-app/issues/165
https://github.com/topcoder-platform/taas-app/issues/170
https://github.com/topcoder-platform/taas-app/issues/171
https://github.com/topcoder-platform/taas-app/issues/173
---
src/components/ActionsMenu/index.jsx | 6 +-
src/components/ActionsMenu/styles.module.scss | 19 ++++-
src/constants/index.js | 17 +++--
.../InterviewDetailsPopup/index.jsx | 31 ++++++--
.../components/PositionCandidates/index.jsx | 70 +++++++++++++------
.../components/SelectCandidatePopup/index.jsx | 35 +++++++---
.../SelectCandidatePopup/styles.module.scss | 8 +++
src/routes/PositionDetails/reducers/index.js | 21 +++---
8 files changed, 150 insertions(+), 57 deletions(-)
diff --git a/src/components/ActionsMenu/index.jsx b/src/components/ActionsMenu/index.jsx
index b888ade0..7bc36da3 100644
--- a/src/components/ActionsMenu/index.jsx
+++ b/src/components/ActionsMenu/index.jsx
@@ -100,7 +100,11 @@ const ActionsMenu = ({ options = [] }) => {
onClick={closeOnAction(option.action)}
role="button"
tabIndex={0}
- styleName="option"
+ styleName={
+ "option" +
+ (option.style ? " " + option.style : "") +
+ (option.disabled ? " disabled" : "")
+ }
>
{option.label}
diff --git a/src/components/ActionsMenu/styles.module.scss b/src/components/ActionsMenu/styles.module.scss
index c9556de7..18d15c78 100644
--- a/src/components/ActionsMenu/styles.module.scss
+++ b/src/components/ActionsMenu/styles.module.scss
@@ -39,10 +39,23 @@
}
.option {
- color: #0d61bf;
+ color: #219174;
cursor: pointer;
- font-size: 14px;
- line-height: 20px;
+ font-size: 12px;
+ font-weight: bold;
+ letter-spacing: 0.8px;
+ line-height: 30px;
+ text-transform: uppercase;
outline: none;
padding: 5px 0;
}
+
+.danger {
+ color: #EF476F;
+}
+
+.disabled {
+ color: gray;
+ opacity: 0.6;
+ pointer-events: none;
+}
diff --git a/src/constants/index.js b/src/constants/index.js
index 9155d978..7cd2c580 100644
--- a/src/constants/index.js
+++ b/src/constants/index.js
@@ -97,8 +97,10 @@ export const RATE_TYPE = {
export const CANDIDATE_STATUS = {
OPEN: "open",
SELECTED: "selected",
- SHORTLIST: "shortlist",
- REJECTED: "rejected",
+ PLACED: "placed",
+ CLIENT_REJECTED_SCREENING: "client rejected - screening",
+ CLIENT_REJECTED_INTERVIEW: "client rejected - interview",
+ REJECTED_OTHER: "rejected - other",
INTERVIEW: "interview",
TOPCODER_REJECTED: "topcoder-rejected",
};
@@ -126,13 +128,18 @@ export const CANDIDATE_STATUS_FILTERS = [
key: CANDIDATE_STATUS_FILTER_KEY.INTERESTED,
buttonText: "Interviews",
title: "Interviews",
- statuses: [CANDIDATE_STATUS.SHORTLIST, CANDIDATE_STATUS.INTERVIEW],
+ statuses: [CANDIDATE_STATUS.SELECTED, CANDIDATE_STATUS.INTERVIEW],
},
{
key: CANDIDATE_STATUS_FILTER_KEY.NOT_INTERESTED,
buttonText: "Declined",
title: "Declined",
- statuses: [CANDIDATE_STATUS.REJECTED, CANDIDATE_STATUS.TOPCODER_REJECTED],
+ statuses: [
+ CANDIDATE_STATUS.CLIENT_REJECTED_SCREENING,
+ CANDIDATE_STATUS.CLIENT_REJECTED_INTERVIEW,
+ CANDIDATE_STATUS.REJECTED_OTHER,
+ CANDIDATE_STATUS.TOPCODER_REJECTED,
+ ],
},
];
@@ -297,7 +304,7 @@ export const JOB_STATUS_OPTIONS = [
* resource booking status options
*/
export const RESOURCE_BOOKING_STATUS_OPTIONS = [
- { value: "assigned", label: "assigned" },
+ { value: "placed", label: "placed" },
{ value: "closed", label: "closed" },
{ value: "cancelled", label: "cancelled" },
];
diff --git a/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx b/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
index 363bf95c..e7a6d363 100644
--- a/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
+++ b/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
@@ -4,11 +4,13 @@
* Popup that allows user to schedule an interview
* Calls addInterview action
*/
-import React, { useCallback } from "react";
+import React, { useCallback, useEffect, useState } from "react";
+import { getAuthUserProfile } from "@topcoder/micro-frontends-navbar-app";
import { Form } from "react-final-form";
import arrayMutators from "final-form-arrays";
import { FieldArray } from "react-final-form-arrays";
-import { useDispatch } from "react-redux";
+import { toastr } from "react-redux-toastr";
+import { useDispatch, useSelector } from "react-redux";
import { addInterview } from "../../actions";
import User from "components/User";
import BaseModal from "components/BaseModal";
@@ -39,10 +41,20 @@ const validator = (values) => {
};
/********************* */
-
+// TODO: preserve form input in case of error
function InterviewDetailsPopup({ open, onClose, candidate, openNext }) {
+ const [isLoading, setIsLoading] = useState(true);
+ const [myEmail, setMyEmail] = useState("");
+ const { loading } = useSelector((state) => state.positionDetails);
const dispatch = useDispatch();
+ useEffect(() => {
+ getAuthUserProfile().then((res) => {
+ setMyEmail(res.email || "");
+ setIsLoading(false);
+ });
+ }, []);
+
const onSubmitCallback = useCallback(
async (formData) => {
const attendeesList =
@@ -54,15 +66,21 @@ function InterviewDetailsPopup({ open, onClose, candidate, openNext }) {
attendeesList,
};
- await dispatch(addInterview(candidate.id, interviewData));
+ try {
+ await dispatch(addInterview(candidate.id, interviewData));
+ } catch (err) {
+ toastr.error("Interview Creation Failed", err.message);
+ throw err;
+ }
},
[dispatch, candidate]
);
- return (
+ return isLoading ? null : (
diff --git a/src/routes/PositionDetails/components/PositionCandidates/index.jsx b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
index ede24193..61cd6f98 100644
--- a/src/routes/PositionDetails/components/PositionCandidates/index.jsx
+++ b/src/routes/PositionDetails/components/PositionCandidates/index.jsx
@@ -140,10 +140,10 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
[setPage]
);
- const markCandidateShortlisted = useCallback(
- (candidateId) => {
- return updateCandidate(candidateId, {
- status: CANDIDATE_STATUS.SHORTLIST,
+ const markCandidateSelected = useCallback(
+ (candidate) => {
+ return updateCandidate(candidate.id, {
+ status: CANDIDATE_STATUS.SELECTED,
})
.then(() => {
toastr.success("Candidate is marked as interested.");
@@ -161,9 +161,13 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
);
const markCandidateRejected = useCallback(
- (candidateId) => {
- return updateCandidate(candidateId, {
- status: CANDIDATE_STATUS.REJECTED,
+ (candidate) => {
+ const hasInterviews =
+ candidate.interviews && candidate.interviews.length > 0;
+ return updateCandidate(candidate.id, {
+ status: hasInterviews
+ ? CANDIDATE_STATUS.CLIENT_REJECTED_INTERVIEW
+ : CANDIDATE_STATUS.CLIENT_REJECTED_SCREENING,
})
.then(() => {
toastr.success("Candidate is marked as not interested.");
@@ -259,6 +263,7 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
action: () => {
openSelectCandidatePopup(candidate, true);
},
+ style: "danger",
},
]}
/>
@@ -267,20 +272,41 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
{statusFilterKey === CANDIDATE_STATUS_FILTER_KEY.INTERESTED &&
hasPermission(PERMISSIONS.UPDATE_JOB_CANDIDATE) && (
-
openInterviewDetailsPopup(candidate)}
- >
- Schedule Another Interview
-
- {candidate.interviews &&
- candidate.interviews.length > 0 && (
-
openPrevInterviewsPopup(candidate)}
- >
- View Previous Interviews
-
- )}
+
{
+ openInterviewDetailsPopup(candidate);
+ },
+ },
+ {
+ label: "View Previous Interviews",
+ action: () => {
+ openPrevInterviewsPopup(candidate);
+ },
+ disabled:
+ !!candidate.interviews !== true ||
+ candidate.interviews.length === 0,
+ },
+ {
+ separator: true,
+ },
+ {
+ label: "Select Candidate",
+ action: () => {
+ openSelectCandidatePopup(candidate);
+ },
+ },
+ {
+ label: "Decline Candidate",
+ action: () => {
+ openSelectCandidatePopup(candidate, true);
+ },
+ style: "danger",
+ },
+ ]}
+ />
)}
@@ -329,7 +355,7 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
candidate={selectedCandidate}
open={selectCandidateOpen}
isReject={isReject}
- shortList={markCandidateShortlisted}
+ select={markCandidateSelected}
reject={markCandidateRejected}
closeModal={() => setSelectCandidateOpen(false)}
/>
diff --git a/src/routes/PositionDetails/components/SelectCandidatePopup/index.jsx b/src/routes/PositionDetails/components/SelectCandidatePopup/index.jsx
index 3ccae975..c2bf6c14 100644
--- a/src/routes/PositionDetails/components/SelectCandidatePopup/index.jsx
+++ b/src/routes/PositionDetails/components/SelectCandidatePopup/index.jsx
@@ -17,19 +17,19 @@ const SelectCandidatePopup = ({
open,
closeModal,
reject,
- shortList,
+ select,
}) => {
const [isLoading, setIsLoading] = useState(false);
const confirmSelection = useCallback(async () => {
setIsLoading(true);
if (isReject) {
- await reject(candidate.id);
+ await reject(candidate);
} else {
- await shortList(candidate.id);
+ await select(candidate);
}
setIsLoading(false);
- }, [isReject, candidate, reject, shortList]);
+ }, [isReject, candidate, reject, select]);
return (
{isLoading ? (
+ ) : isReject ? (
+ Are you sure you want to decline the selected candidate?
) : (
-
- {isReject
- ? "Are you sure you want to decline the selected candidate?"
- : "Please confirm your selection of the above candidate"}
-
+ <>
+
+ You have selected this applicant - you want this member on your
+ team! What happens next:
+
+
+
+ Upon confirmation, Topcoder will confirm the arrangement with
+ the selected member
+
+
+ A Topcoder Rep will contact you with details on the work
+ arrangement
+
+
+ When both sides accept, we will finalize the agreement and
+ begin onboarding
+
+
+ >
)}
>
)}
diff --git a/src/routes/PositionDetails/components/SelectCandidatePopup/styles.module.scss b/src/routes/PositionDetails/components/SelectCandidatePopup/styles.module.scss
index af224c4b..d05d1184 100644
--- a/src/routes/PositionDetails/components/SelectCandidatePopup/styles.module.scss
+++ b/src/routes/PositionDetails/components/SelectCandidatePopup/styles.module.scss
@@ -1,3 +1,11 @@
+ol {
+ list-style-type: decimal;
+ padding: 25px;
+ li {
+ margin-top: 5px;
+ }
+}
+
.user {
font-size: 14px;
color: #0D61BF;
diff --git a/src/routes/PositionDetails/reducers/index.js b/src/routes/PositionDetails/reducers/index.js
index 16289269..8a46a7ac 100644
--- a/src/routes/PositionDetails/reducers/index.js
+++ b/src/routes/PositionDetails/reducers/index.js
@@ -59,9 +59,10 @@ const patchInterviewInState = (state, candidateId, interviewData) => {
return state;
}
+ const hasInterviews = !!state.position.candidates[candidateIndex].interviews;
const updatedCandidate = update(state.position.candidates[candidateIndex], {
status: { $set: "interview" },
- interviews: { $push: [interviewData] },
+ interviews: { [hasInterviews ? "$push" : "$set"]: [interviewData] },
});
return update(state, {
@@ -121,11 +122,10 @@ const reducer = (state = initialState, action) => {
});
case ACTION_TYPE.ADD_INTERVIEW_PENDING:
- return {
- ...state,
- loading: true,
- error: undefined,
- };
+ return update(state, {
+ loading: { $set: true },
+ error: { $set: undefined },
+ });
case ACTION_TYPE.ADD_INTERVIEW_SUCCESS:
return patchInterviewInState(state, action.meta.candidateId, {
@@ -133,11 +133,10 @@ const reducer = (state = initialState, action) => {
});
case ACTION_TYPE.ADD_INTERVIEW_ERROR:
- return {
- ...state,
- loading: false,
- error: action.payload,
- };
+ return update(state, {
+ loading: { $set: false },
+ error: { $set: action.payload },
+ });
default:
return state;
}
From 05d7f496be4f4c5a324fad4a200a386a0a6f41ed Mon Sep 17 00:00:00 2001
From: Cagdas U
Date: Fri, 30 Apr 2021 20:57:57 +0300
Subject: [PATCH 17/20] fix(interview-scheduler): remove externalId assignment
to userId
Addresses https://github.com/topcoder-platform/taas-app/issues/179
---
src/routes/JobForm/index.jsx | 2 --
1 file changed, 2 deletions(-)
diff --git a/src/routes/JobForm/index.jsx b/src/routes/JobForm/index.jsx
index 880f22e2..8f34d31f 100644
--- a/src/routes/JobForm/index.jsx
+++ b/src/routes/JobForm/index.jsx
@@ -64,8 +64,6 @@ const JobForm = ({ teamId, jobId }) => {
// as we are using `PUT` method (not `PATCH`) we have send ALL the fields
// fields which we don't send would become `null` otherwise
const getRequestData = (values) => {
- const externalId = _.get(store.getState(), "authUser.userId");
- values.externalId = externalId && _.toString(externalId);
return _.pick(values, [
"projectId",
"externalId",
From f5c65347475b10dc60cba331ae052ce7f193af0f Mon Sep 17 00:00:00 2001
From: Cagdas U
Date: Sat, 1 May 2021 04:36:41 +0300
Subject: [PATCH 18/20] feat(interview-scheduler): add cap on round & Selected
tab features
1. Add a new tab "Selected" for candidates with `selected` status.
2. Add a limit on max. allowed number of interview rounds. In case of exceeding,
a popup with a message will be shown. [1]
3. Fix the horizontal scrollbar issue on the Schedule Interview popup.
Addresses:
[1]: https://github.com/topcoder-platform/taas-app/issues/174
---
src/components/BaseModal/index.jsx | 1 +
src/constants/index.js | 11 ++++++++-
.../InterviewDetailsPopup/index.jsx | 23 ++++++++++++++++++-
.../InterviewDetailsPopup/styles.module.scss | 13 +++++++++++
4 files changed, 46 insertions(+), 2 deletions(-)
diff --git a/src/components/BaseModal/index.jsx b/src/components/BaseModal/index.jsx
index e0bf5a83..91950b2f 100644
--- a/src/components/BaseModal/index.jsx
+++ b/src/components/BaseModal/index.jsx
@@ -21,6 +21,7 @@ const modalStyle = {
maxWidth: "640px",
width: "100%",
margin: 0,
+ "overflow-x": "hidden",
};
const containerStyle = {
diff --git a/src/constants/index.js b/src/constants/index.js
index 7cd2c580..fa899de3 100644
--- a/src/constants/index.js
+++ b/src/constants/index.js
@@ -111,6 +111,7 @@ export const CANDIDATE_STATUS = {
export const CANDIDATE_STATUS_FILTER_KEY = {
TO_REVIEW: "TO_REVIEW",
INTERESTED: "INTERESTED",
+ SELECTED: "SELECTED",
NOT_INTERESTED: "NOT_INTERESTED",
};
@@ -128,7 +129,13 @@ export const CANDIDATE_STATUS_FILTERS = [
key: CANDIDATE_STATUS_FILTER_KEY.INTERESTED,
buttonText: "Interviews",
title: "Interviews",
- statuses: [CANDIDATE_STATUS.SELECTED, CANDIDATE_STATUS.INTERVIEW],
+ statuses: [CANDIDATE_STATUS.INTERVIEW],
+ },
+ {
+ key: CANDIDATE_STATUS_FILTER_KEY.SELECTED,
+ buttonText: "Selected",
+ title: "Selected",
+ statuses: [CANDIDATE_STATUS.SELECTED],
},
{
key: CANDIDATE_STATUS_FILTER_KEY.NOT_INTERESTED,
@@ -320,3 +327,5 @@ export const DISABLED_DESCRIPTION_MESSAGE =
*/
export const INTERVIEW_POPUP_MEDIA_URL =
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
+
+export const MAX_ALLOWED_INTERVIEWS = 3;
diff --git a/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx b/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
index e7a6d363..4a15c2c1 100644
--- a/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
+++ b/src/routes/PositionDetails/components/InterviewDetailsPopup/index.jsx
@@ -16,7 +16,7 @@ import User from "components/User";
import BaseModal from "components/BaseModal";
import FormField from "components/FormField";
import Button from "components/Button";
-import { FORM_FIELD_TYPE } from "constants";
+import { FORM_FIELD_TYPE, MAX_ALLOWED_INTERVIEWS } from "constants";
import "./styles.module.scss";
import RadioFieldGroup from "components/RadioFieldGroup";
@@ -76,6 +76,23 @@ function InterviewDetailsPopup({ open, onClose, candidate, openNext }) {
[dispatch, candidate]
);
+ // show the warning if exceeds MAX_ALLOWED_INTERVIEW
+ if (
+ candidate &&
+ candidate.interviews &&
+ candidate.interviews.length >= MAX_ALLOWED_INTERVIEWS
+ ) {
+ return (
+
+
+ You've reached the cap of {MAX_ALLOWED_INTERVIEWS} interviews with
+ this candidate. Now please make your decision to Select and Decline
+ them.
+
+
+ );
+ }
+
return isLoading ? null : (
)}
+
+ You may have as many as {MAX_ALLOWED_INTERVIEWS} interviews
+ with each candidate for the job.
+
Date: Sat, 1 May 2021 07:57:22 +0200
Subject: [PATCH 19/20] Remove video from Interview Confirmation popup
---
.../components/InterviewConfirmPopup/index.jsx | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/src/routes/PositionDetails/components/InterviewConfirmPopup/index.jsx b/src/routes/PositionDetails/components/InterviewConfirmPopup/index.jsx
index c2b6ac79..d5efeef5 100644
--- a/src/routes/PositionDetails/components/InterviewConfirmPopup/index.jsx
+++ b/src/routes/PositionDetails/components/InterviewConfirmPopup/index.jsx
@@ -37,11 +37,13 @@ function InterviewConfirmPopup({ open, onClose }) {
If you have any issues with scheduling, please contact
talent@topcoder.com .
-
+/* Commenting out the video UI control for now as there is no appropriate video clip to be embedded
+ *
+*/
);
From e363f215e213017f89ecd0afc24b598a6b161929 Mon Sep 17 00:00:00 2001
From: Nikolay Iakovlev
Date: Sat, 1 May 2021 08:10:19 +0200
Subject: [PATCH 20/20] Remove embedded video
As per https://github.com/topcoder-platform/taas-app/issues/180
---
.../components/InterviewConfirmPopup/index.jsx | 7 -------
1 file changed, 7 deletions(-)
diff --git a/src/routes/PositionDetails/components/InterviewConfirmPopup/index.jsx b/src/routes/PositionDetails/components/InterviewConfirmPopup/index.jsx
index d5efeef5..f5f03d8c 100644
--- a/src/routes/PositionDetails/components/InterviewConfirmPopup/index.jsx
+++ b/src/routes/PositionDetails/components/InterviewConfirmPopup/index.jsx
@@ -37,13 +37,6 @@ function InterviewConfirmPopup({ open, onClose }) {
If you have any issues with scheduling, please contact
talent@topcoder.com .
-/* Commenting out the video UI control for now as there is no appropriate video clip to be embedded
- *
-*/
);