chore: Move to Typescript (#2783)
This PR moves the entire project to Typescript. Due to the ~1000 ignores this will lead to a messy codebase for a while, but the churn is worth it – all of those ignore comments are places that were never type-safe previously. closes #1282
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
|
||||
type InitialState = boolean | (() => boolean);
|
||||
@@ -8,9 +7,10 @@ type InitialState = boolean | (() => boolean);
|
||||
*
|
||||
* @param initialState the initial boolean state value
|
||||
*/
|
||||
export default function useBoolean(initialState: InitialState = false) {
|
||||
export default function useBoolean(
|
||||
initialState: InitialState = false
|
||||
): [boolean, () => void, () => void] {
|
||||
const [value, setValue] = React.useState(initialState);
|
||||
|
||||
const setTrue = React.useCallback(() => {
|
||||
setValue(true);
|
||||
}, []);
|
||||
@@ -1,17 +1,15 @@
|
||||
// @flow
|
||||
import { useRegisterActions } from "kbar";
|
||||
import { flattenDeep } from "lodash";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { actionToKBar } from "actions";
|
||||
import useStores from "hooks/useStores";
|
||||
import type { Action } from "types";
|
||||
import { actionToKBar } from "~/actions";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { Action } from "~/types";
|
||||
|
||||
export default function useCommandBarActions(actions: Action[]) {
|
||||
const stores = useStores();
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
|
||||
const context = {
|
||||
t,
|
||||
isCommandBar: true,
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import invariant from "invariant";
|
||||
import useStores from "./useStores";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import invariant from "invariant";
|
||||
import useStores from "./useStores";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import invariant from "invariant";
|
||||
import useStores from "./useStores";
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
|
||||
export default function useDebouncedCallback(
|
||||
callback: (any) => mixed,
|
||||
callback: (arg0: any) => unknown,
|
||||
wait: number
|
||||
) {
|
||||
// track args & timeout handle between calls
|
||||
const argsRef = React.useRef();
|
||||
const timeout = React.useRef();
|
||||
const timeout = React.useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
function cleanup() {
|
||||
if (timeout.current) {
|
||||
@@ -17,13 +16,12 @@ export default function useDebouncedCallback(
|
||||
|
||||
// make sure our timeout gets cleared if consuming component gets unmounted
|
||||
React.useEffect(() => cleanup, []);
|
||||
|
||||
return function (...args: any) {
|
||||
argsRef.current = args;
|
||||
cleanup();
|
||||
|
||||
timeout.current = setTimeout(() => {
|
||||
if (argsRef.current) {
|
||||
// @ts-expect-error ts-migrate(2556) FIXME: Expected 1 arguments, but got 0 or more.
|
||||
callback(...argsRef.current);
|
||||
}
|
||||
}, wait);
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
|
||||
const activityEvents = [
|
||||
@@ -21,7 +20,7 @@ const activityEvents = [
|
||||
*/
|
||||
export default function useIdle(timeToIdle: number = 3 * 60 * 1000) {
|
||||
const [isIdle, setIsIdle] = React.useState(false);
|
||||
const timeout = React.useRef();
|
||||
const timeout = React.useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
const onActivity = React.useCallback(() => {
|
||||
if (timeout.current) {
|
||||
@@ -42,7 +41,6 @@ export default function useIdle(timeToIdle: number = 3 * 60 * 1000) {
|
||||
activityEvents.forEach((eventName) =>
|
||||
window.addEventListener(eventName, handleUserActivityEvent)
|
||||
);
|
||||
|
||||
return () => {
|
||||
activityEvents.forEach((eventName) =>
|
||||
window.removeEventListener(eventName, handleUserActivityEvent)
|
||||
@@ -1,23 +1,24 @@
|
||||
// @flow
|
||||
import invariant from "invariant";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import useStores from "hooks/useStores";
|
||||
import useToasts from "hooks/useToasts";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import useToasts from "~/hooks/useToasts";
|
||||
|
||||
let importingLock = false;
|
||||
|
||||
export default function useImportDocument(
|
||||
collectionId: string,
|
||||
collectionId?: string,
|
||||
documentId?: string
|
||||
): {| handleFiles: (files: File[]) => Promise<void>, isImporting: boolean |} {
|
||||
): {
|
||||
handleFiles: (files: File[]) => Promise<void>;
|
||||
isImporting: boolean;
|
||||
} {
|
||||
const { documents } = useStores();
|
||||
const { showToast } = useToasts();
|
||||
const [isImporting, setImporting] = React.useState(false);
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
|
||||
const handleFiles = React.useCallback(
|
||||
async (files = []) => {
|
||||
if (importingLock) {
|
||||
@@ -38,7 +39,7 @@ export default function useImportDocument(
|
||||
const redirect = files.length === 1;
|
||||
|
||||
if (documentId && !collectionId) {
|
||||
const { document } = await documents.fetch(documentId);
|
||||
const document = await documents.fetch(documentId);
|
||||
invariant(document, "Document not available");
|
||||
cId = document.collectionId;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
|
||||
/**
|
||||
@@ -11,7 +10,6 @@ export default function useIsMounted() {
|
||||
|
||||
React.useEffect(() => {
|
||||
isMounted.current = true;
|
||||
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
@@ -1,11 +1,12 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import isTextInput from "utils/isTextInput";
|
||||
import isTextInput from "~/utils/isTextInput";
|
||||
|
||||
export type KeyFilter = ((event: KeyboardEvent) => boolean) | string;
|
||||
|
||||
type Callback = (event: KeyboardEvent) => void;
|
||||
|
||||
// Registered keyboard event callbacks
|
||||
let callbacks = [];
|
||||
let callbacks: Callback[] = [];
|
||||
|
||||
// Track if IME input suggestions are open so we can ignore keydown shortcuts
|
||||
// in this case, they should never be triggered from mobile keyboards.
|
||||
@@ -21,13 +22,10 @@ const createKeyPredicate = (keyFilter: KeyFilter) =>
|
||||
event.key === keyFilter ||
|
||||
event.code === `Key${keyFilter.toUpperCase()}`
|
||||
: keyFilter
|
||||
? (_event) => true
|
||||
: (_event) => false;
|
||||
? (_event: KeyboardEvent) => true
|
||||
: (_event: KeyboardEvent) => false;
|
||||
|
||||
export default function useKeyDown(
|
||||
key: KeyFilter,
|
||||
fn: (event: KeyboardEvent) => void
|
||||
): void {
|
||||
export default function useKeyDown(key: KeyFilter, fn: Callback): void {
|
||||
const predicate = createKeyPredicate(key);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -38,7 +36,6 @@ export default function useKeyDown(
|
||||
};
|
||||
|
||||
callbacks.push(handler);
|
||||
|
||||
return () => {
|
||||
callbacks = callbacks.filter((cb) => cb !== handler);
|
||||
};
|
||||
@@ -55,7 +52,12 @@ window.addEventListener("keydown", (event) => {
|
||||
if (event.defaultPrevented === true) {
|
||||
break;
|
||||
}
|
||||
if (!isTextInput(event.target) || event.ctrlKey || event.metaKey) {
|
||||
|
||||
if (
|
||||
!isTextInput(event.target as HTMLElement) ||
|
||||
event.ctrlKey ||
|
||||
event.metaKey
|
||||
) {
|
||||
callback(event);
|
||||
}
|
||||
}
|
||||
@@ -64,6 +66,7 @@ window.addEventListener("keydown", (event) => {
|
||||
window.addEventListener("compositionstart", () => {
|
||||
imeOpen = true;
|
||||
});
|
||||
|
||||
window.addEventListener("compositionend", () => {
|
||||
imeOpen = false;
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export default function useMediaQuery(query: string): boolean {
|
||||
@@ -7,15 +6,20 @@ export default function useMediaQuery(query: string): boolean {
|
||||
useEffect(() => {
|
||||
if (window.matchMedia) {
|
||||
const media = window.matchMedia(query);
|
||||
|
||||
if (media.matches !== matches) {
|
||||
setMatches(media.matches);
|
||||
}
|
||||
|
||||
const listener = () => {
|
||||
setMatches(media.matches);
|
||||
};
|
||||
|
||||
media.addListener(listener);
|
||||
return () => media.removeListener(listener);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [matches, query]);
|
||||
|
||||
return matches;
|
||||
@@ -1,14 +1,12 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { type ElementRef } from "reakit";
|
||||
import useMobile from "hooks/useMobile";
|
||||
import useWindowSize from "hooks/useWindowSize";
|
||||
import useMobile from "~/hooks/useMobile";
|
||||
import useWindowSize from "~/hooks/useWindowSize";
|
||||
|
||||
const useMenuHeight = (
|
||||
visible: void | boolean,
|
||||
unstable_disclosureRef: void | { current: null | ElementRef<"button"> }
|
||||
unstable_disclosureRef?: React.RefObject<HTMLElement | null>
|
||||
) => {
|
||||
const [maxHeight, setMaxHeight] = React.useState(undefined);
|
||||
const [maxHeight, setMaxHeight] = React.useState<number | undefined>();
|
||||
const isMobile = useMobile();
|
||||
const { height: windowHeight } = useWindowSize();
|
||||
|
||||
@@ -25,7 +23,6 @@ const useMenuHeight = (
|
||||
);
|
||||
}
|
||||
}, [visible, unstable_disclosureRef, windowHeight, isMobile]);
|
||||
|
||||
return maxHeight;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// @flow
|
||||
import { useTheme } from "styled-components";
|
||||
import useMediaQuery from "hooks/useMediaQuery";
|
||||
import useMediaQuery from "~/hooks/useMediaQuery";
|
||||
|
||||
export default function useMobile(): boolean {
|
||||
const theme = useTheme();
|
||||
@@ -1,22 +1,20 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
|
||||
/**
|
||||
* Hook to return page visibility state.
|
||||
*
|
||||
* @returns boolean if the page is visible
|
||||
*/
|
||||
|
||||
export default function usePageVisibility(): boolean {
|
||||
const [visible, setVisible] = React.useState(true);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleVisibilityChange = () => setVisible(!document.hidden);
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
return () => {
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return visible;
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
|
||||
export default function usePrevious<T>(value: T): T | void {
|
||||
const ref = React.useRef();
|
||||
const ref = React.useRef<T>();
|
||||
|
||||
React.useEffect(() => {
|
||||
ref.current = value;
|
||||
});
|
||||
|
||||
return ref.current;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
export default function useQuery() {
|
||||
@@ -1,17 +1,15 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { getCookie } from "tiny-cookie";
|
||||
|
||||
type Session = {|
|
||||
url: string,
|
||||
logoUrl: string,
|
||||
name: string,
|
||||
teamId: string,
|
||||
|};
|
||||
type Session = {
|
||||
url: string;
|
||||
logoUrl: string;
|
||||
name: string;
|
||||
teamId: string;
|
||||
};
|
||||
|
||||
function loadSessionsFromCookie(): Session[] {
|
||||
const sessions = JSON.parse(getCookie("sessions") || "{}");
|
||||
|
||||
return Object.keys(sessions).map((teamId) => ({
|
||||
teamId,
|
||||
...sessions[teamId],
|
||||
@@ -20,10 +18,8 @@ function loadSessionsFromCookie(): Session[] {
|
||||
|
||||
export default function useSessions() {
|
||||
const [sessions, setSessions] = React.useState(loadSessionsFromCookie);
|
||||
|
||||
const reload = React.useCallback(() => {
|
||||
setSessions(loadSessionsFromCookie());
|
||||
}, []);
|
||||
|
||||
return [sessions, reload];
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// @flow
|
||||
import { MobXProviderContext } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import RootStore from "stores";
|
||||
|
||||
export default function useStores(): typeof RootStore {
|
||||
return React.useContext(MobXProviderContext);
|
||||
}
|
||||
7
app/hooks/useStores.ts
Normal file
7
app/hooks/useStores.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { MobXProviderContext } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import RootStore from "~/stores";
|
||||
|
||||
export default function useStores() {
|
||||
return React.useContext(MobXProviderContext) as typeof RootStore;
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
// @flow
|
||||
import useStores from "./useStores";
|
||||
|
||||
export default function useToasts() {
|
||||
const { toasts } = useStores();
|
||||
|
||||
return { showToast: toasts.showToast, hideToast: toasts.hideToast };
|
||||
return {
|
||||
showToast: toasts.showToast,
|
||||
hideToast: toasts.hideToast,
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
|
||||
const useUnmount = (callback: Function) => {
|
||||
const useUnmount = (callback: (...args: Array<any>) => any) => {
|
||||
const ref = React.useRef(callback);
|
||||
|
||||
ref.current = callback;
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import useStores from "./useStores";
|
||||
|
||||
export default function useUserLocale() {
|
||||
@@ -1,19 +1,23 @@
|
||||
// @flow
|
||||
// Based on https://github.com/rehooks/window-scroll-position which is no longer
|
||||
// maintained.
|
||||
import { throttle } from "lodash";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
let supportsPassive = false;
|
||||
|
||||
try {
|
||||
var opts = Object.defineProperty({}, "passive", {
|
||||
const opts = Object.defineProperty({}, "passive", {
|
||||
get: function () {
|
||||
supportsPassive = true;
|
||||
},
|
||||
});
|
||||
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
|
||||
window.addEventListener("testPassive", null, opts);
|
||||
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
|
||||
window.removeEventListener("testPassive", null, opts);
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
// No-op
|
||||
}
|
||||
|
||||
const getPosition = () => ({
|
||||
x: window.pageXOffset,
|
||||
@@ -25,23 +29,27 @@ const defaultOptions = {
|
||||
};
|
||||
|
||||
export default function useWindowScrollPosition(options: {
|
||||
throttle: number,
|
||||
}): { x: number, y: number } {
|
||||
let opts = Object.assign({}, defaultOptions, options);
|
||||
|
||||
let [position, setPosition] = useState(getPosition());
|
||||
throttle: number;
|
||||
}): {
|
||||
x: number;
|
||||
y: number;
|
||||
} {
|
||||
const opts = Object.assign({}, defaultOptions, options);
|
||||
const [position, setPosition] = useState(getPosition());
|
||||
|
||||
useEffect(() => {
|
||||
let handleScroll = throttle(() => {
|
||||
const handleScroll = throttle(() => {
|
||||
setPosition(getPosition());
|
||||
}, opts.throttle);
|
||||
|
||||
window.addEventListener(
|
||||
"scroll",
|
||||
handleScroll,
|
||||
supportsPassive ? { passive: true } : false
|
||||
supportsPassive
|
||||
? {
|
||||
passive: true,
|
||||
}
|
||||
: false
|
||||
);
|
||||
|
||||
return () => {
|
||||
handleScroll.cancel();
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
@@ -1,11 +1,10 @@
|
||||
// @flow
|
||||
import { debounce } from "lodash";
|
||||
import * as React from "react";
|
||||
|
||||
export default function useWindowSize() {
|
||||
const [windowSize, setWindowSize] = React.useState({
|
||||
width: parseInt(window.innerWidth),
|
||||
height: parseInt(window.innerHeight),
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -13,8 +12,8 @@ export default function useWindowSize() {
|
||||
const handleResize = debounce(() => {
|
||||
// Set window width/height to state
|
||||
setWindowSize({
|
||||
width: parseInt(window.innerWidth),
|
||||
height: parseInt(window.innerHeight),
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
}, 100);
|
||||
|
||||
@@ -23,9 +22,7 @@ export default function useWindowSize() {
|
||||
|
||||
// Call handler right away so state gets updated with initial window size
|
||||
handleResize();
|
||||
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []);
|
||||
|
||||
return windowSize;
|
||||
}
|
||||
Reference in New Issue
Block a user