From 8ac598b36486b827d47da1ef36c3e2de06eedc50 Mon Sep 17 00:00:00 2001 From: Oleg Petrov Date: Thu, 19 Aug 2021 08:33:56 +0300 Subject: [PATCH] Implements tracking of resume link clicking. --- .../components/PositionCandidates/index.jsx | 35 ++++++++++++++++--- .../PositionCandidates/styles.module.scss | 12 +++++++ src/services/requestInterceptor.js | 8 +++++ src/services/teams.js | 25 ++++++++++++- 4 files changed, 74 insertions(+), 6 deletions(-) diff --git a/src/routes/PositionDetails/components/PositionCandidates/index.jsx b/src/routes/PositionDetails/components/PositionCandidates/index.jsx index cf56f90e..59f4ac76 100644 --- a/src/routes/PositionDetails/components/PositionCandidates/index.jsx +++ b/src/routes/PositionDetails/components/PositionCandidates/index.jsx @@ -5,6 +5,7 @@ */ import React, { useMemo, useState, useCallback, useEffect } from "react"; import PT from "prop-types"; +import cn from "classnames"; import _ from "lodash"; import CardHeader from "components/CardHeader"; import "./styles.module.scss"; @@ -24,6 +25,7 @@ import Pagination from "components/Pagination"; import IconResume from "../../../../assets/images/icon-resume.svg"; import { toastr } from "react-redux-toastr"; import { getJobById } from "services/jobs"; +import { touchCandidateResume } from "services/teams"; import { PERMISSIONS } from "constants/permissions"; import { hasPermission } from "utils/permissions"; import ActionsMenu from "components/ActionsMenu"; @@ -140,6 +142,27 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => { [setPage] ); + const [isTouchingResume, setIsTouchingResume] = useState(false); + const onClickResumeLink = (event) => { + let targetData = event.target.dataset; + let candidateId = targetData.candidateId; + if (!candidateId) { + return; + } + let resumeLink = targetData.resumeLink; + setIsTouchingResume(true); + touchCandidateResume(candidateId) + .then(() => { + if (resumeLink) { + window.open(resumeLink, "_blank"); + } + }) + .catch(console.error) + .finally(() => { + setIsTouchingResume(false); + }); + }; + const markCandidateSelected = useCallback( (candidate) => { return updateCandidate(candidate.id, { @@ -222,14 +245,16 @@ const PositionCandidates = ({ position, statusFilterKey, updateCandidate }) => { limit={7} /> {candidate.resume && ( - Download Resume - + )} {statusFilterKey === CANDIDATE_STATUS_FILTER_KEY.INTERESTED && ( diff --git a/src/routes/PositionDetails/components/PositionCandidates/styles.module.scss b/src/routes/PositionDetails/components/PositionCandidates/styles.module.scss index 11babadb..3c26b107 100644 --- a/src/routes/PositionDetails/components/PositionCandidates/styles.module.scss +++ b/src/routes/PositionDetails/components/PositionCandidates/styles.module.scss @@ -81,12 +81,24 @@ display: inline-flex; align-items: center; margin-top: 5px; + border: none; + padding: 0; + width: auto; + height: auto; + color: #0d61bf; + background: transparent; + outline: none !important; + box-shadow: none !important; > svg { margin-right: 10px; } } +.busy { + cursor: wait; +} + @media (max-width: 850px) { .table-row { flex-wrap: wrap; diff --git a/src/services/requestInterceptor.js b/src/services/requestInterceptor.js index e17878a7..bead2f0a 100644 --- a/src/services/requestInterceptor.js +++ b/src/services/requestInterceptor.js @@ -35,3 +35,11 @@ axiosInstance.interceptors.response.use( return Promise.reject(error); } ); + +export const fetchCustom = async (url, init = {}) => { + let { tokenV3 } = await getAuthUserTokens(); + let headers = init.headers || {}; + headers.Authorization = `Bearer ${tokenV3}`; + init.headers = headers; + return fetch(url, init); +}; diff --git a/src/services/teams.js b/src/services/teams.js index 59a660b9..1e0ed867 100644 --- a/src/services/teams.js +++ b/src/services/teams.js @@ -1,7 +1,11 @@ /** * Topcoder TaaS Service */ -import { axiosInstance as axios } from "./requestInterceptor"; +import { + axiosInstance as axios, + fetchCustom as fetch, +} from "./requestInterceptor"; + import config from "../../config"; /** @@ -93,6 +97,25 @@ export const patchCandidateInterview = (candidateId, interviewData) => { ); }; +/** + * Sends request to candidate's resume URL while trying to avoid downloading + * the resume itself. + * + * @param {string} candidateId interview candidate id + * @returns {Promise} + */ +export const touchCandidateResume = async (candidateId) => { + try { + // The result of redirect to different origin will not contain any useful + // data. See https://fetch.spec.whatwg.org/#atomic-http-redirect-handling + await fetch(`${config.API.V5}/jobCandidates/${candidateId}/resume`, { + redirect: "manual", + }); + } catch (error) { + console.error(error); + } +}; + /** * Get Team Members *