bazarr/frontend/src/components/async.tsx

208 lines
4.4 KiB
TypeScript
Raw Normal View History

2021-03-25 14:22:43 +00:00
import {
faCheck,
faCircleNotch,
faTimes,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { isEmpty } from "lodash";
2021-03-25 14:22:43 +00:00
import React, {
FunctionComponent,
PropsWithChildren,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import { Button, ButtonProps } from "react-bootstrap";
import { useTimeoutWhen } from "rooks";
2021-03-25 14:22:43 +00:00
import { LoadingIndicator } from ".";
import { Selector, SelectorProps } from "./inputs";
interface Props<T extends Async.Base<any>> {
ctx: T;
children: FunctionComponent<T>;
2021-03-25 14:22:43 +00:00
}
export function AsyncOverlay<T extends Async.Base<any>>(props: Props<T>) {
const { ctx, children } = props;
if (
ctx.state === "uninitialized" ||
(ctx.state === "loading" && isEmpty(ctx.content))
) {
return <LoadingIndicator></LoadingIndicator>;
} else if (ctx.state === "failed") {
return <p>{ctx.error}</p>;
2021-03-25 14:22:43 +00:00
} else {
return children(ctx);
2021-03-25 14:22:43 +00:00
}
}
interface PromiseProps<T> {
promise: () => Promise<T>;
children: FunctionComponent<T>;
}
export function PromiseOverlay<T>({ promise, children }: PromiseProps<T>) {
const [item, setItem] = useState<T | null>(null);
useEffect(() => {
promise()
.then(setItem)
2021-03-25 14:22:43 +00:00
.catch(() => {});
}, [promise]);
if (item === null) {
return <LoadingIndicator></LoadingIndicator>;
} else {
return children(item);
}
}
2021-08-23 03:35:04 +00:00
type AsyncSelectorProps<V, T extends Async.Item<V[]>> = {
2021-03-25 14:22:43 +00:00
state: T;
update: () => void;
label: (item: V) => string;
2021-03-25 14:22:43 +00:00
};
type RemovedSelectorProps<T, M extends boolean> = Omit<
SelectorProps<T, M>,
"loading" | "options" | "onFocus"
2021-03-25 14:22:43 +00:00
>;
export function AsyncSelector<
V,
2021-08-23 03:35:04 +00:00
T extends Async.Item<V[]>,
2021-03-25 14:22:43 +00:00
M extends boolean = false
>(props: Override<AsyncSelectorProps<V, T>, RemovedSelectorProps<V, M>>) {
const { label, state, update, ...selector } = props;
2021-03-25 14:22:43 +00:00
const options = useMemo<SelectorOption<V>[]>(
2021-03-25 14:22:43 +00:00
() =>
2021-08-23 03:35:04 +00:00
state.content?.map((v) => ({
2021-03-25 14:22:43 +00:00
label: label(v),
value: v,
2021-08-23 03:35:04 +00:00
})) ?? [],
2021-03-25 14:22:43 +00:00
[state, label]
);
return (
<Selector
loading={state.state === "loading"}
2021-03-25 14:22:43 +00:00
options={options}
label={label}
onFocus={() => {
if (state.state === "uninitialized") {
update();
}
}}
2021-03-25 14:22:43 +00:00
{...selector}
></Selector>
);
}
interface AsyncButtonProps<T> {
as?: ButtonProps["as"];
variant?: ButtonProps["variant"];
size?: ButtonProps["size"];
className?: string;
disabled?: boolean;
onChange?: (v: boolean) => void;
noReset?: boolean;
animation?: boolean;
2021-03-25 14:22:43 +00:00
promise: () => Promise<T> | null;
onSuccess?: (result: T) => void;
error?: () => void;
}
enum RequestState {
Success,
Error,
Invalid,
}
2021-03-25 14:22:43 +00:00
export function AsyncButton<T>(
props: PropsWithChildren<AsyncButtonProps<T>>
): JSX.Element {
const {
children: propChildren,
className,
promise,
onSuccess,
noReset,
animation,
2021-03-25 14:22:43 +00:00
error,
onChange,
disabled,
...button
} = props;
const [loading, setLoading] = useState(false);
const [state, setState] = useState(RequestState.Invalid);
const needFire = state !== RequestState.Invalid && !noReset;
2021-03-25 14:22:43 +00:00
useTimeoutWhen(
() => {
setState(RequestState.Invalid);
},
2 * 1000,
needFire
);
2021-03-25 14:22:43 +00:00
const click = useCallback(() => {
if (state !== RequestState.Invalid) {
return;
}
const result = promise();
if (result) {
setLoading(true);
onChange && onChange(true);
result
.then((res) => {
setState(RequestState.Success);
onSuccess && onSuccess(res);
})
.catch(() => {
setState(RequestState.Error);
error && error();
})
.finally(() => {
setLoading(false);
onChange && onChange(false);
});
}
}, [error, onChange, promise, onSuccess, state]);
const showAnimation = animation ?? true;
2021-03-25 14:22:43 +00:00
let children = propChildren;
if (showAnimation) {
if (loading) {
children = <FontAwesomeIcon icon={faCircleNotch} spin></FontAwesomeIcon>;
}
2021-03-25 14:22:43 +00:00
if (state === RequestState.Success) {
children = <FontAwesomeIcon icon={faCheck}></FontAwesomeIcon>;
} else if (state === RequestState.Error) {
children = <FontAwesomeIcon icon={faTimes}></FontAwesomeIcon>;
}
2021-03-25 14:22:43 +00:00
}
return (
<Button
className={className}
disabled={loading || disabled || state !== RequestState.Invalid}
{...button}
onClick={click}
>
{children}
</Button>
);
}