Skip to content

Commit

Permalink
Hooks for frequently made operations (#2293)
Browse files Browse the repository at this point in the history
* Hooks for frequently made operations

* Release notes

* Fix typecheck errors

* Remove useGlobalPrefs

* Add null checks

* Fix showCleared pref

* Add loaded flag for categories, accounts and payees state

* Refactor to reduce unnecessary states

* Fix eslint errors

* Fix hooks deps

* Add useEffect

* Fix typecheck error

* Set local and global pref hooks

* Fix lint error

* VRT

* Fix typecheck error

* Remove eager loading

* Fix typecheck error

* Fix typo

* Fix typecheck error

* Update useTheme

* Typecheck errors

* Typecheck error

* defaultValue

* Explicitly check undefined

* Remove useGlobalPref and useLocalPref defaults

* Fix default prefs

* Default value

* Fix lint error

* Set default theme

* Default date format in Account

* Update packages/desktop-client/src/style/theme.tsx

Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>

---------

Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
joel-jeremy and MatissJanis authored Feb 12, 2024
1 parent ec2de3b commit 08cbdab
Showing 104 changed files with 1,045 additions and 1,492 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 9 additions & 38 deletions packages/desktop-client/src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -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>
7 changes: 3 additions & 4 deletions packages/desktop-client/src/components/BankSyncStatus.tsx
Original file line number Diff line number Diff line change
@@ -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
41 changes: 20 additions & 21 deletions packages/desktop-client/src/components/FinancesApp.tsx
Original file line number Diff line number Diff line change
@@ -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>
7 changes: 2 additions & 5 deletions packages/desktop-client/src/components/LoggedInUser.tsx
Original file line number Diff line number Diff line change
@@ -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);
23 changes: 9 additions & 14 deletions packages/desktop-client/src/components/ManageRules.tsx
Original file line number Diff line number Diff line change
@@ -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,
20 changes: 5 additions & 15 deletions packages/desktop-client/src/components/MobileWebMessage.tsx
Original file line number Diff line number Diff line change
@@ -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();
39 changes: 6 additions & 33 deletions packages/desktop-client/src/components/Modals.tsx
Original file line number Diff line number Diff line change
@@ -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}
11 changes: 4 additions & 7 deletions packages/desktop-client/src/components/Notifications.tsx
Original file line number Diff line number Diff line change
@@ -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
3 changes: 1 addition & 2 deletions packages/desktop-client/src/components/PrivacyFilter.tsx
Original file line number Diff line number Diff line change
@@ -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';
11 changes: 3 additions & 8 deletions packages/desktop-client/src/components/ThemeSelector.tsx
Original file line number Diff line number Diff line change
@@ -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;
36 changes: 11 additions & 25 deletions packages/desktop-client/src/components/Titlebar.tsx
Original file line number Diff line number Diff line change
@@ -6,18 +6,17 @@ 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';
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,19 +119,16 @@ 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 };

return (
<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
12 changes: 4 additions & 8 deletions packages/desktop-client/src/components/UpdateNotification.tsx
Original file line number Diff line number Diff line change
@@ -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();

57 changes: 40 additions & 17 deletions packages/desktop-client/src/components/accounts/Account.jsx
Original file line number Diff line number Diff line change
@@ -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();
Original file line number Diff line number Diff line change
@@ -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();

7 changes: 3 additions & 4 deletions packages/desktop-client/src/components/accounts/Header.jsx
Original file line number Diff line number Diff line change
@@ -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'));
}
}

