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

Commit c7bbee2

Browse files
committed
Implemented functionality to wait & sync host's calendar before requesting interview.
This commit addresses issue #588.
1 parent 8961b3c commit c7bbee2

File tree

7 files changed

+232
-28
lines changed

7 files changed

+232
-28
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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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>Please, try re-connecting your calendar by clicking "manage connected calendars". Would you have any issues, please don't hesitate to reach out to us by [email protected].</p>
19+
</div>
20+
<div styleName="button-wrapper">
21+
<Button
22+
onClick={() => onContinue(POPUP_STAGES.SEND_INTERVIEW_INVITE)}
23+
size={BUTTON_SIZE.MEDIUM}
24+
type={BUTTON_TYPE.PRIMARY}
25+
>
26+
OK
27+
</Button>
28+
</div>
29+
</div>);
30+
}
31+
32+
CalendarSyncTimedOut.propTypes = {
33+
userSettings: PT.object,
34+
};
35+
36+
export default CalendarSyncTimedOut;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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+
}
Lines changed: 115 additions & 28 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,12 +28,62 @@ const Confirm = ({
2528
onGoBack,
2629
onContinue,
2730
onShowingLoader,
31+
userSettings,
32+
getSettingsModular,
2833
}) => {
2934
const { teamId, positionId } = useParams();
35+
const [loadingMessage, setLoadingMessage] = useState(null);
36+
const [syncCalendarTimeoutId, setSyncCalendarTimeoutId] = useState(null);
37+
// flag indicating the 1st attempt to refetch UserMeetingSettings is made, must repeat requests every 10secs
38+
const [initiateInterviewConfirmation, setInitiateInterviewConfirmation] = useState(null);
39+
// each timeout equals 10 seconds - so 120 seconds = 2mins = 12 timeouts - this excludes time taken for request response
40+
const [totalSyncTimeouts, setTotalSyncTimeouts] = useState(0);
3041
const dispatch = useDispatch();
3142
const { handle, id: candidateId } = candidate;
3243
const { duration } = scheduleDetails;
3344

45+
// check if primary calendar is syncing or synced and handle accordingly
46+
useEffect(() => {
47+
const primaryCalendar = getPrimaryCalendar(userSettings);
48+
const calendarSynced = primaryCalendar ? isCalendarInSync(primaryCalendar) : false;
49+
50+
// to know if calendar is ready and synced to request interview, we check for primary calendar
51+
// & its sync status, combined with either the timeoutid or initiate confirmation flag
52+
if ((primaryCalendar && calendarSynced) && (syncCalendarTimeoutId || initiateInterviewConfirmation))
53+
{
54+
// clear timeout since calendar is now synced
55+
clearTimeout(syncCalendarTimeoutId);
56+
57+
// reset state
58+
setSyncCalendarTimeoutId(null);
59+
setInitiateInterviewConfirmation(false);
60+
61+
// disable loading indicator
62+
onSetLoadingMessage(`Scheduling interview...`);
63+
64+
// continue with interview scheduling
65+
onContinueAhead();
66+
}
67+
else if ((primaryCalendar && !calendarSynced) && (syncCalendarTimeoutId || initiateInterviewConfirmation))
68+
{
69+
if (totalSyncTimeouts > 12)
70+
{
71+
onContinue(POPUP_STAGES.CALENDAR_SYNC_TIMED_OUT);
72+
}
73+
else
74+
{
75+
// since primary calendar is still not synced, reset timeout
76+
setSyncCalendarTimeoutId(setTimeout(() => getSettingsModular(), 10000));
77+
setTotalSyncTimeouts(totalSyncTimeouts + 1);
78+
}
79+
}
80+
81+
// clear timeout on component unmount
82+
return () => clearTimeout(syncCalendarTimeoutId);
83+
}, [userSettings]);
84+
85+
const onSetLoadingMessage = (text) => setLoadingMessage(text);
86+
3487
/**
3588
* This will trigger the API call to the server to request an interview
3689
*/
@@ -40,6 +93,7 @@ const Confirm = ({
4093
duration: scheduleDetails.duration,
4194
availableTime: scheduleDetails.slots,
4295
};
96+
onSetLoadingMessage(null);
4397
onShowingLoader(true);
4498

4599
confirmInterview(candidateId, params)
@@ -53,34 +107,65 @@ const Confirm = ({
53107
});
54108
};
55109

110+
const inviteInterviewCandidate = (userSettingsParam) => {
111+
let primaryCalendar = getPrimaryCalendar(userSettingsParam);
112+
let calendarSynced = isCalendarInSync(primaryCalendar);
113+
114+
if (primaryCalendar && !calendarSynced)
115+
{
116+
// show loading indicator with message
117+
onSetLoadingMessage(`Syncing your new calendar ${primaryCalendar.email}, it might take a few minutes...`);
118+
119+
// fetch UserMeetingSettings in the background, for the 1st time, no timeout is necessary,
120+
// so we set initiateInterviewConfirmation value to true
121+
getSettingsModular();
122+
setInitiateInterviewConfirmation(true);
123+
}
124+
else if (primaryCalendar && calendarSynced)
125+
{
126+
onContinueAhead();
127+
}
128+
}
129+
56130
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>
131+
<>
132+
{loadingMessage && <div
133+
styleName={cn("spinner-wrapper", {
134+
"show-spinner": loadingMessage,
135+
})}
136+
>
137+
<Spinner stype="Oval" width={80} height={80} />
138+
<p styleName="loading-message">{loadingMessage}</p>
139+
</div>}
140+
{!loadingMessage && <div styleName="confirm-wrapper">
141+
<StepsIndicator steps={SCHEDULE_INTERVIEW_STEPS} currentStep="confirm" />
142+
<div styleName="confirm-text">
143+
Send a <span styleName="confirm-text-bold">{duration} Minute</span>{" "}
144+
Interview invite to <span styleName="confirm-text-bold">{handle}</span>.
145+
This invite will allow <span styleName="confirm-text-bold">{handle}</span> to select and schedule an interview date
146+
and time based on your availability.
147+
</div>
148+
149+
<div styleName="button-wrapper">
150+
<Button
151+
styleName="back-button"
152+
onClick={() => onGoBack()}
153+
size={BUTTON_SIZE.MEDIUM}
154+
type={BUTTON_TYPE.SECONDARY}
155+
>
156+
Back
157+
</Button>
158+
<Button
159+
onClick={() => inviteInterviewCandidate(userSettings)}
160+
disabled={initiateInterviewConfirmation}
161+
size={BUTTON_SIZE.MEDIUM}
162+
type={BUTTON_TYPE.PRIMARY}
163+
>
164+
Confirm
165+
</Button>
166+
</div>
167+
</div>}
168+
</>
84169
);
85170
};
86171

