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

Commit 5f11c85

Browse files
authored
Merge pull request #60 from MadOPcode/dev
Added tooltips for work period's selection checkbox, user handle and team name
2 parents 714c26f + 9019778 commit 5f11c85

File tree

22 files changed

+669
-295
lines changed

22 files changed

+669
-295
lines changed

src/components/JobName/index.jsx

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React, { memo, useContext, useEffect } from "react";
2+
import PT from "prop-types";
3+
import cn from "classnames";
4+
import { JobNameContext } from "components/JobNameProvider";
5+
import { JOB_NAME_LOADING } from "constants/workPeriods";
6+
import styles from "./styles.module.scss";
7+
8+
const JobName = ({ className, jobId }) => {
9+
const [getName, fetchName] = useContext(JobNameContext);
10+
const [jobName, error] = getName(jobId);
11+
12+
useEffect(() => {
13+
fetchName(jobId);
14+
}, [fetchName, jobId]);
15+
16+
return (
17+
<span
18+
className={cn(styles.container, { [styles.error]: !!error }, className)}
19+
>
20+
{jobName || JOB_NAME_LOADING}
21+
</span>
22+
);
23+
};
24+
25+
JobName.propTypes = {
26+
className: PT.string,
27+
jobId: PT.oneOfType([PT.number, PT.string]).isRequired,
28+
};
29+
30+
export default memo(JobName);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.container {
2+
display: inline;
3+
}
4+
5+
.error {
6+
color: #e90c5a;
7+
}
+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import React, { createContext, useCallback, useState } from "react";
2+
import PT from "prop-types";
3+
import { fetchJob } from "services/workPeriods";
4+
import { increment } from "utils/misc";
5+
import {
6+
JOB_NAME_ERROR,
7+
JOB_NAME_LOADING,
8+
JOB_NAME_NONE,
9+
} from "constants/workPeriods";
10+
11+
const names = {};
12+
const errors = {};
13+
const promises = {};
14+
15+
/**
16+
* Returns a tuple containing job name and possibly an error.
17+
*
18+
* @param {number|string} id job id
19+
* @returns {Array}
20+
*/
21+
const getName = (id) => (id ? [names[id], errors[id]] : [JOB_NAME_NONE, null]);
22+
23+
export const JobNameContext = createContext([
24+
getName,
25+
(id) => {
26+
`${id}`;
27+
},
28+
]);
29+
30+
const JobNameProvider = ({ children }) => {
31+
const [, setCount] = useState(Number.MIN_SAFE_INTEGER);
32+
33+
const fetchName = useCallback((id) => {
34+
if (!id || ((id in names || id in promises) && !(id in errors))) {
35+
return;
36+
}
37+
names[id] = JOB_NAME_LOADING;
38+
delete errors[id];
39+
setCount(increment);
40+
const [promise] = fetchJob(id);
41+
promises[id] = promise
42+
.then((data) => {
43+
names[id] = data.title;
44+
})
45+
.catch((error) => {
46+
names[id] = JOB_NAME_ERROR;
47+
errors[id] = error;
48+
})
49+
.finally(() => {
50+
delete promises[id];
51+
setCount(increment);
52+
});
53+
}, []);
54+
55+
return (
56+
<JobNameContext.Provider value={[getName, fetchName]}>
57+
{children}
58+
</JobNameContext.Provider>
59+
);
60+
};
61+
62+
JobNameProvider.propTypes = {
63+
children: PT.node,
64+
};
65+
66+
export default JobNameProvider;

src/components/ProjectName/index.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { memo, useContext, useEffect } from "react";
22
import PT from "prop-types";
33
import cn from "classnames";
4-
import { ProjectNameContext } from "components/ProjectNameContextProvider";
4+
import { ProjectNameContext } from "components/ProjectNameProvider";
55
import styles from "./styles.module.scss";
66

