mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-11 12:43:09 -05:00
♻️ (typescript) migrating hooks to TypeScript (#1479)
This commit is contained in:
committed by
GitHub
parent
a0ecd65e70
commit
b325bd9b18
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export default function usePrevious(value) {
|
||||
const ref = useRef();
|
||||
export default function usePrevious<T = unknown>(value: T): T | undefined {
|
||||
const ref = useRef<T | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
@@ -4,13 +4,16 @@ import React, {
|
||||
useLayoutEffect,
|
||||
useContext,
|
||||
useMemo,
|
||||
type RefObject,
|
||||
type ReactElement,
|
||||
type MutableRefObject,
|
||||
} from 'react';
|
||||
|
||||
function getFocusedKey(el) {
|
||||
let node = el;
|
||||
function getFocusedKey(el: HTMLElement): string | null {
|
||||
let node: HTMLElement | ParentNode = el;
|
||||
// Search up to 10 parent nodes
|
||||
for (let i = 0; i < 10 && node; i++) {
|
||||
let key = node.dataset && node.dataset.focusKey;
|
||||
let key = 'dataset' in node ? node.dataset?.focusKey : undefined;
|
||||
if (key) {
|
||||
return key;
|
||||
}
|
||||
@@ -20,7 +23,10 @@ function getFocusedKey(el) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function focusElement(el, refocusContext) {
|
||||
function focusElement(
|
||||
el: HTMLElement,
|
||||
refocusContext: AvoidRefocusScrollContextValue,
|
||||
): void {
|
||||
if (refocusContext) {
|
||||
let key = getFocusedKey(el);
|
||||
el.focus({ preventScroll: key && key === refocusContext.keyRef.current });
|
||||
@@ -29,17 +35,29 @@ function focusElement(el, refocusContext) {
|
||||
el.focus();
|
||||
}
|
||||
|
||||
if (el.tagName === 'INPUT') {
|
||||
if (el instanceof HTMLInputElement) {
|
||||
el.setSelectionRange(0, 10000);
|
||||
}
|
||||
}
|
||||
|
||||
let AvoidRefocusScrollContext = createContext(null);
|
||||
type AvoidRefocusScrollContextValue = {
|
||||
keyRef: MutableRefObject<string>;
|
||||
onKeyChange: (key: string) => void;
|
||||
};
|
||||
|
||||
export function AvoidRefocusScrollProvider({ children }) {
|
||||
let keyRef = useRef(null);
|
||||
let AvoidRefocusScrollContext =
|
||||
createContext<AvoidRefocusScrollContextValue>(null);
|
||||
|
||||
let value = useMemo(
|
||||
type AvoidRefocusScrollProviderProps = {
|
||||
children: ReactElement;
|
||||
};
|
||||
|
||||
export function AvoidRefocusScrollProvider({
|
||||
children,
|
||||
}: AvoidRefocusScrollProviderProps) {
|
||||
let keyRef = useRef<string>(null);
|
||||
|
||||
let value = useMemo<AvoidRefocusScrollContextValue>(
|
||||
() => ({
|
||||
keyRef,
|
||||
onKeyChange: key => {
|
||||
@@ -56,7 +74,10 @@ export function AvoidRefocusScrollProvider({ children }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function useProperFocus(ref, shouldFocus) {
|
||||
export function useProperFocus(
|
||||
ref: RefObject<HTMLElement>,
|
||||
shouldFocus: boolean,
|
||||
): void {
|
||||
let context = useContext(AvoidRefocusScrollContext);
|
||||
let prevShouldFocus = useRef(null);
|
||||
|
||||
@@ -68,7 +89,7 @@ export function useProperFocus(ref, shouldFocus) {
|
||||
let selector = 'input,button,div[tabindex]';
|
||||
let focusEl = view.matches(selector)
|
||||
? view
|
||||
: view.querySelector(selector);
|
||||
: view.querySelector<HTMLElement>(selector);
|
||||
|
||||
if (shouldFocus && focusEl) {
|
||||
focusElement(focusEl, context);
|
||||
@@ -5,14 +5,20 @@ import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
type Dispatch,
|
||||
type ReactElement,
|
||||
} from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { listen } from 'loot-core/src/platform/client/fetch';
|
||||
import * as undo from 'loot-core/src/platform/client/undo';
|
||||
import { type UndoState } from 'loot-core/src/server/undo';
|
||||
import { isNonProductionEnvironment } from 'loot-core/src/shared/environment';
|
||||
|
||||
function iterateRange(range, func) {
|
||||
type Range<T> = { start: T; end: T | null };
|
||||
type Item = { id: string };
|
||||
|
||||
function iterateRange(range: Range<number>, func: (i: number) => void): void {
|
||||
let from = Math.min(range.start, range.end);
|
||||
let to = Math.max(range.start, range.end);
|
||||
|
||||
@@ -21,9 +27,35 @@ function iterateRange(range, func) {
|
||||
}
|
||||
}
|
||||
|
||||
export default function useSelected(name, items, initialSelectedIds) {
|
||||
type State = {
|
||||
selectedRange: Range<string> | null;
|
||||
selectedItems: Set<string>;
|
||||
};
|
||||
|
||||
type WithOptionalMouseEvent = {
|
||||
event?: MouseEvent;
|
||||
};
|
||||
type SelectAction = {
|
||||
type: 'select';
|
||||
id: string;
|
||||
} & WithOptionalMouseEvent;
|
||||
type SelectNoneAction = {
|
||||
type: 'select-none';
|
||||
} & WithOptionalMouseEvent;
|
||||
type SelectAllAction = {
|
||||
type: 'select-all';
|
||||
ids?: string[];
|
||||
} & WithOptionalMouseEvent;
|
||||
|
||||
type Actions = SelectAction | SelectNoneAction | SelectAllAction;
|
||||
|
||||
export default function useSelected<T extends Item>(
|
||||
name: string,
|
||||
items: T[],
|
||||
initialSelectedIds: string[],
|
||||
) {
|
||||
let [state, dispatch] = useReducer(
|
||||
(state, action) => {
|
||||
(state: State, action: Actions) => {
|
||||
switch (action.type) {
|
||||
case 'select': {
|
||||
let { selectedRange } = state;
|
||||
@@ -34,8 +66,8 @@ export default function useSelected(name, items, initialSelectedIds) {
|
||||
let idx = items.findIndex(p => p.id === id);
|
||||
let startIdx = items.findIndex(p => p.id === selectedRange.start);
|
||||
let endIdx = items.findIndex(p => p.id === selectedRange.end);
|
||||
let range;
|
||||
let deleteUntil;
|
||||
let range: Range<number>;
|
||||
let deleteUntil: Range<number>;
|
||||
|
||||
if (endIdx === -1) {
|
||||
range = { start: startIdx, end: idx };
|
||||
@@ -93,7 +125,7 @@ export default function useSelected(name, items, initialSelectedIds) {
|
||||
}
|
||||
|
||||
case 'select-none':
|
||||
return { ...state, selectedItems: new Set() };
|
||||
return { ...state, selectedItems: new Set<string>() };
|
||||
|
||||
case 'select-all':
|
||||
return {
|
||||
@@ -106,12 +138,12 @@ export default function useSelected(name, items, initialSelectedIds) {
|
||||
};
|
||||
|
||||
default:
|
||||
throw new Error('Unexpected action: ' + action.type);
|
||||
throw new Error('Unexpected action: ' + JSON.stringify(action));
|
||||
}
|
||||
},
|
||||
null,
|
||||
() => ({
|
||||
selectedItems: new Set(initialSelectedIds || []),
|
||||
selectedItems: new Set<string>(initialSelectedIds || []),
|
||||
selectedRange:
|
||||
initialSelectedIds && initialSelectedIds.length === 1
|
||||
? { start: initialSelectedIds[0], end: null }
|
||||
@@ -165,7 +197,7 @@ export default function useSelected(name, items, initialSelectedIds) {
|
||||
let lastUndoState = useSelector(state => state.app.lastUndoState);
|
||||
|
||||
useEffect(() => {
|
||||
function onUndo({ messages, undoTag }) {
|
||||
function onUndo({ messages, undoTag }: UndoState) {
|
||||
let tagged = undo.getTaggedState(undoTag);
|
||||
|
||||
let deletedIds = new Set(
|
||||
@@ -174,11 +206,7 @@ export default function useSelected(name, items, initialSelectedIds) {
|
||||
.map(msg => msg.row),
|
||||
);
|
||||
|
||||
if (
|
||||
tagged &&
|
||||
tagged.selectedItems &&
|
||||
tagged.selectedItems.name === name
|
||||
) {
|
||||
if (tagged?.selectedItems?.name === name) {
|
||||
dispatch({
|
||||
type: 'select-all',
|
||||
// Coerce the Set into an array
|
||||
@@ -198,13 +226,12 @@ export default function useSelected(name, items, initialSelectedIds) {
|
||||
|
||||
return {
|
||||
items: state.selectedItems,
|
||||
setItems: state.setSelectedItems,
|
||||
dispatch,
|
||||
};
|
||||
}
|
||||
|
||||
let SelectedDispatch = createContext(null);
|
||||
let SelectedItems = createContext(null);
|
||||
let SelectedDispatch = createContext<(action: Actions) => void>(null);
|
||||
let SelectedItems = createContext<Set<unknown>>(null);
|
||||
|
||||
export function useSelectedDispatch() {
|
||||
return useContext(SelectedDispatch);
|
||||
@@ -214,7 +241,17 @@ export function useSelectedItems() {
|
||||
return useContext(SelectedItems);
|
||||
}
|
||||
|
||||
export function SelectedProvider({ instance, fetchAllIds, children }) {
|
||||
type SelectedProviderProps<T extends Item> = {
|
||||
instance: ReturnType<typeof useSelected<T>>;
|
||||
fetchAllIds?: () => Promise<string[]>;
|
||||
children: ReactElement;
|
||||
};
|
||||
|
||||
export function SelectedProvider<T extends Item>({
|
||||
instance,
|
||||
fetchAllIds,
|
||||
children,
|
||||
}: SelectedProviderProps<T>) {
|
||||
let latestItems = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -222,7 +259,7 @@ export function SelectedProvider({ instance, fetchAllIds, children }) {
|
||||
}, [instance.items]);
|
||||
|
||||
let dispatch = useCallback(
|
||||
async action => {
|
||||
async (action: Actions) => {
|
||||
if (!action.event && isNonProductionEnvironment()) {
|
||||
throw new Error('SelectedDispatch actions must have an event');
|
||||
}
|
||||
@@ -257,24 +294,33 @@ export function SelectedProvider({ instance, fetchAllIds, children }) {
|
||||
);
|
||||
}
|
||||
|
||||
type SelectedProviderWithItemsProps<T extends Item> = {
|
||||
name: string;
|
||||
items: T[];
|
||||
initialSelectedIds: string[];
|
||||
fetchAllIds: () => Promise<string[]>;
|
||||
registerDispatch?: (dispatch: Dispatch<Actions>) => void;
|
||||
children: ReactElement;
|
||||
};
|
||||
|
||||
// This can be helpful in class components if you cannot use the
|
||||
// custom hook
|
||||
export function SelectedProviderWithItems({
|
||||
export function SelectedProviderWithItems<T extends Item>({
|
||||
name,
|
||||
items,
|
||||
initialSelectedIds,
|
||||
fetchAllIds,
|
||||
registerDispatch,
|
||||
children,
|
||||
}) {
|
||||
let selected = useSelected(name, items, initialSelectedIds);
|
||||
}: SelectedProviderWithItemsProps<T>) {
|
||||
let selected = useSelected<T>(name, items, initialSelectedIds);
|
||||
|
||||
useEffect(() => {
|
||||
registerDispatch?.(selected.dispatch);
|
||||
}, [registerDispatch]);
|
||||
|
||||
return (
|
||||
<SelectedProvider
|
||||
<SelectedProvider<T>
|
||||
instance={selected}
|
||||
fetchAllIds={fetchAllIds}
|
||||
children={children}
|
||||
@@ -2,16 +2,19 @@ export type UndoState = {
|
||||
id?: string;
|
||||
url: unknown;
|
||||
openModal: unknown;
|
||||
selectedItems: unknown;
|
||||
selectedItems: {
|
||||
name: string;
|
||||
items: Set<string>;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export function setUndoState(
|
||||
name: keyof Omit<UndoState, 'id'>,
|
||||
value: unknown,
|
||||
export function setUndoState<K extends keyof Omit<UndoState, 'id'>>(
|
||||
name: K,
|
||||
value: UndoState[K],
|
||||
): void;
|
||||
export type SetUndoState = typeof setUndoState;
|
||||
|
||||
export function getUndoState(name: keyof UndoState): unknown;
|
||||
export function getUndoState<K extends keyof UndoState>(name: K): UndoState[K];
|
||||
export type GetUndoState = typeof getUndoState;
|
||||
|
||||
export function getTaggedState(id: string): UndoState | undefined;
|
||||
|
||||
6
upcoming-release-notes/1479.md
Normal file
6
upcoming-release-notes/1479.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Migrate hooks from native JS to TypeScript
|
||||
Reference in New Issue
Block a user