From 2d68c40b52a893e3886cf71b0f6cfb4df5f11142 Mon Sep 17 00:00:00 2001 From: Oleg Petrov Date: Fri, 6 Aug 2021 23:37:08 +0300 Subject: [PATCH 01/12] Implements #79 #80 #81 #82 --- package.json | 2 +- src/assets/images/icon-arrow-down-narrow.svg | 5 + src/components/ActionsMenu/index.jsx | 202 +++++++++++++++ src/components/ActionsMenu/styles.module.scss | 76 ++++++ src/components/Button/index.jsx | 5 + src/components/IntegerFieldHinted/index.jsx | 121 +++++++++ .../IntegerFieldHinted/styles.module.scss | 128 ++++++++++ src/components/Modal/index.jsx | 128 ++++++---- src/components/SelectField/index.jsx | 9 +- src/components/TextField/index.jsx | 77 ++++++ src/components/TextField/styles.module.scss | 77 ++++++ src/components/Tooltip/index.jsx | 6 +- src/components/ValidationError/index.jsx | 23 ++ .../ValidationError/styles.module.scss | 9 + src/constants/workPeriods.js | 9 + .../components/PaymentActions/index.jsx | 87 +++++++ .../PaymentActions/styles.module.scss | 3 + .../PaymentModalAdditional/index.jsx | 131 ++++++++++ .../PaymentModalAdditional/styles.module.scss | 12 + .../components/PaymentModalCancel/index.jsx | 113 +++++++++ .../PaymentModalCancel/styles.module.scss | 3 + .../components/PaymentModalEdit/index.jsx | 138 +++++++++++ .../PaymentModalEdit/styles.module.scss | 38 +++ .../PaymentModalEditAdditional/index.jsx | 131 ++++++++++ .../styles.module.scss | 32 +++ .../components/PaymentModalUpdateBA/index.jsx | 230 ++++++++++++++++++ .../PaymentModalUpdateBA/styles.module.scss | 68 ++++++ .../components/PaymentTotal/index.jsx | 14 +- .../components/PaymentsList/index.jsx | 11 +- .../components/PaymentsListItem/index.jsx | 19 +- .../components/PeriodActions/index.jsx | 85 +++++++ .../PeriodActions/styles.module.scss | 3 + .../components/PeriodDetails/index.jsx | 4 +- .../components/PeriodItem/index.jsx | 13 +- .../components/PeriodList/index.jsx | 2 +- .../components/PeriodListHead/index.jsx | 1 + .../components/PeriodWorkingDays/index.jsx | 55 ++++- .../PeriodWorkingDays/styles.module.scss | 5 +- .../components/PeriodsHistoryItem/index.jsx | 9 +- src/services/workPeriods.js | 42 +++- src/store/actionTypes/workPeriods.js | 1 + src/store/actions/workPeriods.js | 15 +- src/store/reducers/workPeriods.js | 51 +++- src/store/thunks/workPeriods.js | 22 ++ src/styles/variables/_colors.scss | 8 +- src/utils/hooks.js | 5 +- src/utils/misc.js | 22 +- 47 files changed, 2159 insertions(+), 91 deletions(-) create mode 100644 src/assets/images/icon-arrow-down-narrow.svg create mode 100644 src/components/ActionsMenu/index.jsx create mode 100644 src/components/ActionsMenu/styles.module.scss create mode 100644 src/components/IntegerFieldHinted/index.jsx create mode 100644 src/components/IntegerFieldHinted/styles.module.scss create mode 100644 src/components/TextField/index.jsx create mode 100644 src/components/TextField/styles.module.scss create mode 100644 src/components/ValidationError/index.jsx create mode 100644 src/components/ValidationError/styles.module.scss create mode 100644 src/routes/WorkPeriods/components/PaymentActions/index.jsx create mode 100644 src/routes/WorkPeriods/components/PaymentActions/styles.module.scss create mode 100644 src/routes/WorkPeriods/components/PaymentModalAdditional/index.jsx create mode 100644 src/routes/WorkPeriods/components/PaymentModalAdditional/styles.module.scss create mode 100644 src/routes/WorkPeriods/components/PaymentModalCancel/index.jsx create mode 100644 src/routes/WorkPeriods/components/PaymentModalCancel/styles.module.scss create mode 100644 src/routes/WorkPeriods/components/PaymentModalEdit/index.jsx create mode 100644 src/routes/WorkPeriods/components/PaymentModalEdit/styles.module.scss create mode 100644 src/routes/WorkPeriods/components/PaymentModalEditAdditional/index.jsx create mode 100644 src/routes/WorkPeriods/components/PaymentModalEditAdditional/styles.module.scss create mode 100644 src/routes/WorkPeriods/components/PaymentModalUpdateBA/index.jsx create mode 100644 src/routes/WorkPeriods/components/PaymentModalUpdateBA/styles.module.scss create mode 100644 src/routes/WorkPeriods/components/PeriodActions/index.jsx create mode 100644 src/routes/WorkPeriods/components/PeriodActions/styles.module.scss diff --git a/package.json b/package.json index 00665f0..5fa0747 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "dev-https": "cross-env APPMODE=development webpack-dev-server --https --port 8502 --host 0.0.0.0", "build": "webpack --mode=${APPMODE:-production} --env.config=${APPENV:-prod}", "analyze": "webpack --mode=production --env.analyze=true", - "lint": "eslint src --ext js --ext jsx", + "lint": "eslint ./src --ext .js,.jsx", "format": "prettier --write \"./**\"", "test": "cross-env BABEL_ENV=test jest", "watch-tests": "cross-env BABEL_ENV=test jest --watch", diff --git a/src/assets/images/icon-arrow-down-narrow.svg b/src/assets/images/icon-arrow-down-narrow.svg new file mode 100644 index 0000000..8e87a7e --- /dev/null +++ b/src/assets/images/icon-arrow-down-narrow.svg @@ -0,0 +1,5 @@ + + + + diff --git a/src/components/ActionsMenu/index.jsx b/src/components/ActionsMenu/index.jsx new file mode 100644 index 0000000..5507631 --- /dev/null +++ b/src/components/ActionsMenu/index.jsx @@ -0,0 +1,202 @@ +import React, { useCallback, useMemo, useState } from "react"; +import PT from "prop-types"; +import cn from "classnames"; +import { usePopper } from "react-popper"; +import Button from "components/Button"; +import IconArrowDown from "../../assets/images/icon-arrow-down-narrow.svg"; +import { useClickOutside } from "utils/hooks"; +import { negate, stopPropagation } from "utils/misc"; +import compStyles from "./styles.module.scss"; + +/** + * Displays a clickable button with a menu. + * + * @param {Object} props component properties + * @param {'primary'|'error'|'warning'} [props.handleColor] menu handle color + * @param {'small'|'medium'} [props.handleSize] menu handle size + * @param {Array} props.items menu items + * @param {'absolute'|'fixed'} [props.popupStrategy] popup positioning strategy + * @param {boolean} [props.stopClickPropagation] whether to stop click event propagation + * @returns {JSX.Element} + */ +const ActionsMenu = ({ + handleColor = "primary", + handleSize = "small", + items = [], + popupStrategy = "absolute", + stopClickPropagation = false, +}) => { + const [isOpen, setIsOpen] = useState(false); + const [referenceElement, setReferenceElement] = useState(null); + + const closeMenu = useCallback(() => { + setIsOpen(false); + }, []); + + const toggleMenu = useCallback(() => { + setIsOpen(negate); + }, []); + + const onItemClick = useCallback( + (event) => { + let index = +event.target.dataset.actionIndex; + let item = items[index]; + if (!item || item.disabled || item.separator) { + return; + } + closeMenu(); + item.action?.(); + }, + [items, closeMenu] + ); + + const menuItems = useMemo( + () => + items.map((item, index) => { + if (item.hidden) { + return null; + } else if (item.separator) { + return
; + } else { + return ( +
+ {item.label} +
+ ); + } + }), + [items, onItemClick] + ); + + return ( +
+ + {isOpen && ( + + )} +
+ ); +}; + +ActionsMenu.propTypes = { + handleColor: PT.oneOf(["primary", "error", "warning"]), + handleSize: PT.oneOf(["small", "medium"]), + items: PT.arrayOf( + PT.shape({ + label: PT.string, + action: PT.func, + separator: PT.bool, + hidden: PT.bool, + }) + ), + popupStrategy: PT.oneOf(["absolute", "fixed"]), + stopClickPropagation: PT.bool, +}; + +export default ActionsMenu; + +/** + * Displays a list of provided action items. + * + * @param {Object} props component properties + * @returns {JSX.Element} + */ +const Menu = ({ items, onClickOutside, referenceElement, strategy }) => { + const [popperElement, setPopperElement] = useState(null); + const [arrowElement, setArrowElement] = useState(null); + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: "bottom", + strategy, + modifiers: [ + { + name: "flip", + options: { + fallbackPlacements: ["bottom"], + }, + }, + { + name: "offset", + options: { + // use offset to move the dropdown slightly down + offset: [0, 5], + }, + }, + { + name: "arrow", + // padding should be equal to border-radius of the dropdown + options: { element: arrowElement, padding: 8 }, + }, + { + name: "preventOverflow", + options: { + // padding from browser edges + padding: 16, + }, + }, + { + name: "computeStyles", + options: { + // to fix bug in IE 11 https://github.com/popperjs/popper-core/issues/636 + gpuAcceleration: false, + }, + }, + ], + }); + + useClickOutside(popperElement, onClickOutside, []); + + return ( +
+
{items}
+
+
+ ); +}; + +Menu.propTypes = { + items: PT.array.isRequired, + onClickOutside: PT.func.isRequired, + referenceElement: PT.object, + strategy: PT.oneOf(["absolute", "fixed"]), +}; diff --git a/src/components/ActionsMenu/styles.module.scss b/src/components/ActionsMenu/styles.module.scss new file mode 100644 index 0000000..8cb305e --- /dev/null +++ b/src/components/ActionsMenu/styles.module.scss @@ -0,0 +1,76 @@ +@import "styles/variables"; + +.container { + position: relative; + display: inline-block; +} + +.handle { + display: inline-flex; + align-items: center; +} + +.iconArrowDown { + display: inline-block; + width: 12px; + height: 8px; + margin-left: 8px; +} + +.handleMenuOpen { + .iconArrowDown { + transform: rotate(180deg); + } +} + +.popover { + z-index: 100; + border-radius: 8px; + // min-width: 175px; + background-color: #fff; + box-shadow: 0px 5px 25px #c6c6c6; +} + +.popoverArrow { + top: -9px; + border: 10px solid transparent; + border-top: none; + border-bottom-color: #fff; + width: 0; + height: 0; +} + +.items { + padding: 16px; +} + +.separator { + border-top: 1px solid #e7e7e7; + margin: 5px 0; +} + +.item { + padding: 5px 0; + font-size: 12px; + font-weight: bold; + letter-spacing: 0.8px; + text-align: left; + text-transform: uppercase; + white-space: nowrap; + color: $primary-text-color; + cursor: pointer; +} + +.danger { + color: #ef476f; +} + +.itemDisabled { + color: gray; + opacity: 0.6; + pointer-events: none; +} + +.hidden { + display: none; +} diff --git a/src/components/Button/index.jsx b/src/components/Button/index.jsx index 509cf17..0f65223 100644 --- a/src/components/Button/index.jsx +++ b/src/components/Button/index.jsx @@ -11,6 +11,8 @@ import styles from "./styles.module.scss"; * @param {string} [props.className] class name added to root element * @param {'primary'|'primary-dark'|'primary-light'|'error'|'warning'} [props.color] * button color + * @param {Object|function} [props.innerRef] Ref object or function to accept the + * ref for
+ ); +}; + +IntegerFieldHinted.propTypes = { + className: PT.string, + isDisabled: PT.bool, + isDisabledMinus: PT.bool, + isDisabledPlus: PT.bool, + name: PT.string.isRequired, + maxValue: PT.number, + minValue: PT.number, + maxValueMessage: PT.node, + minValueMessage: PT.node, + onChange: PT.func.isRequired, + stopClickPropagation: PT.bool, + tooltipStrategy: PT.oneOf(["absolute", "fixed"]), + value: PT.number.isRequired, +}; + +export default IntegerFieldHinted; diff --git a/src/components/IntegerFieldHinted/styles.module.scss b/src/components/IntegerFieldHinted/styles.module.scss new file mode 100644 index 0000000..1e96a7f --- /dev/null +++ b/src/components/IntegerFieldHinted/styles.module.scss @@ -0,0 +1,128 @@ +@import "styles/variables"; + +.container { + position: relative; + display: inline-flex; + align-items: center; + border: 1px solid $control-border-color; + border-radius: 6px; + width: 100px; + overflow: hidden; +} + +input.input { + flex: 1 1 0; + margin: 0; + border: none !important; + padding: 2px 0 3px; + height: 28px; + font-size: 14px; + line-height: normal; + background: #fff; + outline: none !important; + box-shadow: none !important; + text-align: center; + + &:disabled { + border-color: $control-disabled-border-color; + background-color: $control-disabled-bg-color; + color: $control-disabled-text-color; + cursor: not-allowed; + + ~ .btnMinus, + ~ .btnPlus { + cursor: not-allowed; + } + } +} + +.btnMinus, +.btnPlus { + position: absolute; + top: 0; + bottom: 0; + margin: auto; + width: 30px; + height: 30px; + + button { + display: block; + position: relative; + border: none; + padding: 0; + width: 100%; + height: 100%; + background: transparent; + outline: none !important; + cursor: pointer; + + &:disabled { + background: #ddd; + opacity: 1; + } + } +} + +.btnMinus { + left: 0; + + button { + &::before { + content: ""; + display: block; + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + margin: auto; + width: 8px; + height: 1px; + background: #7f7f7f; + } + } +} + +.btnPlus { + right: 0; + + button { + &::before, + &::after { + content: ""; + display: block; + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + margin: auto; + background: #7f7f7f; + } + + &::before { + width: 9px; + height: 1px; + } + + &::after { + width: 1px; + height: 9px; + } + } +} + +.tooltip { + max-width: 300px; + white-space: normal; +} + +.tooltipTarget { + display: block; + width: 100%; + height: 100%; + + &.notAllowed { + cursor: not-allowed; + } +} diff --git a/src/components/Modal/index.jsx b/src/components/Modal/index.jsx index 09ea934..f122307 100644 --- a/src/components/Modal/index.jsx +++ b/src/components/Modal/index.jsx @@ -1,5 +1,6 @@ -import React from "react"; +import React, { useCallback, useMemo } from "react"; import PT from "prop-types"; +import cn from "classnames"; import { Modal as ReactModal } from "react-responsive-modal"; import Button from "components/Button"; import IconCross from "../../assets/images/icon-cross-light.svg"; @@ -7,87 +8,122 @@ import { stopImmediatePropagation } from "utils/misc"; import styles from "./styles.module.scss"; import "react-responsive-modal/styles.css"; -const classNames = { - modal: styles.modal, - modalContainer: styles.modalContainer, -}; const closeIcon = ; /** * Displays a modal with Approve- and Dismiss-button and an overlay. * * @param {Object} props component properties + * @param {'primary'|'error'|'warning'} [props.approveColor] color for the approve-button + * @param {boolean} [props.approveDisabled] whether the approve button is disabled * @param {string} [props.approveText] text for Approve-button * @param {Object} props.children elements that will be shown inside modal + * @param {string} [props.className] class name to be added to modal element * @param {?Object} [props.controls] custom controls that will be shown below * modal's contents * @param {string} [props.dismissText] text for Dismiss-button + * @param {boolean} [props.isDisabled] whether the modal is disabled * @param {boolean} props.isOpen whether to show or hide the modal * @param {() => void} [props.onApprove] function called on approve action + * @param {() => void} [props.onClose] function called when the modal is closed + * and the close animation has finished * @param {() => void} props.onDismiss function called on dismiss action * @param {string} [props.title] text for modal title * @returns {JSX.Element} */ const Modal = ({ + approveColor = "warning", + approveDisabled = false, approveText = "Apply", children, + className, controls, dismissText = "Cancel", + isDisabled = false, isOpen, onApprove, + onClose, onDismiss, title, -}) => ( - -
{ + const onAnimationEnd = useCallback(() => { + if (!isOpen) { + onClose?.(); + } + }, [isOpen, onClose]); + + const classNames = useMemo( + () => ({ + modal: cn(styles.modal, className), + modalContainer: styles.modalContainer, + }), + [className] + ); + + return ( + - {title &&
{title}
} -
{children}
- {controls || controls === null ? ( - controls - ) : ( -
- - -
- )} - -
-
-); +
+ {title &&
{title}
} +
{children}
+ {controls || controls === null ? ( + controls + ) : ( +
+ + +
+ )} + +
+ + ); +}; Modal.propTypes = { + approveColor: PT.oneOf(["primary", "error", "warning"]), + approveDisabled: PT.bool, approveText: PT.string, children: PT.node, + className: PT.string, container: PT.element, controls: PT.node, dismissText: PT.string, + isDisabled: PT.bool, isOpen: PT.bool.isRequired, onApprove: PT.func, + onClose: PT.func, onDismiss: PT.func.isRequired, title: PT.string, }; diff --git a/src/components/SelectField/index.jsx b/src/components/SelectField/index.jsx index 99c1e26..b8d257e 100644 --- a/src/components/SelectField/index.jsx +++ b/src/components/SelectField/index.jsx @@ -25,10 +25,11 @@ const selectComponents = { DropdownIndicator, IndicatorSeparator: () => null }; * @param {string} props.id control's id * @param {boolean} [props.isDisabled] whether the control should be disabled * @param {string} [props.label] control's label - * @param {(v: string) => void} props.onChange on change handler + * @param {string} [props.labelClassName] class name to be added to label element + * @param {(v: any) => void} props.onChange on change handler * @param {Object} props.options options for dropdown * @param {'medium'|'small'} [props.size] control's size - * @param {string} props.value control's value + * @param {any} props.value control's value * @returns {JSX.Element} */ const SelectField = ({ @@ -36,6 +37,7 @@ const SelectField = ({ id, isDisabled = false, label, + labelClassName, onChange, options, size = "medium", @@ -69,7 +71,7 @@ const SelectField = ({ )} > {label && ( -