diff --git a/src/components/Page/index.jsx b/src/components/Page/index.jsx index 2a85959..a9b51b0 100644 --- a/src/components/Page/index.jsx +++ b/src/components/Page/index.jsx @@ -2,6 +2,7 @@ import React from "react"; import PT from "prop-types"; import cn from "classnames"; import ReduxToastr from "react-redux-toastr"; +import { TOAST_DEFAULT_TIMEOUT } from "constants/index.js"; import styles from "./styles.module.scss"; /** @@ -16,8 +17,12 @@ const Page = ({ className, children }) => (
{children} diff --git a/src/components/ProjectName/index.jsx b/src/components/ProjectName/index.jsx new file mode 100644 index 0000000..463c531 --- /dev/null +++ b/src/components/ProjectName/index.jsx @@ -0,0 +1,25 @@ +import React, { useContext, useEffect } from "react"; +import PT from "prop-types"; +import cn from "classnames"; +import { ProjectNameContext } from "components/ProjectNameContextProvider"; +import styles from "./styles.module.scss"; + +const ProjectName = ({ className, projectId }) => { + const [getName, fetchName] = useContext(ProjectNameContext); + useEffect(() => { + fetchName(projectId); + }, [fetchName, projectId]); + + return ( + + {getName(projectId) || projectId} + + ); +}; + +ProjectName.propTypes = { + className: PT.string, + projectId: PT.number.isRequired, +}; + +export default ProjectName; diff --git a/src/components/ProjectName/styles.module.scss b/src/components/ProjectName/styles.module.scss new file mode 100644 index 0000000..f098c8d --- /dev/null +++ b/src/components/ProjectName/styles.module.scss @@ -0,0 +1,5 @@ +@import "styles/mixins"; + +.container { + @include roboto-medium; +} diff --git a/src/components/ProjectNameContextProvider/index.jsx b/src/components/ProjectNameContextProvider/index.jsx new file mode 100644 index 0000000..f280662 --- /dev/null +++ b/src/components/ProjectNameContextProvider/index.jsx @@ -0,0 +1,47 @@ +import React, { createContext, useCallback, useState } from "react"; +import PT from "prop-types"; +import { fetchProject } from "services/workPeriods"; +import { increment, noop } from "utils/misc"; + +const names = {}; +const promises = {}; + +const getName = (id) => names[id]; + +export const ProjectNameContext = createContext([ + getName, + (id) => { + `${id}`; + }, +]); + +const ProjectNameProvider = ({ children }) => { + const [, setCount] = useState(Number.MIN_SAFE_INTEGER); + + const fetchName = useCallback((id) => { + if (id in names || id in promises) { + return; + } + promises[id] = fetchProject(id) + .then((data) => { + names[id] = data.name; + setCount(increment); + }) + .catch(noop) + .finally(() => { + delete promises[id]; + }); + }, []); + + return ( + + {children} + + ); +}; + +ProjectNameProvider.propTypes = { + children: PT.node, +}; + +export default ProjectNameProvider; diff --git a/src/components/SearchHandleField/index.jsx b/src/components/SearchHandleField/index.jsx index 38a2168..804b919 100644 --- a/src/components/SearchHandleField/index.jsx +++ b/src/components/SearchHandleField/index.jsx @@ -1,10 +1,8 @@ -import React, { useCallback, useState } from "react"; +import React, { useCallback } from "react"; import PT from "prop-types"; import cn from "classnames"; -import _ from "lodash"; import AsyncSelect from "react-select/async"; import { getMemberSuggestions } from "services/teams"; -// import { getOptionByValue } from "utils/misc"; import styles from "./styles.module.scss"; const selectComponents = { @@ -12,6 +10,10 @@ const selectComponents = { IndicatorSeparator: () => null, }; +const loadingMessage = () => "Loading..."; + +const noOptionsMessage = () => "No suggestions"; + /** * Displays search input field. * @@ -25,20 +27,31 @@ const selectComponents = { * @param {string} props.value input value * @returns {JSX.Element} */ -const SearchAutocomplete = ({ +const SearchHandleField = ({ className, id, + name, size = "medium", onChange, placeholder, value, }) => { - // const option = getOptionByValue(options, value); - const [savedInput, setSavedInput] = useState(""); - const onValueChange = useCallback( - (option) => { - onChange(option.value); + (option, { action }) => { + if (action === "clear") { + onChange(""); + } else { + onChange(option.value); + } + }, + [onChange] + ); + + const onInputChange = useCallback( + (value, { action }) => { + if (action === "input-change") { + onChange(value); + } }, [onChange] ); @@ -51,52 +64,52 @@ const SearchAutocomplete = ({ classNamePrefix="custom" components={selectComponents} id={id} + name={name} + isClearable={true} isSearchable={true} // menuIsOpen={true} // for debugging - // onChange={onOptionChange} - // onMenuOpen={onMenuOpen} - // onMenuClose={onMenuClose} - value={{ value, label: value }} - onInputChange={setSavedInput} - onFocus={() => { - setSavedInput(""); - onChange(savedInput); - }} - placeholder={placeholder} + value={null} + inputValue={value} onChange={onValueChange} - noOptionsMessage={() => "No options"} - loadingMessage={() => "Loading..."} + onInputChange={onInputChange} + openMenuOnClick={false} + placeholder={placeholder} + noOptionsMessage={noOptionsMessage} + loadingMessage={loadingMessage} loadOptions={loadSuggestions} - blurInputOnSelect + cacheOptions />
); }; -const loadSuggestions = (inputVal) => { - return getMemberSuggestions(inputVal) - .then((res) => { - const users = _.get(res, "data.result.content", []); - return users.map((user) => ({ - label: user.handle, - value: user.handle, - })); - }) - .catch(() => { - console.warn("could not get suggestions"); - return []; - }); +const loadSuggestions = async (inputVal) => { + let options = []; + if (inputVal.length < 3) { + return options; + } + try { + const res = await getMemberSuggestions(inputVal); + const users = res.data.result.content; + for (let i = 0, len = users.length; i < len; i++) { + let value = users[i].handle; + options.push({ value, label: value }); + } + } catch (error) { + console.error(error); + console.warn("could not get suggestions"); + } + return options; }; -SearchAutocomplete.propTypes = { +SearchHandleField.propTypes = { className: PT.string, id: PT.string.isRequired, size: PT.oneOf(["medium", "small"]), name: PT.string.isRequired, onChange: PT.func.isRequired, - options: PT.array, placeholder: PT.string, value: PT.oneOfType([PT.number, PT.string]), }; -export default SearchAutocomplete; +export default SearchHandleField; diff --git a/src/components/SearchHandleField/styles.module.scss b/src/components/SearchHandleField/styles.module.scss index e6614cc..266d7d4 100644 --- a/src/components/SearchHandleField/styles.module.scss +++ b/src/components/SearchHandleField/styles.module.scss @@ -2,6 +2,7 @@ @import "styles/mixins"; .container { + position: relative; display: flex; align-items: center; border: 1px solid $control-border-color; @@ -18,6 +19,11 @@ } .icon { + display: block; + position: absolute; + top: 0; + bottom: 0; + left: 0; margin: auto 10px; width: 16px; height: 16px; @@ -42,7 +48,7 @@ input.input { display: flex; margin: 0; border: none !important; - padding: 8px 16px 8px 0; + padding: 8px 16px 8px 36px; line-height: 22px; background: none; outline: none !important; @@ -68,6 +74,7 @@ input.input { margin: 0; border: none; padding: 0; + max-width: none !important; > * { display: flex; @@ -79,6 +86,8 @@ input.input { margin: 0; border: none; padding: 0; + max-width: none !important; + transform: none !important; } input { @@ -86,12 +95,14 @@ input.input { margin: 0 !important; padding: 0 !important; border: none !important; + max-width: none !important; width: auto !important; height: 22px !important; outline: none !important; box-shadow: none !important; line-height: 22px; color: inherit; + opacity: 1 !important; } } @@ -103,7 +114,8 @@ input.input { :global(.custom__input) { flex: 1 1 0; - display: flex; + display: flex !important; + max-width: none !important; } :global(.custom__placeholder) { @@ -115,9 +127,12 @@ input.input { } :global(.custom__menu) { - margin: 1px 0 0; + left: -1px; + right: -1px; + margin: 2px 0 0; border: 1px solid $control-border-color; border-radius: 0; + width: auto; box-shadow: none; } @@ -141,8 +156,8 @@ input.input { } } - :global(.custom__option--is-selected) { - background-color: #229174 !important; + :global(.custom__option--is-focused) { + background-color: $primary-text-color !important; color: #fff; } } diff --git a/src/components/ToastrMessage/index.jsx b/src/components/ToastrMessage/index.jsx index 32aa210..bd51c74 100644 --- a/src/components/ToastrMessage/index.jsx +++ b/src/components/ToastrMessage/index.jsx @@ -1,6 +1,8 @@ import React from "react"; +import { toastr } from "react-redux-toastr"; import PT from "prop-types"; import cn from "classnames"; +import { TOAST_DEFAULT_TIMEOUT } from "constants/index.js"; import styles from "./styles.module.scss"; /** @@ -38,3 +40,19 @@ ToastrMessage.propTypes = { }; export default ToastrMessage; + +/** + * Creates a redux toastr message with the specified type and contents. + * + * @param {string|Object} message + * @param {'info'|'success'|'warning'|'error'} type + */ +export function makeToast(message, type = "error") { + const component = + typeof message === "string" ? ( + + ) : ( + {message} + ); + toastr[type]("", { component, options: { timeOut: TOAST_DEFAULT_TIMEOUT } }); +} diff --git a/src/components/ToastrMessage/styles.module.scss b/src/components/ToastrMessage/styles.module.scss index 606e378..b06041b 100644 --- a/src/components/ToastrMessage/styles.module.scss +++ b/src/components/ToastrMessage/styles.module.scss @@ -1,13 +1,19 @@ +@import "styles/mixins"; + .container { display: block; position: relative; border-radius: 8px; - padding: 14px 27px 15px 64px; + padding: 14px 42px 15px; font-size: 16px; line-height: 26px; text-align: center; color: #fff; + @include desktop { + padding: 14px 64px 15px; + } + a { color: #fff; } @@ -20,7 +26,7 @@ .btnClose { position: absolute; top: 22px; - right: 27px; + right: 22px; margin: 0; border: none; padding: 0; @@ -31,6 +37,10 @@ outline: none !important; cursor: pointer; + @include desktop { + right: 27px; + } + &::before, &::after { content: ""; diff --git a/src/constants/index.js b/src/constants/index.js index 51f91e6..198dbbb 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -16,3 +16,5 @@ export const RESOURCE_BOOKING_STATUS = { CLOSED: "closed", CANCELLED: "cancelled", }; + +export const TOAST_DEFAULT_TIMEOUT = 50000; diff --git a/src/constants/workPeriods.js b/src/constants/workPeriods.js index 5de195d..3caa7e6 100644 --- a/src/constants/workPeriods.js +++ b/src/constants/workPeriods.js @@ -63,6 +63,7 @@ export const SORT_BY_MAP = { export const PAYMENT_STATUS_LABELS = { [PAYMENT_STATUS.CANCELLED]: "Cancelled", + [PAYMENT_STATUS.FAILED]: "Failed", [PAYMENT_STATUS.PAID]: "Paid", [PAYMENT_STATUS.PENDING]: "Pending", [PAYMENT_STATUS.IN_PROGRESS]: "In Progress", @@ -71,6 +72,7 @@ export const PAYMENT_STATUS_LABELS = { export const PAYMENT_STATUS_MAP = { [PAYMENT_STATUS.CANCELLED]: API_PAYMENT_STATUS.CANCELLED, + [PAYMENT_STATUS.FAILED]: API_PAYMENT_STATUS.FAILED, [PAYMENT_STATUS.PAID]: API_PAYMENT_STATUS.COMPLETED, [PAYMENT_STATUS.PENDING]: API_PAYMENT_STATUS.PENDING, [PAYMENT_STATUS.IN_PROGRESS]: API_PAYMENT_STATUS.PARTIALLY_COMPLETED, @@ -85,3 +87,11 @@ export const API_PAYMENT_STATUS_MAP = (function () { } return obj; })(); + +export const JOB_NAME_LOADING = "Loading..."; +export const JOB_NAME_NONE = ""; +export const JOB_NAME_ERROR = ""; + +export const BILLING_ACCOUNTS_LOADING = "Loading..."; +export const BILLING_ACCOUNTS_NONE = ""; +export const BILLING_ACCOUNTS_ERROR = ""; diff --git a/src/constants/workPeriods/apiPaymentStatus.js b/src/constants/workPeriods/apiPaymentStatus.js index 6693d5d..727211a 100644 --- a/src/constants/workPeriods/apiPaymentStatus.js +++ b/src/constants/workPeriods/apiPaymentStatus.js @@ -2,3 +2,4 @@ export const PENDING = "pending"; export const PARTIALLY_COMPLETED = "partially-completed"; export const COMPLETED = "completed"; export const CANCELLED = "cancelled"; +export const FAILED = "failed"; diff --git a/src/constants/workPeriods/paymentStatus.js b/src/constants/workPeriods/paymentStatus.js index 382d12f..cde9072 100644 --- a/src/constants/workPeriods/paymentStatus.js +++ b/src/constants/workPeriods/paymentStatus.js @@ -2,4 +2,5 @@ export const PAID = "PAID"; export const PENDING = "PENDING"; export const IN_PROGRESS = "IN_PROGRESS"; export const CANCELLED = "CANCELLED"; +export const FAILED = "FAILED"; export const UNDEFINED = "UNDEFINED"; diff --git a/src/root.component.jsx b/src/root.component.jsx index 9bec738..fe6e308 100644 --- a/src/root.component.jsx +++ b/src/root.component.jsx @@ -20,6 +20,7 @@ export default function Root() { from={APP_BASE_PATH} to={`${APP_BASE_PATH}/work-periods`} exact + noThrow /> diff --git a/src/routes/WorkPeriods/components/PaymentStatus/styles.module.scss b/src/routes/WorkPeriods/components/PaymentStatus/styles.module.scss index a232c95..162480c 100644 --- a/src/routes/WorkPeriods/components/PaymentStatus/styles.module.scss +++ b/src/routes/WorkPeriods/components/PaymentStatus/styles.module.scss @@ -25,7 +25,8 @@ background: #9d41c9; } -.cancelled { +.cancelled, +.failed { background: #da0000; } diff --git a/src/routes/WorkPeriods/components/PeriodDetails/index.jsx b/src/routes/WorkPeriods/components/PeriodDetails/index.jsx index bf0ce72..146d645 100644 --- a/src/routes/WorkPeriods/components/PeriodDetails/index.jsx +++ b/src/routes/WorkPeriods/components/PeriodDetails/index.jsx @@ -12,7 +12,7 @@ import { hideWorkPeriodDetails, setBillingAccount, setDetailsHidePastPeriods, - setDetailsLockWorkingDays, + // setDetailsLockWorkingDays, } from "store/actions/workPeriods"; import styles from "./styles.module.scss"; import { updateWorkPeriodBillingAccount } from "store/thunks/workPeriods"; @@ -25,22 +25,24 @@ import { useUpdateEffect } from "utils/hooks"; * @param {string} [props.className] class name to be added to root element * @param {Object} props.details working period details object * @param {boolean} props.isDisabled whether the details are disabled + * @param {boolean} props.isFailed whether the payments for the period has failed * @returns {JSX.Element} */ -const PeriodDetails = ({ className, details, isDisabled }) => { +const PeriodDetails = ({ className, details, isDisabled, isFailed }) => { const dispatch = useDispatch(); const { periodId, rbId, jobName, - jobNameIsLoading, + jobNameError, billingAccountId, billingAccounts, - billingAccountsIsLoading, + billingAccountsError, + billingAccountsIsDisabled, periodsVisible, periodsIsLoading, hidePastPeriods, - lockWorkingDays, + // lockWorkingDays, } = details; const onHideDetailsBtnClick = useCallback(() => { @@ -54,12 +56,12 @@ const PeriodDetails = ({ className, details, isDisabled }) => { [dispatch, periodId] ); - const onChangeLockWorkingDays = useCallback( - (lock) => { - dispatch(setDetailsLockWorkingDays(periodId, lock)); - }, - [dispatch, periodId] - ); + // const onChangeLockWorkingDays = useCallback( + // (lock) => { + // dispatch(setDetailsLockWorkingDays(periodId, lock)); + // }, + // [dispatch, periodId] + // ); const onChangeBillingAccount = useCallback( (value) => { @@ -83,18 +85,14 @@ const PeriodDetails = ({ className, details, isDisabled }) => { updateBillingAccount(billingAccountId); }, [billingAccountId]); - const isFailedLoadingJobName = !jobNameIsLoading && jobName === "Error"; - const isFailedLoadingBilAccs = - !billingAccountsIsLoading && - billingAccounts.length === 1 && - billingAccounts[0].value === 0; - const isDisabledBilAccs = - !billingAccountsIsLoading && - billingAccounts.length === 1 && - billingAccounts[0].value === -1; - return ( - + {periodsIsLoading ? (
Loading...
@@ -108,14 +106,14 @@ const PeriodDetails = ({ className, details, isDisabled }) => {
Job Name
- {jobNameIsLoading ? "Loading..." : jobName} + {jobName}
-
+ {/*
Lock Working Days
{ onChange={onChangeLockWorkingDays} isOn={lockWorkingDays} /> -
+
*/}
Billing Account
{ const dispatch = useDispatch(); const filters = useSelector(getWorkPeriodsFilters); - const { /*paymentStatuses,*/ userHandle } = filters; + const { paymentStatuses, userHandle } = filters; const onUserHandleChange = useCallback( (value) => { @@ -38,12 +38,12 @@ const PeriodFilters = ({ className }) => { [dispatch] ); - // const onPaymentStatusesChange = useCallback( - // (statuses) => { - // dispatch(setWorkPeriodsPaymentStatuses(statuses)); - // }, - // [dispatch] - // ); + const onPaymentStatusesChange = useCallback( + (statuses) => { + dispatch(setWorkPeriodsPaymentStatuses(statuses)); + }, + [dispatch] + ); const onClearFilter = useCallback(() => { dispatch(resetWorkPeriodsFilters()); @@ -74,14 +74,14 @@ const PeriodFilters = ({ className }) => { value={userHandle} />
- {/* + - */} +