Compare commits

...

32 Commits

Author SHA1 Message Date
Joel Jeremy Marquez
6fa4d673cf Fix refetch happening when switching budget files 2026-02-23 23:26:25 +00:00
Joel Jeremy Marquez
7995d659ab Use budgetId metadata pref to check if a budget has been loaded or not instead of synced prefs 2026-02-23 19:51:24 +00:00
Joel Jeremy Marquez
52c4586051 Update useSyncedPrefs 2026-02-23 19:50:58 +00:00
Joel Jeremy Marquez
a6873cd5c7 Update pref hooks 2026-02-23 19:00:35 +00:00
Joel Jeremy Marquez
6001c37285 Merge remote-tracking branch 'origin/master' into react-query-prefs 2026-02-23 18:54:08 +00:00
Joel Jeremy Marquez
82743c6f90 Remove refetchType 2026-02-18 21:54:02 +00:00
Joel Jeremy Marquez
aca0293750 Remove unused import 2026-02-18 21:47:54 +00:00
autofix-ci[bot]
f24a9023c5 [autofix.ci] apply automated fixes 2026-02-18 21:38:20 +00:00
Joel Jeremy Marquez
d0a653cdae Cleanup 2026-02-18 21:35:49 +00:00
Joel Jeremy Marquez
158c79281d Revert unrelated changes 2026-02-18 21:31:32 +00:00
Joel Jeremy Marquez
470fb13d37 Merge remote-tracking branch 'origin/master' into react-query-prefs 2026-02-18 21:23:45 +00:00
Joel Jeremy Marquez
e993862c5a Replace __actionsFotMenu.saveGlobalPrefs with setTheme 2026-02-18 21:20:58 +00:00
autofix-ci[bot]
329556b56d [autofix.ci] apply automated fixes 2026-02-18 15:15:19 +00:00
Joel Jeremy Marquez
635ae01ea4 Update to useAccounts 2026-02-18 15:12:53 +00:00
autofix-ci[bot]
3f35379244 [autofix.ci] apply automated fixes 2026-02-18 15:12:13 +00:00
Joel Jeremy Marquez
d21aa62186 Fix lint errors 2026-02-18 15:12:13 +00:00
Joel Jeremy Marquez
29d9507254 Fix typecheck errors 2026-02-18 15:12:13 +00:00
Joel Jeremy Marquez
5606216b0d Fix error 2026-02-18 15:12:13 +00:00
Joel Jeremy Marquez
4c7b15d1a4 Fix imports 2026-02-18 15:12:13 +00:00
Joel Jeremy Marquez
6fcf383243 Fix lint errors 2026-02-18 15:12:13 +00:00
Joel Jeremy Marquez
83fb65f413 Fix lint errors 2026-02-18 15:12:12 +00:00
Joel Jeremy Marquez
9665945a69 Move redux state to react-query - prefs states 2026-02-18 15:12:12 +00:00
Joel Jeremy Marquez
f039864a64 Merge remote-tracking branch 'origin/master' into react-query-payees 2026-02-18 13:41:47 +00:00
Joel Jeremy Marquez
1bdb052107 Cleanup and simplify AccountEntity definition to fix satisfies syntax 2026-02-17 17:43:52 +00:00
Joel Jeremy Marquez
696d1df508 Update empty payees list test 2026-02-17 17:13:21 +00:00
Joel Jeremy Marquez
8d4086ad75 Fix imports 2026-02-17 16:33:31 +00:00
Copilot
fedef15cf2 Address feedback on adding default data to usePayees (#6931)
* Initial plan

* Add default data to usePayees usages using inline destructuring

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>
2026-02-17 16:33:31 +00:00
Joel Jeremy Marquez
5ae1f539c4 Replace usage of logger in desktop-client with console 2026-02-17 16:33:31 +00:00
github-actions[bot]
40d45a5c9e Add release notes for PR #6880 2026-02-17 16:33:31 +00:00
Joel Jeremy Marquez
5263424a77 Move redux state to react-query - payees states 2026-02-17 16:33:31 +00:00
Joel Jeremy Marquez
757c93afe2 Fix onbudget and offbudget displaying closed accounts 2026-02-17 16:31:38 +00:00
Joel Jeremy Marquez
6ca922f4a4 Move redux state to react-query - account states 2026-02-17 16:27:40 +00:00
25 changed files with 575 additions and 302 deletions

View File

@@ -6,7 +6,7 @@ import { getUploadError } from 'loot-core/shared/errors';
import type { AtLeastOne } from 'loot-core/types/util';
import { pushModal } from '@desktop-client/modals/modalsSlice';
import { loadPrefs } from '@desktop-client/prefs/prefsSlice';
import { prefQueries } from '@desktop-client/prefs';
import { createAppAsyncThunk } from '@desktop-client/redux';
import { getIsOutdated, getLatestVersion } from '@desktop-client/util/versions';
@@ -89,8 +89,8 @@ export const resetSync = createAppAsyncThunk(
export const sync = createAppAsyncThunk(
`${sliceName}/sync`,
async (_, { dispatch, getState }) => {
const prefs = getState().prefs.local;
async (_, { extra: { queryClient } }) => {
const prefs = await queryClient.ensureQueryData(prefQueries.listMetadata());
if (prefs && prefs.id) {
const result = await send('sync');
if (result && 'error' in result) {
@@ -98,7 +98,9 @@ export const sync = createAppAsyncThunk(
}
// Update the prefs
await dispatch(loadPrefs());
queryClient.invalidateQueries({
queryKey: prefQueries.lists(),
});
}
return {};
@@ -107,8 +109,10 @@ export const sync = createAppAsyncThunk(
export const getLatestAppVersion = createAppAsyncThunk(
`${sliceName}/getLatestAppVersion`,
async (_, { dispatch, getState }) => {
const globalPrefs = getState().prefs.global;
async (_, { dispatch, extra: { queryClient } }) => {
const globalPrefs = await queryClient.ensureQueryData(
prefQueries.listGlobal(),
);
if (globalPrefs && globalPrefs.notifyWhenUpdateIsAvailable) {
const theLatestVersion = await getLatestVersion();
dispatch(

View File

@@ -219,8 +219,8 @@ global.Actual = {
return worker;
},
setTheme: theme => {
window.__actionsForMenu.saveGlobalPrefs({ prefs: { theme } });
setTheme: async theme => {
await window.__actionsForMenu.setTheme(theme);
},
moveBudgetDirectory: () => {

View File

@@ -1,5 +1,6 @@
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import { type QueryClient } from '@tanstack/react-query';
import { t } from 'i18next';
import { send } from 'loot-core/platform/client/connection';
@@ -11,7 +12,7 @@ import type { Handlers } from 'loot-core/types/handlers';
import { resetApp, setAppState } from '@desktop-client/app/appSlice';
import { closeModal, pushModal } from '@desktop-client/modals/modalsSlice';
import { loadGlobalPrefs, loadPrefs } from '@desktop-client/prefs/prefsSlice';
import { prefQueries } from '@desktop-client/prefs';
import { createAppAsyncThunk } from '@desktop-client/redux';
import { signOut } from '@desktop-client/users/usersSlice';
@@ -55,7 +56,7 @@ type LoadBudgetPayload = {
export const loadBudget = createAppAsyncThunk(
`${sliceName}/loadBudget`,
async ({ id, options = {} }: LoadBudgetPayload, { dispatch }) => {
async ({ id, options = {} }: LoadBudgetPayload, { dispatch, extra }) => {
dispatch(setAppState({ loadingText: t('Loading...') }));
// Loading a budget may fail
@@ -89,23 +90,43 @@ export const loadBudget = createAppAsyncThunk(
}
} else {
dispatch(closeModal());
await dispatch(loadPrefs());
extra.queryClient.invalidateQueries({
queryKey: prefQueries.lists(),
});
}
dispatch(setAppState({ loadingText: null }));
},
);
function invalidateClosedBudgetQueries(queryClient: QueryClient) {
// Invalidate all queries but do not cause a refetch.
// This is because we want to clear out all the budget data from the queries,
// but we don't want to trigger a bunch of error states from the queries trying
// to fetch data for a budget that is now closed. The next time a budget is loaded,
// the queries will refetch with the correct budget id.
queryClient.invalidateQueries({
refetchType: 'none',
});
// Invalidate the metadata query since the budget is now closed.
// We want to cause a refetch so that the app can update to show the correct state
// (e.g. show the manager page if no budget is open).
queryClient.invalidateQueries({
queryKey: prefQueries.listMetadata().queryKey,
});
}
export const closeBudget = createAppAsyncThunk(
`${sliceName}/closeBudget`,
async (_, { dispatch, getState, extra: { queryClient } }) => {
const prefs = getState().prefs.local;
async (_, { dispatch, extra: { queryClient } }) => {
const prefs = await queryClient.ensureQueryData(prefQueries.listMetadata());
if (prefs && prefs.id) {
dispatch(resetApp());
queryClient.clear();
dispatch(setAppState({ loadingText: t('Closing...') }));
await send('close-budget');
dispatch(setAppState({ loadingText: null }));
invalidateClosedBudgetQueries(queryClient);
if (localStorage.getItem('SharedArrayBufferOverride')) {
window.location.reload();
}
@@ -115,11 +136,11 @@ export const closeBudget = createAppAsyncThunk(
export const closeBudgetUI = createAppAsyncThunk(
`${sliceName}/closeBudgetUI`,
async (_, { dispatch, getState, extra: { queryClient } }) => {
const prefs = getState().prefs.local;
async (_, { dispatch, extra: { queryClient } }) => {
const prefs = await queryClient.ensureQueryData(prefQueries.listMetadata());
if (prefs && prefs.id) {
dispatch(resetApp());
queryClient.clear();
invalidateClosedBudgetQueries(queryClient);
}
},
);
@@ -146,7 +167,7 @@ export const createBudget = createAppAsyncThunk(
`${sliceName}/createBudget`,
async (
{ testMode = false, demoMode = false }: CreateBudgetPayload,
{ dispatch },
{ dispatch, extra: { queryClient } },
) => {
dispatch(
setAppState({
@@ -164,7 +185,9 @@ export const createBudget = createAppAsyncThunk(
dispatch(closeModal());
await dispatch(loadAllFiles());
await dispatch(loadPrefs());
queryClient.invalidateQueries({
queryKey: prefQueries.lists(),
});
// Set the loadingText to null after we've loaded the budget prefs
// so that the existing manager page doesn't flash
@@ -243,14 +266,19 @@ type ImportBudgetPayload = {
export const importBudget = createAppAsyncThunk(
`${sliceName}/importBudget`,
async ({ filepath, type }: ImportBudgetPayload, { dispatch }) => {
async (
{ filepath, type }: ImportBudgetPayload,
{ dispatch, extra: { queryClient } },
) => {
const { error } = await send('import-budget', { filepath, type });
if (error) {
throw new Error(error);
}
dispatch(closeModal());
await dispatch(loadPrefs());
queryClient.invalidateQueries({
queryKey: prefQueries.lists(),
});
},
);
@@ -304,7 +332,7 @@ export const downloadBudget = createAppAsyncThunk(
`${sliceName}/downloadBudget`,
async (
{ cloudFileId, replace = false }: DownloadBudgetPayload,
{ dispatch },
{ dispatch, extra: { queryClient } },
): Promise<string | null> => {
dispatch(
setAppState({
@@ -368,8 +396,10 @@ export const downloadBudget = createAppAsyncThunk(
if (!id) {
throw new Error('No id returned from download.');
}
queryClient.invalidateQueries({
queryKey: prefQueries.listGlobal().queryKey,
});
await Promise.all([
dispatch(loadGlobalPrefs()),
dispatch(loadAllFiles()),
dispatch(loadBudget({ id })),
]);
@@ -387,8 +417,11 @@ type LoadBackupPayload = {
// Take in the budget id so that backups can be loaded when a budget isn't opened
export const loadBackup = createAppAsyncThunk(
`${sliceName}/loadBackup`,
async ({ budgetId, backupId }: LoadBackupPayload, { dispatch, getState }) => {
const prefs = getState().prefs.local;
async (
{ budgetId, backupId }: LoadBackupPayload,
{ dispatch, extra: { queryClient } },
) => {
const prefs = await queryClient.ensureQueryData(prefQueries.listMetadata());
if (prefs && prefs.id) {
await dispatch(closeBudget());
}
@@ -400,8 +433,8 @@ export const loadBackup = createAppAsyncThunk(
export const makeBackup = createAppAsyncThunk(
`${sliceName}/makeBackup`,
async (_, { getState }) => {
const prefs = getState().prefs.local;
async (_, { extra: { queryClient } }) => {
const prefs = await queryClient.ensureQueryData(prefQueries.listMetadata());
if (prefs && prefs.id) {
await send('backup-make', { id: prefs.id });
}

View File

@@ -38,10 +38,9 @@ import { SpreadsheetProvider } from '@desktop-client/hooks/useSpreadsheet';
import { setI18NextLanguage } from '@desktop-client/i18n';
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
import { installPolyfills } from '@desktop-client/polyfills';
import { loadGlobalPrefs } from '@desktop-client/prefs/prefsSlice';
import { prefQueries } from '@desktop-client/prefs';
import { useDispatch, useSelector, useStore } from '@desktop-client/redux';
import {
CustomThemeStyle,
hasHiddenScrollbars,
ThemeStyle,
useTheme,
@@ -61,6 +60,8 @@ function AppInner() {
setI18NextLanguage(null);
}, []);
const queryClient = useQueryClient();
useEffect(() => {
const maybeUpdate = async <T,>(cb?: () => T): Promise<T | void> => {
if (global.Actual.isUpdateReadyForDownload()) {
@@ -91,7 +92,7 @@ function AppInner() {
loadingText: t('Loading global preferences...'),
}),
);
await dispatch(loadGlobalPrefs());
void queryClient.prefetchQuery(prefQueries.listGlobal());
// Open the last opened budget, if any
dispatch(
@@ -239,7 +240,6 @@ export function App() {
<AppInner />
</ErrorBoundary>
<ThemeStyle />
<CustomThemeStyle />
<ErrorBoundary FallbackComponent={FatalError}>
<Modals />
</ErrorBoundary>

View File

@@ -6,7 +6,7 @@ import { Navigate, Route, Routes, useHref, useLocation } from 'react-router';
import { useResponsive } from '@actual-app/components/hooks/useResponsive';
import { theme } from '@actual-app/components/theme';
import { View } from '@actual-app/components/view';
import { useQuery } from '@tanstack/react-query';
import { usePrefetchQuery } from '@tanstack/react-query';
import * as undo from 'loot-core/platform/client/undo';
@@ -28,17 +28,19 @@ import { FloatableSidebar } from './sidebar';
import { ManageTagsPage } from './tags/ManageTagsPage';
import { Titlebar } from './Titlebar';
import { accountQueries } from '@desktop-client/accounts';
import { getLatestAppVersion, sync } from '@desktop-client/app/appSlice';
import { ProtectedRoute } from '@desktop-client/auth/ProtectedRoute';
import { Permissions } from '@desktop-client/auth/types';
import { useAccounts } from '@desktop-client/hooks/useAccounts';
import { useGlobalPref } from '@desktop-client/hooks/useGlobalPref';
import { useLocalPref } from '@desktop-client/hooks/useLocalPref';
import { useMetaThemeColor } from '@desktop-client/hooks/useMetaThemeColor';
import { useNavigate } from '@desktop-client/hooks/useNavigate';
import { ScrollProvider } from '@desktop-client/hooks/useScrollListener';
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
import { prefQueries } from '@desktop-client/prefs';
import { useDispatch, useSelector } from '@desktop-client/redux';
import { CustomThemeStyle } from '@desktop-client/style';
function NarrowNotSupported({
redirectTo = '/budget',
@@ -91,10 +93,7 @@ export function FinancesApp() {
const dispatch = useDispatch();
const { t } = useTranslation();
// TODO: Replace with `useAccounts` hook once it's updated to return the useQuery results.
const { data: accounts, isFetching: isAccountsFetching } = useQuery(
accountQueries.list(),
);
const { data: accounts, isFetching: isAccountsFetching } = useAccounts();
const versionInfo = useSelector(state => state.app.versionInfo);
const [notifyWhenUpdateIsAvailable] = useGlobalPref(
@@ -198,6 +197,7 @@ export function FinancesApp() {
<RouterBehaviors />
<GlobalKeys />
<CommandBar />
<CustomThemeStyle />
<View
style={{
flexDirection: 'row',

View File

@@ -10,6 +10,7 @@ import { styles } from '@actual-app/components/styles';
import { Text } from '@actual-app/components/text';
import { theme } from '@actual-app/components/theme';
import { View } from '@actual-app/components/view';
import { useQuery } from '@tanstack/react-query';
import { listen } from 'loot-core/platform/client/connection';
import type { RemoteFile, SyncedLocalFile } from 'loot-core/types/file';
@@ -55,7 +56,6 @@ export function LoggedInUser({
f => f.state === 'remote' || f.state === 'synced' || f.state === 'detached',
) as (SyncedLocalFile | RemoteFile)[];
const currentFile = remoteFiles.find(f => f.cloudFileId === cloudFileId);
const hasSyncedPrefs = useSelector(state => state.prefs.synced);
const initializeUserData = useCallback(async () => {
try {
@@ -235,7 +235,7 @@ export function LoggedInUser({
multiuserEnabled &&
userData &&
userData?.displayName &&
!hasSyncedPrefs && (
!budgetId && (
<small>
(
<Trans>
@@ -251,7 +251,7 @@ export function LoggedInUser({
multiuserEnabled &&
userData &&
userData?.displayName &&
hasSyncedPrefs && (
budgetId && (
<small>
(
<Trans>

View File

@@ -28,6 +28,7 @@ import {
useSaveCategoryGroupMutation,
useSaveCategoryMutation,
} from '@desktop-client/budget';
import { LoadingIndicator } from '@desktop-client/components/reports/LoadingIndicator';
import { useCategories } from '@desktop-client/hooks/useCategories';
import { useGlobalPref } from '@desktop-client/hooks/useGlobalPref';
import { useLocalPref } from '@desktop-client/hooks/useLocalPref';
@@ -175,7 +176,7 @@ export function Budget() {
};
if (!initialized || !categoryGroups) {
return null;
return <LoadingIndicator />;
}
let table;

View File

@@ -25,7 +25,7 @@ import {
} from '@desktop-client/components/ServerContext';
import { useGlobalPref } from '@desktop-client/hooks/useGlobalPref';
import { useNavigate } from '@desktop-client/hooks/useNavigate';
import { saveGlobalPrefs } from '@desktop-client/prefs/prefsSlice';
import { useSaveGlobalPrefsMutation } from '@desktop-client/prefs';
import { useDispatch } from '@desktop-client/redux';
import { loggedIn, signOut } from '@desktop-client/users/usersSlice';
@@ -40,7 +40,6 @@ export function ElectronServerConfig({
const navigate = useNavigate();
const setServerUrl = useSetServerURL();
const currentUrl = useServerURL();
const dispatch = useDispatch();
const [syncServerConfig, setSyncServerConfig] =
useGlobalPref('syncServerConfig');
@@ -54,6 +53,7 @@ export function ElectronServerConfig({
const hasInternalServerConfig = syncServerConfig?.port;
const [startingSyncServer, setStartingSyncServer] = useState(false);
const saveGlobalPrefs = useSaveGlobalPrefsMutation();
const onConfigureSyncServer = async () => {
if (startingSyncServer) {
@@ -73,17 +73,13 @@ export function ElectronServerConfig({
setConfigError(null);
setStartingSyncServer(true);
// Ensure config is saved before starting the server
await dispatch(
saveGlobalPrefs({
prefs: {
syncServerConfig: {
...syncServerConfig,
port: electronServerPort,
autoStart: true,
},
},
}),
).unwrap();
await saveGlobalPrefs.mutateAsync({
syncServerConfig: {
...syncServerConfig,
port: electronServerPort,
autoStart: true,
},
});
await window.globalThis.Actual.stopSyncServer();
await window.globalThis.Actual.startSyncServer();

View File

@@ -13,6 +13,7 @@ import { Text } from '@actual-app/components/text';
import { theme } from '@actual-app/components/theme';
import { View } from '@actual-app/components/view';
import { css } from '@emotion/css';
import { useQueryClient } from '@tanstack/react-query';
import { send } from 'loot-core/platform/client/connection';
import { getCreateKeyError } from 'loot-core/shared/errors';
@@ -27,7 +28,7 @@ import {
ModalHeader,
} from '@desktop-client/components/common/Modal';
import type { Modal as ModalType } from '@desktop-client/modals/modalsSlice';
import { loadGlobalPrefs } from '@desktop-client/prefs/prefsSlice';
import { prefQueries } from '@desktop-client/prefs';
import { useDispatch } from '@desktop-client/redux';
type CreateEncryptionKeyModalProps = Extract<
@@ -45,6 +46,7 @@ export function CreateEncryptionKeyModal({
const [showPassword, setShowPassword] = useState(false);
const { isNarrowWidth } = useResponsive();
const dispatch = useDispatch();
const queryClient = useQueryClient();
const isRecreating = recreate;
@@ -60,7 +62,9 @@ export function CreateEncryptionKeyModal({
return;
}
void dispatch(loadGlobalPrefs());
void queryClient.invalidateQueries({
queryKey: prefQueries.listGlobal().queryKey,
});
void dispatch(loadAllFiles());
void dispatch(sync());

View File

@@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useEffectEvent } from 'react';
import type { ReactNode } from 'react';
import { Trans, useTranslation } from 'react-i18next';
@@ -11,7 +11,6 @@ import { tokens } from '@actual-app/components/tokens';
import { View } from '@actual-app/components/view';
import { css } from '@emotion/css';
import { listen } from 'loot-core/platform/client/connection';
import { isElectron } from 'loot-core/shared/environment';
import { AuthSettings } from './AuthSettings';
@@ -43,7 +42,6 @@ import { useFeatureFlag } from '@desktop-client/hooks/useFeatureFlag';
import { useGlobalPref } from '@desktop-client/hooks/useGlobalPref';
import { useMetadataPref } from '@desktop-client/hooks/useMetadataPref';
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
import { loadPrefs } from '@desktop-client/prefs/prefsSlice';
import { useDispatch, useSelector } from '@desktop-client/redux';
function About() {
@@ -183,20 +181,17 @@ export function Settings() {
void dispatch(closeBudget());
};
useEffect(() => {
const unlisten = listen('prefs-updated', () => {
void dispatch(loadPrefs());
});
void dispatch(loadPrefs());
return () => unlisten();
}, [dispatch]);
const onSetDefaultCurrencyCodePref = useEffectEvent(
(isCurrencyExperimentalEnabled: boolean) => {
if (!isCurrencyExperimentalEnabled) {
setDefaultCurrencyCodePref('');
}
},
);
useEffect(() => {
if (!isCurrencyExperimentalEnabled) {
setDefaultCurrencyCodePref('');
}
}, [isCurrencyExperimentalEnabled, setDefaultCurrencyCodePref]);
onSetDefaultCurrencyCodePref(isCurrencyExperimentalEnabled);
}, [isCurrencyExperimentalEnabled]);
const { isNarrowWidth } = useResponsive();

View File

@@ -14,7 +14,7 @@ import {
addNotification,
} from './notifications/notificationsSlice';
import { payeeQueries } from './payees';
import { loadPrefs } from './prefs/prefsSlice';
import { prefQueries } from './prefs';
import type { AppStore } from './redux/store';
import * as syncEvents from './sync-events';
@@ -141,7 +141,9 @@ export function handleGlobalEvents(store: AppStore, queryClient: QueryClient) {
const unlistenFinishLoad = listen('finish-load', () => {
store.dispatch(closeModal());
store.dispatch(setAppState({ loadingText: null }));
void store.dispatch(loadPrefs());
void queryClient.invalidateQueries({
queryKey: prefQueries.lists(),
});
});
const unlistenStartImport = listen('start-import', () => {
@@ -151,7 +153,9 @@ export function handleGlobalEvents(store: AppStore, queryClient: QueryClient) {
const unlistenFinishImport = listen('finish-import', () => {
store.dispatch(closeModal());
store.dispatch(setAppState({ loadingText: null }));
void store.dispatch(loadPrefs());
void queryClient.invalidateQueries({
queryKey: prefQueries.lists(),
});
});
const unlistenShowBudgets = listen('show-budgets', () => {
@@ -163,6 +167,12 @@ export function handleGlobalEvents(store: AppStore, queryClient: QueryClient) {
void window.Actual.reload();
});
const unlistenPrefsUpdated = listen('prefs-updated', () => {
void queryClient.invalidateQueries({
queryKey: prefQueries.lists(),
});
});
return () => {
unlistenServerError();
unlistenOrphanedPayees();
@@ -176,5 +186,6 @@ export function handleGlobalEvents(store: AppStore, queryClient: QueryClient) {
unlistenFinishImport();
unlistenShowBudgets();
unlistenApiFetchRedirected();
unlistenPrefsUpdated();
};
}

View File

@@ -1,9 +1,8 @@
import { useCallback } from 'react';
import { useQuery } from '@tanstack/react-query';
import type { GlobalPrefs } from 'loot-core/types/prefs';
import { saveGlobalPrefs } from '@desktop-client/prefs/prefsSlice';
import { useDispatch, useSelector } from '@desktop-client/redux';
import { prefQueries, useSaveGlobalPrefsMutation } from '@desktop-client/prefs';
type SetGlobalPrefAction<K extends keyof GlobalPrefs> = (
value: GlobalPrefs[K],
@@ -13,23 +12,24 @@ export function useGlobalPref<K extends keyof GlobalPrefs>(
prefName: K,
onSaveGlobalPrefs?: () => void,
): [GlobalPrefs[K], SetGlobalPrefAction<K>] {
const dispatch = useDispatch();
const setGlobalPref = useCallback<SetGlobalPrefAction<K>>(
value => {
void dispatch(
saveGlobalPrefs({
prefs: {
[prefName]: value,
},
onSaveGlobalPrefs,
}),
);
},
[prefName, dispatch, onSaveGlobalPrefs],
);
const globalPref = useSelector(
state => state.prefs.global?.[prefName] as GlobalPrefs[K],
);
const { mutate: saveGlobalPrefs } = useSaveGlobalPrefsMutation();
const saveGlobalPref: SetGlobalPrefAction<K> = value => {
saveGlobalPrefs(
{
[prefName]: value,
},
{
onSuccess: onSaveGlobalPrefs,
},
);
};
return [globalPref, setGlobalPref];
const { data: globalPref } = useQuery({
...prefQueries.listGlobal(),
select: prefs => prefs?.[prefName],
enabled: !!prefName,
notifyOnChangeProps: ['data'],
});
return [globalPref as GlobalPrefs[K], saveGlobalPref];
}

View File

@@ -1,9 +1,11 @@
import { useCallback } from 'react';
import { useQuery } from '@tanstack/react-query';
import type { MetadataPrefs } from 'loot-core/types/prefs';
import { savePrefs } from '@desktop-client/prefs/prefsSlice';
import { useDispatch, useSelector } from '@desktop-client/redux';
import {
prefQueries,
useSaveMetadataPrefsMutation,
} from '@desktop-client/prefs';
type SetMetadataPrefAction<K extends keyof MetadataPrefs> = (
value: MetadataPrefs[K],
@@ -12,14 +14,17 @@ type SetMetadataPrefAction<K extends keyof MetadataPrefs> = (
export function useMetadataPref<K extends keyof MetadataPrefs>(
prefName: K,
): [MetadataPrefs[K], SetMetadataPrefAction<K>] {
const dispatch = useDispatch();
const setLocalPref = useCallback<SetMetadataPrefAction<K>>(
value => {
void dispatch(savePrefs({ prefs: { [prefName]: value } }));
},
[prefName, dispatch],
);
const localPref = useSelector(state => state.prefs.local?.[prefName]);
const { mutate: saveMetadataPrefs } = useSaveMetadataPrefsMutation();
const saveMetadataPref: SetMetadataPrefAction<K> = value => {
saveMetadataPrefs({ [prefName]: value });
};
return [localPref, setLocalPref];
const { data: metadataPref } = useQuery({
...prefQueries.listMetadata(),
select: prefs => prefs?.[prefName],
enabled: !!prefName,
notifyOnChangeProps: ['data'],
});
return [metadataPref as MetadataPrefs[K], saveMetadataPref];
}

View File

@@ -1,9 +1,8 @@
import { useCallback } from 'react';
import { useQuery } from '@tanstack/react-query';
import type { SyncedPrefs } from 'loot-core/types/prefs';
import { saveSyncedPrefs } from '@desktop-client/prefs/prefsSlice';
import { useDispatch, useSelector } from '@desktop-client/redux';
import { prefQueries, useSaveSyncedPrefsMutation } from '@desktop-client/prefs';
type SetSyncedPrefAction<K extends keyof SyncedPrefs> = (
value: SyncedPrefs[K],
@@ -12,18 +11,17 @@ type SetSyncedPrefAction<K extends keyof SyncedPrefs> = (
export function useSyncedPref<K extends keyof SyncedPrefs>(
prefName: K,
): [SyncedPrefs[K], SetSyncedPrefAction<K>] {
const dispatch = useDispatch();
const setPref = useCallback<SetSyncedPrefAction<K>>(
value => {
void dispatch(
saveSyncedPrefs({
prefs: { [prefName]: value },
}),
);
},
[prefName, dispatch],
);
const pref = useSelector(state => state.prefs.synced[prefName]);
const { mutate: saveSyncedPrefs } = useSaveSyncedPrefsMutation();
const saveSyncedPref: SetSyncedPrefAction<K> = value => {
saveSyncedPrefs({ [prefName]: value });
};
return [pref, setPref];
const { data: syncedPref } = useQuery({
...prefQueries.listSynced(),
select: prefs => prefs?.[prefName],
enabled: !!prefName,
notifyOnChangeProps: ['data'],
});
return [syncedPref as SyncedPrefs[K], saveSyncedPref];
}

View File

@@ -1,22 +1,15 @@
import { useCallback } from 'react';
import { useQuery } from '@tanstack/react-query';
import type { SyncedPrefs } from 'loot-core/types/prefs';
import { saveSyncedPrefs } from '@desktop-client/prefs/prefsSlice';
import { useDispatch, useSelector } from '@desktop-client/redux';
import { prefQueries, useSaveSyncedPrefsMutation } from '@desktop-client/prefs';
type SetSyncedPrefsAction = (value: Partial<SyncedPrefs>) => void;
/** @deprecated: please use `useSyncedPref` (singular) */
export function useSyncedPrefs(): [SyncedPrefs, SetSyncedPrefsAction] {
const dispatch = useDispatch();
const setPrefs = useCallback<SetSyncedPrefsAction>(
newValue => {
void dispatch(saveSyncedPrefs({ prefs: newValue }));
},
[dispatch],
);
const prefs = useSelector(state => state.prefs.synced);
const { mutate: saveSyncedPrefs } = useSaveSyncedPrefsMutation();
const { data: syncedPrefs } = useQuery(prefQueries.listSynced());
return [prefs, setPrefs];
return [syncedPrefs as SyncedPrefs, saveSyncedPrefs];
}

View File

@@ -13,6 +13,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { send } from 'loot-core/platform/client/connection';
import { q } from 'loot-core/shared/query';
import type { Theme } from 'loot-core/types/prefs';
import * as accountsSlice from './accounts/accountsSlice';
import * as appSlice from './app/appSlice';
@@ -22,6 +23,7 @@ import { App } from './components/App';
import { ServerProvider } from './components/ServerContext';
import * as modalsSlice from './modals/modalsSlice';
import * as notificationsSlice from './notifications/notificationsSlice';
import { prefQueries } from './prefs';
import * as prefsSlice from './prefs/prefsSlice';
import { aqlQuery } from './queries/aqlQuery';
import { configureAppStore } from './redux/store';
@@ -68,6 +70,16 @@ function inputFocused(e: KeyboardEvent) {
);
}
async function setTheme(theme: Theme) {
// TODO: Tests and `toMatchThemeScreenshots` needs to be updated
// to change the theme via UI navigation instead of going through
// this method because this feels a bit hacky.
await send('save-global-prefs', { theme });
void queryClient.invalidateQueries({
queryKey: prefQueries.listGlobal().queryKey,
});
}
// Expose this to the main process to menu items can access it
window.__actionsForMenu = {
...boundActions,
@@ -75,6 +87,7 @@ window.__actionsForMenu = {
redo,
appFocused,
uploadFile,
setTheme,
};
// Expose send for fun!
@@ -108,6 +121,7 @@ declare global {
redo: typeof redo;
appFocused: typeof appFocused;
uploadFile: typeof uploadFile;
setTheme: typeof setTheme;
};
$send: typeof send;

View File

@@ -0,0 +1,2 @@
export * from './queries';
export * from './mutations';

View File

@@ -0,0 +1,250 @@
import { useTranslation } from 'react-i18next';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { v4 as uuidv4 } from 'uuid';
import { send } from 'loot-core/platform/client/connection';
import type {
GlobalPrefs,
MetadataPrefs,
SyncedPrefs,
} from 'loot-core/types/prefs';
import { prefQueries } from './queries';
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
import { useDispatch } from '@desktop-client/redux';
import type { AppDispatch } from '@desktop-client/redux/store';
function dispatchErrorNotification(
dispatch: AppDispatch,
message: string,
error?: Error,
) {
dispatch(
addNotification({
notification: {
id: uuidv4(),
type: 'error',
message,
pre: error ? error.message : undefined,
},
}),
);
}
type SaveMetadataPrefsPayload = MetadataPrefs;
export function useSaveMetadataPrefsMutation() {
const queryClient = useQueryClient();
const dispatch = useDispatch();
const { t } = useTranslation();
return useMutation({
mutationFn: async (metadataPrefs: SaveMetadataPrefsPayload) => {
const existing = await queryClient.ensureQueryData(
prefQueries.listMetadata(),
);
const prefsToSave = diff(metadataPrefs, existing);
if (Object.keys(prefsToSave).length > 0) {
await send('save-prefs', prefsToSave);
}
return prefsToSave;
},
onSuccess: changedPrefs => {
if (changedPrefs && Object.keys(changedPrefs).length > 0) {
queryClient.setQueryData(
prefQueries.listMetadata().queryKey,
oldData => {
return oldData
? {
...oldData,
...changedPrefs,
}
: oldData;
},
);
// Invalidate individual pref caches in case any components are subscribed to those directly
// const queryKeys = Object.keys(changedPrefs).map(
// prefName =>
// prefQueries.detailMetadata(prefName as keyof MetadataPrefs)
// .queryKey,
// );
// queryKeys.forEach(key => invalidateQueries(queryClient, key));
}
},
onError: error => {
console.error('Error saving metadata preferences:', error);
dispatchErrorNotification(
dispatch,
t(
'There was an error saving the metadata preferences. Please try again.',
),
error,
);
throw error;
},
});
}
type SaveGlobalPrefsPayload = GlobalPrefs;
export function useSaveGlobalPrefsMutation() {
const queryClient = useQueryClient();
const dispatch = useDispatch();
const { t } = useTranslation();
return useMutation({
mutationFn: async (globalPrefs: SaveGlobalPrefsPayload) => {
const existing = await queryClient.ensureQueryData(
prefQueries.listGlobal(),
);
const prefsToSave = diff(globalPrefs, existing);
if (Object.keys(prefsToSave).length > 0) {
await send('save-global-prefs', prefsToSave);
}
return prefsToSave;
},
onSuccess: changedPrefs => {
if (changedPrefs && Object.keys(changedPrefs).length > 0) {
queryClient.setQueryData(prefQueries.listGlobal().queryKey, oldData => {
return oldData
? {
...oldData,
...changedPrefs,
}
: oldData;
});
// Invalidate individual pref caches in case any components are subscribed to those directly
// const queryKeys = Object.keys(changedPrefs).map(
// prefName =>
// prefQueries.detailGlobal(prefName as keyof GlobalPrefs).queryKey,
// );
// queryKeys.forEach(key => invalidateQueries(queryClient, key));
}
},
onError: error => {
console.error('Error saving global preferences:', error);
dispatchErrorNotification(
dispatch,
t(
'There was an error saving the global preferences. Please try again.',
),
error,
);
throw error;
},
});
}
type SaveSyncedPrefsPayload = SyncedPrefs;
export function useSaveSyncedPrefsMutation() {
const queryClient = useQueryClient();
const dispatch = useDispatch();
const { t } = useTranslation();
return useMutation({
mutationFn: async (syncedPrefs: SaveSyncedPrefsPayload) => {
const existing = await queryClient.ensureQueryData(
prefQueries.listSynced(),
);
const prefsToSave = diff(syncedPrefs, existing);
if (Object.keys(prefsToSave).length > 0) {
await Promise.all(
Object.entries(prefsToSave).map(([syncedPrefName, value]) =>
send('preferences/save', {
id: syncedPrefName as keyof SyncedPrefs,
value,
}),
),
);
}
return prefsToSave;
},
onSuccess: changedPrefs => {
if (changedPrefs && Object.keys(changedPrefs).length > 0) {
queryClient.setQueryData(prefQueries.listSynced().queryKey, oldData => {
return oldData
? {
...oldData,
...changedPrefs,
}
: oldData;
});
// Invalidate individual pref caches in case any components are subscribed to those directly
// const queryKeys = Object.keys(changedPrefs).map(
// prefName =>
// prefQueries.detailSynced(prefName as keyof SyncedPrefs).queryKey,
// );
// queryKeys.forEach(key => invalidateQueries(queryClient, key));
}
},
onError: error => {
console.error('Error saving synced preferences:', error);
dispatchErrorNotification(
dispatch,
t(
'There was an error saving the synced preferences. Please try again.',
),
error,
);
throw error;
},
});
}
// type SaveServerPrefsPayload = ServerPrefs;
// export function useSaveServerPrefsMutation() {
// const queryClient = useQueryClient();
// const dispatch = useDispatch();
// const { t } = useTranslation();
// return useMutation({
// mutationFn: async (serverPrefs: SaveServerPrefsPayload) => {
// const result = await send('save-server-prefs', {
// prefs: serverPrefs,
// });
// if (result && 'error' in result) {
// return { error: result.error };
// }
// },
// onSuccess: () => invalidateQueries(queryClient, prefQueries.listServer().queryKey),
// onError: error => {
// console.error('Error saving server preferences:', error);
// dispatchErrorNotification(
// dispatch,
// t(
// 'There was an error saving the server preferences. Please try again.',
// ),
// error,
// );
// throw error;
// },
// });
// }
function diff<T extends object>(incoming: T, existing?: T | null): Partial<T> {
const changed: Partial<T> = {};
for (const [key, value] of Object.entries(incoming) as Array<
[keyof T, T[keyof T]]
>) {
if (!existing || existing[key] !== value) {
(changed as Record<keyof T, T[keyof T]>)[key] = value;
}
}
return changed;
}

View File

@@ -2,134 +2,21 @@ import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import { send } from 'loot-core/platform/client/connection';
import { parseNumberFormat, setNumberFormat } from 'loot-core/shared/util';
import type {
GlobalPrefs,
MetadataPrefs,
ServerPrefs,
SyncedPrefs,
} from 'loot-core/types/prefs';
import type { ServerPrefs } from 'loot-core/types/prefs';
import { resetApp } from '@desktop-client/app/appSlice';
import { setI18NextLanguage } from '@desktop-client/i18n';
import { closeModal } from '@desktop-client/modals/modalsSlice';
import { createAppAsyncThunk } from '@desktop-client/redux';
import { getUserData } from '@desktop-client/users/usersSlice';
const sliceName = 'prefs';
type PrefsState = {
local: MetadataPrefs;
global: GlobalPrefs;
synced: SyncedPrefs;
server: ServerPrefs;
};
const initialState: PrefsState = {
local: {},
global: {},
synced: {},
server: {},
};
export const loadPrefs = createAppAsyncThunk(
`${sliceName}/loadPrefs`,
async (_, { dispatch, getState }) => {
const prefs = await send('load-prefs');
// Remove any modal state if switching between budgets
const currentPrefs = getState().prefs.local;
if (prefs && prefs.id && !currentPrefs) {
dispatch(closeModal());
}
const [globalPrefs, syncedPrefs] = await Promise.all([
send('load-global-prefs'),
send('preferences/get'),
]);
dispatch(
setPrefs({ local: prefs, global: globalPrefs, synced: syncedPrefs }),
);
// Certain loot-core utils depend on state outside of the React tree, update them
setNumberFormat(
parseNumberFormat({
format: syncedPrefs.numberFormat,
hideFraction: syncedPrefs.hideFraction,
}),
);
// We need to load translations before the app renders
setI18NextLanguage(globalPrefs.language ?? '');
return prefs;
},
);
type SavePrefsPayload = {
prefs: MetadataPrefs;
};
export const savePrefs = createAppAsyncThunk(
`${sliceName}/savePrefs`,
async ({ prefs }: SavePrefsPayload, { dispatch }) => {
await send('save-prefs', prefs);
dispatch(mergeLocalPrefs(prefs));
},
);
export const loadGlobalPrefs = createAppAsyncThunk(
`${sliceName}/loadGlobalPrefs`,
async (_, { dispatch, getState }) => {
const globalPrefs = await send('load-global-prefs');
dispatch(
setPrefs({
local: getState().prefs.local,
global: globalPrefs,
synced: getState().prefs.synced,
}),
);
return globalPrefs;
},
);
type SaveGlobalPrefsPayload = {
prefs: GlobalPrefs;
onSaveGlobalPrefs?: () => void;
};
export const saveGlobalPrefs = createAppAsyncThunk(
`${sliceName}/saveGlobalPrefs`,
async (
{ prefs, onSaveGlobalPrefs }: SaveGlobalPrefsPayload,
{ dispatch },
) => {
await send('save-global-prefs', prefs);
dispatch(mergeGlobalPrefs(prefs));
onSaveGlobalPrefs?.();
},
);
type SaveSyncedPrefsPayload = {
prefs: SyncedPrefs;
};
export const saveSyncedPrefs = createAppAsyncThunk(
`${sliceName}/saveSyncedPrefs`,
async ({ prefs }: SaveSyncedPrefsPayload, { dispatch }) => {
await Promise.all(
Object.entries(prefs).map(([prefName, value]) =>
send('preferences/save', {
id: prefName as keyof SyncedPrefs,
value,
}),
),
);
dispatch(mergeSyncedPrefs(prefs));
},
);
type SaveServerPrefsPayload = {
prefs: ServerPrefs;
};
@@ -147,35 +34,12 @@ export const saveServerPrefs = createAppAsyncThunk(
},
);
type SetPrefsPayload = {
local: MetadataPrefs;
global: GlobalPrefs;
synced: SyncedPrefs;
};
type MergeLocalPrefsPayload = MetadataPrefs;
type MergeGlobalPrefsPayload = GlobalPrefs;
type MergeSyncedPrefsPayload = SyncedPrefs;
type MergeServerPrefsPayload = ServerPrefs;
const prefsSlice = createSlice({
name: sliceName,
initialState,
reducers: {
setPrefs(state, action: PayloadAction<SetPrefsPayload>) {
state.local = action.payload.local;
state.global = action.payload.global;
state.synced = action.payload.synced;
},
mergeLocalPrefs(state, action: PayloadAction<MergeLocalPrefsPayload>) {
state.local = { ...state.local, ...action.payload };
},
mergeGlobalPrefs(state, action: PayloadAction<MergeGlobalPrefsPayload>) {
state.global = { ...state.global, ...action.payload };
},
mergeSyncedPrefs(state, action: PayloadAction<MergeSyncedPrefsPayload>) {
state.synced = { ...state.synced, ...action.payload };
},
mergeServerPrefs(state, action: PayloadAction<MergeServerPrefsPayload>) {
state.server = { ...state.server, ...action.payload };
},
@@ -183,7 +47,6 @@ const prefsSlice = createSlice({
extraReducers: builder => {
builder.addCase(resetApp, state => ({
...initialState,
global: state.global || initialState.global,
server: state.server || initialState.server,
}));
builder.addCase(getUserData.fulfilled, (state, action) => {
@@ -211,18 +74,7 @@ export const { name, reducer, getInitialState } = prefsSlice;
export const actions = {
...prefsSlice.actions,
loadPrefs,
savePrefs,
loadGlobalPrefs,
saveGlobalPrefs,
saveSyncedPrefs,
saveServerPrefs,
};
export const {
mergeGlobalPrefs,
mergeLocalPrefs,
mergeServerPrefs,
mergeSyncedPrefs,
setPrefs,
} = actions;
export const { mergeServerPrefs } = actions;

View File

@@ -0,0 +1,104 @@
import { queryOptions } from '@tanstack/react-query';
import { send } from 'loot-core/platform/client/connection';
import { parseNumberFormat, setNumberFormat } from 'loot-core/shared/util';
import type {
GlobalPrefs,
MetadataPrefs,
ServerPrefs,
SyncedPrefs,
} from 'loot-core/types/prefs';
import { setI18NextLanguage } from '@desktop-client/i18n';
export type AllPrefs = {
metadata: MetadataPrefs;
global: GlobalPrefs;
synced: SyncedPrefs;
server: ServerPrefs;
};
export const prefQueries = {
all: () => ['prefs'],
lists: () => [...prefQueries.all(), 'lists'],
list: () =>
queryOptions<AllPrefs>({
queryKey: [...prefQueries.lists(), 'all'],
queryFn: async ({ client }) => {
const [metadataPrefs, globalPrefs, syncedPrefs] = await Promise.all([
client.ensureQueryData(prefQueries.listMetadata()),
client.ensureQueryData(prefQueries.listGlobal()),
client.ensureQueryData(prefQueries.listSynced()),
]);
// Certain loot-core utils depend on state outside of the React tree, update them
setNumberFormat(
parseNumberFormat({
format: syncedPrefs.numberFormat,
hideFraction: syncedPrefs.hideFraction,
}),
);
// We need to load translations before the app renders
setI18NextLanguage(globalPrefs.language ?? '');
return {
metadata: metadataPrefs,
global: globalPrefs,
synced: syncedPrefs,
server: {}, // Server prefs are loaded separately
};
},
placeholderData: {
metadata: {},
global: {},
synced: {},
server: {},
},
// Manually invalidated when preferences change
staleTime: Infinity,
}),
listMetadata: () =>
queryOptions<MetadataPrefs>({
queryKey: [...prefQueries.lists(), 'metadata'],
queryFn: async () => {
return await send('load-prefs');
},
placeholderData: {},
// Manually invalidated when local preferences change
staleTime: Infinity,
}),
listGlobal: () =>
queryOptions<GlobalPrefs>({
queryKey: [...prefQueries.lists(), 'global'],
queryFn: async () => {
return await send('load-global-prefs');
},
placeholderData: {},
// Manually invalidated when global preferences change
staleTime: Infinity,
}),
listSynced: () =>
queryOptions<SyncedPrefs>({
queryKey: [...prefQueries.lists(), 'synced'],
queryFn: async ({ client }) => {
const metadataPrefs = await client.getQueryData(
prefQueries.listMetadata().queryKey,
);
// Synced prefs are budget-specific, so if we don't have
// a budget loaded, just return an empty object.
if (!metadataPrefs?.id) {
return {};
}
return await send('preferences/get');
},
placeholderData: {},
// Manually invalidated when synced preferences change
staleTime: Infinity,
}),
listServer: () =>
queryOptions({
...prefQueries.list(),
select: data => data.server,
}),
};

View File

@@ -14,7 +14,7 @@ import { pushModal } from './modals/modalsSlice';
import { addNotification } from './notifications/notificationsSlice';
import type { Notification } from './notifications/notificationsSlice';
import { payeeQueries } from './payees';
import { loadPrefs } from './prefs/prefsSlice';
import { prefQueries } from './prefs';
import type { AppStore } from './redux/store';
import { signOut } from './users/usersSlice';
@@ -37,8 +37,8 @@ export function listenForSyncEvent(store: AppStore, queryClient: QueryClient) {
let attemptedSyncRepair = false;
const unlistenSuccess = listen('sync-event', event => {
const prefs = store.getState().prefs.local;
const unlistenSuccess = listen('sync-event', async event => {
const prefs = await queryClient.ensureQueryData(prefQueries.listMetadata());
if (!prefs || !prefs.id) {
// Do nothing if no budget is loaded
return;
@@ -62,7 +62,9 @@ export function listenForSyncEvent(store: AppStore, queryClient: QueryClient) {
const tables = event.tables;
if (tables.includes('prefs')) {
void store.dispatch(loadPrefs());
void queryClient.invalidateQueries({
queryKey: prefQueries.lists(),
});
}
if (
@@ -219,8 +221,10 @@ export function listenForSyncEvent(store: AppStore, queryClient: QueryClient) {
title: t('Register'),
action: async () => {
await store.dispatch(uploadBudget({}));
void store.dispatch(sync());
void store.dispatch(loadPrefs());
store.dispatch(sync());
void queryClient.invalidateQueries({
queryKey: prefQueries.lists(),
});
},
},
};
@@ -253,7 +257,9 @@ export function listenForSyncEvent(store: AppStore, queryClient: QueryClient) {
// the server does not match the local one. This can mean a
// few things depending on the state, and we try to show an
// appropriate message and call to action to fix it.
const { cloudFileId } = store.getState().prefs.local;
const { cloudFileId } = await queryClient.ensureQueryData(
prefQueries.listMetadata(),
);
if (!cloudFileId) {
console.error(
'Received file-has-reset or file-has-new-key error but no cloudFileId in prefs',

View File

@@ -9,7 +9,7 @@ import {
closeBudget,
loadAllFiles,
} from '@desktop-client/budgetfiles/budgetfilesSlice';
import { loadGlobalPrefs } from '@desktop-client/prefs/prefsSlice';
import { prefQueries } from '@desktop-client/prefs';
import { createAppAsyncThunk } from '@desktop-client/redux';
const sliceName = 'user';
@@ -48,11 +48,13 @@ export const loggedIn = createAppAsyncThunk(
export const signOut = createAppAsyncThunk(
`${sliceName}/signOut`,
async (_, { dispatch }) => {
async (_, { dispatch, extra: { queryClient } }) => {
await send('subscribe-sign-out');
void dispatch(getUserData());
void dispatch(loadGlobalPrefs());
void queryClient.invalidateQueries({
queryKey: prefQueries.listGlobal().queryKey,
});
void dispatch(closeBudget());
// Handled in budgetSlice
// dispatch({ type: constants.SIGN_OUT });

View File

@@ -25,7 +25,7 @@ import type {
import { copy, exists, mkdir, remove } from 'fs-extra';
import promiseRetry from 'promise-retry';
import type { GlobalPrefsJson } from '../loot-core/src/types/prefs';
import type { GlobalPrefsJson, Theme } from '../loot-core/src/types/prefs';
import { getMenu } from './menu';
import {
@@ -625,11 +625,10 @@ ipcMain.on('message', (_event, msg) => {
serverProcess.postMessage(msg.args);
});
ipcMain.on('set-theme', (_event, theme: string) => {
const obj = { theme };
ipcMain.on('set-theme', async (_event, theme: Theme) => {
if (clientWin) {
void clientWin.webContents.executeJavaScript(
`window.__actionsForMenu && window.__actionsForMenu.saveGlobalPrefs({ prefs: ${JSON.stringify(obj)} })`,
await clientWin.webContents.executeJavaScript(
`window.__actionsForMenu && window.__actionsForMenu.setTheme('${theme}')`,
);
}
});

View File

@@ -1,6 +1,8 @@
import { contextBridge, ipcRenderer } from 'electron';
import type { IpcRenderer } from 'electron';
import type { Theme } from 'loot-core/types/prefs';
import type {
GetBootstrapDataPayload,
OpenFileDialogPayload,
@@ -85,7 +87,7 @@ contextBridge.exposeInMainWorld('Actual', {
return null;
},
setTheme: (theme: string) => {
setTheme: async (theme: Theme) => {
ipcRenderer.send('set-theme', theme);
},

View File

@@ -1,5 +1,7 @@
import type EventEmitter from 'events';
import type { Theme } from 'loot-core/types/prefs';
export type IpcClient = {
on: EventEmitter['on'];
emit: (name: string, data: unknown) => void;
@@ -34,7 +36,7 @@ type Actual = {
applyAppUpdate: () => Promise<void>;
ipcConnect: (callback: (client: IpcClient) => void) => void;
getServerSocket: () => Promise<Worker | null>;
setTheme: (theme: string) => void;
setTheme: (theme: Theme) => Promise<void>;
logToTerminal: (...args: unknown[]) => void;
onEventFromMain: (
event: string,