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

Added tooltips for work period's selection checkbox, user handle and team name #60

Merged
merged 5 commits into from
Jul 3, 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
30 changes: 30 additions & 0 deletions src/components/JobName/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { memo, useContext, useEffect } from "react";
import PT from "prop-types";
import cn from "classnames";
import { JobNameContext } from "components/JobNameProvider";
import { JOB_NAME_LOADING } from "constants/workPeriods";
import styles from "./styles.module.scss";

const JobName = ({ className, jobId }) => {
const [getName, fetchName] = useContext(JobNameContext);
const [jobName, error] = getName(jobId);

useEffect(() => {
fetchName(jobId);
}, [fetchName, jobId]);

return (
<span
className={cn(styles.container, { [styles.error]: !!error }, className)}
>
{jobName || JOB_NAME_LOADING}
</span>
);
};

JobName.propTypes = {
className: PT.string,
jobId: PT.oneOfType([PT.number, PT.string]).isRequired,
};

export default memo(JobName);
7 changes: 7 additions & 0 deletions src/components/JobName/styles.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.container {
display: inline;
}

.error {
color: #e90c5a;
}
66 changes: 66 additions & 0 deletions src/components/JobNameProvider/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React, { createContext, useCallback, useState } from "react";
import PT from "prop-types";
import { fetchJob } from "services/workPeriods";
import { increment } from "utils/misc";
import {
JOB_NAME_ERROR,
JOB_NAME_LOADING,
JOB_NAME_NONE,
} from "constants/workPeriods";

const names = {};
const errors = {};
const promises = {};

/**
* Returns a tuple containing job name and possibly an error.
*
* @param {number|string} id job id
* @returns {Array}
*/
const getName = (id) => (id ? [names[id], errors[id]] : [JOB_NAME_NONE, null]);

export const JobNameContext = createContext([
getName,
(id) => {
`${id}`;
},
]);

const JobNameProvider = ({ children }) => {
const [, setCount] = useState(Number.MIN_SAFE_INTEGER);

const fetchName = useCallback((id) => {
if (!id || ((id in names || id in promises) && !(id in errors))) {
return;
}
names[id] = JOB_NAME_LOADING;
delete errors[id];
setCount(increment);
const [promise] = fetchJob(id);
promises[id] = promise
.then((data) => {
names[id] = data.title;
})
.catch((error) => {
names[id] = JOB_NAME_ERROR;
errors[id] = error;
})
.finally(() => {
delete promises[id];
setCount(increment);
});
}, []);

return (
<JobNameContext.Provider value={[getName, fetchName]}>
{children}
</JobNameContext.Provider>
);
};

JobNameProvider.propTypes = {
children: PT.node,
};

export default JobNameProvider;
2 changes: 1 addition & 1 deletion src/components/ProjectName/index.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { memo, useContext, useEffect } from "react";
import PT from "prop-types";
import cn from "classnames";
import { ProjectNameContext } from "components/ProjectNameContextProvider";
import { ProjectNameContext } from "components/ProjectNameProvider";
import styles from "./styles.module.scss";

const ProjectName = ({ className, projectId }) => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/ProjectName/styles.module.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@import "styles/mixins";

.container {
display: block;
display: inline-block;
max-width: 20em;
overflow: hidden;
text-overflow: ellipsis;
Expand Down
128 changes: 128 additions & 0 deletions src/components/Tooltip/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import { usePopper } from "react-popper";
import PT from "prop-types";
import cn from "classnames";
import compStyles from "./styles.module.scss";

/**
* Displays a tooltip
*
* @param {Object} props component properties
* @param {any} props.children tooltip target
* @param {string} [props.className] class name to be added to root element
* @param {any} props.content tooltip content
* @param {number} [props.delay] postpone showing the tooltip after this delay
* @param {boolean} [props.isDisabled] whether the tooltip is disabled
* @param {import('@popperjs/core').Placement} [props.placement] tooltip's
* preferred placement as defined in PopperJS documentation
* @param {'absolute'|'fixed'} [props.strategy] tooltip positioning strategy
* as defined in PopperJS documentation
* @param {string} [props.targetClassName] class name to be added to element
* wrapping around component's children
* @param {string} [props.tooltipClassName] class name to be added to tooltip
* element itself
* @returns {JSX.Element}
*/
const Tooltip = ({
children,
className,
content,
delay = 150,
isDisabled = false,
placement = "top",
strategy = "absolute",
targetClassName,
tooltipClassName,
}) => {
const containerRef = useRef(null);
const timeoutIdRef = useRef(0);
const [isTooltipShown, setIsTooltipShown] = useState(false);
const [referenceElement, setReferenceElement] = useState(null);
const [popperElement, setPopperElement] = useState(null);
const [arrowElement, setArrowElement] = useState(null);
const { styles, attributes, update } = usePopper(
referenceElement,
popperElement,
{
placement,
strategy,
modifiers: [
{ name: "arrow", options: { element: arrowElement, padding: 10 } },
{ name: "offset", options: { offset: [0, 10] } },
{ name: "preventOverflow", options: { padding: 15 } },
],
}
);

const onMouseEnter = useCallback(() => {
timeoutIdRef.current = window.setTimeout(() => {
timeoutIdRef.current = 0;
setIsTooltipShown(true);
}, delay);
}, [delay]);

const onMouseLeave = useCallback(() => {
if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
}
setIsTooltipShown(false);
}, []);

useEffect(() => {
let observer = null;
if (isTooltipShown && popperElement && update) {
observer = new ResizeObserver(update);
observer.observe(popperElement);
}
return () => {
if (observer) {
observer.unobserve(popperElement);
}
};
}, [isTooltipShown, popperElement, update]);

return (
<div
className={cn(compStyles.container, className)}
ref={containerRef}
onMouseEnter={isDisabled ? null : onMouseEnter}
onMouseLeave={isDisabled ? null : onMouseLeave}
>
<span
className={cn(compStyles.target, targetClassName)}
ref={setReferenceElement}
>
{children}
</span>
{!isDisabled && isTooltipShown && (
<div
ref={setPopperElement}
className={cn(compStyles.tooltip, tooltipClassName)}
style={styles.popper}
{...attributes.popper}
>
{content}
<div
className={compStyles.tooltipArrow}
ref={setArrowElement}
style={styles.arrow}
/>
</div>
)}
</div>
);
};

