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

Commit 04d21ec

Browse files
authored
Merge pull request #40 from MadOPcode/dev
Adds more info into payments popover and mirrors state to URL query
2 parents e78f965 + b2f5333 commit 04d21ec

File tree

27 files changed

+688
-205
lines changed

27 files changed

+688
-205
lines changed

src/components/SearchHandleField/index.jsx

+164-30
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,67 @@
1-
import React, { useCallback } from "react";
1+
import React, { useCallback, 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,149 @@ 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+
97+
const loadOptions = useCallback(
98+
throttle(
99+
async (value) => {
100+
setIsLoading(true);
101+
const options = await loadSuggestions(value);
102+
setOptions(options);
103+
setIsLoading(false);
104+
setIsMenuOpen(true);
105+
setIsMenuFocused(options.length && options[0].value === value);
106+
},
107+
300,
108+
{ leading: false }
109+
),
110+
[]
111+
);
112+
113+
const onValueChange = (option, { action }) => {
114+
if (action === "input-change" || action === "select-option") {
115+
setIsMenuFocused(false);
116+
setIsMenuOpen(false);
117+
if (!isMenuFocused) {
118+
onChange(inputValue);
119+
} else if (option) {
44120
onChange(option.value);
45121
}
46-
},
47-
[onChange]
48-
);
122+
} else if (action === "clear") {
123+
setIsMenuFocused(false);
124+
setIsMenuOpen(false);
125+
onChange("");
126+
}
127+
};
49128

50-
const onInputChange = useCallback(
129+
const onInputValueChange = useCallback(
51130
(value, { action }) => {
52131
if (action === "input-change") {
53-
onChange(value);
132+
setIsMenuFocused(false);
133+
setInputValue(value);
134+
onInputChange && onInputChange(value);
135+
loadOptions(value);
54136
}
55137
},
56-
[onChange]
138+
[onInputChange, loadOptions]
57139
);
58140

141+
const onKeyDown = (event) => {
142+
const key = event.key;
143+
if (key === "Enter" || key === "Escape") {
144+
setIsMenuFocused(false);
145+
setIsMenuOpen(false);
146+
if (!isMenuFocused) {
147+
onChange(inputValue);
148+
}
149+
} else if (key === "ArrowDown") {
150+
if (!isMenuFocused) {
151+
event.preventDefault();
152+
event.stopPropagation();
153+
setIsMenuFocused(true);
154+
}
155+
} else if (key === "Backspace") {
156+
if (!inputValue) {
157+
event.preventDefault();
158+
event.stopPropagation();
159+
}
160+
}
161+
};
162+
163+
const onSelectBlur = useCallback(() => {
164+
setIsMenuFocused(false);
165+
setIsMenuOpen(false);
166+
onBlur && onBlur();
167+
}, [onBlur]);
168+
169+
useUpdateEffect(() => {
170+
setInputValue(value);
171+
}, [value]);
172+
59173
return (
60-
<div className={cn(styles.container, styles[size], className)}>
174+
<div
175+
className={cn(
176+
styles.container,
177+
styles[size],
178+
{ [styles.isMenuFocused]: isMenuFocused },
179+
className
180+
)}
181+
>
61182
<span className={styles.icon} />
62-
<AsyncSelect
183+
<Select
63184
className={styles.select}
64185
classNamePrefix="custom"
65186
components={selectComponents}
66187
id={id}
67188
name={name}
68189
isClearable={true}
69190
isSearchable={true}
70-
// menuIsOpen={true} // for debugging
191+
isLoading={isLoading}
192+
isMenuFocused={isMenuFocused}
193+
setIsMenuFocused={setIsMenuFocused}
194+
menuIsOpen={isMenuOpen}
71195
value={null}
72-
inputValue={value}
196+
inputValue={inputValue}
197+
options={options}
73198
onChange={onValueChange}
74-
onInputChange={onInputChange}
75-
openMenuOnClick={false}
199+
onInputChange={onInputValueChange}
200+
onKeyDown={onKeyDown}
201+
onBlur={onSelectBlur}
76202
placeholder={placeholder}
77203
noOptionsMessage={noOptionsMessage}
78204
loadingMessage={loadingMessage}
79-
loadOptions={loadSuggestions}
80-
cacheOptions
81205
/>
82206
</div>
83207
);
84208
};
85209

