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

Commit 6462a06

Browse files
Merge pull request #44 from topcoder-platform/dev
[PROD] 1.0.1 - Work Period Payments Improvements
2 parents 84b5f56 + 6de41de commit 6462a06

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1361
-462
lines changed

src/components/Popup/index.jsx

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import React, { 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 popup near the reference element.
9+
*
10+
* @param {Object} props component properties
11+
* @param {any} [props.children] child nodes
12+
* @param {string} [props.className] class name to be added to root element
13+
* @param {Object} props.referenceElement reference element
14+
* @param {'absolute'|'fixed'} [props.strategy] positioning strategy
15+
* @returns {JSX.Element}
16+
*/
17+
const Popup = ({
18+
children,
19+
className,
20+
referenceElement,
21+
strategy = "absolute",
22+
}) => {
23+
const [popperElement, setPopperElement] = useState(null);
24+
const [arrowElement, setArrowElement] = useState(null);
25+
const { styles, attributes } = usePopper(referenceElement, popperElement, {
26+
placement: "bottom",
27+
strategy,
28+
modifiers: [
29+
{ name: "arrow", options: { element: arrowElement, padding: 10 } },
30+
{ name: "offset", options: { offset: [0, 5] } },
31+
{ name: "preventOverflow", options: { padding: 15 } },
32+
],
33+
});
34+
35+
return (
36+
<div
37+
ref={setPopperElement}
38+
className={cn(compStyles.container, styles.container, className)}
39+
style={styles.popper}
40+
{...attributes.popper}
41+
>
42+
{children}
43+
<div className="popup-arrow" ref={setArrowElement} style={styles.arrow} />
44+
</div>
45+
);
46+
};
47+
48+
Popup.propTypes = {
49+
children: PT.node,
50+
className: PT.string,
51+
referenceElement: PT.object.isRequired,
52+
strategy: PT.oneOf(["absolute", "fixed"]),
53+
};
54+
55+
export default Popup;
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
@import "styles/variables";
2+
3+
.container {
4+
z-index: 10;
5+
border-radius: 8px;
6+
padding: $popover-padding;
7+
background: #fff;
8+
box-shadow: $popover-box-shadow;
9+
10+
:global(.popup-arrow) {
11+
display: none;
12+
}
13+
}

src/components/SearchHandleField/index.jsx

+178-31
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,67 @@
1-
import React, { useCallback } from "react";
1+
import React, { useCallback, useRef, useState } from "react";
22
import PT from "prop-types";
33
import cn from "classnames";
4-
import AsyncSelect from "react-select/async";
4+
import throttle from "lodash/throttle";
5+
import Select, { components } from "react-select";
56
import { getMemberSuggestions } from "services/teams";
7+
import { useUpdateEffect } from "utils/hooks";
68
import styles from "./styles.module.scss";
79

10+
const loadingMessage = () => "Loading...";
11+
12+
const noOptionsMessage = () => "No suggestions";
13+
14+
function MenuList(props) {
15+
let focusedOption = props.focusedOption;
16+
focusedOption = props.selectProps.isMenuFocused
17+
? focusedOption
18+
: props.getValue()[0];
19+
const setIsMenuFocused = props.selectProps.setIsMenuFocused;
20+
21+
const onMouseEnter = useCallback(() => {
22+
setIsMenuFocused(true);
23+
}, [setIsMenuFocused]);
24+
25+
return (
26+
<div className={styles.menuList} onMouseEnter={onMouseEnter}>
27+
<components.MenuList {...props} focusedOption={focusedOption} />
28+
</div>
29+
);
30+
}
31+
MenuList.propTypes = {
32+
focusedOption: PT.object,
33+
getValue: PT.func,
34+
selectProps: PT.shape({
35+
isMenuFocused: PT.oneOfType([PT.bool, PT.number]),
36+
setIsMenuFocused: PT.func,
37+
}),
38+
};
39+
40+
function Option(props) {
41+
return (
42+
<components.Option
43+
{...props}
44+
isFocused={props.selectProps.isMenuFocused && props.isFocused}
45+
isSelected={!props.selectProps.isMenuFocused && props.isSelected}
46+
/>
47+
);
48+
}
49+
Option.propTypes = {
50+
isFocused: PT.bool,
51+
isSelected: PT.bool,
52+
selectProps: PT.shape({
53+
isMenuFocused: PT.oneOfType([PT.bool, PT.number]),
54+
}),
55+
};
56+
857
const selectComponents = {
958
DropdownIndicator: () => null,
59+
ClearIndicator: () => null,
1060
IndicatorSeparator: () => null,
61+
MenuList,
62+
Option,
1163
};
1264

13-
const loadingMessage = () => "Loading...";
14-
15-
const noOptionsMessage = () => "No suggestions";
16-
1765
/**
1866
* Displays search input field.
1967
*
@@ -23,7 +71,9 @@ const noOptionsMessage = () => "No suggestions";
2371
* @param {string} props.placeholder placeholder text
2472
* @param {string} props.name name for input element
2573
* @param {'medium'|'small'} [props.size] field size
26-
* @param {function} props.onChange function called when input value changes
74+
* @param {function} props.onChange function called when value changes
75+
* @param {function} [props.onInputChange] function called when input value changes
76+
* @param {function} [props.onBlur] function called on input blur
2777
* @param {string} props.value input value
2878
* @returns {JSX.Element}
2979
*/
@@ -33,67 +83,162 @@ const SearchHandleField = ({
3383
name,
3484
size = "medium",
3585
onChange,
86+
onInputChange,
87+
onBlur,
3688
placeholder,
3789
value,
3890
}) => {
39-
const onValueChange = useCallback(
40-
(option, { action }) => {
41-
if (action === "clear") {
42-
onChange("");
43-
} else {
91+
const [inputValue, setInputValue] = useState(value);
92+
const [isLoading, setIsLoading] = useState(false);
93+
const [isMenuOpen, setIsMenuOpen] = useState(false);
94+
const [isMenuFocused, setIsMenuFocused] = useState(false);
95+
const [options, setOptions] = useState([]);
96+
const isChangeAppliedRef = useRef(false);
97+
98+
const onValueChange = (option, { action }) => {
99+
if (action === "input-change" || action === "select-option") {
100+
if (isMenuFocused && !isLoading && option) {
101+
isChangeAppliedRef.current = true;
102+
setIsMenuFocused(false);
103+
setIsMenuOpen(false);
104+
setIsLoading(false);
44105
onChange(option.value);
45106
}
46-
},
47-
[onChange]
48-
);
107+
} else if (action === "clear") {
108+
isChangeAppliedRef.current = true;
109+
setIsMenuFocused(false);
110+
setIsMenuOpen(false);
111+
setIsLoading(false);
112+
onChange("");
113+
}
114+
};
49115

50-
const onInputChange = useCallback(
116+
const onInputValueChange = useCallback(
51117
(value, { action }) => {
52118
if (action === "input-change") {
53-
onChange(value);
119+
isChangeAppliedRef.current = false;
120+
setIsMenuFocused(false);
121+
setInputValue(value);
122+
onInputChange && onInputChange(value);
54123
}
55124
},
56-
[onChange]
125+
[onInputChange]
126+
);
127+
128+
const onKeyDown = (event) => {
129+
const key = event.key;
130+
if (key === "Enter" || key === "Escape") {
131+
if (!isMenuFocused || isLoading) {
132+
isChangeAppliedRef.current = true;
133+
setIsMenuFocused(false);
134+
setIsMenuOpen(false);
135+
setIsLoading(false);
136+
onChange(inputValue);
137+
}
138+
} else if (key === "ArrowDown") {
139+
if (!isMenuFocused) {
140+
event.preventDefault();
141+
event.stopPropagation();
142+
setIsMenuFocused(true);
143+
}
144+
} else if (key === "Backspace") {
145+
if (!inputValue) {
146+
event.preventDefault();
147+
event.stopPropagation();
148+
}
149+
}
150+
};
151+
152+
const onSelectBlur = () => {
153+
setIsMenuFocused(false);
154+
setIsMenuOpen(false);
155+
onChange(inputValue);
156+
onBlur && onBlur();
157+
};
158+
159+
const loadOptions = useCallback(
160+
throttle(
161+
async (value) => {
162+
if (!isChangeAppliedRef.current) {
163+
setIsLoading(true);
164+
const options = await loadSuggestions(value);
165+
if (!isChangeAppliedRef.current) {
166+
setOptions(options);
167+
setIsLoading(false);
168+
setIsMenuOpen(true);
169+
}
170+
}
171+
},
172+
300,
173+
{ leading: false }
174+
),
175+
[]
57176
);
58177

178+
useUpdateEffect(() => {
179+
setInputValue(value);
180+
}, [value]);
181+
182+
useUpdateEffect(() => {
183+
loadOptions(inputValue);
184+
}, [inputValue]);
185+
59186
return (
60-
<div className={cn(styles.container, styles[size], className)}>
187+
<div
188+
className={cn(
189+
styles.container,
190+
styles[size],
191+
{ [styles.isMenuFocused]: isMenuFocused },
192+
className
193+
)}
194+
>
61195
<span className={styles.icon} />
62-
<AsyncSelect
196+
<Select
63197
className={styles.select}
64198
classNamePrefix="custom"
65199
components={selectComponents}
66200
id={id}
67201
name={name}
68202
isClearable={true}
69203
isSearchable={true}
70-
// menuIsOpen={true} // for debugging
204+
isLoading={isLoading}
205+
isMenuFocused={isMenuFocused}
206+
setIsMenuFocused={setIsMenuFocused}
207+
menuIsOpen={isMenuOpen}
71208
value={null}
72-
inputValue={value}
209+
inputValue={inputValue}
210+
options={options}
73211
onChange={onValueChange}
74-
onInputChange={onInputChange}
75-
openMenuOnClick={false}
212+
onInputChange={onInputValueChange}
213+
onKeyDown={onKeyDown}
214+
onBlur={onSelectBlur}
76215
placeholder={placeholder}
77216
noOptionsMessage={noOptionsMessage}
78217
loadingMessage={loadingMessage}
79-
loadOptions={loadSuggestions}
80-
cacheOptions
81218
/>
82219
</div>
83220
);
84221
};
85222

86-
const loadSuggestions = async (inputVal) => {
223+
const loadSuggestions = async (inputValue) => {
87224
let options = [];
88-
if (inputVal.length < 3) {
225+
if (inputValue.length < 3) {
89226
return options;
90227
}
91228
try {
92-
const res = await getMemberSuggestions(inputVal);
93-
const users = res.data.result.content;
229+
const res = await getMemberSuggestions(inputValue);
230+
const users = res.data.result.content.slice(0, 100);
231+
let match = null;
94232
for (let i = 0, len = users.length; i < len; i++) {
95233
let value = users[i].handle;
96-
options.push({ value, label: value });
234+
if (value === inputValue) {
235+
match = { value, label: value };
236+
} else {
237+
options.push({ value, label: value });
238+
}
239+
}
240+
if (match) {
241+
options.unshift(match);
97242
}
98243
} catch (error) {
99244
console.error(error);
@@ -108,6 +253,8 @@ SearchHandleField.propTypes = {
108253
size: PT.oneOf(["medium", "small"]),
109254
name: PT.string.isRequired,
110255
onChange: PT.func.isRequired,
256+
onInputChange: PT.func,
257+
onBlur: PT.func,
111258
placeholder: PT.string,
112259
value: PT.oneOfType([PT.number, PT.string]),
113260
};

src/components/SearchHandleField/styles.module.scss

+15
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ input.input {
4141
}
4242
}
4343

44+
.menuList {
45+
display: block;
46+
}
47+
4448
.select {
4549
z-index: 1;
4650
position: relative;
@@ -157,6 +161,17 @@ input.input {
157161
}
158162
}
159163

164+
:global(.custom__option--is-selected) {
165+
background-color: $primary-text-color !important;
166+
color: #fff;
167+
}
168+
169+
:global(.custom__clear-indicator) {
170+
display: none;
171+
}
172+
}
173+
174+
.isMenuFocused {
160175
:global(.custom__option--is-focused) {
161176
background-color: $primary-text-color !important;
162177
color: #fff;

src/constants/index.js

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ export { PLATFORM_WEBSITE_URL, TOPCODER_WEBSITE_URL };
44

55
export const APP_BASE_PATH = "/taas-admin";
66

7+
export const WORK_PERIODS_PATH = `${APP_BASE_PATH}/work-periods`;
8+
9+
export const FREELANCERS_PATH = `${APP_BASE_PATH}/freelancers`;
10+
711
export const TAAS_BASE_PATH = "/taas";
812

913
export const ADMIN_ROLES = ["bookingmanager", "administrator"];

0 commit comments

Comments
 (0)