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}
+
+ );
+}