diff --git a/packages/desktop-client/src/components/transactions/TransactionsTable.jsx b/packages/desktop-client/src/components/transactions/TransactionsTable.jsx index 34c684b472..6215a57fb3 100644 --- a/packages/desktop-client/src/components/transactions/TransactionsTable.jsx +++ b/packages/desktop-client/src/components/transactions/TransactionsTable.jsx @@ -2094,7 +2094,7 @@ export const TransactionTable = forwardRef((props, ref) => { result = props.transactions.filter((t, idx) => { if (t.parent_id) { if (idx >= index) { - return splitsExpanded.expanded(t.parent_id); + return splitsExpanded.isExpanded(t.parent_id); } else if (prevSplitsExpanded.current) { return prevSplitsExpanded.current.expanded(t.parent_id); } @@ -2113,7 +2113,7 @@ export const TransactionTable = forwardRef((props, ref) => { result = props.transactions.filter(t => { if (t.parent_id) { - return splitsExpanded.expanded(t.parent_id); + return splitsExpanded.isExpanded(t.parent_id); } return true; }); @@ -2584,7 +2584,7 @@ export const TransactionTable = forwardRef((props, ref) => { transactionsByParent={transactionsByParent} transferAccountsByTransaction={transferAccountsByTransaction} selectedItems={selectedItems} - isExpanded={splitsExpanded.expanded} + isExpanded={splitsExpanded.isExpanded} onSave={onSave} onDelete={onDelete} onDuplicate={onDuplicate} diff --git a/packages/desktop-client/src/hooks/useSplitsExpanded.jsx b/packages/desktop-client/src/hooks/useSplitsExpanded.jsx deleted file mode 100644 index 8b8bb4ed24..0000000000 --- a/packages/desktop-client/src/hooks/useSplitsExpanded.jsx +++ /dev/null @@ -1,117 +0,0 @@ -import React, { - createContext, - useMemo, - useEffect, - useContext, - useReducer, -} from 'react'; -import { useSelector, useDispatch } from 'react-redux'; - -const SplitsExpandedContext = createContext(null); - -export function useSplitsExpanded() { - const data = useContext(SplitsExpandedContext); - - return useMemo( - () => ({ - ...data, - expanded: id => - data.state.mode === 'collapse' - ? !data.state.ids.has(id) - : data.state.ids.has(id), - }), - [data], - ); -} - -export function SplitsExpandedProvider({ children, initialMode = 'expand' }) { - const cachedState = useSelector(state => state.app.lastSplitState); - const reduxDispatch = useDispatch(); - - const [state, dispatch] = useReducer( - (state, action) => { - switch (action.type) { - case 'toggle-split': { - const ids = new Set([...state.ids]); - const { id } = action; - if (ids.has(id)) { - ids.delete(id); - } else { - ids.add(id); - } - return { ...state, ids }; - } - case 'open-split': { - const ids = new Set([...state.ids]); - const { id } = action; - if (state.mode === 'collapse') { - ids.delete(id); - } else { - ids.add(id); - } - return { ...state, ids }; - } - case 'close-splits': { - const ids = new Set([...state.ids]); - action.ids.forEach(id => { - if (state.mode === 'collapse') { - ids.add(id); - } else { - ids.delete(id); - } - }); - return { ...state, ids }; - } - case 'set-mode': { - return { - ...state, - mode: action.mode, - ids: new Set(), - transitionId: null, - }; - } - case 'switch-mode': - if (state.transitionId != null) { - // You can only transition once at a time - return state; - } - - return { - ...state, - mode: state.mode === 'expand' ? 'collapse' : 'expand', - transitionId: action.id, - ids: new Set(), - }; - case 'finish-switch-mode': - return { ...state, transitionId: null }; - default: - throw new Error('Unknown action type: ' + action.type); - } - }, - cachedState.current || { ids: new Set(), mode: initialMode }, - ); - - useEffect(() => { - if (state.transitionId != null) { - // This timeout allows animations to finish - setTimeout(() => { - dispatch({ type: 'finish-switch-mode' }); - }, 250); - } - }, [state.transitionId]); - - useEffect(() => { - // In a finished state, cache the state - if (state.transitionId == null) { - reduxDispatch({ type: 'SET_LAST_SPLIT_STATE', splitState: state }); - } - }, [reduxDispatch, state]); - - const value = useMemo(() => ({ state, dispatch }), [state, dispatch]); - - return ( - - {children} - - ); -} diff --git a/packages/desktop-client/src/hooks/useSplitsExpanded.tsx b/packages/desktop-client/src/hooks/useSplitsExpanded.tsx new file mode 100644 index 0000000000..5f6068e674 --- /dev/null +++ b/packages/desktop-client/src/hooks/useSplitsExpanded.tsx @@ -0,0 +1,178 @@ +import React, { + createContext, + useMemo, + useEffect, + useContext, + useReducer, + type ReactNode, +} from 'react'; +import { useSelector, useDispatch } from 'react-redux'; + +import { type SplitState } from 'loot-core/client/state-types/app'; + +type SplitsExpandedState = SplitState & { + transitionId?: string; +}; + +type SplitsExpandedAction = + | { + type: 'toggle-split'; + id: string; + } + | { + type: 'open-split'; + id: string; + } + | { + type: 'close-splits'; + ids: string[]; + } + | { + type: 'set-mode'; + mode: 'expand' | 'collapse'; + } + | { + type: 'switch-mode'; + id: string; + } + | { + type: 'finish-switch-mode'; + }; + +type SplitsExpandedContextValue = { + state: SplitsExpandedState; + dispatch: (action: SplitsExpandedAction) => void; +}; +const SplitsExpandedContext = createContext< + SplitsExpandedContextValue | undefined +>(undefined); + +type UseSplitsExpandedResult = SplitsExpandedContextValue & { + isExpanded: (id: string) => boolean; +}; + +export function useSplitsExpanded(): UseSplitsExpandedResult { + const data = useContext(SplitsExpandedContext); + + if (!data) { + throw new Error( + 'useSplitsExpanded must be used within a SplitsExpandedProvider', + ); + } + + return useMemo( + () => ({ + ...data, + isExpanded: id => + data.state.mode === 'collapse' + ? !data.state.ids.has(id) + : data.state.ids.has(id), + }), + [data], + ); +} + +function splitsExpandedReducer( + state: SplitsExpandedState, + action: SplitsExpandedAction, +): SplitsExpandedState { + switch (action.type) { + case 'toggle-split': { + const ids = new Set([...state.ids]); + const { id } = action; + if (ids.has(id)) { + ids.delete(id); + } else { + ids.add(id); + } + return { ...state, ids }; + } + case 'open-split': { + const ids = new Set([...state.ids]); + const { id } = action; + if (state.mode === 'collapse') { + ids.delete(id); + } else { + ids.add(id); + } + return { ...state, ids }; + } + case 'close-splits': { + const ids = new Set([...state.ids]); + action.ids.forEach(id => { + if (state.mode === 'collapse') { + ids.add(id); + } else { + ids.delete(id); + } + }); + return { ...state, ids }; + } + case 'set-mode': { + return { + ...state, + mode: action.mode, + ids: new Set(), + transitionId: undefined, + }; + } + case 'switch-mode': + if (state.transitionId != null) { + // You can only transition once at a time + return state; + } + + return { + ...state, + mode: state.mode === 'expand' ? 'collapse' : 'expand', + transitionId: action.id, + ids: new Set(), + }; + case 'finish-switch-mode': + return { ...state, transitionId: undefined }; + default: + throw new Error(`Unknown action: ${JSON.stringify(action)}`); + } +} + +type SplitsExpandedProviderProps = { + children: ReactNode; + initialMode?: 'expand' | 'collapse'; +}; + +export function SplitsExpandedProvider({ + children, + initialMode = 'expand', +}: SplitsExpandedProviderProps) { + const cachedState = useSelector(state => state.app.lastSplitState); + const reduxDispatch = useDispatch(); + + const [state, dispatch] = useReducer( + splitsExpandedReducer, + cachedState.current || { ids: new Set(), mode: initialMode }, + ); + + useEffect(() => { + if (state.transitionId != null) { + // This timeout allows animations to finish + setTimeout(() => { + dispatch({ type: 'finish-switch-mode' }); + }, 250); + } + }, [state.transitionId]); + + useEffect(() => { + // In a finished state, cache the state + if (state.transitionId == null) { + reduxDispatch({ type: 'SET_LAST_SPLIT_STATE', splitState: state }); + } + }, [reduxDispatch, state]); + + const value = useMemo(() => ({ state, dispatch }), [state, dispatch]); + + return ( + + {children} + + ); +}