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

[PROD] 1.0.1 - Work Period Payments Improvements #44

Merged
merged 22 commits into from
Jun 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e78f965
fix: loading period details UI
maxceem Jun 17, 2021
85974c1
Update URL/restore state from URL work-in-progress
MadOPcode Jun 18, 2021
7179cc7
Merge branch 'dev' of github.com:MadOPcode/micro-frontends-taas-admin…
MadOPcode Jun 18, 2021
c05a8ef
Added more info into period's payments' popover.
MadOPcode Jun 18, 2021
6d52272
Added userHandle to URL query.
MadOPcode Jun 18, 2021
8adce2a
Fixed: URL with no user handle did not properly update state on histo…
MadOPcode Jun 18, 2021
e3adca3
Previous commit about user handle caused unneeded state update. Fixed.
MadOPcode Jun 18, 2021
8759232
Added search handle field value selection on Enter and other behavior.
MadOPcode Jun 19, 2021
b2f5333
Fixed issues with mouse select in search handle field.
MadOPcode Jun 19, 2021
04d21ec
Merge pull request #40 from MadOPcode/dev
maxceem Jun 19, 2021
eb8661d
Fixes for search handle field behavior.
MadOPcode Jun 19, 2021
b6025a7
Merge pull request #41 from MadOPcode/dev
maxceem Jun 19, 2021
57a09d2
Fixed: sorting wasn't loaded correctly from URL.
MadOPcode Jun 20, 2021
53c8093
Added payments popup to main table and payment error popup.
MadOPcode Jun 21, 2021
cf03d81
Added filtering by failed payments.
MadOPcode Jun 21, 2021
d01eced
Fixed: issues with sorting loaded from URL.
MadOPcode Jun 21, 2021
3dbd970
Merge pull request #42 from MadOPcode/dev
maxceem Jun 21, 2021
d8fec86
feat: smaller error circle
maxceem Jun 21, 2021
7cfbc12
Fixes for popups and pagination.
MadOPcode Jun 21, 2021
cdde4e3
Merge branch 'dev' of github.com:MadOPcode/micro-frontends-taas-admin…
MadOPcode Jun 21, 2021
c2100b0
Fixed: popups inside period details were overlaying container.
MadOPcode Jun 21, 2021
6de41de
Merge pull request #43 from MadOPcode/dev
maxceem Jun 21, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions src/components/Popup/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, { useState } from "react";
import { usePopper } from "react-popper";
import PT from "prop-types";
import cn from "classnames";
import compStyles from "./styles.module.scss";

/**
* Displays a popup near the reference element.
*
* @param {Object} props component properties
* @param {any} [props.children] child nodes
* @param {string} [props.className] class name to be added to root element
* @param {Object} props.referenceElement reference element
* @param {'absolute'|'fixed'} [props.strategy] positioning strategy
* @returns {JSX.Element}
*/
const Popup = ({
children,
className,
referenceElement,
strategy = "absolute",
}) => {
const [popperElement, setPopperElement] = useState(null);
const [arrowElement, setArrowElement] = useState(null);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: "bottom",
strategy,
modifiers: [
{ name: "arrow", options: { element: arrowElement, padding: 10 } },
{ name: "offset", options: { offset: [0, 5] } },
{ name: "preventOverflow", options: { padding: 15 } },
],
});

return (
<div
ref={setPopperElement}
className={cn(compStyles.container, styles.container, className)}
style={styles.popper}
{...attributes.popper}
>
{children}
<div className="popup-arrow" ref={setArrowElement} style={styles.arrow} />
</div>
);
};

Popup.propTypes = {
children: PT.node,
className: PT.string,
referenceElement: PT.object.isRequired,
strategy: PT.oneOf(["absolute", "fixed"]),
};

export default Popup;
13 changes: 13 additions & 0 deletions src/components/Popup/styles.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@import "styles/variables";

.container {
z-index: 10;
border-radius: 8px;
padding: $popover-padding;
background: #fff;
box-shadow: $popover-box-shadow;

:global(.popup-arrow) {
display: none;
}
}
209 changes: 178 additions & 31 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, useRef, 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,162 @@ 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 isChangeAppliedRef = useRef(false);

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

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

const onKeyDown = (event) => {
const key = event.key;
if (key === "Enter" || key === "Escape") {
if (!isMenuFocused || isLoading) {
isChangeAppliedRef.current = true;
setIsMenuFocused(false);
setIsMenuOpen(false);
setIsLoading(false);
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 = () => {
setIsMenuFocused(false);
setIsMenuOpen(false);
onChange(inputValue);
onBlur && onBlur();
};

const loadOptions = useCallback(
throttle(
async (value) => {
if (!isChangeAppliedRef.current) {
setIsLoading(true);
const options = await loadSuggestions(value);
if (!isChangeAppliedRef.current) {
setOptions(options);
setIsLoading(false);
setIsMenuOpen(true);
}
}
},
300,
{ leading: false }
),
[]
);

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

useUpdateEffect(() => {
loadOptions(inputValue);
}, [inputValue]);

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 users = res.data.result.content;
const res = await getMemberSuggestions(inputValue);
const users = res.data.result.content.slice(0, 100);
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 +253,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
Loading