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

Commit a401de6

Browse files
committed
Adds fixes for #80, #82. Implements #94, #95.
- Adds fixes for adding and editing payments (#80, #82) - Adds working period data reloading after adding and editing payments (#94) - Makes toastr messages always stay withing the viewport (#95)
1 parent 765a616 commit a401de6

File tree

23 files changed

+412
-169
lines changed

23 files changed

+412
-169
lines changed

src/components/ActionsMenu/index.jsx

+84-49
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import PT from "prop-types";
33
import cn from "classnames";
44
import { usePopper } from "react-popper";
55
import Button from "components/Button";
6+
import Tooltip from "components/Tooltip";
67
import IconArrowDown from "../../assets/images/icon-arrow-down-narrow.svg";
78
import { useClickOutside } from "utils/hooks";
89
import { negate, stopPropagation } from "utils/misc";
@@ -39,48 +40,6 @@ const ActionsMenu = ({
3940
setIsOpen(negate);
4041
}, []);
4142

42-
const onItemClick = useCallback(
43-
(event) => {
44-
let index = +event.target.dataset.actionIndex;
45-
let item = items[index];
46-
if (!item || item.disabled || item.separator) {
47-
return;
48-
}
49-
closeMenu();
50-
item.action?.();
51-
},
52-
[items, closeMenu]
53-
);
54-
55-
const menuItems = useMemo(
56-
() =>
57-
items.map((item, index) => {
58-
if (item.hidden) {
59-
return null;
60-
} else if (item.separator) {
61-
return <div key={index} className={compStyles.separator} />;
62-
} else {
63-
return (
64-
<div
65-
key={index}
66-
data-action-index={index}
67-
onClick={onItemClick}
68-
role="button"
69-
tabIndex={0}
70-
className={cn(
71-
compStyles.item,
72-
{ [compStyles.itemDisabled]: item.disabled },
73-
item.className
74-
)}
75-
>
76-
{item.label}
77-
</div>
78-
);
79-
}
80-
}),
81-
[items, onItemClick]
82-
);
83-
8443
return (
8544
<div
8645
className={compStyles.container}
@@ -104,8 +63,8 @@ const ActionsMenu = ({
10463
</Button>
10564
{isOpen && (
10665
<Menu
107-
items={menuItems}
108-
onClickOutside={closeMenu}
66+
close={closeMenu}
67+
items={items}
10968
referenceElement={referenceElement}
11069
strategy={popupStrategy}
11170
/>
@@ -123,6 +82,7 @@ ActionsMenu.propTypes = {
12382
label: PT.string,
12483
action: PT.func,
12584
separator: PT.bool,
85+
disabled: PT.bool,
12686
hidden: PT.bool,
12787
})
12888
),
@@ -138,7 +98,7 @@ export default ActionsMenu;
13898
* @param {Object} props component properties
13999
* @returns {JSX.Element}
140100
*/
141-
const Menu = ({ items, onClickOutside, referenceElement, strategy }) => {
101+
const Menu = ({ close, items, referenceElement, strategy }) => {
142102
const [popperElement, setPopperElement] = useState(null);
143103
const [arrowElement, setArrowElement] = useState(null);
144104
const { styles, attributes } = usePopper(referenceElement, popperElement, {
@@ -180,7 +140,73 @@ const Menu = ({ items, onClickOutside, referenceElement, strategy }) => {
180140
],
181141
});
182142

183-
useClickOutside(popperElement, onClickOutside, []);
143+
const onClickItem = useCallback(
144+
(event) => {
145+
let targetData = event.target.dataset;
146+
let index = +targetData.actionIndex;
147+
let item = items[index];
148+
if (!item || targetData.disabled || item.separator) {
149+
return;
150+
}
151+
close();
152+
item.action?.();
153+
},
154+
[close, items]
155+
);
156+
157+
useClickOutside(popperElement, close, []);
158+
159+
const menuItems = useMemo(() => {
160+
return items.map((item, index) => {
161+
if (item.hidden) {
162+
return null;
163+
} else if (item.separator) {
164+
return <div key={index} className={compStyles.separator} />;
165+
} else {
166+
let reasonsDisabled = item.checkDisabled?.();
167+
let disabled = !!item.disabled || !!reasonsDisabled;
168+
let attrs = {
169+
key: index,
170+
"data-action-index": index,
171+
onClick: onClickItem,
172+
role: "button",
173+
tabIndex: 0,
174+
className: cn(
175+
compStyles.item,
176+
{ [compStyles.itemDisabled]: disabled },
177+
item.className
178+
),
179+
};
180+
if (disabled) {
181+
attrs["data-disabled"] = true;
182+
}
183+
return (
184+
<div {...attrs}>
185+
{reasonsDisabled ? (
186+
<Tooltip
187+
content={
188+
reasonsDisabled.length === 1 ? (
189+
reasonsDisabled[0]
190+
) : (
191+
<ul>
192+
{reasonsDisabled.map((text, index) => (
193+
<li key={index}>{text}</li>
194+
))}
195+
</ul>
196+
)
197+
}
198+
strategy="fixed"
199+
>
200+
{item.label}
201+
</Tooltip>
202+
) : (
203+
item.label
204+
)}
205+
</div>
206+
);
207+
}
208+
});
209+
}, [items, onClickItem]);
184210

185211
return (
186212
<div
@@ -189,7 +215,7 @@ const Menu = ({ items, onClickOutside, referenceElement, strategy }) => {
189215
style={styles.popper}
190216
{...attributes.popper}
191217
>
192-
<div className={compStyles.items}>{items}</div>
218+
<div className={compStyles.items}>{menuItems}</div>
193219
<div
194220
ref={setArrowElement}
195221
style={styles.arrow}
@@ -200,8 +226,17 @@ const Menu = ({ items, onClickOutside, referenceElement, strategy }) => {
200226
};
201227

202228
Menu.propTypes = {
203-
items: PT.array.isRequired,
204-
onClickOutside: PT.func.isRequired,
229+
close: PT.func.isRequired,
230+
items: PT.arrayOf(
231+
PT.shape({
232+
label: PT.string,
233+
action: PT.func,
234+
checkDisabled: PT.func,
235+
disabled: PT.bool,
236+
separator: PT.bool,
237+
hidden: PT.bool,
238+
})
239+
),
205240
referenceElement: PT.object,
206241
strategy: PT.oneOf(["absolute", "fixed"]),
207242
};

src/components/ActionsMenu/styles.module.scss

+2-3
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,8 @@
7171
}
7272

7373
.itemDisabled {
74-
color: gray;
75-
opacity: 0.6;
76-
pointer-events: none;
74+
color: #bbb;
75+
cursor: default;
7776
}
7877

7978
.hidden {
+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React, { useCallback } from "react";
2+
import PT from "prop-types";
3+
import TextField from "components/TextField";
4+
5+
/**
6+
* Displays text field with optional label.
7+
*
8+
* @param {Object} props component properties
9+
* @returns {JSX.Element}
10+
*/
11+
const CurrencyField = (props) => {
12+
const { onChange } = props;
13+
14+
const onChangeValue = useCallback(
15+
(value) => {
16+
onChange(normalizeValue(value));
17+
},
18+
[onChange]
19+
);
20+
21+
return <TextField {...props} onChange={onChangeValue} />;
22+
};
23+
24+
CurrencyField.propTypes = {
25+
className: PT.string,
26+
error: PT.string,
27+
id: PT.string,
28+
isDisabled: PT.bool,
29+
isTouched: PT.bool,
30+
label: PT.string,
31+
name: PT.string.isRequired,
32+
onBlur: PT.func,
33+
onChange: PT.func,
34+
onFocus: PT.func,
35+
size: PT.oneOf(["small", "medium"]),
36+
value: PT.oneOfType([PT.number, PT.string]).isRequired,
37+
};
38+
39+
export default CurrencyField;
40+
41+
/**
42+
* Returns normalized payment amount.
43+
*
44+
* @param {string} value peyment amount
45+
* @returns {string}
46+
*/
47+
function normalizeValue(value) {
48+
if (!value) {
49+
return value;
50+
}
51+
value = value.trim();
52+
let dotIndex = value.lastIndexOf(".");
53+
if (isNaN(+value) || dotIndex < 0) {
54+
return value;
55+
}
56+
if (dotIndex === 0) {
57+
return "0" + value;
58+
}
59+
if (value.length - dotIndex > 3) {
60+
return value.slice(0, dotIndex + 3);
61+
}
62+
return value;
63+
}

src/components/CurrencyField/styles.module.scss

Whitespace-only changes.

src/components/Page/index.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import styles from "./styles.module.scss";
1515
*/
1616
const Page = ({ className, children }) => (
1717
<div className={cn(styles.container, className)}>
18-
{children}
1918
<ReduxToastr
2019
timeOut={TOAST_DEFAULT_TIMEOUT}
2120
position="top-right"
@@ -26,6 +25,7 @@ const Page = ({ className, children }) => (
2625
transitionIn="fadeIn"
2726
transitionOut="fadeOut"
2827
/>
28+
{children}
2929
</div>
3030
);
3131

src/components/Page/styles.module.scss

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
@include desktop {
1616
flex-direction: row;
17+
flex-wrap: wrap;
1718
}
1819

1920
*,

src/components/TextField/index.jsx

+18-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useCallback } from "react";
22
import PT from "prop-types";
33
import cn from "classnames";
4+
import ValidationError from "components/ValidationError";
45
import styles from "./styles.module.scss";
56

67
/**
@@ -11,9 +12,12 @@ import styles from "./styles.module.scss";
1112
*/
1213
const TextField = ({
1314
className,
15+
error,
16+
errorClassName,
1417
id,
18+
inputRef,
1519
isDisabled = false,
16-
isValid = true,
20+
isTouched = false,
1721
label,
1822
name,
1923
onBlur,
@@ -39,7 +43,7 @@ const TextField = ({
3943
{
4044
[styles.hasLabel]: !!label,
4145
[styles.disabled]: isDisabled,
42-
[styles.invalid]: !isValid,
46+
[styles.invalid]: !!error,
4347
},
4448
className
4549
)}
@@ -50,21 +54,30 @@ const TextField = ({
5054
disabled={isDisabled}
5155
id={id}
5256
name={name}
53-
type="text"
54-
value={value}
5557
onBlur={onBlur}
5658
onChange={onInputChange}
5759
onFocus={onFocus}
60+
ref={inputRef}
61+
type="text"
62+
value={value}
5863
/>
64+
{isTouched && error && (
65+
<ValidationError className={cn(styles.error, errorClassName)}>
66+
{error}
67+
</ValidationError>
68+
)}
5969
</div>
6070
);
6171
};
6272

6373
TextField.propTypes = {
6474
className: PT.string,
75+
error: PT.string,
76+
errorClassName: PT.string,
6577
id: PT.string,
78+
inputRef: PT.oneOfType([PT.object, PT.func]),
6679
isDisabled: PT.bool,
67-
isValid: PT.bool,
80+
isTouched: PT.bool,
6881
label: PT.string,
6982
name: PT.string.isRequired,
7083
onBlur: PT.func,

src/components/TextField/styles.module.scss

+2-2
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@
3838
}
3939

4040
&.invalid {
41-
input,
42-
input:hover {
41+
input.input,
42+
input.input:hover {
4343
border-color: $error-color;
4444
color: $error-text-color;
4545
}

src/components/Tooltip/styles.module.scss

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@import "styles/variables";
2+
13
.container {
24
position: relative;
35
display: inline-flex;
@@ -14,8 +16,12 @@
1416
border-radius: 8px;
1517
padding: 10px 15px;
1618
line-height: 22px;
17-
box-shadow: 0px 5px 25px #c6c6c6;
19+
font-size: $font-size-px;
20+
font-weight: normal;
21+
text-transform: none;
22+
color: $text-color;
1823
background: #fff;
24+
box-shadow: 0px 5px 25px #c6c6c6;
1925

2026
ul {
2127
margin: 0;

src/components/ValidationError/styles.module.scss

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
.container {
2-
margin: 10px 0;
2+
margin: 10px 0 0;
33
border: 1px solid #ffd5d1;
44
padding: 9px 10px;
55
min-height: 40px;

0 commit comments

Comments
 (0)