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

Adds more info into payments popover and mirrors state to URL query #40

Merged
merged 8 commits into from
Jun 19, 2021
194 changes: 164 additions & 30 deletions src/components/SearchHandleField/index.jsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,67 @@
import React, { useCallback } from "react";
import React, { useCallback, useState } from "react";
import PT from "prop-types";
import cn from "classnames";
import AsyncSelect from "react-select/async";
import throttle from "lodash/throttle";
import Select, { components } from "react-select";
import { getMemberSuggestions } from "services/teams";
import { useUpdateEffect } from "utils/hooks";
import styles from "./styles.module.scss";

const loadingMessage = () => "Loading...";

const noOptionsMessage = () => "No suggestions";

function MenuList(props) {
let focusedOption = props.focusedOption;
focusedOption = props.selectProps.isMenuFocused
? focusedOption
: props.getValue()[0];
const setIsMenuFocused = props.selectProps.setIsMenuFocused;

const onMouseEnter = useCallback(() => {
setIsMenuFocused(true);
}, [setIsMenuFocused]);

return (
<div className={styles.menuList} onMouseEnter={onMouseEnter}>
<components.MenuList {...props} focusedOption={focusedOption} />
</div>
);
}
MenuList.propTypes = {
focusedOption: PT.object,
getValue: PT.func,
selectProps: PT.shape({
isMenuFocused: PT.oneOfType([PT.bool, PT.number]),
setIsMenuFocused: PT.func,
}),
};

function Option(props) {
return (
<components.Option
{...props}
isFocused={props.selectProps.isMenuFocused && props.isFocused}
isSelected={!props.selectProps.isMenuFocused && props.isSelected}
/>
);
}
Option.propTypes = {
isFocused: PT.bool,
isSelected: PT.bool,
selectProps: PT.shape({
isMenuFocused: PT.oneOfType([PT.bool, PT.number]),
}),
};

const selectComponents = {
DropdownIndicator: () => null,
ClearIndicator: () => null,
IndicatorSeparator: () => null,
MenuList,
Option,
};

const loadingMessage = () => "Loading...";

const noOptionsMessage = () => "No suggestions";