@@ -90,6 +175,8 @@ Confirm.propTypes = {
90175
onGoBack: PT.func,
91176
onContinue: PT.func,
92177
onShowingLoader: PT.func,
178+
userSettings: PT.object,
179+
getSettingsModular: PT.func,
93180
};
94181

95182
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: 22 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";
@@ -62,6 +63,21 @@ const InterviewPopup = ({
6263
slots: userSettings.defaultAvailableTime || []
6364
});
6465

66+
const getSettingsModular = () => {
67+
getUserSettings(v5UserProfile.id)
68+
.then((res) => {
69+
setUserSettings(res.data);
70+
})
71+
.catch((e) => {
72+
if (e.response && e.response.status === 404) {
73+
setStage(POPUP_STAGES.SCHEDULE_INTERVIEW);
74+
} else {
75+
toastr.error("Failed to get user settings");
76+
onCloseInterviewPopup();
77+
}
78+
})
79+
}
80+
6581
/**
6682
* Gets the settings from the backend and checks if the calendar is already available
6783
*/
@@ -213,12 +229,18 @@ const InterviewPopup = ({
213229
onGoBack={onGoingBack}
214230
onShowingLoader={onShowingLoader}
215231
candidate={candidate}
232+
userSettings={userSettings}
233+
getSettingsModular={getSettingsModular}
216234
/>
217235
);
218236
case POPUP_STAGES.SUCCESS:
219237
return (
220238
<Success candidate={candidate} onContinue={onChangeStage} />
221239
);
240+
case POPUP_STAGES.CALENDAR_SYNC_TIMED_OUT:
241+
return (
242+
<CalendarSyncTimedOut userSettings={userSettings} onContinue={onChangeStage} />
243+
);
222244
default:
223245
return null;
224246
}

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) => _.find(userMeetingSettings.nylasCalendars, calendar => calendar.isPrimary);
90+
91+
/**
92+
* Checks if calendar is synced or not
93+
* @param {Object} calendar calendar to check
94+
* @returns {boolean} whether the calendar is in sync or not
95+
*/
96+
export const isCalendarInSync = (calendar) => {
97+
if (calendar)
98+
return calendar && calendar.calendarId;
99+
}

0 commit comments

Comments
 (0)