86-
const loadSuggestions = async (inputVal) => {
210+
const loadSuggestions = async (inputValue) => {
87211
let options = [];
88-
if (inputVal.length < 3) {
212+
if (inputValue.length < 3) {
89213
return options;
90214
}
91215
try {
92-
const res = await getMemberSuggestions(inputVal);
216+
const res = await getMemberSuggestions(inputValue);
93217
const users = res.data.result.content;
218+
let match = null;
94219
for (let i = 0, len = users.length; i < len; i++) {
95220
let value = users[i].handle;
96-
options.push({ value, label: value });
221+
if (value === inputValue) {
222+
match = { value, label: value };
223+
} else {
224+
options.push({ value, label: value });
225+
}
226+
}
227+
if (match) {
228+
options.unshift(match);
97229
}
98230
} catch (error) {
99231
console.error(error);
@@ -108,6 +240,8 @@ SearchHandleField.propTypes = {
108240
size: PT.oneOf(["medium", "small"]),
109241
name: PT.string.isRequired,
110242
onChange: PT.func.isRequired,
243+
onInputChange: PT.func,
244+
onBlur: PT.func,
111245
placeholder: PT.string,
112246
value: PT.oneOfType([PT.number, PT.string]),
113247
};

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"];

src/constants/workPeriods.js

+22-4
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const DATE_FORMAT_API = "YYYY-MM-DD";
1919
export const DATE_FORMAT_UI = "MMM DD, YYYY";
2020

2121
// Field names that are required to be retrieved for display, filtering and sorting.
22-
export const REQUIRED_FIELDS = [
22+
export const API_REQUIRED_FIELDS = [
2323
"id",
2424
"jobId",
2525
"projectId",
@@ -40,15 +40,15 @@ export const REQUIRED_FIELDS = [
4040
];
4141

4242
// Valid parameter names for requests.
43-
export const QUERY_PARAM_NAMES = [
43+
export const API_QUERY_PARAM_NAMES = [
4444
"fields",
4545
"page",
4646
"perPage",
4747
"sortBy",
4848
"sortOrder",
49-
].concat(REQUIRED_FIELDS);
49+
].concat(API_REQUIRED_FIELDS);
5050

51-
export const FIELDS_QUERY = REQUIRED_FIELDS.join(",");
51+
export const API_FIELDS_QUERY = API_REQUIRED_FIELDS.join(",");
5252

5353
export const SORT_BY_DEFAULT = SORT_BY.USER_HANDLE;
5454

@@ -90,6 +90,24 @@ export const API_PAYMENT_STATUS_MAP = (function () {
9090
return obj;
9191
})();
9292

93+
export const API_CHALLENGE_PAYMENT_STATUS_MAP = {
94+
cancelled: PAYMENT_STATUS.CANCELLED,
95+
completed: PAYMENT_STATUS.COMPLETED,
96+
failed: PAYMENT_STATUS.FAILED,
97+
"in-progress": PAYMENT_STATUS.IN_PROGRESS,
98+
scheduled: PAYMENT_STATUS.SCHEDULED,
99+
};
100+
101+
export const URL_QUERY_PARAM_MAP = new Map([
102+
["startDate", "startDate"],
103+
["paymentStatuses", "status"],
104+
["userHandle", "user"],
105+
["criteria", "by"],
106+
["order", "order"],
107+
["pageSize", "perPage"],
108+
["pageNumber", "page"],
109+
]);
110+
93111
export const JOB_NAME_LOADING = "Loading...";
94112
export const JOB_NAME_NONE = "<Job is not assigned>";
95113
export const JOB_NAME_ERROR = "<Error loading job>";

src/constants/workPeriods/paymentStatus.js

+4
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@ export const COMPLETED = "COMPLETED";
33
export const PENDING = "PENDING";
44
export const IN_PROGRESS = "IN_PROGRESS";
55
export const NO_DAYS = "NO_DAYS";
6+
export const SCHEDULED = "SCHEDULED";
7+
export const FAILED = "FAILED";
8+
export const CANCELLED = "CANCELLED";
9+
export const UNDEFINED = "UNDEFINED";

src/root.component.jsx

+8-9
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import store from "store";
55
import { disableSidebarForRoute } from "@topcoder/micro-frontends-navbar-app";
66
import WorkPeriods from "routes/WorkPeriods";
77
import Freelancers from "routes/Freelancers";
8-
import { APP_BASE_PATH } from "./constants";
8+
import {
9+
APP_BASE_PATH,
10+
FREELANCERS_PATH,
11+
WORK_PERIODS_PATH,
12+
} from "./constants";
913
import "styles/global.scss";
1014

1115
export default function Root() {
@@ -16,14 +20,9 @@ export default function Root() {
1620
return (
1721
<Provider store={store}>
1822
<Router>
19-
<Redirect
20-
from={APP_BASE_PATH}
21-
to={`${APP_BASE_PATH}/work-periods`}
22-
exact
23-
noThrow
24-
/>
25-
<WorkPeriods path={`${APP_BASE_PATH}/work-periods`} />
26-
<Freelancers path={`${APP_BASE_PATH}/freelancers`} />
23+
<Redirect from={APP_BASE_PATH} to={WORK_PERIODS_PATH} exact noThrow />
24+
<WorkPeriods path={WORK_PERIODS_PATH} />
25+
<Freelancers path={FREELANCERS_PATH} />
2726
</Router>
2827
</Provider>
2928
);

0 commit comments

Comments
 (0)