From fd24b27c64cc42de5087cc49712115fe4653d374 Mon Sep 17 00:00:00 2001 From: Joel Jeremy Marquez Date: Sat, 27 Jan 2024 13:14:48 -0800 Subject: [PATCH 01/32] Hooks for frequently made operations --- .../desktop-client/src/components/App.tsx | 16 +- .../src/components/FinancesApp.tsx | 15 +- .../src/components/ManageRules.tsx | 23 +- .../src/components/MobileWebMessage.tsx | 10 +- .../desktop-client/src/components/Modals.tsx | 40 +--- .../src/components/Titlebar.tsx | 31 +-- .../src/components/accounts/Account.jsx | 58 +++-- .../components/accounts/AccountSyncCheck.jsx | 3 +- .../src/components/accounts/MobileAccount.jsx | 28 ++- .../components/accounts/MobileAccounts.jsx | 12 +- .../autocomplete/AccountAutocomplete.tsx | 4 +- .../autocomplete/PayeeAutocomplete.tsx | 10 +- .../components/budget/DynamicBudgetTable.tsx | 9 +- .../src/components/budget/MobileBudget.tsx | 20 +- .../src/components/budget/index.tsx | 30 +-- .../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 +- .../reports/reports/CustomReport.jsx | 19 +- .../components/reports/reports/NetWorth.jsx | 4 +- .../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 | 19 +- .../src/components/select/DateSelect.tsx | 13 +- .../select/RecurringSchedulePicker.jsx | 18 +- .../src/components/settings/Encryption.tsx | 9 +- .../src/components/settings/Experimental.tsx | 8 +- .../src/components/settings/Export.tsx | 12 +- .../src/components/settings/Format.tsx | 26 +-- .../src/components/settings/Global.tsx | 9 +- .../src/components/settings/Reset.tsx | 8 +- .../src/components/settings/index.tsx | 22 +- .../src/components/sidebar/Accounts.tsx | 86 +++---- .../src/components/sidebar/Sidebar.tsx | 215 +++++++++++++----- .../components/sidebar/SidebarProvider.tsx | 52 +++++ .../components/sidebar/SidebarWithData.tsx | 181 --------------- .../src/components/sidebar/index.tsx | 77 +------ .../transactions/MobileTransaction.jsx | 11 +- .../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 | 17 ++ .../src/hooks/useBudgetedAccounts.ts | 14 ++ .../desktop-client/src/hooks/useCategories.ts | 16 +- .../src/hooks/useClosedAccounts.ts | 11 + .../desktop-client/src/hooks/useDateFormat.ts | 5 + .../src/hooks/useFailedAccounts.ts | 5 + .../desktop-client/src/hooks/useGlobalPref.ts | 9 + .../src/hooks/useGlobalPrefs.ts | 5 + .../desktop-client/src/hooks/useLocalPref.ts | 9 + .../desktop-client/src/hooks/useLocalPrefs.ts | 5 + .../src/hooks/useOffBudgetAccounts.ts | 14 ++ packages/desktop-client/src/hooks/usePayee.ts | 8 + .../desktop-client/src/hooks/usePayees.ts | 17 ++ .../src/hooks/useUpdatedAccounts.ts | 5 + packages/desktop-client/src/style/theme.tsx | 13 +- .../src/client/data-hooks/accounts.tsx | 36 --- .../src/client/data-hooks/payees.tsx | 36 --- 70 files changed, 663 insertions(+), 898 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/useGlobalPrefs.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/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 diff --git a/packages/desktop-client/src/components/App.tsx b/packages/desktop-client/src/components/App.tsx index 696c7633ea1..28a70ddd21c 100644 --- a/packages/desktop-client/src/components/App.tsx +++ b/packages/desktop-client/src/components/App.tsx @@ -7,9 +7,6 @@ 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 { init as initConnection, @@ -18,6 +15,7 @@ import { 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'; @@ -126,15 +124,9 @@ function ErrorFallback({ error }: FallbackProps) { } export function App() { - const budgetId = useSelector( - state => state.prefs.local && state.prefs.local.id, - ); - const cloudFileId = useSelector( - state => state.prefs.local && state.prefs.local.cloudFileId, - ); - const loadingText = useSelector( - state => state.app.loadingText, - ); + const budgetId = useLocalPref('id'); + const cloudFileId = useLocalPref('cloudFileId'); + const loadingText = useSelector(state => state.app.loadingText); const { loadBudget, closeBudget, loadGlobalPrefs, sync } = useActions(); const [hiddenScrollbars, setHiddenScrollbars] = useState( hasHiddenScrollbars(), diff --git a/packages/desktop-client/src/components/FinancesApp.tsx b/packages/desktop-client/src/components/FinancesApp.tsx index 29c241a14da..4768acc775c 100644 --- a/packages/desktop-client/src/components/FinancesApp.tsx +++ b/packages/desktop-client/src/components/FinancesApp.tsx @@ -13,8 +13,6 @@ 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 { checkForUpdateNotification } from 'loot-core/src/client/update-notification'; import * as undo from 'loot-core/src/platform/client/undo'; @@ -39,7 +37,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'; @@ -265,13 +264,9 @@ export function FinancesApp() { - - - - {app} - - - + + {app} + diff --git a/packages/desktop-client/src/components/ManageRules.tsx b/packages/desktop-client/src/components/ManageRules.tsx index 38634a9899a..2459fcb44a1 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 bc3e0c7ac93..23e24941b29 100644 --- a/packages/desktop-client/src/components/MobileWebMessage.tsx +++ b/packages/desktop-client/src/components/MobileWebMessage.tsx @@ -1,10 +1,11 @@ import React, { useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } 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,12 +17,7 @@ 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 hideMobileMessagePref = useLocalPref('hideMobileMessage') || true; const { isNarrowWidth } = useResponsive(); diff --git a/packages/desktop-client/src/components/Modals.tsx b/packages/desktop-client/src/components/Modals.tsx index 93ae5559218..38a6c7996e6 100644 --- a/packages/desktop-client/src/components/Modals.tsx +++ b/packages/desktop-client/src/components/Modals.tsx @@ -3,17 +3,10 @@ import React, { useEffect } from 'react'; 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 +49,8 @@ export type CommonModalProps = { }; export function Modals() { - const modalStack = useSelector( - state => state.modals.modalStack, - ); - const isHidden = useSelector( - state => state.modals.isHidden, - ); - const accounts = useSelector( - state => state.queries.accounts, - ); - const { grouped: categoryGroups, list: categories } = useCategories(); - const budgetId = useSelector( - state => state.prefs.local && state.prefs.local.id, - ); + const modalStack = useSelector(state => state.modals.modalStack); + const isHidden = useSelector(state => state.modals.isHidden); const actions = useActions(); const location = useLocation(); @@ -118,8 +100,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 +110,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 +119,8 @@ export function Modals() { return ( 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 +138,7 @@ export function Modals() { return ( (state => state.prefs.local?.isPrivacyEnabled); + const isPrivacyEnabled = usePrivacyMode(); const { savePrefs } = useActions(); const privacyIconStyle = { width: 15, height: 15 }; @@ -149,9 +147,7 @@ type SyncButtonProps = { isMobile?: boolean; }; function SyncButton({ style, isMobile = false }: SyncButtonProps) { - const cloudFileId = useSelector( - state => state.prefs.local?.cloudFileId, - ); + const cloudFileId = useLocalPref('cloudFileId'); const { sync } = useActions(); const [syncing, setSyncing] = useState(false); @@ -291,12 +287,8 @@ function SyncButton({ style, isMobile = false }: SyncButtonProps) { } function BudgetTitlebar() { - const maxMonths = useSelector( - state => state.prefs.global?.maxMonths, - ); - const budgetType = useSelector( - state => state.prefs.local?.budgetType, - ); + const maxMonths = useGlobalPrefs('maxMonths'); + const budgetType = useLocalPref('budgetType'); const { saveGlobalPrefs } = useActions(); const { sendEvent } = useContext(TitlebarContext); @@ -399,10 +391,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 : ( ({ - 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(); + const hideFraction = useLocalPref('hideFraction') || false; + const expandSplits = useLocalPref('expand-splits') || false; + const showBalances = useLocalPref(`show-balances-${params.id}`) || false; + const showCleared = useLocalPref(`hide-balances-${params.id}`) || false; + 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, + 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 423c99b7c73..09180381df0 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/MobileAccount.jsx b/packages/desktop-client/src/components/accounts/MobileAccount.jsx index b8d4bb6ce77..fd03ec33f4c 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,26 @@ 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') || 'comma-dot'; + const hideFraction = useLocalPref('hideFraction') || false; + + const state = { + payees, + newTransactions, + prefs, + dateFormat, + }; const dispatch = useDispatch(); const actionCreators = useMemo( @@ -216,8 +228,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 ( state.queries.accounts); + 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') || 'comma-dot'; + const hideFraction = useLocalPref('hideFraction') || false; const { list: categories } = useCategories(); const { getAccounts, replaceModal, syncAndDownload } = useActions(); diff --git a/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx b/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx index f296e7b297f..778480a7dff 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 b99009b3227..84ee34bfd79 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/DynamicBudgetTable.tsx b/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx index 7a01439ab81..f1265a553f0 100644 --- a/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx +++ b/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx @@ -1,12 +1,9 @@ // @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 { useLocalPrefs } from '../../hooks/useLocalPrefs'; import { View } from '../common/View'; import { useBudgetMonthCount } from './BudgetMonthCountContext'; @@ -55,9 +52,7 @@ const DynamicBudgetTableInner = forwardRef< }, ref, ) => { - const prefs = useSelector( - state => state.prefs.local, - ); + const prefs = useLocalPrefs(); const { setDisplayMax } = useBudgetMonthCount(); const actions = useActions(); diff --git a/packages/desktop-client/src/components/budget/MobileBudget.tsx b/packages/desktop-client/src/components/budget/MobileBudget.tsx index 4b98fc5f6ea..5f1dd2a3558 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,7 +47,6 @@ function BudgetInner(props: BudgetInnerProps) { const { categoryGroups, categories, - prefs, loadPrefs, savePrefs, budgetType, @@ -75,6 +71,9 @@ function BudgetInner(props: BudgetInnerProps) { const [initialized, setInitialized] = useState(false); const [editMode, setEditMode] = useState(false); + const numberFormat = useLocalPref('numberFormat') || 'comma-dot'; + const hideFraction = useLocalPref('hideFraction') || false; + 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 ( ( - state => state.prefs.local?.budgetType || 'rollover', - ); - const prefs = useSelector( - state => state.prefs.local, - ); + const budgetType = useLocalPref('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/index.tsx b/packages/desktop-client/src/components/budget/index.tsx index ea263830d8d..d363bbd79bf 100644 --- a/packages/desktop-client/src/components/budget/index.tsx +++ b/packages/desktop-client/src/components/budget/index.tsx @@ -7,7 +7,6 @@ import React, { useEffect, useRef, } from 'react'; -import { useSelector } from 'react-redux'; import { type NavigateFunction, type PathMatch, @@ -15,8 +14,6 @@ import { 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 { @@ -30,12 +27,14 @@ import { deleteGroup, } from 'loot-core/src/shared/categories'; import * as monthUtils from 'loot-core/src/shared/months'; +import { type CategoryGroupEntity } from 'loot-core/src/types/models'; 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'; @@ -553,24 +552,11 @@ 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 => state.prefs.local.budgetType || 'rollover', - ); - const maxMonths = useSelector( - state => state.prefs.global.maxMonths, - ); + const startMonth = useLocalPref('budget.startMonth'); + const collapsedPrefs = useLocalPref('budget.collapsed'); + const summaryCollapsed = useLocalPref('budget.summaryCollapsed'); + const budgetType = useLocalPref('budgetType') || 'rollover'; + const maxMonths = useGlobalPref('maxMonths'); const { grouped: categoryGroups } = useCategories(); const actions = useActions(); diff --git a/packages/desktop-client/src/components/filters/FiltersMenu.jsx b/packages/desktop-client/src/components/filters/FiltersMenu.jsx index f4650c5a44f..18889c5159c 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 e5bc05b8df6..9266c584bc2 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 ( void; }; export function ConfirmCategoryDelete({ modalProps, - category, - group, - categoryGroups, + group: groupId, + category: categoryId, onDelete, }: ConfirmCategoryDeleteProps) { const [transferCategory, setTransferCategory] = useState(null); const [error, setError] = useState(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 fa2b492c4c8..0e2d3f1ff33 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 e7ebb209470..83cd8799d0e 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 89a564b7014..3bd05e3008c 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 d430346b223..34e76a10c13 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({ @@ -125,7 +130,7 @@ export function LoadBackup({ ) : ( actions.loadBackup(budgetId, id)} + onSelect={id => actions.loadBackup(budgetIdToLoad, id)} /> )} diff --git a/packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx b/packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx index 5e36e4f9d99..33ca3d70328 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 8d8d82475bc..8f74791de53 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 80aada7a8d5..2e2cb69e5fc 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 => state.prefs.local.budgetType, - ); + const budgetType = useLocalPref('budgetType'); return ( {() => ( diff --git a/packages/desktop-client/src/components/payees/ManagePayeesWithData.jsx b/packages/desktop-client/src/components/payees/ManagePayeesWithData.jsx index a828c5e73f2..55ea8bc78ae 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 16a6b1e321b..493422dadaf 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'; @@ -22,7 +22,7 @@ export function Overview() { const customReportsFeatureFlag = useFeatureFlag('customReports'); - const accounts = useSelector(state => state.queries.accounts); + const accounts = useAccounts(); return ( state.prefs.local?.reportsViewLegend) || false; - const viewSummary = - useSelector(state => state.prefs.local?.reportsViewSummary) || false; - const viewLabels = - useSelector(state => state.prefs.local?.reportsViewLabel) || false; + const viewLegend = useLocalPref('reportsViewLegend') || false; + const viewSummary = useLocalPref('reportsViewSummary') || false; + const viewLabels = useLocalPref('reportsViewLabel') || false; const { savePrefs } = useActions(); const { @@ -126,8 +123,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({ diff --git a/packages/desktop-client/src/components/reports/reports/NetWorth.jsx b/packages/desktop-client/src/components/reports/reports/NetWorth.jsx index 5bdd779b24f..4dbec9a8e05 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/rules/ScheduleValue.tsx b/packages/desktop-client/src/components/rules/ScheduleValue.tsx index 036d0a2ed37..fb05b0b8f2d 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 => 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 fe9ccc73c2f..d8b184b39d5 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({ describe = x => x.name, style, }: ValueProps) { - const dateFormat = useSelector( - state => state.prefs.local.dateFormat || 'MM/dd/yyyy', - ); - const payees = useSelector( - state => state.queries.payees, - ); + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; + const payees = usePayees(); const { list: categories } = useCategories(); - const accounts = useSelector( - 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 03a0aebaf00..1f62f40c8f4 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 => 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 85446055b92..56d20b44265 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 b2ec6ea021c..56e4195ea8f 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 => { - 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) { diff --git a/packages/desktop-client/src/components/select/DateSelect.tsx b/packages/desktop-client/src/components/select/DateSelect.tsx index a6b83fc65f9..23469ec7b8d 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,7 @@ 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') || '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 df10237168f..17ee82e87c2 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 cfe994b13a3..9fbd2f04836 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 => 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 094af86241b..deb697a843c 100644 --- a/packages/desktop-client/src/components/settings/Experimental.tsx +++ b/packages/desktop-client/src/components/settings/Experimental.tsx @@ -1,12 +1,10 @@ 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'; @@ -63,9 +61,7 @@ function FeatureToggle({ } function ReportBudgetFeature() { - const budgetType = useSelector( - 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 6cd2e744989..98ec399db4d 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(null); - const budgetId = useSelector( - state => state.prefs.local.id, - ); - const encryptKeyId = useSelector( - 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 8f36cc434b3..c9ad375425c 100644 --- a/packages/desktop-client/src/components/settings/Format.tsx +++ b/packages/desktop-client/src/components/settings/Format.tsx @@ -1,20 +1,19 @@ // @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'; @@ -59,21 +58,10 @@ 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 => state.prefs.local.dateFormat || 'MM/dd/yyyy', - ); - const numberFormat = useSelector( - state => state.prefs.local.numberFormat || 'comma-dot', - ); - const hideFraction = useSelector( - state => state.prefs.local.hideFraction, - ); + const firstDayOfWeekIdx = useLocalPref('firstDayOfWeekIdx') || '0'; // Sunday; + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; + const numberFormat = useLocalPref('numberFormat') || 'comma-dot'; + const hideFraction = useLocalPref('hideFraction'); return ( ( - state => state.prefs.global.documentDir, - ); + const documentDir = useGlobalPref('documentDir'); const { saveGlobalPrefs } = useActions(); const [documentDirChanged, setDirChanged] = useState(false); diff --git a/packages/desktop-client/src/components/settings/Reset.tsx b/packages/desktop-client/src/components/settings/Reset.tsx index 744ada38e2b..3cb924082e8 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,7 @@ export function ResetCache() { } export function ResetSync() { - const isEnabled = !!useSelector( - state => state.prefs.local.groupId, - ); + const isEnabled = !!useLocalPref('groupId'); const { resetSync } = useActions(); const [resetting, setResetting] = useState(false); diff --git a/packages/desktop-client/src/components/settings/index.tsx b/packages/desktop-client/src/components/settings/index.tsx index 454f415756c..717b6e830f4 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 => state.prefs.local.id, - ); - const groupId = useSelector( - state => state.prefs.local.groupId, - ); + const budgetId = useLocalPref('id'); + const groupId = useLocalPref('groupId'); return ( @@ -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 => 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 cd20ccb5856..fe8eb27c24d 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({ {budgetedAccounts.length > 0 && ( )} @@ -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 && ( )} @@ -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 dadd3ba95fc..37ad5c3f526 100644 --- a/packages/desktop-client/src/components/sidebar/Sidebar.tsx +++ b/packages/desktop-client/src/components/sidebar/Sidebar.tsx @@ -1,68 +1,75 @@ -import React, { type ReactNode } from 'react'; +import React, { useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { closeBudget } from 'loot-core/client/actions'; +import { send } from 'loot-core/platform/client/fetch'; import * as Platform from 'loot-core/src/client/platform'; -import { type AccountEntity } from 'loot-core/src/types/models'; - +import { type LocalPrefs } from 'loot-core/types/prefs'; + +import { useAccounts } from '../../hooks/useAccounts'; +import { useActions } from '../../hooks/useActions'; +import { useGlobalPrefs } from '../../hooks/useGlobalPrefs'; +import { useLocalPrefs } from '../../hooks/useLocalPrefs'; +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 sidebar = useSidebar(); + const accounts = useAccounts(); + const prefs = useLocalPrefs() || {}; + const isFloating = useGlobalPrefs('floatingSidebar') || false; + + const { getAccounts, replaceModal, savePrefs, saveGlobalPrefs } = + useActions(); + + 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; + } + + await send('account-move', { id, targetId: targetIdToMove }); + await getAccounts(); + } + + const onFloat = () => { + saveGlobalPrefs({ floatingSidebar: !isFloating }); + }; + + const onAddAccount = () => { + replaceModal('add-account'); + }; + + const onToggleClosedAccounts = () => { + savePrefs({ + 'ui.showClosedAccounts': !prefs['ui.showClosedAccounts'], + }); + }; return ( - {budgetName} + @@ -123,18 +131,6 @@ export function Sidebar({ /> `/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 +139,96 @@ export function Sidebar({ ); } + +type EditableBudgetNameProps = { + prefs: LocalPrefs; + savePrefs: (prefs: Partial) => Promise; +}; + +function EditableBudgetName({ prefs, savePrefs }: EditableBudgetNameProps) { + const dispatch = useDispatch(); + const navigate = useNavigate(); + 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 ( + + { + const inputEl = e.target as HTMLInputElement; + const newBudgetName = inputEl.value; + if (newBudgetName.trim() !== '') { + await savePrefs({ + budgetName: inputEl.value, + }); + setEditing(false); + } + }} + onBlur={() => setEditing(false)} + /> + + ); + } else { + return ( + + ); + } +} 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 00000000000..9106aced695 --- /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>; + floating: boolean; + alwaysFloats: boolean; +}; + +const SidebarContext = createContext(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 ( + + {children} + + ); +} + +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 202e84bfe9c..00000000000 --- 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) => Promise; -}; - -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 ( - - setEditing(false)} - /> - - ); - } else { - return ( - - ); - } -} - -export function SidebarWithData() { - const accounts = useSelector( - state => state.queries.accounts, - ); - const failedAccounts = useSelector( - state => state.account.failedAccounts, - ); - const updatedAccounts = useSelector( - state => state.queries.updatedAccounts, - ); - const prefs = useSelector(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 ( - } - 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 c3f02e448cb..29ea6b82146 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>; - floating: boolean; - alwaysFloats: boolean; -}; - -const SidebarContext = createContext(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 ( - - {children} - - ); -} - -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', }} > - + ); } diff --git a/packages/desktop-client/src/components/transactions/MobileTransaction.jsx b/packages/desktop-client/src/components/transactions/MobileTransaction.jsx index bda688058a8..eadbea53ca1 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, @@ -1110,12 +1113,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 ea43f6adbd6..736df89d280 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 db3982cbeac..34c5f593acc 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' ? ( + + ) : ( + + ); +} +function AccountDisplayId({ id, noneColor }) { + const account = useAccount(id); return ( - - {data => { - const item = data[id]; + + {account ? account.name : 'None'} + + ); +} - return ( - - {item ? item.name : 'None'} - - ); - }} - +function PayeeDisplayId({ id, noneColor }) { + const payee = usePayee(id); + return ( + + {payee ? payee.name : 'None'} + ); } diff --git a/packages/desktop-client/src/components/util/GenericInput.jsx b/packages/desktop-client/src/components/util/GenericInput.jsx index b42826e01ba..bf2e3c53d30 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 00000000000..619c8522db7 --- /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]); +} diff --git a/packages/desktop-client/src/hooks/useAccounts.ts b/packages/desktop-client/src/hooks/useAccounts.ts new file mode 100644 index 00000000000..20ad3c5ce5b --- /dev/null +++ b/packages/desktop-client/src/hooks/useAccounts.ts @@ -0,0 +1,17 @@ +import { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { getAccounts } from 'loot-core/client/actions'; + +export function useAccounts() { + const dispatch = useDispatch(); + const accounts = useSelector(state => state.queries.accounts); + + useEffect(() => { + if (accounts.length === 0) { + 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 00000000000..dbd8e1f53db --- /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 8e273091541..6ffce338d2c 100644 --- a/packages/desktop-client/src/hooks/useCategories.ts +++ b/packages/desktop-client/src/hooks/useCategories.ts @@ -1,21 +1,15 @@ 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/client/actions'; export function useCategories() { - const { getCategories } = useActions(); - - const categories = useSelector( - state => state.queries.categories.list, - ); + const dispatch = useDispatch(); + const categories = useSelector(state => state.queries.categories.list); useEffect(() => { if (categories.length === 0) { - getCategories(); + dispatch(getCategories()); } }, []); diff --git a/packages/desktop-client/src/hooks/useClosedAccounts.ts b/packages/desktop-client/src/hooks/useClosedAccounts.ts new file mode 100644 index 00000000000..85aa5b92040 --- /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 00000000000..93b1b30451e --- /dev/null +++ b/packages/desktop-client/src/hooks/useDateFormat.ts @@ -0,0 +1,5 @@ +import { useSelector } from 'react-redux'; + +export function useDateFormat() { + return useSelector(state => state.prefs.local?.dateFormat || undefined); +} diff --git a/packages/desktop-client/src/hooks/useFailedAccounts.ts b/packages/desktop-client/src/hooks/useFailedAccounts.ts new file mode 100644 index 00000000000..26027feded7 --- /dev/null +++ b/packages/desktop-client/src/hooks/useFailedAccounts.ts @@ -0,0 +1,5 @@ +import { useSelector } from 'react-redux'; + +export function useFailedAccounts() { + return useSelector(state => state.account.failedAccounts); +} diff --git a/packages/desktop-client/src/hooks/useGlobalPref.ts b/packages/desktop-client/src/hooks/useGlobalPref.ts new file mode 100644 index 00000000000..ef16e19875c --- /dev/null +++ b/packages/desktop-client/src/hooks/useGlobalPref.ts @@ -0,0 +1,9 @@ +import { useSelector } from 'react-redux'; + +import { type GlobalPrefs } from 'loot-core/types/prefs'; + +export function useGlobalPref( + prefName: K, +): GlobalPrefs[K] { + return useSelector(state => state.prefs.global?.[prefName]); +} diff --git a/packages/desktop-client/src/hooks/useGlobalPrefs.ts b/packages/desktop-client/src/hooks/useGlobalPrefs.ts new file mode 100644 index 00000000000..3e6005e3395 --- /dev/null +++ b/packages/desktop-client/src/hooks/useGlobalPrefs.ts @@ -0,0 +1,5 @@ +import { useSelector } from 'react-redux'; + +export function useGlobalPrefs() { + return useSelector(state => state.prefs.global); +} diff --git a/packages/desktop-client/src/hooks/useLocalPref.ts b/packages/desktop-client/src/hooks/useLocalPref.ts new file mode 100644 index 00000000000..75a890d5e85 --- /dev/null +++ b/packages/desktop-client/src/hooks/useLocalPref.ts @@ -0,0 +1,9 @@ +import { useSelector } from 'react-redux'; + +import { type LocalPrefs } from 'loot-core/types/prefs'; + +export function useLocalPref( + prefName: K, +): LocalPrefs[K] { + return useSelector(state => state.prefs.local?.[prefName]); +} diff --git a/packages/desktop-client/src/hooks/useLocalPrefs.ts b/packages/desktop-client/src/hooks/useLocalPrefs.ts new file mode 100644 index 00000000000..3f8a4ce1bba --- /dev/null +++ b/packages/desktop-client/src/hooks/useLocalPrefs.ts @@ -0,0 +1,5 @@ +import { useSelector } from 'react-redux'; + +export function useLocalPrefs() { + return useSelector(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 00000000000..71a5db919bf --- /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 00000000000..56eefd0f9c6 --- /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]); +} diff --git a/packages/desktop-client/src/hooks/usePayees.ts b/packages/desktop-client/src/hooks/usePayees.ts new file mode 100644 index 00000000000..993dd53e2c8 --- /dev/null +++ b/packages/desktop-client/src/hooks/usePayees.ts @@ -0,0 +1,17 @@ +import { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { getPayees } from 'loot-core/client/actions'; + +export function usePayees() { + const dispatch = useDispatch(); + const payees = useSelector(state => state.queries.payees); + + useEffect(() => { + if (payees.length === 0) { + dispatch(getPayees()); + } + }, []); + + return useSelector(state => state.queries.payees); +} diff --git a/packages/desktop-client/src/hooks/useUpdatedAccounts.ts b/packages/desktop-client/src/hooks/useUpdatedAccounts.ts new file mode 100644 index 00000000000..171c8c2b01c --- /dev/null +++ b/packages/desktop-client/src/hooks/useUpdatedAccounts.ts @@ -0,0 +1,5 @@ +import { useSelector } from 'react-redux'; + +export function useUpdatedAccounts() { + return useSelector(state => state.queries.updatedAccounts); +} diff --git a/packages/desktop-client/src/style/theme.tsx b/packages/desktop-client/src/style/theme.tsx index 7cd3b5751fd..57f7fe1c19b 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,12 +23,8 @@ export const themeOptions = Object.entries(themes).map( ([key, { name }]) => [key, name] as [Theme, string], ); -export function useTheme(): Theme { - return ( - useSelector( - state => state.prefs.global?.theme, - ) || 'light' - ); +export function useTheme() { + return useGlobalPref('theme') || 'light'; } export function ThemeStyle() { 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 c2460477fa6..00000000000 --- 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(null); - -export function AccountsProvider({ children }) { - const data = useAccounts(); - return ( - {children} - ); -} - -export function CachedAccounts({ children, idKey }) { - const data = useCachedAccounts({ idKey }); - return children(data); -} - -export function useCachedAccounts(): AccountEntity[]; -export function useCachedAccounts({ - idKey, -}: { - idKey: boolean; -}): Record; -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 4299aa9a31f..00000000000 --- 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(null); - -export function PayeesProvider({ children }) { - const data = usePayees(); - return ( - {children} - ); -} - -export function CachedPayees({ children, idKey }) { - const data = useCachedPayees({ idKey }); - return children(data); -} - -export function useCachedPayees(): PayeeEntity[]; -export function useCachedPayees({ - idKey, -}: { - idKey: boolean; -}): Record; -export function useCachedPayees({ idKey }: { idKey?: boolean } = {}) { - const data = useContext(PayeesContext); - return idKey && data ? getPayeesById(data) : data; -} From 42077a789448dd84d2cad7e4e09707f02c8a703b Mon Sep 17 00:00:00 2001 From: Joel Jeremy Marquez Date: Sat, 27 Jan 2024 13:17:33 -0800 Subject: [PATCH 02/32] Release notes --- upcoming-release-notes/2293.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 upcoming-release-notes/2293.md diff --git a/upcoming-release-notes/2293.md b/upcoming-release-notes/2293.md new file mode 100644 index 00000000000..d65818af8a0 --- /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. From 67c66311bef763b8277fb5f443c939cd1e3ac189 Mon Sep 17 00:00:00 2001 From: Joel Jeremy Marquez Date: Sat, 27 Jan 2024 13:19:23 -0800 Subject: [PATCH 03/32] Fix typecheck errors --- packages/desktop-client/src/components/Titlebar.tsx | 3 +-- packages/desktop-client/src/components/sidebar/Sidebar.tsx | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/desktop-client/src/components/Titlebar.tsx b/packages/desktop-client/src/components/Titlebar.tsx index 70bb2a6bf22..129dbb59e85 100644 --- a/packages/desktop-client/src/components/Titlebar.tsx +++ b/packages/desktop-client/src/components/Titlebar.tsx @@ -17,7 +17,6 @@ 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 { useGlobalPrefs } from '../hooks/useGlobalPrefs'; import { useLocalPref } from '../hooks/useLocalPref'; import { useNavigate } from '../hooks/useNavigate'; import { SvgArrowLeft } from '../icons/v1'; @@ -287,7 +286,7 @@ function SyncButton({ style, isMobile = false }: SyncButtonProps) { } function BudgetTitlebar() { - const maxMonths = useGlobalPrefs('maxMonths'); + const maxMonths = useGlobalPref('maxMonths'); const budgetType = useLocalPref('budgetType'); const { saveGlobalPrefs } = useActions(); const { sendEvent } = useContext(TitlebarContext); diff --git a/packages/desktop-client/src/components/sidebar/Sidebar.tsx b/packages/desktop-client/src/components/sidebar/Sidebar.tsx index 37ad5c3f526..268e7e20128 100644 --- a/packages/desktop-client/src/components/sidebar/Sidebar.tsx +++ b/packages/desktop-client/src/components/sidebar/Sidebar.tsx @@ -8,7 +8,7 @@ import { type LocalPrefs } from 'loot-core/types/prefs'; import { useAccounts } from '../../hooks/useAccounts'; import { useActions } from '../../hooks/useActions'; -import { useGlobalPrefs } from '../../hooks/useGlobalPrefs'; +import { useGlobalPref } from '../../hooks/useGlobalPref'; import { useLocalPrefs } from '../../hooks/useLocalPrefs'; import { useNavigate } from '../../hooks/useNavigate'; import { SvgExpandArrow } from '../../icons/v0'; @@ -37,7 +37,7 @@ export function Sidebar() { const sidebar = useSidebar(); const accounts = useAccounts(); const prefs = useLocalPrefs() || {}; - const isFloating = useGlobalPrefs('floatingSidebar') || false; + const isFloating = useGlobalPref('floatingSidebar') || false; const { getAccounts, replaceModal, savePrefs, saveGlobalPrefs } = useActions(); From ca90ffbaf06e570fb6376a3c441590c8f76791ac Mon Sep 17 00:00:00 2001 From: Joel Jeremy Marquez Date: Sat, 27 Jan 2024 13:24:08 -0800 Subject: [PATCH 04/32] Remove useGlobalPrefs --- packages/desktop-client/src/hooks/useGlobalPrefs.ts | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 packages/desktop-client/src/hooks/useGlobalPrefs.ts diff --git a/packages/desktop-client/src/hooks/useGlobalPrefs.ts b/packages/desktop-client/src/hooks/useGlobalPrefs.ts deleted file mode 100644 index 3e6005e3395..00000000000 --- a/packages/desktop-client/src/hooks/useGlobalPrefs.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { useSelector } from 'react-redux'; - -export function useGlobalPrefs() { - return useSelector(state => state.prefs.global); -} From 054bbb0f5d6baf64a2700012f85ae131a70ab596 Mon Sep 17 00:00:00 2001 From: Joel Jeremy Marquez Date: Sat, 27 Jan 2024 13:26:52 -0800 Subject: [PATCH 05/32] Add null checks --- packages/desktop-client/src/hooks/useAccounts.ts | 2 +- packages/desktop-client/src/hooks/usePayees.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/desktop-client/src/hooks/useAccounts.ts b/packages/desktop-client/src/hooks/useAccounts.ts index 20ad3c5ce5b..2915261cf69 100644 --- a/packages/desktop-client/src/hooks/useAccounts.ts +++ b/packages/desktop-client/src/hooks/useAccounts.ts @@ -8,7 +8,7 @@ export function useAccounts() { const accounts = useSelector(state => state.queries.accounts); useEffect(() => { - if (accounts.length === 0) { + if (accounts == null || accounts.length === 0) { dispatch(getAccounts()); } }, []); diff --git a/packages/desktop-client/src/hooks/usePayees.ts b/packages/desktop-client/src/hooks/usePayees.ts index 993dd53e2c8..797b46ffeb2 100644 --- a/packages/desktop-client/src/hooks/usePayees.ts +++ b/packages/desktop-client/src/hooks/usePayees.ts @@ -8,7 +8,7 @@ export function usePayees() { const payees = useSelector(state => state.queries.payees); useEffect(() => { - if (payees.length === 0) { + if (payees == null || payees.length === 0) { dispatch(getPayees()); } }, []); From da79abb65cd37bbbacfad68bf965053663f76334 Mon Sep 17 00:00:00 2001 From: Joel Jeremy Marquez Date: Sat, 27 Jan 2024 20:21:20 -0800 Subject: [PATCH 06/32] Fix showCleared pref --- .../desktop-client/src/components/accounts/Account.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/desktop-client/src/components/accounts/Account.jsx b/packages/desktop-client/src/components/accounts/Account.jsx index 5e0163413b7..3c5d1a2b95d 100644 --- a/packages/desktop-client/src/components/accounts/Account.jsx +++ b/packages/desktop-client/src/components/accounts/Account.jsx @@ -1545,11 +1545,11 @@ export function Account() { const failedAccounts = useFailedAccounts(); const dateFormat = useDateFormat(); const hideFraction = useLocalPref('hideFraction') || false; - const expandSplits = useLocalPref('expand-splits') || false; - const showBalances = useLocalPref(`show-balances-${params.id}`) || false; - const showCleared = useLocalPref(`hide-balances-${params.id}`) || false; + const expandSplits = useLocalPref('expand-splits'); + const showBalances = useLocalPref(`show-balances-${params.id}`); + const showCleared = !useLocalPref(`hide-cleared-${params.id}`); const showExtraBalances = useLocalPref( - `show-extra-balances-${params.id}` || 'all-accounts', + `show-extra-balances-${params.id || 'all-accounts'}`, ); const modalShowing = useSelector(state => state.modals.modalStack.length > 0); const accountsSyncing = useSelector(state => state.account.accountsSyncing); From 87d3849fb385bb8079cb96ee14eb1999e077646f Mon Sep 17 00:00:00 2001 From: Joel Jeremy Marquez Date: Sat, 27 Jan 2024 21:59:53 -0800 Subject: [PATCH 07/32] Add loaded flag for categories, accounts and payees state --- .../src/components/schedules/SchedulesTable.tsx | 2 +- packages/desktop-client/src/hooks/useAccounts.ts | 11 ++++------- packages/desktop-client/src/hooks/useCategories.ts | 11 ++++------- packages/desktop-client/src/hooks/usePayees.ts | 11 ++++------- packages/loot-core/src/client/reducers/queries.ts | 6 ++++++ .../loot-core/src/client/state-types/queries.d.ts | 3 +++ 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx index 56e4195ea8f..ba34e2df104 100644 --- a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx +++ b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx @@ -233,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/hooks/useAccounts.ts b/packages/desktop-client/src/hooks/useAccounts.ts index 2915261cf69..98d32a37cef 100644 --- a/packages/desktop-client/src/hooks/useAccounts.ts +++ b/packages/desktop-client/src/hooks/useAccounts.ts @@ -1,17 +1,14 @@ -import { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { getAccounts } from 'loot-core/client/actions'; export function useAccounts() { const dispatch = useDispatch(); - const accounts = useSelector(state => state.queries.accounts); + const accountLoaded = useSelector(state => state.queries.accountsLoaded); - useEffect(() => { - if (accounts == null || accounts.length === 0) { - dispatch(getAccounts()); - } - }, []); + if (!accountLoaded) { + dispatch(getAccounts()); + } return useSelector(state => state.queries.accounts); } diff --git a/packages/desktop-client/src/hooks/useCategories.ts b/packages/desktop-client/src/hooks/useCategories.ts index 6ffce338d2c..8ac1d9ff344 100644 --- a/packages/desktop-client/src/hooks/useCategories.ts +++ b/packages/desktop-client/src/hooks/useCategories.ts @@ -1,17 +1,14 @@ -import { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { getCategories } from 'loot-core/client/actions'; export function useCategories() { const dispatch = useDispatch(); - const categories = useSelector(state => state.queries.categories.list); + const categoriesLoaded = useSelector(state => state.queries.categoriesLoaded); - useEffect(() => { - if (categories.length === 0) { - dispatch(getCategories()); - } - }, []); + if (!categoriesLoaded) { + dispatch(getCategories()); + } return useSelector( state => state.queries.categories, diff --git a/packages/desktop-client/src/hooks/usePayees.ts b/packages/desktop-client/src/hooks/usePayees.ts index 797b46ffeb2..b68665cc9bd 100644 --- a/packages/desktop-client/src/hooks/usePayees.ts +++ b/packages/desktop-client/src/hooks/usePayees.ts @@ -1,17 +1,14 @@ -import { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { getPayees } from 'loot-core/client/actions'; export function usePayees() { const dispatch = useDispatch(); - const payees = useSelector(state => state.queries.payees); + const payeesLoaded = useSelector(state => state.queries.payeesLoaded); - useEffect(() => { - if (payees == null || payees.length === 0) { - dispatch(getPayees()); - } - }, []); + if (!payeesLoaded) { + dispatch(getPayees()); + } return useSelector(state => state.queries.payees); } diff --git a/packages/loot-core/src/client/reducers/queries.ts b/packages/loot-core/src/client/reducers/queries.ts index 470c3591692..b0c38817a24 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/queries.d.ts b/packages/loot-core/src/client/state-types/queries.d.ts index a7f18526fe0..0d511868b39 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>; + categoriesLoaded: boolean; payees: Awaited>; + payeesLoaded: boolean; earliestTransaction: unknown | null; }; From 0d6469312d76adacdedce9570c064c94ea99f3ff Mon Sep 17 00:00:00 2001 From: Joel Jeremy Marquez Date: Sun, 28 Jan 2024 01:47:00 -0800 Subject: [PATCH 08/32] Refactor to reduce unnecessary states --- .../desktop-client/src/components/App.tsx | 32 +- .../src/components/FinancesApp.tsx | 29 +- .../src/components/accounts/MobileAccount.jsx | 5 - .../components/accounts/MobileAccounts.jsx | 24 +- .../components/budget/BudgetCategories.jsx | 69 +++- .../src/components/budget/BudgetTable.jsx | 35 +- .../components/budget/DynamicBudgetTable.tsx | 3 +- .../src/components/budget/index.tsx | 322 +++++------------- .../budget/rollover/RolloverContext.tsx | 3 - .../src/components/sidebar/Sidebar.tsx | 56 +-- .../transactions/MobileTransaction.jsx | 5 - .../loot-core/src/client/actions/account.ts | 7 + .../src/client/state-types/modals.d.ts | 2 +- packages/loot-core/src/shared/categories.ts | 106 ------ 14 files changed, 231 insertions(+), 467 deletions(-) delete mode 100644 packages/loot-core/src/shared/categories.ts diff --git a/packages/desktop-client/src/components/App.tsx b/packages/desktop-client/src/components/App.tsx index 28a70ddd21c..37d474dc739 100644 --- a/packages/desktop-client/src/components/App.tsx +++ b/packages/desktop-client/src/components/App.tsx @@ -12,7 +12,6 @@ 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'; @@ -32,26 +31,13 @@ import { UpdateNotification } from './UpdateNotification'; type AppInnerProps = { budgetId: string; cloudFileId: string; - loadingText: string; - loadBudget: ( - id: string, - loadingText?: string, - options?: object, - ) => Promise; - closeBudget: () => Promise; - loadGlobalPrefs: () => Promise; }; -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.app.loadingText); + const { loadBudget, closeBudget, loadGlobalPrefs } = useActions(); async function init() { const socketName = await global.Actual.getServerSocket(); @@ -126,8 +112,7 @@ function ErrorFallback({ error }: FallbackProps) { export function App() { const budgetId = useLocalPref('id'); const cloudFileId = useLocalPref('cloudFileId'); - const loadingText = useSelector(state => state.app.loadingText); - const { loadBudget, closeBudget, loadGlobalPrefs, sync } = useActions(); + const { sync } = useActions(); const [hiddenScrollbars, setHiddenScrollbars] = useState( hasHiddenScrollbars(), ); @@ -176,14 +161,7 @@ export function App() { {process.env.REACT_APP_REVIEW_ID && !Platform.isPlaywright && ( )} - + diff --git a/packages/desktop-client/src/components/FinancesApp.tsx b/packages/desktop-client/src/components/FinancesApp.tsx index 4768acc775c..775607d678d 100644 --- a/packages/desktop-client/src/components/FinancesApp.tsx +++ b/packages/desktop-client/src/components/FinancesApp.tsx @@ -17,6 +17,7 @@ import { SpreadsheetProvider } from 'loot-core/src/client/SpreadsheetProvider'; 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'; @@ -41,6 +42,7 @@ import { FloatableSidebar } from './sidebar'; import { SidebarProvider } from './sidebar/SidebarProvider'; import { Titlebar, TitlebarProvider } from './Titlebar'; import { TransactionEdit } from './transactions/MobileTransaction'; +import { useSelector } from 'react-redux'; function NarrowNotSupported({ redirectTo = '/budget', @@ -70,18 +72,17 @@ 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.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); @@ -113,9 +114,15 @@ function FinancesAppWithoutContext() { }, 100); }, []); + useEffect(() => { + actions.getAccounts(); + actions.getCategories(); + actions.getPayees(); + }, []); + return ( - + diff --git a/packages/desktop-client/src/components/accounts/MobileAccount.jsx b/packages/desktop-client/src/components/accounts/MobileAccount.jsx index fd03ec33f4c..8c640275037 100644 --- a/packages/desktop-client/src/components/accounts/MobileAccount.jsx +++ b/packages/desktop-client/src/components/accounts/MobileAccount.jsx @@ -146,11 +146,6 @@ export function Account(props) { } }); - if (accounts.length === 0) { - await actionCreators.getAccounts(); - } - - await actionCreators.initiallyLoadPayees(); await fetchTransactions(); actionCreators.markAccountRead(accountId); diff --git a/packages/desktop-client/src/components/accounts/MobileAccounts.jsx b/packages/desktop-client/src/components/accounts/MobileAccounts.jsx index b18aec87c30..046e2511bfb 100644 --- a/packages/desktop-client/src/components/accounts/MobileAccounts.jsx +++ b/packages/desktop-client/src/components/accounts/MobileAccounts.jsx @@ -1,10 +1,10 @@ -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/client/actions'; import * as queries from 'loot-core/src/client/queries'; import { useAccounts } from '../../hooks/useAccounts'; -import { useActions } from '../../hooks/useActions'; import { useCategories } from '../../hooks/useCategories'; import { useLocalPref } from '../../hooks/useLocalPref'; import { useNavigate } from '../../hooks/useNavigate'; @@ -218,6 +218,7 @@ function AccountList({ } export function Accounts() { + const dispatch = useDispatch(); const accounts = useAccounts(); const newTransactions = useSelector(state => state.queries.newTransactions); const updatedAccounts = useSelector(state => state.queries.updatedAccounts); @@ -225,15 +226,10 @@ export function Accounts() { const hideFraction = useLocalPref('hideFraction') || false; const { list: categories } = useCategories(); - const { getAccounts, replaceModal, syncAndDownload } = useActions(); const transactions = useState({}); const navigate = useNavigate(); - useEffect(() => { - (async () => getAccounts())(); - }, []); - const onSelectAccount = id => { navigate(`/accounts/${id}`); }; @@ -242,6 +238,14 @@ export function Accounts() { navigate(`/transaction/${transaction}`); }; + const onAddAccount = () => { + dispatch(replaceModal('add-account')); + }; + + const onSync = () => { + dispatch(syncAndDownload()); + }; + useSetThemeColor(theme.mobileViewTheme); return ( @@ -258,10 +262,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} /> ); diff --git a/packages/desktop-client/src/components/budget/BudgetCategories.jsx b/packages/desktop-client/src/components/budget/BudgetCategories.jsx index c80bfb2ab35..fc3cc812eea 100644 --- a/packages/desktop-client/src/components/budget/BudgetCategories.jsx +++ b/packages/desktop-client/src/components/budget/BudgetCategories.jsx @@ -1,5 +1,9 @@ import React, { memo, useState, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; +import { savePrefs } from 'loot-core/client/actions'; + +import { useLocalPref } from '../../hooks/useLocalPref'; import { theme, styles } from '../../style'; import { View } from '../common/View'; import { DropHighlightPosContext } from '../sort'; @@ -17,12 +21,8 @@ import { separateGroups } from './util'; export const BudgetCategories = memo( ({ categoryGroups, - newCategoryForGroup, showHiddenCategories, - isAddingGroup, editingCell, - collapsed, - setCollapsed, dataComponents, onBudgetAction, onShowActivity, @@ -34,11 +34,15 @@ export const BudgetCategories = memo( onDeleteGroup, onReorderCategory, onReorderGroup, - onShowNewCategory, - onHideNewCategory, - onShowNewGroup, - onHideNewGroup, }) => { + const dispatch = useDispatch(); + const collapsed = useLocalPref('budget.collapsed') || []; + function onCollapse(value) { + dispatch(savePrefs({ 'budget.collapsed': value })); + } + + const [isAddingGroup, setIsAddingGroup] = useState(false); + const [newCategoryForGroup, setNewCategoryForGroup] = useState(null); const items = useMemo(() => { const [expenseGroups, incomeGroup] = separateGroups(categoryGroups); @@ -133,15 +137,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 +202,7 @@ export const BudgetCategories = memo( @@ -187,7 +222,7 @@ export const BudgetCategories = memo( id: 'new', }} editing={true} - onSave={onSaveCategory} + onSave={_onSaveCategory} onHideNewCategory={onHideNewCategory} onEditName={onEditName} /> @@ -204,7 +239,7 @@ export const BudgetCategories = memo( MonthComponent={dataComponents.ExpenseGroupComponent} dragState={dragState} onEditName={onEditName} - onSave={onSaveGroup} + onSave={_onSaveGroup} onDelete={onDeleteGroup} onDragChange={onDragChange} onReorderGroup={onReorderGroup} @@ -223,7 +258,7 @@ export const BudgetCategories = memo( dragState={dragState} onEditName={onEditName} onEditMonth={onEditMonth} - onSave={onSaveCategory} + onSave={_onSaveCategory} onDelete={onDeleteCategory} onDragChange={onDragChange} onReorder={onReorderCategory} @@ -255,7 +290,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 +305,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 3000ebb1af4..a11f0c382b5 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/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(); @@ -151,12 +153,12 @@ export class BudgetTable extends Component { }; 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,19 +169,11 @@ 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; @@ -256,11 +250,7 @@ export class BudgetTable extends Component { @@ -285,3 +271,12 @@ export class BudgetTable extends Component { ); } } + +const mapDispatchToProps = dispatch => { + return { + onCollapse: collapsed => + dispatch(savePrefs({ 'budget.collapsed': collapsed })), + }; +}; + +export const BudgetTable = connect(null, mapDispatchToProps)(BudgetTableInner); diff --git a/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx b/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx index f1265a553f0..4084cf0ff57 100644 --- a/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx +++ b/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx @@ -3,6 +3,7 @@ import React, { forwardRef, useEffect, type ComponentProps } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { useActions } from '../../hooks/useActions'; +import { useCategories } from '../../hooks/useCategories'; import { useLocalPrefs } from '../../hooks/useLocalPrefs'; import { View } from '../common/View'; @@ -41,7 +42,6 @@ const DynamicBudgetTableInner = forwardRef< { width, height, - categoryGroups, prewarmStartMonth, startMonth, maxMonths = 3, @@ -52,6 +52,7 @@ const DynamicBudgetTableInner = forwardRef< }, ref, ) => { + const { grouped: categoryGroups } = useCategories(); const prefs = useLocalPrefs(); const { setDisplayMax } = useBudgetMonthCount(); const actions = useActions(); diff --git a/packages/desktop-client/src/components/budget/index.tsx b/packages/desktop-client/src/components/budget/index.tsx index d363bbd79bf..5d60823d17e 100644 --- a/packages/desktop-client/src/components/budget/index.tsx +++ b/packages/desktop-client/src/components/budget/index.tsx @@ -7,30 +7,28 @@ import React, { useEffect, useRef, } from 'react'; -import { - type NavigateFunction, - type PathMatch, - useLocation, - useMatch, -} from 'react-router-dom'; +import { useDispatch } from 'react-redux'; -import { useSpreadsheet } from 'loot-core/src/client/SpreadsheetProvider'; -import { send, listen } from 'loot-core/src/platform/client/fetch'; import { - addCategory, + addNotification, + applyBudgetAction, + createCategory, + createGroup, + deleteCategory, + deleteGroup, + getCategories, + loadPrefs, moveCategory, moveCategoryGroup, + pushModal, + savePrefs, 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 CategoryGroupEntity } from 'loot-core/src/types/models'; -import { type GlobalPrefs, type LocalPrefs } from 'loot-core/src/types/prefs'; -import { type BoundActions, useActions } from '../../hooks/useActions'; import { useCategories } from '../../hooks/useCategories'; import { useFeatureFlag } from '../../hooks/useFeatureFlag'; import { useGlobalPref } from '../../hooks/useGlobalPref'; @@ -74,62 +72,35 @@ 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; - spreadsheet: ReturnType; - 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 spreadsheet = useSpreadsheet(); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const startMonth = useLocalPref('budget.startMonth') || currentMonth; + const summaryCollapsed = useLocalPref('budget.summaryCollapsed'); + const budgetType = useLocalPref('budgetType') || 'rollover'; + const maxMonths = useGlobalPref('maxMonths') || 1; const [initialized, setInitialized] = useState(false); - const [prewarmStartMonth, setPrewarmStartMonth] = useState( - props.startMonth || currentMonth, - ); - - const [newCategoryForGroup, setNewCategoryForGroup] = useState(null); - const [isAddingGroup, setIsAddingGroup] = useState(false); - const [collapsed, setCollapsed] = useState(props.collapsedPrefs || []); 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(); @@ -139,9 +110,9 @@ function BudgetInner(props: BudgetProps) { await prewarmAllMonths( budgetType, - props.spreadsheet, + spreadsheet, { start, end }, - prewarmStartMonth, + startMonth, ); setInitialized(true); @@ -186,10 +157,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) { @@ -199,12 +166,10 @@ function BudgetInner(props: BudgetProps) { }, [props.accountId]); const onMonthSelect = async (month, numDisplayed) => { - setPrewarmStartMonth(month); + dispatch(savePrefs({ 'budget.startMonth': 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 @@ -214,51 +179,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 }); + dispatch(savePrefs({ 'budget.startMonth': 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(), @@ -272,24 +223,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)); } }; @@ -297,55 +240,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)); } }; @@ -361,27 +275,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( @@ -397,7 +309,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 @@ -412,23 +324,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 }); + dispatch(savePrefs({ 'budget.summaryCollapsed': !summaryCollapsed })); }; const onTitlebarEvent = async ({ type, payload }: TitlebarMessage) => { @@ -436,10 +340,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; } @@ -447,23 +353,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 = ( (props => { }); export function Budget() { - const startMonth = useLocalPref('budget.startMonth'); - const collapsedPrefs = useLocalPref('budget.collapsed'); - const summaryCollapsed = useLocalPref('budget.summaryCollapsed'); - const budgetType = useLocalPref('budgetType') || 'rollover'; - const maxMonths = useGlobalPref('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( () => ({ @@ -606,19 +472,9 @@ export function Budget() { }} > ); diff --git a/packages/desktop-client/src/components/budget/rollover/RolloverContext.tsx b/packages/desktop-client/src/components/budget/rollover/RolloverContext.tsx index 636953b16ac..fcbc714cd55 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({ { - saveGlobalPrefs({ floatingSidebar: !isFloating }); + dispatch(saveGlobalPrefs({ floatingSidebar: !isFloating })); }; const onAddAccount = () => { - replaceModal('add-account'); + dispatch(replaceModal('add-account')); }; const onToggleClosedAccounts = () => { - savePrefs({ - 'ui.showClosedAccounts': !prefs['ui.showClosedAccounts'], - }); + dispatch( + savePrefs({ + 'ui.showClosedAccounts': !showClosedAccounts, + }), + ); }; return ( @@ -104,7 +106,7 @@ export function Sidebar() { }), }} > - + @@ -140,14 +142,10 @@ export function Sidebar() { ); } -type EditableBudgetNameProps = { - prefs: LocalPrefs; - savePrefs: (prefs: Partial) => Promise; -}; - -function EditableBudgetName({ prefs, savePrefs }: EditableBudgetNameProps) { +function EditableBudgetName() { const dispatch = useDispatch(); const navigate = useNavigate(); + const budgetName = useLocalPref('budgetName'); const [editing, setEditing] = useState(false); const [menuOpen, setMenuOpen] = useState(false); @@ -187,14 +185,16 @@ function EditableBudgetName({ prefs, savePrefs }: EditableBudgetNameProps) { fontSize: 16, fontWeight: 500, }} - defaultValue={prefs.budgetName} + defaultValue={budgetName} onEnter={async e => { const inputEl = e.target as HTMLInputElement; const newBudgetName = inputEl.value; if (newBudgetName.trim() !== '') { - await savePrefs({ - budgetName: inputEl.value, - }); + await dispatch( + savePrefs({ + budgetName: inputEl.value, + }), + ); setEditing(false); } }} @@ -216,7 +216,7 @@ function EditableBudgetName({ prefs, savePrefs }: EditableBudgetNameProps) { onClick={() => setMenuOpen(true)} > - {prefs.budgetName || 'A budget has no name'} + {budgetName || 'A budget has no name'} {menuOpen && ( diff --git a/packages/desktop-client/src/components/transactions/MobileTransaction.jsx b/packages/desktop-client/src/components/transactions/MobileTransaction.jsx index eadbea53ca1..bbce43aee6d 100644 --- a/packages/desktop-client/src/components/transactions/MobileTransaction.jsx +++ b/packages/desktop-client/src/components/transactions/MobileTransaction.jsx @@ -942,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. // diff --git a/packages/loot-core/src/client/actions/account.ts b/packages/loot-core/src/client/actions/account.ts index c8349d84c57..e7f7f269b4b 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/state-types/modals.d.ts b/packages/loot-core/src/client/state-types/modals.d.ts index e25e38feb32..72cd5dbc54b 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/shared/categories.ts b/packages/loot-core/src/shared/categories.ts deleted file mode 100644 index 9e2c4c190aa..00000000000 --- 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); -} From 34f62f659856a9467084fd5f695426ca1c07de45 Mon Sep 17 00:00:00 2001 From: Joel Jeremy Marquez Date: Sun, 28 Jan 2024 01:50:29 -0800 Subject: [PATCH 09/32] Fix eslint errors --- packages/desktop-client/src/components/FinancesApp.tsx | 2 +- .../src/components/budget/DynamicBudgetTable.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/desktop-client/src/components/FinancesApp.tsx b/packages/desktop-client/src/components/FinancesApp.tsx index 775607d678d..d0f3f33f739 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, @@ -42,7 +43,6 @@ import { FloatableSidebar } from './sidebar'; import { SidebarProvider } from './sidebar/SidebarProvider'; import { Titlebar, TitlebarProvider } from './Titlebar'; import { TransactionEdit } from './transactions/MobileTransaction'; -import { useSelector } from 'react-redux'; function NarrowNotSupported({ redirectTo = '/budget', diff --git a/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx b/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx index 4084cf0ff57..b87adf0e07d 100644 --- a/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx +++ b/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx @@ -35,7 +35,7 @@ type DynamicBudgetTableInnerProps = { } & ComponentProps; const DynamicBudgetTableInner = forwardRef< - BudgetTable, + typeof BudgetTable, DynamicBudgetTableInnerProps >( ( @@ -103,7 +103,7 @@ const DynamicBudgetTableInner = forwardRef< ); export const DynamicBudgetTable = forwardRef< - BudgetTable, + typeof BudgetTable, DynamicBudgetTableInnerProps >((props, ref) => { return ( From 5cc60192722b4405ba2883969ca9f0bb8a7d7348 Mon Sep 17 00:00:00 2001 From: Joel Jeremy Marquez Date: Fri, 2 Feb 2024 23:51:03 -0800 Subject: [PATCH 10/32] Fix hooks deps --- packages/desktop-client/src/hooks/useAccount.ts | 2 +- packages/desktop-client/src/hooks/useDateFormat.ts | 2 +- packages/desktop-client/src/hooks/usePayee.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/desktop-client/src/hooks/useAccount.ts b/packages/desktop-client/src/hooks/useAccount.ts index 619c8522db7..e3da8f35e6b 100644 --- a/packages/desktop-client/src/hooks/useAccount.ts +++ b/packages/desktop-client/src/hooks/useAccount.ts @@ -4,5 +4,5 @@ import { useAccounts } from './useAccounts'; export function useAccount(id: string) { const accounts = useAccounts(); - return useMemo(() => accounts.find(a => a.id === id), [id]); + return useMemo(() => accounts.find(a => a.id === id), [id, accounts]); } diff --git a/packages/desktop-client/src/hooks/useDateFormat.ts b/packages/desktop-client/src/hooks/useDateFormat.ts index 93b1b30451e..e22293e202a 100644 --- a/packages/desktop-client/src/hooks/useDateFormat.ts +++ b/packages/desktop-client/src/hooks/useDateFormat.ts @@ -1,5 +1,5 @@ import { useSelector } from 'react-redux'; export function useDateFormat() { - return useSelector(state => state.prefs.local?.dateFormat || undefined); + return useSelector(state => state.prefs.local?.dateFormat); } diff --git a/packages/desktop-client/src/hooks/usePayee.ts b/packages/desktop-client/src/hooks/usePayee.ts index 56eefd0f9c6..2606c60a875 100644 --- a/packages/desktop-client/src/hooks/usePayee.ts +++ b/packages/desktop-client/src/hooks/usePayee.ts @@ -4,5 +4,5 @@ import { usePayees } from './usePayees'; export function usePayee(id: string) { const payees = usePayees(); - return useMemo(() => payees.find(p => p.id === id), [id]); + return useMemo(() => payees.find(p => p.id === id), [id, payees]); } From 02d37d7aa44f4073bc64d6306641793e160ea66e Mon Sep 17 00:00:00 2001 From: Joel Jeremy Marquez Date: Sat, 3 Feb 2024 00:04:28 -0800 Subject: [PATCH 11/32] Add useEffect --- packages/desktop-client/src/hooks/useAccounts.ts | 9 ++++++--- packages/desktop-client/src/hooks/useCategories.ts | 9 ++++++--- packages/desktop-client/src/hooks/usePayees.ts | 9 ++++++--- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/desktop-client/src/hooks/useAccounts.ts b/packages/desktop-client/src/hooks/useAccounts.ts index 98d32a37cef..bc74fb60932 100644 --- a/packages/desktop-client/src/hooks/useAccounts.ts +++ b/packages/desktop-client/src/hooks/useAccounts.ts @@ -1,3 +1,4 @@ +import { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { getAccounts } from 'loot-core/client/actions'; @@ -6,9 +7,11 @@ export function useAccounts() { const dispatch = useDispatch(); const accountLoaded = useSelector(state => state.queries.accountsLoaded); - if (!accountLoaded) { - dispatch(getAccounts()); - } + useEffect(() => { + if (!accountLoaded) { + dispatch(getAccounts()); + } + }, []); return useSelector(state => state.queries.accounts); } diff --git a/packages/desktop-client/src/hooks/useCategories.ts b/packages/desktop-client/src/hooks/useCategories.ts index 8ac1d9ff344..ecfd013464c 100644 --- a/packages/desktop-client/src/hooks/useCategories.ts +++ b/packages/desktop-client/src/hooks/useCategories.ts @@ -1,3 +1,4 @@ +import { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { getCategories } from 'loot-core/client/actions'; @@ -6,9 +7,11 @@ export function useCategories() { const dispatch = useDispatch(); const categoriesLoaded = useSelector(state => state.queries.categoriesLoaded); - if (!categoriesLoaded) { - dispatch(getCategories()); - } + useEffect(() => { + if (!categoriesLoaded) { + dispatch(getCategories()); + } + }, []); return useSelector( state => state.queries.categories, diff --git a/packages/desktop-client/src/hooks/usePayees.ts b/packages/desktop-client/src/hooks/usePayees.ts index b68665cc9bd..7892269866b 100644 --- a/packages/desktop-client/src/hooks/usePayees.ts +++ b/packages/desktop-client/src/hooks/usePayees.ts @@ -1,3 +1,4 @@ +import { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { getPayees } from 'loot-core/client/actions'; @@ -6,9 +7,11 @@ export function usePayees() { const dispatch = useDispatch(); const payeesLoaded = useSelector(state => state.queries.payeesLoaded); - if (!payeesLoaded) { - dispatch(getPayees()); - } + useEffect(() => { + if (!payeesLoaded) { + dispatch(getPayees()); + } + }, []); return useSelector(state => state.queries.payees); } From 262a036fec89bd8be767a2fcfbd44f15397259f6 Mon Sep 17 00:00:00 2001 From: Joel Jeremy Marquez Date: Wed, 7 Feb 2024 07:31:02 -0800 Subject: [PATCH 12/32] Fix typecheck error --- packages/desktop-client/src/hooks/useCategories.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/desktop-client/src/hooks/useCategories.ts b/packages/desktop-client/src/hooks/useCategories.ts index ecfd013464c..5fc58a5c668 100644 --- a/packages/desktop-client/src/hooks/useCategories.ts +++ b/packages/desktop-client/src/hooks/useCategories.ts @@ -13,7 +13,5 @@ export function useCategories() { } }, []); - return useSelector( - state => state.queries.categories, - ); + return useSelector(state => state.queries.categories); } From 4a1b878be1f2d883cab2c1cb8240da9596c388ff Mon Sep 17 00:00:00 2001 From: Joel Jeremy Marquez Date: Wed, 7 Feb 2024 22:08:16 -0800 Subject: [PATCH 13/32] Set local and global pref hooks --- .../desktop-client/src/components/App.tsx | 7 ++-- .../src/components/BankSyncStatus.tsx | 7 ++-- .../src/components/FinancesApp.tsx | 5 ++- .../src/components/LoggedInUser.tsx | 7 +--- .../src/components/MobileWebMessage.tsx | 16 +++----- .../desktop-client/src/components/Modals.tsx | 5 ++- .../src/components/Notifications.tsx | 11 ++--- .../src/components/PrivacyFilter.tsx | 3 +- .../src/components/Titlebar.tsx | 15 ++++--- .../src/components/UpdateNotification.tsx | 12 ++---- .../src/components/accounts/Account.jsx | 13 +++--- .../src/components/accounts/Header.jsx | 7 ++-- .../src/components/accounts/MobileAccount.jsx | 4 +- .../components/accounts/MobileAccounts.jsx | 6 +-- .../components/budget/BudgetCategories.jsx | 8 +--- .../src/components/budget/BudgetTable.jsx | 2 +- .../src/components/budget/MobileBudget.tsx | 9 ++-- .../components/budget/MobileBudgetTable.jsx | 41 ++++++++----------- .../src/components/budget/index.tsx | 20 +++++---- .../src/components/filters/AppliedFilters.tsx | 2 +- .../src/components/modals/LoadBackup.jsx | 2 +- .../components/modals/SwitchBudgetType.tsx | 2 +- .../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 | 23 +++++++---- .../reports/spreadsheets/filterEmptyRows.ts | 2 +- .../spreadsheets/grouped-spreadsheet.ts | 2 +- .../src/components/select/DateSelect.tsx | 2 +- .../src/components/settings/Encryption.tsx | 2 +- .../src/components/settings/Experimental.tsx | 14 +++---- .../src/components/settings/Export.tsx | 4 +- .../src/components/settings/Format.tsx | 26 ++++++------ .../src/components/settings/Global.tsx | 2 +- .../src/components/settings/Reset.tsx | 3 +- .../src/components/settings/index.tsx | 8 ++-- .../src/components/sidebar/Accounts.tsx | 2 +- .../src/components/sidebar/Sidebar.tsx | 29 ++++++------- .../components/sidebar/SidebarProvider.tsx | 2 +- .../src/components/sidebar/index.tsx | 2 +- .../desktop-client/src/hooks/useAccounts.ts | 7 +++- .../desktop-client/src/hooks/useCategories.ts | 7 +++- .../desktop-client/src/hooks/useDateFormat.ts | 4 +- .../src/hooks/useFailedAccounts.ts | 4 +- .../src/hooks/useFeatureFlag.ts | 17 ++++---- .../desktop-client/src/hooks/useGlobalPref.ts | 22 ++++++++-- .../desktop-client/src/hooks/useLocalPref.ts | 22 ++++++++-- .../desktop-client/src/hooks/useLocalPrefs.ts | 4 +- .../desktop-client/src/hooks/usePayees.ts | 7 +++- .../src/hooks/usePrivacyMode.ts | 9 ++++ .../desktop-client/src/hooks/useSelected.tsx | 7 +--- .../src/hooks/useSyncServerStatus.ts | 7 +--- .../src/hooks/useUpdatedAccounts.ts | 4 +- packages/desktop-client/src/style/theme.tsx | 3 +- packages/loot-core/src/client/privacy.ts | 5 --- packages/loot-core/src/types/prefs.d.ts | 1 + 57 files changed, 243 insertions(+), 222 deletions(-) create mode 100644 packages/desktop-client/src/hooks/usePrivacyMode.ts delete mode 100644 packages/loot-core/src/client/privacy.ts diff --git a/packages/desktop-client/src/components/App.tsx b/packages/desktop-client/src/components/App.tsx index 37d474dc739..8323f724721 100644 --- a/packages/desktop-client/src/components/App.tsx +++ b/packages/desktop-client/src/components/App.tsx @@ -8,6 +8,7 @@ import { import { useSelector } from 'react-redux'; import * as Platform from 'loot-core/src/client/platform'; +import { type State } from 'loot-core/src/client/state-types'; import { init as initConnection, send, @@ -36,7 +37,7 @@ type AppInnerProps = { function AppInner({ budgetId, cloudFileId }: AppInnerProps) { const [initializing, setInitializing] = useState(true); const { showBoundary: showErrorBoundary } = useErrorBoundary(); - const loadingText = useSelector(state => state.app.loadingText); + const loadingText = useSelector((state: State) => state.app.loadingText); const { loadBudget, closeBudget, loadGlobalPrefs } = useActions(); async function init() { @@ -110,8 +111,8 @@ function ErrorFallback({ error }: FallbackProps) { } export function App() { - const budgetId = useLocalPref('id'); - const cloudFileId = useLocalPref('cloudFileId'); + const [budgetId] = useLocalPref('id'); + const [cloudFileId] = useLocalPref('cloudFileId'); const { sync } = useActions(); const [hiddenScrollbars, setHiddenScrollbars] = useState( hasHiddenScrollbars(), diff --git a/packages/desktop-client/src/components/BankSyncStatus.tsx b/packages/desktop-client/src/components/BankSyncStatus.tsx index 3fde066fa4c..f49b9200749 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 => 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 d0f3f33f739..aa10744daca 100644 --- a/packages/desktop-client/src/components/FinancesApp.tsx +++ b/packages/desktop-client/src/components/FinancesApp.tsx @@ -15,6 +15,7 @@ import { import hotkeys from 'hotkeys-js'; 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'; @@ -75,7 +76,9 @@ function WideNotSupported({ children, redirectTo = '/budget' }) { function RouterBehaviors() { const navigate = useNavigate(); const accounts = useAccounts(); - const accountsLoaded = useSelector(state => state.queries.accountsLoaded); + const accountsLoaded = useSelector( + (state: State) => state.queries.accountsLoaded, + ); useEffect(() => { // If there are no accounts, we want to redirect the user to // the All Accounts screen which will prompt them to add an account diff --git a/packages/desktop-client/src/components/LoggedInUser.tsx b/packages/desktop-client/src/components/LoggedInUser.tsx index bfd037ad437..f1043b764db 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 => 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/MobileWebMessage.tsx b/packages/desktop-client/src/components/MobileWebMessage.tsx index 23e24941b29..5725921322d 100644 --- a/packages/desktop-client/src/components/MobileWebMessage.tsx +++ b/packages/desktop-client/src/components/MobileWebMessage.tsx @@ -1,9 +1,4 @@ import React, { useState } from 'react'; -import { useDispatch } 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'; @@ -17,25 +12,26 @@ import { Checkbox } from './forms'; const buttonStyle = { border: 0, fontSize: 15, padding: '10px 13px' }; export function MobileWebMessage() { - const hideMobileMessagePref = useLocalPref('hideMobileMessage') || true; + const [hideMobileMessage, setHideMobileMessagePref] = useLocalPref( + 'hideMobileMessage', + true, + ); 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 38a6c7996e6..56b599861d5 100644 --- a/packages/desktop-client/src/components/Modals.tsx +++ b/packages/desktop-client/src/components/Modals.tsx @@ -3,6 +3,7 @@ import React, { useEffect } from 'react'; import { useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; +import { type State } from 'loot-core/src/client/state-types'; import { type PopModalAction } from 'loot-core/src/client/state-types/modals'; import { send } from 'loot-core/src/platform/client/fetch'; @@ -49,8 +50,8 @@ export type CommonModalProps = { }; export function Modals() { - const modalStack = useSelector(state => state.modals.modalStack); - const isHidden = useSelector(state => state.modals.isHidden); + const modalStack = useSelector((state: State) => state.modals.modalStack); + const isHidden = useSelector((state: State) => state.modals.isHidden); const actions = useActions(); const location = useLocation(); diff --git a/packages/desktop-client/src/components/Notifications.tsx b/packages/desktop-client/src/components/Notifications.tsx index ef5db09f4fb..290b3ae7c90 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 => state.notifications.notifications, + const notifications = useSelector( + (state: State) => state.notifications.notifications, ); return ( savePrefs({ isPrivacyEnabled: !isPrivacyEnabled })} + onClick={() => setPrivacyEnabledPref(!isPrivacyEnabled)} style={style} > {isPrivacyEnabled ? ( @@ -146,7 +145,7 @@ type SyncButtonProps = { isMobile?: boolean; }; function SyncButton({ style, isMobile = false }: SyncButtonProps) { - const cloudFileId = useLocalPref('cloudFileId'); + const [cloudFileId] = useLocalPref('cloudFileId'); const { sync } = useActions(); const [syncing, setSyncing] = useState(false); @@ -286,8 +285,8 @@ function SyncButton({ style, isMobile = false }: SyncButtonProps) { } function BudgetTitlebar() { - const maxMonths = useGlobalPref('maxMonths'); - const budgetType = useLocalPref('budgetType'); + const [maxMonths] = useGlobalPref('maxMonths'); + const [budgetType] = useLocalPref('budgetType'); const { saveGlobalPrefs } = useActions(); const { sendEvent } = useContext(TitlebarContext); @@ -390,7 +389,7 @@ export function Titlebar({ style }: TitlebarProps) { const sidebar = useSidebar(); const { isNarrowWidth } = useResponsive(); const serverURL = useServerURL(); - const floatingSidebar = useGlobalPref('floatingSidebar'); + const [floatingSidebar] = useGlobalPref('floatingSidebar'); return isNarrowWidth ? null : ( ( - 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 3c5d1a2b95d..6212104c578 100644 --- a/packages/desktop-client/src/components/accounts/Account.jsx +++ b/packages/desktop-client/src/components/accounts/Account.jsx @@ -44,6 +44,7 @@ import { } from '../transactions/TransactionsTable'; import { AccountHeader } from './Header'; + function EmptyMessage({ onAdd }) { return ( state.modals.modalStack.length > 0); @@ -1564,7 +1565,7 @@ export function Account() { hideFraction, expandSplits, showBalances, - showCleared, + showCleared: !hideCleared, showExtraBalances, payees, modalShowing, diff --git a/packages/desktop-client/src/components/accounts/Header.jsx b/packages/desktop-client/src/components/accounts/Header.jsx index 4e181a5146c..80c1030ca21 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 8c640275037..cd72e12fc44 100644 --- a/packages/desktop-client/src/components/accounts/MobileAccount.jsx +++ b/packages/desktop-client/src/components/accounts/MobileAccount.jsx @@ -88,8 +88,8 @@ export function Account(props) { const newTransactions = useSelector(state => state.queries.newTransactions); const prefs = useLocalPrefs(); const dateFormat = useDateFormat() || 'MM/dd/yyyy'; - const numberFormat = useLocalPref('numberFormat') || 'comma-dot'; - const hideFraction = useLocalPref('hideFraction') || false; + const [numberFormat] = useLocalPref('numberFormat', 'comma-dot'); + const [hideFraction] = useLocalPref('hideFraction', false); const state = { payees, diff --git a/packages/desktop-client/src/components/accounts/MobileAccounts.jsx b/packages/desktop-client/src/components/accounts/MobileAccounts.jsx index 046e2511bfb..b69eecd3496 100644 --- a/packages/desktop-client/src/components/accounts/MobileAccounts.jsx +++ b/packages/desktop-client/src/components/accounts/MobileAccounts.jsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { replaceModal, syncAndDownload } from 'loot-core/client/actions'; +import { replaceModal, syncAndDownload } from 'loot-core/src/client/actions'; import * as queries from 'loot-core/src/client/queries'; import { useAccounts } from '../../hooks/useAccounts'; @@ -222,8 +222,8 @@ export function Accounts() { const accounts = useAccounts(); const newTransactions = useSelector(state => state.queries.newTransactions); const updatedAccounts = useSelector(state => state.queries.updatedAccounts); - const numberFormat = useLocalPref('numberFormat') || 'comma-dot'; - const hideFraction = useLocalPref('hideFraction') || false; + const [numberFormat] = useLocalPref('numberFormat', 'comma-dot'); + const [hideFraction] = useLocalPref('hideFraction', false); const { list: categories } = useCategories(); diff --git a/packages/desktop-client/src/components/budget/BudgetCategories.jsx b/packages/desktop-client/src/components/budget/BudgetCategories.jsx index fc3cc812eea..6b9e5343c7a 100644 --- a/packages/desktop-client/src/components/budget/BudgetCategories.jsx +++ b/packages/desktop-client/src/components/budget/BudgetCategories.jsx @@ -1,7 +1,4 @@ import React, { memo, useState, useMemo } from 'react'; -import { useDispatch } from 'react-redux'; - -import { savePrefs } from 'loot-core/client/actions'; import { useLocalPref } from '../../hooks/useLocalPref'; import { theme, styles } from '../../style'; @@ -35,10 +32,9 @@ export const BudgetCategories = memo( onReorderCategory, onReorderGroup, }) => { - const dispatch = useDispatch(); - const collapsed = useLocalPref('budget.collapsed') || []; + const [collapsed, setCollapsedPref] = useLocalPref('budget.collapsed', []); function onCollapse(value) { - dispatch(savePrefs({ 'budget.collapsed': value })); + setCollapsedPref(value); } const [isAddingGroup, setIsAddingGroup] = useState(false); diff --git a/packages/desktop-client/src/components/budget/BudgetTable.jsx b/packages/desktop-client/src/components/budget/BudgetTable.jsx index a11f0c382b5..a608cd1d853 100644 --- a/packages/desktop-client/src/components/budget/BudgetTable.jsx +++ b/packages/desktop-client/src/components/budget/BudgetTable.jsx @@ -1,7 +1,7 @@ import React, { createRef, Component } from 'react'; import { connect } from 'react-redux'; -import { savePrefs } from 'loot-core/client/actions'; +import { savePrefs } from 'loot-core/src/client/actions'; import * as monthUtils from 'loot-core/src/shared/months'; import { theme, styles } from '../../style'; diff --git a/packages/desktop-client/src/components/budget/MobileBudget.tsx b/packages/desktop-client/src/components/budget/MobileBudget.tsx index 5f1dd2a3558..9b2e8280c84 100644 --- a/packages/desktop-client/src/components/budget/MobileBudget.tsx +++ b/packages/desktop-client/src/components/budget/MobileBudget.tsx @@ -71,8 +71,8 @@ function BudgetInner(props: BudgetInnerProps) { const [initialized, setInitialized] = useState(false); const [editMode, setEditMode] = useState(false); - const numberFormat = useLocalPref('numberFormat') || 'comma-dot'; - const hideFraction = useLocalPref('hideFraction') || false; + const [numberFormat] = useLocalPref('numberFormat', 'comma-dot'); + const [hideFraction] = useLocalPref('hideFraction', false); useEffect(() => { async function init() { @@ -381,7 +381,7 @@ function BudgetInner(props: BudgetInnerProps) { { - return state.prefs?.local?.toggleMobileDisplayPref || true; - }); - - const showHiddenCategories = useSelector(state => { - return state.prefs?.local?.['budget.showHiddenCategories'] || false; - }); + const [showSpentColumn, setShowSpentColumnPref] = useLocalPref( + 'mobile.showSpentColumn', + false, + ); - const [showBudgetedCol, setShowBudgetedCol] = useState( - !mobileShowBudgetedColPref && - !document.cookie.match(/mobileShowBudgetedColPref=true/), + const [showHiddenCategories, setShowHiddenCategoriesPref] = useLocalPref( + 'budget.showHiddenCategories', + false, ); function toggleDisplay() { - setShowBudgetedCol(!showBudgetedCol); - if (!showBudgetedCol) { - savePrefs({ mobileShowBudgetedColPref: true }); - } + setShowSpentColumnPref(!showSpentColumn); } const buttonStyle = { @@ -1176,9 +1169,7 @@ export function BudgetTable({ }; const onToggleHiddenCategories = () => { - savePrefs({ - 'budget.showHiddenCategories': !showHiddenCategories, - }); + setShowHiddenCategoriesPref(!showHiddenCategories); }; return ( @@ -1244,7 +1235,7 @@ export function BudgetTable({ /> )} - {(show3Cols || showBudgetedCol) && ( + {(show3Cols || !showSpentColumn) && ( )} - {(show3Cols || !showBudgetedCol) && ( + {(show3Cols || showSpentColumn) && (