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

Implemented functionality to wait & sync host's calendar before requesting interview. #594

Merged
merged 3 commits into from Dec 20, 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
5 changes: 5 additions & 0 deletions src/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@ export const POPUP_STAGES = {
SELECT_DURATION: "selectDuration",
SEND_INTERVIEW_INVITE: "sendInterviewInvite",
SUCCESS: "sucess",
CALENDAR_SYNC_TIMED_OUT: "calendarSyncTimedOut",
CLOSE: "close",
};

Expand Down Expand Up @@ -478,6 +479,10 @@ export const INTERVIEW_POPUP_STAGES = [
id: POPUP_STAGES.SUCCESS,
title: "Success! Interview invite sent",
},
{
id: POPUP_STAGES.CALENDAR_SYNC_TIMED_OUT,
title: "Send Interview Invite",
},
];

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from "react";
import PT from "prop-types";
import Button from "components/Button";
import { BUTTON_SIZE, BUTTON_TYPE, POPUP_STAGES } from "constants";
import "./styles.module.scss";
import { getPrimaryCalendar } from "utils/helpers";

/**
* The CalendarSyncTimedOut component showing error message about calendar sync failure
*/
const CalendarSyncTimedOut = ({ userSettings, onContinue }) => {
const primaryCalendar = getPrimaryCalendar(userSettings);

return (
<div styleName="timedout-wrapper">
<div styleName="timedout-text">
<p>Strange, it takes too long to sync your calendar {primaryCalendar.email}.</p>
<p>
Please, try re-connecting your calendar by clicking "
<span
onClick={() => onContinue(POPUP_STAGES.MANAGE_CALENDAR)}
styleName="manage-calendar"
>
manage connected calendars</span>". Would you have any issues, please don't hesitate to reach out to us
by <a styleName="topcoder-support-email-link" href="mailto:[email protected]">[email protected]</a>.
</p>
</div>
<div styleName="button-wrapper">
<Button
onClick={() => onContinue(POPUP_STAGES.SEND_INTERVIEW_INVITE)}
size={BUTTON_SIZE.MEDIUM}
type={BUTTON_TYPE.PRIMARY}
>
OK
</Button>
</div>
</div>);
}

CalendarSyncTimedOut.propTypes = {
userSettings: PT.object,
};

export default CalendarSyncTimedOut;
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
@import "styles/include";

.timedout-wrapper {
}

.timedout-text {
margin-top: 70px;
margin-bottom: 232px;
@include font-roboto;
font-size: 16px;
line-height: 26px;
font-weight: 400;
}

.button-wrapper {
display: flex;
align-items: center;
margin-top: 16px;
justify-content: flex-end;
}

.confirm-text-bold {
font-weight: bold;
}

.manage-calendar {
color: #0d61bf;
@include font-roboto;
font-weight: 400;
font-size: 14px;
cursor: pointer;
text-decoration: underline;
}

.topcoder-support-email-link {
color: #0d61bf !important;
@include font-roboto;
font-weight: 400;
font-size: 14px;
cursor: pointer;
text-decoration: underline;
}
148 changes: 119 additions & 29 deletions src/routes/PositionDetails/components/InterviewPopup/Confirm/index.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import React from "react";
import React, { useState, useEffect } from "react";
import PT from "prop-types";
import cn from "classnames";
import { useDispatch } from "react-redux";
import { useParams } from "@reach/router";
import { loadPosition } from "../../../actions";
import Button from "components/Button";
import { toastr } from "react-redux-toastr";

import StepsIndicator from "../../StepsIndicator";
import Spinner from "components/CenteredSpinner";
import { confirmInterview } from "../../../../../services/interviews";
import {
SCHEDULE_INTERVIEW_STEPS,
Expand All @@ -15,6 +17,7 @@ import {
POPUP_STAGES,
} from "constants";
import "./styles.module.scss";
import { getPrimaryCalendar, isCalendarInSync } from "utils/helpers";

/**
* This component is used to get the confirmation before scheduling the interview
Expand All @@ -25,23 +28,76 @@ const Confirm = ({
onGoBack,
onContinue,
onShowingLoader,
onSetLoadingMessage,
userSettings,
getSettingsModular,
}) => {
const { teamId, positionId } = useParams();
const [loadingMessage, setLoadingMessage] = useState(null);
const [syncCalendarTimeoutId, setSyncCalendarTimeoutId] = useState(null);
// flag indicating the 1st attempt to refetch UserMeetingSettings is made, must repeat requests every 10secs
const [initiateInterviewConfirmation, setInitiateInterviewConfirmation] = useState(null);
// each timeout equals 10 seconds - so 120 seconds = 2mins = 12 timeouts - this excludes time taken for request response
const [totalSyncTimeouts, setTotalSyncTimeouts] = useState(0);
const dispatch = useDispatch();
const { handle, id: candidateId } = candidate;
const { duration } = scheduleDetails;

// check if primary calendar is syncing or synced and handle accordingly
useEffect(() => {
const primaryCalendar = getPrimaryCalendar(userSettings);
const calendarSynced = primaryCalendar ? isCalendarInSync(primaryCalendar) : false;

// to know if calendar is ready and synced to request interview, we check for primary calendar
// & its sync status, combined with either the timeoutid or initiate confirmation flag
if ((primaryCalendar && calendarSynced) && (syncCalendarTimeoutId || initiateInterviewConfirmation))
{
// clear timeout since calendar is now synced
clearTimeout(syncCalendarTimeoutId);

// reset state
setSyncCalendarTimeoutId(null);
setInitiateInterviewConfirmation(false);

// continue with interview scheduling
onContinueAhead(true);
}
else if ((primaryCalendar && !calendarSynced) && (syncCalendarTimeoutId || initiateInterviewConfirmation))
{
if (totalSyncTimeouts > 12)
{
onContinue(POPUP_STAGES.CALENDAR_SYNC_TIMED_OUT);
}
else
{
// since primary calendar is still not synced, reset timeout
setSyncCalendarTimeoutId(setTimeout(() => getSettingsModular(), 10000));
setTotalSyncTimeouts(totalSyncTimeouts + 1);
}
}

// clear timeout on component unmount
return () => clearTimeout(syncCalendarTimeoutId);
}, [userSettings]);

const onSetLoadingMessageLocal = (text) => setLoadingMessage(text);

/**
* This will trigger the API call to the server to request an interview
*/
const onContinueAhead = () => {
const onContinueAhead = (showSyncCompletedMessage) => {
const params = {
hostTimezone: scheduleDetails.timezone,
duration: scheduleDetails.duration,
availableTime: scheduleDetails.slots,
};
onSetLoadingMessageLocal(null);
onShowingLoader(true);

// sync completed, let the user know scheduling is underway
if (showSyncCompletedMessage)
onSetLoadingMessage("Scheduling interview...");

confirmInterview(candidateId, params)
.then(() => dispatch(loadPosition(teamId, positionId)))
.then(() => onContinue(POPUP_STAGES.SUCCESS))
Expand All @@ -53,34 +109,65 @@ const Confirm = ({
});
};

const inviteInterviewCandidate = (userSettingsParam) => {
let primaryCalendar = getPrimaryCalendar(userSettingsParam);
let calendarSynced = isCalendarInSync(primaryCalendar);

if (primaryCalendar && !calendarSynced)
{
// show loading indicator with message
onSetLoadingMessageLocal(`Syncing your new calendar ${primaryCalendar.email}, it might take a few minutes...`);

// fetch UserMeetingSettings in the background, for the 1st time, no timeout is necessary,
// so we set initiateInterviewConfirmation value to true
getSettingsModular();
setInitiateInterviewConfirmation(true);
}
else if (primaryCalendar && calendarSynced)
{
onContinueAhead();
}
}

return (
<div styleName="confirm-wrapper">
<StepsIndicator steps={SCHEDULE_INTERVIEW_STEPS} currentStep="confirm" />
<div styleName="confirm-text">
Send a <span styleName="confirm-text-bold">{duration} Minute</span>{" "}
Interview invite to <span styleName="confirm-text-bold">{handle}</span>.
This invite will allow <span styleName="confirm-text-bold">{handle}</span> to select and schedule an interview date
and time based on your availability.
</div>

<div styleName="button-wrapper">
<Button
styleName="back-button"
onClick={() => onGoBack()}
size={BUTTON_SIZE.MEDIUM}
type={BUTTON_TYPE.SECONDARY}
>
Back
</Button>
<Button
onClick={() => onContinueAhead()}
size={BUTTON_SIZE.MEDIUM}
type={BUTTON_TYPE.PRIMARY}
>
Confirm
</Button>
</div>
</div>
<>
{loadingMessage && <div
styleName={cn("spinner-wrapper", {
"show-spinner": loadingMessage,
})}
>
<Spinner stype="Oval" width={80} height={80} />
<p styleName="loading-message">{loadingMessage}</p>
</div>}
{!loadingMessage && <div styleName="confirm-wrapper">
<StepsIndicator steps={SCHEDULE_INTERVIEW_STEPS} currentStep="confirm" />
<div styleName="confirm-text">
Send a <span styleName="confirm-text-bold">{duration} Minute</span>{" "}
Interview invite to <span styleName="confirm-text-bold">{handle}</span>.
This invite will allow <span styleName="confirm-text-bold">{handle}</span> to select and schedule an interview date
and time based on your availability.
</div>

<div styleName="button-wrapper">
<Button
styleName="back-button"
onClick={() => onGoBack()}
size={BUTTON_SIZE.MEDIUM}
type={BUTTON_TYPE.SECONDARY}
>
Back
</Button>
<Button
onClick={() => inviteInterviewCandidate(userSettings)}
disabled={initiateInterviewConfirmation}
size={BUTTON_SIZE.MEDIUM}
type={BUTTON_TYPE.PRIMARY}
>
Confirm
</Button>
</div>
</div>}
</>
);
};

Expand All @@ -90,6 +177,9 @@ Confirm.propTypes = {
onGoBack: PT.func,
onContinue: PT.func,
onShowingLoader: PT.func,
onSetLoadingMessage: PT.func,
userSettings: PT.object,
getSettingsModular: PT.func,
};

export default Confirm;
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
@import "styles/include";

.spinner-wrapper {
padding-top: 103px;
display: none;
}

.show-spinner {
display: block;
}

.loading-message {
margin-top: 10px;
}

.confirm-wrapper {
}

Expand Down
Loading