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

Commit 49b6589

Browse files
Merge pull request #83 from topcoder-platform/dev
[PROD] Next Release
2 parents cd42c7d + 73252ea commit 49b6589

File tree

30 files changed

+2093
-51
lines changed

30 files changed

+2093
-51
lines changed
Lines changed: 2 additions & 0 deletions
Loading
Lines changed: 22 additions & 0 deletions
Loading

src/assets/images/icon-search.svg

Lines changed: 18 additions & 0 deletions
Loading

src/components/Icons/Roles/index.jsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React from "react";
2+
import PT from "prop-types";
3+
import cn from "classnames";
4+
import IconWrapper from "components/IconWrapper";
5+
import IconRoleManagement from "../../../assets/images/icon-menu-item-roles.svg";
6+
import styles from "./styles.module.scss";
7+
8+
/**
9+
* Displays a "role management" icon used in navigation menu.
10+
*
11+
* @param {Object} props component props
12+
* @param {string} [props.className] class name added to root element
13+
* @param {boolean} [props.isActive] a flag indicating whether the icon is active
14+
* @returns {JSX.Element}
15+
*/
16+
const Roles = ({ className, isActive = false }) => (
17+
<IconWrapper
18+
className={cn(styles.container, className, { [styles.isActive]: isActive })}
19+
>
20+
<IconRoleManagement />
21+
</IconWrapper>
22+
);
23+
24+
Roles.propTypes = {
25+
className: PT.string,
26+
isActive: PT.bool,
27+
};
28+
29+
export default Roles;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.container {
2+
svg {
3+
display: block;
4+
width: auto;
5+
height: 100%;
6+
7+
path {
8+
fill: #7f7f7f;
9+
}
10+
}
11+
12+
&.isActive {
13+
svg {
14+
path {
15+
fill: #06d6a0;
16+
}
17+
}
18+
}
19+
}

src/components/IntegerField/index.jsx

Lines changed: 95 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import React from "react";
1+
import React, { useMemo } from "react";
22
import PT from "prop-types";
33
import cn from "classnames";
4+
import IconExclamationMark from "components/Icons/ExclamationMarkCircled";
5+
import Popover from "components/Popover";
46
import styles from "./styles.module.scss";
57