Tooltip.propTypes = {
children: PT.node,
className: PT.string,
content: PT.node,
delay: PT.number,
isDisabled: PT.bool,
placement: PT.string,
strategy: PT.oneOf(["absolute", "fixed"]),
targetClassName: PT.string,
tooltipClassName: PT.string,
};

export default Tooltip;
29 changes: 29 additions & 0 deletions src/components/Tooltip/styles.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
.container {
position: relative;
display: inline-flex;
align-items: baseline;
}

.target {
display: inline-flex;
align-items: baseline;
}

.tooltip {
z-index: 8;
border-radius: 8px;
padding: 10px 15px;
line-height: 22px;
box-shadow: 0px 5px 25px #c6c6c6;
background: #fff;

.tooltipArrow {
display: block;
top: 100%;
border: 10px solid transparent;
border-bottom: none;
border-top-color: #fff;
width: 0;
height: 0;
}
}
9 changes: 9 additions & 0 deletions src/constants/workPeriods.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as API_SORT_BY from "./workPeriods/apiSortBy";
import * as SORT_BY from "./workPeriods/sortBy";
import * as SORT_ORDER from "./workPeriods/sortOrder";
import * as PAYMENT_STATUS from "./workPeriods/paymentStatus";
import * as REASON_DISABLED from "./workPeriods/reasonDisabled";

export {
API_CHALLENGE_PAYMENT_STATUS,
Expand All @@ -14,6 +15,7 @@ export {
SORT_BY,
SORT_ORDER,
PAYMENT_STATUS,
REASON_DISABLED,
};

// resource bookings API url
Expand Down Expand Up @@ -135,3 +137,10 @@ export const JOB_NAME_ERROR = "<Error loading job>";
export const BILLING_ACCOUNTS_LOADING = "Loading...";
export const BILLING_ACCOUNTS_NONE = "<No accounts available>";
export const BILLING_ACCOUNTS_ERROR = "<Error loading accounts>";

export const REASON_DISABLED_MESSAGE_MAP = {
[REASON_DISABLED.NO_BILLING_ACCOUNT]:
"Billing Account is not set for the Resorce Booking",
[REASON_DISABLED.NO_DAYS_TO_PAY_FOR]: "There are no days to pay for",
[REASON_DISABLED.NO_MEMBER_RATE]: "Member Rate should be greater than 0",
};
3 changes: 3 additions & 0 deletions src/constants/workPeriods/reasonDisabled.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const NO_BILLING_ACCOUNT = "NO_BILLING_ACCOUNT";
export const NO_DAYS_TO_PAY_FOR = "NO_DAYS_TO_PAY_FOR";
export const NO_MEMBER_RATE = "NO_MEMBER_RATE";
15 changes: 5 additions & 10 deletions src/routes/WorkPeriods/components/PeriodDetails/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@ import debounce from "lodash/debounce";
import Button from "components/Button";
import Toggle from "components/Toggle";
import SelectField from "components/SelectField";
import JobName from "components/JobName";
import PeriodsHistory from "../PeriodsHistory";
import IconComputer from "../../../../assets/images/icon-computer.svg";
import {
hideWorkPeriodDetails,
setBillingAccount,
setDetailsHidePastPeriods,
} from "store/actions/workPeriods";
import styles from "./styles.module.scss";
import { updateWorkPeriodBillingAccount } from "store/thunks/workPeriods";
import { useUpdateEffect } from "utils/hooks";
import styles from "./styles.module.scss";

/**
* Displays working period details.
Expand All @@ -32,8 +33,7 @@ const PeriodDetails = ({ className, details, isDisabled, isFailed }) => {
const {
periodId,
rbId,
jobName,
jobNameError,
jobId,
billingAccountId,
billingAccounts,
billingAccountsError,
Expand Down Expand Up @@ -95,13 +95,7 @@ const PeriodDetails = ({ className, details, isDisabled, isFailed }) => {
<IconComputer className={styles.jobNameIcon} />
<div className={styles.sectionField}>
<div className={styles.label}>Job Name</div>
<div
className={cn(styles.jobName, {
[styles.jobNameError]: !!jobNameError,
})}
>
{jobName}
</div>
<JobName jobId={jobId} className={styles.jobName} />
</div>
</div>
<div className={styles.billingAccountsSection}>
Expand Down Expand Up @@ -161,6 +155,7 @@ PeriodDetails.propTypes = {
details: PT.shape({
periodId: PT.string.isRequired,
rbId: PT.string.isRequired,
jobId: PT.string.isRequired,
jobName: PT.string,
jobNameError: PT.string,
jobNameIsLoading: PT.bool.isRequired,
Expand Down
Loading