/**
* Displays search input field.
*
Expand All @@ -23,7 +71,9 @@ const noOptionsMessage = () => "No suggestions";
* @param {string} props.placeholder placeholder text
* @param {string} props.name name for input element
* @param {'medium'|'small'} [props.size] field size
* @param {function} props.onChange function called when input value changes
* @param {function} props.onChange function called when value changes
* @param {function} [props.onInputChange] function called when input value changes
* @param {function} [props.onBlur] function called on input blur
* @param {string} props.value input value
* @returns {JSX.Element}
*/
Expand All @@ -33,67 +83,149 @@ const SearchHandleField = ({
name,
size = "medium",
onChange,
onInputChange,
onBlur,
placeholder,
value,
}) => {
const onValueChange = useCallback(
(option, { action }) => {
if (action === "clear") {
onChange("");
} else {
const [inputValue, setInputValue] = useState(value);
const [isLoading, setIsLoading] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isMenuFocused, setIsMenuFocused] = useState(false);
const [options, setOptions] = useState([]);

const loadOptions = useCallback(
throttle(
async (value) => {
setIsLoading(true);
const options = await loadSuggestions(value);
setOptions(options);
setIsLoading(false);
setIsMenuOpen(true);
setIsMenuFocused(options.length && options[0].value === value);
},
300,
{ leading: false }
),
[]
);

const onValueChange = (option, { action }) => {
if (action === "input-change" || action === "select-option") {
setIsMenuFocused(false);
setIsMenuOpen(false);
if (!isMenuFocused) {
onChange(inputValue);
} else if (option) {
onChange(option.value);
}
},
[onChange]
);
} else if (action === "clear") {
setIsMenuFocused(false);
setIsMenuOpen(false);
onChange("");
}
};

const onInputChange = useCallback(
const onInputValueChange = useCallback(
(value, { action }) => {
if (action === "input-change") {
onChange(value);
setIsMenuFocused(false);
setInputValue(value);
onInputChange && onInputChange(value);
loadOptions(value);
}
},
[onChange]
[onInputChange, loadOptions]
);

const onKeyDown = (event) => {
const key = event.key;
if (key === "Enter" || key === "Escape") {
setIsMenuFocused(false);
setIsMenuOpen(false);
if (!isMenuFocused) {
onChange(inputValue);
}
} else if (key === "ArrowDown") {
if (!isMenuFocused) {
event.preventDefault();
event.stopPropagation();
setIsMenuFocused(true);
}
} else if (key === "Backspace") {
if (!inputValue) {
event.preventDefault();
event.stopPropagation();
}
}
};

const onSelectBlur = useCallback(() => {
setIsMenuFocused(false);
setIsMenuOpen(false);
onBlur && onBlur();
}, [onBlur]);

useUpdateEffect(() => {
setInputValue(value);
}, [value]);

return (
<div className={cn(styles.container, styles[size], className)}>
<div
className={cn(
styles.container,
styles[size],
{ [styles.isMenuFocused]: isMenuFocused },
className
)}
>
<span className={styles.icon} />
<AsyncSelect
<Select
className={styles.select}
classNamePrefix="custom"
components={selectComponents}
id={id}
name={name}
isClearable={true}
isSearchable={true}
// menuIsOpen={true} // for debugging
isLoading={isLoading}
isMenuFocused={isMenuFocused}
setIsMenuFocused={setIsMenuFocused}
menuIsOpen={isMenuOpen}
value={null}
inputValue={value}
inputValue={inputValue}
options={options}
onChange={onValueChange}
onInputChange={onInputChange}
openMenuOnClick={false}
onInputChange={onInputValueChange}
onKeyDown={onKeyDown}
onBlur={onSelectBlur}
placeholder={placeholder}
noOptionsMessage={noOptionsMessage}
loadingMessage={loadingMessage}
loadOptions={loadSuggestions}
cacheOptions
/>
</div>
);
};

const loadSuggestions = async (inputVal) => {
const loadSuggestions = async (inputValue) => {
let options = [];
if (inputVal.length < 3) {
if (inputValue.length < 3) {
return options;
}
try {
const res = await getMemberSuggestions(inputVal);
const res = await getMemberSuggestions(inputValue);
const users = res.data.result.content;
let match = null;
for (let i = 0, len = users.length; i < len; i++) {
let value = users[i].handle;
options.push({ value, label: value });
if (value === inputValue) {
match = { value, label: value };
} else {
options.push({ value, label: value });
}
}
if (match) {
options.unshift(match);
}
} catch (error) {
console.error(error);
Expand All @@ -108,6 +240,8 @@ SearchHandleField.propTypes = {
size: PT.oneOf(["medium", "small"]),
name: PT.string.isRequired,
onChange: PT.func.isRequired,
onInputChange: PT.func,
onBlur: PT.func,
placeholder: PT.string,
value: PT.oneOfType([PT.number, PT.string]),
};
Expand Down
15 changes: 15 additions & 0 deletions src/components/SearchHandleField/styles.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ input.input {
}
}

.menuList {
display: block;
}

.select {
z-index: 1;
position: relative;
Expand Down Expand Up @@ -157,6 +161,17 @@ input.input {
}
}

:global(.custom__option--is-selected) {
background-color: $primary-text-color !important;
color: #fff;
}

:global(.custom__clear-indicator) {
display: none;
}
}

.isMenuFocused {
:global(.custom__option--is-focused) {
background-color: $primary-text-color !important;
color: #fff;
Expand Down
4 changes: 4 additions & 0 deletions src/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ export { PLATFORM_WEBSITE_URL, TOPCODER_WEBSITE_URL };

export const APP_BASE_PATH = "/taas-admin";

export const WORK_PERIODS_PATH = `${APP_BASE_PATH}/work-periods`;

export const FREELANCERS_PATH = `${APP_BASE_PATH}/freelancers`;

export const TAAS_BASE_PATH = "/taas";

export const ADMIN_ROLES = ["bookingmanager", "administrator"];
Expand Down
26 changes: 22 additions & 4 deletions src/constants/workPeriods.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const DATE_FORMAT_API = "YYYY-MM-DD";
export const DATE_FORMAT_UI = "MMM DD, YYYY";

// Field names that are required to be retrieved for display, filtering and sorting.
export const REQUIRED_FIELDS = [
export const API_REQUIRED_FIELDS = [
"id",
"jobId",
"projectId",
Expand All @@ -40,15 +40,15 @@ export const REQUIRED_FIELDS = [
];

// Valid parameter names for requests.
export const QUERY_PARAM_NAMES = [
export const API_QUERY_PARAM_NAMES = [
"fields",
"page",
"perPage",
"sortBy",
"sortOrder",
].concat(REQUIRED_FIELDS);
].concat(API_REQUIRED_FIELDS);

export const FIELDS_QUERY = REQUIRED_FIELDS.join(",");
export const API_FIELDS_QUERY = API_REQUIRED_FIELDS.join(",");

export const SORT_BY_DEFAULT = SORT_BY.USER_HANDLE;

Expand Down Expand Up @@ -90,6 +90,24 @@ export const API_PAYMENT_STATUS_MAP = (function () {
return obj;
})();

export const API_CHALLENGE_PAYMENT_STATUS_MAP = {
cancelled: PAYMENT_STATUS.CANCELLED,
completed: PAYMENT_STATUS.COMPLETED,
failed: PAYMENT_STATUS.FAILED,
"in-progress": PAYMENT_STATUS.IN_PROGRESS,
scheduled: PAYMENT_STATUS.SCHEDULED,
};

export const URL_QUERY_PARAM_MAP = new Map([
["startDate", "startDate"],
["paymentStatuses", "status"],
["userHandle", "user"],
["criteria", "by"],
["order", "order"],
["pageSize", "perPage"],
["pageNumber", "page"],
]);

export const JOB_NAME_LOADING = "Loading...";
export const JOB_NAME_NONE = "<Job is not assigned>";
export const JOB_NAME_ERROR = "<Error loading job>";
Expand Down
4 changes: 4 additions & 0 deletions src/constants/workPeriods/paymentStatus.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ export const COMPLETED = "COMPLETED";
export const PENDING = "PENDING";
export const IN_PROGRESS = "IN_PROGRESS";
export const NO_DAYS = "NO_DAYS";
export const SCHEDULED = "SCHEDULED";
export const FAILED = "FAILED";
export const CANCELLED = "CANCELLED";
export const UNDEFINED = "UNDEFINED";
17 changes: 8 additions & 9 deletions src/root.component.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import store from "store";
import { disableSidebarForRoute } from "@topcoder/micro-frontends-navbar-app";
import WorkPeriods from "routes/WorkPeriods";
import Freelancers from "routes/Freelancers";
import { APP_BASE_PATH } from "./constants";
import {
APP_BASE_PATH,
FREELANCERS_PATH,
WORK_PERIODS_PATH,
} from "./constants";
import "styles/global.scss";

export default function Root() {
Expand All @@ -16,14 +20,9 @@ export default function Root() {
return (
<Provider store={store}>
<Router>
<Redirect
from={APP_BASE_PATH}
to={`${APP_BASE_PATH}/work-periods`}
exact
noThrow
/>
<WorkPeriods path={`${APP_BASE_PATH}/work-periods`} />
<Freelancers path={`${APP_BASE_PATH}/freelancers`} />
<Redirect from={APP_BASE_PATH} to={WORK_PERIODS_PATH} exact noThrow />
<WorkPeriods path={WORK_PERIODS_PATH} />
<Freelancers path={FREELANCERS_PATH} />
</Router>
</Provider>
);
Expand Down
Loading