Convert Spinner button components to TypeScript

This commit is contained in:
Mark McDowall 2024-12-21 13:40:19 -08:00
parent 9d0acba000
commit a1d4bb5399
No known key found for this signature in database
11 changed files with 224 additions and 271 deletions

View file

@ -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') {

View file

@ -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}
/>
);

View file

@ -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;

View file

@ -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;

View 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;

View file

@ -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;

View 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;

View file

@ -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;

View 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;

View file

@ -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;

View file

@ -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;