Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit b757d12

Browse files
authoredDec 20, 2021
Merge pull request #594 from mahidulalvi-bonic/dev
Implemented functionality to wait & sync host's calendar before requesting interview.
2 parents 8961b3c + 8d83101 commit b757d12

File tree

9 files changed

+279
-30
lines changed

9 files changed

+279
-30
lines changed
 

‎src/constants/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,7 @@ export const POPUP_STAGES = {
440440
SELECT_DURATION: "selectDuration",
441441
SEND_INTERVIEW_INVITE: "sendInterviewInvite",
442442
SUCCESS: "sucess",
443+
CALENDAR_SYNC_TIMED_OUT: "calendarSyncTimedOut",
443444
CLOSE: "close",
444445
};
445446

@@ -478,6 +479,10 @@ export const INTERVIEW_POPUP_STAGES = [
478479
id: POPUP_STAGES.SUCCESS,
479480
title: "Success! Interview invite sent",
480481
},
482+
{
483+
id: POPUP_STAGES.CALENDAR_SYNC_TIMED_OUT,
484+
title: "Send Interview Invite",
485+
},
481486
];
482487

483488
/**
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React from "react";
2+
import PT from "prop-types";
3+
import Button from "components/Button";
4+
import { BUTTON_SIZE, BUTTON_TYPE, POPUP_STAGES } from "constants";
5+
import "./styles.module.scss";
6+
import { getPrimaryCalendar } from "utils/helpers";
7+
8+
/**
9+
* The CalendarSyncTimedOut component showing error message about calendar sync failure
10+
*/
11+
const CalendarSyncTimedOut = ({ userSettings, onContinue }) => {
12+
const primaryCalendar = getPrimaryCalendar(userSettings);
13+
14+
return (
15+
<div styleName="timedout-wrapper">
16+
<div styleName="timedout-text">
17+
<p>Strange, it takes too long to sync your calendar {primaryCalendar.email}.</p>
18+
<p>
19+
Please, try re-connecting your calendar by clicking "
20+
<span
21+
onClick={() => onContinue(POPUP_STAGES.MANAGE_CALENDAR)}
22+
styleName="manage-calendar"
23+
>
24+
manage connected calendars</span>". Would you have any issues, please don't hesitate to reach out to us
25+
by <a styleName="topcoder-support-email-link" href="mailto:support@topcoder.com">support@topcoder.com</a>.
26+
</p>
27+
</div>
28+
<div styleName="button-wrapper">
29+
<Button
30+
onClick={() => onContinue(POPUP_STAGES.SEND_INTERVIEW_INVITE)}
31+
size={BUTTON_SIZE.MEDIUM}
32+
type={BUTTON_TYPE.PRIMARY}
33+
>
34+
OK
35+
</Button>
36+
</div>
37+
</div>);
38+
}
39+
40+
CalendarSyncTimedOut.propTypes = {
41+
userSettings: PT.object,
42+
};
43+
44+
export default CalendarSyncTimedOut;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
@import "styles/include";
2+
3+
.timedout-wrapper {
4+
}
5+
6+
.timedout-text {
7+
margin-top: 70px;
8+
margin-bottom: 232px;
9+
@include font-roboto;
10+
font-size: 16px;
11+
line-height: 26px;
12+
font-weight: 400;
13+
}
14+
15+
.button-wrapper {
16+
display: flex;
17+
align-items: center;
18+
margin-top: 16px;
19+
justify-content: flex-end;
20+
}
21+
22+
.confirm-text-bold {
23+
font-weight: bold;
24+
}
25+
26+
.manage-calendar {
27+
color: #0d61bf;
28+
@include font-roboto;
29+
font-weight: 400;
30+
font-size: 14px;
31+
cursor: pointer;
32+
text-decoration: underline;
33+
}
34+
35+
.topcoder-support-email-link {
36+
color: #0d61bf !important;
37+
@include font-roboto;
38+
font-weight: 400;
39+
font-size: 14px;
40+
cursor: pointer;
41+
text-decoration: underline;
42+
}
Lines changed: 119 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import React from "react";
1+
import React, { useState, useEffect } from "react";
22
import PT from "prop-types";
3+
import cn from "classnames";
34
import { useDispatch } from "react-redux";
45
import { useParams } from "@reach/router";
56
import { loadPosition } from "../../../actions";
67
import Button from "components/Button";
78
import { toastr } from "react-redux-toastr";
89

