Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

fix(interview-scheduler): various fixes & improvements #178

Merged
merged 1 commit into from
Apr 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/components/ActionsMenu/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
</div>
Expand Down
19 changes: 16 additions & 3 deletions src/components/ActionsMenu/styles.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
17 changes: 12 additions & 5 deletions src/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
};
Expand Down Expand Up @@ -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,
],
},
];

Expand Down Expand Up @@ -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" },
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 =
Expand All @@ -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 : (
<Form
initialValues={{
time: "interview-30",
emails: [myEmail],
}}
onSubmit={onSubmitCallback}
mutators={{
Expand Down Expand Up @@ -97,7 +115,7 @@ function InterviewDetailsPopup({ open, onClose, candidate, openNext }) {
}}
size="medium"
isSubmit
disabled={submitting || hasValidationErrors}
disabled={submitting || hasValidationErrors || loading}
>
Begin scheduling
</Button>
Expand Down Expand Up @@ -154,6 +172,7 @@ function InterviewDetailsPopup({ open, onClose, candidate, openNext }) {
label: "Email Address",
maxLength: 320,
customValidator: true,
disabled: index === 0,
}}
/>
</div>
Expand Down
70 changes: 48 additions & 22 deletions src/routes/PositionDetails/components/PositionCandidates/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand All @@ -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.");
Expand Down Expand Up @@ -259,6 +263,7 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
action: () => {
openSelectCandidatePopup(candidate, true);
},
style: "danger",
},
]}
/>
Expand All @@ -267,20 +272,41 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
{statusFilterKey === CANDIDATE_STATUS_FILTER_KEY.INTERESTED &&
hasPermission(PERMISSIONS.UPDATE_JOB_CANDIDATE) && (
<div styleName="actions">
<Button
onClick={() => openInterviewDetailsPopup(candidate)}
>
Schedule Another Interview
</Button>
{candidate.interviews &&
candidate.interviews.length > 0 && (
<Button
type="secondary"
onClick={() => openPrevInterviewsPopup(candidate)}
>
View Previous Interviews
</Button>
)}
<ActionsMenu
options={[
{
label: "Schedule Another Interview",
action: () => {
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",
},
]}
/>
</div>
)}
</div>
Expand Down Expand Up @@ -329,7 +355,7 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => {
candidate={selectedCandidate}
open={selectCandidateOpen}
isReject={isReject}
shortList={markCandidateShortlisted}
select={markCandidateSelected}
reject={markCandidateRejected}
closeModal={() => setSelectCandidateOpen(false)}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<BaseModal
Expand Down Expand Up @@ -65,12 +65,29 @@ const SelectCandidatePopup = ({
</div>
{isLoading ? (
<CenteredSpinner />
) : isReject ? (
<p>Are you sure you want to decline the selected candidate?</p>
) : (
<p>
{isReject
? "Are you sure you want to decline the selected candidate?"
: "Please confirm your selection of the above candidate"}
</p>
<>
<p>
You have selected this applicant - you want this member on your
team! What happens next:
</p>
<ol>
<li>
Upon confirmation, Topcoder will confirm the arrangement with
the selected member
</li>
<li>
A Topcoder Rep will contact you with details on the work
arrangement
</li>
<li>
When both sides accept, we will finalize the agreement and
begin onboarding
</li>
</ol>
</>
)}
</>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
ol {
list-style-type: decimal;
padding: 25px;
li {
margin-top: 5px;
}
}

.user {
font-size: 14px;
color: #0D61BF;
Expand Down
21 changes: 10 additions & 11 deletions src/routes/PositionDetails/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down Expand Up @@ -121,23 +122,21 @@ 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, {
...action.payload,
});

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;
}
Expand Down