mirror of
https://github.com/Sonarr/Sonarr.git
synced 2025-04-23 05:47:11 -04:00
Convert Spinner button components to TypeScript
This commit is contained in:
parent
9d0acba000
commit
a1d4bb5399
11 changed files with 224 additions and 271 deletions
|
@ -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') {
|
||||
|
|
|
@ -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({
|
|||
<SpinnerButton
|
||||
className={classNames(className, !isLastButton && styles.middleButton)}
|
||||
kind={kind}
|
||||
isSpinning={isSpinning}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Kind } from 'Helpers/Props/kinds';
|
|||
import styles from './Icon.css';
|
||||
|
||||
export type IconName = FontAwesomeIconProps['icon'];
|
||||
export type IconKind = Extract<Kind, keyof typeof styles>;
|
||||
|
||||
export interface IconProps
|
||||
extends Omit<
|
||||
|
@ -17,7 +18,7 @@ export interface IconProps
|
|||
> {
|
||||
containerClassName?: ComponentProps<'span'>['className'];
|
||||
name: IconName;
|
||||
kind?: Extract<Kind, keyof typeof styles>;
|
||||
kind?: IconKind;
|
||||
size?: number;
|
||||
isSpinning?: FontAwesomeIconProps['spin'];
|
||||
title?: string | (() => string) | 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 (
|
||||
<Button
|
||||
className={classNames(
|
||||
className,
|
||||
styles.button,
|
||||
isSpinning && styles.isSpinning
|
||||
)}
|
||||
isDisabled={isDisabled || isSpinning}
|
||||
{...otherProps}
|
||||
>
|
||||
<span className={styles.spinnerContainer}>
|
||||
<Icon
|
||||
className={styles.spinner}
|
||||
name={spinnerIcon}
|
||||
isSpinning={true}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span className={styles.label}>
|
||||
{children}
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
41
frontend/src/Components/Link/SpinnerButton.tsx
Normal file
41
frontend/src/Components/Link/SpinnerButton.tsx
Normal file
|
@ -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 (
|
||||
<Button
|
||||
className={classNames(
|
||||
className,
|
||||
styles.button,
|
||||
isSpinning && styles.isSpinning
|
||||
)}
|
||||
isDisabled={isDisabled || isSpinning}
|
||||
{...otherProps}
|
||||
>
|
||||
<span className={styles.spinnerContainer}>
|
||||
<Icon className={styles.spinner} name={spinnerIcon} isSpinning={true} />
|
||||
</span>
|
||||
|
||||
<span className={styles.label}>{children}</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default SpinnerButton;
|
|
@ -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 (
|
||||
<SpinnerButton
|
||||
kind={kind}
|
||||
isSpinning={isSpinning}
|
||||
{...otherProps}
|
||||
>
|
||||
<span className={showIcon ? styles.showIcon : undefined}>
|
||||
{
|
||||
showIcon &&
|
||||
<span className={styles.iconContainer}>
|
||||
<Icon
|
||||
name={iconName}
|
||||
kind={iconKind}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
|
||||
{
|
||||
<span className={styles.label}>
|
||||
{
|
||||
children
|
||||
}
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
</SpinnerButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SpinnerErrorButton.propTypes = {
|
||||
kind: PropTypes.oneOf(kinds.all),
|
||||
isSpinning: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
children: PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
export default SpinnerErrorButton;
|
143
frontend/src/Components/Link/SpinnerErrorButton.tsx
Normal file
143
frontend/src/Components/Link/SpinnerErrorButton.tsx
Normal file
|
@ -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<ReturnType<typeof setTimeout>>();
|
||||
|
||||
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 (
|
||||
<SpinnerButton kind={kind} isSpinning={isSpinning} {...otherProps}>
|
||||
<span className={showIcon ? styles.showIcon : undefined}>
|
||||
{showIcon && (
|
||||
<span className={styles.iconContainer}>
|
||||
<Icon name={iconName} kind={iconKind} />
|
||||
</span>
|
||||
)}
|
||||
|
||||
<span className={styles.label}>{children}</span>
|
||||
</span>
|
||||
</SpinnerButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default SpinnerErrorButton;
|
|
@ -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 (
|
||||
<IconButton
|
||||
name={isSpinning ? (spinningName || name) : name}
|
||||
isDisabled={isDisabled || isSpinning}
|
||||
isSpinning={isSpinning}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
27
frontend/src/Components/Link/SpinnerIconButton.tsx
Normal file
27
frontend/src/Components/Link/SpinnerIconButton.tsx
Normal file
|
@ -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 (
|
||||
<IconButton
|
||||
name={isSpinning ? spinningName || name : name}
|
||||
isDisabled={isDisabled || isSpinning}
|
||||
isSpinning={isSpinning}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default SpinnerIconButton;
|
|
@ -1,16 +1,17 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
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';
|
||||
|
||||
interface ConfirmModalProps extends Omit<ModalProps, 'onModalClose'> {
|
||||
kind?: Kind;
|
||||
kind?: SpinnerButtonProps['kind'];
|
||||
title: string;
|
||||
message: React.ReactNode;
|
||||
confirmLabel?: string;
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue