diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png index fa4ff40ee97ecb8e257683496581ccbe298620d7..e1ed15d720db70723d09bf378709c32a6c0559ee 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png index 4ac36791ac107c7087a25023cfa1651d2a316408..866d234e40ee46bb86bdb7bf2f37d07f5fec9b77 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png differ diff --git a/packages/desktop-client/src/components/App.tsx b/packages/desktop-client/src/components/App.tsx index 696c7633ea18a88a33fcb3f7de46189063317baf..8323f7247212e4009e4d586187831ea93c38ac02 100644 --- a/packages/desktop-client/src/components/App.tsx +++ b/packages/desktop-client/src/components/App.tsx @@ -7,17 +7,15 @@ import { } from 'react-error-boundary'; import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type AppState } from 'loot-core/client/state-types/app'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; import * as Platform from 'loot-core/src/client/platform'; +import { type State } from 'loot-core/src/client/state-types'; import { init as initConnection, send, } from 'loot-core/src/platform/client/fetch'; -import { type GlobalPrefs } from 'loot-core/src/types/prefs'; import { useActions } from '../hooks/useActions'; +import { useLocalPref } from '../hooks/useLocalPref'; import { installPolyfills } from '../polyfills'; import { ResponsiveProvider } from '../ResponsiveProvider'; import { styles, hasHiddenScrollbars, ThemeStyle } from '../style'; @@ -34,26 +32,13 @@ import { UpdateNotification } from './UpdateNotification'; type AppInnerProps = { budgetId: string; cloudFileId: string; - loadingText: string; - loadBudget: ( - id: string, - loadingText?: string, - options?: object, - ) => Promise<void>; - closeBudget: () => Promise<void>; - loadGlobalPrefs: () => Promise<GlobalPrefs>; }; -function AppInner({ - budgetId, - cloudFileId, - loadingText, - loadBudget, - closeBudget, - loadGlobalPrefs, -}: AppInnerProps) { +function AppInner({ budgetId, cloudFileId }: AppInnerProps) { const [initializing, setInitializing] = useState(true); const { showBoundary: showErrorBoundary } = useErrorBoundary(); + const loadingText = useSelector((state: State) => state.app.loadingText); + const { loadBudget, closeBudget, loadGlobalPrefs } = useActions(); async function init() { const socketName = await global.Actual.getServerSocket(); @@ -126,16 +111,9 @@ function ErrorFallback({ error }: FallbackProps) { } export function App() { - const budgetId = useSelector<State, PrefsState['local']['id']>( - state => state.prefs.local && state.prefs.local.id, - ); - const cloudFileId = useSelector<State, PrefsState['local']['cloudFileId']>( - state => state.prefs.local && state.prefs.local.cloudFileId, - ); - const loadingText = useSelector<State, AppState['loadingText']>( - state => state.app.loadingText, - ); - const { loadBudget, closeBudget, loadGlobalPrefs, sync } = useActions(); + const [budgetId] = useLocalPref('id'); + const [cloudFileId] = useLocalPref('cloudFileId'); + const { sync } = useActions(); const [hiddenScrollbars, setHiddenScrollbars] = useState( hasHiddenScrollbars(), ); @@ -184,14 +162,7 @@ export function App() { {process.env.REACT_APP_REVIEW_ID && !Platform.isPlaywright && ( <DevelopmentTopBar /> )} - <AppInner - budgetId={budgetId} - cloudFileId={cloudFileId} - loadingText={loadingText} - loadBudget={loadBudget} - closeBudget={closeBudget} - loadGlobalPrefs={loadGlobalPrefs} - /> + <AppInner budgetId={budgetId} cloudFileId={cloudFileId} /> </ErrorBoundary> <ThemeStyle /> </View> diff --git a/packages/desktop-client/src/components/BankSyncStatus.tsx b/packages/desktop-client/src/components/BankSyncStatus.tsx index 3fde066fa4cdafad903645761208277ccf76b4dc..f49b920074996ec221f9a588722b1555fe74ea0a 100644 --- a/packages/desktop-client/src/components/BankSyncStatus.tsx +++ b/packages/desktop-client/src/components/BankSyncStatus.tsx @@ -2,8 +2,7 @@ import React from 'react'; import { useSelector } from 'react-redux'; import { useTransition, animated } from 'react-spring'; -import { type State } from 'loot-core/client/state-types'; -import { type AccountState } from 'loot-core/client/state-types/account'; +import { type State } from 'loot-core/src/client/state-types'; import { theme, styles } from '../style'; @@ -12,8 +11,8 @@ import { Text } from './common/Text'; import { View } from './common/View'; export function BankSyncStatus() { - const accountsSyncing = useSelector<State, AccountState['accountsSyncing']>( - state => state.account.accountsSyncing, + const accountsSyncing = useSelector( + (state: State) => state.account.accountsSyncing, ); const name = accountsSyncing diff --git a/packages/desktop-client/src/components/FinancesApp.tsx b/packages/desktop-client/src/components/FinancesApp.tsx index 29c241a14da54837bb21f34adb8d09310957ca47..8024afe7957e39cffa03a62aca42f5d0105b47d8 100644 --- a/packages/desktop-client/src/components/FinancesApp.tsx +++ b/packages/desktop-client/src/components/FinancesApp.tsx @@ -2,6 +2,7 @@ import React, { type ReactElement, useEffect, useMemo } from 'react'; import { DndProvider } from 'react-dnd'; import { HTML5Backend as Backend } from 'react-dnd-html5-backend'; +import { useSelector } from 'react-redux'; import { Route, Routes, @@ -13,12 +14,12 @@ import { import hotkeys from 'hotkeys-js'; -import { AccountsProvider } from 'loot-core/src/client/data-hooks/accounts'; -import { PayeesProvider } from 'loot-core/src/client/data-hooks/payees'; import { SpreadsheetProvider } from 'loot-core/src/client/SpreadsheetProvider'; +import { type State } from 'loot-core/src/client/state-types'; import { checkForUpdateNotification } from 'loot-core/src/client/update-notification'; import * as undo from 'loot-core/src/platform/client/undo'; +import { useAccounts } from '../hooks/useAccounts'; import { useActions } from '../hooks/useActions'; import { useNavigate } from '../hooks/useNavigate'; import { useResponsive } from '../ResponsiveProvider'; @@ -39,7 +40,8 @@ import { Reports } from './reports'; import { NarrowAlternate, WideComponent } from './responsive'; import { ScrollProvider } from './ScrollProvider'; import { Settings } from './settings'; -import { FloatableSidebar, SidebarProvider } from './sidebar'; +import { FloatableSidebar } from './sidebar'; +import { SidebarProvider } from './sidebar/SidebarProvider'; import { Titlebar, TitlebarProvider } from './Titlebar'; import { TransactionEdit } from './transactions/MobileTransaction'; @@ -71,18 +73,19 @@ function WideNotSupported({ children, redirectTo = '/budget' }) { return isNarrowWidth ? children : null; } -function RouterBehaviors({ getAccounts }) { +function RouterBehaviors() { const navigate = useNavigate(); + const accounts = useAccounts(); + const accountsLoaded = useSelector( + (state: State) => state.queries.accountsLoaded, + ); useEffect(() => { - // Get the accounts and check if any exist. If there are no - // accounts, we want to redirect the user to the All Accounts - // screen which will prompt them to add an account - getAccounts().then(accounts => { - if (accounts.length === 0) { - navigate('/accounts'); - } - }); - }, []); + // If there are no accounts, we want to redirect the user to + // the All Accounts screen which will prompt them to add an account + if (accountsLoaded && accounts.length === 0) { + navigate('/accounts'); + } + }, [accountsLoaded, accounts]); const location = useLocation(); const href = useHref(location); @@ -116,7 +119,7 @@ function FinancesAppWithoutContext() { return ( <BrowserRouter> - <RouterBehaviors getAccounts={actions.getAccounts} /> + <RouterBehaviors /> <ExposeNavigate /> <View style={{ height: '100%' }}> @@ -265,13 +268,9 @@ export function FinancesApp() { <TitlebarProvider> <SidebarProvider> <BudgetMonthCountProvider> - <PayeesProvider> - <AccountsProvider> - <DndProvider backend={Backend}> - <ScrollProvider>{app}</ScrollProvider> - </DndProvider> - </AccountsProvider> - </PayeesProvider> + <DndProvider backend={Backend}> + <ScrollProvider>{app}</ScrollProvider> + </DndProvider> </BudgetMonthCountProvider> </SidebarProvider> </TitlebarProvider> diff --git a/packages/desktop-client/src/components/LoggedInUser.tsx b/packages/desktop-client/src/components/LoggedInUser.tsx index bfd037ad437cb0653b395e728f0c998225df787e..f1043b764db27d5aa12b18709a9a067d4b6c19dc 100644 --- a/packages/desktop-client/src/components/LoggedInUser.tsx +++ b/packages/desktop-client/src/components/LoggedInUser.tsx @@ -2,8 +2,7 @@ import React, { useState, useEffect } from 'react'; import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type UserState } from 'loot-core/client/state-types/user'; +import { type State } from 'loot-core/src/client/state-types'; import { useActions } from '../hooks/useActions'; import { theme, styles, type CSSProperties } from '../style'; @@ -25,9 +24,7 @@ export function LoggedInUser({ style, color, }: LoggedInUserProps) { - const userData = useSelector<State, UserState['data']>( - state => state.user.data, - ); + const userData = useSelector((state: State) => state.user.data); const { getUserData, signOut, closeBudget } = useActions(); const [loading, setLoading] = useState(true); const [menuOpen, setMenuOpen] = useState(false); diff --git a/packages/desktop-client/src/components/ManageRules.tsx b/packages/desktop-client/src/components/ManageRules.tsx index 38634a9899a0be7ae8e893c8736e7c8abb3e5b6c..2459fcb44a1415acf3e0b5fef1af3aa7a6ac385d 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 bc3e0c7ac93ece0f412ca5cfde33f056b10c5772..c54da6eedf105995c7738f3877896aef740453ce 100644 --- a/packages/desktop-client/src/components/MobileWebMessage.tsx +++ b/packages/desktop-client/src/components/MobileWebMessage.tsx @@ -1,10 +1,6 @@ import React, { useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; - -import { savePrefs } from 'loot-core/src/client/actions'; -import { type State } from 'loot-core/src/client/state-types'; -import { type PrefsState } from 'loot-core/src/client/state-types/prefs'; +import { useLocalPref } from '../hooks/useLocalPref'; import { useResponsive } from '../ResponsiveProvider'; import { theme, styles } from '../style'; @@ -16,30 +12,24 @@ import { Checkbox } from './forms'; const buttonStyle = { border: 0, fontSize: 15, padding: '10px 13px' }; export function MobileWebMessage() { - const hideMobileMessagePref = useSelector< - State, - PrefsState['local']['hideMobileMessage'] - >(state => { - return (state.prefs.local && state.prefs.local.hideMobileMessage) || true; - }); + const [hideMobileMessage = true, setHideMobileMessagePref] = + useLocalPref('hideMobileMessage'); const { isNarrowWidth } = useResponsive(); const [show, setShow] = useState( isNarrowWidth && - !hideMobileMessagePref && + !hideMobileMessage && !document.cookie.match(/hideMobileMessage=true/), ); const [requestDontRemindMe, setRequestDontRemindMe] = useState(false); - const dispatch = useDispatch(); - function onTry() { setShow(false); if (requestDontRemindMe) { // remember the pref indefinitely - dispatch(savePrefs({ hideMobileMessage: true })); + setHideMobileMessagePref(true); } else { // Set a cookie for 5 minutes const d = new Date(); diff --git a/packages/desktop-client/src/components/Modals.tsx b/packages/desktop-client/src/components/Modals.tsx index 93ae55592185211ae2f9de170903b2678d5fc064..56b599861d5110b833c45198fff611e8534ef2e6 100644 --- a/packages/desktop-client/src/components/Modals.tsx +++ b/packages/desktop-client/src/components/Modals.tsx @@ -4,16 +4,10 @@ import { useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; import { type State } from 'loot-core/src/client/state-types'; -import { - type ModalsState, - type PopModalAction, -} from 'loot-core/src/client/state-types/modals'; -import { type PrefsState } from 'loot-core/src/client/state-types/prefs'; -import { type QueriesState } from 'loot-core/src/client/state-types/queries'; +import { type PopModalAction } from 'loot-core/src/client/state-types/modals'; import { send } from 'loot-core/src/platform/client/fetch'; import { useActions } from '../hooks/useActions'; -import { useCategories } from '../hooks/useCategories'; import { useSyncServerStatus } from '../hooks/useSyncServerStatus'; import { CategoryGroupMenu } from './modals/CategoryGroupMenu'; @@ -56,19 +50,8 @@ export type CommonModalProps = { }; export function Modals() { - const modalStack = useSelector<State, ModalsState['modalStack']>( - state => state.modals.modalStack, - ); - const isHidden = useSelector<State, ModalsState['isHidden']>( - state => state.modals.isHidden, - ); - const accounts = useSelector<State, QueriesState['accounts']>( - state => state.queries.accounts, - ); - const { grouped: categoryGroups, list: categories } = useCategories(); - const budgetId = useSelector<State, PrefsState['local']['id']>( - state => state.prefs.local && state.prefs.local.id, - ); + const modalStack = useSelector((state: State) => state.modals.modalStack); + const isHidden = useSelector((state: State) => state.modals.isHidden); const actions = useActions(); const location = useLocation(); @@ -118,8 +101,6 @@ export function Modals() { account={options.account} balance={options.balance} canDelete={options.canDelete} - accounts={accounts.filter(acct => acct.closed === 0)} - categoryGroups={categoryGroups} actions={actions} /> ); @@ -130,7 +111,6 @@ export function Modals() { modalProps={modalProps} externalAccounts={options.accounts} requisitionId={options.requisitionId} - localAccounts={accounts.filter(acct => acct.closed === 0)} actions={actions} syncSource={options.syncSource} /> @@ -140,15 +120,8 @@ export function Modals() { return ( <ConfirmCategoryDelete modalProps={modalProps} - category={ - 'category' in options && - categories.find(c => c.id === options.category) - } - group={ - 'group' in options && - categoryGroups.find(g => g.id === options.group) - } - categoryGroups={categoryGroups} + category={options.category} + group={options.group} onDelete={options.onDelete} /> ); @@ -166,7 +139,7 @@ export function Modals() { return ( <LoadBackup watchUpdates - budgetId={budgetId} + budgetId={options.budgetId} modalProps={modalProps} actions={actions} backupDisabled={false} diff --git a/packages/desktop-client/src/components/Notifications.tsx b/packages/desktop-client/src/components/Notifications.tsx index ef5db09f4fbbfbd45391c985f453ed19e72d3541..290b3ae7c908b13520ecfdd52ec7fdea6cca5112 100644 --- a/packages/desktop-client/src/components/Notifications.tsx +++ b/packages/desktop-client/src/components/Notifications.tsx @@ -7,11 +7,8 @@ import React, { } from 'react'; import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import type { - NotificationWithId, - NotificationsState, -} from 'loot-core/src/client/state-types/notifications'; +import { type State } from 'loot-core/src/client/state-types'; +import type { NotificationWithId } from 'loot-core/src/client/state-types/notifications'; import { useActions } from '../hooks/useActions'; import { AnimatedLoading } from '../icons/AnimatedLoading'; @@ -242,8 +239,8 @@ function Notification({ export function Notifications({ style }: { style?: CSSProperties }) { const { removeNotification } = useActions(); - const notifications = useSelector<State, NotificationsState['notifications']>( - state => state.notifications.notifications, + const notifications = useSelector( + (state: State) => state.notifications.notifications, ); return ( <View diff --git a/packages/desktop-client/src/components/PrivacyFilter.tsx b/packages/desktop-client/src/components/PrivacyFilter.tsx index f8596b7c86d5da3be9165c72e2a4addf646fe5f5..5a455dc60dd332c013ed8f1f424e96162a3c9ebd 100644 --- a/packages/desktop-client/src/components/PrivacyFilter.tsx +++ b/packages/desktop-client/src/components/PrivacyFilter.tsx @@ -7,8 +7,7 @@ import React, { type ReactNode, } from 'react'; -import { usePrivacyMode } from 'loot-core/src/client/privacy'; - +import { usePrivacyMode } from '../hooks/usePrivacyMode'; import { useResponsive } from '../ResponsiveProvider'; import { View } from './common/View'; diff --git a/packages/desktop-client/src/components/ThemeSelector.tsx b/packages/desktop-client/src/components/ThemeSelector.tsx index e8daef9d686fda3c670495be2390ba00c9789288..415089cb1a456af216db0e486a571b007bf5f96e 100644 --- a/packages/desktop-client/src/components/ThemeSelector.tsx +++ b/packages/desktop-client/src/components/ThemeSelector.tsx @@ -2,7 +2,6 @@ import React, { useState } from 'react'; import type { Theme } from 'loot-core/src/types/prefs'; -import { useActions } from '../hooks/useActions'; import { SvgMoonStars, SvgSun, SvgSystem } from '../icons/v2'; import { useResponsive } from '../ResponsiveProvider'; import { type CSSProperties, themeOptions, useTheme } from '../style'; @@ -16,8 +15,7 @@ type ThemeSelectorProps = { }; export function ThemeSelector({ style }: ThemeSelectorProps) { - const theme = useTheme(); - const { saveGlobalPrefs } = useActions(); + const [theme, switchTheme] = useTheme(); const [menuOpen, setMenuOpen] = useState(false); const { isNarrowWidth } = useResponsive(); @@ -28,12 +26,9 @@ export function ThemeSelector({ style }: ThemeSelectorProps) { auto: SvgSystem, } as const; - async function onMenuSelect(newTheme: string) { + function onMenuSelect(newTheme: Theme) { setMenuOpen(false); - - saveGlobalPrefs({ - theme: newTheme as Theme, - }); + switchTheme(newTheme); } const Icon = themeIcons[theme] || SvgSun; diff --git a/packages/desktop-client/src/components/Titlebar.tsx b/packages/desktop-client/src/components/Titlebar.tsx index b532412dc29e8bd321610b2728939511f8e1f910..8a08b283f350e3b60550b4a66176ef6413426389 100644 --- a/packages/desktop-client/src/components/Titlebar.tsx +++ b/packages/desktop-client/src/components/Titlebar.tsx @@ -6,11 +6,8 @@ import React, { useContext, type ReactNode, } from 'react'; -import { useSelector } from 'react-redux'; import { Routes, Route, useLocation } from 'react-router-dom'; -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; import * as Platform from 'loot-core/src/client/platform'; import * as queries from 'loot-core/src/client/queries'; import { listen } from 'loot-core/src/platform/client/fetch'; @@ -18,6 +15,8 @@ import { type LocalPrefs } from 'loot-core/src/types/prefs'; import { useActions } from '../hooks/useActions'; import { useFeatureFlag } from '../hooks/useFeatureFlag'; +import { useGlobalPref } from '../hooks/useGlobalPref'; +import { useLocalPref } from '../hooks/useLocalPref'; import { useNavigate } from '../hooks/useNavigate'; import { SvgArrowLeft } from '../icons/v1'; import { @@ -41,7 +40,7 @@ import { View } from './common/View'; import { KeyHandlers } from './KeyHandlers'; import { LoggedInUser } from './LoggedInUser'; import { useServerURL } from './ServerContext'; -import { useSidebar } from './sidebar'; +import { useSidebar } from './sidebar/SidebarProvider'; import { useSheetValue } from './spreadsheet/useSheetValue'; import { ThemeSelector } from './ThemeSelector'; import { Tooltip } from './tooltips'; @@ -120,11 +119,8 @@ type PrivacyButtonProps = { }; function PrivacyButton({ style }: PrivacyButtonProps) { - const isPrivacyEnabled = useSelector< - State, - PrefsState['local']['isPrivacyEnabled'] - >(state => state.prefs.local?.isPrivacyEnabled); - const { savePrefs } = useActions(); + const [isPrivacyEnabled, setPrivacyEnabledPref] = + useLocalPref('isPrivacyEnabled'); const privacyIconStyle = { width: 15, height: 15 }; @@ -132,7 +128,7 @@ function PrivacyButton({ style }: PrivacyButtonProps) { <Button type="bare" aria-label={`${isPrivacyEnabled ? 'Disable' : 'Enable'} privacy mode`} - onClick={() => savePrefs({ isPrivacyEnabled: !isPrivacyEnabled })} + onClick={() => setPrivacyEnabledPref(!isPrivacyEnabled)} style={style} > {isPrivacyEnabled ? ( @@ -149,9 +145,7 @@ type SyncButtonProps = { isMobile?: boolean; }; function SyncButton({ style, isMobile = false }: SyncButtonProps) { - const cloudFileId = useSelector<State, PrefsState['local']['cloudFileId']>( - state => state.prefs.local?.cloudFileId, - ); + const [cloudFileId] = useLocalPref('cloudFileId'); const { sync } = useActions(); const [syncing, setSyncing] = useState(false); @@ -291,13 +285,8 @@ function SyncButton({ style, isMobile = false }: SyncButtonProps) { } function BudgetTitlebar() { - const maxMonths = useSelector<State, PrefsState['global']['maxMonths']>( - state => state.prefs.global?.maxMonths, - ); - const budgetType = useSelector<State, PrefsState['local']['budgetType']>( - state => state.prefs.local?.budgetType, - ); - const { saveGlobalPrefs } = useActions(); + const [maxMonths, setMaxMonthsPref] = useGlobalPref('maxMonths'); + const [budgetType] = useLocalPref('budgetType'); const { sendEvent } = useContext(TitlebarContext); const [loading, setLoading] = useState(false); @@ -326,7 +315,7 @@ function BudgetTitlebar() { <View style={{ flexDirection: 'row', alignItems: 'center' }}> <MonthCountSelector maxMonths={maxMonths || 1} - onChange={value => saveGlobalPrefs({ maxMonths: value })} + onChange={value => setMaxMonthsPref(value)} /> {reportBudgetEnabled && ( <View style={{ marginLeft: -5 }}> @@ -399,10 +388,7 @@ export function Titlebar({ style }: TitlebarProps) { const sidebar = useSidebar(); const { isNarrowWidth } = useResponsive(); const serverURL = useServerURL(); - const floatingSidebar = useSelector< - State, - PrefsState['global']['floatingSidebar'] - >(state => state.prefs.global?.floatingSidebar); + const [floatingSidebar] = useGlobalPref('floatingSidebar'); return isNarrowWidth ? null : ( <View diff --git a/packages/desktop-client/src/components/UpdateNotification.tsx b/packages/desktop-client/src/components/UpdateNotification.tsx index 18c9924662e7f1846e84919beee00504b2146684..f7052502880af3f4a6f5d0be50c2fae25a98f82b 100644 --- a/packages/desktop-client/src/components/UpdateNotification.tsx +++ b/packages/desktop-client/src/components/UpdateNotification.tsx @@ -1,8 +1,7 @@ import React from 'react'; import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type AppState } from 'loot-core/client/state-types/app'; +import { type State } from 'loot-core/src/client/state-types'; import { useActions } from '../hooks/useActions'; import { SvgClose } from '../icons/v1'; @@ -14,13 +13,10 @@ import { Text } from './common/Text'; import { View } from './common/View'; export function UpdateNotification() { - const updateInfo = useSelector<State, AppState['updateInfo']>( - state => state.app.updateInfo, + const updateInfo = useSelector((state: State) => state.app.updateInfo); + const showUpdateNotification = useSelector( + (state: State) => state.app.showUpdateNotification, ); - const showUpdateNotification = useSelector< - State, - AppState['showUpdateNotification'] - >(state => state.app.showUpdateNotification); const { updateApp, setAppState } = useActions(); diff --git a/packages/desktop-client/src/components/accounts/Account.jsx b/packages/desktop-client/src/components/accounts/Account.jsx index 1fb7e32a11619d83ac1fc93446cf90731a2085d1..48f30a99a4490c7de247f868a0f8dcacc8830a4b 100644 --- a/packages/desktop-client/src/components/accounts/Account.jsx +++ b/packages/desktop-client/src/components/accounts/Account.jsx @@ -26,7 +26,12 @@ import { } from 'loot-core/src/shared/transactions'; import { applyChanges, groupById } from 'loot-core/src/shared/util'; +import { useAccounts } from '../../hooks/useAccounts'; import { useCategories } from '../../hooks/useCategories'; +import { useDateFormat } from '../../hooks/useDateFormat'; +import { useFailedAccounts } from '../../hooks/useFailedAccounts'; +import { useLocalPref } from '../../hooks/useLocalPref'; +import { usePayees } from '../../hooks/usePayees'; import { SelectedProviderWithItems } from '../../hooks/useSelected'; import { styles, theme } from '../../style'; import { Button } from '../common/Button'; @@ -1532,23 +1537,41 @@ export function Account() { const location = useLocation(); const { grouped: categoryGroups } = useCategories(); - const state = useSelector(state => ({ - newTransactions: state.queries.newTransactions, - matchedTransactions: state.queries.matchedTransactions, - accounts: state.queries.accounts, - failedAccounts: state.account.failedAccounts, - dateFormat: state.prefs.local.dateFormat || 'MM/dd/yyyy', - hideFraction: state.prefs.local.hideFraction || false, - expandSplits: state.prefs.local['expand-splits'], - showBalances: params.id && state.prefs.local['show-balances-' + params.id], - showCleared: params.id && !state.prefs.local['hide-cleared-' + params.id], - showExtraBalances: - state.prefs.local['show-extra-balances-' + params.id || 'all-accounts'], - payees: state.queries.payees, - modalShowing: state.modals.modalStack.length > 0, - accountsSyncing: state.account.accountsSyncing, - lastUndoState: state.app.lastUndoState, - })); + const newTransactions = useSelector(state => state.queries.newTransactions); + const matchedTransactions = useSelector( + state => state.queries.matchedTransactions, + ); + const accounts = useAccounts(); + const payees = usePayees(); + const failedAccounts = useFailedAccounts(); + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; + const [hideFraction = false] = useLocalPref('hideFraction'); + const [expandSplits] = useLocalPref('expand-splits'); + const [showBalances] = useLocalPref(`show-balances-${params.id}`); + const [hideCleared] = useLocalPref(`hide-cleared-${params.id}`); + const [showExtraBalances] = useLocalPref( + `show-extra-balances-${params.id || 'all-accounts'}`, + ); + const modalShowing = useSelector(state => state.modals.modalStack.length > 0); + const accountsSyncing = useSelector(state => state.account.accountsSyncing); + const lastUndoState = useSelector(state => state.app.lastUndoState); + + const state = { + newTransactions, + matchedTransactions, + accounts, + failedAccounts, + dateFormat, + hideFraction, + expandSplits, + showBalances, + showCleared: !hideCleared, + showExtraBalances, + payees, + modalShowing, + accountsSyncing, + lastUndoState, + }; const dispatch = useDispatch(); const filtersList = useFilters(); diff --git a/packages/desktop-client/src/components/accounts/AccountSyncCheck.jsx b/packages/desktop-client/src/components/accounts/AccountSyncCheck.jsx index 423c99b7c73006d27b3b7812ad4f26743d40e4ec..09180381df0d24b5c7b256505c014b3b5c4b9cf2 100644 --- a/packages/desktop-client/src/components/accounts/AccountSyncCheck.jsx +++ b/packages/desktop-client/src/components/accounts/AccountSyncCheck.jsx @@ -3,6 +3,7 @@ import { useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; import { authorizeBank } from '../../gocardless'; +import { useAccounts } from '../../hooks/useAccounts'; import { useActions } from '../../hooks/useActions'; import { SvgExclamationOutline } from '../../icons/v1'; import { theme } from '../../style'; @@ -49,7 +50,7 @@ function getErrorMessage(type, code) { } export function AccountSyncCheck() { - const accounts = useSelector(state => state.queries.accounts); + const accounts = useAccounts(); const failedAccounts = useSelector(state => state.account.failedAccounts); const { unlinkAccount, pushModal } = useActions(); diff --git a/packages/desktop-client/src/components/accounts/Header.jsx b/packages/desktop-client/src/components/accounts/Header.jsx index 4e181a5146c4449d06e0a24a8302793e367c45fd..80c1030ca219e0148c0762036a18a5a24de02c68 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 b8d4bb6ce772263468d6e385c6217c6b889e1288..0b06e280e08fe92f3a69620660a239a40f5b0348 100644 --- a/packages/desktop-client/src/components/accounts/MobileAccount.jsx +++ b/packages/desktop-client/src/components/accounts/MobileAccount.jsx @@ -19,8 +19,13 @@ import { ungroupTransactions, } from 'loot-core/src/shared/transactions'; +import { useAccounts } from '../../hooks/useAccounts'; import { useCategories } from '../../hooks/useCategories'; +import { useDateFormat } from '../../hooks/useDateFormat'; +import { useLocalPref } from '../../hooks/useLocalPref'; +import { useLocalPrefs } from '../../hooks/useLocalPrefs'; import { useNavigate } from '../../hooks/useNavigate'; +import { usePayees } from '../../hooks/usePayees'; import { useSetThemeColor } from '../../hooks/useSetThemeColor'; import { theme, styles } from '../../style'; import { Button } from '../common/Button'; @@ -72,19 +77,27 @@ function PreviewTransactions({ children }) { let paged; export function Account(props) { - const accounts = useSelector(state => state.queries.accounts); + const accounts = useAccounts(); + const payees = usePayees(); const navigate = useNavigate(); const [transactions, setTransactions] = useState([]); const [searchText, setSearchText] = useState(''); const [currentQuery, setCurrentQuery] = useState(); - const state = useSelector(state => ({ - payees: state.queries.payees, - newTransactions: state.queries.newTransactions, - prefs: state.prefs.local, - dateFormat: state.prefs.local.dateFormat || 'MM/dd/yyyy', - })); + const newTransactions = useSelector(state => state.queries.newTransactions); + const prefs = useLocalPrefs(); + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; + const [_numberFormat] = useLocalPref('numberFormat'); + const numberFormat = _numberFormat || 'comma-dot'; + const [hideFraction = false] = useLocalPref('hideFraction'); + + const state = { + payees, + newTransactions, + prefs, + dateFormat, + }; const dispatch = useDispatch(); const actionCreators = useMemo( @@ -134,11 +147,6 @@ export function Account(props) { } }); - if (accounts.length === 0) { - await actionCreators.getAccounts(); - } - - await actionCreators.initiallyLoadPayees(); await fetchTransactions(); actionCreators.markAccountRead(accountId); @@ -216,8 +224,6 @@ export function Account(props) { const balance = queries.accountBalance(account); const balanceCleared = queries.accountBalanceCleared(account); const balanceUncleared = queries.accountBalanceUncleared(account); - const numberFormat = state.prefs.numberFormat || 'comma-dot'; - const hideFraction = state.prefs.hideFraction || false; return ( <SchedulesProvider diff --git a/packages/desktop-client/src/components/accounts/MobileAccounts.jsx b/packages/desktop-client/src/components/accounts/MobileAccounts.jsx index 4087fdbe0d03ddc65ae71a94df511f3de20ba94a..f056629501eb2d3345e269edb3be9a9b88c4f19d 100644 --- a/packages/desktop-client/src/components/accounts/MobileAccounts.jsx +++ b/packages/desktop-client/src/components/accounts/MobileAccounts.jsx @@ -1,10 +1,12 @@ -import React, { useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; +import React, { useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { replaceModal, syncAndDownload } from 'loot-core/src/client/actions'; import * as queries from 'loot-core/src/client/queries'; -import { useActions } from '../../hooks/useActions'; +import { useAccounts } from '../../hooks/useAccounts'; import { useCategories } from '../../hooks/useCategories'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { useNavigate } from '../../hooks/useNavigate'; import { useSetThemeColor } from '../../hooks/useSetThemeColor'; import { SvgAdd } from '../../icons/v1'; @@ -221,26 +223,19 @@ function AccountList({ } export function Accounts() { - const accounts = useSelector(state => state.queries.accounts); + const dispatch = useDispatch(); + const accounts = useAccounts(); const newTransactions = useSelector(state => state.queries.newTransactions); const updatedAccounts = useSelector(state => state.queries.updatedAccounts); - const numberFormat = useSelector( - state => state.prefs.local.numberFormat || 'comma-dot', - ); - const hideFraction = useSelector( - state => state.prefs.local.hideFraction || false, - ); + const [_numberFormat] = useLocalPref('numberFormat'); + const numberFormat = _numberFormat || 'comma-dot'; + const [hideFraction = false] = useLocalPref('hideFraction'); const { list: categories } = useCategories(); - const { getAccounts, replaceModal, syncAndDownload } = useActions(); const transactions = useState({}); const navigate = useNavigate(); - useEffect(() => { - (async () => getAccounts())(); - }, []); - const onSelectAccount = id => { navigate(`/accounts/${id}`); }; @@ -249,6 +244,14 @@ export function Accounts() { navigate(`/transaction/${transaction}`); }; + const onAddAccount = () => { + dispatch(replaceModal('add-account')); + }; + + const onSync = () => { + dispatch(syncAndDownload()); + }; + useSetThemeColor(theme.mobileViewTheme); return ( @@ -265,10 +268,10 @@ export function Accounts() { getBalanceQuery={queries.accountBalance} getOnBudgetBalance={queries.budgetedAccountBalance} getOffBudgetBalance={queries.offbudgetAccountBalance} - onAddAccount={() => replaceModal('add-account')} + onAddAccount={onAddAccount} onSelectAccount={onSelectAccount} onSelectTransaction={onSelectTransaction} - onSync={syncAndDownload} + onSync={onSync} /> </View> ); diff --git a/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx b/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx index f296e7b297fdafd9e25b3d6c809b2b54dee96198..778480a7dff82db286cef18c2cdcdeceb399e3c8 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 b99009b32270be84972f6fb896106af779fa5ed1..84ee34bfd79d3967db97247d4037d7ef4fa3c7ad 100644 --- a/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.tsx +++ b/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.tsx @@ -13,14 +13,14 @@ import { useDispatch } from 'react-redux'; import { css } from 'glamor'; import { createPayee } from 'loot-core/src/client/actions/queries'; -import { useCachedAccounts } from 'loot-core/src/client/data-hooks/accounts'; -import { useCachedPayees } from 'loot-core/src/client/data-hooks/payees'; import { getActivePayees } from 'loot-core/src/client/reducers/queries'; import { type AccountEntity, type PayeeEntity, } from 'loot-core/src/types/models'; +import { useAccounts } from '../../hooks/useAccounts'; +import { usePayees } from '../../hooks/usePayees'; import { SvgAdd } from '../../icons/v1'; import { useResponsive } from '../../ResponsiveProvider'; import { type CSSProperties, theme } from '../../style'; @@ -187,12 +187,12 @@ export function PayeeAutocomplete({ payees, ...props }: PayeeAutocompleteProps) { - const cachedPayees = useCachedPayees(); + const retrievedPayees = usePayees(); if (!payees) { - payees = cachedPayees; + payees = retrievedPayees; } - const cachedAccounts = useCachedAccounts(); + const cachedAccounts = useAccounts(); if (!accounts) { accounts = cachedAccounts; } diff --git a/packages/desktop-client/src/components/budget/BudgetCategories.jsx b/packages/desktop-client/src/components/budget/BudgetCategories.jsx index c80bfb2ab354aaef372b4766ee7aea22f2daa550..51183003fcd1f0171b49ae546f02f8e81744a6c3 100644 --- a/packages/desktop-client/src/components/budget/BudgetCategories.jsx +++ b/packages/desktop-client/src/components/budget/BudgetCategories.jsx @@ -1,5 +1,6 @@ import React, { memo, useState, useMemo } from 'react'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { theme, styles } from '../../style'; import { View } from '../common/View'; import { DropHighlightPosContext } from '../sort'; @@ -17,12 +18,7 @@ import { separateGroups } from './util'; export const BudgetCategories = memo( ({ categoryGroups, - newCategoryForGroup, - showHiddenCategories, - isAddingGroup, editingCell, - collapsed, - setCollapsed, dataComponents, onBudgetAction, onShowActivity, @@ -34,11 +30,16 @@ export const BudgetCategories = memo( onDeleteGroup, onReorderCategory, onReorderGroup, - onShowNewCategory, - onHideNewCategory, - onShowNewGroup, - onHideNewGroup, }) => { + const [_collapsed, setCollapsedPref] = useLocalPref('budget.collapsed'); + const collapsed = _collapsed || []; + const [showHiddenCategories] = useLocalPref('budget.showHiddenCategories'); + function onCollapse(value) { + setCollapsedPref(value); + } + + const [isAddingGroup, setIsAddingGroup] = useState(false); + const [newCategoryForGroup, setNewCategoryForGroup] = useState(null); const items = useMemo(() => { const [expenseGroups, incomeGroup] = separateGroups(categoryGroups); @@ -133,15 +134,46 @@ export const BudgetCategories = memo( }); } else if (state === 'end') { setDragState(null); - setCollapsed(savedCollapsed || []); + onCollapse(savedCollapsed || []); } } function onToggleCollapse(id) { if (collapsed.includes(id)) { - setCollapsed(collapsed.filter(id_ => id_ !== id)); + onCollapse(collapsed.filter(id_ => id_ !== id)); } else { - setCollapsed([...collapsed, id]); + onCollapse([...collapsed, id]); + } + } + + function onShowNewGroup() { + setIsAddingGroup(true); + } + + function onHideNewGroup() { + setIsAddingGroup(false); + } + + function _onSaveGroup(group) { + onSaveGroup?.(group); + if (group.id === 'new') { + onHideNewGroup(); + } + } + + function onShowNewCategory(groupId) { + onCollapse(collapsed.filter(c => c !== groupId)); + setNewCategoryForGroup(groupId); + } + + function onHideNewCategory() { + setNewCategoryForGroup(null); + } + + function _onSaveCategory(category) { + onSaveCategory?.(category); + if (category.id === 'new') { + onHideNewCategory(); } } @@ -167,7 +199,7 @@ export const BudgetCategories = memo( <SidebarGroup group={{ id: 'new', name: '' }} editing={true} - onSave={onSaveGroup} + onSave={_onSaveGroup} onHideNewGroup={onHideNewGroup} onEdit={onEditName} /> @@ -187,7 +219,7 @@ export const BudgetCategories = memo( id: 'new', }} editing={true} - onSave={onSaveCategory} + onSave={_onSaveCategory} onHideNewCategory={onHideNewCategory} onEditName={onEditName} /> @@ -204,7 +236,7 @@ export const BudgetCategories = memo( MonthComponent={dataComponents.ExpenseGroupComponent} dragState={dragState} onEditName={onEditName} - onSave={onSaveGroup} + onSave={_onSaveGroup} onDelete={onDeleteGroup} onDragChange={onDragChange} onReorderGroup={onReorderGroup} @@ -223,7 +255,7 @@ export const BudgetCategories = memo( dragState={dragState} onEditName={onEditName} onEditMonth={onEditMonth} - onSave={onSaveCategory} + onSave={_onSaveCategory} onDelete={onDeleteCategory} onDragChange={onDragChange} onReorder={onReorderCategory} @@ -255,7 +287,7 @@ export const BudgetCategories = memo( MonthComponent={dataComponents.IncomeGroupComponent} collapsed={collapsed.includes(item.value.id)} onEditName={onEditName} - onSave={onSaveGroup} + onSave={_onSaveGroup} onToggleCollapse={onToggleCollapse} onShowNewCategory={onShowNewCategory} /> @@ -270,7 +302,7 @@ export const BudgetCategories = memo( MonthComponent={dataComponents.IncomeCategoryComponent} onEditName={onEditName} onEditMonth={onEditMonth} - onSave={onSaveCategory} + onSave={_onSaveCategory} onDelete={onDeleteCategory} onDragChange={onDragChange} onReorder={onReorderCategory} diff --git a/packages/desktop-client/src/components/budget/BudgetTable.jsx b/packages/desktop-client/src/components/budget/BudgetTable.jsx index 3000ebb1af4e1c161f7c555291d410ef94b08f37..d9f4d4df8bb8a7b93fcf192471b274575d3c20f6 100644 --- a/packages/desktop-client/src/components/budget/BudgetTable.jsx +++ b/packages/desktop-client/src/components/budget/BudgetTable.jsx @@ -1,5 +1,7 @@ import React, { createRef, Component } from 'react'; +import { connect } from 'react-redux'; +import { savePrefs } from 'loot-core/src/client/actions'; import * as monthUtils from 'loot-core/src/shared/months'; import { theme, styles } from '../../style'; @@ -12,7 +14,7 @@ import { BudgetTotals } from './BudgetTotals'; import { MonthsProvider } from './MonthsContext'; import { findSortDown, findSortUp, getScrollbarWidth } from './util'; -export class BudgetTable extends Component { +class BudgetTableInner extends Component { constructor(props) { super(props); this.budgetCategoriesRef = createRef(); @@ -20,7 +22,6 @@ export class BudgetTable extends Component { this.state = { editing: null, draggingState: null, - showHiddenCategories: props.prefs['budget.showHiddenCategories'] ?? false, }; } @@ -137,26 +138,22 @@ export class BudgetTable extends Component { return monthUtils.addMonths(this.props.startMonth, monthIndex); }; + // This is called via ref. clearEditing() { this.setState({ editing: null }); } toggleHiddenCategories = () => { - this.setState(prevState => ({ - showHiddenCategories: !prevState.showHiddenCategories, - })); - this.props.savePrefs({ - 'budget.showHiddenCategories': !this.state.showHiddenCategories, - }); + this.props.onToggleHiddenCategories(); }; expandAllCategories = () => { - this.props.setCollapsed([]); + this.props.onCollapse([]); }; collapseAllCategories = () => { - const { setCollapsed, categoryGroups } = this.props; - setCollapsed(categoryGroups.map(g => g.id)); + const { onCollapse, categoryGroups } = this.props; + onCollapse(categoryGroups.map(g => g.id)); }; render() { @@ -167,21 +164,13 @@ export class BudgetTable extends Component { startMonth, numMonths, monthBounds, - collapsed, - setCollapsed, - newCategoryForGroup, dataComponents, - isAddingGroup, onSaveCategory, onSaveGroup, onDeleteCategory, onDeleteGroup, - onShowNewCategory, - onHideNewCategory, - onShowNewGroup, - onHideNewGroup, } = this.props; - const { editing, draggingState, showHiddenCategories } = this.state; + const { editing, draggingState } = this.state; return ( <View @@ -254,13 +243,8 @@ export class BudgetTable extends Component { innerRef={el => (this.budgetDataNode = el)} > <BudgetCategories - showHiddenCategories={showHiddenCategories} categoryGroups={categoryGroups} - newCategoryForGroup={newCategoryForGroup} - isAddingGroup={isAddingGroup} editingCell={editing} - collapsed={collapsed} - setCollapsed={setCollapsed} dataComponents={dataComponents} onEditMonth={this.onEditMonth} onEditName={this.onEditName} @@ -270,10 +254,6 @@ export class BudgetTable extends Component { onDeleteGroup={onDeleteGroup} onReorderCategory={this.onReorderCategory} onReorderGroup={this.onReorderGroup} - onShowNewCategory={onShowNewCategory} - onHideNewCategory={onHideNewCategory} - onShowNewGroup={onShowNewGroup} - onHideNewGroup={onHideNewGroup} onBudgetAction={this.onBudgetAction} onShowActivity={this.onShowActivity} /> @@ -285,3 +265,35 @@ export class BudgetTable extends Component { ); } } + +const mapStateToProps = state => { + const { grouped: categoryGroups } = state.queries.categories; + return { + categoryGroups, + }; +}; + +const mapDispatchToProps = dispatch => { + const onCollapse = collapsedIds => { + dispatch(savePrefs({ 'budget.collapsed': collapsedIds })); + }; + + const onToggleHiddenCategories = () => + dispatch((innerDispatch, getState) => { + const { prefs } = getState(); + const showHiddenCategories = prefs.local['budget.showHiddenCategories']; + innerDispatch( + savePrefs({ + 'budget.showHiddenCategories': !showHiddenCategories, + }), + ); + }); + return { + onCollapse, + onToggleHiddenCategories, + }; +}; + +export const BudgetTable = connect(mapStateToProps, mapDispatchToProps, null, { + forwardRef: true, +})(BudgetTableInner); diff --git a/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx b/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx index 7a01439ab81c890079171c4429536562a41dbfa2..ab366f3a6087cbe2f4a1ef3d3c8a133b6d002bd7 100644 --- a/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx +++ b/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx @@ -1,11 +1,7 @@ // @ts-strict-ignore import React, { forwardRef, useEffect, type ComponentProps } from 'react'; -import { useSelector } from 'react-redux'; import AutoSizer from 'react-virtualized-auto-sizer'; -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; - import { useActions } from '../../hooks/useActions'; import { View } from '../common/View'; @@ -37,14 +33,13 @@ type DynamicBudgetTableInnerProps = { } & ComponentProps<typeof BudgetTable>; const DynamicBudgetTableInner = forwardRef< - BudgetTable, + typeof BudgetTable, DynamicBudgetTableInnerProps >( ( { width, height, - categoryGroups, prewarmStartMonth, startMonth, maxMonths = 3, @@ -55,9 +50,6 @@ const DynamicBudgetTableInner = forwardRef< }, ref, ) => { - const prefs = useSelector<State, PrefsState['local']>( - state => state.prefs.local, - ); const { setDisplayMax } = useBudgetMonthCount(); const actions = useActions(); @@ -91,12 +83,10 @@ const DynamicBudgetTableInner = forwardRef< /> <BudgetTable ref={ref} - categoryGroups={categoryGroups} prewarmStartMonth={prewarmStartMonth} startMonth={startMonth} numMonths={numMonths} monthBounds={monthBounds} - prefs={prefs} {...actions} {...props} /> @@ -107,7 +97,7 @@ const DynamicBudgetTableInner = forwardRef< ); export const DynamicBudgetTable = forwardRef< - BudgetTable, + typeof BudgetTable, DynamicBudgetTableInnerProps >((props, ref) => { return ( diff --git a/packages/desktop-client/src/components/budget/MobileBudget.tsx b/packages/desktop-client/src/components/budget/MobileBudget.tsx index 4b98fc5f6ea9e232ff6d24b63f9e0b76f7017c5c..f5145aeec43312b06a2e428b5a8b1349a809a834 100644 --- a/packages/desktop-client/src/components/budget/MobileBudget.tsx +++ b/packages/desktop-client/src/components/budget/MobileBudget.tsx @@ -1,10 +1,7 @@ // @ts-strict-ignore import React, { useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; import { useSpreadsheet } from 'loot-core/src/client/SpreadsheetProvider'; -import { type PrefsState } from 'loot-core/src/client/state-types/prefs'; import { send, listen } from 'loot-core/src/platform/client/fetch'; import * as monthUtils from 'loot-core/src/shared/months'; import { @@ -14,6 +11,7 @@ import { import { type BoundActions, useActions } from '../../hooks/useActions'; import { useCategories } from '../../hooks/useCategories'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { useSetThemeColor } from '../../hooks/useSetThemeColor'; import { AnimatedLoading } from '../../icons/AnimatedLoading'; import { theme } from '../../style'; @@ -26,7 +24,6 @@ import { prewarmMonth, switchBudgetType } from './util'; type BudgetInnerProps = { categories: CategoryEntity[]; categoryGroups: CategoryGroupEntity[]; - prefs: PrefsState['local']; loadPrefs: BoundActions['loadPrefs']; savePrefs: BoundActions['savePrefs']; budgetType: 'rollover' | 'report'; @@ -50,9 +47,7 @@ function BudgetInner(props: BudgetInnerProps) { const { categoryGroups, categories, - prefs, loadPrefs, - savePrefs, budgetType, spreadsheet, applyBudgetAction, @@ -75,6 +70,10 @@ function BudgetInner(props: BudgetInnerProps) { const [initialized, setInitialized] = useState(false); const [editMode, setEditMode] = useState(false); + const [_numberFormat] = useLocalPref('numberFormat'); + const numberFormat = _numberFormat || 'comma-dot'; + const [hideFraction = false] = useLocalPref('hideFraction'); + useEffect(() => { async function init() { const { start, end } = await send('get-budget-bounds'); @@ -356,9 +355,6 @@ function BudgetInner(props: BudgetInnerProps) { }); }; - const numberFormat = prefs?.numberFormat || 'comma-dot'; - const hideFraction = prefs?.hideFraction || false; - if (!categoryGroups || !initialized) { return ( <View @@ -385,7 +381,7 @@ function BudgetInner(props: BudgetInnerProps) { <BudgetTable // This key forces the whole table rerender when the number // format changes - key={numberFormat + hideFraction} + key={`${numberFormat}${hideFraction}`} categoryGroups={categoryGroups} type={budgetType} month={currentMonth} @@ -407,7 +403,6 @@ function BudgetInner(props: BudgetInnerProps) { onBudgetAction={applyBudgetAction} onRefresh={onRefresh} onSwitchBudgetType={onSwitchBudgetType} - savePrefs={savePrefs} pushModal={pushModal} onEditGroup={onEditGroup} onEditCategory={onEditCategory} @@ -419,12 +414,8 @@ function BudgetInner(props: BudgetInnerProps) { export function Budget() { const { list: categories, grouped: categoryGroups } = useCategories(); - const budgetType = useSelector<State, PrefsState['local']['budgetType']>( - state => state.prefs.local?.budgetType || 'rollover', - ); - const prefs = useSelector<State, PrefsState['local']>( - state => state.prefs.local, - ); + const [_budgetType] = useLocalPref('budgetType'); + const budgetType = _budgetType || 'rollover'; const actions = useActions(); const spreadsheet = useSpreadsheet(); @@ -434,7 +425,6 @@ export function Budget() { categoryGroups={categoryGroups} categories={categories} budgetType={budgetType} - prefs={prefs} {...actions} spreadsheet={spreadsheet} /> diff --git a/packages/desktop-client/src/components/budget/MobileBudgetTable.jsx b/packages/desktop-client/src/components/budget/MobileBudgetTable.jsx index a7214cf1214efd244ce8c15ae9056a0b0ec9bd30..9a03bb71c0c7877194fc89ad306cc4f073f16b48 100644 --- a/packages/desktop-client/src/components/budget/MobileBudgetTable.jsx +++ b/packages/desktop-client/src/components/budget/MobileBudgetTable.jsx @@ -1,5 +1,4 @@ import React, { memo, useRef, useState } from 'react'; -import { useSelector } from 'react-redux'; import memoizeOne from 'memoize-one'; @@ -7,6 +6,7 @@ import { rolloverBudget, reportBudget } from 'loot-core/src/client/queries'; import * as monthUtils from 'loot-core/src/shared/months'; import { useFeatureFlag } from '../../hooks/useFeatureFlag'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { SingleActiveEditFormProvider, useSingleActiveEditForm, @@ -1133,7 +1133,6 @@ export function BudgetTable({ onBudgetAction, onRefresh, onSwitchBudgetType, - savePrefs, pushModal, onEditGroup, onEditCategory, @@ -1144,24 +1143,15 @@ export function BudgetTable({ // let editMode = false; // neuter editMode -- sorry, not rewriting drag-n-drop right now const format = useFormat(); - const mobileShowBudgetedColPref = useSelector(state => { - return state.prefs?.local?.toggleMobileDisplayPref || true; - }); - - const showHiddenCategories = useSelector(state => { - return state.prefs?.local?.['budget.showHiddenCategories'] || false; - }); - - const [showBudgetedCol, setShowBudgetedCol] = useState( - !mobileShowBudgetedColPref && - !document.cookie.match(/mobileShowBudgetedColPref=true/), + const [showSpentColumn = false, setShowSpentColumnPref] = useLocalPref( + 'mobile.showSpentColumn', ); + const [showHiddenCategories = false, setShowHiddenCategoriesPref] = + useLocalPref('budget.showHiddenCategories'); + function toggleDisplay() { - setShowBudgetedCol(!showBudgetedCol); - if (!showBudgetedCol) { - savePrefs({ mobileShowBudgetedColPref: true }); - } + setShowSpentColumnPref(!showSpentColumn); } const buttonStyle = { @@ -1177,9 +1167,7 @@ export function BudgetTable({ }; const onToggleHiddenCategories = () => { - savePrefs({ - 'budget.showHiddenCategories': !showHiddenCategories, - }); + setShowHiddenCategoriesPref(!showHiddenCategories); }; return ( @@ -1245,7 +1233,7 @@ export function BudgetTable({ /> )} <View style={{ flex: 1 }} /> - {(show3Cols || showBudgetedCol) && ( + {(show3Cols || !showSpentColumn) && ( <Button type="bare" disabled={show3Cols} @@ -1255,7 +1243,7 @@ export function BudgetTable({ padding: '0 8px', margin: '0 -8px', background: - showBudgetedCol && !show3Cols + !showSpentColumn && !show3Cols ? `linear-gradient(-45deg, ${theme.formInputBackgroundSelection} 8px, transparent 0)` : null, }} @@ -1292,7 +1280,7 @@ export function BudgetTable({ </View> </Button> )} - {(show3Cols || !showBudgetedCol) && ( + {(show3Cols || showSpentColumn) && ( <Button type="bare" disabled={show3Cols} @@ -1300,7 +1288,7 @@ export function BudgetTable({ style={{ ...buttonStyle, background: - !showBudgetedCol && !show3Cols + showSpentColumn && !show3Cols ? `linear-gradient(45deg, ${theme.formInputBackgroundSelection} 8px, transparent 0)` : null, }} @@ -1372,7 +1360,7 @@ export function BudgetTable({ <BudgetGroups type={type} categoryGroups={categoryGroups} - showBudgetedCol={showBudgetedCol} + showBudgetedCol={!showSpentColumn} show3Cols={show3Cols} showHiddenCategories={showHiddenCategories} // gestures={gestures} @@ -1407,7 +1395,7 @@ export function BudgetTable({ <BudgetGroups type={type} categoryGroups={categoryGroups} - showBudgetedCol={showBudgetedCol} + showBudgetedCol={!showSpentColumn} show3Cols={show3Cols} showHiddenCategories={showHiddenCategories} // gestures={gestures} diff --git a/packages/desktop-client/src/components/budget/MonthCountSelector.tsx b/packages/desktop-client/src/components/budget/MonthCountSelector.tsx index 737dcdc69375726db5df08252f46d4aabb0a6b80..d64ed0082b8170db415d88553ee6a19c48ddfb2d 100644 --- a/packages/desktop-client/src/components/budget/MonthCountSelector.tsx +++ b/packages/desktop-client/src/components/budget/MonthCountSelector.tsx @@ -22,7 +22,7 @@ function Calendar({ color, onClick }: CalendarProps) { type MonthCountSelectorProps = { maxMonths: number; - onChange: (value: number) => Promise<void>; + onChange: (value: number) => void; }; export function MonthCountSelector({ diff --git a/packages/desktop-client/src/components/budget/index.tsx b/packages/desktop-client/src/components/budget/index.tsx index ea263830d8d228fb78b2af95724a32671f8a0adb..724a969c231f55fdb0ec9b8c0959936b23dda91d 100644 --- a/packages/desktop-client/src/components/budget/index.tsx +++ b/packages/desktop-client/src/components/budget/index.tsx @@ -7,35 +7,31 @@ import React, { useEffect, useRef, } from 'react'; -import { useSelector } from 'react-redux'; -import { - type NavigateFunction, - type PathMatch, - useLocation, - useMatch, -} from 'react-router-dom'; - -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; -import { useSpreadsheet } from 'loot-core/src/client/SpreadsheetProvider'; -import { send, listen } from 'loot-core/src/platform/client/fetch'; +import { useDispatch } from 'react-redux'; + import { - addCategory, + addNotification, + applyBudgetAction, + createCategory, + createGroup, + deleteCategory, + deleteGroup, + getCategories, + loadPrefs, moveCategory, moveCategoryGroup, + pushModal, updateCategory, - deleteCategory, - addGroup, updateGroup, - deleteGroup, -} from 'loot-core/src/shared/categories'; +} from 'loot-core/src/client/actions'; +import { useSpreadsheet } from 'loot-core/src/client/SpreadsheetProvider'; +import { send, listen } from 'loot-core/src/platform/client/fetch'; import * as monthUtils from 'loot-core/src/shared/months'; -import { type GlobalPrefs, type LocalPrefs } from 'loot-core/src/types/prefs'; -import { type CategoryGroupEntity } from 'loot-core/types/models'; -import { type BoundActions, useActions } from '../../hooks/useActions'; import { useCategories } from '../../hooks/useCategories'; import { useFeatureFlag } from '../../hooks/useFeatureFlag'; +import { useGlobalPref } from '../../hooks/useGlobalPref'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { useNavigate } from '../../hooks/useNavigate'; import { styles } from '../../style'; import { View } from '../common/View'; @@ -75,62 +71,41 @@ type RolloverComponents = { type BudgetProps = { accountId?: string; - startMonth: LocalPrefs['budget.startMonth']; - collapsedPrefs: LocalPrefs['budget.collapsed']; - summaryCollapsed: LocalPrefs['budget.summaryCollapsed']; - budgetType: LocalPrefs['budgetType']; - maxMonths: GlobalPrefs['maxMonths']; - categoryGroups: CategoryGroupEntity[]; reportComponents: ReportComponents; rolloverComponents: RolloverComponents; titlebar: TitlebarContextValue; - match: PathMatch<string>; - spreadsheet: ReturnType<typeof useSpreadsheet>; - navigate: NavigateFunction; - getCategories: BoundActions['getCategories']; - savePrefs: BoundActions['savePrefs']; - createCategory: BoundActions['createCategory']; - updateCategory: BoundActions['updateCategory']; - pushModal: BoundActions['pushModal']; - deleteCategory: BoundActions['deleteCategory']; - createGroup: BoundActions['createGroup']; - updateGroup: BoundActions['updateGroup']; - deleteGroup: BoundActions['deleteGroup']; - applyBudgetAction: BoundActions['applyBudgetAction']; - moveCategory: BoundActions['moveCategory']; - moveCategoryGroup: BoundActions['moveCategoryGroup']; - loadPrefs: BoundActions['loadPrefs']; - addNotification: BoundActions['addNotification']; }; function BudgetInner(props: BudgetProps) { const currentMonth = monthUtils.currentMonth(); const tableRef = useRef(null); - - const [initialized, setInitialized] = useState(false); - const [prewarmStartMonth, setPrewarmStartMonth] = useState( - props.startMonth || currentMonth, + const spreadsheet = useSpreadsheet(); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const [_startMonth, setBudgetStartMonthPref] = + useLocalPref('budget.startMonth'); + const startMonth = _startMonth || currentMonth; + const [summaryCollapsed, setSummaryCollapsedPref] = useLocalPref( + 'budget.summaryCollapsed', ); + const [_budgetType] = useLocalPref('budgetType'); + const budgetType = _budgetType || 'rollover'; + const [_maxMonths] = useGlobalPref('maxMonths'); + const maxMonths = _maxMonths || 1; - const [newCategoryForGroup, setNewCategoryForGroup] = useState(null); - const [isAddingGroup, setIsAddingGroup] = useState(false); - const [collapsed, setCollapsed] = useState(props.collapsedPrefs || []); + const [initialized, setInitialized] = useState(false); const [bounds, setBounds] = useState({ start: currentMonth, end: currentMonth, }); - const [categoryGroups, setCategoryGroups] = useState(null); - const [summaryCollapsed, setSummaryCollapsed] = useState( - props.summaryCollapsed, - ); + const { grouped: categoryGroups } = useCategories(); - async function loadCategories() { - const result = await props.getCategories(); - setCategoryGroups(result.grouped); + function loadCategories() { + dispatch(getCategories()); } useEffect(() => { - const { titlebar, budgetType } = props; + const { titlebar } = props; async function run() { loadCategories(); @@ -140,9 +115,9 @@ function BudgetInner(props: BudgetProps) { await prewarmAllMonths( budgetType, - props.spreadsheet, + spreadsheet, { start, end }, - prewarmStartMonth, + startMonth, ); setInitialized(true); @@ -187,10 +162,6 @@ function BudgetInner(props: BudgetProps) { }; }, []); - useEffect(() => { - props.savePrefs({ 'budget.collapsed': collapsed }); - }, [collapsed]); - useEffect(() => { send('get-budget-bounds').then(({ start, end }) => { if (bounds.start !== start || bounds.end !== end) { @@ -200,12 +171,10 @@ function BudgetInner(props: BudgetProps) { }, [props.accountId]); const onMonthSelect = async (month, numDisplayed) => { - setPrewarmStartMonth(month); + setBudgetStartMonthPref(month); const warmingMonth = month; - const startMonth = props.startMonth || currentMonth; - // We could be smarter about this, but this is a good start. We // optimize for the case where users press the left/right button // to move between months. This loads the month data all at once @@ -215,51 +184,37 @@ function BudgetInner(props: BudgetProps) { if (month < startMonth) { // pre-warm prev month await prewarmMonth( - props.budgetType, - props.spreadsheet, + budgetType, + spreadsheet, monthUtils.subMonths(month, 1), ); } else if (month > startMonth) { // pre-warm next month await prewarmMonth( - props.budgetType, - props.spreadsheet, + budgetType, + spreadsheet, monthUtils.addMonths(month, numDisplayed), ); } if (warmingMonth === month) { - props.savePrefs({ 'budget.startMonth': month }); + setBudgetStartMonthPref(month); } }; - const onShowNewCategory = groupId => { - setNewCategoryForGroup(groupId); - setCollapsed(state => state.filter(c => c !== groupId)); - }; - - const onHideNewCategory = () => { - setNewCategoryForGroup(null); - }; - - const onShowNewGroup = () => { - setIsAddingGroup(true); - }; - - const onHideNewGroup = () => { - setIsAddingGroup(false); - }; - const categoryNameAlreadyExistsNotification = name => { - props.addNotification({ - type: 'error', - message: `Category ‘${name}’ already exists in group (May be Hidden)`, - }); + dispatch( + addNotification({ + type: 'error', + message: `Category ‘${name}’ already exists in group (May be Hidden)`, + }), + ); }; const onSaveCategory = async category => { + const cats = await send('get-categories'); const exists = - (await props.getCategories()).grouped + cats.grouped .filter(g => g.id === category.cat_group)[0] .categories.filter( c => c.name.toUpperCase() === category.name.toUpperCase(), @@ -273,24 +228,16 @@ function BudgetInner(props: BudgetProps) { } if (category.id === 'new') { - const id = await props.createCategory( - category.name, - category.cat_group, - category.is_income, - category.hidden, - ); - - setNewCategoryForGroup(null); - setCategoryGroups(state => - addCategory(state, { - ...category, - is_income: category.is_income ? 1 : 0, - id, - }), + dispatch( + createCategory( + category.name, + category.cat_group, + category.is_income, + category.hidden, + ), ); } else { - props.updateCategory(category); - setCategoryGroups(state => updateCategory(state, category)); + dispatch(updateCategory(category)); } }; @@ -298,55 +245,26 @@ function BudgetInner(props: BudgetProps) { const mustTransfer = await send('must-category-transfer', { id }); if (mustTransfer) { - props.pushModal('confirm-category-delete', { - category: id, - onDelete: transferCategory => { - if (id !== transferCategory) { - props.deleteCategory(id, transferCategory); - - setCategoryGroups(state => deleteCategory(state, id)); - } - }, - }); + dispatch( + pushModal('confirm-category-delete', { + category: id, + onDelete: transferCategory => { + if (id !== transferCategory) { + dispatch(deleteCategory(id, transferCategory)); + } + }, + }), + ); } else { - props.deleteCategory(id); - - setCategoryGroups(state => deleteCategory(state, id)); + dispatch(deleteCategory(id)); } }; - const groupNameAlreadyExistsNotification = group => { - props.addNotification({ - type: 'error', - message: `A ${group.hidden ? 'hidden ' : ''}’${group.name}’ category group already exists.`, - }); - }; - - const onSaveGroup = async group => { - const categories = await props.getCategories(); - const matchingGroups = categories.grouped - .filter(g => g.name.toUpperCase() === group.name.toUpperCase()) - .filter(g => group.id === 'new' || group.id !== g.id); - - if (matchingGroups.length > 0) { - groupNameAlreadyExistsNotification(matchingGroups[0]); - return; - } - + const onSaveGroup = group => { if (group.id === 'new') { - const id = await props.createGroup(group.name); - setIsAddingGroup(false); - setCategoryGroups(state => - addGroup(state, { - ...group, - is_income: 0, - categories: group.categories || [], - id, - }), - ); + dispatch(createGroup(group.name)); } else { - props.updateGroup(group); - setCategoryGroups(state => updateGroup(state, group)); + dispatch(updateGroup(group)); } }; @@ -362,27 +280,25 @@ function BudgetInner(props: BudgetProps) { } if (mustTransfer) { - props.pushModal('confirm-category-delete', { - group: id, - onDelete: transferCategory => { - props.deleteGroup(id, transferCategory); - - setCategoryGroups(state => deleteGroup(state, id)); - }, - }); + dispatch( + pushModal('confirm-category-delete', { + group: id, + onDelete: transferCategory => { + dispatch(deleteGroup(id, transferCategory)); + }, + }), + ); } else { - props.deleteGroup(id); - - setCategoryGroups(state => deleteGroup(state, id)); + dispatch(deleteGroup(id)); } }; const onBudgetAction = (month, type, args) => { - props.applyBudgetAction(month, type, args); + dispatch(applyBudgetAction(month, type, args)); }; const onShowActivity = (categoryName, categoryId, month) => { - props.navigate('/accounts', { + navigate('/accounts', { state: { goBack: true, filterName: `${categoryName} (${monthUtils.format( @@ -398,7 +314,7 @@ function BudgetInner(props: BudgetProps) { }; const onReorderCategory = async sortInfo => { - const cats = await props.getCategories(); + const cats = await send('get-categories'); const moveCandidate = cats.list.filter(c => c.id === sortInfo.id)[0]; const exists = cats.grouped @@ -413,23 +329,15 @@ function BudgetInner(props: BudgetProps) { return; } - props.moveCategory(sortInfo.id, sortInfo.groupId, sortInfo.targetId); - setCategoryGroups(state => - moveCategory(state, sortInfo.id, sortInfo.groupId, sortInfo.targetId), - ); + dispatch(moveCategory(sortInfo.id, sortInfo.groupId, sortInfo.targetId)); }; const onReorderGroup = async sortInfo => { - props.moveCategoryGroup(sortInfo.id, sortInfo.targetId); - setCategoryGroups(state => - moveCategoryGroup(state, sortInfo.id, sortInfo.targetId), - ); + dispatch(moveCategoryGroup(sortInfo.id, sortInfo.targetId)); }; const onToggleCollapse = () => { - const collapsed = !summaryCollapsed; - setSummaryCollapsed(collapsed); - props.savePrefs({ 'budget.summaryCollapsed': collapsed }); + setSummaryCollapsedPref(!summaryCollapsed); }; const onTitlebarEvent = async ({ type, payload }: TitlebarMessage) => { @@ -437,10 +345,12 @@ function BudgetInner(props: BudgetProps) { case SWITCH_BUDGET_MESSAGE_TYPE: { await switchBudgetType( payload.newBudgetType, - props.spreadsheet, + spreadsheet, bounds, - prewarmStartMonth, - () => props.loadPrefs(), + startMonth, + async () => { + dispatch(loadPrefs()); + }, ); break; } @@ -448,23 +358,14 @@ function BudgetInner(props: BudgetProps) { } }; - const { - maxMonths: originalMaxMonths, - budgetType: type, - reportComponents, - rolloverComponents, - } = props; - - const maxMonths = originalMaxMonths || 1; + const { reportComponents, rolloverComponents } = props; if (!initialized || !categoryGroups) { return null; } - const startMonth = props.startMonth || currentMonth; - let table; - if (type === 'report') { + if (budgetType === 'report') { table = ( <ReportProvider summaryCollapsed={summaryCollapsed} @@ -473,22 +374,13 @@ function BudgetInner(props: BudgetProps) { > <DynamicBudgetTable ref={tableRef} - type={type} - categoryGroups={categoryGroups} - prewarmStartMonth={prewarmStartMonth} + type={budgetType} + prewarmStartMonth={startMonth} startMonth={startMonth} monthBounds={bounds} maxMonths={maxMonths} - collapsed={collapsed} - setCollapsed={setCollapsed} - newCategoryForGroup={newCategoryForGroup} - isAddingGroup={isAddingGroup} dataComponents={reportComponents} onMonthSelect={onMonthSelect} - onShowNewCategory={onShowNewCategory} - onHideNewCategory={onHideNewCategory} - onShowNewGroup={onShowNewGroup} - onHideNewGroup={onHideNewGroup} onDeleteCategory={onDeleteCategory} onDeleteGroup={onDeleteGroup} onSaveCategory={onSaveCategory} @@ -503,29 +395,19 @@ function BudgetInner(props: BudgetProps) { } else { table = ( <RolloverContext - categoryGroups={categoryGroups} summaryCollapsed={summaryCollapsed} onBudgetAction={onBudgetAction} onToggleSummaryCollapse={onToggleCollapse} > <DynamicBudgetTable ref={tableRef} - type={type} - categoryGroups={categoryGroups} - prewarmStartMonth={prewarmStartMonth} + type={budgetType} + prewarmStartMonth={startMonth} startMonth={startMonth} monthBounds={bounds} maxMonths={maxMonths} - collapsed={collapsed} - setCollapsed={setCollapsed} - newCategoryForGroup={newCategoryForGroup} - isAddingGroup={isAddingGroup} dataComponents={rolloverComponents} onMonthSelect={onMonthSelect} - onShowNewCategory={onShowNewCategory} - onHideNewCategory={onHideNewCategory} - onShowNewGroup={onShowNewGroup} - onHideNewGroup={onHideNewGroup} onDeleteCategory={onDeleteCategory} onDeleteGroup={onDeleteGroup} onSaveCategory={onSaveCategory} @@ -553,32 +435,7 @@ const RolloverBudgetSummary = memo<{ month: string }>(props => { }); export function Budget() { - const startMonth = useSelector< - State, - PrefsState['local']['budget.startMonth'] - >(state => state.prefs.local['budget.startMonth']); - const collapsedPrefs = useSelector< - State, - PrefsState['local']['budget.collapsed'] - >(state => state.prefs.local['budget.collapsed']); - const summaryCollapsed = useSelector< - State, - PrefsState['local']['budget.summaryCollapsed'] - >(state => state.prefs.local['budget.summaryCollapsed']); - const budgetType = useSelector<State, PrefsState['local']['budgetType']>( - state => state.prefs.local.budgetType || 'rollover', - ); - const maxMonths = useSelector<State, PrefsState['global']['maxMonths']>( - state => state.prefs.global.maxMonths, - ); - const { grouped: categoryGroups } = useCategories(); - - const actions = useActions(); - const spreadsheet = useSpreadsheet(); const titlebar = useContext(TitlebarContext); - const location = useLocation(); - const match = useMatch(location.pathname); - const navigate = useNavigate(); const reportComponents = useMemo<ReportComponents>( () => ({ @@ -620,19 +477,9 @@ export function Budget() { }} > <BudgetInner - startMonth={startMonth} - collapsedPrefs={collapsedPrefs} - summaryCollapsed={summaryCollapsed} - budgetType={budgetType} - maxMonths={maxMonths} - categoryGroups={categoryGroups} - {...actions} reportComponents={reportComponents} rolloverComponents={rolloverComponents} - spreadsheet={spreadsheet} titlebar={titlebar} - navigate={navigate} - match={match} /> </View> ); diff --git a/packages/desktop-client/src/components/budget/rollover/RolloverContext.tsx b/packages/desktop-client/src/components/budget/rollover/RolloverContext.tsx index 636953b16ac16f4de04fed3d1976caf6f4942e68..fcbc714cd557acc9c8bffe95cbdc43e9f2ad9a7e 100644 --- a/packages/desktop-client/src/components/budget/rollover/RolloverContext.tsx +++ b/packages/desktop-client/src/components/budget/rollover/RolloverContext.tsx @@ -6,14 +6,12 @@ import * as monthUtils from 'loot-core/src/shared/months'; const Context = createContext(null); type RolloverContextProps = { - categoryGroups: unknown[]; summaryCollapsed: boolean; onBudgetAction: (idx: number, action: string, arg?: unknown) => void; onToggleSummaryCollapse: () => void; children: ReactNode; }; export function RolloverContext({ - categoryGroups, summaryCollapsed, onBudgetAction, onToggleSummaryCollapse, @@ -25,7 +23,6 @@ export function RolloverContext({ <Context.Provider value={{ currentMonth, - categoryGroups, summaryCollapsed, onBudgetAction, onToggleSummaryCollapse, diff --git a/packages/desktop-client/src/components/filters/AppliedFilters.tsx b/packages/desktop-client/src/components/filters/AppliedFilters.tsx index 1f5ead431ea702da4d00c318ed82bf6e01fb99bd..eac0510a1ab7713ac0fb43f7598181819abe3aec 100644 --- a/packages/desktop-client/src/components/filters/AppliedFilters.tsx +++ b/packages/desktop-client/src/components/filters/AppliedFilters.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { type RuleConditionEntity } from 'loot-core/types/models'; +import { type RuleConditionEntity } from 'loot-core/src/types/models'; import { View } from '../common/View'; diff --git a/packages/desktop-client/src/components/filters/FiltersMenu.jsx b/packages/desktop-client/src/components/filters/FiltersMenu.jsx index f4650c5a44fa68e3a6be3edce75496eda84eb551..18889c5159c3a7117b8e5c161f9f6270aa858829 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 e5bc05b8df6d12b2df5be6a5efb15100c4e46a79..9266c584bc28c38905674667dda818c9618ca61d 100644 --- a/packages/desktop-client/src/components/modals/CloseAccount.tsx +++ b/packages/desktop-client/src/components/modals/CloseAccount.tsx @@ -2,12 +2,11 @@ import React, { useState } from 'react'; import { integerToCurrency } from 'loot-core/src/shared/util'; -import { - type AccountEntity, - type CategoryGroupEntity, -} from 'loot-core/src/types/models'; +import { type AccountEntity } from 'loot-core/src/types/models'; +import { useAccounts } from '../../hooks/useAccounts'; import { type BoundActions } from '../../hooks/useActions'; +import { useCategories } from '../../hooks/useCategories'; import { theme } from '../../style'; import { AccountAutocomplete } from '../autocomplete/AccountAutocomplete'; import { CategoryAutocomplete } from '../autocomplete/CategoryAutocomplete'; @@ -35,8 +34,6 @@ function needsCategory( type CloseAccountProps = { account: AccountEntity; - accounts: AccountEntity[]; - categoryGroups: CategoryGroupEntity[]; balance: number; canDelete: boolean; actions: BoundActions; @@ -45,8 +42,6 @@ type CloseAccountProps = { export function CloseAccount({ account, - accounts, - categoryGroups, balance, canDelete, actions, @@ -58,6 +53,8 @@ export function CloseAccount({ const [transferError, setTransferError] = useState(false); const [categoryError, setCategoryError] = useState(false); + const accounts = useAccounts().filter(a => a.closed === 0); + const { grouped: categoryGroups } = useCategories(); return ( <Modal diff --git a/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.tsx b/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.tsx index 803e950035099523fb201db698b8e0a50be249a4..a448f351ca077f935ae3f9efbe451ee5992c1361 100644 --- a/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.tsx +++ b/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.tsx @@ -1,8 +1,7 @@ // @ts-strict-ignore import React, { useState } from 'react'; -import { type CategoryGroupEntity } from 'loot-core/src/types/models'; - +import { useCategories } from '../../hooks/useCategories'; import { theme } from '../../style'; import { CategoryAutocomplete } from '../autocomplete/CategoryAutocomplete'; import { Block } from '../common/Block'; @@ -14,21 +13,22 @@ import { type CommonModalProps } from '../Modals'; type ConfirmCategoryDeleteProps = { modalProps: CommonModalProps; - category: CategoryGroupEntity; - group: CategoryGroupEntity; - categoryGroups: CategoryGroupEntity[]; + category: string; + group: string; onDelete: (categoryId: string) => void; }; export function ConfirmCategoryDelete({ modalProps, - category, - group, - categoryGroups, + group: groupId, + category: categoryId, onDelete, }: ConfirmCategoryDeleteProps) { const [transferCategory, setTransferCategory] = useState<string | null>(null); const [error, setError] = useState<string | null>(null); + const { grouped: categoryGroups, list: categories } = useCategories(); + const group = categoryGroups.find(g => g.id === groupId); + const category = categories.find(c => c.id === categoryId); const renderError = (error: string) => { let msg: string; diff --git a/packages/desktop-client/src/components/modals/EditField.jsx b/packages/desktop-client/src/components/modals/EditField.jsx index fa2b492c4c84e8dc225430db107865a146fd8baa..0e2d3f1ff331a792a7c3c171b804e3c316a739a7 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 e7ebb2094700f80cd5ab9b8ab5c587a2f0f65233..83cd8799d0e79d369e47b6ac506db61ce7c835ff 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 89a564b7014b845c0c251e963b8bd92a1b68b608..3bd05e3008cfe13e24474c3b4ced92c957d3bcd1 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 d430346b22315568005aee49fe59e0729f27c7ff..e09f783322a8a35283491e310bc6469f518d5a58 100644 --- a/packages/desktop-client/src/components/modals/LoadBackup.jsx +++ b/packages/desktop-client/src/components/modals/LoadBackup.jsx @@ -2,6 +2,7 @@ import React, { Component, useState, useEffect } from 'react'; import { send, listen, unlisten } from 'loot-core/src/platform/client/fetch'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { theme } from '../../style'; import { Block } from '../common/Block'; import { Button } from '../common/Button'; @@ -55,10 +56,12 @@ export function LoadBackup({ modalProps, }) { const [backups, setBackups] = useState([]); + const [prefsBudgetId] = useLocalPref('id'); + const budgetIdToLoad = budgetId || prefsBudgetId; useEffect(() => { - send('backups-get', { id: budgetId }).then(setBackups); - }, [budgetId]); + send('backups-get', { id: budgetIdToLoad }).then(setBackups); + }, [budgetIdToLoad]); useEffect(() => { if (watchUpdates) { @@ -93,7 +96,9 @@ export function LoadBackup({ </Block> <Button type="primary" - onClick={() => actions.loadBackup(budgetId, latestBackup.id)} + onClick={() => + actions.loadBackup(budgetIdToLoad, latestBackup.id) + } > Revert to original version </Button> @@ -125,7 +130,7 @@ export function LoadBackup({ ) : ( <BackupTable backups={previousBackups} - onSelect={id => actions.loadBackup(budgetId, id)} + onSelect={id => actions.loadBackup(budgetIdToLoad, id)} /> )} </View> diff --git a/packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx b/packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx index 5e36e4f9d994ea8c9237132229fdff820de338b6..33ca3d70328f19ce281c4a2474dde4d9e341f39b 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 8d8d82475bc1e5f7b4b448eb0a3b1a3e3350647c..8f74791de538268e86f47962a5c19ab0830fe544 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 80aada7a8d5376c2aff27f9446f010f62d86346f..eb04d7e86feba349ecb2c4eec6c3decd6fceb1d0 100644 --- a/packages/desktop-client/src/components/modals/SwitchBudgetType.tsx +++ b/packages/desktop-client/src/components/modals/SwitchBudgetType.tsx @@ -1,10 +1,7 @@ // @ts-strict-ignore import React from 'react'; -import { useSelector } from 'react-redux'; - -import { type State } from 'loot-core/src/client/state-types'; -import { type PrefsState } from 'loot-core/src/client/state-types/prefs'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { Button } from '../common/Button'; import { ExternalLink } from '../common/ExternalLink'; import { Modal } from '../common/Modal'; @@ -21,9 +18,7 @@ export function SwitchBudgetType({ modalProps, onSwitch, }: SwitchBudgetTypeProps) { - const budgetType = useSelector<State, PrefsState['local']['budgetType']>( - state => state.prefs.local.budgetType, - ); + const [budgetType] = useLocalPref('budgetType'); return ( <Modal title="Switch budget type?" {...modalProps}> {() => ( diff --git a/packages/desktop-client/src/components/payees/ManagePayeesWithData.jsx b/packages/desktop-client/src/components/payees/ManagePayeesWithData.jsx index a828c5e73f2774647cf4cc3253b63a875409ee8f..55ea8bc78ae0e8955018bb4468d755360108ef0a 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 e0b8163d6dadabe7278708b412e5c66b3ea51a86..539aeda890fa57448a3ef9ed7516e4d0f092cd48 100644 --- a/packages/desktop-client/src/components/reports/Overview.jsx +++ b/packages/desktop-client/src/components/reports/Overview.jsx @@ -1,8 +1,8 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import { useReports } from 'loot-core/src/client/data-hooks/reports'; +import { useAccounts } from '../../hooks/useAccounts'; import { useFeatureFlag } from '../../hooks/useFeatureFlag'; import { styles } from '../../style'; import { View } from '../common/View'; @@ -18,7 +18,7 @@ export function Overview() { const customReportsFeatureFlag = useFeatureFlag('customReports'); - const accounts = useSelector(state => state.queries.accounts); + const accounts = useAccounts(); return ( <View style={{ diff --git a/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx b/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx index a261be8e3537ba7c2ed2a493377872cc1617f7b0..54b1420301fe7a17b177765ac5b52b563f6c5f6e 100644 --- a/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx +++ b/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx @@ -13,13 +13,13 @@ import { ResponsiveContainer, } from 'recharts'; -import { usePrivacyMode } from 'loot-core/src/client/privacy'; import { amountToCurrency, amountToCurrencyNoDecimal, } from 'loot-core/src/shared/util'; import { type GroupedEntity } from 'loot-core/src/types/models/reports'; +import { usePrivacyMode } from '../../../hooks/usePrivacyMode'; import { theme } from '../../../style'; import { type CSSProperties } from '../../../style'; import { AlignedText } from '../../common/AlignedText'; diff --git a/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx b/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx index 3d81b5ced335564289f0ec489fe141699e634bd8..6263c66513c0a9c5b7c1c03e1d221fdbf06ab240 100644 --- a/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx +++ b/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx @@ -15,13 +15,13 @@ import { ResponsiveContainer, } from 'recharts'; -import { usePrivacyMode } from 'loot-core/src/client/privacy'; import { amountToCurrency, amountToCurrencyNoDecimal, } from 'loot-core/src/shared/util'; import { type GroupedEntity } from 'loot-core/src/types/models/reports'; +import { usePrivacyMode } from '../../../hooks/usePrivacyMode'; import { theme } from '../../../style'; import { type CSSProperties } from '../../../style'; import { AlignedText } from '../../common/AlignedText'; diff --git a/packages/desktop-client/src/components/reports/graphs/CashFlowGraph.tsx b/packages/desktop-client/src/components/reports/graphs/CashFlowGraph.tsx index e186a70b64b93ad89184a33c2c49fe07e160819f..8334fa9de80b1b4c452bb75cd5f23ff7340a1864 100644 --- a/packages/desktop-client/src/components/reports/graphs/CashFlowGraph.tsx +++ b/packages/desktop-client/src/components/reports/graphs/CashFlowGraph.tsx @@ -15,12 +15,12 @@ import { type TooltipProps, } from 'recharts'; -import { usePrivacyMode } from 'loot-core/src/client/privacy'; import { amountToCurrency, amountToCurrencyNoDecimal, } from 'loot-core/src/shared/util'; +import { usePrivacyMode } from '../../../hooks/usePrivacyMode'; import { theme } from '../../../style'; import { AlignedText } from '../../common/AlignedText'; import { chartTheme } from '../chart-theme'; diff --git a/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx b/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx index ab2fcf993b3ad601e69194a0e1e089a8f1de3556..777f4be4c7cfb762ee1c553e7a92f502b194f0c0 100644 --- a/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx +++ b/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx @@ -13,13 +13,13 @@ import { ResponsiveContainer, } from 'recharts'; -import { usePrivacyMode } from 'loot-core/src/client/privacy'; import { amountToCurrency, amountToCurrencyNoDecimal, } from 'loot-core/src/shared/util'; import { type GroupedEntity } from 'loot-core/src/types/models/reports'; +import { usePrivacyMode } from '../../../hooks/usePrivacyMode'; import { theme } from '../../../style'; import { type CSSProperties } from '../../../style'; import { AlignedText } from '../../common/AlignedText'; diff --git a/packages/desktop-client/src/components/reports/reports/CustomReport.jsx b/packages/desktop-client/src/components/reports/reports/CustomReport.jsx index f786572ef3df8f56470d774c133fb5da6cf7cc6c..139f4716f95070e22427b4a0378337327bbd62de 100644 --- a/packages/desktop-client/src/components/reports/reports/CustomReport.jsx +++ b/packages/desktop-client/src/components/reports/reports/CustomReport.jsx @@ -1,18 +1,17 @@ import React, { useState, useEffect, useMemo } from 'react'; -import { useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; import * as d from 'date-fns'; -import { useCachedAccounts } from 'loot-core/src/client/data-hooks/accounts'; -import { useCachedPayees } from 'loot-core/src/client/data-hooks/payees'; import { send } from 'loot-core/src/platform/client/fetch'; import * as monthUtils from 'loot-core/src/shared/months'; import { amountToCurrency } from 'loot-core/src/shared/util'; -import { useActions } from '../../../hooks/useActions'; +import { useAccounts } from '../../../hooks/useAccounts'; import { useCategories } from '../../../hooks/useCategories'; import { useFilters } from '../../../hooks/useFilters'; +import { useLocalPref } from '../../../hooks/useLocalPref'; +import { usePayees } from '../../../hooks/usePayees'; import { theme, styles } from '../../../style'; import { AlignedText } from '../../common/AlignedText'; import { Block } from '../../common/Block'; @@ -36,13 +35,12 @@ import { fromDateRepr } from '../util'; export function CustomReport() { const categories = useCategories(); - const viewLegend = - useSelector(state => state.prefs.local?.reportsViewLegend) || false; - const viewSummary = - useSelector(state => state.prefs.local?.reportsViewSummary) || false; - const viewLabels = - useSelector(state => state.prefs.local?.reportsViewLabel) || false; - const { savePrefs } = useActions(); + const [viewLegend = false, setViewLegendPref] = + useLocalPref('reportsViewLegend'); + const [viewSummary = false, setViewSummaryPref] = + useLocalPref('reportsViewSummary'); + const [viewLabels = false, setViewLabelsPref] = + useLocalPref('reportsViewLabel'); const { filters, @@ -126,8 +124,8 @@ export function CustomReport() { }, []); const balanceTypeOp = ReportOptions.balanceTypeMap.get(balanceType); - const payees = useCachedPayees(); - const accounts = useCachedAccounts(); + const payees = usePayees(); + const accounts = useAccounts(); const getGroupData = useMemo(() => { return createGroupedSpreadsheet({ @@ -235,13 +233,13 @@ export function CustomReport() { const onChangeViews = (viewType, status) => { if (viewType === 'viewLegend') { - savePrefs({ reportsViewLegend: status ?? !viewLegend }); + setViewLegendPref(status ?? !viewLegend); } if (viewType === 'viewSummary') { - savePrefs({ reportsViewSummary: !viewSummary }); + setViewSummaryPref(!viewSummary); } if (viewType === 'viewLabels') { - savePrefs({ reportsViewLabel: !viewLabels }); + setViewLabelsPref(!viewLabels); } }; diff --git a/packages/desktop-client/src/components/reports/reports/NetWorth.jsx b/packages/desktop-client/src/components/reports/reports/NetWorth.jsx index 5bdd779b24f213017d33ced0f20a1b4f25c41af2..4dbec9a8e056aa319976eb2b033b070c574b11eb 100644 --- a/packages/desktop-client/src/components/reports/reports/NetWorth.jsx +++ b/packages/desktop-client/src/components/reports/reports/NetWorth.jsx @@ -1,5 +1,4 @@ import React, { useState, useEffect, useMemo } from 'react'; -import { useSelector } from 'react-redux'; import * as d from 'date-fns'; @@ -7,6 +6,7 @@ import { send } from 'loot-core/src/platform/client/fetch'; import * as monthUtils from 'loot-core/src/shared/months'; import { integerToCurrency } from 'loot-core/src/shared/util'; +import { useAccounts } from '../../../hooks/useAccounts'; import { useFilters } from '../../../hooks/useFilters'; import { theme, styles } from '../../../style'; import { Paragraph } from '../../common/Paragraph'; @@ -20,7 +20,7 @@ import { useReport } from '../useReport'; import { fromDateRepr } from '../util'; export function NetWorth() { - const accounts = useSelector(state => state.queries.accounts); + const accounts = useAccounts(); const { filters, saved, diff --git a/packages/desktop-client/src/components/reports/spreadsheets/filterEmptyRows.ts b/packages/desktop-client/src/components/reports/spreadsheets/filterEmptyRows.ts index 45efef476f4ed0af4fd70f0853f67170caf35dce..c0b6e8f3443f004dda1b964f276305bf1695fecb 100644 --- a/packages/desktop-client/src/components/reports/spreadsheets/filterEmptyRows.ts +++ b/packages/desktop-client/src/components/reports/spreadsheets/filterEmptyRows.ts @@ -1,5 +1,5 @@ // @ts-strict-ignore -import { type GroupedEntity } from 'loot-core/types/models/reports'; +import { type GroupedEntity } from 'loot-core/src/types/models/reports'; export function filterEmptyRows( showEmpty: boolean, diff --git a/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts b/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts index 0adc9b119360ad34aebe30a993632c3243f4158b..f44cddf8ca43d4ec9f0ea943abf56741580f1b06 100644 --- a/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts +++ b/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts @@ -3,7 +3,7 @@ import { runQuery } from 'loot-core/src/client/query-helpers'; import { send } from 'loot-core/src/platform/client/fetch'; import * as monthUtils from 'loot-core/src/shared/months'; import { integerToAmount } from 'loot-core/src/shared/util'; -import { type GroupedEntity } from 'loot-core/types/models/reports'; +import { type GroupedEntity } from 'loot-core/src/types/models/reports'; import { categoryLists } from '../ReportOptions'; diff --git a/packages/desktop-client/src/components/rules/ScheduleValue.tsx b/packages/desktop-client/src/components/rules/ScheduleValue.tsx index 036d0a2ed37226ee83fb0e12c740a04a23331771..fb05b0b8f2dddbed526705c1e75a191298d1defa 100644 --- a/packages/desktop-client/src/components/rules/ScheduleValue.tsx +++ b/packages/desktop-client/src/components/rules/ScheduleValue.tsx @@ -1,12 +1,11 @@ import React from 'react'; -import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type QueriesState } from 'loot-core/client/state-types/queries'; import { getPayeesById } from 'loot-core/src/client/reducers/queries'; import { describeSchedule } from 'loot-core/src/shared/schedules'; import { type ScheduleEntity } from 'loot-core/src/types/models'; +import { usePayees } from '../../hooks/usePayees'; + import { SchedulesQuery } from './SchedulesQuery'; import { Value } from './Value'; @@ -15,9 +14,7 @@ type ScheduleValueProps = { }; export function ScheduleValue({ value }: ScheduleValueProps) { - const payees = useSelector<State, QueriesState['payees']>( - state => state.queries.payees, - ); + const payees = usePayees(); const byId = getPayeesById(payees); const { data: schedules } = SchedulesQuery.useQuery(); diff --git a/packages/desktop-client/src/components/rules/Value.tsx b/packages/desktop-client/src/components/rules/Value.tsx index fe9ccc73c2f701a6349a2ddef9b16ba0cbb319ae..d8b184b39d5c1c2ae6a0a6071224ea459c153c69 100644 --- a/packages/desktop-client/src/components/rules/Value.tsx +++ b/packages/desktop-client/src/components/rules/Value.tsx @@ -1,17 +1,16 @@ // @ts-strict-ignore import React, { useState } from 'react'; -import { useSelector } from 'react-redux'; import { format as formatDate, parseISO } from 'date-fns'; -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; -import { type QueriesState } from 'loot-core/client/state-types/queries'; import { getMonthYearFormat } from 'loot-core/src/shared/months'; import { getRecurringDescription } from 'loot-core/src/shared/schedules'; import { integerToCurrency } from 'loot-core/src/shared/util'; +import { useAccounts } from '../../hooks/useAccounts'; import { useCategories } from '../../hooks/useCategories'; +import { useDateFormat } from '../../hooks/useDateFormat'; +import { usePayees } from '../../hooks/usePayees'; import { type CSSProperties, theme } from '../../style'; import { LinkButton } from '../common/LinkButton'; import { Text } from '../common/Text'; @@ -36,16 +35,10 @@ export function Value<T>({ describe = x => x.name, style, }: ValueProps<T>) { - const dateFormat = useSelector<State, PrefsState['local']['dateFormat']>( - state => state.prefs.local.dateFormat || 'MM/dd/yyyy', - ); - const payees = useSelector<State, QueriesState['payees']>( - state => state.queries.payees, - ); + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; + const payees = usePayees(); const { list: categories } = useCategories(); - const accounts = useSelector<State, QueriesState['accounts']>( - state => state.queries.accounts, - ); + const accounts = useAccounts(); const valueStyle = { color: theme.pageTextPositive, ...style, diff --git a/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx b/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx index 03a0aebaf00326a14a5df228f6aa0a31786d9c81..1f62f40c8f439936a2324310caff0dd28ce67743 100644 --- a/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx +++ b/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx @@ -1,9 +1,6 @@ // @ts-strict-ignore import React, { useState } from 'react'; -import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; import { runQuery } from 'loot-core/src/client/query-helpers'; import { send } from 'loot-core/src/platform/client/fetch'; import { q } from 'loot-core/src/shared/query'; @@ -11,6 +8,7 @@ import { getRecurringDescription } from 'loot-core/src/shared/schedules'; import type { DiscoverScheduleEntity } from 'loot-core/src/types/models'; import type { BoundActions } from '../../hooks/useActions'; +import { useDateFormat } from '../../hooks/useDateFormat'; import { useSelected, useSelectedDispatch, @@ -41,9 +39,7 @@ function DiscoverSchedulesTable({ }) { const selectedItems = useSelectedItems(); const dispatchSelected = useSelectedDispatch(); - const dateFormat = useSelector<State, PrefsState['local']['dateFormat']>( - state => state.prefs.local.dateFormat || 'MM/dd/yyyy', - ); + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; function renderItem({ item }: { item: DiscoverScheduleEntity }) { const selected = selectedItems.has(item.id); diff --git a/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx b/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx index 85446055b9221f805b15c943062167216b62b984..56d20b4426521b38a064be9e3fe44a3620c9ecac 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 b2ec6ea021c717a2fa34d90b6815e1b3b10bc273..ba34e2df104192b6dd2d7e839553d446408f910c 100644 --- a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx +++ b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx @@ -1,11 +1,6 @@ // @ts-strict-ignore import React, { useState, useMemo, type CSSProperties } from 'react'; -import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; -import { useCachedAccounts } from 'loot-core/src/client/data-hooks/accounts'; -import { useCachedPayees } from 'loot-core/src/client/data-hooks/payees'; import { type ScheduleStatusType, type ScheduleStatuses, @@ -15,6 +10,9 @@ import { getScheduledAmount } from 'loot-core/src/shared/schedules'; import { integerToCurrency } from 'loot-core/src/shared/util'; import { type ScheduleEntity } from 'loot-core/src/types/models'; +import { useAccounts } from '../../hooks/useAccounts'; +import { useDateFormat } from '../../hooks/useDateFormat'; +import { usePayees } from '../../hooks/usePayees'; import { SvgDotsHorizontalTriple } from '../../icons/v1'; import { SvgCheck } from '../../icons/v2'; import { theme } from '../../style'; @@ -196,16 +194,11 @@ export function SchedulesTable({ onAction, tableStyle, }: SchedulesTableProps) { - const dateFormat = useSelector<State, PrefsState['local']['dateFormat']>( - state => { - return state.prefs.local.dateFormat || 'MM/dd/yyyy'; - }, - ); - + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; const [showCompleted, setShowCompleted] = useState(false); - const payees = useCachedPayees(); - const accounts = useCachedAccounts(); + const payees = usePayees(); + const accounts = useAccounts(); const filteredSchedules = useMemo(() => { if (!filter) { @@ -240,7 +233,7 @@ export function SchedulesTable({ filterIncludes(dateStr) ); }); - }, [schedules, filter, statuses]); + }, [payees, accounts, schedules, filter, statuses]); const items: SchedulesTableItem[] = useMemo(() => { const unCompletedSchedules = filteredSchedules.filter(s => !s.completed); diff --git a/packages/desktop-client/src/components/select/DateSelect.tsx b/packages/desktop-client/src/components/select/DateSelect.tsx index a6b83fc65f9f0ea4b05887ef07d4dc03e0b1dbb7..6893edc8d268089766094c6f166c6a65b6e3821f 100644 --- a/packages/desktop-client/src/components/select/DateSelect.tsx +++ b/packages/desktop-client/src/components/select/DateSelect.tsx @@ -10,15 +10,12 @@ import React, { type MutableRefObject, type KeyboardEvent, } from 'react'; -import { useSelector } from 'react-redux'; import { parse, parseISO, format, subDays, addDays, isValid } from 'date-fns'; import Pikaday from 'pikaday'; import 'pikaday/css/pikaday.css'; -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; import { getDayMonthFormat, getDayMonthRegex, @@ -28,6 +25,7 @@ import { } from 'loot-core/src/shared/months'; import { stringToInteger } from 'loot-core/src/shared/util'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { type CSSProperties, theme } from '../../style'; import { Input, type InputProps } from '../common/Input'; import { View, type ViewProps } from '../common/View'; @@ -233,14 +231,8 @@ export function DateSelect({ const [selectedValue, setSelectedValue] = useState(value); const userSelectedValue = useRef(selectedValue); - const firstDayOfWeekIdx = useSelector< - State, - PrefsState['local']['firstDayOfWeekIdx'] - >(state => - state.prefs.local?.firstDayOfWeekIdx - ? state.prefs.local.firstDayOfWeekIdx - : '0', - ); + const [_firstDayOfWeekIdx] = useLocalPref('firstDayOfWeekIdx'); + const firstDayOfWeekIdx = _firstDayOfWeekIdx || '0'; useEffect(() => { userSelectedValue.current = value; diff --git a/packages/desktop-client/src/components/select/RecurringSchedulePicker.jsx b/packages/desktop-client/src/components/select/RecurringSchedulePicker.jsx index df10237168f60ab9f610fb74661b11ca4ba174af..17ee82e87c29c5de3077183eaa75c7d580a9824e 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 cfe994b13a3b62d2c74275939f086181cc3b14d1..97a7660ce2f6476ef162e8f19044de5a9a04de39 100644 --- a/packages/desktop-client/src/components/settings/Encryption.tsx +++ b/packages/desktop-client/src/components/settings/Encryption.tsx @@ -1,11 +1,8 @@ // @ts-strict-ignore import React from 'react'; -import { useSelector } from 'react-redux'; - -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; import { useActions } from '../../hooks/useActions'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { theme } from '../../style'; import { Button } from '../common/Button'; import { ExternalLink } from '../common/ExternalLink'; @@ -17,9 +14,7 @@ import { Setting } from './UI'; export function EncryptionSettings() { const { pushModal } = useActions(); const serverURL = useServerURL(); - const encryptKeyId = useSelector<State, PrefsState['local']['encryptKeyId']>( - state => state.prefs.local.encryptKeyId, - ); + const [encryptKeyId] = useLocalPref('encryptKeyId'); const missingCryptoAPI = !(window.crypto && crypto.subtle); diff --git a/packages/desktop-client/src/components/settings/Experimental.tsx b/packages/desktop-client/src/components/settings/Experimental.tsx index fc1a942c521c571646de45065112327c9575b618..15f20989fecf4cbf98cb8bf2707eac438bbb7912 100644 --- a/packages/desktop-client/src/components/settings/Experimental.tsx +++ b/packages/desktop-client/src/components/settings/Experimental.tsx @@ -1,12 +1,9 @@ import { type ReactNode, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/src/client/state-types'; -import { type PrefsState } from 'loot-core/src/client/state-types/prefs'; import type { FeatureFlag } from 'loot-core/src/types/prefs'; -import { useActions } from '../../hooks/useActions'; import { useFeatureFlag } from '../../hooks/useFeatureFlag'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { theme } from '../../style'; import { LinkButton } from '../common/LinkButton'; import { Text } from '../common/Text'; @@ -23,23 +20,20 @@ type FeatureToggleProps = { }; function FeatureToggle({ - flag, + flag: flagName, disableToggle = false, error, children, }: FeatureToggleProps) { - const { savePrefs } = useActions(); - const enabled = useFeatureFlag(flag); + const enabled = useFeatureFlag(flagName); + const [_, setFlagPref] = useLocalPref(`flags.${flagName}`); return ( <label style={{ display: 'flex' }}> <Checkbox checked={enabled} onChange={() => { - // @ts-expect-error key type is not correctly inferred - savePrefs({ - [`flags.${flag}`]: !enabled, - }); + setFlagPref(!enabled); }} disabled={disableToggle} /> @@ -63,9 +57,7 @@ function FeatureToggle({ } function ReportBudgetFeature() { - const budgetType = useSelector<State, PrefsState['local']['budgetType']>( - state => state.prefs.local?.budgetType, - ); + const [budgetType] = useLocalPref('budgetType'); const enabled = useFeatureFlag('reportBudget'); const blockToggleOff = budgetType === 'report' && enabled; return ( diff --git a/packages/desktop-client/src/components/settings/Export.tsx b/packages/desktop-client/src/components/settings/Export.tsx index 6cd2e7449894ab378ea4c30bb0f75359a3ef6c96..824c0f4fa41b7f96f6d7284f05a435b9d9b8afb3 100644 --- a/packages/desktop-client/src/components/settings/Export.tsx +++ b/packages/desktop-client/src/components/settings/Export.tsx @@ -1,13 +1,11 @@ // @ts-strict-ignore import React, { useState } from 'react'; -import { useSelector } from 'react-redux'; import { format } from 'date-fns'; -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; import { send } from 'loot-core/src/platform/client/fetch'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { theme } from '../../style'; import { Block } from '../common/Block'; import { ButtonWithLoading } from '../common/Button'; @@ -18,12 +16,8 @@ import { Setting } from './UI'; export function ExportBudget() { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState<string | null>(null); - const budgetId = useSelector<State, PrefsState['local']['id']>( - state => state.prefs.local.id, - ); - const encryptKeyId = useSelector<State, PrefsState['local']['encryptKeyId']>( - state => state.prefs.local.encryptKeyId, - ); + const [budgetId] = useLocalPref('id'); + const [encryptKeyId] = useLocalPref('encryptKeyId'); async function onExport() { setIsLoading(true); diff --git a/packages/desktop-client/src/components/settings/Format.tsx b/packages/desktop-client/src/components/settings/Format.tsx index 8f36cc434b331f37f75cf2731b5239b6e1f52ec7..0dc2fbf0387c431b2e7d9a6e244336f49ca87992 100644 --- a/packages/desktop-client/src/components/settings/Format.tsx +++ b/packages/desktop-client/src/components/settings/Format.tsx @@ -1,20 +1,18 @@ // @ts-strict-ignore import React, { type ReactNode } from 'react'; -import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; import { numberFormats } from 'loot-core/src/shared/util'; import { type LocalPrefs } from 'loot-core/src/types/prefs'; -import { useActions } from '../../hooks/useActions'; +import { useDateFormat } from '../../hooks/useDateFormat'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { tokens } from '../../tokens'; import { Button } from '../common/Button'; import { Select } from '../common/Select'; import { Text } from '../common/Text'; import { View } from '../common/View'; import { Checkbox } from '../forms'; -import { useSidebar } from '../sidebar'; +import { useSidebar } from '../sidebar/SidebarProvider'; import { Setting } from './UI'; @@ -56,24 +54,16 @@ function Column({ title, children }: { title: string; children: ReactNode }) { } export function FormatSettings() { - const { savePrefs } = useActions(); - const sidebar = useSidebar(); - const firstDayOfWeekIdx = useSelector< - State, - PrefsState['local']['firstDayOfWeekIdx'] - >( - state => state.prefs.local.firstDayOfWeekIdx || '0', // Sunday - ); - const dateFormat = useSelector<State, PrefsState['local']['dateFormat']>( - state => state.prefs.local.dateFormat || 'MM/dd/yyyy', - ); - const numberFormat = useSelector<State, PrefsState['local']['numberFormat']>( - state => state.prefs.local.numberFormat || 'comma-dot', - ); - const hideFraction = useSelector<State, PrefsState['local']['hideFraction']>( - state => state.prefs.local.hideFraction, - ); + const [_firstDayOfWeekIdx, setFirstDayOfWeekIdxPref] = + useLocalPref('firstDayOfWeekIdx'); // Sunday; + const firstDayOfWeekIdx = _firstDayOfWeekIdx || '0'; + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; + const [, setDateFormatPref] = useLocalPref('dateFormat'); + const [_numberFormat, setNumberFormatPref] = useLocalPref('numberFormat'); + const numberFormat = _numberFormat || 'comma-dot'; + const [hideFraction = false, setHideFractionPref] = + useLocalPref('hideFraction'); return ( <Setting @@ -98,7 +88,7 @@ export function FormatSettings() { bare key={String(hideFraction)} // needed because label does not update value={numberFormat} - onChange={format => savePrefs({ numberFormat: format })} + onChange={format => setNumberFormatPref(format)} options={numberFormats.map(f => [ f.value, hideFraction ? f.labelNoFraction : f.label, @@ -111,9 +101,7 @@ export function FormatSettings() { <Checkbox id="settings-textDecimal" checked={!!hideFraction} - onChange={e => - savePrefs({ hideFraction: e.currentTarget.checked }) - } + onChange={e => setHideFractionPref(e.currentTarget.checked)} /> <label htmlFor="settings-textDecimal">Hide decimal places</label> </Text> @@ -124,7 +112,7 @@ export function FormatSettings() { <Select bare value={dateFormat} - onChange={format => savePrefs({ dateFormat: format })} + onChange={format => setDateFormatPref(format)} options={dateFormats.map(f => [f.value, f.label])} style={{ padding: '2px 10px', fontSize: 15 }} /> @@ -136,7 +124,7 @@ export function FormatSettings() { <Select bare value={firstDayOfWeekIdx} - onChange={idx => savePrefs({ firstDayOfWeekIdx: idx })} + onChange={idx => setFirstDayOfWeekIdxPref(idx)} options={daysOfWeek.map(f => [f.value, f.label])} style={{ padding: '2px 10px', fontSize: 15 }} /> diff --git a/packages/desktop-client/src/components/settings/Global.tsx b/packages/desktop-client/src/components/settings/Global.tsx index aad153e5ebb9df6cd94b2fbe4d3569c2b9d72034..92e4ba843a871fbf15cad60f2940fbfea4ddeb4c 100644 --- a/packages/desktop-client/src/components/settings/Global.tsx +++ b/packages/desktop-client/src/components/settings/Global.tsx @@ -1,11 +1,7 @@ // @ts-strict-ignore import React, { useState, useEffect, useRef } from 'react'; -import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; - -import { useActions } from '../../hooks/useActions'; +import { useGlobalPref } from '../../hooks/useGlobalPref'; import { theme } from '../../style'; import { Information } from '../alerts'; import { Button } from '../common/Button'; @@ -15,10 +11,7 @@ import { View } from '../common/View'; import { Setting } from './UI'; export function GlobalSettings() { - const documentDir = useSelector<State, PrefsState['global']['documentDir']>( - state => state.prefs.global.documentDir, - ); - const { saveGlobalPrefs } = useActions(); + const [documentDir, setDocumentDirPref] = useGlobalPref('documentDir'); const [documentDirChanged, setDirChanged] = useState(false); const dirScrolled = useRef<HTMLSpanElement>(null); @@ -34,7 +27,7 @@ export function GlobalSettings() { properties: ['openDirectory'], }); if (res) { - saveGlobalPrefs({ documentDir: res[0] }); + setDocumentDirPref(res[0]); setDirChanged(true); } } diff --git a/packages/desktop-client/src/components/settings/Reset.tsx b/packages/desktop-client/src/components/settings/Reset.tsx index 744ada38e2b6b01cbacfd65ac86183959916604b..0e8ea3cea5a2fd1cf97441fd8dd25b2fb051af0f 100644 --- a/packages/desktop-client/src/components/settings/Reset.tsx +++ b/packages/desktop-client/src/components/settings/Reset.tsx @@ -1,12 +1,10 @@ // @ts-strict-ignore import React, { useState } from 'react'; -import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; import { send } from 'loot-core/src/platform/client/fetch'; import { useActions } from '../../hooks/useActions'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { ButtonWithLoading } from '../common/Button'; import { Text } from '../common/Text'; @@ -41,9 +39,8 @@ export function ResetCache() { } export function ResetSync() { - const isEnabled = !!useSelector<State, PrefsState['local']['groupId']>( - state => state.prefs.local.groupId, - ); + const [groupId] = useLocalPref('groupId'); + const isEnabled = !!groupId; const { resetSync } = useActions(); const [resetting, setResetting] = useState(false); diff --git a/packages/desktop-client/src/components/settings/Themes.tsx b/packages/desktop-client/src/components/settings/Themes.tsx index 775ef52534090ff5fe31b076f6a0298180653dad..7966b546a33d2d7c4811d7f7f7b587bf24fc0147 100644 --- a/packages/desktop-client/src/components/settings/Themes.tsx +++ b/packages/desktop-client/src/components/settings/Themes.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { useActions } from '../../hooks/useActions'; +import { type Theme } from 'loot-core/types/prefs'; + import { themeOptions, useTheme } from '../../style'; import { Button } from '../common/Button'; import { Select } from '../common/Select'; @@ -9,17 +10,16 @@ import { Text } from '../common/Text'; import { Setting } from './UI'; export function ThemeSettings() { - const theme = useTheme(); - const { saveGlobalPrefs } = useActions(); + const [theme, switchTheme] = useTheme(); return ( <Setting primaryAction={ <Button bounce={false} style={{ padding: 0 }}> - <Select + <Select<Theme> bare onChange={value => { - saveGlobalPrefs({ theme: value }); + switchTheme(value); }} value={theme} options={themeOptions} diff --git a/packages/desktop-client/src/components/settings/index.tsx b/packages/desktop-client/src/components/settings/index.tsx index 454f415756cba0657e7e53e37505254d60ecd74d..2f7ef5ada70ced36938eda5739c96e19f1dd257e 100644 --- a/packages/desktop-client/src/components/settings/index.tsx +++ b/packages/desktop-client/src/components/settings/index.tsx @@ -1,16 +1,15 @@ // @ts-strict-ignore import React, { type ReactNode, useEffect } from 'react'; -import { useSelector } from 'react-redux'; import { media } from 'glamor'; -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; import * as Platform from 'loot-core/src/client/platform'; import { listen } from 'loot-core/src/platform/client/fetch'; import { useActions } from '../../hooks/useActions'; +import { useGlobalPref } from '../../hooks/useGlobalPref'; import { useLatestVersion, useIsOutdated } from '../../hooks/useLatestVersion'; +import { useLocalPref } from '../../hooks/useLocalPref'; import { useSetThemeColor } from '../../hooks/useSetThemeColor'; import { useResponsive } from '../../ResponsiveProvider'; import { theme } from '../../style'; @@ -91,12 +90,8 @@ function IDName({ children }: { children: ReactNode }) { } function AdvancedAbout() { - const budgetId = useSelector<State, PrefsState['local']['id']>( - state => state.prefs.local.id, - ); - const groupId = useSelector<State, PrefsState['local']['groupId']>( - state => state.prefs.local.groupId, - ); + const [budgetId] = useLocalPref('id'); + const [groupId] = useLocalPref('groupId'); return ( <Setting> @@ -124,13 +119,8 @@ function AdvancedAbout() { } export function Settings() { - const floatingSidebar = useSelector< - State, - PrefsState['global']['floatingSidebar'] - >(state => state.prefs.global.floatingSidebar); - const budgetName = useSelector<State, PrefsState['local']['budgetName']>( - state => state.prefs.local.budgetName, - ); + const [floatingSidebar] = useGlobalPref('floatingSidebar'); + const [budgetName] = useLocalPref('budgetName'); const { loadPrefs, closeBudget } = useActions(); diff --git a/packages/desktop-client/src/components/sidebar/Accounts.tsx b/packages/desktop-client/src/components/sidebar/Accounts.tsx index cd20ccb58562f69b6de0aef7e9d12e0bb78ad762..e85e32e4a193508811955b8527b38d6e45988bc6 100644 --- a/packages/desktop-client/src/components/sidebar/Accounts.tsx +++ b/packages/desktop-client/src/components/sidebar/Accounts.tsx @@ -1,12 +1,17 @@ // @ts-strict-ignore -import React, { useState, useMemo } from 'react'; +import React, { useState } from 'react'; -import { type AccountEntity } from 'loot-core/src/types/models'; +import * as queries from 'loot-core/src/client/queries'; +import { useBudgetedAccounts } from '../../hooks/useBudgetedAccounts'; +import { useClosedAccounts } from '../../hooks/useClosedAccounts'; +import { useFailedAccounts } from '../../hooks/useFailedAccounts'; +import { useLocalPref } from '../../hooks/useLocalPref'; +import { useOffBudgetAccounts } from '../../hooks/useOffBudgetAccounts'; +import { useUpdatedAccounts } from '../../hooks/useUpdatedAccounts'; import { SvgAdd } from '../../icons/v1'; import { View } from '../common/View'; import { type OnDropCallback } from '../sort'; -import { type Binding } from '../spreadsheet'; import { Account } from './Account'; import { SecondaryItem } from './SecondaryItem'; @@ -14,65 +19,26 @@ import { SecondaryItem } from './SecondaryItem'; const fontWeight = 600; type AccountsProps = { - accounts: AccountEntity[]; - failedAccounts: Map< - string, - { - type: string; - code: string; - } - >; - updatedAccounts: string[]; - getAccountPath: (account: AccountEntity) => string; - allAccountsPath: string; - budgetedAccountPath: string; - offBudgetAccountPath: string; - getBalanceQuery: (account: AccountEntity) => Binding; - getAllAccountBalance: () => Binding; - getOnBudgetBalance: () => Binding; - getOffBudgetBalance: () => Binding; - showClosedAccounts: boolean; onAddAccount: () => void; onToggleClosedAccounts: () => void; onReorder: OnDropCallback; }; export function Accounts({ - accounts, - failedAccounts, - updatedAccounts, - getAccountPath, - allAccountsPath, - budgetedAccountPath, - offBudgetAccountPath, - getBalanceQuery, - getAllAccountBalance, - getOnBudgetBalance, - getOffBudgetBalance, - showClosedAccounts, onAddAccount, onToggleClosedAccounts, onReorder, }: AccountsProps) { const [isDragging, setIsDragging] = useState(false); - const offbudgetAccounts = useMemo( - () => - accounts.filter( - account => account.closed === 0 && account.offbudget === 1, - ), - [accounts], - ); - const budgetedAccounts = useMemo( - () => - accounts.filter( - account => account.closed === 0 && account.offbudget === 0, - ), - [accounts], - ); - const closedAccounts = useMemo( - () => accounts.filter(account => account.closed === 1), - [accounts], - ); + const failedAccounts = useFailedAccounts(); + const updatedAccounts = useUpdatedAccounts(); + const offbudgetAccounts = useOffBudgetAccounts(); + const budgetedAccounts = useBudgetedAccounts(); + const closedAccounts = useClosedAccounts(); + + const getAccountPath = account => `/accounts/${account.id}`; + + const [showClosedAccounts] = useLocalPref('ui.showClosedAccounts'); function onDragChange(drag) { setIsDragging(drag.state === 'start'); @@ -92,16 +58,16 @@ export function Accounts({ <View> <Account name="All accounts" - to={allAccountsPath} - query={getAllAccountBalance()} + to="/accounts" + query={queries.allAccountBalance()} style={{ fontWeight, marginTop: 15 }} /> {budgetedAccounts.length > 0 && ( <Account name="For budget" - to={budgetedAccountPath} - query={getOnBudgetBalance()} + to="/accounts/budgeted" + query={queries.budgetedAccountBalance()} style={{ fontWeight, marginTop: 13 }} /> )} @@ -115,7 +81,7 @@ export function Accounts({ failed={failedAccounts && failedAccounts.has(account.id)} updated={updatedAccounts && updatedAccounts.includes(account.id)} to={getAccountPath(account)} - query={getBalanceQuery(account)} + query={queries.accountBalance(account)} onDragChange={onDragChange} onDrop={onReorder} outerStyle={makeDropPadding(i)} @@ -125,8 +91,8 @@ export function Accounts({ {offbudgetAccounts.length > 0 && ( <Account name="Off budget" - to={offBudgetAccountPath} - query={getOffBudgetBalance()} + to="/accounts/offbudget" + query={queries.offbudgetAccountBalance()} style={{ fontWeight, marginTop: 13 }} /> )} @@ -140,7 +106,7 @@ export function Accounts({ failed={failedAccounts && failedAccounts.has(account.id)} updated={updatedAccounts && updatedAccounts.includes(account.id)} to={getAccountPath(account)} - query={getBalanceQuery(account)} + query={queries.accountBalance(account)} onDragChange={onDragChange} onDrop={onReorder} outerStyle={makeDropPadding(i)} @@ -163,7 +129,7 @@ export function Accounts({ name={account.name} account={account} to={getAccountPath(account)} - query={getBalanceQuery(account)} + query={queries.accountBalance(account)} onDragChange={onDragChange} onDrop={onReorder} /> diff --git a/packages/desktop-client/src/components/sidebar/Sidebar.tsx b/packages/desktop-client/src/components/sidebar/Sidebar.tsx index dadd3ba95fcab62f03300eb10afc3b917ff831ac..b8d59bbdec60b9c887dbceeb5ddfcb99f5bff3c9 100644 --- a/packages/desktop-client/src/components/sidebar/Sidebar.tsx +++ b/packages/desktop-client/src/components/sidebar/Sidebar.tsx @@ -1,68 +1,74 @@ -import React, { type ReactNode } from 'react'; - +import React, { useState } from 'react'; +import { useDispatch } from 'react-redux'; + +import { + closeBudget, + moveAccount, + replaceModal, +} from 'loot-core/src/client/actions'; import * as Platform from 'loot-core/src/client/platform'; -import { type AccountEntity } from 'loot-core/src/types/models'; +import { useAccounts } from '../../hooks/useAccounts'; +import { useGlobalPref } from '../../hooks/useGlobalPref'; +import { useLocalPref } from '../../hooks/useLocalPref'; +import { useNavigate } from '../../hooks/useNavigate'; +import { SvgExpandArrow } from '../../icons/v0'; import { SvgReports, SvgWallet } from '../../icons/v1'; import { SvgCalendar } from '../../icons/v2'; -import { type CSSProperties, theme } from '../../style'; +import { styles, theme } from '../../style'; +import { Button } from '../common/Button'; +import { InitialFocus } from '../common/InitialFocus'; +import { Input } from '../common/Input'; +import { Menu } from '../common/Menu'; +import { Text } from '../common/Text'; import { View } from '../common/View'; -import { type OnDropCallback } from '../sort'; -import { type Binding } from '../spreadsheet'; +import { Tooltip } from '../tooltips'; import { Accounts } from './Accounts'; import { Item } from './Item'; +import { useSidebar } from './SidebarProvider'; import { ToggleButton } from './ToggleButton'; import { Tools } from './Tools'; -import { useSidebar } from '.'; - export const SIDEBAR_WIDTH = 240; -type SidebarProps = { - style: CSSProperties; - budgetName: ReactNode; - accounts: AccountEntity[]; - failedAccounts: Map< - string, - { - type: string; - code: string; - } - >; - updatedAccounts: string[]; - getBalanceQuery: (account: AccountEntity) => Binding; - getAllAccountBalance: () => Binding; - getOnBudgetBalance: () => Binding; - getOffBudgetBalance: () => Binding; - showClosedAccounts: boolean; - isFloating: boolean; - onFloat: () => void; - onAddAccount: () => void; - onToggleClosedAccounts: () => void; - onReorder: OnDropCallback; -}; - -export function Sidebar({ - style, - budgetName, - accounts, - failedAccounts, - updatedAccounts, - getBalanceQuery, - getAllAccountBalance, - getOnBudgetBalance, - getOffBudgetBalance, - showClosedAccounts, - isFloating, - onFloat, - onAddAccount, - onToggleClosedAccounts, - onReorder, -}: SidebarProps) { +export function Sidebar() { const hasWindowButtons = !Platform.isBrowser && Platform.OS === 'mac'; + const dispatch = useDispatch(); const sidebar = useSidebar(); + const accounts = useAccounts(); + const [showClosedAccounts, setShowClosedAccountsPref] = useLocalPref( + 'ui.showClosedAccounts', + ); + const [isFloating = false, setFloatingSidebarPref] = + useGlobalPref('floatingSidebar'); + + async function onReorder( + id: string, + dropPos: 'top' | 'bottom', + targetId: unknown, + ) { + let targetIdToMove = targetId; + if (dropPos === 'bottom') { + const idx = accounts.findIndex(a => a.id === targetId) + 1; + targetIdToMove = idx < accounts.length ? accounts[idx].id : null; + } + + dispatch(moveAccount(id, targetIdToMove)); + } + + const onFloat = () => { + setFloatingSidebarPref(!isFloating); + }; + + const onAddAccount = () => { + dispatch(replaceModal('add-account')); + }; + + const onToggleClosedAccounts = () => { + setShowClosedAccountsPref(!showClosedAccounts); + }; return ( <View @@ -79,7 +85,8 @@ export function Sidebar({ opacity: 1, width: hasWindowButtons ? null : 'auto', }, - ...style, + flex: 1, + ...styles.darkScrollbar, }} > <View @@ -96,7 +103,7 @@ export function Sidebar({ }), }} > - {budgetName} + <EditableBudgetName /> <View style={{ flex: 1, flexDirection: 'row' }} /> @@ -123,18 +130,6 @@ export function Sidebar({ /> <Accounts - accounts={accounts} - failedAccounts={failedAccounts} - updatedAccounts={updatedAccounts} - getAccountPath={account => `/accounts/${account.id}`} - allAccountsPath="/accounts" - budgetedAccountPath="/accounts/budgeted" - offBudgetAccountPath="/accounts/offbudget" - getBalanceQuery={getBalanceQuery} - getAllAccountBalance={getAllAccountBalance} - getOnBudgetBalance={getOnBudgetBalance} - getOffBudgetBalance={getOffBudgetBalance} - showClosedAccounts={showClosedAccounts} onAddAccount={onAddAccount} onToggleClosedAccounts={onToggleClosedAccounts} onReorder={onReorder} @@ -143,3 +138,90 @@ export function Sidebar({ </View> ); } + +function EditableBudgetName() { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const [budgetName, setBudgetNamePref] = useLocalPref('budgetName'); + const [editing, setEditing] = useState(false); + const [menuOpen, setMenuOpen] = useState(false); + + function onMenuSelect(type: string) { + setMenuOpen(false); + + switch (type) { + case 'rename': + setEditing(true); + break; + case 'settings': + navigate('/settings'); + break; + case 'help': + window.open('https://actualbudget.org/docs/', '_blank'); + break; + case 'close': + dispatch(closeBudget()); + break; + default: + } + } + + const items = [ + { name: 'rename', text: 'Rename budget' }, + { name: 'settings', text: 'Settings' }, + ...(Platform.isBrowser ? [{ name: 'help', text: 'Help' }] : []), + { name: 'close', text: 'Close file' }, + ]; + + if (editing) { + return ( + <InitialFocus> + <Input + style={{ + width: 160, + fontSize: 16, + fontWeight: 500, + }} + defaultValue={budgetName} + onEnter={async e => { + const inputEl = e.target as HTMLInputElement; + const newBudgetName = inputEl.value; + if (newBudgetName.trim() !== '') { + setBudgetNamePref(inputEl.name); + setEditing(false); + } + }} + onBlur={() => setEditing(false)} + /> + </InitialFocus> + ); + } else { + return ( + <Button + type="bare" + color={theme.buttonNormalBorder} + style={{ + fontSize: 16, + fontWeight: 500, + marginLeft: -5, + flex: '0 auto', + }} + onClick={() => setMenuOpen(true)} + > + <Text style={{ whiteSpace: 'nowrap', overflow: 'hidden' }}> + {budgetName || 'A budget has no name'} + </Text> + <SvgExpandArrow width={7} height={7} style={{ marginLeft: 5 }} /> + {menuOpen && ( + <Tooltip + position="bottom-left" + style={{ padding: 0 }} + onClose={() => setMenuOpen(false)} + > + <Menu onMenuSelect={onMenuSelect} items={items} /> + </Tooltip> + )} + </Button> + ); + } +} diff --git a/packages/desktop-client/src/components/sidebar/SidebarProvider.tsx b/packages/desktop-client/src/components/sidebar/SidebarProvider.tsx new file mode 100644 index 0000000000000000000000000000000000000000..717f3393e53e1d881b5a7e9c3b91e36f0c4e2375 --- /dev/null +++ b/packages/desktop-client/src/components/sidebar/SidebarProvider.tsx @@ -0,0 +1,52 @@ +// @ts-strict-ignore +import React, { + createContext, + useState, + useContext, + useMemo, + type ReactNode, + type Dispatch, + type SetStateAction, +} from 'react'; + +import { useGlobalPref } from '../../hooks/useGlobalPref'; +import { useResponsive } from '../../ResponsiveProvider'; + +type SidebarContextValue = { + hidden: boolean; + setHidden: Dispatch<SetStateAction<boolean>>; + floating: boolean; + alwaysFloats: boolean; +}; + +const SidebarContext = createContext<SidebarContextValue>(null); + +type SidebarProviderProps = { + children: ReactNode; +}; + +export function SidebarProvider({ children }: SidebarProviderProps) { + const [floatingSidebar] = useGlobalPref('floatingSidebar'); + const [hidden, setHidden] = useState(true); + const { width } = useResponsive(); + const alwaysFloats = width < 668; + const floating = floatingSidebar || alwaysFloats; + + return ( + <SidebarContext.Provider + value={{ hidden, setHidden, floating, alwaysFloats }} + > + {children} + </SidebarContext.Provider> + ); +} + +export function useSidebar() { + const { hidden, setHidden, floating, alwaysFloats } = + useContext(SidebarContext); + + return useMemo( + () => ({ hidden, setHidden, floating, alwaysFloats }), + [hidden, setHidden, floating, alwaysFloats], + ); +} diff --git a/packages/desktop-client/src/components/sidebar/SidebarWithData.tsx b/packages/desktop-client/src/components/sidebar/SidebarWithData.tsx deleted file mode 100644 index 202e84bfe9c2c51d91af10353e333ba955675026..0000000000000000000000000000000000000000 --- a/packages/desktop-client/src/components/sidebar/SidebarWithData.tsx +++ /dev/null @@ -1,181 +0,0 @@ -// @ts-strict-ignore -import React, { useState, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; - -import { type State } from 'loot-core/client/state-types'; -import { type AccountState } from 'loot-core/client/state-types/account'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; -import { type QueriesState } from 'loot-core/client/state-types/queries'; -import { closeBudget } from 'loot-core/src/client/actions/budgets'; -import * as Platform from 'loot-core/src/client/platform'; -import * as queries from 'loot-core/src/client/queries'; -import { send } from 'loot-core/src/platform/client/fetch'; -import { type LocalPrefs } from 'loot-core/src/types/prefs'; - -import { useActions } from '../../hooks/useActions'; -import { useNavigate } from '../../hooks/useNavigate'; -import { SvgExpandArrow } from '../../icons/v0'; -import { styles, theme } from '../../style'; -import { Button } from '../common/Button'; -import { InitialFocus } from '../common/InitialFocus'; -import { Input } from '../common/Input'; -import { Menu } from '../common/Menu'; -import { Text } from '../common/Text'; -import { Tooltip } from '../tooltips'; - -import { Sidebar } from './Sidebar'; - -type EditableBudgetNameProps = { - prefs: LocalPrefs; - savePrefs: (prefs: Partial<LocalPrefs>) => Promise<void>; -}; - -function EditableBudgetName({ prefs, savePrefs }: EditableBudgetNameProps) { - const dispatch = useDispatch(); - const navigate = useNavigate(); - const [editing, setEditing] = useState(false); - const [menuOpen, setMenuOpen] = useState(false); - - function onMenuSelect(type) { - setMenuOpen(false); - - switch (type) { - case 'rename': - setEditing(true); - break; - case 'settings': - navigate('/settings'); - break; - case 'help': - window.open('https://actualbudget.org/docs/', '_blank'); - break; - case 'close': - dispatch(closeBudget()); - break; - default: - } - } - - const items = [ - { name: 'rename', text: 'Rename budget' }, - { name: 'settings', text: 'Settings' }, - ...(Platform.isBrowser ? [{ name: 'help', text: 'Help' }] : []), - { name: 'close', text: 'Close file' }, - ]; - - const onSaveChanges = async e => { - const inputEl = e.target; - const newBudgetName = inputEl.value; - if (newBudgetName.trim() !== '') { - await savePrefs({ - budgetName: inputEl.value, - }); - setEditing(false); - } - }; - - if (editing) { - return ( - <InitialFocus> - <Input - style={{ - width: 160, - fontSize: 16, - fontWeight: 500, - }} - defaultValue={prefs.budgetName} - onEnter={onSaveChanges} - onBlur={onSaveChanges} - onEscape={() => setEditing(false)} - /> - </InitialFocus> - ); - } else { - return ( - <Button - type="bare" - color={theme.buttonNormalBorder} - style={{ - fontSize: 16, - fontWeight: 500, - marginLeft: -5, - flex: '0 auto', - }} - onClick={() => setMenuOpen(true)} - > - <Text style={{ whiteSpace: 'nowrap', overflow: 'hidden' }}> - {prefs.budgetName || 'A budget has no name'} - </Text> - <SvgExpandArrow width={7} height={7} style={{ marginLeft: 5 }} /> - {menuOpen && ( - <Tooltip - position="bottom-left" - style={{ padding: 0 }} - onClose={() => setMenuOpen(false)} - > - <Menu onMenuSelect={onMenuSelect} items={items} /> - </Tooltip> - )} - </Button> - ); - } -} - -export function SidebarWithData() { - const accounts = useSelector<State, QueriesState['accounts']>( - state => state.queries.accounts, - ); - const failedAccounts = useSelector<State, AccountState['failedAccounts']>( - state => state.account.failedAccounts, - ); - const updatedAccounts = useSelector<State, QueriesState['updatedAccounts']>( - state => state.queries.updatedAccounts, - ); - const prefs = useSelector<State, LocalPrefs>(state => state.prefs.local); - const floatingSidebar = useSelector< - State, - PrefsState['global']['floatingSidebar'] - >(state => state.prefs.global.floatingSidebar); - - const { getAccounts, replaceModal, savePrefs, saveGlobalPrefs } = - useActions(); - - useEffect(() => void getAccounts(), [getAccounts]); - - async function onReorder(id, dropPos, targetId) { - if (dropPos === 'bottom') { - const idx = accounts.findIndex(a => a.id === targetId) + 1; - targetId = idx < accounts.length ? accounts[idx].id : null; - } - - await send('account-move', { id, targetId }); - await getAccounts(); - } - - return ( - <Sidebar - budgetName={<EditableBudgetName prefs={prefs} savePrefs={savePrefs} />} - isFloating={floatingSidebar} - accounts={accounts} - failedAccounts={failedAccounts} - updatedAccounts={updatedAccounts} - getBalanceQuery={queries.accountBalance} - getAllAccountBalance={queries.allAccountBalance} - getOnBudgetBalance={queries.budgetedAccountBalance} - getOffBudgetBalance={queries.offbudgetAccountBalance} - onFloat={() => saveGlobalPrefs({ floatingSidebar: !floatingSidebar })} - onReorder={onReorder} - onAddAccount={() => replaceModal('add-account')} - showClosedAccounts={prefs['ui.showClosedAccounts']} - onToggleClosedAccounts={() => - savePrefs({ - 'ui.showClosedAccounts': !prefs['ui.showClosedAccounts'], - }) - } - style={{ - flex: 1, - ...styles.darkScrollbar, - }} - /> - ); -} diff --git a/packages/desktop-client/src/components/sidebar/index.tsx b/packages/desktop-client/src/components/sidebar/index.tsx index c3f02e448cb791efc14aad0e91d65ebe5be5c570..59f7b1628ad86e58eb71e1e735b8944694107e7e 100644 --- a/packages/desktop-client/src/components/sidebar/index.tsx +++ b/packages/desktop-client/src/components/sidebar/index.tsx @@ -1,71 +1,14 @@ -// @ts-strict-ignore -import React, { - createContext, - useState, - useContext, - useMemo, - type ReactNode, - type Dispatch, - type SetStateAction, -} from 'react'; -import { useSelector } from 'react-redux'; - -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; +import React from 'react'; +import { useGlobalPref } from '../../hooks/useGlobalPref'; import { useResponsive } from '../../ResponsiveProvider'; import { View } from '../common/View'; -import { SIDEBAR_WIDTH } from './Sidebar'; -import { SidebarWithData } from './SidebarWithData'; - -type SidebarContextValue = { - hidden: boolean; - setHidden: Dispatch<SetStateAction<boolean>>; - floating: boolean; - alwaysFloats: boolean; -}; - -const SidebarContext = createContext<SidebarContextValue>(null); - -type SidebarProviderProps = { - children: ReactNode; -}; - -export function SidebarProvider({ children }: SidebarProviderProps) { - const floatingSidebar = useSelector< - State, - PrefsState['global']['floatingSidebar'] - >(state => state.prefs.global.floatingSidebar); - const [hidden, setHidden] = useState(true); - const { width } = useResponsive(); - const alwaysFloats = width < 668; - const floating = floatingSidebar || alwaysFloats; - - return ( - <SidebarContext.Provider - value={{ hidden, setHidden, floating, alwaysFloats }} - > - {children} - </SidebarContext.Provider> - ); -} - -export function useSidebar() { - const { hidden, setHidden, floating, alwaysFloats } = - useContext(SidebarContext); - - return useMemo( - () => ({ hidden, setHidden, floating, alwaysFloats }), - [hidden, setHidden, floating, alwaysFloats], - ); -} +import { SIDEBAR_WIDTH, Sidebar } from './Sidebar'; +import { useSidebar } from './SidebarProvider'; export function FloatableSidebar() { - const floatingSidebar = useSelector< - State, - PrefsState['global']['floatingSidebar'] - >(state => state.prefs.global.floatingSidebar); + const [floatingSidebar] = useGlobalPref('floatingSidebar'); const sidebar = useSidebar(); const { isNarrowWidth } = useResponsive(); @@ -80,11 +23,13 @@ export function FloatableSidebar() { e.stopPropagation(); sidebar.setHidden(false); } - : null + : undefined + } + onMouseLeave={ + sidebarShouldFloat ? () => sidebar.setHidden(true) : undefined } - onMouseLeave={sidebarShouldFloat ? () => sidebar.setHidden(true) : null} style={{ - position: sidebarShouldFloat ? 'absolute' : null, + position: sidebarShouldFloat ? 'absolute' : undefined, top: 12, // If not floating, the -50 takes into account the transform below bottom: sidebarShouldFloat ? 12 : -50, @@ -105,7 +50,7 @@ export function FloatableSidebar() { 'transform .5s, box-shadow .5s, border-radius .5s, bottom .5s', }} > - <SidebarWithData /> + <Sidebar /> </View> ); } diff --git a/packages/desktop-client/src/components/transactions/MobileTransaction.jsx b/packages/desktop-client/src/components/transactions/MobileTransaction.jsx index bda688058a8c25341f8b8ced7f5ae0513d5f8623..bbce43aee6d4860500ea6d5700c3bec45b70c224 100644 --- a/packages/desktop-client/src/components/transactions/MobileTransaction.jsx +++ b/packages/desktop-client/src/components/transactions/MobileTransaction.jsx @@ -46,9 +46,12 @@ import { groupById, } from 'loot-core/src/shared/util'; +import { useAccounts } from '../../hooks/useAccounts'; import { useActions } from '../../hooks/useActions'; import { useCategories } from '../../hooks/useCategories'; +import { useDateFormat } from '../../hooks/useDateFormat'; import { useNavigate } from '../../hooks/useNavigate'; +import { usePayees } from '../../hooks/usePayees'; import { useSetThemeColor } from '../../hooks/useSetThemeColor'; import { SingleActiveEditFormProvider, @@ -939,11 +942,6 @@ function TransactionEditUnconnected(props) { useSetThemeColor(theme.mobileViewTheme); useEffect(() => { - // May as well update categories / accounts when transaction ID changes - props.getCategories(); - props.getAccounts(); - props.getPayees(); - async function fetchTransaction() { // Query for the transaction based on the ID with grouped splits. // @@ -1110,12 +1108,10 @@ function TransactionEditUnconnected(props) { export const TransactionEdit = props => { const { list: categories } = useCategories(); - const payees = useSelector(state => state.queries.payees); + const payees = usePayees(); const lastTransaction = useSelector(state => state.queries.lastTransaction); - const accounts = useSelector(state => state.queries.accounts); - const dateFormat = useSelector( - state => state.prefs.local.dateFormat || 'MM/dd/yyyy', - ); + const accounts = useAccounts(); + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; const actions = useActions(); return ( diff --git a/packages/desktop-client/src/components/transactions/SimpleTransactionsTable.jsx b/packages/desktop-client/src/components/transactions/SimpleTransactionsTable.jsx index ea43f6adbd69b86bb499e8a397b01e24b37d71b6..736df89d280b3a25e3eb7bf379be9c9d8c8155e6 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 db3982cbeac3b9ad67ec25c2014f2519c8b1f7af..34c5f593accb1e7611bd59c5566fc16658227886 100644 --- a/packages/desktop-client/src/components/util/DisplayId.tsx +++ b/packages/desktop-client/src/components/util/DisplayId.tsx @@ -1,9 +1,8 @@ // @ts-strict-ignore import React from 'react'; -import { CachedAccounts } from 'loot-core/src/client/data-hooks/accounts'; -import { CachedPayees } from 'loot-core/src/client/data-hooks/payees'; - +import { useAccount } from '../../hooks/useAccount'; +import { usePayee } from '../../hooks/usePayee'; import { theme } from '../../style'; import { Text } from '../common/Text'; @@ -18,33 +17,33 @@ export function DisplayId({ id, noneColor = theme.pageTextSubdued, }: DisplayIdProps) { - let DataComponent; - - switch (type) { - case 'payees': - DataComponent = CachedPayees; - break; - case 'accounts': - DataComponent = CachedAccounts; - break; - default: - throw new Error('DisplayId: unknown object type: ' + type); - } + return type === 'accounts' ? ( + <AccountDisplayId id={id} noneColor={noneColor} /> + ) : ( + <PayeeDisplayId id={id} noneColor={noneColor} /> + ); +} +function AccountDisplayId({ id, noneColor }) { + const account = useAccount(id); return ( - <DataComponent idKey={true}> - {data => { - const item = data[id]; + <Text + style={account == null ? { color: noneColor } : null} + title={account ? account.name : 'None'} + > + {account ? account.name : 'None'} + </Text> + ); +} - return ( - <Text - style={item == null ? { color: noneColor } : null} - title={item ? item.name : 'None'} - > - {item ? item.name : 'None'} - </Text> - ); - }} - </DataComponent> +function PayeeDisplayId({ id, noneColor }) { + const payee = usePayee(id); + return ( + <Text + style={payee == null ? { color: noneColor } : null} + title={payee ? payee.name : 'None'} + > + {payee ? payee.name : 'None'} + </Text> ); } diff --git a/packages/desktop-client/src/components/util/GenericInput.jsx b/packages/desktop-client/src/components/util/GenericInput.jsx index b42826e01baa868339b82e2f24f0376a21ce4d44..bf2e3c53d302eef47b1775b122c3854d01915a01 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 0000000000000000000000000000000000000000..e3da8f35e6b29ba3bce8f648e8b1d2c948e207ea --- /dev/null +++ b/packages/desktop-client/src/hooks/useAccount.ts @@ -0,0 +1,8 @@ +import { useMemo } from 'react'; + +import { useAccounts } from './useAccounts'; + +export function useAccount(id: string) { + const accounts = useAccounts(); + return useMemo(() => accounts.find(a => a.id === id), [id, accounts]); +} diff --git a/packages/desktop-client/src/hooks/useAccounts.ts b/packages/desktop-client/src/hooks/useAccounts.ts new file mode 100644 index 0000000000000000000000000000000000000000..4c44e9cec937a851e365a1a36e3fb44abfa6a3b9 --- /dev/null +++ b/packages/desktop-client/src/hooks/useAccounts.ts @@ -0,0 +1,20 @@ +import { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { getAccounts } from 'loot-core/src/client/actions'; +import { type State } from 'loot-core/src/client/state-types'; + +export function useAccounts() { + const dispatch = useDispatch(); + const accountsLoaded = useSelector( + (state: State) => state.queries.accountsLoaded, + ); + + useEffect(() => { + if (!accountsLoaded) { + dispatch(getAccounts()); + } + }, []); + + return useSelector(state => state.queries.accounts); +} diff --git a/packages/desktop-client/src/hooks/useBudgetedAccounts.ts b/packages/desktop-client/src/hooks/useBudgetedAccounts.ts new file mode 100644 index 0000000000000000000000000000000000000000..dbd8e1f53db9fd64ec3c6725c3093a85ad455ffd --- /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 8e2730915419afd2caf0e5986a789c89fac64c2c..4c85fdfaff2f298030e2ca8f55f01967d596a72c 100644 --- a/packages/desktop-client/src/hooks/useCategories.ts +++ b/packages/desktop-client/src/hooks/useCategories.ts @@ -1,25 +1,20 @@ import { useEffect } from 'react'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type QueriesState } from 'loot-core/client/state-types/queries'; - -import { useActions } from './useActions'; +import { getCategories } from 'loot-core/src/client/actions'; +import { type State } from 'loot-core/src/client/state-types'; export function useCategories() { - const { getCategories } = useActions(); - - const categories = useSelector<State, QueriesState['categories']['list']>( - state => state.queries.categories.list, + const dispatch = useDispatch(); + const categoriesLoaded = useSelector( + (state: State) => state.queries.categoriesLoaded, ); useEffect(() => { - if (categories.length === 0) { - getCategories(); + if (!categoriesLoaded) { + dispatch(getCategories()); } }, []); - return useSelector<State, QueriesState['categories']>( - state => state.queries.categories, - ); + return useSelector(state => state.queries.categories); } diff --git a/packages/desktop-client/src/hooks/useClosedAccounts.ts b/packages/desktop-client/src/hooks/useClosedAccounts.ts new file mode 100644 index 0000000000000000000000000000000000000000..85aa5b92040174b9232f8ab14c3ce300a323ab30 --- /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 0000000000000000000000000000000000000000..258f156e1fc978566829a11da3f0de6ece4d549b --- /dev/null +++ b/packages/desktop-client/src/hooks/useDateFormat.ts @@ -0,0 +1,7 @@ +import { useSelector } from 'react-redux'; + +import { type State } from 'loot-core/src/client/state-types'; + +export function useDateFormat() { + return useSelector((state: State) => state.prefs.local?.dateFormat); +} diff --git a/packages/desktop-client/src/hooks/useFailedAccounts.ts b/packages/desktop-client/src/hooks/useFailedAccounts.ts new file mode 100644 index 0000000000000000000000000000000000000000..86aeb89959d0f9f047b951dc94c185b41419d91e --- /dev/null +++ b/packages/desktop-client/src/hooks/useFailedAccounts.ts @@ -0,0 +1,7 @@ +import { useSelector } from 'react-redux'; + +import { type State } from 'loot-core/src/client/state-types'; + +export function useFailedAccounts() { + return useSelector((state: State) => state.account.failedAccounts); +} diff --git a/packages/desktop-client/src/hooks/useFeatureFlag.ts b/packages/desktop-client/src/hooks/useFeatureFlag.ts index 40b79b84dba20f16708533742ecbb3caf71f642d..5550cfed2cd0f17084c21e036abdd244f903ef8c 100644 --- a/packages/desktop-client/src/hooks/useFeatureFlag.ts +++ b/packages/desktop-client/src/hooks/useFeatureFlag.ts @@ -1,8 +1,7 @@ // @ts-strict-ignore import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; +import { type State } from 'loot-core/src/client/state-types'; import type { FeatureFlag } from 'loot-core/src/types/prefs'; const DEFAULT_FEATURE_FLAG_STATE: Record<FeatureFlag, boolean> = { @@ -15,13 +14,11 @@ const DEFAULT_FEATURE_FLAG_STATE: Record<FeatureFlag, boolean> = { }; export function useFeatureFlag(name: FeatureFlag): boolean { - return useSelector<State, PrefsState['local'][`flags.${FeatureFlag}`]>( - state => { - const value = state.prefs.local[`flags.${name}`]; + return useSelector((state: State) => { + const value = state.prefs.local[`flags.${name}`]; - return value === undefined - ? DEFAULT_FEATURE_FLAG_STATE[name] || false - : value; - }, - ); + return value === undefined + ? DEFAULT_FEATURE_FLAG_STATE[name] || false + : value; + }); } diff --git a/packages/desktop-client/src/hooks/useGlobalPref.ts b/packages/desktop-client/src/hooks/useGlobalPref.ts new file mode 100644 index 0000000000000000000000000000000000000000..02d5773ac62d0a4c91e9cbf208cc4053a1cb9341 --- /dev/null +++ b/packages/desktop-client/src/hooks/useGlobalPref.ts @@ -0,0 +1,27 @@ +import { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { saveGlobalPrefs } from 'loot-core/src/client/actions'; +import { type State } from 'loot-core/src/client/state-types'; +import { type GlobalPrefs } from 'loot-core/src/types/prefs'; + +type SetGlobalPrefAction<K extends keyof GlobalPrefs> = ( + value: GlobalPrefs[K], +) => void; + +export function useGlobalPref<K extends keyof GlobalPrefs>( + prefName: K, +): [GlobalPrefs[K], SetGlobalPrefAction<K>] { + const dispatch = useDispatch(); + const setGlobalPref = useCallback<SetGlobalPrefAction<K>>( + value => { + dispatch(saveGlobalPrefs({ [prefName]: value } as GlobalPrefs)); + }, + [prefName, dispatch], + ); + const globalPref = useSelector( + (state: State) => state.prefs.global?.[prefName] as GlobalPrefs[K], + ); + + return [globalPref, setGlobalPref]; +} diff --git a/packages/desktop-client/src/hooks/useLocalPref.ts b/packages/desktop-client/src/hooks/useLocalPref.ts new file mode 100644 index 0000000000000000000000000000000000000000..70a50cb554568cf14ea5bc1bb7c48d3178bdcb5f --- /dev/null +++ b/packages/desktop-client/src/hooks/useLocalPref.ts @@ -0,0 +1,27 @@ +import { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { savePrefs } from 'loot-core/src/client/actions'; +import { type State } from 'loot-core/src/client/state-types'; +import { type LocalPrefs } from 'loot-core/src/types/prefs'; + +type SetLocalPrefAction<K extends keyof LocalPrefs> = ( + value: LocalPrefs[K], +) => void; + +export function useLocalPref<K extends keyof LocalPrefs>( + prefName: K, +): [LocalPrefs[K], SetLocalPrefAction<K>] { + const dispatch = useDispatch(); + const setLocalPref = useCallback<SetLocalPrefAction<K>>( + value => { + dispatch(savePrefs({ [prefName]: value } as LocalPrefs)); + }, + [prefName, dispatch], + ); + const localPref = useSelector( + (state: State) => state.prefs.local?.[prefName] as LocalPrefs[K], + ); + + return [localPref, setLocalPref]; +} diff --git a/packages/desktop-client/src/hooks/useLocalPrefs.ts b/packages/desktop-client/src/hooks/useLocalPrefs.ts new file mode 100644 index 0000000000000000000000000000000000000000..870bef808e340661745e538aa3b619e91c60e34d --- /dev/null +++ b/packages/desktop-client/src/hooks/useLocalPrefs.ts @@ -0,0 +1,7 @@ +import { useSelector } from 'react-redux'; + +import { type State } from 'loot-core/src/client/state-types'; + +export function useLocalPrefs() { + return useSelector((state: State) => state.prefs.local); +} diff --git a/packages/desktop-client/src/hooks/useOffBudgetAccounts.ts b/packages/desktop-client/src/hooks/useOffBudgetAccounts.ts new file mode 100644 index 0000000000000000000000000000000000000000..71a5db919bfa3608880bdae6714826f7c4a83df8 --- /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 0000000000000000000000000000000000000000..2606c60a875f4d5d18429c4e4246f0ed0ef12afb --- /dev/null +++ b/packages/desktop-client/src/hooks/usePayee.ts @@ -0,0 +1,8 @@ +import { useMemo } from 'react'; + +import { usePayees } from './usePayees'; + +export function usePayee(id: string) { + const payees = usePayees(); + return useMemo(() => payees.find(p => p.id === id), [id, payees]); +} diff --git a/packages/desktop-client/src/hooks/usePayees.ts b/packages/desktop-client/src/hooks/usePayees.ts new file mode 100644 index 0000000000000000000000000000000000000000..cf51d6b9a7f1baf5977bdae3943596c5bbca3564 --- /dev/null +++ b/packages/desktop-client/src/hooks/usePayees.ts @@ -0,0 +1,20 @@ +import { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { getPayees } from 'loot-core/src/client/actions'; +import { type State } from 'loot-core/src/client/state-types'; + +export function usePayees() { + const dispatch = useDispatch(); + const payeesLoaded = useSelector( + (state: State) => state.queries.payeesLoaded, + ); + + useEffect(() => { + if (!payeesLoaded) { + dispatch(getPayees()); + } + }, []); + + return useSelector(state => state.queries.payees); +} diff --git a/packages/desktop-client/src/hooks/usePrivacyMode.ts b/packages/desktop-client/src/hooks/usePrivacyMode.ts new file mode 100644 index 0000000000000000000000000000000000000000..ffa633bc04f3c7a2c6e58ab426aa9c011b1d1f10 --- /dev/null +++ b/packages/desktop-client/src/hooks/usePrivacyMode.ts @@ -0,0 +1,9 @@ +import { useSelector } from 'react-redux'; + +import { type State } from 'loot-core/src/client/state-types'; + +export function usePrivacyMode() { + return useSelector( + (state: State) => state.prefs.local?.isPrivacyEnabled ?? false, + ); +} diff --git a/packages/desktop-client/src/hooks/useSelected.tsx b/packages/desktop-client/src/hooks/useSelected.tsx index 398350e0dd1d1c63eb10e5551f76815bd94a7bca..63a0437a815169a89dce1a65073923438a560869 100644 --- a/packages/desktop-client/src/hooks/useSelected.tsx +++ b/packages/desktop-client/src/hooks/useSelected.tsx @@ -12,8 +12,7 @@ import React, { } from 'react'; import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type AppState } from 'loot-core/client/state-types/app'; +import { type State } from 'loot-core/src/client/state-types'; import { listen } from 'loot-core/src/platform/client/fetch'; import * as undo from 'loot-core/src/platform/client/undo'; import { type UndoState } from 'loot-core/src/server/undo'; @@ -210,9 +209,7 @@ export function useSelected<T extends Item>( return () => undo.setUndoState('selectedItems', prevState); }, [state.selectedItems]); - const lastUndoState = useSelector<State, AppState['lastUndoState']>( - state => state.app.lastUndoState, - ); + const lastUndoState = useSelector((state: State) => state.app.lastUndoState); useEffect(() => { function onUndo({ messages, undoTag }: UndoState) { diff --git a/packages/desktop-client/src/hooks/useSyncServerStatus.ts b/packages/desktop-client/src/hooks/useSyncServerStatus.ts index 2b48a0c6f5779c3cfb43020229f1063613784851..d788340bee9f2a706c0510bdf8f2f350d6af10c6 100644 --- a/packages/desktop-client/src/hooks/useSyncServerStatus.ts +++ b/packages/desktop-client/src/hooks/useSyncServerStatus.ts @@ -1,7 +1,6 @@ import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type UserState } from 'loot-core/client/state-types/user'; +import { type State } from 'loot-core/src/client/state-types'; import { useServerURL } from '../components/ServerContext'; @@ -9,9 +8,7 @@ export type SyncServerStatus = 'offline' | 'no-server' | 'online'; export function useSyncServerStatus(): SyncServerStatus { const serverUrl = useServerURL(); - const userData = useSelector<State, UserState['data']>( - state => state.user.data, - ); + const userData = useSelector((state: State) => state.user.data); if (!serverUrl) { return 'no-server'; diff --git a/packages/desktop-client/src/hooks/useUpdatedAccounts.ts b/packages/desktop-client/src/hooks/useUpdatedAccounts.ts new file mode 100644 index 0000000000000000000000000000000000000000..483d22c9b5d69ea02e003ff316f25f4a66efffc3 --- /dev/null +++ b/packages/desktop-client/src/hooks/useUpdatedAccounts.ts @@ -0,0 +1,7 @@ +import { useSelector } from 'react-redux'; + +import { type State } from 'loot-core/src/client/state-types'; + +export function useUpdatedAccounts() { + return useSelector((state: State) => state.queries.updatedAccounts); +} diff --git a/packages/desktop-client/src/style/theme.tsx b/packages/desktop-client/src/style/theme.tsx index 7cd3b5751fd9903546a18f6c451a057d153fb72d..fe6b2374d14d31df10a7aded560789d609fa8ef0 100644 --- a/packages/desktop-client/src/style/theme.tsx +++ b/packages/desktop-client/src/style/theme.tsx @@ -1,12 +1,11 @@ // @ts-strict-ignore import { useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { type State } from 'loot-core/client/state-types'; -import { type PrefsState } from 'loot-core/client/state-types/prefs'; import { isNonProductionEnvironment } from 'loot-core/src/shared/environment'; import type { Theme } from 'loot-core/src/types/prefs'; +import { useGlobalPref } from '../hooks/useGlobalPref'; + import * as darkTheme from './themes/dark'; import * as developmentTheme from './themes/development'; import * as lightTheme from './themes/light'; @@ -24,16 +23,13 @@ export const themeOptions = Object.entries(themes).map( ([key, { name }]) => [key, name] as [Theme, string], ); -export function useTheme(): Theme { - return ( - useSelector<State, PrefsState['global']['theme']>( - state => state.prefs.global?.theme, - ) || 'light' - ); +export function useTheme() { + const [theme = 'light', setThemePref] = useGlobalPref('theme'); + return [theme, setThemePref] as const; } export function ThemeStyle() { - const theme = useTheme(); + const [theme] = useTheme(); const [themeColors, setThemeColors] = useState< typeof lightTheme | typeof darkTheme | undefined >(undefined); diff --git a/packages/loot-core/src/client/actions/account.ts b/packages/loot-core/src/client/actions/account.ts index c8349d84c578607b951c5dedb941ca14274a4bf7..e7f7f269b4b2d67fab7208cf71808e63d2b3a215 100644 --- a/packages/loot-core/src/client/actions/account.ts +++ b/packages/loot-core/src/client/actions/account.ts @@ -263,3 +263,10 @@ export function markAccountRead(accountId): MarkAccountReadAction { accountId, }; } + +export function moveAccount(id, targetId) { + return async (dispatch: Dispatch) => { + await send('account-move', { id, targetId }); + dispatch(getAccounts()); + }; +} diff --git a/packages/loot-core/src/client/actions/prefs.ts b/packages/loot-core/src/client/actions/prefs.ts index b93a38eb3894e86f08c32d1bc2139c0f35d5505e..5dbca103f50908b1787b2da89deb92ac5330e2c2 100644 --- a/packages/loot-core/src/client/actions/prefs.ts +++ b/packages/loot-core/src/client/actions/prefs.ts @@ -26,7 +26,7 @@ export function loadPrefs() { }; } -export function savePrefs(prefs: Partial<prefs.LocalPrefs>) { +export function savePrefs(prefs: prefs.LocalPrefs) { return async (dispatch: Dispatch) => { await send('save-prefs', prefs); dispatch({ @@ -48,7 +48,7 @@ export function loadGlobalPrefs() { }; } -export function saveGlobalPrefs(prefs: Partial<prefs.GlobalPrefs>) { +export function saveGlobalPrefs(prefs: prefs.GlobalPrefs) { return async (dispatch: Dispatch) => { await send('save-global-prefs', prefs); dispatch({ diff --git a/packages/loot-core/src/client/data-hooks/accounts.tsx b/packages/loot-core/src/client/data-hooks/accounts.tsx deleted file mode 100644 index c2460477fa68879174e35e1b767ae9df8a6fbbd3..0000000000000000000000000000000000000000 --- a/packages/loot-core/src/client/data-hooks/accounts.tsx +++ /dev/null @@ -1,36 +0,0 @@ -// @ts-strict-ignore -import React, { createContext, useContext } from 'react'; - -import { q } from '../../shared/query'; -import { type AccountEntity } from '../../types/models'; -import { useLiveQuery } from '../query-hooks'; -import { getAccountsById } from '../reducers/queries'; - -function useAccounts(): AccountEntity[] { - return useLiveQuery(() => q('accounts').select('*'), []); -} - -const AccountsContext = createContext<AccountEntity[]>(null); - -export function AccountsProvider({ children }) { - const data = useAccounts(); - return ( - <AccountsContext.Provider value={data}>{children}</AccountsContext.Provider> - ); -} - -export function CachedAccounts({ children, idKey }) { - const data = useCachedAccounts({ idKey }); - return children(data); -} - -export function useCachedAccounts(): AccountEntity[]; -export function useCachedAccounts({ - idKey, -}: { - idKey: boolean; -}): Record<AccountEntity['id'], AccountEntity>; -export function useCachedAccounts({ idKey }: { idKey?: boolean } = {}) { - const data = useContext(AccountsContext); - return idKey && data ? getAccountsById(data) : data; -} diff --git a/packages/loot-core/src/client/data-hooks/payees.tsx b/packages/loot-core/src/client/data-hooks/payees.tsx deleted file mode 100644 index 4299aa9a31f6fe354a05fe0fdfae9ad54082245e..0000000000000000000000000000000000000000 --- a/packages/loot-core/src/client/data-hooks/payees.tsx +++ /dev/null @@ -1,36 +0,0 @@ -// @ts-strict-ignore -import React, { createContext, useContext } from 'react'; - -import { q } from '../../shared/query'; -import { type PayeeEntity } from '../../types/models'; -import { useLiveQuery } from '../query-hooks'; -import { getPayeesById } from '../reducers/queries'; - -function usePayees(): PayeeEntity[] { - return useLiveQuery(() => q('payees').select('*'), []); -} - -const PayeesContext = createContext<PayeeEntity[]>(null); - -export function PayeesProvider({ children }) { - const data = usePayees(); - return ( - <PayeesContext.Provider value={data}>{children}</PayeesContext.Provider> - ); -} - -export function CachedPayees({ children, idKey }) { - const data = useCachedPayees({ idKey }); - return children(data); -} - -export function useCachedPayees(): PayeeEntity[]; -export function useCachedPayees({ - idKey, -}: { - idKey: boolean; -}): Record<PayeeEntity['id'], PayeeEntity>; -export function useCachedPayees({ idKey }: { idKey?: boolean } = {}) { - const data = useContext(PayeesContext); - return idKey && data ? getPayeesById(data) : data; -} diff --git a/packages/loot-core/src/client/privacy.ts b/packages/loot-core/src/client/privacy.ts deleted file mode 100644 index 295bba5e510d77b821968441bd8f1717036572a8..0000000000000000000000000000000000000000 --- a/packages/loot-core/src/client/privacy.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { useSelector } from 'react-redux'; - -export function usePrivacyMode() { - return useSelector(state => state.prefs?.local?.isPrivacyEnabled ?? false); -} diff --git a/packages/loot-core/src/client/reducers/queries.ts b/packages/loot-core/src/client/reducers/queries.ts index 470c3591692ada59de9fb8cf2278306a57cbc990..b0c38817a240fc7a79992e91252d3a9dd194220f 100644 --- a/packages/loot-core/src/client/reducers/queries.ts +++ b/packages/loot-core/src/client/reducers/queries.ts @@ -13,11 +13,14 @@ const initialState: QueriesState = { lastTransaction: null, updatedAccounts: [], accounts: [], + accountsLoaded: false, categories: { grouped: [], list: [], }, + categoriesLoaded: false, payees: [], + payeesLoaded: false, earliestTransaction: null, }; @@ -56,6 +59,7 @@ export function update(state = initialState, action: Action): QueriesState { return { ...state, accounts: action.accounts, + accountsLoaded: true, }; case constants.UPDATE_ACCOUNT: { return { @@ -72,11 +76,13 @@ export function update(state = initialState, action: Action): QueriesState { return { ...state, categories: action.categories, + categoriesLoaded: true, }; case constants.LOAD_PAYEES: return { ...state, payees: action.payees, + payeesLoaded: true, }; default: } diff --git a/packages/loot-core/src/client/state-types/modals.d.ts b/packages/loot-core/src/client/state-types/modals.d.ts index e25e38feb327a3ee438d2714547abcecc13d6115..72cd5dbc54b407d3099fb351de719e3b3d1043ac 100644 --- a/packages/loot-core/src/client/state-types/modals.d.ts +++ b/packages/loot-core/src/client/state-types/modals.d.ts @@ -42,7 +42,7 @@ type FinanceModals = { syncSource?: AccountSyncSource; }; - 'confirm-category-delete': { onDelete: () => void } & ( + 'confirm-category-delete': { onDelete: (categoryId: string) => void } & ( | { category: string } | { group: string } ); diff --git a/packages/loot-core/src/client/state-types/queries.d.ts b/packages/loot-core/src/client/state-types/queries.d.ts index a7f18526fe008822ed97a71d8d31c8cfe289000a..0d511868b39da8e2d1eeab25420dc2760fb7b26b 100644 --- a/packages/loot-core/src/client/state-types/queries.d.ts +++ b/packages/loot-core/src/client/state-types/queries.d.ts @@ -8,8 +8,11 @@ export type QueriesState = { lastTransaction: unknown | null; updatedAccounts: string[]; accounts: AccountEntity[]; + accountsLoaded: boolean; categories: Awaited<ReturnType<Handlers['get-categories']>>; + categoriesLoaded: boolean; payees: Awaited<ReturnType<Handlers['payees-get']>>; + payeesLoaded: boolean; earliestTransaction: unknown | null; }; diff --git a/packages/loot-core/src/shared/categories.ts b/packages/loot-core/src/shared/categories.ts deleted file mode 100644 index 9e2c4c190aa28febf655db0486662f7402117412..0000000000000000000000000000000000000000 --- a/packages/loot-core/src/shared/categories.ts +++ /dev/null @@ -1,106 +0,0 @@ -// @ts-strict-ignore -export function addCategory(categoryGroups, cat) { - return categoryGroups.map(group => { - if (group.id === cat.cat_group) { - group.categories = [cat, ...group.categories]; - } - return { ...group }; - }); -} - -export function updateCategory(categoryGroups, category) { - return categoryGroups.map(group => { - if (group.id === category.cat_group) { - group.categories = group.categories.map(c => { - if (c.id === category.id) { - return { ...c, ...category }; - } - return c; - }); - } - return group; - }); -} - -export function moveCategory(categoryGroups, id, groupId, targetId) { - if (id === targetId) { - return categoryGroups; - } - - let moveCat = categoryGroups.reduce((value, group) => { - return value || group.categories.find(cat => cat.id === id); - }, null); - - // Update the group id on the category - moveCat = { ...moveCat, cat_group: groupId }; - - return categoryGroups.map(group => { - if (group.id === groupId) { - group.categories = group.categories.reduce((cats, cat) => { - if (cat.id === targetId) { - cats.push(moveCat); - cats.push(cat); - } else if (cat.id !== id) { - cats.push(cat); - } - return cats; - }, []); - - if (!targetId) { - group.categories.push(moveCat); - } - } else { - group.categories = group.categories.filter(cat => cat.id !== id); - } - - return { ...group }; - }); -} - -export function moveCategoryGroup(categoryGroups, id, targetId) { - if (id === targetId) { - return categoryGroups; - } - - const moveGroup = categoryGroups.find(g => g.id === id); - - categoryGroups = categoryGroups.reduce((groups, group) => { - if (group.id === targetId) { - groups.push(moveGroup); - groups.push(group); - } else if (group.id !== id) { - groups.push(group); - } - return groups; - }, []); - - if (!targetId) { - categoryGroups.push(moveGroup); - } - - return categoryGroups; -} - -export function deleteCategory(categoryGroups, id) { - return categoryGroups.map(group => { - group.categories = group.categories.filter(c => c.id !== id); - return group; - }); -} - -export function addGroup(categoryGroups, group) { - return [...categoryGroups, group]; -} - -export function updateGroup(categoryGroups, group) { - return categoryGroups.map(g => { - if (g.id === group.id) { - return { ...g, ...group }; - } - return g; - }); -} - -export function deleteGroup(categoryGroups, id) { - return categoryGroups.filter(g => g.id !== id); -} diff --git a/packages/loot-core/src/types/prefs.d.ts b/packages/loot-core/src/types/prefs.d.ts index df63517dd8d4ae9d2116afec44e3e954a79a0e6d..251b6e5dd6ab619c0670ef517940f4f79ea839dc 100644 --- a/packages/loot-core/src/types/prefs.d.ts +++ b/packages/loot-core/src/types/prefs.d.ts @@ -53,6 +53,7 @@ export type LocalPrefs = Partial< reportsViewLegend: boolean; reportsViewSummary: boolean; reportsViewLabel: boolean; + 'mobile.showSpentColumn': boolean; } & Record<`flags.${FeatureFlag}`, boolean> >; diff --git a/upcoming-release-notes/2293.md b/upcoming-release-notes/2293.md new file mode 100644 index 0000000000000000000000000000000000000000..d65818af8a0005770cd3e9d18d1371ff63dcb9e3 --- /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.