68
/**
@@ -9,58 +11,119 @@ import styles from "./styles.module.scss";
911
* @param {Object} props component properties
1012
* @param {string} [props.className] class name to be added to root element
1113
* @param {boolean} [props.isDisabled] if the field is disabled
14+
* @param {boolean} [props.readOnly] if the field is readOnly
15+
* @param {boolean} [props.displayButtons] whether to display +/- buttons
1216
* @param {string} props.name field's name
1317
* @param {number} props.value field's value
1418
* @param {number} [props.maxValue] maximum allowed value
1519
* @param {number} [props.minValue] minimum allowed value
16-
* @param {(v: number) => void} props.onChange
20+
* @param {(v: number) => void} [props.onChange]
21+
* @param {(v: string) => void} [props.onInputChange]
1722
* @returns {JSX.Element}
1823
*/
1924
const IntegerField = ({
2025
className,
2126
isDisabled = false,
27+
readOnly = true,
28+
displayButtons = true,
2229
name,
30+
onInputChange,
2331
onChange,
2432
value,
2533
maxValue = Infinity,
2634
minValue = -Infinity,
27-
}) => (
28-
<div className={cn(styles.container, className)}>
29-
<input
30-
disabled={isDisabled}
31-
readOnly
32-
className={styles.input}
33-
name={name}
34-
value={value}
35-
/>
36-
<button
37-
className={styles.btnMinus}
38-
onClick={(event) => {
39-
event.stopPropagation();
40-
if (!isDisabled) {
41-
onChange(Math.max(value - 1, minValue));
42-
}
43-
}}
44-
/>
45-
<button
46-
className={styles.btnPlus}
47-
onClick={(event) => {
48-
event.stopPropagation();
49-
if (!isDisabled) {
50-
onChange(Math.min(+value + 1, maxValue));
51-
}
52-
}}
53-
/>
54-
</div>
55-
);
35+
}) => {
36+
const isInvalid = useMemo(
37+
() =>
38+
!!value &&
39+
(isNaN(value) ||
40+
!Number.isInteger(+value) ||
41+
+value > maxValue ||
42+
+value < minValue),
43+
[value, minValue, maxValue]
44+
);
45+
46+
const errorPopupContent = useMemo(() => {
47+
if (value && (isNaN(value) || !Number.isInteger(+value))) {
48+
return <>You must enter a valid integer.</>;
49+
}
50+
if (+value > maxValue) {
51+
return (
52+
<>
53+
You must enter an integer less than or equal to{" "}
54+
<strong>{maxValue}</strong>.
55+
</>
56+
);
57+
}
58+
if (+value < minValue) {
59+
return (
60+
<>
61+
You must enter an integer greater than or equal to{" "}
62+
<strong>{minValue}</strong>.
63+
</>
64+
);
65+
}
66+
}, [value, minValue, maxValue]);
67+
68+
return (
69+
<div className={cn(styles.container, className)}>
70+
{isInvalid && (
71+
<Popover
72+
className={styles.popup}
73+
stopClickPropagation={true}
74+
content={errorPopupContent}
75+
strategy="fixed"
76+
>
77+
<IconExclamationMark className={styles.icon} />
78+
</Popover>
79+
)}
80+
<input
81+
type="number"
82+
onChange={(event) => onInputChange && onInputChange(event.target.value)}
83+
disabled={isDisabled}
84+
readOnly={readOnly}
85+
className={cn(styles.input, {
86+
error: isInvalid,
87+
})}
88+
name={name}
89+
value={value}
90+
/>
91+
{displayButtons && (
92+
<>
93+
<button
94+
className={styles.btnMinus}
95+
onClick={(event) => {
96+
event.stopPropagation();
97+
if (!isDisabled) {
98+
onChange(Math.max(value - 1, minValue));
99+
}
100+
}}
101+
/>
102+
<button
103+
className={styles.btnPlus}
104+
onClick={(event) => {
105+
event.stopPropagation();
106+
if (!isDisabled) {
107+
onChange(Math.min(+value + 1, maxValue));
108+
}
109+
}}
110+
/>
111+
</>
112+
)}
113+
</div>
114+
);
115+
};
56116

57117
IntegerField.propTypes = {
58118
className: PT.string,
59119
isDisabled: PT.bool,
120+
readOnly: PT.bool,
121+
displayButtons: PT.bool,
60122
name: PT.string.isRequired,
61123
maxValue: PT.number,
62124
minValue: PT.number,
63-
onChange: PT.func.isRequired,
125+
onChange: PT.func,
126+
onInputChange: PT.func,
64127
value: PT.number.isRequired,
65128
};
66129

src/components/IntegerField/styles.module.scss

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ input.input {
1919
outline: none !important;
2020
box-shadow: none !important;
2121
text-align: center;
22+
appearance: textfield;
23+
&::-webkit-outer-spin-button, &::-webkit-inner-spin-button {
24+
-webkit-appearance: none;
25+
margin: 0;
26+
}
2227

2328
&:disabled {
2429
border-color: $control-disabled-border-color;
@@ -94,3 +99,17 @@ input.input {
9499
height: 9px;
95100
}
96101
}
102+
103+
.popup {
104+
margin-right: 5px;
105+
max-width: 400px;
106+
max-height: 200px;
107+
line-height: $line-height-px;
108+
white-space: normal;
109+
}
110+
111+
.icon {
112+
padding-top: 1px;
113+
width: 15px;
114+
height: 15px;
115+
}

src/components/Sidebar/index.jsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import NavMenu from "components/NavMenu";
55
import styles from "./styles.module.scss";
66
import WorkPeriods from "components/Icons/WorkPeriods";
77
import Freelancers from "components/Icons/Freelancers";
8+
import Roles from "components/Icons/Roles";
89
import { APP_BASE_PATH } from "../../constants";
910

1011
/**
@@ -38,6 +39,11 @@ const NAV_ITEMS = [
3839
label: "Freelancers",
3940
path: `${APP_BASE_PATH}/freelancers`,
4041
},
42+
{
43+
icon: Roles,
44+
label: "Roles",
45+
path: `${APP_BASE_PATH}/roles`,
46+
},
4147
];
4248

4349
export default Sidebar;

0 commit comments

Comments
 (0)