From 08cbdab2a1b7f3e5de8f8b9994ad13a7e19694af Mon Sep 17 00:00:00 2001 From: Joel Jeremy Marquez <joeljeremy.marquez@gmail.com> Date: Mon, 12 Feb 2024 07:07:44 -0800 Subject: [PATCH] Hooks for frequently made operations (#2293) * Hooks for frequently made operations * Release notes * Fix typecheck errors * Remove useGlobalPrefs * Add null checks * Fix showCleared pref * Add loaded flag for categories, accounts and payees state * Refactor to reduce unnecessary states * Fix eslint errors * Fix hooks deps * Add useEffect * Fix typecheck error * Set local and global pref hooks * Fix lint error * VRT * Fix typecheck error * Remove eager loading * Fix typecheck error * Fix typo * Fix typecheck error * Update useTheme * Typecheck errors * Typecheck error * defaultValue * Explicitly check undefined * Remove useGlobalPref and useLocalPref defaults * Fix default prefs * Default value * Fix lint error * Set default theme * Default date format in Account * Update packages/desktop-client/src/style/theme.tsx Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv> --------- Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv> --- ...with-budgeted-amounts-1-chromium-linux.png | Bin 25638 -> 24585 bytes ...with-budgeted-amounts-2-chromium-linux.png | Bin 26212 -> 24829 bytes .../desktop-client/src/components/App.tsx | 47 +-- .../src/components/BankSyncStatus.tsx | 7 +- .../src/components/FinancesApp.tsx | 41 +-- .../src/components/LoggedInUser.tsx | 7 +- .../src/components/ManageRules.tsx | 23 +- .../src/components/MobileWebMessage.tsx | 20 +- .../desktop-client/src/components/Modals.tsx | 39 +- .../src/components/Notifications.tsx | 11 +- .../src/components/PrivacyFilter.tsx | 3 +- .../src/components/ThemeSelector.tsx | 11 +- .../src/components/Titlebar.tsx | 36 +- .../src/components/UpdateNotification.tsx | 12 +- .../src/components/accounts/Account.jsx | 57 ++- .../components/accounts/AccountSyncCheck.jsx | 3 +- .../src/components/accounts/Header.jsx | 7 +- .../src/components/accounts/MobileAccount.jsx | 34 +- .../components/accounts/MobileAccounts.jsx | 37 +- .../autocomplete/AccountAutocomplete.tsx | 4 +- .../autocomplete/PayeeAutocomplete.tsx | 10 +- .../components/budget/BudgetCategories.jsx | 68 +++- .../src/components/budget/BudgetTable.jsx | 70 ++-- .../components/budget/DynamicBudgetTable.tsx | 14 +- .../src/components/budget/MobileBudget.tsx | 26 +- .../components/budget/MobileBudgetTable.jsx | 40 +- .../components/budget/MonthCountSelector.tsx | 2 +- .../src/components/budget/index.tsx | 347 +++++------------- .../budget/rollover/RolloverContext.tsx | 3 - .../src/components/filters/AppliedFilters.tsx | 2 +- .../src/components/filters/FiltersMenu.jsx | 8 +- .../src/components/modals/CloseAccount.tsx | 13 +- .../modals/ConfirmCategoryDelete.tsx | 16 +- .../src/components/modals/EditField.jsx | 12 +- .../src/components/modals/EditRule.jsx | 7 +- .../components/modals/ImportTransactions.jsx | 9 +- .../src/components/modals/LoadBackup.jsx | 13 +- .../components/modals/MergeUnusedPayees.jsx | 7 +- .../modals/SelectLinkedAccounts.jsx | 3 +- .../components/modals/SwitchBudgetType.tsx | 9 +- .../payees/ManagePayeesWithData.jsx | 3 +- .../src/components/reports/Overview.jsx | 4 +- .../components/reports/graphs/AreaGraph.tsx | 2 +- .../components/reports/graphs/BarGraph.tsx | 2 +- .../reports/graphs/CashFlowGraph.tsx | 2 +- .../reports/graphs/StackedBarGraph.tsx | 2 +- .../reports/reports/CustomReport.jsx | 30 +- .../components/reports/reports/NetWorth.jsx | 4 +- .../reports/spreadsheets/filterEmptyRows.ts | 2 +- .../spreadsheets/grouped-spreadsheet.ts | 2 +- .../src/components/rules/ScheduleValue.tsx | 9 +- .../src/components/rules/Value.tsx | 19 +- .../schedules/DiscoverSchedules.tsx | 8 +- .../components/schedules/ScheduleDetails.jsx | 12 +- .../components/schedules/SchedulesTable.tsx | 21 +- .../src/components/select/DateSelect.tsx | 14 +- .../select/RecurringSchedulePicker.jsx | 18 +- .../src/components/settings/Encryption.tsx | 9 +- .../src/components/settings/Experimental.tsx | 20 +- .../src/components/settings/Export.tsx | 12 +- .../src/components/settings/Format.tsx | 44 +-- .../src/components/settings/Global.tsx | 13 +- .../src/components/settings/Reset.tsx | 9 +- .../src/components/settings/Themes.tsx | 10 +- .../src/components/settings/index.tsx | 22 +- .../src/components/sidebar/Accounts.tsx | 86 ++--- .../src/components/sidebar/Sidebar.tsx | 208 +++++++---- .../components/sidebar/SidebarProvider.tsx | 52 +++ .../components/sidebar/SidebarWithData.tsx | 181 --------- .../src/components/sidebar/index.tsx | 77 +--- .../transactions/MobileTransaction.jsx | 16 +- .../transactions/SimpleTransactionsTable.jsx | 18 +- .../src/components/util/DisplayId.tsx | 55 ++- .../src/components/util/GenericInput.jsx | 5 +- .../desktop-client/src/hooks/useAccount.ts | 8 + .../desktop-client/src/hooks/useAccounts.ts | 20 + .../src/hooks/useBudgetedAccounts.ts | 14 + .../desktop-client/src/hooks/useCategories.ts | 23 +- .../src/hooks/useClosedAccounts.ts | 11 + .../desktop-client/src/hooks/useDateFormat.ts | 7 + .../src/hooks/useFailedAccounts.ts | 7 + .../src/hooks/useFeatureFlag.ts | 17 +- .../desktop-client/src/hooks/useGlobalPref.ts | 27 ++ .../desktop-client/src/hooks/useLocalPref.ts | 27 ++ .../desktop-client/src/hooks/useLocalPrefs.ts | 7 + .../src/hooks/useOffBudgetAccounts.ts | 14 + packages/desktop-client/src/hooks/usePayee.ts | 8 + .../desktop-client/src/hooks/usePayees.ts | 20 + .../src/hooks/usePrivacyMode.ts | 9 + .../desktop-client/src/hooks/useSelected.tsx | 7 +- .../src/hooks/useSyncServerStatus.ts | 7 +- .../src/hooks/useUpdatedAccounts.ts | 7 + packages/desktop-client/src/style/theme.tsx | 16 +- .../loot-core/src/client/actions/account.ts | 7 + .../loot-core/src/client/actions/prefs.ts | 4 +- .../src/client/data-hooks/accounts.tsx | 36 -- .../src/client/data-hooks/payees.tsx | 36 -- packages/loot-core/src/client/privacy.ts | 5 - .../loot-core/src/client/reducers/queries.ts | 6 + .../src/client/state-types/modals.d.ts | 2 +- .../src/client/state-types/queries.d.ts | 3 + packages/loot-core/src/shared/categories.ts | 106 ------ packages/loot-core/src/types/prefs.d.ts | 1 + upcoming-release-notes/2293.md | 6 + 104 files changed, 1045 insertions(+), 1492 deletions(-) create mode 100644 packages/desktop-client/src/components/sidebar/SidebarProvider.tsx delete mode 100644 packages/desktop-client/src/components/sidebar/SidebarWithData.tsx create mode 100644 packages/desktop-client/src/hooks/useAccount.ts create mode 100644 packages/desktop-client/src/hooks/useAccounts.ts create mode 100644 packages/desktop-client/src/hooks/useBudgetedAccounts.ts create mode 100644 packages/desktop-client/src/hooks/useClosedAccounts.ts create mode 100644 packages/desktop-client/src/hooks/useDateFormat.ts create mode 100644 packages/desktop-client/src/hooks/useFailedAccounts.ts create mode 100644 packages/desktop-client/src/hooks/useGlobalPref.ts create mode 100644 packages/desktop-client/src/hooks/useLocalPref.ts create mode 100644 packages/desktop-client/src/hooks/useLocalPrefs.ts create mode 100644 packages/desktop-client/src/hooks/useOffBudgetAccounts.ts create mode 100644 packages/desktop-client/src/hooks/usePayee.ts create mode 100644 packages/desktop-client/src/hooks/usePayees.ts create mode 100644 packages/desktop-client/src/hooks/usePrivacyMode.ts create mode 100644 packages/desktop-client/src/hooks/useUpdatedAccounts.ts delete mode 100644 packages/loot-core/src/client/data-hooks/accounts.tsx delete mode 100644 packages/loot-core/src/client/data-hooks/payees.tsx delete mode 100644 packages/loot-core/src/client/privacy.ts delete mode 100644 packages/loot-core/src/shared/categories.ts create mode 100644 upcoming-release-notes/2293.md diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png index fa4ff40ee97ecb8e257683496581ccbe298620d7..e1ed15d720db70723d09bf378709c32a6c0559ee 100644 GIT binary patch literal 24585 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 E0Q$GZ!2kdN literal 25638 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 diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png index 4ac36791ac107c7087a25023cfa1651d2a316408..866d234e40ee46bb86bdb7bf2f37d07f5fec9b77 100644 GIT binary patch literal 24829 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 XeM+Wl$a4d3kLt0Sj%u;8^_zbH{;yuG literal 26212 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 NWkq#`GI_J$e*^BQvSa`N diff --git a/packages/desktop-client/src/components/App.tsx b/packages/desktop-client/src/components/App.tsx index 696c7633e..8323f7247 100644 --- a/packages/desktop-client/src/components/App.tsx +++ b/packages/desktop-client/src/components/App.tsx @@ -7,17 +7,15 @@ import { } from 'react-error-boundary'; import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type AppState } from 'loot-core/client/state-types/app'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; import * as Platform from 'loot-core/src/client/platform'; +import { type State } from 'loot-core/src/client/state-types'; import { init as initConnection, send, } from 'loot-core/src/platform/client/fetch'; -import { type GlobalPrefs } from 'loot-core/src/types/prefs'; import { useActions } from '../hooks/useActions'; +import { useLocalPref } from '../hooks/useLocalPref'; import { installPolyfills } from '../polyfills'; import { ResponsiveProvider } from '../ResponsiveProvider'; import { styles, hasHiddenScrollbars, ThemeStyle } from '../style'; @@ -34,26 +32,13 @@ import { UpdateNotification } from './UpdateNotification'; type AppInnerProps = { budgetId: string; cloudFileId: string; - loadingText: string; - loadBudget: ( - id: string, - loadingText?: string, - options?: object, - ) => Promise<void>; - closeBudget: () => Promise<void>; - loadGlobalPrefs: () => Promise<GlobalPrefs>; }; -function AppInner({ - budgetId, - cloudFileId, - loadingText, - loadBudget, - closeBudget, - loadGlobalPrefs, -}: AppInnerProps) { +function AppInner({ budgetId, cloudFileId }: AppInnerProps) { const [initializing, setInitializing] = useState(true); const { showBoundary: showErrorBoundary } = useErrorBoundary(); + const loadingText = useSelector((state: State) => state.app.loadingText); + const { loadBudget, closeBudget, loadGlobalPrefs } = useActions(); async function init() { const socketName = await global.Actual.getServerSocket(); @@ -126,16 +111,9 @@ function ErrorFallback({ error }: FallbackProps) { } export function App() { - const budgetId = useSelector<State, PrefsState['local']['id']>( - state => state.prefs.local && state.prefs.local.id, - ); - const cloudFileId = useSelector<State, PrefsState['local']['cloudFileId']>( - state => state.prefs.local && state.prefs.local.cloudFileId, - ); - const loadingText = useSelector<State, AppState['loadingText']>( - state => state.app.loadingText, - ); - const { loadBudget, closeBudget, loadGlobalPrefs, sync } = useActions(); + const [budgetId] = useLocalPref('id'); + const [cloudFileId] = useLocalPref('cloudFileId'); + const { sync } = useActions(); const [hiddenScrollbars, setHiddenScrollbars] = useState( hasHiddenScrollbars(), ); @@ -184,14 +162,7 @@ export function App() { {process.env.REACT_APP_REVIEW_ID && !Platform.isPlaywright && ( <DevelopmentTopBar /> )} - <AppInner - budgetId={budgetId} - cloudFileId={cloudFileId} - loadingText={loadingText} - loadBudget={loadBudget} - closeBudget={closeBudget} - loadGlobalPrefs={loadGlobalPrefs} - /> + <AppInner budgetId={budgetId} cloudFileId={cloudFileId} /> </ErrorBoundary> <ThemeStyle /> </View> diff --git a/packages/desktop-client/src/components/BankSyncStatus.tsx b/packages/desktop-client/src/components/BankSyncStatus.tsx index 3fde066fa..f49b92007 100644 --- a/packages/desktop-client/src/components/BankSyncStatus.tsx +++ b/packages/desktop-client/src/components/BankSyncStatus.tsx @@ -2,8 +2,7 @@ import React from 'react'; import { useSelector } from 'react-redux'; import { useTransition, animated } from 'react-spring'; -import { type State } from 'loot-core/client/state-types'; -import { type AccountState } from 'loot-core/client/state-types/account'; +import { type State } from 'loot-core/src/client/state-types'; import { theme, styles } from '../style'; @@ -12,8 +11,8 @@ import { Text } from './common/Text'; import { View } from './common/View'; export function BankSyncStatus() { - const accountsSyncing = useSelector<State, AccountState['accountsSyncing']>( - state => state.account.accountsSyncing, + const accountsSyncing = useSelector( + (state: State) => state.account.accountsSyncing, ); const name = accountsSyncing diff --git a/packages/desktop-client/src/components/FinancesApp.tsx b/packages/desktop-client/src/components/FinancesApp.tsx index 29c241a14..8024afe79 100644 --- a/packages/desktop-client/src/components/FinancesApp.tsx +++ b/packages/desktop-client/src/components/FinancesApp.tsx @@ -2,6 +2,7 @@ import React, { type ReactElement, useEffect, useMemo } from 'react'; import { DndProvider } from 'react-dnd'; import { HTML5Backend as Backend } from 'react-dnd-html5-backend'; +import { useSelector } from 'react-redux'; import { Route, Routes, @@ -13,12 +14,12 @@ import { import hotkeys from 'hotkeys-js'; -import { AccountsProvider } from 'loot-core/src/client/data-hooks/accounts'; -import { PayeesProvider } from 'loot-core/src/client/data-hooks/payees'; import { SpreadsheetProvider } from 'loot-core/src/client/SpreadsheetProvider'; +import { type State } from 'loot-core/src/client/state-types'; import { checkForUpdateNotification } from 'loot-core/src/client/update-notification'; import * as undo from 'loot-core/src/platform/client/undo'; +import { useAccounts } from '../hooks/useAccounts'; import { useActions } from '../hooks/useActions'; import { useNavigate } from '../hooks/useNavigate'; import { useResponsive } from '../ResponsiveProvider'; @@ -39,7 +40,8 @@ import { Reports } from './reports'; import { NarrowAlternate, WideComponent } from './responsive'; import { ScrollProvider } from './ScrollProvider'; import { Settings } from './settings'; -import { FloatableSidebar, SidebarProvider } from './sidebar'; +import { FloatableSidebar } from './sidebar'; +import { SidebarProvider } from './sidebar/SidebarProvider'; import { Titlebar, TitlebarProvider } from './Titlebar'; import { TransactionEdit } from './transactions/MobileTransaction'; @@ -71,18 +73,19 @@ function WideNotSupported({ children, redirectTo = '/budget' }) { return isNarrowWidth ? children : null; } -function RouterBehaviors({ getAccounts }) { +function RouterBehaviors() { const navigate = useNavigate(); + const accounts = useAccounts(); + const accountsLoaded = useSelector( + (state: State) => state.queries.accountsLoaded, + ); useEffect(() => { - // Get the accounts and check if any exist. If there are no - // accounts, we want to redirect the user to the All Accounts - // screen which will prompt them to add an account - getAccounts().then(accounts => { - if (accounts.length === 0) { - navigate('/accounts'); - } - }); - }, []); + // If there are no accounts, we want to redirect the user to + // the All Accounts screen which will prompt them to add an account + if (accountsLoaded && accounts.length === 0) { + navigate('/accounts'); + } + }, [accountsLoaded, accounts]); const location = useLocation(); const href = useHref(location); @@ -116,7 +119,7 @@ function FinancesAppWithoutContext() { return ( <BrowserRouter> - <RouterBehaviors getAccounts={actions.getAccounts} /> + <RouterBehaviors /> <ExposeNavigate /> <View style={{ height: '100%' }}> @@ -265,13 +268,9 @@ export function FinancesApp() { <TitlebarProvider> <SidebarProvider> <BudgetMonthCountProvider> - <PayeesProvider> - <AccountsProvider> - <DndProvider backend={Backend}> - <ScrollProvider>{app}</ScrollProvider> - </DndProvider> - </AccountsProvider> - </PayeesProvider> + <DndProvider backend={Backend}> + <ScrollProvider>{app}</ScrollProvider> + </DndProvider> </BudgetMonthCountProvider> </SidebarProvider> </TitlebarProvider> diff --git a/packages/desktop-client/src/components/LoggedInUser.tsx b/packages/desktop-client/src/components/LoggedInUser.tsx index bfd037ad4..f1043b764 100644 --- a/packages/desktop-client/src/components/LoggedInUser.tsx +++ b/packages/desktop-client/src/components/LoggedInUser.tsx @@ -2,8 +2,7 @@ import React, { useState, useEffect } from 'react'; import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type UserState } from 'loot-core/client/state-types/user'; +import { type State } from 'loot-core/src/client/state-types'; import { useActions } from '../hooks/useActions'; import { theme, styles, type CSSProperties } from '../style'; @@ -25,9 +24,7 @@ export function LoggedInUser({ style, color, }: LoggedInUserProps) { - const userData = useSelector<State, UserState['data']>( - state => state.user.data, - ); + const userData = useSelector((state: State) => state.user.data); const { getUserData, signOut, closeBudget } = useActions(); const [loading, setLoading] = useState(true); const [menuOpen, setMenuOpen] = useState(false); diff --git a/packages/desktop-client/src/components/ManageRules.tsx b/packages/desktop-client/src/components/ManageRules.tsx index 38634a989..2459fcb44 100644 --- a/packages/desktop-client/src/components/ManageRules.tsx +++ b/packages/desktop-client/src/components/ManageRules.tsx @@ -7,10 +7,8 @@ import React, { type SetStateAction, type Dispatch, } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type QueriesState } from 'loot-core/client/state-types/queries'; import { pushModal } from 'loot-core/src/client/actions/modals'; import { initiallyLoadPayees } from 'loot-core/src/client/actions/queries'; import { send } from 'loot-core/src/platform/client/fetch'; @@ -19,7 +17,9 @@ import { mapField, friendlyOp } from 'loot-core/src/shared/rules'; import { describeSchedule } from 'loot-core/src/shared/schedules'; import { type RuleEntity } from 'loot-core/src/types/models'; +import { useAccounts } from '../hooks/useAccounts'; import { useCategories } from '../hooks/useCategories'; +import { usePayees } from '../hooks/usePayees'; import { useSelected, SelectedProvider } from '../hooks/useSelected'; import { theme } from '../style'; @@ -105,18 +105,13 @@ function ManageRulesContent({ const { data: schedules } = SchedulesQuery.useQuery(); const { list: categories } = useCategories(); - const state = useSelector< - State, - { - payees: QueriesState['payees']; - accounts: QueriesState['accounts']; - schedules: ReturnType<(typeof SchedulesQuery)['useQuery']>; - } - >(state => ({ - payees: state.queries.payees, - accounts: state.queries.accounts, + const payees = usePayees(); + const accounts = useAccounts(); + const state = { + payees, + accounts, schedules, - })); + }; const filterData = useMemo( () => ({ ...state, diff --git a/packages/desktop-client/src/components/MobileWebMessage.tsx b/packages/desktop-client/src/components/MobileWebMessage.tsx index bc3e0c7ac..c54da6eed 100644 --- a/packages/desktop-client/src/components/MobileWebMessage.tsx +++ b/packages/desktop-client/src/components/MobileWebMessage.tsx @@ -1,10 +1,6 @@ import React, { useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; - -import { savePrefs } from 'loot-core/src/client/actions'; -import { type State } from 'loot-core/src/client/state-types'; -import { type PrefsState } from 'loot-core/src/client/state-types/prefs'; +import { useLocalPref } from '../hooks/useLocalPref'; import { useResponsive } from '../ResponsiveProvider'; import { theme, styles } from '../style'; @@ -16,30 +12,24 @@ import { Checkbox } from './forms'; const buttonStyle = { border: 0, fontSize: 15, padding: '10px 13px' }; export function MobileWebMessage() { - const hideMobileMessagePref = useSelector< - State, - PrefsState['local']['hideMobileMessage'] - >(state => { - return (state.prefs.local && state.prefs.local.hideMobileMessage) || true; - }); + const [hideMobileMessage = true, setHideMobileMessagePref] = + useLocalPref('hideMobileMessage'); const { isNarrowWidth } = useResponsive(); const [show, setShow] = useState( isNarrowWidth && - !hideMobileMessagePref && + !hideMobileMessage && !document.cookie.match(/hideMobileMessage=true/), ); const [requestDontRemindMe, setRequestDontRemindMe] = useState(false); - const dispatch = useDispatch(); - function onTry() { setShow(false); if (requestDontRemindMe) { // remember the pref indefinitely - dispatch(savePrefs({ hideMobileMessage: true })); + setHideMobileMessagePref(true); } else { // Set a cookie for 5 minutes const d = new Date(); diff --git a/packages/desktop-client/src/components/Modals.tsx b/packages/desktop-client/src/components/Modals.tsx index 93ae55592..56b599861 100644 --- a/packages/desktop-client/src/components/Modals.tsx +++ b/packages/desktop-client/src/components/Modals.tsx @@ -4,16 +4,10 @@ import { useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; import { type State } from 'loot-core/src/client/state-types'; -import { - type ModalsState, - type PopModalAction, -} from 'loot-core/src/client/state-types/modals'; -import { type PrefsState } from 'loot-core/src/client/state-types/prefs'; -import { type QueriesState } from 'loot-core/src/client/state-types/queries'; +import { type PopModalAction } from 'loot-core/src/client/state-types/modals'; import { send } from 'loot-core/src/platform/client/fetch'; import { useActions } from '../hooks/useActions'; -import { useCategories } from '../hooks/useCategories'; import { useSyncServerStatus } from '../hooks/useSyncServerStatus'; import { CategoryGroupMenu } from './modals/CategoryGroupMenu'; @@ -56,19 +50,8 @@ export type CommonModalProps = { }; export function Modals() { - const modalStack = useSelector<State, ModalsState['modalStack']>( - state => state.modals.modalStack, - ); - const isHidden = useSelector<State, ModalsState['isHidden']>( - state => state.modals.isHidden, - ); - const accounts = useSelector<State, QueriesState['accounts']>( - state => state.queries.accounts, - ); - const { grouped: categoryGroups, list: categories } = useCategories(); - const budgetId = useSelector<State, PrefsState['local']['id']>( - state => state.prefs.local && state.prefs.local.id, - ); + const modalStack = useSelector((state: State) => state.modals.modalStack); + const isHidden = useSelector((state: State) => state.modals.isHidden); const actions = useActions(); const location = useLocation(); @@ -118,8 +101,6 @@ export function Modals() { account={options.account} balance={options.balance} canDelete={options.canDelete} - accounts={accounts.filter(acct => acct.closed === 0)} - categoryGroups={categoryGroups} actions={actions} /> ); @@ -130,7 +111,6 @@ export function Modals() { modalProps={modalProps} externalAccounts={options.accounts} requisitionId={options.requisitionId} - localAccounts={accounts.filter(acct => acct.closed === 0)} actions={actions} syncSource={options.syncSource} /> @@ -140,15 +120,8 @@ export function Modals() { return ( <ConfirmCategoryDelete modalProps={modalProps} - category={ - 'category' in options && - categories.find(c => c.id === options.category) - } - group={ - 'group' in options && - categoryGroups.find(g => g.id === options.group) - } - categoryGroups={categoryGroups} + category={options.category} + group={options.group} onDelete={options.onDelete} /> ); @@ -166,7 +139,7 @@ export function Modals() { return ( <LoadBackup watchUpdates - budgetId={budgetId} + budgetId={options.budgetId} modalProps={modalProps} actions={actions} backupDisabled={false} diff --git a/packages/desktop-client/src/components/Notifications.tsx b/packages/desktop-client/src/components/Notifications.tsx index ef5db09f4..290b3ae7c 100644 --- a/packages/desktop-client/src/components/Notifications.tsx +++ b/packages/desktop-client/src/components/Notifications.tsx @@ -7,11 +7,8 @@ import React, { } from 'react'; import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import type { - NotificationWithId, - NotificationsState, -} from 'loot-core/src/client/state-types/notifications'; +import { type State } from 'loot-core/src/client/state-types'; +import type { NotificationWithId } from 'loot-core/src/client/state-types/notifications'; import { useActions } from '../hooks/useActions'; import { AnimatedLoading } from '../icons/AnimatedLoading'; @@ -242,8 +239,8 @@ function Notification({ export function Notifications({ style }: { style?: CSSProperties }) { const { removeNotification } = useActions(); - const notifications = useSelector<State, NotificationsState['notifications']>( - state => state.notifications.notifications, + const notifications = useSelector( + (state: State) => state.notifications.notifications, ); return ( <View diff --git a/packages/desktop-client/src/components/PrivacyFilter.tsx b/packages/desktop-client/src/components/PrivacyFilter.tsx index f8596b7c8..5a455dc60 100644 --- a/packages/desktop-client/src/components/PrivacyFilter.tsx +++ b/packages/desktop-client/src/components/PrivacyFilter.tsx @@ -7,8 +7,7 @@ import React, { type ReactNode, } from 'react'; -import { usePrivacyMode } from 'loot-core/src/client/privacy'; - +import { usePrivacyMode } from '../hooks/usePrivacyMode'; import { useResponsive } from '../ResponsiveProvider'; import { View } from './common/View'; diff --git a/packages/desktop-client/src/components/ThemeSelector.tsx b/packages/desktop-client/src/components/ThemeSelector.tsx index e8daef9d6..415089cb1 100644 --- a/packages/desktop-client/src/components/ThemeSelector.tsx +++ b/packages/desktop-client/src/components/ThemeSelector.tsx @@ -2,7 +2,6 @@ import React, { useState } from 'react'; import type { Theme } from 'loot-core/src/types/prefs'; -import { useActions } from '../hooks/useActions'; import { SvgMoonStars, SvgSun, SvgSystem } from '../icons/v2'; import { useResponsive } from '../ResponsiveProvider'; import { type CSSProperties, themeOptions, useTheme } from '../style'; @@ -16,8 +15,7 @@ type ThemeSelectorProps = { }; export function ThemeSelector({ style }: ThemeSelectorProps) { - const theme = useTheme(); - const { saveGlobalPrefs } = useActions(); + const [theme, switchTheme] = useTheme(); const [menuOpen, setMenuOpen] = useState(false); const { isNarrowWidth } = useResponsive(); @@ -28,12 +26,9 @@ export function ThemeSelector({ style }: ThemeSelectorProps) { auto: SvgSystem, } as const; - async function onMenuSelect(newTheme: string) { + function onMenuSelect(newTheme: Theme) { setMenuOpen(false); - - saveGlobalPrefs({ - theme: newTheme as Theme, - }); + switchTheme(newTheme); } const Icon = themeIcons[theme] || SvgSun; diff --git a/packages/desktop-client/src/components/Titlebar.tsx b/packages/desktop-client/src/components/Titlebar.tsx index b532412dc..8a08b283f 100644 --- a/packages/desktop-client/src/components/Titlebar.tsx +++ b/packages/desktop-client/src/components/Titlebar.tsx @@ -6,11 +6,8 @@ import React, { useContext, type ReactNode, } from 'react'; -import { useSelector } from 'react-redux'; import { Routes, Route, useLocation } from 'react-router-dom'; -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; import * as Platform from 'loot-core/src/client/platform'; import * as queries from 'loot-core/src/client/queries'; import { listen } from 'loot-core/src/platform/client/fetch'; @@ -18,6 +15,8 @@ import { type LocalPrefs } from 'loot-core/src/types/prefs'; import { useActions } from '../hooks/useActions'; import { useFeatureFlag } from '../hooks/useFeatureFlag'; +import { useGlobalPref } from '../hooks/useGlobalPref'; +import { useLocalPref } from '../hooks/useLocalPref'; import { useNavigate } from '../hooks/useNavigate'; import { SvgArrowLeft } from '../icons/v1'; import { @@ -41,7 +40,7 @@ import { View } from './common/View'; import { KeyHandlers } from './KeyHandlers'; import { LoggedInUser } from './LoggedInUser'; import { useServerURL } from './ServerContext'; -import { useSidebar } from './sidebar'; +import { useSidebar } from './sidebar/SidebarProvider'; import { useSheetValue } from './spreadsheet/useSheetValue'; import { ThemeSelector } from './ThemeSelector'; import { Tooltip } from './tooltips'; @@ -120,11 +119,8 @@ type PrivacyButtonProps = { }; function PrivacyButton({ style }: PrivacyButtonProps) { - const isPrivacyEnabled = useSelector< - State, - PrefsState['local']['isPrivacyEnabled'] - >(state => state.prefs.local?.isPrivacyEnabled); - const { savePrefs } = useActions(); + const [isPrivacyEnabled, setPrivacyEnabledPref] = + useLocalPref('isPrivacyEnabled'); const privacyIconStyle = { width: 15, height: 15 }; @@ -132,7 +128,7 @@ function PrivacyButton({ style }: PrivacyButtonProps) { <Button type="bare" aria-label={`${isPrivacyEnabled ? 'Disable' : 'Enable'} privacy mode`} - onClick={() => savePrefs({ isPrivacyEnabled: !isPrivacyEnabled })} + onClick={() => setPrivacyEnabledPref(!isPrivacyEnabled)} style={style} > {isPrivacyEnabled ? ( @@ -149,9 +145,7 @@ type SyncButtonProps = { isMobile?: boolean; }; function SyncButton({ style, isMobile = false }: SyncButtonProps) { - const cloudFileId = useSelector<State, PrefsState['local']['cloudFileId']>( - state => state.prefs.local?.cloudFileId, - ); + const [cloudFileId] = useLocalPref('cloudFileId'); const { sync } = useActions(); const [syncing, setSyncing] = useState(false); @@ -291,13 +285,8 @@ function SyncButton({ style, isMobile = false }: SyncButtonProps) { } function BudgetTitlebar() { - const maxMonths = useSelector<State, PrefsState['global']['maxMonths']>( - state => state.prefs.global?.maxMonths, - ); - const budgetType = useSelector<State, PrefsState['local']['budgetType']>( - state => state.prefs.local?.budgetType, - ); - const { saveGlobalPrefs } = useActions(); + const [maxMonths, setMaxMonthsPref] = useGlobalPref('maxMonths'); + const [budgetType] = useLocalPref('budgetType'); const { sendEvent } = useContext(TitlebarContext); const [loading, setLoading] = useState(false); @@ -326,7 +315,7 @@ function BudgetTitlebar() { <View style={{ flexDirection: 'row', alignItems: 'center' }}> <MonthCountSelector maxMonths={maxMonths || 1} - onChange={value => saveGlobalPrefs({ maxMonths: value })} + onChange={value => setMaxMonthsPref(value)} /> {reportBudgetEnabled && ( <View style={{ marginLeft: -5 }}> @@ -399,10 +388,7 @@ export function Titlebar({ style }: TitlebarProps) { const sidebar = useSidebar(); const { isNarrowWidth } = useResponsive(); const serverURL = useServerURL(); - const floatingSidebar = useSelector< - State, - PrefsState['global']['floatingSidebar'] - >(state => state.prefs.global?.floatingSidebar); + const [floatingSidebar] = useGlobalPref('floatingSidebar'); return isNarrowWidth ? null : ( <View diff --git a/packages/desktop-client/src/components/UpdateNotification.tsx b/packages/desktop-client/src/components/UpdateNotification.tsx index 18c992466..f70525028 100644 --- a/packages/desktop-client/src/components/UpdateNotification.tsx +++ b/packages/desktop-client/src/components/UpdateNotification.tsx @@ -1,8 +1,7 @@ import React from 'react'; import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type AppState } from 'loot-core/client/state-types/app'; +import { type State } from 'loot-core/src/client/state-types'; import { useActions } from '../hooks/useActions'; import { SvgClose } from '../icons/v1'; @@ -14,13 +13,10 @@ import { Text } from './common/Text'; import { View } from './common/View'; export function UpdateNotification() { - const updateInfo = useSelector<State, AppState['updateInfo']>( - state => state.app.updateInfo, + const updateInfo = useSelector((state: State) => state.app.updateInfo); + const showUpdateNotification = useSelector( + (state: State) => state.app.showUpdateNotification, ); - const showUpdateNotification = useSelector< - State, - AppState['showUpdateNotification'] - >(state => state.app.showUpdateNotification); const { updateApp, setAppState } = useActions(); diff --git a/packages/desktop-client/src/components/accounts/Account.jsx b/packages/desktop-client/src/components/accounts/Account.jsx index 1fb7e32a1..48f30a99a 100644 --- a/packages/desktop-client/src/components/accounts/Account.jsx +++ b/packages/desktop-client/src/components/accounts/Account.jsx @@ -26,7 +26,12 @@ import { } from 'loot-core/src/shared/transactions'; import { applyChanges, groupById } from 'loot-core/src/shared/util'; +import { useAccounts } from '../../hooks/useAccounts'; import { useCategories } from '../../hooks/useCategories'; +import { useDateFormat } from '../../hooks/useDateFormat'; +import { useFailedAccounts } from '../../hooks/useFailedAccounts'; +import { useLocalPref } from '../../hooks/useLocalPref'; +import { usePayees } from '../../hooks/usePayees'; import { SelectedProviderWithItems } from '../../hooks/useSelected'; import { styles, theme } from '../../style'; import { Button } from '../common/Button'; @@ -1532,23 +1537,41 @@ export function Account() { const location = useLocation(); const { grouped: categoryGroups } = useCategories(); - const state = useSelector(state => ({ - newTransactions: state.queries.newTransactions, - matchedTransactions: state.queries.matchedTransactions, - accounts: state.queries.accounts, - failedAccounts: state.account.failedAccounts, - dateFormat: state.prefs.local.dateFormat || 'MM/dd/yyyy', - hideFraction: state.prefs.local.hideFraction || false, - expandSplits: state.prefs.local['expand-splits'], - showBalances: params.id && state.prefs.local['show-balances-' + params.id], - showCleared: params.id && !state.prefs.local['hide-cleared-' + params.id], - showExtraBalances: - state.prefs.local['show-extra-balances-' + params.id || 'all-accounts'], - payees: state.queries.payees, - modalShowing: state.modals.modalStack.length > 0, - accountsSyncing: state.account.accountsSyncing, - lastUndoState: state.app.lastUndoState, - })); + const newTransactions = useSelector(state => state.queries.newTransactions); + const matchedTransactions = useSelector( + state => state.queries.matchedTransactions, + ); + const accounts = useAccounts(); + const payees = usePayees(); + const failedAccounts = useFailedAccounts(); + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; + const [hideFraction = false] = useLocalPref('hideFraction'); + const [expandSplits] = useLocalPref('expand-splits'); + const [showBalances] = useLocalPref(`show-balances-${params.id}`); + const [hideCleared] = useLocalPref(`hide-cleared-${params.id}`); + const [showExtraBalances] = useLocalPref( + `show-extra-balances-${params.id || 'all-accounts'}`, + ); + const modalShowing = useSelector(state => state.modals.modalStack.length > 0); + const accountsSyncing = useSelector(state => state.account.accountsSyncing); + const lastUndoState = useSelector(state => state.app.lastUndoState); + + const state = { + newTransactions, + matchedTransactions, + accounts, + failedAccounts, + dateFormat, + hideFraction, + expandSplits, + showBalances, + showCleared: !hideCleared, + showExtraBalances, + payees, + modalShowing, + accountsSyncing, + lastUndoState, + }; const dispatch = useDispatch(); const filtersList = useFilters(); diff --git a/packages/desktop-client/src/components/accounts/AccountSyncCheck.jsx b/packages/desktop-client/src/components/accounts/AccountSyncCheck.jsx index 423c99b7c..09180381d 100644 --- a/packages/desktop-client/src/components/accounts/AccountSyncCheck.jsx +++ b/packages/desktop-client/src/components/accounts/AccountSyncCheck.jsx @@ -3,6 +3,7 @@ import { useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; import { authorizeBank } from '../../gocardless'; +import { useAccounts } from '../../hooks/useAccounts'; import { useActions } from '../../hooks/useActions'; import { SvgExclamationOutline } from '../../icons/v1'; import { theme } from '../../style'; @@ -49,7 +50,7 @@ function getErrorMessage(type, code) { } export function AccountSyncCheck() { - const accounts = useSelector(state => state.queries.accounts); + const accounts = useAccounts(); const failedAccounts = useSelector(state => state.account.failedAccounts); const { unlinkAccount, pushModal } = useActions(); diff --git a/packages/desktop-client/src/components/accounts/Header.jsx b/packages/desktop-client/src/components/accounts/Header.jsx index 4e181a514..80c1030ca 100644 --- a/packages/desktop-client/src/components/accounts/Header.jsx +++ b/packages/desktop-client/src/components/accounts/Header.jsx @@ -1,5 +1,6 @@ import React, { useState, useRef } from 'react'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { useSyncServerStatus } from '../../hooks/useSyncServerStatus'; import { AnimatedLoading } from '../../icons/AnimatedLoading'; import { SvgAdd } from '../../icons/v1'; @@ -53,7 +54,6 @@ export function AccountHeader({ search, filters, conditionsOp, - savePrefs, pushModal, onSearch, onAddTransaction, @@ -86,6 +86,7 @@ export function AccountHeader({ const syncServerStatus = useSyncServerStatus(); const isUsingServer = syncServerStatus !== 'no-server'; const isServerOffline = syncServerStatus === 'offline'; + const [_, setExpandSplitsPref] = useLocalPref('expand-splits'); let canSync = account && account.account_id && isUsingServer; if (!account) { @@ -100,9 +101,7 @@ export function AccountHeader({ id: tableRef.current.getScrolledItem(), }); - savePrefs({ - 'expand-splits': !(splitsExpanded.state.mode === 'expand'), - }); + setExpandSplitsPref(!(splitsExpanded.state.mode === 'expand')); } } diff --git a/packages/desktop-client/src/components/accounts/MobileAccount.jsx b/packages/desktop-client/src/components/accounts/MobileAccount.jsx index b8d4bb6ce..0b06e280e 100644 --- a/packages/desktop-client/src/components/accounts/MobileAccount.jsx +++ b/packages/desktop-client/src/components/accounts/MobileAccount.jsx @@ -19,8 +19,13 @@ import { ungroupTransactions, } from 'loot-core/src/shared/transactions'; +import { useAccounts } from '../../hooks/useAccounts'; import { useCategories } from '../../hooks/useCategories'; +import { useDateFormat } from '../../hooks/useDateFormat'; +import { useLocalPref } from '../../hooks/useLocalPref'; +import { useLocalPrefs } from '../../hooks/useLocalPrefs'; import { useNavigate } from '../../hooks/useNavigate'; +import { usePayees } from '../../hooks/usePayees'; import { useSetThemeColor } from '../../hooks/useSetThemeColor'; import { theme, styles } from '../../style'; import { Button } from '../common/Button'; @@ -72,19 +77,27 @@ function PreviewTransactions({ children }) { let paged; export function Account(props) { - const accounts = useSelector(state => state.queries.accounts); + const accounts = useAccounts(); + const payees = usePayees(); const navigate = useNavigate(); const [transactions, setTransactions] = useState([]); const [searchText, setSearchText] = useState(''); const [currentQuery, setCurrentQuery] = useState(); - const state = useSelector(state => ({ - payees: state.queries.payees, - newTransactions: state.queries.newTransactions, - prefs: state.prefs.local, - dateFormat: state.prefs.local.dateFormat || 'MM/dd/yyyy', - })); + const newTransactions = useSelector(state => state.queries.newTransactions); + const prefs = useLocalPrefs(); + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; + const [_numberFormat] = useLocalPref('numberFormat'); + const numberFormat = _numberFormat || 'comma-dot'; + const [hideFraction = false] = useLocalPref('hideFraction'); + + const state = { + payees, + newTransactions, + prefs, + dateFormat, + }; const dispatch = useDispatch(); const actionCreators = useMemo( @@ -134,11 +147,6 @@ export function Account(props) { } }); - if (accounts.length === 0) { - await actionCreators.getAccounts(); - } - - await actionCreators.initiallyLoadPayees(); await fetchTransactions(); actionCreators.markAccountRead(accountId); @@ -216,8 +224,6 @@ export function Account(props) { const balance = queries.accountBalance(account); const balanceCleared = queries.accountBalanceCleared(account); const balanceUncleared = queries.accountBalanceUncleared(account); - const numberFormat = state.prefs.numberFormat || 'comma-dot'; - const hideFraction = state.prefs.hideFraction || false; return ( <SchedulesProvider diff --git a/packages/desktop-client/src/components/accounts/MobileAccounts.jsx b/packages/desktop-client/src/components/accounts/MobileAccounts.jsx index 4087fdbe0..f05662950 100644 --- a/packages/desktop-client/src/components/accounts/MobileAccounts.jsx +++ b/packages/desktop-client/src/components/accounts/MobileAccounts.jsx @@ -1,10 +1,12 @@ -import React, { useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; +import React, { useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { replaceModal, syncAndDownload } from 'loot-core/src/client/actions'; import * as queries from 'loot-core/src/client/queries'; -import { useActions } from '../../hooks/useActions'; +import { useAccounts } from '../../hooks/useAccounts'; import { useCategories } from '../../hooks/useCategories'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { useNavigate } from '../../hooks/useNavigate'; import { useSetThemeColor } from '../../hooks/useSetThemeColor'; import { SvgAdd } from '../../icons/v1'; @@ -221,26 +223,19 @@ function AccountList({ } export function Accounts() { - const accounts = useSelector(state => state.queries.accounts); + const dispatch = useDispatch(); + const accounts = useAccounts(); const newTransactions = useSelector(state => state.queries.newTransactions); const updatedAccounts = useSelector(state => state.queries.updatedAccounts); - const numberFormat = useSelector( - state => state.prefs.local.numberFormat || 'comma-dot', - ); - const hideFraction = useSelector( - state => state.prefs.local.hideFraction || false, - ); + const [_numberFormat] = useLocalPref('numberFormat'); + const numberFormat = _numberFormat || 'comma-dot'; + const [hideFraction = false] = useLocalPref('hideFraction'); const { list: categories } = useCategories(); - const { getAccounts, replaceModal, syncAndDownload } = useActions(); const transactions = useState({}); const navigate = useNavigate(); - useEffect(() => { - (async () => getAccounts())(); - }, []); - const onSelectAccount = id => { navigate(`/accounts/${id}`); }; @@ -249,6 +244,14 @@ export function Accounts() { navigate(`/transaction/${transaction}`); }; + const onAddAccount = () => { + dispatch(replaceModal('add-account')); + }; + + const onSync = () => { + dispatch(syncAndDownload()); + }; + useSetThemeColor(theme.mobileViewTheme); return ( @@ -265,10 +268,10 @@ export function Accounts() { getBalanceQuery={queries.accountBalance} getOnBudgetBalance={queries.budgetedAccountBalance} getOffBudgetBalance={queries.offbudgetAccountBalance} - onAddAccount={() => replaceModal('add-account')} + onAddAccount={onAddAccount} onSelectAccount={onSelectAccount} onSelectTransaction={onSelectTransaction} - onSync={syncAndDownload} + onSync={onSync} /> </View> ); diff --git a/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx b/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx index f296e7b29..778480a7d 100644 --- a/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx +++ b/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx @@ -3,9 +3,9 @@ import React, { Fragment, type ComponentProps, type ReactNode } from 'react'; import { css } from 'glamor'; -import { useCachedAccounts } from 'loot-core/src/client/data-hooks/accounts'; import { type AccountEntity } from 'loot-core/src/types/models'; +import { useAccounts } from '../../hooks/useAccounts'; import { useResponsive } from '../../ResponsiveProvider'; import { type CSSProperties, theme } from '../../style'; import { View } from '../common/View'; @@ -86,7 +86,7 @@ export function AccountAutocomplete({ closeOnBlur, ...props }: AccountAutoCompleteProps) { - let accounts = useCachedAccounts() || []; + let accounts = useAccounts() || []; //remove closed accounts if needed //then sort by closed, then offbudget diff --git a/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.tsx b/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.tsx index b99009b32..84ee34bfd 100644 --- a/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.tsx +++ b/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.tsx @@ -13,14 +13,14 @@ import { useDispatch } from 'react-redux'; import { css } from 'glamor'; import { createPayee } from 'loot-core/src/client/actions/queries'; -import { useCachedAccounts } from 'loot-core/src/client/data-hooks/accounts'; -import { useCachedPayees } from 'loot-core/src/client/data-hooks/payees'; import { getActivePayees } from 'loot-core/src/client/reducers/queries'; import { type AccountEntity, type PayeeEntity, } from 'loot-core/src/types/models'; +import { useAccounts } from '../../hooks/useAccounts'; +import { usePayees } from '../../hooks/usePayees'; import { SvgAdd } from '../../icons/v1'; import { useResponsive } from '../../ResponsiveProvider'; import { type CSSProperties, theme } from '../../style'; @@ -187,12 +187,12 @@ export function PayeeAutocomplete({ payees, ...props }: PayeeAutocompleteProps) { - const cachedPayees = useCachedPayees(); + const retrievedPayees = usePayees(); if (!payees) { - payees = cachedPayees; + payees = retrievedPayees; } - const cachedAccounts = useCachedAccounts(); + const cachedAccounts = useAccounts(); if (!accounts) { accounts = cachedAccounts; } diff --git a/packages/desktop-client/src/components/budget/BudgetCategories.jsx b/packages/desktop-client/src/components/budget/BudgetCategories.jsx index c80bfb2ab..51183003f 100644 --- a/packages/desktop-client/src/components/budget/BudgetCategories.jsx +++ b/packages/desktop-client/src/components/budget/BudgetCategories.jsx @@ -1,5 +1,6 @@ import React, { memo, useState, useMemo } from 'react'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { theme, styles } from '../../style'; import { View } from '../common/View'; import { DropHighlightPosContext } from '../sort'; @@ -17,12 +18,7 @@ import { separateGroups } from './util'; export const BudgetCategories = memo( ({ categoryGroups, - newCategoryForGroup, - showHiddenCategories, - isAddingGroup, editingCell, - collapsed, - setCollapsed, dataComponents, onBudgetAction, onShowActivity, @@ -34,11 +30,16 @@ export const BudgetCategories = memo( onDeleteGroup, onReorderCategory, onReorderGroup, - onShowNewCategory, - onHideNewCategory, - onShowNewGroup, - onHideNewGroup, }) => { + const [_collapsed, setCollapsedPref] = useLocalPref('budget.collapsed'); + const collapsed = _collapsed || []; + const [showHiddenCategories] = useLocalPref('budget.showHiddenCategories'); + function onCollapse(value) { + setCollapsedPref(value); + } + + const [isAddingGroup, setIsAddingGroup] = useState(false); + const [newCategoryForGroup, setNewCategoryForGroup] = useState(null); const items = useMemo(() => { const [expenseGroups, incomeGroup] = separateGroups(categoryGroups); @@ -133,15 +134,46 @@ export const BudgetCategories = memo( }); } else if (state === 'end') { setDragState(null); - setCollapsed(savedCollapsed || []); + onCollapse(savedCollapsed || []); } } function onToggleCollapse(id) { if (collapsed.includes(id)) { - setCollapsed(collapsed.filter(id_ => id_ !== id)); + onCollapse(collapsed.filter(id_ => id_ !== id)); } else { - setCollapsed([...collapsed, id]); + onCollapse([...collapsed, id]); + } + } + + function onShowNewGroup() { + setIsAddingGroup(true); + } + + function onHideNewGroup() { + setIsAddingGroup(false); + } + + function _onSaveGroup(group) { + onSaveGroup?.(group); + if (group.id === 'new') { + onHideNewGroup(); + } + } + + function onShowNewCategory(groupId) { + onCollapse(collapsed.filter(c => c !== groupId)); + setNewCategoryForGroup(groupId); + } + + function onHideNewCategory() { + setNewCategoryForGroup(null); + } + + function _onSaveCategory(category) { + onSaveCategory?.(category); + if (category.id === 'new') { + onHideNewCategory(); } } @@ -167,7 +199,7 @@ export const BudgetCategories = memo( <SidebarGroup group={{ id: 'new', name: '' }} editing={true} - onSave={onSaveGroup} + onSave={_onSaveGroup} onHideNewGroup={onHideNewGroup} onEdit={onEditName} /> @@ -187,7 +219,7 @@ export const BudgetCategories = memo( id: 'new', }} editing={true} - onSave={onSaveCategory} + onSave={_onSaveCategory} onHideNewCategory={onHideNewCategory} onEditName={onEditName} /> @@ -204,7 +236,7 @@ export const BudgetCategories = memo( MonthComponent={dataComponents.ExpenseGroupComponent} dragState={dragState} onEditName={onEditName} - onSave={onSaveGroup} + onSave={_onSaveGroup} onDelete={onDeleteGroup} onDragChange={onDragChange} onReorderGroup={onReorderGroup} @@ -223,7 +255,7 @@ export const BudgetCategories = memo( dragState={dragState} onEditName={onEditName} onEditMonth={onEditMonth} - onSave={onSaveCategory} + onSave={_onSaveCategory} onDelete={onDeleteCategory} onDragChange={onDragChange} onReorder={onReorderCategory} @@ -255,7 +287,7 @@ export const BudgetCategories = memo( MonthComponent={dataComponents.IncomeGroupComponent} collapsed={collapsed.includes(item.value.id)} onEditName={onEditName} - onSave={onSaveGroup} + onSave={_onSaveGroup} onToggleCollapse={onToggleCollapse} onShowNewCategory={onShowNewCategory} /> @@ -270,7 +302,7 @@ export const BudgetCategories = memo( MonthComponent={dataComponents.IncomeCategoryComponent} onEditName={onEditName} onEditMonth={onEditMonth} - onSave={onSaveCategory} + onSave={_onSaveCategory} onDelete={onDeleteCategory} onDragChange={onDragChange} onReorder={onReorderCategory} diff --git a/packages/desktop-client/src/components/budget/BudgetTable.jsx b/packages/desktop-client/src/components/budget/BudgetTable.jsx index 3000ebb1a..d9f4d4df8 100644 --- a/packages/desktop-client/src/components/budget/BudgetTable.jsx +++ b/packages/desktop-client/src/components/budget/BudgetTable.jsx @@ -1,5 +1,7 @@ import React, { createRef, Component } from 'react'; +import { connect } from 'react-redux'; +import { savePrefs } from 'loot-core/src/client/actions'; import * as monthUtils from 'loot-core/src/shared/months'; import { theme, styles } from '../../style'; @@ -12,7 +14,7 @@ import { BudgetTotals } from './BudgetTotals'; import { MonthsProvider } from './MonthsContext'; import { findSortDown, findSortUp, getScrollbarWidth } from './util'; -export class BudgetTable extends Component { +class BudgetTableInner extends Component { constructor(props) { super(props); this.budgetCategoriesRef = createRef(); @@ -20,7 +22,6 @@ export class BudgetTable extends Component { this.state = { editing: null, draggingState: null, - showHiddenCategories: props.prefs['budget.showHiddenCategories'] ?? false, }; } @@ -137,26 +138,22 @@ export class BudgetTable extends Component { return monthUtils.addMonths(this.props.startMonth, monthIndex); }; + // This is called via ref. clearEditing() { this.setState({ editing: null }); } toggleHiddenCategories = () => { - this.setState(prevState => ({ - showHiddenCategories: !prevState.showHiddenCategories, - })); - this.props.savePrefs({ - 'budget.showHiddenCategories': !this.state.showHiddenCategories, - }); + this.props.onToggleHiddenCategories(); }; expandAllCategories = () => { - this.props.setCollapsed([]); + this.props.onCollapse([]); }; collapseAllCategories = () => { - const { setCollapsed, categoryGroups } = this.props; - setCollapsed(categoryGroups.map(g => g.id)); + const { onCollapse, categoryGroups } = this.props; + onCollapse(categoryGroups.map(g => g.id)); }; render() { @@ -167,21 +164,13 @@ export class BudgetTable extends Component { startMonth, numMonths, monthBounds, - collapsed, - setCollapsed, - newCategoryForGroup, dataComponents, - isAddingGroup, onSaveCategory, onSaveGroup, onDeleteCategory, onDeleteGroup, - onShowNewCategory, - onHideNewCategory, - onShowNewGroup, - onHideNewGroup, } = this.props; - const { editing, draggingState, showHiddenCategories } = this.state; + const { editing, draggingState } = this.state; return ( <View @@ -254,13 +243,8 @@ export class BudgetTable extends Component { innerRef={el => (this.budgetDataNode = el)} > <BudgetCategories - showHiddenCategories={showHiddenCategories} categoryGroups={categoryGroups} - newCategoryForGroup={newCategoryForGroup} - isAddingGroup={isAddingGroup} editingCell={editing} - collapsed={collapsed} - setCollapsed={setCollapsed} dataComponents={dataComponents} onEditMonth={this.onEditMonth} onEditName={this.onEditName} @@ -270,10 +254,6 @@ export class BudgetTable extends Component { onDeleteGroup={onDeleteGroup} onReorderCategory={this.onReorderCategory} onReorderGroup={this.onReorderGroup} - onShowNewCategory={onShowNewCategory} - onHideNewCategory={onHideNewCategory} - onShowNewGroup={onShowNewGroup} - onHideNewGroup={onHideNewGroup} onBudgetAction={this.onBudgetAction} onShowActivity={this.onShowActivity} /> @@ -285,3 +265,35 @@ export class BudgetTable extends Component { ); } } + +const mapStateToProps = state => { + const { grouped: categoryGroups } = state.queries.categories; + return { + categoryGroups, + }; +}; + +const mapDispatchToProps = dispatch => { + const onCollapse = collapsedIds => { + dispatch(savePrefs({ 'budget.collapsed': collapsedIds })); + }; + + const onToggleHiddenCategories = () => + dispatch((innerDispatch, getState) => { + const { prefs } = getState(); + const showHiddenCategories = prefs.local['budget.showHiddenCategories']; + innerDispatch( + savePrefs({ + 'budget.showHiddenCategories': !showHiddenCategories, + }), + ); + }); + return { + onCollapse, + onToggleHiddenCategories, + }; +}; + +export const BudgetTable = connect(mapStateToProps, mapDispatchToProps, null, { + forwardRef: true, +})(BudgetTableInner); diff --git a/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx b/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx index 7a01439ab..ab366f3a6 100644 --- a/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx +++ b/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx @@ -1,11 +1,7 @@ // @ts-strict-ignore import React, { forwardRef, useEffect, type ComponentProps } from 'react'; -import { useSelector } from 'react-redux'; import AutoSizer from 'react-virtualized-auto-sizer'; -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; - import { useActions } from '../../hooks/useActions'; import { View } from '../common/View'; @@ -37,14 +33,13 @@ type DynamicBudgetTableInnerProps = { } & ComponentProps<typeof BudgetTable>; const DynamicBudgetTableInner = forwardRef< - BudgetTable, + typeof BudgetTable, DynamicBudgetTableInnerProps >( ( { width, height, - categoryGroups, prewarmStartMonth, startMonth, maxMonths = 3, @@ -55,9 +50,6 @@ const DynamicBudgetTableInner = forwardRef< }, ref, ) => { - const prefs = useSelector<State, PrefsState['local']>( - state => state.prefs.local, - ); const { setDisplayMax } = useBudgetMonthCount(); const actions = useActions(); @@ -91,12 +83,10 @@ const DynamicBudgetTableInner = forwardRef< /> <BudgetTable ref={ref} - categoryGroups={categoryGroups} prewarmStartMonth={prewarmStartMonth} startMonth={startMonth} numMonths={numMonths} monthBounds={monthBounds} - prefs={prefs} {...actions} {...props} /> @@ -107,7 +97,7 @@ const DynamicBudgetTableInner = forwardRef< ); export const DynamicBudgetTable = forwardRef< - BudgetTable, + typeof BudgetTable, DynamicBudgetTableInnerProps >((props, ref) => { return ( diff --git a/packages/desktop-client/src/components/budget/MobileBudget.tsx b/packages/desktop-client/src/components/budget/MobileBudget.tsx index 4b98fc5f6..f5145aeec 100644 --- a/packages/desktop-client/src/components/budget/MobileBudget.tsx +++ b/packages/desktop-client/src/components/budget/MobileBudget.tsx @@ -1,10 +1,7 @@ // @ts-strict-ignore import React, { useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; import { useSpreadsheet } from 'loot-core/src/client/SpreadsheetProvider'; -import { type PrefsState } from 'loot-core/src/client/state-types/prefs'; import { send, listen } from 'loot-core/src/platform/client/fetch'; import * as monthUtils from 'loot-core/src/shared/months'; import { @@ -14,6 +11,7 @@ import { import { type BoundActions, useActions } from '../../hooks/useActions'; import { useCategories } from '../../hooks/useCategories'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { useSetThemeColor } from '../../hooks/useSetThemeColor'; import { AnimatedLoading } from '../../icons/AnimatedLoading'; import { theme } from '../../style'; @@ -26,7 +24,6 @@ import { prewarmMonth, switchBudgetType } from './util'; type BudgetInnerProps = { categories: CategoryEntity[]; categoryGroups: CategoryGroupEntity[]; - prefs: PrefsState['local']; loadPrefs: BoundActions['loadPrefs']; savePrefs: BoundActions['savePrefs']; budgetType: 'rollover' | 'report'; @@ -50,9 +47,7 @@ function BudgetInner(props: BudgetInnerProps) { const { categoryGroups, categories, - prefs, loadPrefs, - savePrefs, budgetType, spreadsheet, applyBudgetAction, @@ -75,6 +70,10 @@ function BudgetInner(props: BudgetInnerProps) { const [initialized, setInitialized] = useState(false); const [editMode, setEditMode] = useState(false); + const [_numberFormat] = useLocalPref('numberFormat'); + const numberFormat = _numberFormat || 'comma-dot'; + const [hideFraction = false] = useLocalPref('hideFraction'); + useEffect(() => { async function init() { const { start, end } = await send('get-budget-bounds'); @@ -356,9 +355,6 @@ function BudgetInner(props: BudgetInnerProps) { }); }; - const numberFormat = prefs?.numberFormat || 'comma-dot'; - const hideFraction = prefs?.hideFraction || false; - if (!categoryGroups || !initialized) { return ( <View @@ -385,7 +381,7 @@ function BudgetInner(props: BudgetInnerProps) { <BudgetTable // This key forces the whole table rerender when the number // format changes - key={numberFormat + hideFraction} + key={`${numberFormat}${hideFraction}`} categoryGroups={categoryGroups} type={budgetType} month={currentMonth} @@ -407,7 +403,6 @@ function BudgetInner(props: BudgetInnerProps) { onBudgetAction={applyBudgetAction} onRefresh={onRefresh} onSwitchBudgetType={onSwitchBudgetType} - savePrefs={savePrefs} pushModal={pushModal} onEditGroup={onEditGroup} onEditCategory={onEditCategory} @@ -419,12 +414,8 @@ function BudgetInner(props: BudgetInnerProps) { export function Budget() { const { list: categories, grouped: categoryGroups } = useCategories(); - const budgetType = useSelector<State, PrefsState['local']['budgetType']>( - state => state.prefs.local?.budgetType || 'rollover', - ); - const prefs = useSelector<State, PrefsState['local']>( - state => state.prefs.local, - ); + const [_budgetType] = useLocalPref('budgetType'); + const budgetType = _budgetType || 'rollover'; const actions = useActions(); const spreadsheet = useSpreadsheet(); @@ -434,7 +425,6 @@ export function Budget() { categoryGroups={categoryGroups} categories={categories} budgetType={budgetType} - prefs={prefs} {...actions} spreadsheet={spreadsheet} /> diff --git a/packages/desktop-client/src/components/budget/MobileBudgetTable.jsx b/packages/desktop-client/src/components/budget/MobileBudgetTable.jsx index a7214cf12..9a03bb71c 100644 --- a/packages/desktop-client/src/components/budget/MobileBudgetTable.jsx +++ b/packages/desktop-client/src/components/budget/MobileBudgetTable.jsx @@ -1,5 +1,4 @@ import React, { memo, useRef, useState } from 'react'; -import { useSelector } from 'react-redux'; import memoizeOne from 'memoize-one'; @@ -7,6 +6,7 @@ import { rolloverBudget, reportBudget } from 'loot-core/src/client/queries'; import * as monthUtils from 'loot-core/src/shared/months'; import { useFeatureFlag } from '../../hooks/useFeatureFlag'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { SingleActiveEditFormProvider, useSingleActiveEditForm, @@ -1133,7 +1133,6 @@ export function BudgetTable({ onBudgetAction, onRefresh, onSwitchBudgetType, - savePrefs, pushModal, onEditGroup, onEditCategory, @@ -1144,24 +1143,15 @@ export function BudgetTable({ // let editMode = false; // neuter editMode -- sorry, not rewriting drag-n-drop right now const format = useFormat(); - const mobileShowBudgetedColPref = useSelector(state => { - return state.prefs?.local?.toggleMobileDisplayPref || true; - }); - - const showHiddenCategories = useSelector(state => { - return state.prefs?.local?.['budget.showHiddenCategories'] || false; - }); - - const [showBudgetedCol, setShowBudgetedCol] = useState( - !mobileShowBudgetedColPref && - !document.cookie.match(/mobileShowBudgetedColPref=true/), + const [showSpentColumn = false, setShowSpentColumnPref] = useLocalPref( + 'mobile.showSpentColumn', ); + const [showHiddenCategories = false, setShowHiddenCategoriesPref] = + useLocalPref('budget.showHiddenCategories'); + function toggleDisplay() { - setShowBudgetedCol(!showBudgetedCol); - if (!showBudgetedCol) { - savePrefs({ mobileShowBudgetedColPref: true }); - } + setShowSpentColumnPref(!showSpentColumn); } const buttonStyle = { @@ -1177,9 +1167,7 @@ export function BudgetTable({ }; const onToggleHiddenCategories = () => { - savePrefs({ - 'budget.showHiddenCategories': !showHiddenCategories, - }); + setShowHiddenCategoriesPref(!showHiddenCategories); }; return ( @@ -1245,7 +1233,7 @@ export function BudgetTable({ /> )} <View style={{ flex: 1 }} /> - {(show3Cols || showBudgetedCol) && ( + {(show3Cols || !showSpentColumn) && ( <Button type="bare" disabled={show3Cols} @@ -1255,7 +1243,7 @@ export function BudgetTable({ padding: '0 8px', margin: '0 -8px', background: - showBudgetedCol && !show3Cols + !showSpentColumn && !show3Cols ? `linear-gradient(-45deg, ${theme.formInputBackgroundSelection} 8px, transparent 0)` : null, }} @@ -1292,7 +1280,7 @@ export function BudgetTable({ </View> </Button> )} - {(show3Cols || !showBudgetedCol) && ( + {(show3Cols || showSpentColumn) && ( <Button type="bare" disabled={show3Cols} @@ -1300,7 +1288,7 @@ export function BudgetTable({ style={{ ...buttonStyle, background: - !showBudgetedCol && !show3Cols + showSpentColumn && !show3Cols ? `linear-gradient(45deg, ${theme.formInputBackgroundSelection} 8px, transparent 0)` : null, }} @@ -1372,7 +1360,7 @@ export function BudgetTable({ <BudgetGroups type={type} categoryGroups={categoryGroups} - showBudgetedCol={showBudgetedCol} + showBudgetedCol={!showSpentColumn} show3Cols={show3Cols} showHiddenCategories={showHiddenCategories} // gestures={gestures} @@ -1407,7 +1395,7 @@ export function BudgetTable({ <BudgetGroups type={type} categoryGroups={categoryGroups} - showBudgetedCol={showBudgetedCol} + showBudgetedCol={!showSpentColumn} show3Cols={show3Cols} showHiddenCategories={showHiddenCategories} // gestures={gestures} diff --git a/packages/desktop-client/src/components/budget/MonthCountSelector.tsx b/packages/desktop-client/src/components/budget/MonthCountSelector.tsx index 737dcdc69..d64ed0082 100644 --- a/packages/desktop-client/src/components/budget/MonthCountSelector.tsx +++ b/packages/desktop-client/src/components/budget/MonthCountSelector.tsx @@ -22,7 +22,7 @@ function Calendar({ color, onClick }: CalendarProps) { type MonthCountSelectorProps = { maxMonths: number; - onChange: (value: number) => Promise<void>; + onChange: (value: number) => void; }; export function MonthCountSelector({ diff --git a/packages/desktop-client/src/components/budget/index.tsx b/packages/desktop-client/src/components/budget/index.tsx index ea263830d..724a969c2 100644 --- a/packages/desktop-client/src/components/budget/index.tsx +++ b/packages/desktop-client/src/components/budget/index.tsx @@ -7,35 +7,31 @@ import React, { useEffect, useRef, } from 'react'; -import { useSelector } from 'react-redux'; -import { - type NavigateFunction, - type PathMatch, - useLocation, - useMatch, -} from 'react-router-dom'; - -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; -import { useSpreadsheet } from 'loot-core/src/client/SpreadsheetProvider'; -import { send, listen } from 'loot-core/src/platform/client/fetch'; +import { useDispatch } from 'react-redux'; + import { - addCategory, + addNotification, + applyBudgetAction, + createCategory, + createGroup, + deleteCategory, + deleteGroup, + getCategories, + loadPrefs, moveCategory, moveCategoryGroup, + pushModal, updateCategory, - deleteCategory, - addGroup, updateGroup, - deleteGroup, -} from 'loot-core/src/shared/categories'; +} from 'loot-core/src/client/actions'; +import { useSpreadsheet } from 'loot-core/src/client/SpreadsheetProvider'; +import { send, listen } from 'loot-core/src/platform/client/fetch'; import * as monthUtils from 'loot-core/src/shared/months'; -import { type GlobalPrefs, type LocalPrefs } from 'loot-core/src/types/prefs'; -import { type CategoryGroupEntity } from 'loot-core/types/models'; -import { type BoundActions, useActions } from '../../hooks/useActions'; import { useCategories } from '../../hooks/useCategories'; import { useFeatureFlag } from '../../hooks/useFeatureFlag'; +import { useGlobalPref } from '../../hooks/useGlobalPref'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { useNavigate } from '../../hooks/useNavigate'; import { styles } from '../../style'; import { View } from '../common/View'; @@ -75,62 +71,41 @@ type RolloverComponents = { type BudgetProps = { accountId?: string; - startMonth: LocalPrefs['budget.startMonth']; - collapsedPrefs: LocalPrefs['budget.collapsed']; - summaryCollapsed: LocalPrefs['budget.summaryCollapsed']; - budgetType: LocalPrefs['budgetType']; - maxMonths: GlobalPrefs['maxMonths']; - categoryGroups: CategoryGroupEntity[]; reportComponents: ReportComponents; rolloverComponents: RolloverComponents; titlebar: TitlebarContextValue; - match: PathMatch<string>; - spreadsheet: ReturnType<typeof useSpreadsheet>; - navigate: NavigateFunction; - getCategories: BoundActions['getCategories']; - savePrefs: BoundActions['savePrefs']; - createCategory: BoundActions['createCategory']; - updateCategory: BoundActions['updateCategory']; - pushModal: BoundActions['pushModal']; - deleteCategory: BoundActions['deleteCategory']; - createGroup: BoundActions['createGroup']; - updateGroup: BoundActions['updateGroup']; - deleteGroup: BoundActions['deleteGroup']; - applyBudgetAction: BoundActions['applyBudgetAction']; - moveCategory: BoundActions['moveCategory']; - moveCategoryGroup: BoundActions['moveCategoryGroup']; - loadPrefs: BoundActions['loadPrefs']; - addNotification: BoundActions['addNotification']; }; function BudgetInner(props: BudgetProps) { const currentMonth = monthUtils.currentMonth(); const tableRef = useRef(null); - - const [initialized, setInitialized] = useState(false); - const [prewarmStartMonth, setPrewarmStartMonth] = useState( - props.startMonth || currentMonth, + const spreadsheet = useSpreadsheet(); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const [_startMonth, setBudgetStartMonthPref] = + useLocalPref('budget.startMonth'); + const startMonth = _startMonth || currentMonth; + const [summaryCollapsed, setSummaryCollapsedPref] = useLocalPref( + 'budget.summaryCollapsed', ); + const [_budgetType] = useLocalPref('budgetType'); + const budgetType = _budgetType || 'rollover'; + const [_maxMonths] = useGlobalPref('maxMonths'); + const maxMonths = _maxMonths || 1; - const [newCategoryForGroup, setNewCategoryForGroup] = useState(null); - const [isAddingGroup, setIsAddingGroup] = useState(false); - const [collapsed, setCollapsed] = useState(props.collapsedPrefs || []); + const [initialized, setInitialized] = useState(false); const [bounds, setBounds] = useState({ start: currentMonth, end: currentMonth, }); - const [categoryGroups, setCategoryGroups] = useState(null); - const [summaryCollapsed, setSummaryCollapsed] = useState( - props.summaryCollapsed, - ); + const { grouped: categoryGroups } = useCategories(); - async function loadCategories() { - const result = await props.getCategories(); - setCategoryGroups(result.grouped); + function loadCategories() { + dispatch(getCategories()); } useEffect(() => { - const { titlebar, budgetType } = props; + const { titlebar } = props; async function run() { loadCategories(); @@ -140,9 +115,9 @@ function BudgetInner(props: BudgetProps) { await prewarmAllMonths( budgetType, - props.spreadsheet, + spreadsheet, { start, end }, - prewarmStartMonth, + startMonth, ); setInitialized(true); @@ -187,10 +162,6 @@ function BudgetInner(props: BudgetProps) { }; }, []); - useEffect(() => { - props.savePrefs({ 'budget.collapsed': collapsed }); - }, [collapsed]); - useEffect(() => { send('get-budget-bounds').then(({ start, end }) => { if (bounds.start !== start || bounds.end !== end) { @@ -200,12 +171,10 @@ function BudgetInner(props: BudgetProps) { }, [props.accountId]); const onMonthSelect = async (month, numDisplayed) => { - setPrewarmStartMonth(month); + setBudgetStartMonthPref(month); const warmingMonth = month; - const startMonth = props.startMonth || currentMonth; - // We could be smarter about this, but this is a good start. We // optimize for the case where users press the left/right button // to move between months. This loads the month data all at once @@ -215,51 +184,37 @@ function BudgetInner(props: BudgetProps) { if (month < startMonth) { // pre-warm prev month await prewarmMonth( - props.budgetType, - props.spreadsheet, + budgetType, + spreadsheet, monthUtils.subMonths(month, 1), ); } else if (month > startMonth) { // pre-warm next month await prewarmMonth( - props.budgetType, - props.spreadsheet, + budgetType, + spreadsheet, monthUtils.addMonths(month, numDisplayed), ); } if (warmingMonth === month) { - props.savePrefs({ 'budget.startMonth': month }); + setBudgetStartMonthPref(month); } }; - const onShowNewCategory = groupId => { - setNewCategoryForGroup(groupId); - setCollapsed(state => state.filter(c => c !== groupId)); - }; - - const onHideNewCategory = () => { - setNewCategoryForGroup(null); - }; - - const onShowNewGroup = () => { - setIsAddingGroup(true); - }; - - const onHideNewGroup = () => { - setIsAddingGroup(false); - }; - const categoryNameAlreadyExistsNotification = name => { - props.addNotification({ - type: 'error', - message: `Category ‘${name}’ already exists in group (May be Hidden)`, - }); + dispatch( + addNotification({ + type: 'error', + message: `Category ‘${name}’ already exists in group (May be Hidden)`, + }), + ); }; const onSaveCategory = async category => { + const cats = await send('get-categories'); const exists = - (await props.getCategories()).grouped + cats.grouped .filter(g => g.id === category.cat_group)[0] .categories.filter( c => c.name.toUpperCase() === category.name.toUpperCase(), @@ -273,24 +228,16 @@ function BudgetInner(props: BudgetProps) { } if (category.id === 'new') { - const id = await props.createCategory( - category.name, - category.cat_group, - category.is_income, - category.hidden, - ); - - setNewCategoryForGroup(null); - setCategoryGroups(state => - addCategory(state, { - ...category, - is_income: category.is_income ? 1 : 0, - id, - }), + dispatch( + createCategory( + category.name, + category.cat_group, + category.is_income, + category.hidden, + ), ); } else { - props.updateCategory(category); - setCategoryGroups(state => updateCategory(state, category)); + dispatch(updateCategory(category)); } }; @@ -298,55 +245,26 @@ function BudgetInner(props: BudgetProps) { const mustTransfer = await send('must-category-transfer', { id }); if (mustTransfer) { - props.pushModal('confirm-category-delete', { - category: id, - onDelete: transferCategory => { - if (id !== transferCategory) { - props.deleteCategory(id, transferCategory); - - setCategoryGroups(state => deleteCategory(state, id)); - } - }, - }); + dispatch( + pushModal('confirm-category-delete', { + category: id, + onDelete: transferCategory => { + if (id !== transferCategory) { + dispatch(deleteCategory(id, transferCategory)); + } + }, + }), + ); } else { - props.deleteCategory(id); - - setCategoryGroups(state => deleteCategory(state, id)); + dispatch(deleteCategory(id)); } }; - const groupNameAlreadyExistsNotification = group => { - props.addNotification({ - type: 'error', - message: `A ${group.hidden ? 'hidden ' : ''}’${group.name}’ category group already exists.`, - }); - }; - - const onSaveGroup = async group => { - const categories = await props.getCategories(); - const matchingGroups = categories.grouped - .filter(g => g.name.toUpperCase() === group.name.toUpperCase()) - .filter(g => group.id === 'new' || group.id !== g.id); - - if (matchingGroups.length > 0) { - groupNameAlreadyExistsNotification(matchingGroups[0]); - return; - } - + const onSaveGroup = group => { if (group.id === 'new') { - const id = await props.createGroup(group.name); - setIsAddingGroup(false); - setCategoryGroups(state => - addGroup(state, { - ...group, - is_income: 0, - categories: group.categories || [], - id, - }), - ); + dispatch(createGroup(group.name)); } else { - props.updateGroup(group); - setCategoryGroups(state => updateGroup(state, group)); + dispatch(updateGroup(group)); } }; @@ -362,27 +280,25 @@ function BudgetInner(props: BudgetProps) { } if (mustTransfer) { - props.pushModal('confirm-category-delete', { - group: id, - onDelete: transferCategory => { - props.deleteGroup(id, transferCategory); - - setCategoryGroups(state => deleteGroup(state, id)); - }, - }); + dispatch( + pushModal('confirm-category-delete', { + group: id, + onDelete: transferCategory => { + dispatch(deleteGroup(id, transferCategory)); + }, + }), + ); } else { - props.deleteGroup(id); - - setCategoryGroups(state => deleteGroup(state, id)); + dispatch(deleteGroup(id)); } }; const onBudgetAction = (month, type, args) => { - props.applyBudgetAction(month, type, args); + dispatch(applyBudgetAction(month, type, args)); }; const onShowActivity = (categoryName, categoryId, month) => { - props.navigate('/accounts', { + navigate('/accounts', { state: { goBack: true, filterName: `${categoryName} (${monthUtils.format( @@ -398,7 +314,7 @@ function BudgetInner(props: BudgetProps) { }; const onReorderCategory = async sortInfo => { - const cats = await props.getCategories(); + const cats = await send('get-categories'); const moveCandidate = cats.list.filter(c => c.id === sortInfo.id)[0]; const exists = cats.grouped @@ -413,23 +329,15 @@ function BudgetInner(props: BudgetProps) { return; } - props.moveCategory(sortInfo.id, sortInfo.groupId, sortInfo.targetId); - setCategoryGroups(state => - moveCategory(state, sortInfo.id, sortInfo.groupId, sortInfo.targetId), - ); + dispatch(moveCategory(sortInfo.id, sortInfo.groupId, sortInfo.targetId)); }; const onReorderGroup = async sortInfo => { - props.moveCategoryGroup(sortInfo.id, sortInfo.targetId); - setCategoryGroups(state => - moveCategoryGroup(state, sortInfo.id, sortInfo.targetId), - ); + dispatch(moveCategoryGroup(sortInfo.id, sortInfo.targetId)); }; const onToggleCollapse = () => { - const collapsed = !summaryCollapsed; - setSummaryCollapsed(collapsed); - props.savePrefs({ 'budget.summaryCollapsed': collapsed }); + setSummaryCollapsedPref(!summaryCollapsed); }; const onTitlebarEvent = async ({ type, payload }: TitlebarMessage) => { @@ -437,10 +345,12 @@ function BudgetInner(props: BudgetProps) { case SWITCH_BUDGET_MESSAGE_TYPE: { await switchBudgetType( payload.newBudgetType, - props.spreadsheet, + spreadsheet, bounds, - prewarmStartMonth, - () => props.loadPrefs(), + startMonth, + async () => { + dispatch(loadPrefs()); + }, ); break; } @@ -448,23 +358,14 @@ function BudgetInner(props: BudgetProps) { } }; - const { - maxMonths: originalMaxMonths, - budgetType: type, - reportComponents, - rolloverComponents, - } = props; - - const maxMonths = originalMaxMonths || 1; + const { reportComponents, rolloverComponents } = props; if (!initialized || !categoryGroups) { return null; } - const startMonth = props.startMonth || currentMonth; - let table; - if (type === 'report') { + if (budgetType === 'report') { table = ( <ReportProvider summaryCollapsed={summaryCollapsed} @@ -473,22 +374,13 @@ function BudgetInner(props: BudgetProps) { > <DynamicBudgetTable ref={tableRef} - type={type} - categoryGroups={categoryGroups} - prewarmStartMonth={prewarmStartMonth} + type={budgetType} + prewarmStartMonth={startMonth} startMonth={startMonth} monthBounds={bounds} maxMonths={maxMonths} - collapsed={collapsed} - setCollapsed={setCollapsed} - newCategoryForGroup={newCategoryForGroup} - isAddingGroup={isAddingGroup} dataComponents={reportComponents} onMonthSelect={onMonthSelect} - onShowNewCategory={onShowNewCategory} - onHideNewCategory={onHideNewCategory} - onShowNewGroup={onShowNewGroup} - onHideNewGroup={onHideNewGroup} onDeleteCategory={onDeleteCategory} onDeleteGroup={onDeleteGroup} onSaveCategory={onSaveCategory} @@ -503,29 +395,19 @@ function BudgetInner(props: BudgetProps) { } else { table = ( <RolloverContext - categoryGroups={categoryGroups} summaryCollapsed={summaryCollapsed} onBudgetAction={onBudgetAction} onToggleSummaryCollapse={onToggleCollapse} > <DynamicBudgetTable ref={tableRef} - type={type} - categoryGroups={categoryGroups} - prewarmStartMonth={prewarmStartMonth} + type={budgetType} + prewarmStartMonth={startMonth} startMonth={startMonth} monthBounds={bounds} maxMonths={maxMonths} - collapsed={collapsed} - setCollapsed={setCollapsed} - newCategoryForGroup={newCategoryForGroup} - isAddingGroup={isAddingGroup} dataComponents={rolloverComponents} onMonthSelect={onMonthSelect} - onShowNewCategory={onShowNewCategory} - onHideNewCategory={onHideNewCategory} - onShowNewGroup={onShowNewGroup} - onHideNewGroup={onHideNewGroup} onDeleteCategory={onDeleteCategory} onDeleteGroup={onDeleteGroup} onSaveCategory={onSaveCategory} @@ -553,32 +435,7 @@ const RolloverBudgetSummary = memo<{ month: string }>(props => { }); export function Budget() { - const startMonth = useSelector< - State, - PrefsState['local']['budget.startMonth'] - >(state => state.prefs.local['budget.startMonth']); - const collapsedPrefs = useSelector< - State, - PrefsState['local']['budget.collapsed'] - >(state => state.prefs.local['budget.collapsed']); - const summaryCollapsed = useSelector< - State, - PrefsState['local']['budget.summaryCollapsed'] - >(state => state.prefs.local['budget.summaryCollapsed']); - const budgetType = useSelector<State, PrefsState['local']['budgetType']>( - state => state.prefs.local.budgetType || 'rollover', - ); - const maxMonths = useSelector<State, PrefsState['global']['maxMonths']>( - state => state.prefs.global.maxMonths, - ); - const { grouped: categoryGroups } = useCategories(); - - const actions = useActions(); - const spreadsheet = useSpreadsheet(); const titlebar = useContext(TitlebarContext); - const location = useLocation(); - const match = useMatch(location.pathname); - const navigate = useNavigate(); const reportComponents = useMemo<ReportComponents>( () => ({ @@ -620,19 +477,9 @@ export function Budget() { }} > <BudgetInner - startMonth={startMonth} - collapsedPrefs={collapsedPrefs} - summaryCollapsed={summaryCollapsed} - budgetType={budgetType} - maxMonths={maxMonths} - categoryGroups={categoryGroups} - {...actions} reportComponents={reportComponents} rolloverComponents={rolloverComponents} - spreadsheet={spreadsheet} titlebar={titlebar} - navigate={navigate} - match={match} /> </View> ); diff --git a/packages/desktop-client/src/components/budget/rollover/RolloverContext.tsx b/packages/desktop-client/src/components/budget/rollover/RolloverContext.tsx index 636953b16..fcbc714cd 100644 --- a/packages/desktop-client/src/components/budget/rollover/RolloverContext.tsx +++ b/packages/desktop-client/src/components/budget/rollover/RolloverContext.tsx @@ -6,14 +6,12 @@ import * as monthUtils from 'loot-core/src/shared/months'; const Context = createContext(null); type RolloverContextProps = { - categoryGroups: unknown[]; summaryCollapsed: boolean; onBudgetAction: (idx: number, action: string, arg?: unknown) => void; onToggleSummaryCollapse: () => void; children: ReactNode; }; export function RolloverContext({ - categoryGroups, summaryCollapsed, onBudgetAction, onToggleSummaryCollapse, @@ -25,7 +23,6 @@ export function RolloverContext({ <Context.Provider value={{ currentMonth, - categoryGroups, summaryCollapsed, onBudgetAction, onToggleSummaryCollapse, diff --git a/packages/desktop-client/src/components/filters/AppliedFilters.tsx b/packages/desktop-client/src/components/filters/AppliedFilters.tsx index 1f5ead431..eac0510a1 100644 --- a/packages/desktop-client/src/components/filters/AppliedFilters.tsx +++ b/packages/desktop-client/src/components/filters/AppliedFilters.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { type RuleConditionEntity } from 'loot-core/types/models'; +import { type RuleConditionEntity } from 'loot-core/src/types/models'; import { View } from '../common/View'; diff --git a/packages/desktop-client/src/components/filters/FiltersMenu.jsx b/packages/desktop-client/src/components/filters/FiltersMenu.jsx index f4650c5a4..18889c515 100644 --- a/packages/desktop-client/src/components/filters/FiltersMenu.jsx +++ b/packages/desktop-client/src/components/filters/FiltersMenu.jsx @@ -1,5 +1,4 @@ import React, { useState, useRef, useEffect, useReducer } from 'react'; -import { useSelector } from 'react-redux'; import { FocusScope } from '@react-aria/focus'; import { @@ -21,6 +20,7 @@ import { } from 'loot-core/src/shared/rules'; import { titleFirst } from 'loot-core/src/shared/util'; +import { useDateFormat } from '../../hooks/useDateFormat'; import { theme } from '../../style'; import { Button } from '../common/Button'; import { HoverTarget } from '../common/HoverTarget'; @@ -246,11 +246,7 @@ function ConfigureField({ export function FilterButton({ onApply, compact, hover }) { const filters = useFilters(); - const { dateFormat } = useSelector(state => { - return { - dateFormat: state.prefs.local.dateFormat || 'MM/dd/yyyy', - }; - }); + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; const [state, dispatch] = useReducer( (state, action) => { diff --git a/packages/desktop-client/src/components/modals/CloseAccount.tsx b/packages/desktop-client/src/components/modals/CloseAccount.tsx index e5bc05b8d..9266c584b 100644 --- a/packages/desktop-client/src/components/modals/CloseAccount.tsx +++ b/packages/desktop-client/src/components/modals/CloseAccount.tsx @@ -2,12 +2,11 @@ import React, { useState } from 'react'; import { integerToCurrency } from 'loot-core/src/shared/util'; -import { - type AccountEntity, - type CategoryGroupEntity, -} from 'loot-core/src/types/models'; +import { type AccountEntity } from 'loot-core/src/types/models'; +import { useAccounts } from '../../hooks/useAccounts'; import { type BoundActions } from '../../hooks/useActions'; +import { useCategories } from '../../hooks/useCategories'; import { theme } from '../../style'; import { AccountAutocomplete } from '../autocomplete/AccountAutocomplete'; import { CategoryAutocomplete } from '../autocomplete/CategoryAutocomplete'; @@ -35,8 +34,6 @@ function needsCategory( type CloseAccountProps = { account: AccountEntity; - accounts: AccountEntity[]; - categoryGroups: CategoryGroupEntity[]; balance: number; canDelete: boolean; actions: BoundActions; @@ -45,8 +42,6 @@ type CloseAccountProps = { export function CloseAccount({ account, - accounts, - categoryGroups, balance, canDelete, actions, @@ -58,6 +53,8 @@ export function CloseAccount({ const [transferError, setTransferError] = useState(false); const [categoryError, setCategoryError] = useState(false); + const accounts = useAccounts().filter(a => a.closed === 0); + const { grouped: categoryGroups } = useCategories(); return ( <Modal diff --git a/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.tsx b/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.tsx index 803e95003..a448f351c 100644 --- a/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.tsx +++ b/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.tsx @@ -1,8 +1,7 @@ // @ts-strict-ignore import React, { useState } from 'react'; -import { type CategoryGroupEntity } from 'loot-core/src/types/models'; - +import { useCategories } from '../../hooks/useCategories'; import { theme } from '../../style'; import { CategoryAutocomplete } from '../autocomplete/CategoryAutocomplete'; import { Block } from '../common/Block'; @@ -14,21 +13,22 @@ import { type CommonModalProps } from '../Modals'; type ConfirmCategoryDeleteProps = { modalProps: CommonModalProps; - category: CategoryGroupEntity; - group: CategoryGroupEntity; - categoryGroups: CategoryGroupEntity[]; + category: string; + group: string; onDelete: (categoryId: string) => void; }; export function ConfirmCategoryDelete({ modalProps, - category, - group, - categoryGroups, + group: groupId, + category: categoryId, onDelete, }: ConfirmCategoryDeleteProps) { const [transferCategory, setTransferCategory] = useState<string | null>(null); const [error, setError] = useState<string | null>(null); + const { grouped: categoryGroups, list: categories } = useCategories(); + const group = categoryGroups.find(g => g.id === groupId); + const category = categories.find(c => c.id === categoryId); const renderError = (error: string) => { let msg: string; diff --git a/packages/desktop-client/src/components/modals/EditField.jsx b/packages/desktop-client/src/components/modals/EditField.jsx index fa2b492c4..0e2d3f1ff 100644 --- a/packages/desktop-client/src/components/modals/EditField.jsx +++ b/packages/desktop-client/src/components/modals/EditField.jsx @@ -1,13 +1,15 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import { parseISO, format as formatDate, parse as parseDate } from 'date-fns'; import { currentDay, dayFromDate } from 'loot-core/src/shared/months'; import { amountToInteger } from 'loot-core/src/shared/util'; +import { useAccounts } from '../../hooks/useAccounts'; import { useActions } from '../../hooks/useActions'; import { useCategories } from '../../hooks/useCategories'; +import { useDateFormat } from '../../hooks/useDateFormat'; +import { usePayees } from '../../hooks/usePayees'; import { SvgAdd } from '../../icons/v1'; import { useResponsive } from '../../ResponsiveProvider'; import { styles, theme } from '../../style'; @@ -38,12 +40,10 @@ function CreatePayeeIcon(props) { } export function EditField({ modalProps, name, onSubmit, onClose }) { - const dateFormat = useSelector( - state => state.prefs.local.dateFormat || 'MM/dd/yyyy', - ); + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; const { grouped: categoryGroups } = useCategories(); - const accounts = useSelector(state => state.queries.accounts); - const payees = useSelector(state => state.queries.payees); + const accounts = useAccounts(); + const payees = usePayees(); const { createPayee } = useActions(); diff --git a/packages/desktop-client/src/components/modals/EditRule.jsx b/packages/desktop-client/src/components/modals/EditRule.jsx index e7ebb2094..83cd8799d 100644 --- a/packages/desktop-client/src/components/modals/EditRule.jsx +++ b/packages/desktop-client/src/components/modals/EditRule.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { v4 as uuid } from 'uuid'; @@ -28,6 +28,7 @@ import { amountToInteger, } from 'loot-core/src/shared/util'; +import { useDateFormat } from '../../hooks/useDateFormat'; import { useFeatureFlag } from '../../hooks/useFeatureFlag'; import { useSelected, SelectedProvider } from '../../hooks/useSelected'; import { SvgDelete, SvgAdd, SvgSubtract } from '../../icons/v0'; @@ -268,9 +269,7 @@ function formatAmount(amount) { } function ScheduleDescription({ id }) { - const dateFormat = useSelector(state => { - return state.prefs.local.dateFormat || 'MM/dd/yyyy'; - }); + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; const scheduleData = useSchedules({ transform: useCallback(q => q.filter({ id }), []), }); diff --git a/packages/desktop-client/src/components/modals/ImportTransactions.jsx b/packages/desktop-client/src/components/modals/ImportTransactions.jsx index 89a564b70..3bd05e300 100644 --- a/packages/desktop-client/src/components/modals/ImportTransactions.jsx +++ b/packages/desktop-client/src/components/modals/ImportTransactions.jsx @@ -1,5 +1,4 @@ import React, { useState, useEffect, useMemo } from 'react'; -import { useSelector } from 'react-redux'; import * as d from 'date-fns'; @@ -11,6 +10,8 @@ import { } from 'loot-core/src/shared/util'; import { useActions } from '../../hooks/useActions'; +import { useDateFormat } from '../../hooks/useDateFormat'; +import { useLocalPrefs } from '../../hooks/useLocalPrefs'; import { theme, styles } from '../../style'; import { Button, ButtonWithLoading } from '../common/Button'; import { Input } from '../common/Input'; @@ -703,10 +704,8 @@ function FieldMappings({ } export function ImportTransactions({ modalProps, options }) { - const dateFormat = useSelector( - state => state.prefs.local.dateFormat || 'MM/dd/yyyy', - ); - const prefs = useSelector(state => state.prefs.local); + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; + const prefs = useLocalPrefs(); const { parseTransactions, importTransactions, getPayees, savePrefs } = useActions(); diff --git a/packages/desktop-client/src/components/modals/LoadBackup.jsx b/packages/desktop-client/src/components/modals/LoadBackup.jsx index d430346b2..e09f78332 100644 --- a/packages/desktop-client/src/components/modals/LoadBackup.jsx +++ b/packages/desktop-client/src/components/modals/LoadBackup.jsx @@ -2,6 +2,7 @@ import React, { Component, useState, useEffect } from 'react'; import { send, listen, unlisten } from 'loot-core/src/platform/client/fetch'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { theme } from '../../style'; import { Block } from '../common/Block'; import { Button } from '../common/Button'; @@ -55,10 +56,12 @@ export function LoadBackup({ modalProps, }) { const [backups, setBackups] = useState([]); + const [prefsBudgetId] = useLocalPref('id'); + const budgetIdToLoad = budgetId || prefsBudgetId; useEffect(() => { - send('backups-get', { id: budgetId }).then(setBackups); - }, [budgetId]); + send('backups-get', { id: budgetIdToLoad }).then(setBackups); + }, [budgetIdToLoad]); useEffect(() => { if (watchUpdates) { @@ -93,7 +96,9 @@ export function LoadBackup({ </Block> <Button type="primary" - onClick={() => actions.loadBackup(budgetId, latestBackup.id)} + onClick={() => + actions.loadBackup(budgetIdToLoad, latestBackup.id) + } > Revert to original version </Button> @@ -125,7 +130,7 @@ export function LoadBackup({ ) : ( <BackupTable backups={previousBackups} - onSelect={id => actions.loadBackup(budgetId, id)} + onSelect={id => actions.loadBackup(budgetIdToLoad, id)} /> )} </View> diff --git a/packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx b/packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx index 5e36e4f9d..33ca3d703 100644 --- a/packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx +++ b/packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx @@ -4,6 +4,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { replaceModal } from 'loot-core/src/client/actions/modals'; import { send } from 'loot-core/src/platform/client/fetch'; +import { usePayees } from '../../hooks/usePayees'; import { theme } from '../../style'; import { Information } from '../alerts'; import { Button } from '../common/Button'; @@ -15,10 +16,8 @@ import { View } from '../common/View'; const highlightStyle = { color: theme.pageTextPositive }; export function MergeUnusedPayees({ modalProps, payeeIds, targetPayeeId }) { - const { payees: allPayees, modalStack } = useSelector(state => ({ - payees: state.queries.payees, - modalStack: state.modals.modalStack, - })); + const allPayees = usePayees(); + const modalStack = useSelector(state => state.modals.modalStack); const isEditingRule = !!modalStack.find(m => m.name === 'edit-rule'); const dispatch = useDispatch(); const [shouldCreateRule, setShouldCreateRule] = useState(true); diff --git a/packages/desktop-client/src/components/modals/SelectLinkedAccounts.jsx b/packages/desktop-client/src/components/modals/SelectLinkedAccounts.jsx index 8d8d82475..8f74791de 100644 --- a/packages/desktop-client/src/components/modals/SelectLinkedAccounts.jsx +++ b/packages/desktop-client/src/components/modals/SelectLinkedAccounts.jsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; +import { useAccounts } from '../../hooks/useAccounts'; import { theme } from '../../style'; import { Autocomplete } from '../autocomplete/Autocomplete'; import { Button } from '../common/Button'; @@ -14,10 +15,10 @@ export function SelectLinkedAccounts({ modalProps, requisitionId, externalAccounts, - localAccounts, actions, syncSource, }) { + const localAccounts = useAccounts().filter(a => a.closed === 0); const [chosenAccounts, setChosenAccounts] = useState(() => { return Object.fromEntries( localAccounts diff --git a/packages/desktop-client/src/components/modals/SwitchBudgetType.tsx b/packages/desktop-client/src/components/modals/SwitchBudgetType.tsx index 80aada7a8..eb04d7e86 100644 --- a/packages/desktop-client/src/components/modals/SwitchBudgetType.tsx +++ b/packages/desktop-client/src/components/modals/SwitchBudgetType.tsx @@ -1,10 +1,7 @@ // @ts-strict-ignore import React from 'react'; -import { useSelector } from 'react-redux'; - -import { type State } from 'loot-core/src/client/state-types'; -import { type PrefsState } from 'loot-core/src/client/state-types/prefs'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { Button } from '../common/Button'; import { ExternalLink } from '../common/ExternalLink'; import { Modal } from '../common/Modal'; @@ -21,9 +18,7 @@ export function SwitchBudgetType({ modalProps, onSwitch, }: SwitchBudgetTypeProps) { - const budgetType = useSelector<State, PrefsState['local']['budgetType']>( - state => state.prefs.local.budgetType, - ); + const [budgetType] = useLocalPref('budgetType'); return ( <Modal title="Switch budget type?" {...modalProps}> {() => ( diff --git a/packages/desktop-client/src/components/payees/ManagePayeesWithData.jsx b/packages/desktop-client/src/components/payees/ManagePayeesWithData.jsx index a828c5e73..55ea8bc78 100644 --- a/packages/desktop-client/src/components/payees/ManagePayeesWithData.jsx +++ b/packages/desktop-client/src/components/payees/ManagePayeesWithData.jsx @@ -6,11 +6,12 @@ import { applyChanges } from 'loot-core/src/shared/util'; import { useActions } from '../../hooks/useActions'; import { useCategories } from '../../hooks/useCategories'; +import { usePayees } from '../../hooks/usePayees'; import { ManagePayees } from './ManagePayees'; export function ManagePayeesWithData({ initialSelectedIds }) { - const initialPayees = useSelector(state => state.queries.payees); + const initialPayees = usePayees(); const lastUndoState = useSelector(state => state.app.lastUndoState); const { grouped: categoryGroups } = useCategories(); diff --git a/packages/desktop-client/src/components/reports/Overview.jsx b/packages/desktop-client/src/components/reports/Overview.jsx index e0b8163d6..539aeda89 100644 --- a/packages/desktop-client/src/components/reports/Overview.jsx +++ b/packages/desktop-client/src/components/reports/Overview.jsx @@ -1,8 +1,8 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import { useReports } from 'loot-core/src/client/data-hooks/reports'; +import { useAccounts } from '../../hooks/useAccounts'; import { useFeatureFlag } from '../../hooks/useFeatureFlag'; import { styles } from '../../style'; import { View } from '../common/View'; @@ -18,7 +18,7 @@ export function Overview() { const customReportsFeatureFlag = useFeatureFlag('customReports'); - const accounts = useSelector(state => state.queries.accounts); + const accounts = useAccounts(); return ( <View style={{ diff --git a/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx b/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx index a261be8e3..54b142030 100644 --- a/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx +++ b/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx @@ -13,13 +13,13 @@ import { ResponsiveContainer, } from 'recharts'; -import { usePrivacyMode } from 'loot-core/src/client/privacy'; import { amountToCurrency, amountToCurrencyNoDecimal, } from 'loot-core/src/shared/util'; import { type GroupedEntity } from 'loot-core/src/types/models/reports'; +import { usePrivacyMode } from '../../../hooks/usePrivacyMode'; import { theme } from '../../../style'; import { type CSSProperties } from '../../../style'; import { AlignedText } from '../../common/AlignedText'; diff --git a/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx b/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx index 3d81b5ced..6263c6651 100644 --- a/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx +++ b/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx @@ -15,13 +15,13 @@ import { ResponsiveContainer, } from 'recharts'; -import { usePrivacyMode } from 'loot-core/src/client/privacy'; import { amountToCurrency, amountToCurrencyNoDecimal, } from 'loot-core/src/shared/util'; import { type GroupedEntity } from 'loot-core/src/types/models/reports'; +import { usePrivacyMode } from '../../../hooks/usePrivacyMode'; import { theme } from '../../../style'; import { type CSSProperties } from '../../../style'; import { AlignedText } from '../../common/AlignedText'; diff --git a/packages/desktop-client/src/components/reports/graphs/CashFlowGraph.tsx b/packages/desktop-client/src/components/reports/graphs/CashFlowGraph.tsx index e186a70b6..8334fa9de 100644 --- a/packages/desktop-client/src/components/reports/graphs/CashFlowGraph.tsx +++ b/packages/desktop-client/src/components/reports/graphs/CashFlowGraph.tsx @@ -15,12 +15,12 @@ import { type TooltipProps, } from 'recharts'; -import { usePrivacyMode } from 'loot-core/src/client/privacy'; import { amountToCurrency, amountToCurrencyNoDecimal, } from 'loot-core/src/shared/util'; +import { usePrivacyMode } from '../../../hooks/usePrivacyMode'; import { theme } from '../../../style'; import { AlignedText } from '../../common/AlignedText'; import { chartTheme } from '../chart-theme'; diff --git a/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx b/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx index ab2fcf993..777f4be4c 100644 --- a/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx +++ b/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx @@ -13,13 +13,13 @@ import { ResponsiveContainer, } from 'recharts'; -import { usePrivacyMode } from 'loot-core/src/client/privacy'; import { amountToCurrency, amountToCurrencyNoDecimal, } from 'loot-core/src/shared/util'; import { type GroupedEntity } from 'loot-core/src/types/models/reports'; +import { usePrivacyMode } from '../../../hooks/usePrivacyMode'; import { theme } from '../../../style'; import { type CSSProperties } from '../../../style'; import { AlignedText } from '../../common/AlignedText'; diff --git a/packages/desktop-client/src/components/reports/reports/CustomReport.jsx b/packages/desktop-client/src/components/reports/reports/CustomReport.jsx index f786572ef..139f4716f 100644 --- a/packages/desktop-client/src/components/reports/reports/CustomReport.jsx +++ b/packages/desktop-client/src/components/reports/reports/CustomReport.jsx @@ -1,18 +1,17 @@ import React, { useState, useEffect, useMemo } from 'react'; -import { useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; import * as d from 'date-fns'; -import { useCachedAccounts } from 'loot-core/src/client/data-hooks/accounts'; -import { useCachedPayees } from 'loot-core/src/client/data-hooks/payees'; import { send } from 'loot-core/src/platform/client/fetch'; import * as monthUtils from 'loot-core/src/shared/months'; import { amountToCurrency } from 'loot-core/src/shared/util'; -import { useActions } from '../../../hooks/useActions'; +import { useAccounts } from '../../../hooks/useAccounts'; import { useCategories } from '../../../hooks/useCategories'; import { useFilters } from '../../../hooks/useFilters'; +import { useLocalPref } from '../../../hooks/useLocalPref'; +import { usePayees } from '../../../hooks/usePayees'; import { theme, styles } from '../../../style'; import { AlignedText } from '../../common/AlignedText'; import { Block } from '../../common/Block'; @@ -36,13 +35,12 @@ import { fromDateRepr } from '../util'; export function CustomReport() { const categories = useCategories(); - const viewLegend = - useSelector(state => state.prefs.local?.reportsViewLegend) || false; - const viewSummary = - useSelector(state => state.prefs.local?.reportsViewSummary) || false; - const viewLabels = - useSelector(state => state.prefs.local?.reportsViewLabel) || false; - const { savePrefs } = useActions(); + const [viewLegend = false, setViewLegendPref] = + useLocalPref('reportsViewLegend'); + const [viewSummary = false, setViewSummaryPref] = + useLocalPref('reportsViewSummary'); + const [viewLabels = false, setViewLabelsPref] = + useLocalPref('reportsViewLabel'); const { filters, @@ -126,8 +124,8 @@ export function CustomReport() { }, []); const balanceTypeOp = ReportOptions.balanceTypeMap.get(balanceType); - const payees = useCachedPayees(); - const accounts = useCachedAccounts(); + const payees = usePayees(); + const accounts = useAccounts(); const getGroupData = useMemo(() => { return createGroupedSpreadsheet({ @@ -235,13 +233,13 @@ export function CustomReport() { const onChangeViews = (viewType, status) => { if (viewType === 'viewLegend') { - savePrefs({ reportsViewLegend: status ?? !viewLegend }); + setViewLegendPref(status ?? !viewLegend); } if (viewType === 'viewSummary') { - savePrefs({ reportsViewSummary: !viewSummary }); + setViewSummaryPref(!viewSummary); } if (viewType === 'viewLabels') { - savePrefs({ reportsViewLabel: !viewLabels }); + setViewLabelsPref(!viewLabels); } }; diff --git a/packages/desktop-client/src/components/reports/reports/NetWorth.jsx b/packages/desktop-client/src/components/reports/reports/NetWorth.jsx index 5bdd779b2..4dbec9a8e 100644 --- a/packages/desktop-client/src/components/reports/reports/NetWorth.jsx +++ b/packages/desktop-client/src/components/reports/reports/NetWorth.jsx @@ -1,5 +1,4 @@ import React, { useState, useEffect, useMemo } from 'react'; -import { useSelector } from 'react-redux'; import * as d from 'date-fns'; @@ -7,6 +6,7 @@ import { send } from 'loot-core/src/platform/client/fetch'; import * as monthUtils from 'loot-core/src/shared/months'; import { integerToCurrency } from 'loot-core/src/shared/util'; +import { useAccounts } from '../../../hooks/useAccounts'; import { useFilters } from '../../../hooks/useFilters'; import { theme, styles } from '../../../style'; import { Paragraph } from '../../common/Paragraph'; @@ -20,7 +20,7 @@ import { useReport } from '../useReport'; import { fromDateRepr } from '../util'; export function NetWorth() { - const accounts = useSelector(state => state.queries.accounts); + const accounts = useAccounts(); const { filters, saved, diff --git a/packages/desktop-client/src/components/reports/spreadsheets/filterEmptyRows.ts b/packages/desktop-client/src/components/reports/spreadsheets/filterEmptyRows.ts index 45efef476..c0b6e8f34 100644 --- a/packages/desktop-client/src/components/reports/spreadsheets/filterEmptyRows.ts +++ b/packages/desktop-client/src/components/reports/spreadsheets/filterEmptyRows.ts @@ -1,5 +1,5 @@ // @ts-strict-ignore -import { type GroupedEntity } from 'loot-core/types/models/reports'; +import { type GroupedEntity } from 'loot-core/src/types/models/reports'; export function filterEmptyRows( showEmpty: boolean, diff --git a/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts b/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts index 0adc9b119..f44cddf8c 100644 --- a/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts +++ b/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts @@ -3,7 +3,7 @@ import { runQuery } from 'loot-core/src/client/query-helpers'; import { send } from 'loot-core/src/platform/client/fetch'; import * as monthUtils from 'loot-core/src/shared/months'; import { integerToAmount } from 'loot-core/src/shared/util'; -import { type GroupedEntity } from 'loot-core/types/models/reports'; +import { type GroupedEntity } from 'loot-core/src/types/models/reports'; import { categoryLists } from '../ReportOptions'; diff --git a/packages/desktop-client/src/components/rules/ScheduleValue.tsx b/packages/desktop-client/src/components/rules/ScheduleValue.tsx index 036d0a2ed..fb05b0b8f 100644 --- a/packages/desktop-client/src/components/rules/ScheduleValue.tsx +++ b/packages/desktop-client/src/components/rules/ScheduleValue.tsx @@ -1,12 +1,11 @@ import React from 'react'; -import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type QueriesState } from 'loot-core/client/state-types/queries'; import { getPayeesById } from 'loot-core/src/client/reducers/queries'; import { describeSchedule } from 'loot-core/src/shared/schedules'; import { type ScheduleEntity } from 'loot-core/src/types/models'; +import { usePayees } from '../../hooks/usePayees'; + import { SchedulesQuery } from './SchedulesQuery'; import { Value } from './Value'; @@ -15,9 +14,7 @@ type ScheduleValueProps = { }; export function ScheduleValue({ value }: ScheduleValueProps) { - const payees = useSelector<State, QueriesState['payees']>( - state => state.queries.payees, - ); + const payees = usePayees(); const byId = getPayeesById(payees); const { data: schedules } = SchedulesQuery.useQuery(); diff --git a/packages/desktop-client/src/components/rules/Value.tsx b/packages/desktop-client/src/components/rules/Value.tsx index fe9ccc73c..d8b184b39 100644 --- a/packages/desktop-client/src/components/rules/Value.tsx +++ b/packages/desktop-client/src/components/rules/Value.tsx @@ -1,17 +1,16 @@ // @ts-strict-ignore import React, { useState } from 'react'; -import { useSelector } from 'react-redux'; import { format as formatDate, parseISO } from 'date-fns'; -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; -import { type QueriesState } from 'loot-core/client/state-types/queries'; import { getMonthYearFormat } from 'loot-core/src/shared/months'; import { getRecurringDescription } from 'loot-core/src/shared/schedules'; import { integerToCurrency } from 'loot-core/src/shared/util'; +import { useAccounts } from '../../hooks/useAccounts'; import { useCategories } from '../../hooks/useCategories'; +import { useDateFormat } from '../../hooks/useDateFormat'; +import { usePayees } from '../../hooks/usePayees'; import { type CSSProperties, theme } from '../../style'; import { LinkButton } from '../common/LinkButton'; import { Text } from '../common/Text'; @@ -36,16 +35,10 @@ export function Value<T>({ describe = x => x.name, style, }: ValueProps<T>) { - const dateFormat = useSelector<State, PrefsState['local']['dateFormat']>( - state => state.prefs.local.dateFormat || 'MM/dd/yyyy', - ); - const payees = useSelector<State, QueriesState['payees']>( - state => state.queries.payees, - ); + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; + const payees = usePayees(); const { list: categories } = useCategories(); - const accounts = useSelector<State, QueriesState['accounts']>( - state => state.queries.accounts, - ); + const accounts = useAccounts(); const valueStyle = { color: theme.pageTextPositive, ...style, diff --git a/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx b/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx index 03a0aebaf..1f62f40c8 100644 --- a/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx +++ b/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx @@ -1,9 +1,6 @@ // @ts-strict-ignore import React, { useState } from 'react'; -import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; import { runQuery } from 'loot-core/src/client/query-helpers'; import { send } from 'loot-core/src/platform/client/fetch'; import { q } from 'loot-core/src/shared/query'; @@ -11,6 +8,7 @@ import { getRecurringDescription } from 'loot-core/src/shared/schedules'; import type { DiscoverScheduleEntity } from 'loot-core/src/types/models'; import type { BoundActions } from '../../hooks/useActions'; +import { useDateFormat } from '../../hooks/useDateFormat'; import { useSelected, useSelectedDispatch, @@ -41,9 +39,7 @@ function DiscoverSchedulesTable({ }) { const selectedItems = useSelectedItems(); const dispatchSelected = useSelectedDispatch(); - const dateFormat = useSelector<State, PrefsState['local']['dateFormat']>( - state => state.prefs.local.dateFormat || 'MM/dd/yyyy', - ); + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; function renderItem({ item }: { item: DiscoverScheduleEntity }) { const selected = selectedItems.has(item.id); diff --git a/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx b/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx index 85446055b..56d20b442 100644 --- a/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx +++ b/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx @@ -1,14 +1,15 @@ import React, { useEffect, useReducer } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { pushModal } from 'loot-core/src/client/actions/modals'; -import { useCachedPayees } from 'loot-core/src/client/data-hooks/payees'; import { runQuery, liveQuery } from 'loot-core/src/client/query-helpers'; import { send, sendCatch } from 'loot-core/src/platform/client/fetch'; import * as monthUtils from 'loot-core/src/shared/months'; import { q } from 'loot-core/src/shared/query'; import { extractScheduleConds } from 'loot-core/src/shared/schedules'; +import { useDateFormat } from '../../hooks/useDateFormat'; +import { usePayees } from '../../hooks/usePayees'; import { useSelected, SelectedProvider } from '../../hooks/useSelected'; import { theme } from '../../style'; import { AccountAutocomplete } from '../autocomplete/AccountAutocomplete'; @@ -70,11 +71,10 @@ function updateScheduleConditions(schedule, fields) { export function ScheduleDetails({ modalProps, actions, id, transaction }) { const adding = id == null; const fromTrans = transaction != null; - const payees = useCachedPayees({ idKey: true }); + const payees = usePayees({ idKey: true }); const globalDispatch = useDispatch(); - const dateFormat = useSelector(state => { - return state.prefs.local.dateFormat || 'MM/dd/yyyy'; - }); + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; + const [state, dispatch] = useReducer( (state, action) => { switch (action.type) { diff --git a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx index b2ec6ea02..ba34e2df1 100644 --- a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx +++ b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx @@ -1,11 +1,6 @@ // @ts-strict-ignore import React, { useState, useMemo, type CSSProperties } from 'react'; -import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; -import { useCachedAccounts } from 'loot-core/src/client/data-hooks/accounts'; -import { useCachedPayees } from 'loot-core/src/client/data-hooks/payees'; import { type ScheduleStatusType, type ScheduleStatuses, @@ -15,6 +10,9 @@ import { getScheduledAmount } from 'loot-core/src/shared/schedules'; import { integerToCurrency } from 'loot-core/src/shared/util'; import { type ScheduleEntity } from 'loot-core/src/types/models'; +import { useAccounts } from '../../hooks/useAccounts'; +import { useDateFormat } from '../../hooks/useDateFormat'; +import { usePayees } from '../../hooks/usePayees'; import { SvgDotsHorizontalTriple } from '../../icons/v1'; import { SvgCheck } from '../../icons/v2'; import { theme } from '../../style'; @@ -196,16 +194,11 @@ export function SchedulesTable({ onAction, tableStyle, }: SchedulesTableProps) { - const dateFormat = useSelector<State, PrefsState['local']['dateFormat']>( - state => { - return state.prefs.local.dateFormat || 'MM/dd/yyyy'; - }, - ); - + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; const [showCompleted, setShowCompleted] = useState(false); - const payees = useCachedPayees(); - const accounts = useCachedAccounts(); + const payees = usePayees(); + const accounts = useAccounts(); const filteredSchedules = useMemo(() => { if (!filter) { @@ -240,7 +233,7 @@ export function SchedulesTable({ filterIncludes(dateStr) ); }); - }, [schedules, filter, statuses]); + }, [payees, accounts, schedules, filter, statuses]); const items: SchedulesTableItem[] = useMemo(() => { const unCompletedSchedules = filteredSchedules.filter(s => !s.completed); diff --git a/packages/desktop-client/src/components/select/DateSelect.tsx b/packages/desktop-client/src/components/select/DateSelect.tsx index a6b83fc65..6893edc8d 100644 --- a/packages/desktop-client/src/components/select/DateSelect.tsx +++ b/packages/desktop-client/src/components/select/DateSelect.tsx @@ -10,15 +10,12 @@ import React, { type MutableRefObject, type KeyboardEvent, } from 'react'; -import { useSelector } from 'react-redux'; import { parse, parseISO, format, subDays, addDays, isValid } from 'date-fns'; import Pikaday from 'pikaday'; import 'pikaday/css/pikaday.css'; -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; import { getDayMonthFormat, getDayMonthRegex, @@ -28,6 +25,7 @@ import { } from 'loot-core/src/shared/months'; import { stringToInteger } from 'loot-core/src/shared/util'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { type CSSProperties, theme } from '../../style'; import { Input, type InputProps } from '../common/Input'; import { View, type ViewProps } from '../common/View'; @@ -233,14 +231,8 @@ export function DateSelect({ const [selectedValue, setSelectedValue] = useState(value); const userSelectedValue = useRef(selectedValue); - const firstDayOfWeekIdx = useSelector< - State, - PrefsState['local']['firstDayOfWeekIdx'] - >(state => - state.prefs.local?.firstDayOfWeekIdx - ? state.prefs.local.firstDayOfWeekIdx - : '0', - ); + const [_firstDayOfWeekIdx] = useLocalPref('firstDayOfWeekIdx'); + const firstDayOfWeekIdx = _firstDayOfWeekIdx || '0'; useEffect(() => { userSelectedValue.current = value; diff --git a/packages/desktop-client/src/components/select/RecurringSchedulePicker.jsx b/packages/desktop-client/src/components/select/RecurringSchedulePicker.jsx index df1023716..17ee82e87 100644 --- a/packages/desktop-client/src/components/select/RecurringSchedulePicker.jsx +++ b/packages/desktop-client/src/components/select/RecurringSchedulePicker.jsx @@ -1,10 +1,10 @@ import React, { useEffect, useReducer, useState } from 'react'; -import { useSelector } from 'react-redux'; import { sendCatch } from 'loot-core/src/platform/client/fetch'; import * as monthUtils from 'loot-core/src/shared/months'; import { getRecurringDescription } from 'loot-core/src/shared/schedules'; +import { useDateFormat } from '../../hooks/useDateFormat'; import { SvgAdd, SvgSubtract } from '../../icons/v0'; import { theme } from '../../style'; import { Button } from '../common/Button'; @@ -159,11 +159,9 @@ function reducer(state, action) { } function SchedulePreview({ previewDates }) { - const dateFormat = useSelector(state => - (state.prefs.local.dateFormat || 'MM/dd/yyyy') - .replace('MM', 'M') - .replace('dd', 'd'), - ); + const dateFormat = (useDateFormat() || 'MM/dd/yyyy') + .replace('MM', 'M') + .replace('dd', 'd'); if (!previewDates) { return null; @@ -281,9 +279,7 @@ function RecurringScheduleTooltip({ config: currentConfig, onClose, onSave }) { const skipWeekend = state.config.hasOwnProperty('skipWeekend') ? state.config.skipWeekend : false; - const dateFormat = useSelector( - state => state.prefs.local.dateFormat || 'MM/dd/yyyy', - ); + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; useEffect(() => { dispatch({ @@ -481,9 +477,7 @@ function RecurringScheduleTooltip({ config: currentConfig, onClose, onSave }) { export function RecurringSchedulePicker({ value, buttonStyle, onChange }) { const { isOpen, close, getOpenEvents } = useTooltip(); - const dateFormat = useSelector( - state => state.prefs.local.dateFormat || 'MM/dd/yyyy', - ); + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; function onSave(config) { onChange(config); diff --git a/packages/desktop-client/src/components/settings/Encryption.tsx b/packages/desktop-client/src/components/settings/Encryption.tsx index cfe994b13..97a7660ce 100644 --- a/packages/desktop-client/src/components/settings/Encryption.tsx +++ b/packages/desktop-client/src/components/settings/Encryption.tsx @@ -1,11 +1,8 @@ // @ts-strict-ignore import React from 'react'; -import { useSelector } from 'react-redux'; - -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; import { useActions } from '../../hooks/useActions'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { theme } from '../../style'; import { Button } from '../common/Button'; import { ExternalLink } from '../common/ExternalLink'; @@ -17,9 +14,7 @@ import { Setting } from './UI'; export function EncryptionSettings() { const { pushModal } = useActions(); const serverURL = useServerURL(); - const encryptKeyId = useSelector<State, PrefsState['local']['encryptKeyId']>( - state => state.prefs.local.encryptKeyId, - ); + const [encryptKeyId] = useLocalPref('encryptKeyId'); const missingCryptoAPI = !(window.crypto && crypto.subtle); diff --git a/packages/desktop-client/src/components/settings/Experimental.tsx b/packages/desktop-client/src/components/settings/Experimental.tsx index fc1a942c5..15f20989f 100644 --- a/packages/desktop-client/src/components/settings/Experimental.tsx +++ b/packages/desktop-client/src/components/settings/Experimental.tsx @@ -1,12 +1,9 @@ import { type ReactNode, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/src/client/state-types'; -import { type PrefsState } from 'loot-core/src/client/state-types/prefs'; import type { FeatureFlag } from 'loot-core/src/types/prefs'; -import { useActions } from '../../hooks/useActions'; import { useFeatureFlag } from '../../hooks/useFeatureFlag'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { theme } from '../../style'; import { LinkButton } from '../common/LinkButton'; import { Text } from '../common/Text'; @@ -23,23 +20,20 @@ type FeatureToggleProps = { }; function FeatureToggle({ - flag, + flag: flagName, disableToggle = false, error, children, }: FeatureToggleProps) { - const { savePrefs } = useActions(); - const enabled = useFeatureFlag(flag); + const enabled = useFeatureFlag(flagName); + const [_, setFlagPref] = useLocalPref(`flags.${flagName}`); return ( <label style={{ display: 'flex' }}> <Checkbox checked={enabled} onChange={() => { - // @ts-expect-error key type is not correctly inferred - savePrefs({ - [`flags.${flag}`]: !enabled, - }); + setFlagPref(!enabled); }} disabled={disableToggle} /> @@ -63,9 +57,7 @@ function FeatureToggle({ } function ReportBudgetFeature() { - const budgetType = useSelector<State, PrefsState['local']['budgetType']>( - state => state.prefs.local?.budgetType, - ); + const [budgetType] = useLocalPref('budgetType'); const enabled = useFeatureFlag('reportBudget'); const blockToggleOff = budgetType === 'report' && enabled; return ( diff --git a/packages/desktop-client/src/components/settings/Export.tsx b/packages/desktop-client/src/components/settings/Export.tsx index 6cd2e7449..824c0f4fa 100644 --- a/packages/desktop-client/src/components/settings/Export.tsx +++ b/packages/desktop-client/src/components/settings/Export.tsx @@ -1,13 +1,11 @@ // @ts-strict-ignore import React, { useState } from 'react'; -import { useSelector } from 'react-redux'; import { format } from 'date-fns'; -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; import { send } from 'loot-core/src/platform/client/fetch'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { theme } from '../../style'; import { Block } from '../common/Block'; import { ButtonWithLoading } from '../common/Button'; @@ -18,12 +16,8 @@ import { Setting } from './UI'; export function ExportBudget() { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState<string | null>(null); - const budgetId = useSelector<State, PrefsState['local']['id']>( - state => state.prefs.local.id, - ); - const encryptKeyId = useSelector<State, PrefsState['local']['encryptKeyId']>( - state => state.prefs.local.encryptKeyId, - ); + const [budgetId] = useLocalPref('id'); + const [encryptKeyId] = useLocalPref('encryptKeyId'); async function onExport() { setIsLoading(true); diff --git a/packages/desktop-client/src/components/settings/Format.tsx b/packages/desktop-client/src/components/settings/Format.tsx index 8f36cc434..0dc2fbf03 100644 --- a/packages/desktop-client/src/components/settings/Format.tsx +++ b/packages/desktop-client/src/components/settings/Format.tsx @@ -1,20 +1,18 @@ // @ts-strict-ignore import React, { type ReactNode } from 'react'; -import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; import { numberFormats } from 'loot-core/src/shared/util'; import { type LocalPrefs } from 'loot-core/src/types/prefs'; -import { useActions } from '../../hooks/useActions'; +import { useDateFormat } from '../../hooks/useDateFormat'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { tokens } from '../../tokens'; import { Button } from '../common/Button'; import { Select } from '../common/Select'; import { Text } from '../common/Text'; import { View } from '../common/View'; import { Checkbox } from '../forms'; -import { useSidebar } from '../sidebar'; +import { useSidebar } from '../sidebar/SidebarProvider'; import { Setting } from './UI'; @@ -56,24 +54,16 @@ function Column({ title, children }: { title: string; children: ReactNode }) { } export function FormatSettings() { - const { savePrefs } = useActions(); - const sidebar = useSidebar(); - const firstDayOfWeekIdx = useSelector< - State, - PrefsState['local']['firstDayOfWeekIdx'] - >( - state => state.prefs.local.firstDayOfWeekIdx || '0', // Sunday - ); - const dateFormat = useSelector<State, PrefsState['local']['dateFormat']>( - state => state.prefs.local.dateFormat || 'MM/dd/yyyy', - ); - const numberFormat = useSelector<State, PrefsState['local']['numberFormat']>( - state => state.prefs.local.numberFormat || 'comma-dot', - ); - const hideFraction = useSelector<State, PrefsState['local']['hideFraction']>( - state => state.prefs.local.hideFraction, - ); + const [_firstDayOfWeekIdx, setFirstDayOfWeekIdxPref] = + useLocalPref('firstDayOfWeekIdx'); // Sunday; + const firstDayOfWeekIdx = _firstDayOfWeekIdx || '0'; + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; + const [, setDateFormatPref] = useLocalPref('dateFormat'); + const [_numberFormat, setNumberFormatPref] = useLocalPref('numberFormat'); + const numberFormat = _numberFormat || 'comma-dot'; + const [hideFraction = false, setHideFractionPref] = + useLocalPref('hideFraction'); return ( <Setting @@ -98,7 +88,7 @@ export function FormatSettings() { bare key={String(hideFraction)} // needed because label does not update value={numberFormat} - onChange={format => savePrefs({ numberFormat: format })} + onChange={format => setNumberFormatPref(format)} options={numberFormats.map(f => [ f.value, hideFraction ? f.labelNoFraction : f.label, @@ -111,9 +101,7 @@ export function FormatSettings() { <Checkbox id="settings-textDecimal" checked={!!hideFraction} - onChange={e => - savePrefs({ hideFraction: e.currentTarget.checked }) - } + onChange={e => setHideFractionPref(e.currentTarget.checked)} /> <label htmlFor="settings-textDecimal">Hide decimal places</label> </Text> @@ -124,7 +112,7 @@ export function FormatSettings() { <Select bare value={dateFormat} - onChange={format => savePrefs({ dateFormat: format })} + onChange={format => setDateFormatPref(format)} options={dateFormats.map(f => [f.value, f.label])} style={{ padding: '2px 10px', fontSize: 15 }} /> @@ -136,7 +124,7 @@ export function FormatSettings() { <Select bare value={firstDayOfWeekIdx} - onChange={idx => savePrefs({ firstDayOfWeekIdx: idx })} + onChange={idx => setFirstDayOfWeekIdxPref(idx)} options={daysOfWeek.map(f => [f.value, f.label])} style={{ padding: '2px 10px', fontSize: 15 }} /> diff --git a/packages/desktop-client/src/components/settings/Global.tsx b/packages/desktop-client/src/components/settings/Global.tsx index aad153e5e..92e4ba843 100644 --- a/packages/desktop-client/src/components/settings/Global.tsx +++ b/packages/desktop-client/src/components/settings/Global.tsx @@ -1,11 +1,7 @@ // @ts-strict-ignore import React, { useState, useEffect, useRef } from 'react'; -import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; - -import { useActions } from '../../hooks/useActions'; +import { useGlobalPref } from '../../hooks/useGlobalPref'; import { theme } from '../../style'; import { Information } from '../alerts'; import { Button } from '../common/Button'; @@ -15,10 +11,7 @@ import { View } from '../common/View'; import { Setting } from './UI'; export function GlobalSettings() { - const documentDir = useSelector<State, PrefsState['global']['documentDir']>( - state => state.prefs.global.documentDir, - ); - const { saveGlobalPrefs } = useActions(); + const [documentDir, setDocumentDirPref] = useGlobalPref('documentDir'); const [documentDirChanged, setDirChanged] = useState(false); const dirScrolled = useRef<HTMLSpanElement>(null); @@ -34,7 +27,7 @@ export function GlobalSettings() { properties: ['openDirectory'], }); if (res) { - saveGlobalPrefs({ documentDir: res[0] }); + setDocumentDirPref(res[0]); setDirChanged(true); } } diff --git a/packages/desktop-client/src/components/settings/Reset.tsx b/packages/desktop-client/src/components/settings/Reset.tsx index 744ada38e..0e8ea3cea 100644 --- a/packages/desktop-client/src/components/settings/Reset.tsx +++ b/packages/desktop-client/src/components/settings/Reset.tsx @@ -1,12 +1,10 @@ // @ts-strict-ignore import React, { useState } from 'react'; -import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; import { send } from 'loot-core/src/platform/client/fetch'; import { useActions } from '../../hooks/useActions'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { ButtonWithLoading } from '../common/Button'; import { Text } from '../common/Text'; @@ -41,9 +39,8 @@ export function ResetCache() { } export function ResetSync() { - const isEnabled = !!useSelector<State, PrefsState['local']['groupId']>( - state => state.prefs.local.groupId, - ); + const [groupId] = useLocalPref('groupId'); + const isEnabled = !!groupId; const { resetSync } = useActions(); const [resetting, setResetting] = useState(false); diff --git a/packages/desktop-client/src/components/settings/Themes.tsx b/packages/desktop-client/src/components/settings/Themes.tsx index 775ef5253..7966b546a 100644 --- a/packages/desktop-client/src/components/settings/Themes.tsx +++ b/packages/desktop-client/src/components/settings/Themes.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { useActions } from '../../hooks/useActions'; +import { type Theme } from 'loot-core/types/prefs'; + import { themeOptions, useTheme } from '../../style'; import { Button } from '../common/Button'; import { Select } from '../common/Select'; @@ -9,17 +10,16 @@ import { Text } from '../common/Text'; import { Setting } from './UI'; export function ThemeSettings() { - const theme = useTheme(); - const { saveGlobalPrefs } = useActions(); + const [theme, switchTheme] = useTheme(); return ( <Setting primaryAction={ <Button bounce={false} style={{ padding: 0 }}> - <Select + <Select<Theme> bare onChange={value => { - saveGlobalPrefs({ theme: value }); + switchTheme(value); }} value={theme} options={themeOptions} diff --git a/packages/desktop-client/src/components/settings/index.tsx b/packages/desktop-client/src/components/settings/index.tsx index 454f41575..2f7ef5ada 100644 --- a/packages/desktop-client/src/components/settings/index.tsx +++ b/packages/desktop-client/src/components/settings/index.tsx @@ -1,16 +1,15 @@ // @ts-strict-ignore import React, { type ReactNode, useEffect } from 'react'; -import { useSelector } from 'react-redux'; import { media } from 'glamor'; -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; import * as Platform from 'loot-core/src/client/platform'; import { listen } from 'loot-core/src/platform/client/fetch'; import { useActions } from '../../hooks/useActions'; +import { useGlobalPref } from '../../hooks/useGlobalPref'; import { useLatestVersion, useIsOutdated } from '../../hooks/useLatestVersion'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { useSetThemeColor } from '../../hooks/useSetThemeColor'; import { useResponsive } from '../../ResponsiveProvider'; import { theme } from '../../style'; @@ -91,12 +90,8 @@ function IDName({ children }: { children: ReactNode }) { } function AdvancedAbout() { - const budgetId = useSelector<State, PrefsState['local']['id']>( - state => state.prefs.local.id, - ); - const groupId = useSelector<State, PrefsState['local']['groupId']>( - state => state.prefs.local.groupId, - ); + const [budgetId] = useLocalPref('id'); + const [groupId] = useLocalPref('groupId'); return ( <Setting> @@ -124,13 +119,8 @@ function AdvancedAbout() { } export function Settings() { - const floatingSidebar = useSelector< - State, - PrefsState['global']['floatingSidebar'] - >(state => state.prefs.global.floatingSidebar); - const budgetName = useSelector<State, PrefsState['local']['budgetName']>( - state => state.prefs.local.budgetName, - ); + const [floatingSidebar] = useGlobalPref('floatingSidebar'); + const [budgetName] = useLocalPref('budgetName'); const { loadPrefs, closeBudget } = useActions(); diff --git a/packages/desktop-client/src/components/sidebar/Accounts.tsx b/packages/desktop-client/src/components/sidebar/Accounts.tsx index cd20ccb58..e85e32e4a 100644 --- a/packages/desktop-client/src/components/sidebar/Accounts.tsx +++ b/packages/desktop-client/src/components/sidebar/Accounts.tsx @@ -1,12 +1,17 @@ // @ts-strict-ignore -import React, { useState, useMemo } from 'react'; +import React, { useState } from 'react'; -import { type AccountEntity } from 'loot-core/src/types/models'; +import * as queries from 'loot-core/src/client/queries'; +import { useBudgetedAccounts } from '../../hooks/useBudgetedAccounts'; +import { useClosedAccounts } from '../../hooks/useClosedAccounts'; +import { useFailedAccounts } from '../../hooks/useFailedAccounts'; +import { useLocalPref } from '../../hooks/useLocalPref'; +import { useOffBudgetAccounts } from '../../hooks/useOffBudgetAccounts'; +import { useUpdatedAccounts } from '../../hooks/useUpdatedAccounts'; import { SvgAdd } from '../../icons/v1'; import { View } from '../common/View'; import { type OnDropCallback } from '../sort'; -import { type Binding } from '../spreadsheet'; import { Account } from './Account'; import { SecondaryItem } from './SecondaryItem'; @@ -14,65 +19,26 @@ import { SecondaryItem } from './SecondaryItem'; const fontWeight = 600; type AccountsProps = { - accounts: AccountEntity[]; - failedAccounts: Map< - string, - { - type: string; - code: string; - } - >; - updatedAccounts: string[]; - getAccountPath: (account: AccountEntity) => string; - allAccountsPath: string; - budgetedAccountPath: string; - offBudgetAccountPath: string; - getBalanceQuery: (account: AccountEntity) => Binding; - getAllAccountBalance: () => Binding; - getOnBudgetBalance: () => Binding; - getOffBudgetBalance: () => Binding; - showClosedAccounts: boolean; onAddAccount: () => void; onToggleClosedAccounts: () => void; onReorder: OnDropCallback; }; export function Accounts({ - accounts, - failedAccounts, - updatedAccounts, - getAccountPath, - allAccountsPath, - budgetedAccountPath, - offBudgetAccountPath, - getBalanceQuery, - getAllAccountBalance, - getOnBudgetBalance, - getOffBudgetBalance, - showClosedAccounts, onAddAccount, onToggleClosedAccounts, onReorder, }: AccountsProps) { const [isDragging, setIsDragging] = useState(false); - const offbudgetAccounts = useMemo( - () => - accounts.filter( - account => account.closed === 0 && account.offbudget === 1, - ), - [accounts], - ); - const budgetedAccounts = useMemo( - () => - accounts.filter( - account => account.closed === 0 && account.offbudget === 0, - ), - [accounts], - ); - const closedAccounts = useMemo( - () => accounts.filter(account => account.closed === 1), - [accounts], - ); + const failedAccounts = useFailedAccounts(); + const updatedAccounts = useUpdatedAccounts(); + const offbudgetAccounts = useOffBudgetAccounts(); + const budgetedAccounts = useBudgetedAccounts(); + const closedAccounts = useClosedAccounts(); + + const getAccountPath = account => `/accounts/${account.id}`; + + const [showClosedAccounts] = useLocalPref('ui.showClosedAccounts'); function onDragChange(drag) { setIsDragging(drag.state === 'start'); @@ -92,16 +58,16 @@ export function Accounts({ <View> <Account name="All accounts" - to={allAccountsPath} - query={getAllAccountBalance()} + to="/accounts" + query={queries.allAccountBalance()} style={{ fontWeight, marginTop: 15 }} /> {budgetedAccounts.length > 0 && ( <Account name="For budget" - to={budgetedAccountPath} - query={getOnBudgetBalance()} + to="/accounts/budgeted" + query={queries.budgetedAccountBalance()} style={{ fontWeight, marginTop: 13 }} /> )} @@ -115,7 +81,7 @@ export function Accounts({ failed={failedAccounts && failedAccounts.has(account.id)} updated={updatedAccounts && updatedAccounts.includes(account.id)} to={getAccountPath(account)} - query={getBalanceQuery(account)} + query={queries.accountBalance(account)} onDragChange={onDragChange} onDrop={onReorder} outerStyle={makeDropPadding(i)} @@ -125,8 +91,8 @@ export function Accounts({ {offbudgetAccounts.length > 0 && ( <Account name="Off budget" - to={offBudgetAccountPath} - query={getOffBudgetBalance()} + to="/accounts/offbudget" + query={queries.offbudgetAccountBalance()} style={{ fontWeight, marginTop: 13 }} /> )} @@ -140,7 +106,7 @@ export function Accounts({ failed={failedAccounts && failedAccounts.has(account.id)} updated={updatedAccounts && updatedAccounts.includes(account.id)} to={getAccountPath(account)} - query={getBalanceQuery(account)} + query={queries.accountBalance(account)} onDragChange={onDragChange} onDrop={onReorder} outerStyle={makeDropPadding(i)} @@ -163,7 +129,7 @@ export function Accounts({ name={account.name} account={account} to={getAccountPath(account)} - query={getBalanceQuery(account)} + query={queries.accountBalance(account)} onDragChange={onDragChange} onDrop={onReorder} /> diff --git a/packages/desktop-client/src/components/sidebar/Sidebar.tsx b/packages/desktop-client/src/components/sidebar/Sidebar.tsx index dadd3ba95..b8d59bbde 100644 --- a/packages/desktop-client/src/components/sidebar/Sidebar.tsx +++ b/packages/desktop-client/src/components/sidebar/Sidebar.tsx @@ -1,68 +1,74 @@ -import React, { type ReactNode } from 'react'; - +import React, { useState } from 'react'; +import { useDispatch } from 'react-redux'; + +import { + closeBudget, + moveAccount, + replaceModal, +} from 'loot-core/src/client/actions'; import * as Platform from 'loot-core/src/client/platform'; -import { type AccountEntity } from 'loot-core/src/types/models'; +import { useAccounts } from '../../hooks/useAccounts'; +import { useGlobalPref } from '../../hooks/useGlobalPref'; +import { useLocalPref } from '../../hooks/useLocalPref'; +import { useNavigate } from '../../hooks/useNavigate'; +import { SvgExpandArrow } from '../../icons/v0'; import { SvgReports, SvgWallet } from '../../icons/v1'; import { SvgCalendar } from '../../icons/v2'; -import { type CSSProperties, theme } from '../../style'; +import { styles, theme } from '../../style'; +import { Button } from '../common/Button'; +import { InitialFocus } from '../common/InitialFocus'; +import { Input } from '../common/Input'; +import { Menu } from '../common/Menu'; +import { Text } from '../common/Text'; import { View } from '../common/View'; -import { type OnDropCallback } from '../sort'; -import { type Binding } from '../spreadsheet'; +import { Tooltip } from '../tooltips'; import { Accounts } from './Accounts'; import { Item } from './Item'; +import { useSidebar } from './SidebarProvider'; import { ToggleButton } from './ToggleButton'; import { Tools } from './Tools'; -import { useSidebar } from '.'; - export const SIDEBAR_WIDTH = 240; -type SidebarProps = { - style: CSSProperties; - budgetName: ReactNode; - accounts: AccountEntity[]; - failedAccounts: Map< - string, - { - type: string; - code: string; - } - >; - updatedAccounts: string[]; - getBalanceQuery: (account: AccountEntity) => Binding; - getAllAccountBalance: () => Binding; - getOnBudgetBalance: () => Binding; - getOffBudgetBalance: () => Binding; - showClosedAccounts: boolean; - isFloating: boolean; - onFloat: () => void; - onAddAccount: () => void; - onToggleClosedAccounts: () => void; - onReorder: OnDropCallback; -}; - -export function Sidebar({ - style, - budgetName, - accounts, - failedAccounts, - updatedAccounts, - getBalanceQuery, - getAllAccountBalance, - getOnBudgetBalance, - getOffBudgetBalance, - showClosedAccounts, - isFloating, - onFloat, - onAddAccount, - onToggleClosedAccounts, - onReorder, -}: SidebarProps) { +export function Sidebar() { const hasWindowButtons = !Platform.isBrowser && Platform.OS === 'mac'; + const dispatch = useDispatch(); const sidebar = useSidebar(); + const accounts = useAccounts(); + const [showClosedAccounts, setShowClosedAccountsPref] = useLocalPref( + 'ui.showClosedAccounts', + ); + const [isFloating = false, setFloatingSidebarPref] = + useGlobalPref('floatingSidebar'); + + async function onReorder( + id: string, + dropPos: 'top' | 'bottom', + targetId: unknown, + ) { + let targetIdToMove = targetId; + if (dropPos === 'bottom') { + const idx = accounts.findIndex(a => a.id === targetId) + 1; + targetIdToMove = idx < accounts.length ? accounts[idx].id : null; + } + + dispatch(moveAccount(id, targetIdToMove)); + } + + const onFloat = () => { + setFloatingSidebarPref(!isFloating); + }; + + const onAddAccount = () => { + dispatch(replaceModal('add-account')); + }; + + const onToggleClosedAccounts = () => { + setShowClosedAccountsPref(!showClosedAccounts); + }; return ( <View @@ -79,7 +85,8 @@ export function Sidebar({ opacity: 1, width: hasWindowButtons ? null : 'auto', }, - ...style, + flex: 1, + ...styles.darkScrollbar, }} > <View @@ -96,7 +103,7 @@ export function Sidebar({ }), }} > - {budgetName} + <EditableBudgetName /> <View style={{ flex: 1, flexDirection: 'row' }} /> @@ -123,18 +130,6 @@ export function Sidebar({ /> <Accounts - accounts={accounts} - failedAccounts={failedAccounts} - updatedAccounts={updatedAccounts} - getAccountPath={account => `/accounts/${account.id}`} - allAccountsPath="/accounts" - budgetedAccountPath="/accounts/budgeted" - offBudgetAccountPath="/accounts/offbudget" - getBalanceQuery={getBalanceQuery} - getAllAccountBalance={getAllAccountBalance} - getOnBudgetBalance={getOnBudgetBalance} - getOffBudgetBalance={getOffBudgetBalance} - showClosedAccounts={showClosedAccounts} onAddAccount={onAddAccount} onToggleClosedAccounts={onToggleClosedAccounts} onReorder={onReorder} @@ -143,3 +138,90 @@ export function Sidebar({ </View> ); } + +function EditableBudgetName() { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const [budgetName, setBudgetNamePref] = useLocalPref('budgetName'); + const [editing, setEditing] = useState(false); + const [menuOpen, setMenuOpen] = useState(false); + + function onMenuSelect(type: string) { + setMenuOpen(false); + + switch (type) { + case 'rename': + setEditing(true); + break; + case 'settings': + navigate('/settings'); + break; + case 'help': + window.open('https://actualbudget.org/docs/', '_blank'); + break; + case 'close': + dispatch(closeBudget()); + break; + default: + } + } + + const items = [ + { name: 'rename', text: 'Rename budget' }, + { name: 'settings', text: 'Settings' }, + ...(Platform.isBrowser ? [{ name: 'help', text: 'Help' }] : []), + { name: 'close', text: 'Close file' }, + ]; + + if (editing) { + return ( + <InitialFocus> + <Input + style={{ + width: 160, + fontSize: 16, + fontWeight: 500, + }} + defaultValue={budgetName} + onEnter={async e => { + const inputEl = e.target as HTMLInputElement; + const newBudgetName = inputEl.value; + if (newBudgetName.trim() !== '') { + setBudgetNamePref(inputEl.name); + setEditing(false); + } + }} + onBlur={() => setEditing(false)} + /> + </InitialFocus> + ); + } else { + return ( + <Button + type="bare" + color={theme.buttonNormalBorder} + style={{ + fontSize: 16, + fontWeight: 500, + marginLeft: -5, + flex: '0 auto', + }} + onClick={() => setMenuOpen(true)} + > + <Text style={{ whiteSpace: 'nowrap', overflow: 'hidden' }}> + {budgetName || 'A budget has no name'} + </Text> + <SvgExpandArrow width={7} height={7} style={{ marginLeft: 5 }} /> + {menuOpen && ( + <Tooltip + position="bottom-left" + style={{ padding: 0 }} + onClose={() => setMenuOpen(false)} + > + <Menu onMenuSelect={onMenuSelect} items={items} /> + </Tooltip> + )} + </Button> + ); + } +} diff --git a/packages/desktop-client/src/components/sidebar/SidebarProvider.tsx b/packages/desktop-client/src/components/sidebar/SidebarProvider.tsx new file mode 100644 index 000000000..717f3393e --- /dev/null +++ b/packages/desktop-client/src/components/sidebar/SidebarProvider.tsx @@ -0,0 +1,52 @@ +// @ts-strict-ignore +import React, { + createContext, + useState, + useContext, + useMemo, + type ReactNode, + type Dispatch, + type SetStateAction, +} from 'react'; + +import { useGlobalPref } from '../../hooks/useGlobalPref'; +import { useResponsive } from '../../ResponsiveProvider'; + +type SidebarContextValue = { + hidden: boolean; + setHidden: Dispatch<SetStateAction<boolean>>; + floating: boolean; + alwaysFloats: boolean; +}; + +const SidebarContext = createContext<SidebarContextValue>(null); + +type SidebarProviderProps = { + children: ReactNode; +}; + +export function SidebarProvider({ children }: SidebarProviderProps) { + const [floatingSidebar] = useGlobalPref('floatingSidebar'); + const [hidden, setHidden] = useState(true); + const { width } = useResponsive(); + const alwaysFloats = width < 668; + const floating = floatingSidebar || alwaysFloats; + + return ( + <SidebarContext.Provider + value={{ hidden, setHidden, floating, alwaysFloats }} + > + {children} + </SidebarContext.Provider> + ); +} + +export function useSidebar() { + const { hidden, setHidden, floating, alwaysFloats } = + useContext(SidebarContext); + + return useMemo( + () => ({ hidden, setHidden, floating, alwaysFloats }), + [hidden, setHidden, floating, alwaysFloats], + ); +} diff --git a/packages/desktop-client/src/components/sidebar/SidebarWithData.tsx b/packages/desktop-client/src/components/sidebar/SidebarWithData.tsx deleted file mode 100644 index 202e84bfe..000000000 --- a/packages/desktop-client/src/components/sidebar/SidebarWithData.tsx +++ /dev/null @@ -1,181 +0,0 @@ -// @ts-strict-ignore -import React, { useState, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; - -import { type State } from 'loot-core/client/state-types'; -import { type AccountState } from 'loot-core/client/state-types/account'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; -import { type QueriesState } from 'loot-core/client/state-types/queries'; -import { closeBudget } from 'loot-core/src/client/actions/budgets'; -import * as Platform from 'loot-core/src/client/platform'; -import * as queries from 'loot-core/src/client/queries'; -import { send } from 'loot-core/src/platform/client/fetch'; -import { type LocalPrefs } from 'loot-core/src/types/prefs'; - -import { useActions } from '../../hooks/useActions'; -import { useNavigate } from '../../hooks/useNavigate'; -import { SvgExpandArrow } from '../../icons/v0'; -import { styles, theme } from '../../style'; -import { Button } from '../common/Button'; -import { InitialFocus } from '../common/InitialFocus'; -import { Input } from '../common/Input'; -import { Menu } from '../common/Menu'; -import { Text } from '../common/Text'; -import { Tooltip } from '../tooltips'; - -import { Sidebar } from './Sidebar'; - -type EditableBudgetNameProps = { - prefs: LocalPrefs; - savePrefs: (prefs: Partial<LocalPrefs>) => Promise<void>; -}; - -function EditableBudgetName({ prefs, savePrefs }: EditableBudgetNameProps) { - const dispatch = useDispatch(); - const navigate = useNavigate(); - const [editing, setEditing] = useState(false); - const [menuOpen, setMenuOpen] = useState(false); - - function onMenuSelect(type) { - setMenuOpen(false); - - switch (type) { - case 'rename': - setEditing(true); - break; - case 'settings': - navigate('/settings'); - break; - case 'help': - window.open('https://actualbudget.org/docs/', '_blank'); - break; - case 'close': - dispatch(closeBudget()); - break; - default: - } - } - - const items = [ - { name: 'rename', text: 'Rename budget' }, - { name: 'settings', text: 'Settings' }, - ...(Platform.isBrowser ? [{ name: 'help', text: 'Help' }] : []), - { name: 'close', text: 'Close file' }, - ]; - - const onSaveChanges = async e => { - const inputEl = e.target; - const newBudgetName = inputEl.value; - if (newBudgetName.trim() !== '') { - await savePrefs({ - budgetName: inputEl.value, - }); - setEditing(false); - } - }; - - if (editing) { - return ( - <InitialFocus> - <Input - style={{ - width: 160, - fontSize: 16, - fontWeight: 500, - }} - defaultValue={prefs.budgetName} - onEnter={onSaveChanges} - onBlur={onSaveChanges} - onEscape={() => setEditing(false)} - /> - </InitialFocus> - ); - } else { - return ( - <Button - type="bare" - color={theme.buttonNormalBorder} - style={{ - fontSize: 16, - fontWeight: 500, - marginLeft: -5, - flex: '0 auto', - }} - onClick={() => setMenuOpen(true)} - > - <Text style={{ whiteSpace: 'nowrap', overflow: 'hidden' }}> - {prefs.budgetName || 'A budget has no name'} - </Text> - <SvgExpandArrow width={7} height={7} style={{ marginLeft: 5 }} /> - {menuOpen && ( - <Tooltip - position="bottom-left" - style={{ padding: 0 }} - onClose={() => setMenuOpen(false)} - > - <Menu onMenuSelect={onMenuSelect} items={items} /> - </Tooltip> - )} - </Button> - ); - } -} - -export function SidebarWithData() { - const accounts = useSelector<State, QueriesState['accounts']>( - state => state.queries.accounts, - ); - const failedAccounts = useSelector<State, AccountState['failedAccounts']>( - state => state.account.failedAccounts, - ); - const updatedAccounts = useSelector<State, QueriesState['updatedAccounts']>( - state => state.queries.updatedAccounts, - ); - const prefs = useSelector<State, LocalPrefs>(state => state.prefs.local); - const floatingSidebar = useSelector< - State, - PrefsState['global']['floatingSidebar'] - >(state => state.prefs.global.floatingSidebar); - - const { getAccounts, replaceModal, savePrefs, saveGlobalPrefs } = - useActions(); - - useEffect(() => void getAccounts(), [getAccounts]); - - async function onReorder(id, dropPos, targetId) { - if (dropPos === 'bottom') { - const idx = accounts.findIndex(a => a.id === targetId) + 1; - targetId = idx < accounts.length ? accounts[idx].id : null; - } - - await send('account-move', { id, targetId }); - await getAccounts(); - } - - return ( - <Sidebar - budgetName={<EditableBudgetName prefs={prefs} savePrefs={savePrefs} />} - isFloating={floatingSidebar} - accounts={accounts} - failedAccounts={failedAccounts} - updatedAccounts={updatedAccounts} - getBalanceQuery={queries.accountBalance} - getAllAccountBalance={queries.allAccountBalance} - getOnBudgetBalance={queries.budgetedAccountBalance} - getOffBudgetBalance={queries.offbudgetAccountBalance} - onFloat={() => saveGlobalPrefs({ floatingSidebar: !floatingSidebar })} - onReorder={onReorder} - onAddAccount={() => replaceModal('add-account')} - showClosedAccounts={prefs['ui.showClosedAccounts']} - onToggleClosedAccounts={() => - savePrefs({ - 'ui.showClosedAccounts': !prefs['ui.showClosedAccounts'], - }) - } - style={{ - flex: 1, - ...styles.darkScrollbar, - }} - /> - ); -} diff --git a/packages/desktop-client/src/components/sidebar/index.tsx b/packages/desktop-client/src/components/sidebar/index.tsx index c3f02e448..59f7b1628 100644 --- a/packages/desktop-client/src/components/sidebar/index.tsx +++ b/packages/desktop-client/src/components/sidebar/index.tsx @@ -1,71 +1,14 @@ -// @ts-strict-ignore -import React, { - createContext, - useState, - useContext, - useMemo, - type ReactNode, - type Dispatch, - type SetStateAction, -} from 'react'; -import { useSelector } from 'react-redux'; - -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; +import React from 'react'; +import { useGlobalPref } from '../../hooks/useGlobalPref'; import { useResponsive } from '../../ResponsiveProvider'; import { View } from '../common/View'; -import { SIDEBAR_WIDTH } from './Sidebar'; -import { SidebarWithData } from './SidebarWithData'; - -type SidebarContextValue = { - hidden: boolean; - setHidden: Dispatch<SetStateAction<boolean>>; - floating: boolean; - alwaysFloats: boolean; -}; - -const SidebarContext = createContext<SidebarContextValue>(null); - -type SidebarProviderProps = { - children: ReactNode; -}; - -export function SidebarProvider({ children }: SidebarProviderProps) { - const floatingSidebar = useSelector< - State, - PrefsState['global']['floatingSidebar'] - >(state => state.prefs.global.floatingSidebar); - const [hidden, setHidden] = useState(true); - const { width } = useResponsive(); - const alwaysFloats = width < 668; - const floating = floatingSidebar || alwaysFloats; - - return ( - <SidebarContext.Provider - value={{ hidden, setHidden, floating, alwaysFloats }} - > - {children} - </SidebarContext.Provider> - ); -} - -export function useSidebar() { - const { hidden, setHidden, floating, alwaysFloats } = - useContext(SidebarContext); - - return useMemo( - () => ({ hidden, setHidden, floating, alwaysFloats }), - [hidden, setHidden, floating, alwaysFloats], - ); -} +import { SIDEBAR_WIDTH, Sidebar } from './Sidebar'; +import { useSidebar } from './SidebarProvider'; export function FloatableSidebar() { - const floatingSidebar = useSelector< - State, - PrefsState['global']['floatingSidebar'] - >(state => state.prefs.global.floatingSidebar); + const [floatingSidebar] = useGlobalPref('floatingSidebar'); const sidebar = useSidebar(); const { isNarrowWidth } = useResponsive(); @@ -80,11 +23,13 @@ export function FloatableSidebar() { e.stopPropagation(); sidebar.setHidden(false); } - : null + : undefined + } + onMouseLeave={ + sidebarShouldFloat ? () => sidebar.setHidden(true) : undefined } - onMouseLeave={sidebarShouldFloat ? () => sidebar.setHidden(true) : null} style={{ - position: sidebarShouldFloat ? 'absolute' : null, + position: sidebarShouldFloat ? 'absolute' : undefined, top: 12, // If not floating, the -50 takes into account the transform below bottom: sidebarShouldFloat ? 12 : -50, @@ -105,7 +50,7 @@ export function FloatableSidebar() { 'transform .5s, box-shadow .5s, border-radius .5s, bottom .5s', }} > - <SidebarWithData /> + <Sidebar /> </View> ); } diff --git a/packages/desktop-client/src/components/transactions/MobileTransaction.jsx b/packages/desktop-client/src/components/transactions/MobileTransaction.jsx index bda688058..bbce43aee 100644 --- a/packages/desktop-client/src/components/transactions/MobileTransaction.jsx +++ b/packages/desktop-client/src/components/transactions/MobileTransaction.jsx @@ -46,9 +46,12 @@ import { groupById, } from 'loot-core/src/shared/util'; +import { useAccounts } from '../../hooks/useAccounts'; import { useActions } from '../../hooks/useActions'; import { useCategories } from '../../hooks/useCategories'; +import { useDateFormat } from '../../hooks/useDateFormat'; import { useNavigate } from '../../hooks/useNavigate'; +import { usePayees } from '../../hooks/usePayees'; import { useSetThemeColor } from '../../hooks/useSetThemeColor'; import { SingleActiveEditFormProvider, @@ -939,11 +942,6 @@ function TransactionEditUnconnected(props) { useSetThemeColor(theme.mobileViewTheme); useEffect(() => { - // May as well update categories / accounts when transaction ID changes - props.getCategories(); - props.getAccounts(); - props.getPayees(); - async function fetchTransaction() { // Query for the transaction based on the ID with grouped splits. // @@ -1110,12 +1108,10 @@ function TransactionEditUnconnected(props) { export const TransactionEdit = props => { const { list: categories } = useCategories(); - const payees = useSelector(state => state.queries.payees); + const payees = usePayees(); const lastTransaction = useSelector(state => state.queries.lastTransaction); - const accounts = useSelector(state => state.queries.accounts); - const dateFormat = useSelector( - state => state.prefs.local.dateFormat || 'MM/dd/yyyy', - ); + const accounts = useAccounts(); + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; const actions = useActions(); return ( diff --git a/packages/desktop-client/src/components/transactions/SimpleTransactionsTable.jsx b/packages/desktop-client/src/components/transactions/SimpleTransactionsTable.jsx index ea43f6adb..736df89d2 100644 --- a/packages/desktop-client/src/components/transactions/SimpleTransactionsTable.jsx +++ b/packages/desktop-client/src/components/transactions/SimpleTransactionsTable.jsx @@ -1,5 +1,4 @@ -import React, { memo, useCallback, useMemo } from 'react'; -import { useSelector } from 'react-redux'; +import React, { memo, useMemo, useCallback } from 'react'; import { format as formatDate, @@ -13,8 +12,11 @@ import { } from 'loot-core/src/client/reducers/queries'; import { integerToCurrency } from 'loot-core/src/shared/util'; +import { useAccounts } from '../../hooks/useAccounts'; import { useCategories } from '../../hooks/useCategories'; -import { useSelectedDispatch, useSelectedItems } from '../../hooks/useSelected'; +import { useDateFormat } from '../../hooks/useDateFormat'; +import { usePayees } from '../../hooks/usePayees'; +import { useSelectedItems, useSelectedDispatch } from '../../hooks/useSelected'; import { SvgArrowsSynchronize } from '../../icons/v2'; import { styles, theme } from '../../style'; import { Cell, Field, Row, SelectCell, Table } from '../table'; @@ -141,13 +143,9 @@ export function SimpleTransactionsTable({ style, }) { const { grouped: categories } = useCategories(); - const { payees, accounts, dateFormat } = useSelector(state => { - return { - payees: state.queries.payees, - accounts: state.queries.accounts, - dateFormat: state.prefs.local.dateFormat || 'MM/dd/yyyy', - }; - }); + const payees = usePayees(); + const accounts = useAccounts(); + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; const selectedItems = useSelectedItems(); const dispatchSelected = useSelectedDispatch(); const memoFields = useMemo(() => fields, [JSON.stringify(fields)]); diff --git a/packages/desktop-client/src/components/util/DisplayId.tsx b/packages/desktop-client/src/components/util/DisplayId.tsx index db3982cbe..34c5f593a 100644 --- a/packages/desktop-client/src/components/util/DisplayId.tsx +++ b/packages/desktop-client/src/components/util/DisplayId.tsx @@ -1,9 +1,8 @@ // @ts-strict-ignore import React from 'react'; -import { CachedAccounts } from 'loot-core/src/client/data-hooks/accounts'; -import { CachedPayees } from 'loot-core/src/client/data-hooks/payees'; - +import { useAccount } from '../../hooks/useAccount'; +import { usePayee } from '../../hooks/usePayee'; import { theme } from '../../style'; import { Text } from '../common/Text'; @@ -18,33 +17,33 @@ export function DisplayId({ id, noneColor = theme.pageTextSubdued, }: DisplayIdProps) { - let DataComponent; - - switch (type) { - case 'payees': - DataComponent = CachedPayees; - break; - case 'accounts': - DataComponent = CachedAccounts; - break; - default: - throw new Error('DisplayId: unknown object type: ' + type); - } + return type === 'accounts' ? ( + <AccountDisplayId id={id} noneColor={noneColor} /> + ) : ( + <PayeeDisplayId id={id} noneColor={noneColor} /> + ); +} +function AccountDisplayId({ id, noneColor }) { + const account = useAccount(id); return ( - <DataComponent idKey={true}> - {data => { - const item = data[id]; + <Text + style={account == null ? { color: noneColor } : null} + title={account ? account.name : 'None'} + > + {account ? account.name : 'None'} + </Text> + ); +} - return ( - <Text - style={item == null ? { color: noneColor } : null} - title={item ? item.name : 'None'} - > - {item ? item.name : 'None'} - </Text> - ); - }} - </DataComponent> +function PayeeDisplayId({ id, noneColor }) { + const payee = usePayee(id); + return ( + <Text + style={payee == null ? { color: noneColor } : null} + title={payee ? payee.name : 'None'} + > + {payee ? payee.name : 'None'} + </Text> ); } diff --git a/packages/desktop-client/src/components/util/GenericInput.jsx b/packages/desktop-client/src/components/util/GenericInput.jsx index b42826e01..bf2e3c53d 100644 --- a/packages/desktop-client/src/components/util/GenericInput.jsx +++ b/packages/desktop-client/src/components/util/GenericInput.jsx @@ -4,6 +4,7 @@ import { useSelector } from 'react-redux'; import { getMonthYearFormat } from 'loot-core/src/shared/months'; import { useCategories } from '../../hooks/useCategories'; +import { useDateFormat } from '../../hooks/useDateFormat'; import { AccountAutocomplete } from '../autocomplete/AccountAutocomplete'; import { Autocomplete } from '../autocomplete/Autocomplete'; import { CategoryAutocomplete } from '../autocomplete/CategoryAutocomplete'; @@ -27,9 +28,7 @@ export function GenericInput({ }) { const { grouped: categoryGroups } = useCategories(); const saved = useSelector(state => state.queries.saved); - const dateFormat = useSelector( - state => state.prefs.local.dateFormat || 'MM/dd/yyyy', - ); + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; // This makes the UI more resilient in case of faulty data if (multi && !Array.isArray(value)) { diff --git a/packages/desktop-client/src/hooks/useAccount.ts b/packages/desktop-client/src/hooks/useAccount.ts new file mode 100644 index 000000000..e3da8f35e --- /dev/null +++ b/packages/desktop-client/src/hooks/useAccount.ts @@ -0,0 +1,8 @@ +import { useMemo } from 'react'; + +import { useAccounts } from './useAccounts'; + +export function useAccount(id: string) { + const accounts = useAccounts(); + return useMemo(() => accounts.find(a => a.id === id), [id, accounts]); +} diff --git a/packages/desktop-client/src/hooks/useAccounts.ts b/packages/desktop-client/src/hooks/useAccounts.ts new file mode 100644 index 000000000..4c44e9cec --- /dev/null +++ b/packages/desktop-client/src/hooks/useAccounts.ts @@ -0,0 +1,20 @@ +import { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { getAccounts } from 'loot-core/src/client/actions'; +import { type State } from 'loot-core/src/client/state-types'; + +export function useAccounts() { + const dispatch = useDispatch(); + const accountsLoaded = useSelector( + (state: State) => state.queries.accountsLoaded, + ); + + useEffect(() => { + if (!accountsLoaded) { + dispatch(getAccounts()); + } + }, []); + + return useSelector(state => state.queries.accounts); +} diff --git a/packages/desktop-client/src/hooks/useBudgetedAccounts.ts b/packages/desktop-client/src/hooks/useBudgetedAccounts.ts new file mode 100644 index 000000000..dbd8e1f53 --- /dev/null +++ b/packages/desktop-client/src/hooks/useBudgetedAccounts.ts @@ -0,0 +1,14 @@ +import { useMemo } from 'react'; + +import { useAccounts } from './useAccounts'; + +export function useBudgetedAccounts() { + const accounts = useAccounts(); + return useMemo( + () => + accounts.filter( + account => account.closed === 0 && account.offbudget === 0, + ), + [accounts], + ); +} diff --git a/packages/desktop-client/src/hooks/useCategories.ts b/packages/desktop-client/src/hooks/useCategories.ts index 8e2730915..4c85fdfaf 100644 --- a/packages/desktop-client/src/hooks/useCategories.ts +++ b/packages/desktop-client/src/hooks/useCategories.ts @@ -1,25 +1,20 @@ import { useEffect } from 'react'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type QueriesState } from 'loot-core/client/state-types/queries'; - -import { useActions } from './useActions'; +import { getCategories } from 'loot-core/src/client/actions'; +import { type State } from 'loot-core/src/client/state-types'; export function useCategories() { - const { getCategories } = useActions(); - - const categories = useSelector<State, QueriesState['categories']['list']>( - state => state.queries.categories.list, + const dispatch = useDispatch(); + const categoriesLoaded = useSelector( + (state: State) => state.queries.categoriesLoaded, ); useEffect(() => { - if (categories.length === 0) { - getCategories(); + if (!categoriesLoaded) { + dispatch(getCategories()); } }, []); - return useSelector<State, QueriesState['categories']>( - state => state.queries.categories, - ); + return useSelector(state => state.queries.categories); } diff --git a/packages/desktop-client/src/hooks/useClosedAccounts.ts b/packages/desktop-client/src/hooks/useClosedAccounts.ts new file mode 100644 index 000000000..85aa5b920 --- /dev/null +++ b/packages/desktop-client/src/hooks/useClosedAccounts.ts @@ -0,0 +1,11 @@ +import { useMemo } from 'react'; + +import { useAccounts } from './useAccounts'; + +export function useClosedAccounts() { + const accounts = useAccounts(); + return useMemo( + () => accounts.filter(account => account.closed === 1), + [accounts], + ); +} diff --git a/packages/desktop-client/src/hooks/useDateFormat.ts b/packages/desktop-client/src/hooks/useDateFormat.ts new file mode 100644 index 000000000..258f156e1 --- /dev/null +++ b/packages/desktop-client/src/hooks/useDateFormat.ts @@ -0,0 +1,7 @@ +import { useSelector } from 'react-redux'; + +import { type State } from 'loot-core/src/client/state-types'; + +export function useDateFormat() { + return useSelector((state: State) => state.prefs.local?.dateFormat); +} diff --git a/packages/desktop-client/src/hooks/useFailedAccounts.ts b/packages/desktop-client/src/hooks/useFailedAccounts.ts new file mode 100644 index 000000000..86aeb8995 --- /dev/null +++ b/packages/desktop-client/src/hooks/useFailedAccounts.ts @@ -0,0 +1,7 @@ +import { useSelector } from 'react-redux'; + +import { type State } from 'loot-core/src/client/state-types'; + +export function useFailedAccounts() { + return useSelector((state: State) => state.account.failedAccounts); +} diff --git a/packages/desktop-client/src/hooks/useFeatureFlag.ts b/packages/desktop-client/src/hooks/useFeatureFlag.ts index 40b79b84d..5550cfed2 100644 --- a/packages/desktop-client/src/hooks/useFeatureFlag.ts +++ b/packages/desktop-client/src/hooks/useFeatureFlag.ts @@ -1,8 +1,7 @@ // @ts-strict-ignore import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; +import { type State } from 'loot-core/src/client/state-types'; import type { FeatureFlag } from 'loot-core/src/types/prefs'; const DEFAULT_FEATURE_FLAG_STATE: Record<FeatureFlag, boolean> = { @@ -15,13 +14,11 @@ const DEFAULT_FEATURE_FLAG_STATE: Record<FeatureFlag, boolean> = { }; export function useFeatureFlag(name: FeatureFlag): boolean { - return useSelector<State, PrefsState['local'][`flags.${FeatureFlag}`]>( - state => { - const value = state.prefs.local[`flags.${name}`]; + return useSelector((state: State) => { + const value = state.prefs.local[`flags.${name}`]; - return value === undefined - ? DEFAULT_FEATURE_FLAG_STATE[name] || false - : value; - }, - ); + return value === undefined + ? DEFAULT_FEATURE_FLAG_STATE[name] || false + : value; + }); } diff --git a/packages/desktop-client/src/hooks/useGlobalPref.ts b/packages/desktop-client/src/hooks/useGlobalPref.ts new file mode 100644 index 000000000..02d5773ac --- /dev/null +++ b/packages/desktop-client/src/hooks/useGlobalPref.ts @@ -0,0 +1,27 @@ +import { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { saveGlobalPrefs } from 'loot-core/src/client/actions'; +import { type State } from 'loot-core/src/client/state-types'; +import { type GlobalPrefs } from 'loot-core/src/types/prefs'; + +type SetGlobalPrefAction<K extends keyof GlobalPrefs> = ( + value: GlobalPrefs[K], +) => void; + +export function useGlobalPref<K extends keyof GlobalPrefs>( + prefName: K, +): [GlobalPrefs[K], SetGlobalPrefAction<K>] { + const dispatch = useDispatch(); + const setGlobalPref = useCallback<SetGlobalPrefAction<K>>( + value => { + dispatch(saveGlobalPrefs({ [prefName]: value } as GlobalPrefs)); + }, + [prefName, dispatch], + ); + const globalPref = useSelector( + (state: State) => state.prefs.global?.[prefName] as GlobalPrefs[K], + ); + + return [globalPref, setGlobalPref]; +} diff --git a/packages/desktop-client/src/hooks/useLocalPref.ts b/packages/desktop-client/src/hooks/useLocalPref.ts new file mode 100644 index 000000000..70a50cb55 --- /dev/null +++ b/packages/desktop-client/src/hooks/useLocalPref.ts @@ -0,0 +1,27 @@ +import { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { savePrefs } from 'loot-core/src/client/actions'; +import { type State } from 'loot-core/src/client/state-types'; +import { type LocalPrefs } from 'loot-core/src/types/prefs'; + +type SetLocalPrefAction<K extends keyof LocalPrefs> = ( + value: LocalPrefs[K], +) => void; + +export function useLocalPref<K extends keyof LocalPrefs>( + prefName: K, +): [LocalPrefs[K], SetLocalPrefAction<K>] { + const dispatch = useDispatch(); + const setLocalPref = useCallback<SetLocalPrefAction<K>>( + value => { + dispatch(savePrefs({ [prefName]: value } as LocalPrefs)); + }, + [prefName, dispatch], + ); + const localPref = useSelector( + (state: State) => state.prefs.local?.[prefName] as LocalPrefs[K], + ); + + return [localPref, setLocalPref]; +} diff --git a/packages/desktop-client/src/hooks/useLocalPrefs.ts b/packages/desktop-client/src/hooks/useLocalPrefs.ts new file mode 100644 index 000000000..870bef808 --- /dev/null +++ b/packages/desktop-client/src/hooks/useLocalPrefs.ts @@ -0,0 +1,7 @@ +import { useSelector } from 'react-redux'; + +import { type State } from 'loot-core/src/client/state-types'; + +export function useLocalPrefs() { + return useSelector((state: State) => state.prefs.local); +} diff --git a/packages/desktop-client/src/hooks/useOffBudgetAccounts.ts b/packages/desktop-client/src/hooks/useOffBudgetAccounts.ts new file mode 100644 index 000000000..71a5db919 --- /dev/null +++ b/packages/desktop-client/src/hooks/useOffBudgetAccounts.ts @@ -0,0 +1,14 @@ +import { useMemo } from 'react'; + +import { useAccounts } from './useAccounts'; + +export function useOffBudgetAccounts() { + const accounts = useAccounts(); + return useMemo( + () => + accounts.filter( + account => account.closed === 0 && account.offbudget === 1, + ), + [accounts], + ); +} diff --git a/packages/desktop-client/src/hooks/usePayee.ts b/packages/desktop-client/src/hooks/usePayee.ts new file mode 100644 index 000000000..2606c60a8 --- /dev/null +++ b/packages/desktop-client/src/hooks/usePayee.ts @@ -0,0 +1,8 @@ +import { useMemo } from 'react'; + +import { usePayees } from './usePayees'; + +export function usePayee(id: string) { + const payees = usePayees(); + return useMemo(() => payees.find(p => p.id === id), [id, payees]); +} diff --git a/packages/desktop-client/src/hooks/usePayees.ts b/packages/desktop-client/src/hooks/usePayees.ts new file mode 100644 index 000000000..cf51d6b9a --- /dev/null +++ b/packages/desktop-client/src/hooks/usePayees.ts @@ -0,0 +1,20 @@ +import { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { getPayees } from 'loot-core/src/client/actions'; +import { type State } from 'loot-core/src/client/state-types'; + +export function usePayees() { + const dispatch = useDispatch(); + const payeesLoaded = useSelector( + (state: State) => state.queries.payeesLoaded, + ); + + useEffect(() => { + if (!payeesLoaded) { + dispatch(getPayees()); + } + }, []); + + return useSelector(state => state.queries.payees); +} diff --git a/packages/desktop-client/src/hooks/usePrivacyMode.ts b/packages/desktop-client/src/hooks/usePrivacyMode.ts new file mode 100644 index 000000000..ffa633bc0 --- /dev/null +++ b/packages/desktop-client/src/hooks/usePrivacyMode.ts @@ -0,0 +1,9 @@ +import { useSelector } from 'react-redux'; + +import { type State } from 'loot-core/src/client/state-types'; + +export function usePrivacyMode() { + return useSelector( + (state: State) => state.prefs.local?.isPrivacyEnabled ?? false, + ); +} diff --git a/packages/desktop-client/src/hooks/useSelected.tsx b/packages/desktop-client/src/hooks/useSelected.tsx index 398350e0d..63a0437a8 100644 --- a/packages/desktop-client/src/hooks/useSelected.tsx +++ b/packages/desktop-client/src/hooks/useSelected.tsx @@ -12,8 +12,7 @@ import React, { } from 'react'; import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type AppState } from 'loot-core/client/state-types/app'; +import { type State } from 'loot-core/src/client/state-types'; 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'; @@ -210,9 +209,7 @@ export function useSelected<T extends Item>( return () => undo.setUndoState('selectedItems', prevState); }, [state.selectedItems]); - const lastUndoState = useSelector<State, AppState['lastUndoState']>( - state => state.app.lastUndoState, - ); + const lastUndoState = useSelector((state: State) => state.app.lastUndoState); useEffect(() => { function onUndo({ messages, undoTag }: UndoState) { diff --git a/packages/desktop-client/src/hooks/useSyncServerStatus.ts b/packages/desktop-client/src/hooks/useSyncServerStatus.ts index 2b48a0c6f..d788340be 100644 --- a/packages/desktop-client/src/hooks/useSyncServerStatus.ts +++ b/packages/desktop-client/src/hooks/useSyncServerStatus.ts @@ -1,7 +1,6 @@ import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type UserState } from 'loot-core/client/state-types/user'; +import { type State } from 'loot-core/src/client/state-types'; import { useServerURL } from '../components/ServerContext'; @@ -9,9 +8,7 @@ export type SyncServerStatus = 'offline' | 'no-server' | 'online'; export function useSyncServerStatus(): SyncServerStatus { const serverUrl = useServerURL(); - const userData = useSelector<State, UserState['data']>( - state => state.user.data, - ); + const userData = useSelector((state: State) => state.user.data); if (!serverUrl) { return 'no-server'; diff --git a/packages/desktop-client/src/hooks/useUpdatedAccounts.ts b/packages/desktop-client/src/hooks/useUpdatedAccounts.ts new file mode 100644 index 000000000..483d22c9b --- /dev/null +++ b/packages/desktop-client/src/hooks/useUpdatedAccounts.ts @@ -0,0 +1,7 @@ +import { useSelector } from 'react-redux'; + +import { type State } from 'loot-core/src/client/state-types'; + +export function useUpdatedAccounts() { + return useSelector((state: State) => state.queries.updatedAccounts); +} diff --git a/packages/desktop-client/src/style/theme.tsx b/packages/desktop-client/src/style/theme.tsx index 7cd3b5751..fe6b2374d 100644 --- a/packages/desktop-client/src/style/theme.tsx +++ b/packages/desktop-client/src/style/theme.tsx @@ -1,12 +1,11 @@ // @ts-strict-ignore import { useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; import { isNonProductionEnvironment } from 'loot-core/src/shared/environment'; import type { Theme } from 'loot-core/src/types/prefs'; +import { useGlobalPref } from '../hooks/useGlobalPref'; + import * as darkTheme from './themes/dark'; import * as developmentTheme from './themes/development'; import * as lightTheme from './themes/light'; @@ -24,16 +23,13 @@ export const themeOptions = Object.entries(themes).map( ([key, { name }]) => [key, name] as [Theme, string], ); -export function useTheme(): Theme { - return ( - useSelector<State, PrefsState['global']['theme']>( - state => state.prefs.global?.theme, - ) || 'light' - ); +export function useTheme() { + const [theme = 'light', setThemePref] = useGlobalPref('theme'); + return [theme, setThemePref] as const; } export function ThemeStyle() { - const theme = useTheme(); + const [theme] = useTheme(); const [themeColors, setThemeColors] = useState< typeof lightTheme | typeof darkTheme | undefined >(undefined); diff --git a/packages/loot-core/src/client/actions/account.ts b/packages/loot-core/src/client/actions/account.ts index c8349d84c..e7f7f269b 100644 --- a/packages/loot-core/src/client/actions/account.ts +++ b/packages/loot-core/src/client/actions/account.ts @@ -263,3 +263,10 @@ export function markAccountRead(accountId): MarkAccountReadAction { accountId, }; } + +export function moveAccount(id, targetId) { + return async (dispatch: Dispatch) => { + await send('account-move', { id, targetId }); + dispatch(getAccounts()); + }; +} diff --git a/packages/loot-core/src/client/actions/prefs.ts b/packages/loot-core/src/client/actions/prefs.ts index b93a38eb3..5dbca103f 100644 --- a/packages/loot-core/src/client/actions/prefs.ts +++ b/packages/loot-core/src/client/actions/prefs.ts @@ -26,7 +26,7 @@ export function loadPrefs() { }; } -export function savePrefs(prefs: Partial<prefs.LocalPrefs>) { +export function savePrefs(prefs: prefs.LocalPrefs) { return async (dispatch: Dispatch) => { await send('save-prefs', prefs); dispatch({ @@ -48,7 +48,7 @@ export function loadGlobalPrefs() { }; } -export function saveGlobalPrefs(prefs: Partial<prefs.GlobalPrefs>) { +export function saveGlobalPrefs(prefs: prefs.GlobalPrefs) { return async (dispatch: Dispatch) => { await send('save-global-prefs', prefs); dispatch({ diff --git a/packages/loot-core/src/client/data-hooks/accounts.tsx b/packages/loot-core/src/client/data-hooks/accounts.tsx deleted file mode 100644 index c2460477f..000000000 --- a/packages/loot-core/src/client/data-hooks/accounts.tsx +++ /dev/null @@ -1,36 +0,0 @@ -// @ts-strict-ignore -import React, { createContext, useContext } from 'react'; - -import { q } from '../../shared/query'; -import { type AccountEntity } from '../../types/models'; -import { useLiveQuery } from '../query-hooks'; -import { getAccountsById } from '../reducers/queries'; - -function useAccounts(): AccountEntity[] { - return useLiveQuery(() => q('accounts').select('*'), []); -} - -const AccountsContext = createContext<AccountEntity[]>(null); - -export function AccountsProvider({ children }) { - const data = useAccounts(); - return ( - <AccountsContext.Provider value={data}>{children}</AccountsContext.Provider> - ); -} - -export function CachedAccounts({ children, idKey }) { - const data = useCachedAccounts({ idKey }); - return children(data); -} - -export function useCachedAccounts(): AccountEntity[]; -export function useCachedAccounts({ - idKey, -}: { - idKey: boolean; -}): Record<AccountEntity['id'], AccountEntity>; -export function useCachedAccounts({ idKey }: { idKey?: boolean } = {}) { - const data = useContext(AccountsContext); - return idKey && data ? getAccountsById(data) : data; -} diff --git a/packages/loot-core/src/client/data-hooks/payees.tsx b/packages/loot-core/src/client/data-hooks/payees.tsx deleted file mode 100644 index 4299aa9a3..000000000 --- a/packages/loot-core/src/client/data-hooks/payees.tsx +++ /dev/null @@ -1,36 +0,0 @@ -// @ts-strict-ignore -import React, { createContext, useContext } from 'react'; - -import { q } from '../../shared/query'; -import { type PayeeEntity } from '../../types/models'; -import { useLiveQuery } from '../query-hooks'; -import { getPayeesById } from '../reducers/queries'; - -function usePayees(): PayeeEntity[] { - return useLiveQuery(() => q('payees').select('*'), []); -} - -const PayeesContext = createContext<PayeeEntity[]>(null); - -export function PayeesProvider({ children }) { - const data = usePayees(); - return ( - <PayeesContext.Provider value={data}>{children}</PayeesContext.Provider> - ); -} - -export function CachedPayees({ children, idKey }) { - const data = useCachedPayees({ idKey }); - return children(data); -} - -export function useCachedPayees(): PayeeEntity[]; -export function useCachedPayees({ - idKey, -}: { - idKey: boolean; -}): Record<PayeeEntity['id'], PayeeEntity>; -export function useCachedPayees({ idKey }: { idKey?: boolean } = {}) { - const data = useContext(PayeesContext); - return idKey && data ? getPayeesById(data) : data; -} diff --git a/packages/loot-core/src/client/privacy.ts b/packages/loot-core/src/client/privacy.ts deleted file mode 100644 index 295bba5e5..000000000 --- a/packages/loot-core/src/client/privacy.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { useSelector } from 'react-redux'; - -export function usePrivacyMode() { - return useSelector(state => state.prefs?.local?.isPrivacyEnabled ?? false); -} diff --git a/packages/loot-core/src/client/reducers/queries.ts b/packages/loot-core/src/client/reducers/queries.ts index 470c35916..b0c38817a 100644 --- a/packages/loot-core/src/client/reducers/queries.ts +++ b/packages/loot-core/src/client/reducers/queries.ts @@ -13,11 +13,14 @@ const initialState: QueriesState = { lastTransaction: null, updatedAccounts: [], accounts: [], + accountsLoaded: false, categories: { grouped: [], list: [], }, + categoriesLoaded: false, payees: [], + payeesLoaded: false, earliestTransaction: null, }; @@ -56,6 +59,7 @@ export function update(state = initialState, action: Action): QueriesState { return { ...state, accounts: action.accounts, + accountsLoaded: true, }; case constants.UPDATE_ACCOUNT: { return { @@ -72,11 +76,13 @@ export function update(state = initialState, action: Action): QueriesState { return { ...state, categories: action.categories, + categoriesLoaded: true, }; case constants.LOAD_PAYEES: return { ...state, payees: action.payees, + payeesLoaded: true, }; default: } diff --git a/packages/loot-core/src/client/state-types/modals.d.ts b/packages/loot-core/src/client/state-types/modals.d.ts index e25e38feb..72cd5dbc5 100644 --- a/packages/loot-core/src/client/state-types/modals.d.ts +++ b/packages/loot-core/src/client/state-types/modals.d.ts @@ -42,7 +42,7 @@ type FinanceModals = { syncSource?: AccountSyncSource; }; - 'confirm-category-delete': { onDelete: () => void } & ( + 'confirm-category-delete': { onDelete: (categoryId: string) => void } & ( | { category: string } | { group: string } ); diff --git a/packages/loot-core/src/client/state-types/queries.d.ts b/packages/loot-core/src/client/state-types/queries.d.ts index a7f18526f..0d511868b 100644 --- a/packages/loot-core/src/client/state-types/queries.d.ts +++ b/packages/loot-core/src/client/state-types/queries.d.ts @@ -8,8 +8,11 @@ export type QueriesState = { lastTransaction: unknown | null; updatedAccounts: string[]; accounts: AccountEntity[]; + accountsLoaded: boolean; categories: Awaited<ReturnType<Handlers['get-categories']>>; + categoriesLoaded: boolean; payees: Awaited<ReturnType<Handlers['payees-get']>>; + payeesLoaded: boolean; earliestTransaction: unknown | null; }; diff --git a/packages/loot-core/src/shared/categories.ts b/packages/loot-core/src/shared/categories.ts deleted file mode 100644 index 9e2c4c190..000000000 --- a/packages/loot-core/src/shared/categories.ts +++ /dev/null @@ -1,106 +0,0 @@ -// @ts-strict-ignore -export function addCategory(categoryGroups, cat) { - return categoryGroups.map(group => { - if (group.id === cat.cat_group) { - group.categories = [cat, ...group.categories]; - } - return { ...group }; - }); -} - -export function updateCategory(categoryGroups, category) { - return categoryGroups.map(group => { - if (group.id === category.cat_group) { - group.categories = group.categories.map(c => { - if (c.id === category.id) { - return { ...c, ...category }; - } - return c; - }); - } - return group; - }); -} - -export function moveCategory(categoryGroups, id, groupId, targetId) { - if (id === targetId) { - return categoryGroups; - } - - let moveCat = categoryGroups.reduce((value, group) => { - return value || group.categories.find(cat => cat.id === id); - }, null); - - // Update the group id on the category - moveCat = { ...moveCat, cat_group: groupId }; - - return categoryGroups.map(group => { - if (group.id === groupId) { - group.categories = group.categories.reduce((cats, cat) => { - if (cat.id === targetId) { - cats.push(moveCat); - cats.push(cat); - } else if (cat.id !== id) { - cats.push(cat); - } - return cats; - }, []); - - if (!targetId) { - group.categories.push(moveCat); - } - } else { - group.categories = group.categories.filter(cat => cat.id !== id); - } - - return { ...group }; - }); -} - -export function moveCategoryGroup(categoryGroups, id, targetId) { - if (id === targetId) { - return categoryGroups; - } - - const moveGroup = categoryGroups.find(g => g.id === id); - - categoryGroups = categoryGroups.reduce((groups, group) => { - if (group.id === targetId) { - groups.push(moveGroup); - groups.push(group); - } else if (group.id !== id) { - groups.push(group); - } - return groups; - }, []); - - if (!targetId) { - categoryGroups.push(moveGroup); - } - - return categoryGroups; -} - -export function deleteCategory(categoryGroups, id) { - return categoryGroups.map(group => { - group.categories = group.categories.filter(c => c.id !== id); - return group; - }); -} - -export function addGroup(categoryGroups, group) { - return [...categoryGroups, group]; -} - -export function updateGroup(categoryGroups, group) { - return categoryGroups.map(g => { - if (g.id === group.id) { - return { ...g, ...group }; - } - return g; - }); -} - -export function deleteGroup(categoryGroups, id) { - return categoryGroups.filter(g => g.id !== id); -} diff --git a/packages/loot-core/src/types/prefs.d.ts b/packages/loot-core/src/types/prefs.d.ts index df63517dd..251b6e5dd 100644 --- a/packages/loot-core/src/types/prefs.d.ts +++ b/packages/loot-core/src/types/prefs.d.ts @@ -53,6 +53,7 @@ export type LocalPrefs = Partial< reportsViewLegend: boolean; reportsViewSummary: boolean; reportsViewLabel: boolean; + 'mobile.showSpentColumn': boolean; } & Record<`flags.${FeatureFlag}`, boolean> >; diff --git a/upcoming-release-notes/2293.md b/upcoming-release-notes/2293.md new file mode 100644 index 000000000..d65818af8 --- /dev/null +++ b/upcoming-release-notes/2293.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [joel-jeremy] +--- + +Add hooks for frequently-made operations in the codebase. -- GitLab