910
import StepsIndicator from "../../StepsIndicator";
11+
import Spinner from "components/CenteredSpinner";
1012
import { confirmInterview } from "../../../../../services/interviews";
1113
import {
1214
SCHEDULE_INTERVIEW_STEPS,
@@ -15,6 +17,7 @@ import {
1517
POPUP_STAGES,
1618
} from "constants";
1719
import "./styles.module.scss";
20+
import { getPrimaryCalendar, isCalendarInSync } from "utils/helpers";
1821

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

46+
// check if primary calendar is syncing or synced and handle accordingly
47+
useEffect(() => {
48+
const primaryCalendar = getPrimaryCalendar(userSettings);
49+
const calendarSynced = primaryCalendar ? isCalendarInSync(primaryCalendar) : false;
50+
51+
// to know if calendar is ready and synced to request interview, we check for primary calendar
52+
// & its sync status, combined with either the timeoutid or initiate confirmation flag
53+
if ((primaryCalendar && calendarSynced) && (syncCalendarTimeoutId || initiateInterviewConfirmation))
54+
{
55+
// clear timeout since calendar is now synced
56+
clearTimeout(syncCalendarTimeoutId);
57+
58+
// reset state
59+
setSyncCalendarTimeoutId(null);
60+
setInitiateInterviewConfirmation(false);
61+
62+
// continue with interview scheduling
63+
onContinueAhead(true);
64+
}
65+
else if ((primaryCalendar && !calendarSynced) && (syncCalendarTimeoutId || initiateInterviewConfirmation))
66+
{
67+
if (totalSyncTimeouts > 12)
68+
{
69+
onContinue(POPUP_STAGES.CALENDAR_SYNC_TIMED_OUT);
70+
}
71+
else
72+
{
73+
// since primary calendar is still not synced, reset timeout
74+
setSyncCalendarTimeoutId(setTimeout(() => getSettingsModular(), 10000));
75+
setTotalSyncTimeouts(totalSyncTimeouts + 1);
76+
}
77+
}
78+
79+
// clear timeout on component unmount
80+
return () => clearTimeout(syncCalendarTimeoutId);
81+
}, [userSettings]);
82+
83+
const onSetLoadingMessageLocal = (text) => setLoadingMessage(text);
84+
3485
/**
3586
* This will trigger the API call to the server to request an interview
3687
*/
37-
const onContinueAhead = () => {
88+
const onContinueAhead = (showSyncCompletedMessage) => {
3889
const params = {
3990
hostTimezone: scheduleDetails.timezone,
4091
duration: scheduleDetails.duration,
4192
availableTime: scheduleDetails.slots,
4293
};
94+
onSetLoadingMessageLocal(null);
4395
onShowingLoader(true);
4496

97+
// sync completed, let the user know scheduling is underway
98+
if (showSyncCompletedMessage)
99+
onSetLoadingMessage("Scheduling interview...");
100+
45101
confirmInterview(candidateId, params)
46102
.then(() => dispatch(loadPosition(teamId, positionId)))
47103
.then(() => onContinue(POPUP_STAGES.SUCCESS))
@@ -53,34 +109,65 @@ const Confirm = ({
53109
});
54110
};
55111

112+
const inviteInterviewCandidate = (userSettingsParam) => {
113+
let primaryCalendar = getPrimaryCalendar(userSettingsParam);
114+
let calendarSynced = isCalendarInSync(primaryCalendar);
115+
116+
if (primaryCalendar && !calendarSynced)
117+
{
118+
// show loading indicator with message
119+
onSetLoadingMessageLocal(`Syncing your new calendar ${primaryCalendar.email}, it might take a few minutes...`);
120+
121+
// fetch UserMeetingSettings in the background, for the 1st time, no timeout is necessary,
122+
// so we set initiateInterviewConfirmation value to true
123+
getSettingsModular();
124+
setInitiateInterviewConfirmation(true);
125+
}
126+
else if (primaryCalendar && calendarSynced)
127+
{
128+
onContinueAhead();
129+
}
130+
}
131+
56132
return (
57-
<div styleName="confirm-wrapper">
58-
<StepsIndicator steps={SCHEDULE_INTERVIEW_STEPS} currentStep="confirm" />
59-
<div styleName="confirm-text">
60-
Send a <span styleName="confirm-text-bold">{duration} Minute</span>{" "}
61-
Interview invite to <span styleName="confirm-text-bold">{handle}</span>.
62-
This invite will allow <span styleName="confirm-text-bold">{handle}</span> to select and schedule an interview date
63-
and time based on your availability.
64-
</div>
65-
66-
<div styleName="button-wrapper">
67-
<Button
68-
styleName="back-button"
69-
onClick={() => onGoBack()}
70-
size={BUTTON_SIZE.MEDIUM}
71-
type={BUTTON_TYPE.SECONDARY}
72-
>
73-
Back
74-
</Button>
75-
<Button
76-
onClick={() => onContinueAhead()}
77-
size={BUTTON_SIZE.MEDIUM}
78-
type={BUTTON_TYPE.PRIMARY}
79-
>
80-
Confirm
81-
</Button>
82-
</div>
83-
</div>
133+
<>
134+
{loadingMessage && <div
135+
styleName={cn("spinner-wrapper", {
136+
"show-spinner": loadingMessage,
137+
})}
138+
>
139+
<Spinner stype="Oval" width={80} height={80} />
140+
<p styleName="loading-message">{loadingMessage}</p>
141+
</div>}
142+
{!loadingMessage && <div styleName="confirm-wrapper">
143+
<StepsIndicator steps={SCHEDULE_INTERVIEW_STEPS} currentStep="confirm" />
144+
<div styleName="confirm-text">
145+
Send a <span styleName="confirm-text-bold">{duration} Minute</span>{" "}
146+
Interview invite to <span styleName="confirm-text-bold">{handle}</span>.
147+
This invite will allow <span styleName="confirm-text-bold">{handle}</span> to select and schedule an interview date
148+
and time based on your availability.
149+
</div>
150+
151+
<div styleName="button-wrapper">
152+
<Button
153+
styleName="back-button"
154+
onClick={() => onGoBack()}
155+
size={BUTTON_SIZE.MEDIUM}
156+
type={BUTTON_TYPE.SECONDARY}
157+
>
158+
Back
159+
</Button>
160+
<Button
161+
onClick={() => inviteInterviewCandidate(userSettings)}
162+
disabled={initiateInterviewConfirmation}
163+
size={BUTTON_SIZE.MEDIUM}
164+
type={BUTTON_TYPE.PRIMARY}
165+
>
166+
Confirm
167+
</Button>
168+
</div>
169+
</div>}
170+
</>
84171
);
85172
};
86173

@@ -90,6 +177,9 @@ Confirm.propTypes = {
90177
onGoBack: PT.func,
91178
onContinue: PT.func,
92179
onShowingLoader: PT.func,
180+
onSetLoadingMessage: PT.func,
181+
userSettings: PT.object,
182+
getSettingsModular: PT.func,
93183
};
94184

95185
export default Confirm;

‎src/routes/PositionDetails/components/InterviewPopup/Confirm/styles.module.scss

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
@import "styles/include";
22

3+
.spinner-wrapper {
4+
padding-top: 103px;
5+
display: none;
6+
}
7+
8+
.show-spinner {
9+
display: block;
10+
}
11+
12+
.loading-message {
13+
margin-top: 10px;
14+
}
15+
316
.confirm-wrapper {
417
}
518

‎src/routes/PositionDetails/components/InterviewPopup/index.jsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import ConnectCalendar from "./ConnectCalendar";
1212
import SelectDuration from "./SelectDuration";
1313
import Confirm from "./Confirm";
1414
import Success from "./Success";
15+
import CalendarSyncTimedOut from "./CalendarSyncTimedOut";
1516

1617
import { getUserSettings } from "../../../../services/interviews";
1718
import IconCrossBlack from "../../../../assets/images/icon-cross-black.svg";
@@ -35,6 +36,7 @@ const InterviewPopup = ({
3536
const [stage, setStage] = useState(initialStage);
3637
const [previousStage, setPreviousStage] = useState(initialStage);
3738
const [isLoading, setLoading] = useState(false);
39+
const [loadingMessage, setLoadingMessage] = useState(null);
3840
const [userSettings, setUserSettings] = useState();
3941
const { v5UserProfile } = useSelector((state) => state.authUser);
4042
const [scheduleDetails, setScheduleDetails] = useState({
@@ -48,6 +50,12 @@ const InterviewPopup = ({
4850
}
4951
}, [open]);
5052

53+
// if loading finished, reset value of loadingMessage
54+
useEffect(() => {
55+
if (!isLoading)
56+
setLoadingMessage(null);
57+
}, [isLoading])
58+
5159
const onCloseInterviewPopup = () => {
5260
setStage("");
5361
setScheduleDetails({
@@ -62,6 +70,21 @@ const InterviewPopup = ({
6270
slots: userSettings.defaultAvailableTime || []
6371
});
6472

73+
const getSettingsModular = () => {
74+
getUserSettings(v5UserProfile.id)
75+
.then((res) => {
76+
setUserSettings(res.data);
77+
})
78+
.catch((e) => {
79+
if (e.response && e.response.status === 404) {
80+
setStage(POPUP_STAGES.SCHEDULE_INTERVIEW);
81+
} else {
82+
toastr.error("Failed to get user settings");
83+
onCloseInterviewPopup();
84+
}
85+
})
86+
}
87+
6588
/**
6689
* Gets the settings from the backend and checks if the calendar is already available
6790
*/
@@ -153,6 +176,8 @@ const InterviewPopup = ({
153176
setLoading(loading);
154177
};
155178

179+
const onSetLoadingMessage = (text) => setLoadingMessage(text);
180+
156181
/**
157182
* Removes the calendar from the state once it is removed from the server
158183
*/
@@ -213,12 +238,19 @@ const InterviewPopup = ({
213238
onGoBack={onGoingBack}
214239
onShowingLoader={onShowingLoader}
215240
candidate={candidate}
241+
userSettings={userSettings}
242+
getSettingsModular={getSettingsModular}
243+
onSetLoadingMessage={onSetLoadingMessage}
216244
/>
217245
);
218246
case POPUP_STAGES.SUCCESS:
219247
return (
220248
<Success candidate={candidate} onContinue={onChangeStage} />
221249
);
250+
case POPUP_STAGES.CALENDAR_SYNC_TIMED_OUT:
251+
return (
252+
<CalendarSyncTimedOut userSettings={userSettings} onContinue={onChangeStage} />
253+
);
222254
default:
223255
return null;
224256
}
@@ -279,6 +311,7 @@ const InterviewPopup = ({
279311
})}
280312
>
281313
<Spinner stype="Oval" width={80} height={80} />
314+
{isLoading && loadingMessage && <p styleName="loading-message">{loadingMessage}</p>}
282315
</div>
283316
<div
284317
styleName={cn("component-wrapper", {

‎src/routes/PositionDetails/components/InterviewPopup/styles.module.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,8 @@
2323
.show-component-wrapper {
2424
display: block;
2525
}
26+
27+
.loading-message {
28+
margin-top: 10px;
29+
text-align: center;
30+
}

‎src/server/misc/routes/interview-thank-you-page.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ async function getInterviewThankYouPageController(req, res) {
3636
);
3737
const object = {
3838
jobTitle: nylasconfig.name,
39-
calendarName: _.get(timeslots, '[0].host_name', ''),
39+
calendarName: _.get(timeslots, "[0].host_name", ""),
4040
tz: query.tz,
4141
week: moment.unix(query.start_time).format("dddd"),
4242
startDate: moment.unix(query.start_time).format("MMMM DD, yyyy"),

‎src/utils/helpers.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,20 @@ export const setCurrentStage = (currentStepIdx, stages, setStagesCallback) => {
8080
*/
8181
export const isCustomRole = (role) =>
8282
!role || !role.name || CUSTOM_ROLE_NAMES.includes(role.name.toLowerCase());
83+
84+
/**
85+
* Extracts primaryCalendar from UserMeetingSettings object
86+
* @param {Object} userMeetingSettings UserMeetingSettings instance to check
87+
* @returns {object} the calendar object which is primary
88+
*/
89+
export const getPrimaryCalendar = (userMeetingSettings) =>
90+
_.find(userMeetingSettings.nylasCalendars, (calendar) => calendar.isPrimary);
91+
92+
/**
93+
* Checks if calendar is synced or not
94+
* @param {Object} calendar calendar to check
95+
* @returns {boolean} whether the calendar is in sync or not
96+
*/
97+
export const isCalendarInSync = (calendar) => {
98+
if (calendar) return calendar && calendar.calendarId;
99+
};

0 commit comments

Comments
 (0)
This repository has been archived.