34 changes: 20 additions & 14 deletions packages/desktop-client/src/components/accounts/MobileAccount.jsx
Original file line number Diff line number Diff line change
@@ -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
37 changes: 20 additions & 17 deletions packages/desktop-client/src/components/accounts/MobileAccounts.jsx
Original file line number Diff line number Diff line change
@@ -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>
);
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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;
}
68 changes: 50 additions & 18 deletions packages/desktop-client/src/components/budget/BudgetCategories.jsx
Original file line number Diff line number Diff line change
@@ -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}
70 changes: 41 additions & 29 deletions packages/desktop-client/src/components/budget/BudgetTable.jsx
Original file line number Diff line number Diff line change
@@ -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,15 +14,14 @@ 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();

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);
Original file line number Diff line number Diff line change
@@ -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 (
26 changes: 8 additions & 18 deletions packages/desktop-client/src/components/budget/MobileBudget.tsx
Original file line number Diff line number Diff line change
@@ -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}
/>
40 changes: 14 additions & 26 deletions packages/desktop-client/src/components/budget/MobileBudgetTable.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React, { memo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';

import memoizeOne from 'memoize-one';

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,15 +1280,15 @@ export function BudgetTable({
</View>
</Button>
)}
{(show3Cols || !showBudgetedCol) && (
{(show3Cols || showSpentColumn) && (
<Button
type="bare"
disabled={show3Cols}
onClick={toggleDisplay}
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}
Original file line number Diff line number Diff line change
@@ -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({
347 changes: 97 additions & 250 deletions packages/desktop-client/src/components/budget/index.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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,
Original file line number Diff line number Diff line change
@@ -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';

Original file line number Diff line number Diff line change
@@ -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) => {
13 changes: 5 additions & 8 deletions packages/desktop-client/src/components/modals/CloseAccount.tsx
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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;
12 changes: 6 additions & 6 deletions packages/desktop-client/src/components/modals/EditField.jsx
Original file line number Diff line number Diff line change
@@ -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();

7 changes: 3 additions & 4 deletions packages/desktop-client/src/components/modals/EditRule.jsx
Original file line number Diff line number Diff line change
@@ -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 }), []),
});
Original file line number Diff line number Diff line change
@@ -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();

13 changes: 9 additions & 4 deletions packages/desktop-client/src/components/modals/LoadBackup.jsx
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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}>
{() => (
Original file line number Diff line number Diff line change
@@ -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();

4 changes: 2 additions & 2 deletions packages/desktop-client/src/components/reports/Overview.jsx
Original file line number Diff line number Diff line change
@@ -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={{
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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);
}
};

Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React, { useState, useEffect, useMemo } from 'react';
import { useSelector } from 'react-redux';

import * as d from 'date-fns';

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,
Original file line number Diff line number Diff line change
@@ -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,
Original file line number Diff line number Diff line change
@@ -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';

Original file line number Diff line number Diff line change
@@ -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();

19 changes: 6 additions & 13 deletions packages/desktop-client/src/components/rules/Value.tsx
Original file line number Diff line number Diff line change
@@ -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,
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
// @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';
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);
Original file line number Diff line number Diff line change
@@ -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) {
Original file line number Diff line number Diff line change
@@ -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);
14 changes: 3 additions & 11 deletions packages/desktop-client/src/components/select/DateSelect.tsx
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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);

20 changes: 6 additions & 14 deletions packages/desktop-client/src/components/settings/Experimental.tsx
Original file line number Diff line number Diff line change
@@ -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 (
12 changes: 3 additions & 9 deletions packages/desktop-client/src/components/settings/Export.tsx
Original file line number Diff line number Diff line change
@@ -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);
44 changes: 16 additions & 28 deletions packages/desktop-client/src/components/settings/Format.tsx
Original file line number Diff line number Diff line change
@@ -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 }}
/>
13 changes: 3 additions & 10 deletions packages/desktop-client/src/components/settings/Global.tsx
Original file line number Diff line number Diff line change
@@ -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);
}
}
9 changes: 3 additions & 6 deletions packages/desktop-client/src/components/settings/Reset.tsx
Original file line number Diff line number Diff line change
@@ -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);
10 changes: 5 additions & 5 deletions packages/desktop-client/src/components/settings/Themes.tsx
Original file line number Diff line number Diff line change
@@ -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}
22 changes: 6 additions & 16 deletions packages/desktop-client/src/components/settings/index.tsx
Original file line number Diff line number Diff line change
@@ -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();

86 changes: 26 additions & 60 deletions packages/desktop-client/src/components/sidebar/Accounts.tsx
Original file line number Diff line number Diff line change
@@ -1,78 +1,44 @@
// @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';

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}
/>
208 changes: 145 additions & 63 deletions packages/desktop-client/src/components/sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
}
52 changes: 52 additions & 0 deletions packages/desktop-client/src/components/sidebar/SidebarProvider.tsx
Original file line number Diff line number Diff line change
@@ -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],
);
}
181 changes: 0 additions & 181 deletions packages/desktop-client/src/components/sidebar/SidebarWithData.tsx

This file was deleted.

Loading

0 comments on commit 08cbdab

Please sign in to comment.