77
const ProjectName = ({ className, projectId }) => {

src/components/ProjectName/styles.module.scss

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
@import "styles/mixins";
22

33
.container {
4-
display: block;
4+
display: inline-block;
55
max-width: 20em;
66
overflow: hidden;
77
text-overflow: ellipsis;

src/components/Tooltip/index.jsx

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import React, { useCallback, useEffect, useRef, useState } from "react";
2+
import { usePopper } from "react-popper";
3+
import PT from "prop-types";
4+
import cn from "classnames";
5+
import compStyles from "./styles.module.scss";
6+
7+
/**
8+
* Displays a tooltip
9+
*
10+
* @param {Object} props component properties
11+
* @param {any} props.children tooltip target
12+
* @param {string} [props.className] class name to be added to root element
13+
* @param {any} props.content tooltip content
14+
* @param {number} [props.delay] postpone showing the tooltip after this delay
15+
* @param {boolean} [props.isDisabled] whether the tooltip is disabled
16+
* @param {import('@popperjs/core').Placement} [props.placement] tooltip's
17+
* preferred placement as defined in PopperJS documentation
18+
* @param {'absolute'|'fixed'} [props.strategy] tooltip positioning strategy
19+
* as defined in PopperJS documentation
20+
* @param {string} [props.targetClassName] class name to be added to element
21+
* wrapping around component's children
22+
* @param {string} [props.tooltipClassName] class name to be added to tooltip
23+
* element itself
24+
* @returns {JSX.Element}
25+
*/
26+
const Tooltip = ({
27+
children,
28+
className,
29+
content,
30+
delay = 150,
31+
isDisabled = false,
32+
placement = "top",
33+
strategy = "absolute",
34+
targetClassName,
35+
tooltipClassName,
36+
}) => {
37+
const containerRef = useRef(null);
38+
const timeoutIdRef = useRef(0);
39+
const [isTooltipShown, setIsTooltipShown] = useState(false);
40+
const [referenceElement, setReferenceElement] = useState(null);
41+
const [popperElement, setPopperElement] = useState(null);
42+
const [arrowElement, setArrowElement] = useState(null);
43+
const { styles, attributes, update } = usePopper(
44+
referenceElement,
45+
popperElement,
46+
{
47+
placement,
48+
strategy,
49+
modifiers: [
50+
{ name: "arrow", options: { element: arrowElement, padding: 10 } },
51+
{ name: "offset", options: { offset: [0, 10] } },
52+
{ name: "preventOverflow", options: { padding: 15 } },
53+
],
54+
}
55+
);
56+
57+
const onMouseEnter = useCallback(() => {
58+
timeoutIdRef.current = window.setTimeout(() => {
59+
timeoutIdRef.current = 0;
60+
setIsTooltipShown(true);
61+
}, delay);
62+
}, [delay]);
63+
64+
const onMouseLeave = useCallback(() => {
65+
if (timeoutIdRef.current) {
66+
clearTimeout(timeoutIdRef.current);
67+
}
68+
setIsTooltipShown(false);
69+
}, []);
70+
71+
useEffect(() => {
72+
let observer = null;
73+
if (isTooltipShown && popperElement && update) {
74+
observer = new ResizeObserver(update);
75+
observer.observe(popperElement);
76+
}
77+
return () => {
78+
if (observer) {
79+
observer.unobserve(popperElement);
80+
}
81+
};
82+
}, [isTooltipShown, popperElement, update]);
83+
84+
return (
85+
<div
86+
className={cn(compStyles.container, className)}
87+
ref={containerRef}
88+
onMouseEnter={isDisabled ? null : onMouseEnter}
89+
onMouseLeave={isDisabled ? null : onMouseLeave}
90+
>
91+
<span
92+
className={cn(compStyles.target, targetClassName)}
93+
ref={setReferenceElement}
94+
>
95+
{children}
96+
</span>
97+
{!isDisabled && isTooltipShown && (
98+
<div
99+
ref={setPopperElement}
100+
className={cn(compStyles.tooltip, tooltipClassName)}
101+
style={styles.popper}
102+
{...attributes.popper}
103+
>
104+
{content}
105+
<div
106+
className={compStyles.tooltipArrow}
107+
ref={setArrowElement}
108+
style={styles.arrow}
109+
/>
110+
</div>
111+
)}
112+
</div>
113+
);
114+
};
115+
116+
Tooltip.propTypes = {
117+
children: PT.node,
118+
className: PT.string,
119+
content: PT.node,
120+
delay: PT.number,
121+
isDisabled: PT.bool,
122+
placement: PT.string,
123+
strategy: PT.oneOf(["absolute", "fixed"]),
124+
targetClassName: PT.string,
125+
tooltipClassName: PT.string,
126+
};
127+
128+
export default Tooltip;
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
.container {
2+
position: relative;
3+
display: inline-flex;
4+
align-items: baseline;
5+
}
6+
7+
.target {
8+
display: inline-flex;
9+
align-items: baseline;
10+
}
11+
12+
.tooltip {
13+
z-index: 8;
14+
border-radius: 8px;
15+
padding: 10px 15px;
16+
line-height: 22px;
17+
box-shadow: 0px 5px 25px #c6c6c6;
18+
background: #fff;
19+
20+
.tooltipArrow {
21+
display: block;
22+
top: 100%;
23+
border: 10px solid transparent;
24+
border-bottom: none;
25+
border-top-color: #fff;
26+
width: 0;
27+
height: 0;
28+
}
29+
}

src/constants/workPeriods.js

+9
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as API_SORT_BY from "./workPeriods/apiSortBy";
66
import * as SORT_BY from "./workPeriods/sortBy";
77
import * as SORT_ORDER from "./workPeriods/sortOrder";
88
import * as PAYMENT_STATUS from "./workPeriods/paymentStatus";
9+
import * as REASON_DISABLED from "./workPeriods/reasonDisabled";
910

1011
export {
1112
API_CHALLENGE_PAYMENT_STATUS,
@@ -14,6 +15,7 @@ export {
1415
SORT_BY,
1516
SORT_ORDER,
1617
PAYMENT_STATUS,
18+
REASON_DISABLED,
1719
};
1820

1921
// resource bookings API url
@@ -135,3 +137,10 @@ export const JOB_NAME_ERROR = "<Error loading job>";
135137
export const BILLING_ACCOUNTS_LOADING = "Loading...";
136138
export const BILLING_ACCOUNTS_NONE = "<No accounts available>";
137139
export const BILLING_ACCOUNTS_ERROR = "<Error loading accounts>";
140+
141+
export const REASON_DISABLED_MESSAGE_MAP = {
142+
[REASON_DISABLED.NO_BILLING_ACCOUNT]:
143+
"Billing Account is not set for the Resorce Booking",
144+
[REASON_DISABLED.NO_DAYS_TO_PAY_FOR]: "There are no days to pay for",
145+
[REASON_DISABLED.NO_MEMBER_RATE]: "Member Rate should be greater than 0",
146+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const NO_BILLING_ACCOUNT = "NO_BILLING_ACCOUNT";
2+
export const NO_DAYS_TO_PAY_FOR = "NO_DAYS_TO_PAY_FOR";
3+
export const NO_MEMBER_RATE = "NO_MEMBER_RATE";

src/routes/WorkPeriods/components/PeriodDetails/index.jsx

+5-10
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,17 @@ import debounce from "lodash/debounce";
66
import Button from "components/Button";
77
import Toggle from "components/Toggle";
88
import SelectField from "components/SelectField";
9+
import JobName from "components/JobName";
910
import PeriodsHistory from "../PeriodsHistory";
1011
import IconComputer from "../../../../assets/images/icon-computer.svg";
1112
import {
1213
hideWorkPeriodDetails,
1314
setBillingAccount,
1415
setDetailsHidePastPeriods,
1516
} from "store/actions/workPeriods";
16-
import styles from "./styles.module.scss";
1717
import { updateWorkPeriodBillingAccount } from "store/thunks/workPeriods";
1818
import { useUpdateEffect } from "utils/hooks";
19+
import styles from "./styles.module.scss";
1920

2021
/**
2122
* Displays working period details.
@@ -32,8 +33,7 @@ const PeriodDetails = ({ className, details, isDisabled, isFailed }) => {
3233
const {
3334
periodId,
3435
rbId,
35-
jobName,
36-
jobNameError,
36+
jobId,
3737
billingAccountId,
3838
billingAccounts,
3939
billingAccountsError,
@@ -95,13 +95,7 @@ const PeriodDetails = ({ className, details, isDisabled, isFailed }) => {
9595
<IconComputer className={styles.jobNameIcon} />
9696
<div className={styles.sectionField}>
9797
<div className={styles.label}>Job Name</div>
98-
<div
99-
className={cn(styles.jobName, {
100-
[styles.jobNameError]: !!jobNameError,
101-
})}
102-
>
103-
{jobName}
104-
</div>
98+
<JobName jobId={jobId} className={styles.jobName} />
10599
</div>
106100
</div>
107101
<div className={styles.billingAccountsSection}>
@@ -161,6 +155,7 @@ PeriodDetails.propTypes = {
161155
details: PT.shape({
162156
periodId: PT.string.isRequired,
163157
rbId: PT.string.isRequired,
158+
jobId: PT.string.isRequired,
164159
jobName: PT.string,
165160
jobNameError: PT.string,
166161
jobNameIsLoading: PT.bool.isRequired,

0 commit comments

Comments
 (0)