diff --git a/frontend/src/Activity/Queue/QueueStatus.tsx b/frontend/src/Activity/Queue/QueueStatus.tsx index 31a28f35c2..8a9f973d05 100644 --- a/frontend/src/Activity/Queue/QueueStatus.tsx +++ b/frontend/src/Activity/Queue/QueueStatus.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import Icon, { IconProps } from 'Components/Icon'; +import Icon, { IconKind } from 'Components/Icon'; import Popover from 'Components/Tooltip/Popover'; import { icons, kinds } from 'Helpers/Props'; import { TooltipPosition } from 'Helpers/Props/tooltipPositions'; @@ -61,7 +61,7 @@ function QueueStatus(props: QueueStatusProps) { // status === 'downloading' let iconName = icons.DOWNLOADING; - let iconKind: IconProps['kind'] = kinds.DEFAULT; + let iconKind: IconKind = kinds.DEFAULT; let title = translate('Downloading'); if (status === 'paused') { diff --git a/frontend/src/Components/Form/FormInputButton.tsx b/frontend/src/Components/Form/FormInputButton.tsx index e5149ab14e..1235010ebb 100644 --- a/frontend/src/Components/Form/FormInputButton.tsx +++ b/frontend/src/Components/Form/FormInputButton.tsx @@ -8,12 +8,14 @@ import styles from './FormInputButton.css'; export interface FormInputButtonProps extends ButtonProps { canSpin?: boolean; isLastButton?: boolean; + isSpinning?: boolean; } function FormInputButton({ className = styles.button, canSpin = false, isLastButton = true, + isSpinning = false, kind = kinds.PRIMARY, ...otherProps }: FormInputButtonProps) { @@ -22,6 +24,7 @@ function FormInputButton({ ); diff --git a/frontend/src/Components/Link/SpinnerButton.js b/frontend/src/Components/Link/SpinnerButton.js deleted file mode 100644 index 09f11499b5..0000000000 --- a/frontend/src/Components/Link/SpinnerButton.js +++ /dev/null @@ -1,58 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import Icon from 'Components/Icon'; -import { icons } from 'Helpers/Props'; -import Button from './Button'; -import styles from './SpinnerButton.css'; - -function SpinnerButton(props) { - const { - className, - isSpinning, - isDisabled, - spinnerIcon, - children, - ...otherProps - } = props; - - return ( - - ); -} - -SpinnerButton.propTypes = { - ...Button.Props, - className: PropTypes.string.isRequired, - isSpinning: PropTypes.bool.isRequired, - isDisabled: PropTypes.bool, - spinnerIcon: PropTypes.object.isRequired, - children: PropTypes.node -}; - -SpinnerButton.defaultProps = { - className: styles.button, - spinnerIcon: icons.SPINNER -}; - -export default SpinnerButton; diff --git a/frontend/src/Components/Link/SpinnerButton.tsx b/frontend/src/Components/Link/SpinnerButton.tsx new file mode 100644 index 0000000000..3a5ab2b325 --- /dev/null +++ b/frontend/src/Components/Link/SpinnerButton.tsx @@ -0,0 +1,41 @@ +import classNames from 'classnames'; +import React from 'react'; +import Icon, { IconName } from 'Components/Icon'; +import { icons } from 'Helpers/Props'; +import Button, { ButtonProps } from './Button'; +import styles from './SpinnerButton.css'; + +export interface SpinnerButtonProps extends ButtonProps { + isSpinning: boolean; + isDisabled?: boolean; + spinnerIcon?: IconName; +} + +function SpinnerButton({ + className = styles.button, + isSpinning, + isDisabled, + spinnerIcon = icons.SPINNER, + children, + ...otherProps +}: SpinnerButtonProps) { + return ( + + ); +} + +export default SpinnerButton; diff --git a/frontend/src/Components/Link/SpinnerErrorButton.js b/frontend/src/Components/Link/SpinnerErrorButton.js deleted file mode 100644 index b0f39bc267..0000000000 --- a/frontend/src/Components/Link/SpinnerErrorButton.js +++ /dev/null @@ -1,165 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Icon from 'Components/Icon'; -import SpinnerButton from 'Components/Link/SpinnerButton'; -import { icons, kinds } from 'Helpers/Props'; -import styles from './SpinnerErrorButton.css'; - -function getTestResult(error) { - if (!error) { - return { - wasSuccessful: true, - hasWarning: false, - hasError: false - }; - } - - if (error.status !== 400) { - return { - wasSuccessful: false, - hasWarning: false, - hasError: true - }; - } - - const failures = error.responseJSON; - - const hasWarning = _.some(failures, { isWarning: true }); - const hasError = _.some(failures, (failure) => !failure.isWarning); - - return { - wasSuccessful: false, - hasWarning, - hasError - }; -} - -class SpinnerErrorButton extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this._testResultTimeout = null; - - this.state = { - wasSuccessful: false, - hasWarning: false, - hasError: false - }; - } - - componentDidUpdate(prevProps) { - const { - isSpinning, - error - } = this.props; - - if (prevProps.isSpinning && !isSpinning) { - const testResult = getTestResult(error); - - this.setState(testResult, () => { - const { - wasSuccessful, - hasWarning, - hasError - } = testResult; - - if (wasSuccessful || hasWarning || hasError) { - this._testResultTimeout = setTimeout(this.resetState, 3000); - } - }); - } - } - - componentWillUnmount() { - if (this._testResultTimeout) { - clearTimeout(this._testResultTimeout); - } - } - - // - // Control - - resetState = () => { - this.setState({ - wasSuccessful: false, - hasWarning: false, - hasError: false - }); - }; - - // - // Render - - render() { - const { - kind, - isSpinning, - error, - children, - ...otherProps - } = this.props; - - const { - wasSuccessful, - hasWarning, - hasError - } = this.state; - - const showIcon = wasSuccessful || hasWarning || hasError; - - let iconName = icons.CHECK; - let iconKind = kind === kinds.PRIMARY ? kinds.DEFAULT : kinds.SUCCESS; - - if (hasWarning) { - iconName = icons.WARNING; - iconKind = kinds.WARNING; - } - - if (hasError) { - iconName = icons.DANGER; - iconKind = kinds.DANGER; - } - - return ( - - - { - showIcon && - - - - } - - { - - { - children - } - - } - - - ); - } -} - -SpinnerErrorButton.propTypes = { - kind: PropTypes.oneOf(kinds.all), - isSpinning: PropTypes.bool.isRequired, - error: PropTypes.object, - children: PropTypes.node.isRequired -}; - -export default SpinnerErrorButton; diff --git a/frontend/src/Components/Link/SpinnerErrorButton.tsx b/frontend/src/Components/Link/SpinnerErrorButton.tsx new file mode 100644 index 0000000000..221a12b610 --- /dev/null +++ b/frontend/src/Components/Link/SpinnerErrorButton.tsx @@ -0,0 +1,143 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { Error } from 'App/State/AppSectionState'; +import Icon, { IconKind, IconName } from 'Components/Icon'; +import SpinnerButton, { + SpinnerButtonProps, +} from 'Components/Link/SpinnerButton'; +import usePrevious from 'Helpers/Hooks/usePrevious'; +import { icons } from 'Helpers/Props'; +import { ValidationFailure } from 'typings/pending'; +import styles from './SpinnerErrorButton.css'; + +function getTestResult(error: Error | string | undefined) { + if (!error) { + return { + wasSuccessful: true, + hasWarning: false, + hasError: false, + }; + } + + if (typeof error === 'string' || error.status !== 400) { + return { + wasSuccessful: false, + hasWarning: false, + hasError: true, + }; + } + + const failures = error.responseJSON as ValidationFailure[]; + + const { hasError, hasWarning } = failures.reduce( + (acc, failure) => { + if (failure.isWarning) { + acc.hasWarning = true; + } else { + acc.hasError = true; + } + + return acc; + }, + { hasWarning: false, hasError: false } + ); + + return { + wasSuccessful: false, + hasWarning, + hasError, + }; +} + +interface SpinnerErrorButtonProps extends SpinnerButtonProps { + isSpinning: boolean; + error?: Error | string; + children: React.ReactNode; +} + +function SpinnerErrorButton({ + kind, + isSpinning, + error, + children, + ...otherProps +}: SpinnerErrorButtonProps) { + const wasSpinning = usePrevious(isSpinning); + const updateTimeout = useRef>(); + + const [result, setResult] = useState({ + wasSuccessful: false, + hasWarning: false, + hasError: false, + }); + const { wasSuccessful, hasWarning, hasError } = result; + + const showIcon = wasSuccessful || hasWarning || hasError; + + const { iconName, iconKind } = useMemo<{ + iconName: IconName; + iconKind: IconKind; + }>(() => { + if (hasWarning) { + return { + iconName: icons.WARNING, + iconKind: 'warning', + }; + } + + if (hasError) { + return { + iconName: icons.DANGER, + iconKind: 'danger', + }; + } + + return { + iconName: icons.CHECK, + iconKind: kind === 'primary' ? 'default' : 'success', + }; + }, [kind, hasError, hasWarning]); + + useEffect(() => { + if (wasSpinning && !isSpinning) { + const testResult = getTestResult(error); + + setResult(testResult); + + const { wasSuccessful, hasWarning, hasError } = testResult; + + if (wasSuccessful || hasWarning || hasError) { + updateTimeout.current = setTimeout(() => { + setResult({ + wasSuccessful: false, + hasWarning: false, + hasError: false, + }); + }, 3000); + } + } + }, [isSpinning, wasSpinning, error]); + + useEffect(() => { + return () => { + if (updateTimeout.current) { + clearTimeout(updateTimeout.current); + } + }; + }, []); + + return ( + + + {showIcon && ( + + + + )} + + {children} + + + ); +} + +export default SpinnerErrorButton; diff --git a/frontend/src/Components/Link/SpinnerIconButton.js b/frontend/src/Components/Link/SpinnerIconButton.js deleted file mode 100644 index d36ebb24d2..0000000000 --- a/frontend/src/Components/Link/SpinnerIconButton.js +++ /dev/null @@ -1,40 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { icons } from 'Helpers/Props'; -import IconButton from './IconButton'; - -function SpinnerIconButton(props) { - const { - name, - spinningName, - isDisabled, - isSpinning, - ...otherProps - } = props; - - return ( - - ); -} - -SpinnerIconButton.propTypes = { - ...IconButton.propTypes, - className: PropTypes.string, - name: PropTypes.object.isRequired, - spinningName: PropTypes.object.isRequired, - isDisabled: PropTypes.bool.isRequired, - isSpinning: PropTypes.bool.isRequired -}; - -SpinnerIconButton.defaultProps = { - spinningName: icons.SPINNER, - isDisabled: false, - isSpinning: false -}; - -export default SpinnerIconButton; diff --git a/frontend/src/Components/Link/SpinnerIconButton.tsx b/frontend/src/Components/Link/SpinnerIconButton.tsx new file mode 100644 index 0000000000..e0df823048 --- /dev/null +++ b/frontend/src/Components/Link/SpinnerIconButton.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { IconName } from 'Components/Icon'; +import { icons } from 'Helpers/Props'; +import IconButton, { IconButtonProps } from './IconButton'; + +interface SpinnerIconButtonProps extends IconButtonProps { + spinningName?: IconName; +} + +function SpinnerIconButton({ + name, + spinningName = icons.SPINNER, + isDisabled = false, + isSpinning = false, + ...otherProps +}: SpinnerIconButtonProps) { + return ( + + ); +} + +export default SpinnerIconButton; diff --git a/frontend/src/Components/Modal/ConfirmModal.tsx b/frontend/src/Components/Modal/ConfirmModal.tsx index 2adf99a3a5..259d753bfe 100644 --- a/frontend/src/Components/Modal/ConfirmModal.tsx +++ b/frontend/src/Components/Modal/ConfirmModal.tsx @@ -1,20 +1,17 @@ import React, { useEffect } from 'react'; import Button from 'Components/Link/Button'; -import SpinnerButton from 'Components/Link/SpinnerButton'; -import Modal from 'Components/Modal/Modal'; +import SpinnerButton, { + SpinnerButtonProps, +} from 'Components/Link/SpinnerButton'; +import Modal, { ModalProps } from 'Components/Modal/Modal'; import ModalBody from 'Components/Modal/ModalBody'; import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import useKeyboardShortcuts from 'Helpers/Hooks/useKeyboardShortcuts'; -import { Kind } from 'Helpers/Props/kinds'; -import { Size } from 'Helpers/Props/sizes'; -interface ConfirmModalProps { - className?: string; - isOpen: boolean; - kind?: Kind; - size?: Size; +interface ConfirmModalProps extends Omit { + kind?: SpinnerButtonProps['kind']; title: string; message: React.ReactNode; confirmLabel?: string; diff --git a/frontend/src/System/Status/Health/Health.tsx b/frontend/src/System/Status/Health/Health.tsx index 087146740a..b921c4d964 100644 --- a/frontend/src/System/Status/Health/Health.tsx +++ b/frontend/src/System/Status/Health/Health.tsx @@ -3,7 +3,7 @@ import { useDispatch, useSelector } from 'react-redux'; import AppState from 'App/State/AppState'; import Alert from 'Components/Alert'; import FieldSet from 'Components/FieldSet'; -import Icon, { IconProps } from 'Components/Icon'; +import Icon, { IconKind } from 'Components/Icon'; import IconButton from 'Components/Link/IconButton'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; @@ -97,7 +97,7 @@ function Health() { {items.map((item) => { const source = item.source; - let kind: IconProps['kind'] = kinds.WARNING; + let kind: IconKind = kinds.WARNING; switch (item.type.toLowerCase()) { case 'error': kind = kinds.DANGER;