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) && (
- - + { + 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) && ( +
+ + +
+ )}
))} 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) && (
- + {candidate.interviews.length > 0 && ( + + )}
)}
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 ( +
+ + {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}
+
+ +
+
+); + +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) => ( -
-
- -
-
- - {candidate.resume && ( - - - Download Resume - - )} -
- {statusFilterKey === CANDIDATE_STATUS_FILTER_KEY.INTERESTED && ( -
- + {filteredCandidates.length === 0 && ( +
No {statusFilter.title}
+ )} + {filteredCandidates.length > 0 && ( +
+ {pageCandidates.map((candidate) => ( +
+
+ +
+
+ + {candidate.resume && ( + + + Download Resume + + )}
- )} -
- {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) && ( -
- - {candidate.interviews.length > 0 && ( - - )} -
- )} + ]} + /> +
+ )} + {statusFilterKey === CANDIDATE_STATUS_FILTER_KEY.INTERESTED && + hasPermission(PERMISSIONS.UPDATE_JOB_CANDIDATE) && ( +
+ + {candidate.interviews.length > 0 && ( + + )} +
+ )} +
-
- ))} -
- )} - -
- - {filteredCandidates.length > 0 && ( - + ))} +
)} + +
+ + {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 ( -
- + } + > +
+
+
+ {candidate === null ? ( + "" + ) : ( + + )} +
+
+ + +
+
+
+

Attendees:

+

+ Please provide email addresses for all parties you would like + involved with the interview. +

+ + +
+
+

+ Selecting “Begin Scheduling” will initiate emails to all + attendees to coordinate availabiltiy. Please check your email to + input your availability. +

+
+
+ + )} + + ); } 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 : (
+ {({ + handleSubmit, + form: { + mutators: { push }, + }, + }) => ( + + Begin scheduling + + } + > +
+
+
+ {candidate === null ? ( + "" + ) : ( + + )} +
+ +
+
+

Attendees:

+

+ Please provide email addresses for all parties you would like + involved with the interview. +

+ + + + + {({ fields }) => { + return fields.map((name, index) => ( +
+
+ +
+ fields.remove(index)} + styleName="remove-item" + > + × + +
+ )); + }} +
+
+
+

+ Selecting “Begin Scheduling” will initiate emails to all + attendees to coordinate availabiltiy. Please check your email to + input your availability. +

+
+
+
+ )} +
+ /*
)} -
+ */ ); } 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) && (
- + {candidate.interviews.length > 0 && (