chore(ui): migrate more code to typescript

This commit is contained in:
Łukasz Mierzwa
2020-06-29 16:14:53 +01:00
committed by Łukasz Mierzwa
parent 55170f8812
commit 4d4dd111c1
36 changed files with 392 additions and 205 deletions

View File

@@ -18,6 +18,7 @@ import {
ReactSelectStyles,
} from "Components/Theme/ReactSelect";
import { BodyTheme, ThemeContext } from "Components/Theme";
import { UIDefaults } from "./AppBoot";
import { ErrorBoundary } from "./ErrorBoundary";
import "Styles/ResetCSS.scss";
@@ -46,19 +47,9 @@ const FaviconBadge = React.lazy(() =>
}))
);
interface UIDefaults {
Refresh: number;
HideFiltersWhenIdle: boolean;
ColorTitlebar: boolean;
Theme: "light" | "dark" | "auto";
MinimalGroupWidth: number;
AlertsPerGroup: number;
CollapseGroups: "expanded" | "collapsed" | "collapsedOnMobile";
}
interface AppProps {
defaultFilters: Array<string>;
uiDefaults: UIDefaults;
uiDefaults: UIDefaults | null;
}
const App: FunctionComponent<AppProps> = ({ defaultFilters, uiDefaults }) => {

View File

@@ -2,9 +2,19 @@
import { init } from "@sentry/browser";
export interface UIDefaults {
Refresh: number;
HideFiltersWhenIdle: boolean;
ColorTitlebar: boolean;
Theme: "light" | "dark" | "auto";
MinimalGroupWidth: number;
AlertsPerGroup: number;
CollapseGroups: "expanded" | "collapsed" | "collapsedOnMobile";
}
const SettingsElement = () => document.getElementById("settings");
const SetupSentry = (settingsElement) => {
const SetupSentry = (settingsElement: HTMLElement | null) => {
if (
settingsElement !== null &&
settingsElement.dataset.sentryDsn &&
@@ -29,7 +39,7 @@ const SetupSentry = (settingsElement) => {
}
};
const ParseDefaultFilters = (settingsElement) => {
const ParseDefaultFilters = (settingsElement: HTMLElement | null): string[] => {
let defaultFilters = [];
if (
settingsElement !== null &&
@@ -48,7 +58,9 @@ const ParseDefaultFilters = (settingsElement) => {
return defaultFilters;
};
const ParseUIDefaults = (defaultsElement) => {
const ParseUIDefaults = (
defaultsElement: HTMLElement | null
): UIDefaults | null => {
if (defaultsElement === null) {
return null;
}

View File

@@ -1,4 +1,4 @@
function IsMobile() {
function IsMobile(): boolean {
return window.innerWidth < 768;
}

View File

@@ -14,7 +14,13 @@ const FetchRetryConfig = {
maxTimeout: 5000,
};
const FetchGet = async (uri, options, beforeRetry) =>
type PreRetryCallback = (number: number) => void;
const FetchGet = async (
uri: string,
options: RequestInit,
beforeRetry: PreRetryCallback
) =>
await promiseRetry(
(retry, number) =>
fetch(

View File

@@ -14,7 +14,7 @@ const StaticLabels = Object.freeze({
SilenceID: "@silence_id",
});
function FormatQuery(name, operator, value) {
function FormatQuery(name: string, operator: string, value: string) {
return `${name}${operator}${value}`;
}

View File

@@ -1,5 +0,0 @@
const NewLabelName = (v) => `New label: ${v}`;
const NewLabelValue = (v) => `New value: ${v}`;
export { NewLabelName, NewLabelValue };

5
ui/src/Common/Select.ts Normal file
View File

@@ -0,0 +1,5 @@
const NewLabelName = (v: string) => `New label: ${v}`;
const NewLabelValue = (v: string) => `New value: ${v}`;
export { NewLabelName, NewLabelValue };

View File

@@ -1,9 +1,12 @@
import React from "react";
import React, { FC, ReactNode } from "react";
import PropTypes from "prop-types";
import { CSSTransition } from "react-transition-group";
const DropdownSlide = ({ children, duration, ...props }) => (
const DropdownSlide: FC<{
children: ReactNode;
duration: number;
}> = ({ children, duration, ...props }) => (
<CSSTransition
classNames="components-animation-slide"
timeout={150}

View File

@@ -1,9 +1,13 @@
import React from "react";
import React, { FC, ReactNode } from "react";
import PropTypes from "prop-types";
import { CSSTransition } from "react-transition-group";
const MountModal = ({ children, duration, ...props }) => (
const MountModal: FC<{
children: ReactNode;
in: boolean;
unmountOnExit?: boolean;
}> = ({ children, ...props }) => (
<CSSTransition
classNames="components-animation-modal"
timeout={200}
@@ -19,9 +23,12 @@ MountModal.propTypes = {
children: PropTypes.node.isRequired,
};
const MountModalBackdrop = ({ children, duration, ...props }) => (
const MountModalBackdrop: FC<{
children: ReactNode;
in?: boolean;
unmountOnExit?: boolean;
}> = ({ children, ...props }) => (
<CSSTransition
in={true}
classNames="components-animation-backdrop"
timeout={200}
appear={true}

View File

@@ -1,10 +1,13 @@
import React from "react";
import React, { FC, ReactNode } from "react";
import { CSSTransition } from "react-transition-group";
import { ThemeContext } from "Components/Theme";
const CenteredMessage = ({ children, className }) => {
const CenteredMessage: FC<{
children: ReactNode;
className: string;
}> = ({ children, className }) => {
const context = React.useContext(ThemeContext);
return (
<CSSTransition

View File

@@ -1,5 +1,4 @@
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import React, { useState, useEffect, FC } from "react";
import { autorun } from "mobx";
import { useObserver } from "mobx-react-lite";
@@ -8,7 +7,9 @@ import Favico from "favico.js";
import { AlertStore } from "Stores/AlertStore";
const FaviconBadge = ({ alertStore }) => {
const FaviconBadge: FC<{
alertStore: AlertStore;
}> = ({ alertStore }) => {
const [favico] = useState(
new Favico({
animation: "none",
@@ -38,8 +39,5 @@ const FaviconBadge = ({ alertStore }) => {
/>
));
};
FaviconBadge.propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
};
export { FaviconBadge };

View File

@@ -1,19 +0,0 @@
import { useEffect } from "react";
import PropTypes from "prop-types";
import { AlertStore } from "Stores/AlertStore";
const FetchPauser = ({ children, alertStore }) => {
useEffect(() => {
alertStore.status.pause();
return alertStore.status.resume;
}, [alertStore.status]);
return children;
};
FetchPauser.propTypes = {
children: PropTypes.any,
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
};
export { FetchPauser };

View File

@@ -0,0 +1,17 @@
import { FC, ReactElement, useEffect } from "react";
import { AlertStore } from "Stores/AlertStore";
const FetchPauser: FC<{
children: ReactElement;
alertStore: AlertStore;
}> = ({ children, alertStore }) => {
useEffect(() => {
alertStore.status.pause();
return alertStore.status.resume;
}, [alertStore.status]);
return children;
};
export { FetchPauser };

View File

@@ -1,5 +1,4 @@
import React, { useEffect, useRef } from "react";
import PropTypes from "prop-types";
import React, { useEffect, useRef, FC } from "react";
import { reaction } from "mobx";
@@ -8,8 +7,11 @@ import addSeconds from "date-fns/addSeconds";
import { AlertStore, AlertStoreStatuses } from "Stores/AlertStore";
import { Settings } from "Stores/Settings";
const Fetcher = ({ alertStore, settingsStore }) => {
const timer = useRef(null);
const Fetcher: FC<{
alertStore: AlertStore;
settingsStore: Settings;
}> = ({ alertStore, settingsStore }) => {
const timer = useRef(undefined as number | undefined);
const getSortSettings = () => {
let sortSettings = {
@@ -81,7 +83,7 @@ const Fetcher = ({ alertStore, settingsStore }) => {
};
useEffect(() => {
return () => clearInterval(timer.current);
return () => window.clearInterval(timer.current);
}, []);
useEffect(
@@ -89,7 +91,9 @@ const Fetcher = ({ alertStore, settingsStore }) => {
reaction(
() =>
JSON.stringify({
filters: alertStore.filters.values.map((f) => f.raw).join(" "),
filters: alertStore.filters.values
.map((f: { raw: string }) => f.raw)
.join(" "),
grid: {
sortOrder: settingsStore.gridConfig.config.sortOrder,
sortLabel: settingsStore.gridConfig.config.sortLabel,
@@ -115,10 +119,10 @@ const Fetcher = ({ alertStore, settingsStore }) => {
() => alertStore.status.paused,
(paused) => {
if (paused) {
clearInterval(timer.current);
timer.current = null;
window.clearInterval(timer.current);
timer.current = undefined;
} else {
timer.current = setInterval(
timer.current = window.setInterval(
() => window.requestAnimationFrame(fetchIfIdle),
1000
);
@@ -131,9 +135,5 @@ const Fetcher = ({ alertStore, settingsStore }) => {
return <span />;
};
Fetcher.propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
settingsStore: PropTypes.instanceOf(Settings).isRequired,
};
export { Fetcher };

View File

@@ -1,20 +0,0 @@
import React from "react";
import PropTypes from "prop-types";
const Tab = ({ title, active, onClick }) => (
<span
className={`nav-item nav-link cursor-pointer mx-1 px-2 ${
active ? "active" : "components-tab-inactive"
}`}
onClick={onClick}
>
{title}
</span>
);
Tab.propTypes = {
title: PropTypes.string.isRequired,
active: PropTypes.bool,
onClick: PropTypes.func.isRequired,
};
export { Tab };

View File

@@ -0,0 +1,18 @@
import React, { FC, MouseEvent } from "react";
const Tab: FC<{
title: string;
active?: boolean;
onClick: (event: MouseEvent<HTMLElement>) => void;
}> = ({ title, active, onClick }) => (
<span
className={`nav-item nav-link cursor-pointer mx-1 px-2 ${
active ? "active" : "components-tab-inactive"
}`}
onClick={onClick}
>
{title}
</span>
);
export { Tab };

View File

@@ -3,7 +3,16 @@ import React from "react";
import { mount } from "enzyme";
import { PressKey } from "__mocks__/PressKey";
import { Modal } from ".";
import { Modal, ModalInner } from ".";
beforeEach(() => {
jest.restoreAllMocks();
});
afterEach(() => {
jest.restoreAllMocks();
document.body.className = "";
});
const fakeToggle = jest.fn();
@@ -15,12 +24,21 @@ const MountedModal = (isOpen, isUpper) => {
);
};
afterEach(() => {
jest.resetAllMocks();
document.body.className = "";
});
describe("<ModalInner />", () => {
it("scroll isn't enabled if ref is null", () => {
const useRefSpy = jest.spyOn(React, "useRef").mockImplementation(() =>
Object.defineProperty({}, "current", {
get: () => null,
set: () => {},
})
);
const tree = mount(<ModalInner isUpper toggleOpen={fakeToggle} />);
tree.setProps({ isUpper: false });
tree.setProps({ isUpper: true });
tree.setProps({ isUpper: false });
expect(useRefSpy).toHaveBeenCalledTimes(4);
});
describe("<Modal />", () => {
it("'modal-open' class is appended to MountModal container", () => {
const tree = MountedModal(true);
expect(tree.find("div").at(0).hasClass("modal-open")).toBe(true);

View File

@@ -1,6 +1,5 @@
import React, { useRef, useEffect } from "react";
import React, { FC, useEffect } from "react";
import ReactDOM from "react-dom";
import PropTypes from "prop-types";
import { disableBodyScroll, enableBodyScroll } from "body-scroll-lock";
@@ -11,18 +10,25 @@ import {
MountModalBackdrop,
} from "Components/Animations/MountModal";
const ModalInner = ({ size, isUpper, toggleOpen, children }) => {
const ref = useRef(null);
const ModalInner: FC<{
size: "lg" | "xl";
isUpper: boolean;
toggleOpen: () => void;
}> = ({ size, isUpper, toggleOpen, children }) => {
// needed for tests to spy on useRef
const ref = React.useRef(null as HTMLDivElement | null);
useEffect(() => {
document.body.classList.add("modal-open");
disableBodyScroll(ref.current, { reserveScrollBarGap: true });
if (ref.current !== null) {
document.body.classList.add("modal-open");
disableBodyScroll(ref.current, { reserveScrollBarGap: true });
let modal = ref.current;
return () => {
if (!isUpper) document.body.classList.remove("modal-open");
enableBodyScroll(modal);
};
let modal = ref.current;
return () => {
if (!isUpper) document.body.classList.remove("modal-open");
enableBodyScroll(modal);
};
}
}, [isUpper]);
useHotkeys("esc", toggleOpen);
@@ -43,7 +49,19 @@ const ModalInner = ({ size, isUpper, toggleOpen, children }) => {
);
};
const Modal = ({ size, isOpen, isUpper, toggleOpen, children, ...props }) => {
const Modal: FC<{
size?: "lg" | "xl";
isOpen: boolean;
isUpper?: boolean;
toggleOpen: () => void;
}> = ({
size = "lg",
isOpen,
isUpper = false,
toggleOpen,
children,
...props
}) => {
return ReactDOM.createPortal(
<React.Fragment>
<MountModal in={isOpen} unmountOnExit {...props}>
@@ -58,16 +76,5 @@ const Modal = ({ size, isOpen, isUpper, toggleOpen, children, ...props }) => {
document.body
);
};
Modal.propTypes = {
size: PropTypes.oneOf(["lg", "xl"]),
isOpen: PropTypes.bool.isRequired,
isUpper: PropTypes.bool,
toggleOpen: PropTypes.func.isRequired,
children: PropTypes.node.isRequired,
};
Modal.defaultProps = {
size: "lg",
isUpper: false,
};
export { Modal };
export { Modal, ModalInner };

View File

@@ -1,5 +1,4 @@
import React from "react";
import PropTypes from "prop-types";
import React, { FC, ReactNode } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
@@ -12,7 +11,7 @@ import {
} from "Components/LabelSetList";
import { useFetchGet } from "Hooks/useFetchGet";
const FetchError = ({ message }) => (
const FetchError: FC<{ message: ReactNode }> = ({ message }) => (
<div className="text-center">
<h2 className="display-2 text-danger">
<FontAwesomeIcon icon={faExclamationCircle} />
@@ -20,9 +19,6 @@ const FetchError = ({ message }) => (
<p className="lead text-muted">{message}</p>
</div>
);
FetchError.propTypes = {
message: PropTypes.node.isRequired,
};
const Placeholder = () => (
<div className="jumbotron bg-transparent">
@@ -32,7 +28,11 @@ const Placeholder = () => (
</div>
);
const PaginatedAlertList = ({ alertStore, filters, title }) => {
const PaginatedAlertList: FC<{
alertStore: AlertStore;
filters: string[];
title: string;
}> = ({ alertStore, filters, title }) => {
const { response, error, isLoading } = useFetchGet(
FormatBackendURI("alerts.json?") + FormatAlertsQ(filters)
);
@@ -51,10 +51,5 @@ const PaginatedAlertList = ({ alertStore, filters, title }) => {
/>
);
};
PaginatedAlertList.propTypes = {
alertStore: PropTypes.instanceOf(AlertStore).isRequired,
filters: PropTypes.arrayOf(PropTypes.string).isRequired,
title: PropTypes.string,
};
export { PaginatedAlertList };

View File

@@ -1,5 +1,4 @@
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import React, { useState, useEffect, FC } from "react";
import { useHotkeys } from "react-hotkeys-hook";
@@ -13,16 +12,24 @@ import { faAngleDoubleRight } from "@fortawesome/free-solid-svg-icons/faAngleDou
import { IsMobile } from "Common/Device";
const PageSelect = ({
type PageCallback = (page: number) => void;
const PageSelect: FC<{
totalItemsCount: number;
totalPages: number;
maxPerPage: number;
initialPage: number;
setPageCallback: PageCallback;
}> = ({
totalItemsCount,
totalPages,
maxPerPage,
initialPage,
initialPage = 1,
setPageCallback,
}) => {
const [activePage, setActivePage] = useState(initialPage);
const onChange = (page) => {
const onChange = (page: number) => {
setActivePage(page);
setPageCallback(page);
};
@@ -83,15 +90,5 @@ const PageSelect = ({
</div>
);
};
PageSelect.propTypes = {
totalPages: PropTypes.number.isRequired,
initialPage: PropTypes.number,
maxPerPage: PropTypes.number.isRequired,
totalItemsCount: PropTypes.number.isRequired,
setPageCallback: PropTypes.func.isRequired,
};
PageSelect.defaultProps = {
initialPage: 1,
};
export { PageSelect };

View File

@@ -1,4 +1,25 @@
const ReactSelectColors = {
import { CSSProperties } from "react";
import { Styles } from "react-select";
interface ReactSelectTheme {
color: string;
singleValueColor: string;
backgroundColor: string;
borderColor: string;
focusedBoxShadow: string;
focusedBorderColor: string;
menuBackground: string;
optionHoverBackground: string;
valueContainerBackground: string;
disabledValueContainerBackground: string;
}
interface ReactSelectThemes {
Light: ReactSelectTheme;
Dark: ReactSelectTheme;
}
const ReactSelectColors: ReactSelectThemes = {
Light: {
color: "#fff",
singleValueColor: "#000",
@@ -25,8 +46,8 @@ const ReactSelectColors = {
},
};
const ReactSelectStyles = (theme) => ({
control: (base, state) =>
const ReactSelectStyles = (theme: ReactSelectTheme): Styles => ({
control: (base: CSSProperties, state: any) =>
state.isFocused
? {
...base,
@@ -47,7 +68,7 @@ const ReactSelectStyles = (theme) => ({
borderColor: theme.borderColor,
"&:hover": { borderColor: theme.borderColor },
},
valueContainer: (base, state) =>
valueContainer: (base: CSSProperties, state: any) =>
state.isMulti
? {
...base,
@@ -72,11 +93,11 @@ const ReactSelectStyles = (theme) => ({
? theme.disabledValueContainerBackground
: theme.valueContainerBackground,
},
singleValue: (base, state) => ({
singleValue: (base: CSSProperties) => ({
...base,
color: theme.singleValueColor,
}),
multiValue: (base, state) => ({
multiValue: (base: CSSProperties) => ({
...base,
borderRadius: "4px",
backgroundColor: theme.optionHoverBackground,
@@ -84,7 +105,7 @@ const ReactSelectStyles = (theme) => ({
backgroundColor: theme.optionHoverBackground,
},
}),
multiValueLabel: (base, state) => ({
multiValueLabel: (base: CSSProperties) => ({
...base,
color: theme.color,
whiteSpace: "normal",
@@ -94,7 +115,7 @@ const ReactSelectStyles = (theme) => ({
color: theme.color,
},
}),
multiValueRemove: (base, state) => ({
multiValueRemove: (base: CSSProperties) => ({
...base,
cursor: "pointer",
color: theme.color,
@@ -107,11 +128,11 @@ const ReactSelectStyles = (theme) => ({
opacity: "0.75",
},
}),
input: (base, state) => ({
input: (base: CSSProperties) => ({
...base,
color: "inherit",
}),
indicatorsContainer: (base, state) => ({
indicatorsContainer: (base: CSSProperties, state: any) => ({
...base,
backgroundColor: state.isDisabled
? theme.disabledValueContainerBackground
@@ -119,19 +140,19 @@ const ReactSelectStyles = (theme) => ({
borderTopRightRadius: "0.25rem",
borderBottomRightRadius: "0.25rem",
}),
dropdownIndicator: (base, state) =>
dropdownIndicator: (base: CSSProperties, state: any) =>
state.isFocused
? {
...base,
"&:hover": { color: "inherit" },
}
: { ...base },
menu: (base, state) => ({
menu: (base: CSSProperties) => ({
...base,
zIndex: 1500,
backgroundColor: theme.menuBackground,
}),
option: (base, state) => ({
option: (base: CSSProperties) => ({
...base,
color: "inherit",
backgroundColor: "inherit",

View File

@@ -38,7 +38,19 @@ const Placeholder = () => {
);
};
const ThemeContext = React.createContext();
interface ThemeCtx {
isDark: boolean;
reactSelectStyles: any;
animations: {
duration: number;
};
}
const ThemeContext = React.createContext({
isDark: false,
reactSelectStyles: {},
animations: { duration: 1000 },
} as ThemeCtx);
const BodyTheme = () => {
const context = React.useContext(ThemeContext);

View File

@@ -1,6 +1,5 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, ReactNode, FC } from "react";
import { createPortal } from "react-dom";
import PropTypes from "prop-types";
import { CSSTransition } from "react-transition-group";
@@ -8,16 +7,24 @@ import { usePopper } from "react-popper";
import { useSupportsTouch } from "Hooks/useSupportsTouch";
const TooltipWrapper = ({ title, children, className }) => {
const [referenceElement, setReferenceElement] = useState(null);
const [popperElement, setPopperElement] = useState(null);
const TooltipWrapper: FC<{
title: ReactNode;
children: ReactNode;
className?: string;
}> = ({ title, children, className }) => {
const [referenceElement, setReferenceElement] = useState(
null as HTMLElement | null
);
const [popperElement, setPopperElement] = useState(
null as HTMLElement | null
);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: "top",
modifiers: [
{
name: "preventOverflow",
options: {
boundariesElement: "viewport",
rootBoundary: "viewport",
},
},
],
@@ -32,22 +39,22 @@ const TooltipWrapper = ({ title, children, className }) => {
const hideTooltip = () => setIsHovering(false);
useEffect(() => {
let timerShow;
let timerHide;
let timerShow: number | undefined;
let timerHide: number | undefined;
if (!isHovering) {
if (isVisible) {
clearTimeout(timerShow);
timerHide = setTimeout(() => setIsVisible(false), 100);
window.clearTimeout(timerShow);
timerHide = window.setTimeout(() => setIsVisible(false), 100);
}
setWasClicked(false);
} else if (wasClicked) {
clearTimeout(timerShow);
clearTimeout(timerHide);
window.clearTimeout(timerShow);
window.clearTimeout(timerHide);
setIsVisible(false);
} else if (!isVisible && isHovering) {
clearTimeout(timerHide);
timerShow = setTimeout(() => setIsVisible(true), 1000);
timerShow = window.setTimeout(() => setIsVisible(true), 1000);
}
return () => {
clearTimeout(timerShow);
@@ -59,11 +66,11 @@ const TooltipWrapper = ({ title, children, className }) => {
<React.Fragment>
<div
onClick={() => setWasClicked(true)}
onMouseOver={supportsTouch ? null : showTooltip}
onMouseLeave={supportsTouch ? null : hideTooltip}
onTouchStart={supportsTouch ? showTooltip : null}
onTouchCancel={supportsTouch ? hideTooltip : null}
onTouchEnd={supportsTouch ? hideTooltip : null}
onMouseOver={supportsTouch ? undefined : showTooltip}
onMouseLeave={supportsTouch ? undefined : hideTooltip}
onTouchStart={supportsTouch ? showTooltip : undefined}
onTouchCancel={supportsTouch ? hideTooltip : undefined}
onTouchEnd={supportsTouch ? hideTooltip : undefined}
ref={setReferenceElement}
className={`${className ? className : ""} tooltip-trigger`}
>
@@ -94,10 +101,5 @@ const TooltipWrapper = ({ title, children, className }) => {
</React.Fragment>
);
};
TooltipWrapper.propTypes = {
title: PropTypes.node.isRequired,
children: PropTypes.node.isRequired,
className: PropTypes.string,
};
export { TooltipWrapper };

View File

@@ -1,7 +1,7 @@
import { useState, useEffect } from "react";
// https://usehooks.com/useDebounce/
function useDebounce(value, delay) {
function useDebounce(value: any, delay: number) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {

View File

@@ -4,14 +4,26 @@ import merge from "lodash.merge";
import { CommonOptions } from "Common/Fetch";
const useFetchAny = (upstreams, { fetcher = null } = {}) => {
interface Upstream {
uri: string;
options: RequestInit;
}
interface ResponseState {
response: string | null;
error: string | null;
responseURI: string | null;
inProgress: boolean;
}
const useFetchAny = (upstreams: Upstream[], { fetcher = null } = {}) => {
const [index, setIndex] = useState(0);
const [response, setResponse] = useState({
response: null,
error: null,
responseURI: null,
inProgress: false,
});
} as ResponseState);
const reset = useCallback(() => {
setIndex(0);
@@ -39,7 +51,7 @@ const useFetchAny = (upstreams, { fetcher = null } = {}) => {
try {
const res = await (fetcher || fetch)(
uri,
merge({}, { method: "GET" }, CommonOptions, options)
merge({}, { method: "GET" }, CommonOptions, options) as RequestInit
);
if (!isCancelled) {

View File

@@ -4,9 +4,9 @@ import merge from "lodash.merge";
import { CommonOptions } from "Common/Fetch";
const useFetchDelete = (uri, options, deps = []) => {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const useFetchDelete = (uri: string, options: RequestInit, deps = []) => {
const [response, setResponse] = useState(null as string | null);
const [error, setError] = useState(null as string | null);
const [isDeleting, setIsDeleting] = useState(true);
useEffect(() => {

View File

@@ -7,11 +7,11 @@ import promiseRetry from "promise-retry";
import { CommonOptions, FetchRetryConfig } from "Common/Fetch";
const useFetchGet = (
uri,
uri: string,
{ autorun = true, deps = [], fetcher = null } = {}
) => {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [response, setResponse] = useState(null as any);
const [error, setError] = useState(null as string | null);
const [isLoading, setIsLoading] = useState(true);
const [isRetrying, setIsRetrying] = useState(false);
const [retryCount, setRetryCount] = useState(0);
@@ -29,7 +29,7 @@ const useFetchGet = (
setRetryCount(0);
setError(null);
const res = await promiseRetry(
(retry, number) =>
(retry: Function, number: number) =>
(fetcher || fetch)(
uri,
merge(
@@ -41,7 +41,7 @@ const useFetchGet = (
{
mode: number <= FetchRetryConfig.retries ? "cors" : "no-cors",
}
)
) as RequestInit
).catch((err) => {
if (!isCanceled.current) {
setIsRetrying(true);

View File

@@ -1,8 +1,10 @@
import { useState, useEffect, useRef } from "react";
import { TransitionProps } from "react-transition-group/Transition";
import { useInView } from "react-intersection-observer";
const defaultProps = {
const defaultProps: TransitionProps = {
in: false,
classNames: "components-animation-flash",
timeout: 800,
@@ -11,7 +13,7 @@ const defaultProps = {
exit: false,
};
const useFlashTransition = (flashOn) => {
const useFlashTransition = (flashOn: any) => {
const mountRef = useRef(false);
const [ref, inView] = useInView();
const [isPending, setIsPending] = useState(false);

View File

@@ -1,15 +1,15 @@
import { useEffect, useRef, useState } from "react";
import Bricks from "bricks.js";
import Bricks, { SizeDetail, Instance } from "bricks.js";
const useGrid = (sizes) => {
const ref = useRef(null);
const grid = useRef(null);
const useGrid = (sizes: SizeDetail[]) => {
const ref = useRef(null as Node | null);
const grid = useRef(null as Instance | null);
const [repack, setRepack] = useState(() => () => {});
useEffect(() => {
if (!grid.current && ref.current) {
grid.current = new Bricks({
grid.current = Bricks({
container: ref.current,
sizes: sizes,
packed: "packed",

View File

@@ -1,10 +1,18 @@
import { useEffect } from "react";
import { useEffect, RefObject } from "react";
type Handler = (event: MouseEvent | TouchEvent) => void;
// https://usehooks.com/useOnClickOutside/
function useOnClickOutside(ref, handler, enabled) {
function useOnClickOutside(
ref: RefObject<HTMLElement>,
handler: Handler,
enabled: boolean
) {
useEffect(() => {
const listener = (event) => {
if (!ref.current || ref.current.contains(event.target)) {
const listener: { (event: MouseEvent | TouchEvent): void } = (
event: MouseEvent | TouchEvent
) => {
if (!ref.current || ref.current.contains(event.target as Node)) {
return;
}
handler(event);

View File

@@ -3,3 +3,5 @@
declare module "react-media-hook" {
function useMediaPredicate(query: string): boolean;
}
declare module "favico.js";