mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-22 20:40:08 -05:00
Compare commits
32 Commits
react-quer
...
react-quer
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fa4d673cf | ||
|
|
7995d659ab | ||
|
|
52c4586051 | ||
|
|
a6873cd5c7 | ||
|
|
6001c37285 | ||
|
|
82743c6f90 | ||
|
|
aca0293750 | ||
|
|
f24a9023c5 | ||
|
|
d0a653cdae | ||
|
|
158c79281d | ||
|
|
470fb13d37 | ||
|
|
e993862c5a | ||
|
|
329556b56d | ||
|
|
635ae01ea4 | ||
|
|
3f35379244 | ||
|
|
d21aa62186 | ||
|
|
29d9507254 | ||
|
|
5606216b0d | ||
|
|
4c7b15d1a4 | ||
|
|
6fcf383243 | ||
|
|
83fb65f413 | ||
|
|
9665945a69 | ||
|
|
f039864a64 | ||
|
|
1bdb052107 | ||
|
|
696d1df508 | ||
|
|
8d4086ad75 | ||
|
|
fedef15cf2 | ||
|
|
5ae1f539c4 | ||
|
|
40d45a5c9e | ||
|
|
5263424a77 | ||
|
|
757c93afe2 | ||
|
|
6ca922f4a4 |
@@ -6,7 +6,7 @@ import { getUploadError } from 'loot-core/shared/errors';
|
|||||||
import type { AtLeastOne } from 'loot-core/types/util';
|
import type { AtLeastOne } from 'loot-core/types/util';
|
||||||
|
|
||||||
import { pushModal } from '@desktop-client/modals/modalsSlice';
|
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 { createAppAsyncThunk } from '@desktop-client/redux';
|
||||||
import { getIsOutdated, getLatestVersion } from '@desktop-client/util/versions';
|
import { getIsOutdated, getLatestVersion } from '@desktop-client/util/versions';
|
||||||
|
|
||||||
@@ -89,8 +89,8 @@ export const resetSync = createAppAsyncThunk(
|
|||||||
|
|
||||||
export const sync = createAppAsyncThunk(
|
export const sync = createAppAsyncThunk(
|
||||||
`${sliceName}/sync`,
|
`${sliceName}/sync`,
|
||||||
async (_, { dispatch, getState }) => {
|
async (_, { extra: { queryClient } }) => {
|
||||||
const prefs = getState().prefs.local;
|
const prefs = await queryClient.ensureQueryData(prefQueries.listMetadata());
|
||||||
if (prefs && prefs.id) {
|
if (prefs && prefs.id) {
|
||||||
const result = await send('sync');
|
const result = await send('sync');
|
||||||
if (result && 'error' in result) {
|
if (result && 'error' in result) {
|
||||||
@@ -98,7 +98,9 @@ export const sync = createAppAsyncThunk(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update the prefs
|
// Update the prefs
|
||||||
await dispatch(loadPrefs());
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: prefQueries.lists(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
@@ -107,8 +109,10 @@ export const sync = createAppAsyncThunk(
|
|||||||
|
|
||||||
export const getLatestAppVersion = createAppAsyncThunk(
|
export const getLatestAppVersion = createAppAsyncThunk(
|
||||||
`${sliceName}/getLatestAppVersion`,
|
`${sliceName}/getLatestAppVersion`,
|
||||||
async (_, { dispatch, getState }) => {
|
async (_, { dispatch, extra: { queryClient } }) => {
|
||||||
const globalPrefs = getState().prefs.global;
|
const globalPrefs = await queryClient.ensureQueryData(
|
||||||
|
prefQueries.listGlobal(),
|
||||||
|
);
|
||||||
if (globalPrefs && globalPrefs.notifyWhenUpdateIsAvailable) {
|
if (globalPrefs && globalPrefs.notifyWhenUpdateIsAvailable) {
|
||||||
const theLatestVersion = await getLatestVersion();
|
const theLatestVersion = await getLatestVersion();
|
||||||
dispatch(
|
dispatch(
|
||||||
|
|||||||
@@ -219,8 +219,8 @@ global.Actual = {
|
|||||||
return worker;
|
return worker;
|
||||||
},
|
},
|
||||||
|
|
||||||
setTheme: theme => {
|
setTheme: async theme => {
|
||||||
window.__actionsForMenu.saveGlobalPrefs({ prefs: { theme } });
|
await window.__actionsForMenu.setTheme(theme);
|
||||||
},
|
},
|
||||||
|
|
||||||
moveBudgetDirectory: () => {
|
moveBudgetDirectory: () => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import { type QueryClient } from '@tanstack/react-query';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
|
|
||||||
import { send } from 'loot-core/platform/client/connection';
|
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 { resetApp, setAppState } from '@desktop-client/app/appSlice';
|
||||||
import { closeModal, pushModal } from '@desktop-client/modals/modalsSlice';
|
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 { createAppAsyncThunk } from '@desktop-client/redux';
|
||||||
import { signOut } from '@desktop-client/users/usersSlice';
|
import { signOut } from '@desktop-client/users/usersSlice';
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ type LoadBudgetPayload = {
|
|||||||
|
|
||||||
export const loadBudget = createAppAsyncThunk(
|
export const loadBudget = createAppAsyncThunk(
|
||||||
`${sliceName}/loadBudget`,
|
`${sliceName}/loadBudget`,
|
||||||
async ({ id, options = {} }: LoadBudgetPayload, { dispatch }) => {
|
async ({ id, options = {} }: LoadBudgetPayload, { dispatch, extra }) => {
|
||||||
dispatch(setAppState({ loadingText: t('Loading...') }));
|
dispatch(setAppState({ loadingText: t('Loading...') }));
|
||||||
|
|
||||||
// Loading a budget may fail
|
// Loading a budget may fail
|
||||||
@@ -89,23 +90,43 @@ export const loadBudget = createAppAsyncThunk(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dispatch(closeModal());
|
dispatch(closeModal());
|
||||||
await dispatch(loadPrefs());
|
extra.queryClient.invalidateQueries({
|
||||||
|
queryKey: prefQueries.lists(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(setAppState({ loadingText: null }));
|
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(
|
export const closeBudget = createAppAsyncThunk(
|
||||||
`${sliceName}/closeBudget`,
|
`${sliceName}/closeBudget`,
|
||||||
async (_, { dispatch, getState, extra: { queryClient } }) => {
|
async (_, { dispatch, extra: { queryClient } }) => {
|
||||||
const prefs = getState().prefs.local;
|
const prefs = await queryClient.ensureQueryData(prefQueries.listMetadata());
|
||||||
if (prefs && prefs.id) {
|
if (prefs && prefs.id) {
|
||||||
dispatch(resetApp());
|
dispatch(resetApp());
|
||||||
queryClient.clear();
|
|
||||||
dispatch(setAppState({ loadingText: t('Closing...') }));
|
dispatch(setAppState({ loadingText: t('Closing...') }));
|
||||||
await send('close-budget');
|
await send('close-budget');
|
||||||
dispatch(setAppState({ loadingText: null }));
|
dispatch(setAppState({ loadingText: null }));
|
||||||
|
invalidateClosedBudgetQueries(queryClient);
|
||||||
if (localStorage.getItem('SharedArrayBufferOverride')) {
|
if (localStorage.getItem('SharedArrayBufferOverride')) {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
@@ -115,11 +136,11 @@ export const closeBudget = createAppAsyncThunk(
|
|||||||
|
|
||||||
export const closeBudgetUI = createAppAsyncThunk(
|
export const closeBudgetUI = createAppAsyncThunk(
|
||||||
`${sliceName}/closeBudgetUI`,
|
`${sliceName}/closeBudgetUI`,
|
||||||
async (_, { dispatch, getState, extra: { queryClient } }) => {
|
async (_, { dispatch, extra: { queryClient } }) => {
|
||||||
const prefs = getState().prefs.local;
|
const prefs = await queryClient.ensureQueryData(prefQueries.listMetadata());
|
||||||
if (prefs && prefs.id) {
|
if (prefs && prefs.id) {
|
||||||
dispatch(resetApp());
|
dispatch(resetApp());
|
||||||
queryClient.clear();
|
invalidateClosedBudgetQueries(queryClient);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -146,7 +167,7 @@ export const createBudget = createAppAsyncThunk(
|
|||||||
`${sliceName}/createBudget`,
|
`${sliceName}/createBudget`,
|
||||||
async (
|
async (
|
||||||
{ testMode = false, demoMode = false }: CreateBudgetPayload,
|
{ testMode = false, demoMode = false }: CreateBudgetPayload,
|
||||||
{ dispatch },
|
{ dispatch, extra: { queryClient } },
|
||||||
) => {
|
) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
setAppState({
|
setAppState({
|
||||||
@@ -164,7 +185,9 @@ export const createBudget = createAppAsyncThunk(
|
|||||||
dispatch(closeModal());
|
dispatch(closeModal());
|
||||||
|
|
||||||
await dispatch(loadAllFiles());
|
await dispatch(loadAllFiles());
|
||||||
await dispatch(loadPrefs());
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: prefQueries.lists(),
|
||||||
|
});
|
||||||
|
|
||||||
// Set the loadingText to null after we've loaded the budget prefs
|
// Set the loadingText to null after we've loaded the budget prefs
|
||||||
// so that the existing manager page doesn't flash
|
// so that the existing manager page doesn't flash
|
||||||
@@ -243,14 +266,19 @@ type ImportBudgetPayload = {
|
|||||||
|
|
||||||
export const importBudget = createAppAsyncThunk(
|
export const importBudget = createAppAsyncThunk(
|
||||||
`${sliceName}/importBudget`,
|
`${sliceName}/importBudget`,
|
||||||
async ({ filepath, type }: ImportBudgetPayload, { dispatch }) => {
|
async (
|
||||||
|
{ filepath, type }: ImportBudgetPayload,
|
||||||
|
{ dispatch, extra: { queryClient } },
|
||||||
|
) => {
|
||||||
const { error } = await send('import-budget', { filepath, type });
|
const { error } = await send('import-budget', { filepath, type });
|
||||||
if (error) {
|
if (error) {
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(closeModal());
|
dispatch(closeModal());
|
||||||
await dispatch(loadPrefs());
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: prefQueries.lists(),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -304,7 +332,7 @@ export const downloadBudget = createAppAsyncThunk(
|
|||||||
`${sliceName}/downloadBudget`,
|
`${sliceName}/downloadBudget`,
|
||||||
async (
|
async (
|
||||||
{ cloudFileId, replace = false }: DownloadBudgetPayload,
|
{ cloudFileId, replace = false }: DownloadBudgetPayload,
|
||||||
{ dispatch },
|
{ dispatch, extra: { queryClient } },
|
||||||
): Promise<string | null> => {
|
): Promise<string | null> => {
|
||||||
dispatch(
|
dispatch(
|
||||||
setAppState({
|
setAppState({
|
||||||
@@ -368,8 +396,10 @@ export const downloadBudget = createAppAsyncThunk(
|
|||||||
if (!id) {
|
if (!id) {
|
||||||
throw new Error('No id returned from download.');
|
throw new Error('No id returned from download.');
|
||||||
}
|
}
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: prefQueries.listGlobal().queryKey,
|
||||||
|
});
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
dispatch(loadGlobalPrefs()),
|
|
||||||
dispatch(loadAllFiles()),
|
dispatch(loadAllFiles()),
|
||||||
dispatch(loadBudget({ id })),
|
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
|
// Take in the budget id so that backups can be loaded when a budget isn't opened
|
||||||
export const loadBackup = createAppAsyncThunk(
|
export const loadBackup = createAppAsyncThunk(
|
||||||
`${sliceName}/loadBackup`,
|
`${sliceName}/loadBackup`,
|
||||||
async ({ budgetId, backupId }: LoadBackupPayload, { dispatch, getState }) => {
|
async (
|
||||||
const prefs = getState().prefs.local;
|
{ budgetId, backupId }: LoadBackupPayload,
|
||||||
|
{ dispatch, extra: { queryClient } },
|
||||||
|
) => {
|
||||||
|
const prefs = await queryClient.ensureQueryData(prefQueries.listMetadata());
|
||||||
if (prefs && prefs.id) {
|
if (prefs && prefs.id) {
|
||||||
await dispatch(closeBudget());
|
await dispatch(closeBudget());
|
||||||
}
|
}
|
||||||
@@ -400,8 +433,8 @@ export const loadBackup = createAppAsyncThunk(
|
|||||||
|
|
||||||
export const makeBackup = createAppAsyncThunk(
|
export const makeBackup = createAppAsyncThunk(
|
||||||
`${sliceName}/makeBackup`,
|
`${sliceName}/makeBackup`,
|
||||||
async (_, { getState }) => {
|
async (_, { extra: { queryClient } }) => {
|
||||||
const prefs = getState().prefs.local;
|
const prefs = await queryClient.ensureQueryData(prefQueries.listMetadata());
|
||||||
if (prefs && prefs.id) {
|
if (prefs && prefs.id) {
|
||||||
await send('backup-make', { id: prefs.id });
|
await send('backup-make', { id: prefs.id });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,10 +38,9 @@ import { SpreadsheetProvider } from '@desktop-client/hooks/useSpreadsheet';
|
|||||||
import { setI18NextLanguage } from '@desktop-client/i18n';
|
import { setI18NextLanguage } from '@desktop-client/i18n';
|
||||||
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
|
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
|
||||||
import { installPolyfills } from '@desktop-client/polyfills';
|
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 { useDispatch, useSelector, useStore } from '@desktop-client/redux';
|
||||||
import {
|
import {
|
||||||
CustomThemeStyle,
|
|
||||||
hasHiddenScrollbars,
|
hasHiddenScrollbars,
|
||||||
ThemeStyle,
|
ThemeStyle,
|
||||||
useTheme,
|
useTheme,
|
||||||
@@ -61,6 +60,8 @@ function AppInner() {
|
|||||||
setI18NextLanguage(null);
|
setI18NextLanguage(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const maybeUpdate = async <T,>(cb?: () => T): Promise<T | void> => {
|
const maybeUpdate = async <T,>(cb?: () => T): Promise<T | void> => {
|
||||||
if (global.Actual.isUpdateReadyForDownload()) {
|
if (global.Actual.isUpdateReadyForDownload()) {
|
||||||
@@ -91,7 +92,7 @@ function AppInner() {
|
|||||||
loadingText: t('Loading global preferences...'),
|
loadingText: t('Loading global preferences...'),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await dispatch(loadGlobalPrefs());
|
void queryClient.prefetchQuery(prefQueries.listGlobal());
|
||||||
|
|
||||||
// Open the last opened budget, if any
|
// Open the last opened budget, if any
|
||||||
dispatch(
|
dispatch(
|
||||||
@@ -239,7 +240,6 @@ export function App() {
|
|||||||
<AppInner />
|
<AppInner />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
<ThemeStyle />
|
<ThemeStyle />
|
||||||
<CustomThemeStyle />
|
|
||||||
<ErrorBoundary FallbackComponent={FatalError}>
|
<ErrorBoundary FallbackComponent={FatalError}>
|
||||||
<Modals />
|
<Modals />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Navigate, Route, Routes, useHref, useLocation } from 'react-router';
|
|||||||
import { useResponsive } from '@actual-app/components/hooks/useResponsive';
|
import { useResponsive } from '@actual-app/components/hooks/useResponsive';
|
||||||
import { theme } from '@actual-app/components/theme';
|
import { theme } from '@actual-app/components/theme';
|
||||||
import { View } from '@actual-app/components/view';
|
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';
|
import * as undo from 'loot-core/platform/client/undo';
|
||||||
|
|
||||||
@@ -28,17 +28,19 @@ import { FloatableSidebar } from './sidebar';
|
|||||||
import { ManageTagsPage } from './tags/ManageTagsPage';
|
import { ManageTagsPage } from './tags/ManageTagsPage';
|
||||||
import { Titlebar } from './Titlebar';
|
import { Titlebar } from './Titlebar';
|
||||||
|
|
||||||
import { accountQueries } from '@desktop-client/accounts';
|
|
||||||
import { getLatestAppVersion, sync } from '@desktop-client/app/appSlice';
|
import { getLatestAppVersion, sync } from '@desktop-client/app/appSlice';
|
||||||
import { ProtectedRoute } from '@desktop-client/auth/ProtectedRoute';
|
import { ProtectedRoute } from '@desktop-client/auth/ProtectedRoute';
|
||||||
import { Permissions } from '@desktop-client/auth/types';
|
import { Permissions } from '@desktop-client/auth/types';
|
||||||
|
import { useAccounts } from '@desktop-client/hooks/useAccounts';
|
||||||
import { useGlobalPref } from '@desktop-client/hooks/useGlobalPref';
|
import { useGlobalPref } from '@desktop-client/hooks/useGlobalPref';
|
||||||
import { useLocalPref } from '@desktop-client/hooks/useLocalPref';
|
import { useLocalPref } from '@desktop-client/hooks/useLocalPref';
|
||||||
import { useMetaThemeColor } from '@desktop-client/hooks/useMetaThemeColor';
|
import { useMetaThemeColor } from '@desktop-client/hooks/useMetaThemeColor';
|
||||||
import { useNavigate } from '@desktop-client/hooks/useNavigate';
|
import { useNavigate } from '@desktop-client/hooks/useNavigate';
|
||||||
import { ScrollProvider } from '@desktop-client/hooks/useScrollListener';
|
import { ScrollProvider } from '@desktop-client/hooks/useScrollListener';
|
||||||
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
|
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
|
||||||
|
import { prefQueries } from '@desktop-client/prefs';
|
||||||
import { useDispatch, useSelector } from '@desktop-client/redux';
|
import { useDispatch, useSelector } from '@desktop-client/redux';
|
||||||
|
import { CustomThemeStyle } from '@desktop-client/style';
|
||||||
|
|
||||||
function NarrowNotSupported({
|
function NarrowNotSupported({
|
||||||
redirectTo = '/budget',
|
redirectTo = '/budget',
|
||||||
@@ -91,10 +93,7 @@ export function FinancesApp() {
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// TODO: Replace with `useAccounts` hook once it's updated to return the useQuery results.
|
const { data: accounts, isFetching: isAccountsFetching } = useAccounts();
|
||||||
const { data: accounts, isFetching: isAccountsFetching } = useQuery(
|
|
||||||
accountQueries.list(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const versionInfo = useSelector(state => state.app.versionInfo);
|
const versionInfo = useSelector(state => state.app.versionInfo);
|
||||||
const [notifyWhenUpdateIsAvailable] = useGlobalPref(
|
const [notifyWhenUpdateIsAvailable] = useGlobalPref(
|
||||||
@@ -198,6 +197,7 @@ export function FinancesApp() {
|
|||||||
<RouterBehaviors />
|
<RouterBehaviors />
|
||||||
<GlobalKeys />
|
<GlobalKeys />
|
||||||
<CommandBar />
|
<CommandBar />
|
||||||
|
<CustomThemeStyle />
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { styles } from '@actual-app/components/styles';
|
|||||||
import { Text } from '@actual-app/components/text';
|
import { Text } from '@actual-app/components/text';
|
||||||
import { theme } from '@actual-app/components/theme';
|
import { theme } from '@actual-app/components/theme';
|
||||||
import { View } from '@actual-app/components/view';
|
import { View } from '@actual-app/components/view';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { listen } from 'loot-core/platform/client/connection';
|
import { listen } from 'loot-core/platform/client/connection';
|
||||||
import type { RemoteFile, SyncedLocalFile } from 'loot-core/types/file';
|
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',
|
f => f.state === 'remote' || f.state === 'synced' || f.state === 'detached',
|
||||||
) as (SyncedLocalFile | RemoteFile)[];
|
) as (SyncedLocalFile | RemoteFile)[];
|
||||||
const currentFile = remoteFiles.find(f => f.cloudFileId === cloudFileId);
|
const currentFile = remoteFiles.find(f => f.cloudFileId === cloudFileId);
|
||||||
const hasSyncedPrefs = useSelector(state => state.prefs.synced);
|
|
||||||
|
|
||||||
const initializeUserData = useCallback(async () => {
|
const initializeUserData = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -235,7 +235,7 @@ export function LoggedInUser({
|
|||||||
multiuserEnabled &&
|
multiuserEnabled &&
|
||||||
userData &&
|
userData &&
|
||||||
userData?.displayName &&
|
userData?.displayName &&
|
||||||
!hasSyncedPrefs && (
|
!budgetId && (
|
||||||
<small>
|
<small>
|
||||||
(
|
(
|
||||||
<Trans>
|
<Trans>
|
||||||
@@ -251,7 +251,7 @@ export function LoggedInUser({
|
|||||||
multiuserEnabled &&
|
multiuserEnabled &&
|
||||||
userData &&
|
userData &&
|
||||||
userData?.displayName &&
|
userData?.displayName &&
|
||||||
hasSyncedPrefs && (
|
budgetId && (
|
||||||
<small>
|
<small>
|
||||||
(
|
(
|
||||||
<Trans>
|
<Trans>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
useSaveCategoryGroupMutation,
|
useSaveCategoryGroupMutation,
|
||||||
useSaveCategoryMutation,
|
useSaveCategoryMutation,
|
||||||
} from '@desktop-client/budget';
|
} from '@desktop-client/budget';
|
||||||
|
import { LoadingIndicator } from '@desktop-client/components/reports/LoadingIndicator';
|
||||||
import { useCategories } from '@desktop-client/hooks/useCategories';
|
import { useCategories } from '@desktop-client/hooks/useCategories';
|
||||||
import { useGlobalPref } from '@desktop-client/hooks/useGlobalPref';
|
import { useGlobalPref } from '@desktop-client/hooks/useGlobalPref';
|
||||||
import { useLocalPref } from '@desktop-client/hooks/useLocalPref';
|
import { useLocalPref } from '@desktop-client/hooks/useLocalPref';
|
||||||
@@ -175,7 +176,7 @@ export function Budget() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!initialized || !categoryGroups) {
|
if (!initialized || !categoryGroups) {
|
||||||
return null;
|
return <LoadingIndicator />;
|
||||||
}
|
}
|
||||||
|
|
||||||
let table;
|
let table;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
} from '@desktop-client/components/ServerContext';
|
} from '@desktop-client/components/ServerContext';
|
||||||
import { useGlobalPref } from '@desktop-client/hooks/useGlobalPref';
|
import { useGlobalPref } from '@desktop-client/hooks/useGlobalPref';
|
||||||
import { useNavigate } from '@desktop-client/hooks/useNavigate';
|
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 { useDispatch } from '@desktop-client/redux';
|
||||||
import { loggedIn, signOut } from '@desktop-client/users/usersSlice';
|
import { loggedIn, signOut } from '@desktop-client/users/usersSlice';
|
||||||
|
|
||||||
@@ -40,7 +40,6 @@ export function ElectronServerConfig({
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const setServerUrl = useSetServerURL();
|
const setServerUrl = useSetServerURL();
|
||||||
const currentUrl = useServerURL();
|
const currentUrl = useServerURL();
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const [syncServerConfig, setSyncServerConfig] =
|
const [syncServerConfig, setSyncServerConfig] =
|
||||||
useGlobalPref('syncServerConfig');
|
useGlobalPref('syncServerConfig');
|
||||||
@@ -54,6 +53,7 @@ export function ElectronServerConfig({
|
|||||||
const hasInternalServerConfig = syncServerConfig?.port;
|
const hasInternalServerConfig = syncServerConfig?.port;
|
||||||
|
|
||||||
const [startingSyncServer, setStartingSyncServer] = useState(false);
|
const [startingSyncServer, setStartingSyncServer] = useState(false);
|
||||||
|
const saveGlobalPrefs = useSaveGlobalPrefsMutation();
|
||||||
|
|
||||||
const onConfigureSyncServer = async () => {
|
const onConfigureSyncServer = async () => {
|
||||||
if (startingSyncServer) {
|
if (startingSyncServer) {
|
||||||
@@ -73,17 +73,13 @@ export function ElectronServerConfig({
|
|||||||
setConfigError(null);
|
setConfigError(null);
|
||||||
setStartingSyncServer(true);
|
setStartingSyncServer(true);
|
||||||
// Ensure config is saved before starting the server
|
// Ensure config is saved before starting the server
|
||||||
await dispatch(
|
await saveGlobalPrefs.mutateAsync({
|
||||||
saveGlobalPrefs({
|
syncServerConfig: {
|
||||||
prefs: {
|
...syncServerConfig,
|
||||||
syncServerConfig: {
|
port: electronServerPort,
|
||||||
...syncServerConfig,
|
autoStart: true,
|
||||||
port: electronServerPort,
|
},
|
||||||
autoStart: true,
|
});
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).unwrap();
|
|
||||||
|
|
||||||
await window.globalThis.Actual.stopSyncServer();
|
await window.globalThis.Actual.stopSyncServer();
|
||||||
await window.globalThis.Actual.startSyncServer();
|
await window.globalThis.Actual.startSyncServer();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { Text } from '@actual-app/components/text';
|
|||||||
import { theme } from '@actual-app/components/theme';
|
import { theme } from '@actual-app/components/theme';
|
||||||
import { View } from '@actual-app/components/view';
|
import { View } from '@actual-app/components/view';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { send } from 'loot-core/platform/client/connection';
|
import { send } from 'loot-core/platform/client/connection';
|
||||||
import { getCreateKeyError } from 'loot-core/shared/errors';
|
import { getCreateKeyError } from 'loot-core/shared/errors';
|
||||||
@@ -27,7 +28,7 @@ import {
|
|||||||
ModalHeader,
|
ModalHeader,
|
||||||
} from '@desktop-client/components/common/Modal';
|
} from '@desktop-client/components/common/Modal';
|
||||||
import type { Modal as ModalType } from '@desktop-client/modals/modalsSlice';
|
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';
|
import { useDispatch } from '@desktop-client/redux';
|
||||||
|
|
||||||
type CreateEncryptionKeyModalProps = Extract<
|
type CreateEncryptionKeyModalProps = Extract<
|
||||||
@@ -45,6 +46,7 @@ export function CreateEncryptionKeyModal({
|
|||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const { isNarrowWidth } = useResponsive();
|
const { isNarrowWidth } = useResponsive();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const isRecreating = recreate;
|
const isRecreating = recreate;
|
||||||
|
|
||||||
@@ -60,7 +62,9 @@ export function CreateEncryptionKeyModal({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void dispatch(loadGlobalPrefs());
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: prefQueries.listGlobal().queryKey,
|
||||||
|
});
|
||||||
void dispatch(loadAllFiles());
|
void dispatch(loadAllFiles());
|
||||||
void dispatch(sync());
|
void dispatch(sync());
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useEffectEvent } from 'react';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
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 { View } from '@actual-app/components/view';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
import { listen } from 'loot-core/platform/client/connection';
|
|
||||||
import { isElectron } from 'loot-core/shared/environment';
|
import { isElectron } from 'loot-core/shared/environment';
|
||||||
|
|
||||||
import { AuthSettings } from './AuthSettings';
|
import { AuthSettings } from './AuthSettings';
|
||||||
@@ -43,7 +42,6 @@ import { useFeatureFlag } from '@desktop-client/hooks/useFeatureFlag';
|
|||||||
import { useGlobalPref } from '@desktop-client/hooks/useGlobalPref';
|
import { useGlobalPref } from '@desktop-client/hooks/useGlobalPref';
|
||||||
import { useMetadataPref } from '@desktop-client/hooks/useMetadataPref';
|
import { useMetadataPref } from '@desktop-client/hooks/useMetadataPref';
|
||||||
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
|
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
|
||||||
import { loadPrefs } from '@desktop-client/prefs/prefsSlice';
|
|
||||||
import { useDispatch, useSelector } from '@desktop-client/redux';
|
import { useDispatch, useSelector } from '@desktop-client/redux';
|
||||||
|
|
||||||
function About() {
|
function About() {
|
||||||
@@ -183,20 +181,17 @@ export function Settings() {
|
|||||||
void dispatch(closeBudget());
|
void dispatch(closeBudget());
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const onSetDefaultCurrencyCodePref = useEffectEvent(
|
||||||
const unlisten = listen('prefs-updated', () => {
|
(isCurrencyExperimentalEnabled: boolean) => {
|
||||||
void dispatch(loadPrefs());
|
if (!isCurrencyExperimentalEnabled) {
|
||||||
});
|
setDefaultCurrencyCodePref('');
|
||||||
|
}
|
||||||
void dispatch(loadPrefs());
|
},
|
||||||
return () => unlisten();
|
);
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isCurrencyExperimentalEnabled) {
|
onSetDefaultCurrencyCodePref(isCurrencyExperimentalEnabled);
|
||||||
setDefaultCurrencyCodePref('');
|
}, [isCurrencyExperimentalEnabled]);
|
||||||
}
|
|
||||||
}, [isCurrencyExperimentalEnabled, setDefaultCurrencyCodePref]);
|
|
||||||
|
|
||||||
const { isNarrowWidth } = useResponsive();
|
const { isNarrowWidth } = useResponsive();
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
addNotification,
|
addNotification,
|
||||||
} from './notifications/notificationsSlice';
|
} from './notifications/notificationsSlice';
|
||||||
import { payeeQueries } from './payees';
|
import { payeeQueries } from './payees';
|
||||||
import { loadPrefs } from './prefs/prefsSlice';
|
import { prefQueries } from './prefs';
|
||||||
import type { AppStore } from './redux/store';
|
import type { AppStore } from './redux/store';
|
||||||
import * as syncEvents from './sync-events';
|
import * as syncEvents from './sync-events';
|
||||||
|
|
||||||
@@ -141,7 +141,9 @@ export function handleGlobalEvents(store: AppStore, queryClient: QueryClient) {
|
|||||||
const unlistenFinishLoad = listen('finish-load', () => {
|
const unlistenFinishLoad = listen('finish-load', () => {
|
||||||
store.dispatch(closeModal());
|
store.dispatch(closeModal());
|
||||||
store.dispatch(setAppState({ loadingText: null }));
|
store.dispatch(setAppState({ loadingText: null }));
|
||||||
void store.dispatch(loadPrefs());
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: prefQueries.lists(),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const unlistenStartImport = listen('start-import', () => {
|
const unlistenStartImport = listen('start-import', () => {
|
||||||
@@ -151,7 +153,9 @@ export function handleGlobalEvents(store: AppStore, queryClient: QueryClient) {
|
|||||||
const unlistenFinishImport = listen('finish-import', () => {
|
const unlistenFinishImport = listen('finish-import', () => {
|
||||||
store.dispatch(closeModal());
|
store.dispatch(closeModal());
|
||||||
store.dispatch(setAppState({ loadingText: null }));
|
store.dispatch(setAppState({ loadingText: null }));
|
||||||
void store.dispatch(loadPrefs());
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: prefQueries.lists(),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const unlistenShowBudgets = listen('show-budgets', () => {
|
const unlistenShowBudgets = listen('show-budgets', () => {
|
||||||
@@ -163,6 +167,12 @@ export function handleGlobalEvents(store: AppStore, queryClient: QueryClient) {
|
|||||||
void window.Actual.reload();
|
void window.Actual.reload();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const unlistenPrefsUpdated = listen('prefs-updated', () => {
|
||||||
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: prefQueries.lists(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unlistenServerError();
|
unlistenServerError();
|
||||||
unlistenOrphanedPayees();
|
unlistenOrphanedPayees();
|
||||||
@@ -176,5 +186,6 @@ export function handleGlobalEvents(store: AppStore, queryClient: QueryClient) {
|
|||||||
unlistenFinishImport();
|
unlistenFinishImport();
|
||||||
unlistenShowBudgets();
|
unlistenShowBudgets();
|
||||||
unlistenApiFetchRedirected();
|
unlistenApiFetchRedirected();
|
||||||
|
unlistenPrefsUpdated();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useCallback } from 'react';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import type { GlobalPrefs } from 'loot-core/types/prefs';
|
import type { GlobalPrefs } from 'loot-core/types/prefs';
|
||||||
|
|
||||||
import { saveGlobalPrefs } from '@desktop-client/prefs/prefsSlice';
|
import { prefQueries, useSaveGlobalPrefsMutation } from '@desktop-client/prefs';
|
||||||
import { useDispatch, useSelector } from '@desktop-client/redux';
|
|
||||||
|
|
||||||
type SetGlobalPrefAction<K extends keyof GlobalPrefs> = (
|
type SetGlobalPrefAction<K extends keyof GlobalPrefs> = (
|
||||||
value: GlobalPrefs[K],
|
value: GlobalPrefs[K],
|
||||||
@@ -13,23 +12,24 @@ export function useGlobalPref<K extends keyof GlobalPrefs>(
|
|||||||
prefName: K,
|
prefName: K,
|
||||||
onSaveGlobalPrefs?: () => void,
|
onSaveGlobalPrefs?: () => void,
|
||||||
): [GlobalPrefs[K], SetGlobalPrefAction<K>] {
|
): [GlobalPrefs[K], SetGlobalPrefAction<K>] {
|
||||||
const dispatch = useDispatch();
|
const { mutate: saveGlobalPrefs } = useSaveGlobalPrefsMutation();
|
||||||
const setGlobalPref = useCallback<SetGlobalPrefAction<K>>(
|
const saveGlobalPref: SetGlobalPrefAction<K> = value => {
|
||||||
value => {
|
saveGlobalPrefs(
|
||||||
void dispatch(
|
{
|
||||||
saveGlobalPrefs({
|
[prefName]: value,
|
||||||
prefs: {
|
},
|
||||||
[prefName]: value,
|
{
|
||||||
},
|
onSuccess: onSaveGlobalPrefs,
|
||||||
onSaveGlobalPrefs,
|
},
|
||||||
}),
|
);
|
||||||
);
|
};
|
||||||
},
|
|
||||||
[prefName, dispatch, onSaveGlobalPrefs],
|
|
||||||
);
|
|
||||||
const globalPref = useSelector(
|
|
||||||
state => state.prefs.global?.[prefName] as GlobalPrefs[K],
|
|
||||||
);
|
|
||||||
|
|
||||||
return [globalPref, setGlobalPref];
|
const { data: globalPref } = useQuery({
|
||||||
|
...prefQueries.listGlobal(),
|
||||||
|
select: prefs => prefs?.[prefName],
|
||||||
|
enabled: !!prefName,
|
||||||
|
notifyOnChangeProps: ['data'],
|
||||||
|
});
|
||||||
|
|
||||||
|
return [globalPref as GlobalPrefs[K], saveGlobalPref];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { useCallback } from 'react';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import type { MetadataPrefs } from 'loot-core/types/prefs';
|
import type { MetadataPrefs } from 'loot-core/types/prefs';
|
||||||
|
|
||||||
import { savePrefs } from '@desktop-client/prefs/prefsSlice';
|
import {
|
||||||
import { useDispatch, useSelector } from '@desktop-client/redux';
|
prefQueries,
|
||||||
|
useSaveMetadataPrefsMutation,
|
||||||
|
} from '@desktop-client/prefs';
|
||||||
|
|
||||||
type SetMetadataPrefAction<K extends keyof MetadataPrefs> = (
|
type SetMetadataPrefAction<K extends keyof MetadataPrefs> = (
|
||||||
value: MetadataPrefs[K],
|
value: MetadataPrefs[K],
|
||||||
@@ -12,14 +14,17 @@ type SetMetadataPrefAction<K extends keyof MetadataPrefs> = (
|
|||||||
export function useMetadataPref<K extends keyof MetadataPrefs>(
|
export function useMetadataPref<K extends keyof MetadataPrefs>(
|
||||||
prefName: K,
|
prefName: K,
|
||||||
): [MetadataPrefs[K], SetMetadataPrefAction<K>] {
|
): [MetadataPrefs[K], SetMetadataPrefAction<K>] {
|
||||||
const dispatch = useDispatch();
|
const { mutate: saveMetadataPrefs } = useSaveMetadataPrefsMutation();
|
||||||
const setLocalPref = useCallback<SetMetadataPrefAction<K>>(
|
const saveMetadataPref: SetMetadataPrefAction<K> = value => {
|
||||||
value => {
|
saveMetadataPrefs({ [prefName]: value });
|
||||||
void dispatch(savePrefs({ prefs: { [prefName]: value } }));
|
};
|
||||||
},
|
|
||||||
[prefName, dispatch],
|
|
||||||
);
|
|
||||||
const localPref = useSelector(state => state.prefs.local?.[prefName]);
|
|
||||||
|
|
||||||
return [localPref, setLocalPref];
|
const { data: metadataPref } = useQuery({
|
||||||
|
...prefQueries.listMetadata(),
|
||||||
|
select: prefs => prefs?.[prefName],
|
||||||
|
enabled: !!prefName,
|
||||||
|
notifyOnChangeProps: ['data'],
|
||||||
|
});
|
||||||
|
|
||||||
|
return [metadataPref as MetadataPrefs[K], saveMetadataPref];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useCallback } from 'react';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import type { SyncedPrefs } from 'loot-core/types/prefs';
|
import type { SyncedPrefs } from 'loot-core/types/prefs';
|
||||||
|
|
||||||
import { saveSyncedPrefs } from '@desktop-client/prefs/prefsSlice';
|
import { prefQueries, useSaveSyncedPrefsMutation } from '@desktop-client/prefs';
|
||||||
import { useDispatch, useSelector } from '@desktop-client/redux';
|
|
||||||
|
|
||||||
type SetSyncedPrefAction<K extends keyof SyncedPrefs> = (
|
type SetSyncedPrefAction<K extends keyof SyncedPrefs> = (
|
||||||
value: SyncedPrefs[K],
|
value: SyncedPrefs[K],
|
||||||
@@ -12,18 +11,17 @@ type SetSyncedPrefAction<K extends keyof SyncedPrefs> = (
|
|||||||
export function useSyncedPref<K extends keyof SyncedPrefs>(
|
export function useSyncedPref<K extends keyof SyncedPrefs>(
|
||||||
prefName: K,
|
prefName: K,
|
||||||
): [SyncedPrefs[K], SetSyncedPrefAction<K>] {
|
): [SyncedPrefs[K], SetSyncedPrefAction<K>] {
|
||||||
const dispatch = useDispatch();
|
const { mutate: saveSyncedPrefs } = useSaveSyncedPrefsMutation();
|
||||||
const setPref = useCallback<SetSyncedPrefAction<K>>(
|
const saveSyncedPref: SetSyncedPrefAction<K> = value => {
|
||||||
value => {
|
saveSyncedPrefs({ [prefName]: value });
|
||||||
void dispatch(
|
};
|
||||||
saveSyncedPrefs({
|
|
||||||
prefs: { [prefName]: value },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[prefName, dispatch],
|
|
||||||
);
|
|
||||||
const pref = useSelector(state => state.prefs.synced[prefName]);
|
|
||||||
|
|
||||||
return [pref, setPref];
|
const { data: syncedPref } = useQuery({
|
||||||
|
...prefQueries.listSynced(),
|
||||||
|
select: prefs => prefs?.[prefName],
|
||||||
|
enabled: !!prefName,
|
||||||
|
notifyOnChangeProps: ['data'],
|
||||||
|
});
|
||||||
|
|
||||||
|
return [syncedPref as SyncedPrefs[K], saveSyncedPref];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,15 @@
|
|||||||
import { useCallback } from 'react';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import type { SyncedPrefs } from 'loot-core/types/prefs';
|
import type { SyncedPrefs } from 'loot-core/types/prefs';
|
||||||
|
|
||||||
import { saveSyncedPrefs } from '@desktop-client/prefs/prefsSlice';
|
import { prefQueries, useSaveSyncedPrefsMutation } from '@desktop-client/prefs';
|
||||||
import { useDispatch, useSelector } from '@desktop-client/redux';
|
|
||||||
|
|
||||||
type SetSyncedPrefsAction = (value: Partial<SyncedPrefs>) => void;
|
type SetSyncedPrefsAction = (value: Partial<SyncedPrefs>) => void;
|
||||||
|
|
||||||
/** @deprecated: please use `useSyncedPref` (singular) */
|
/** @deprecated: please use `useSyncedPref` (singular) */
|
||||||
export function useSyncedPrefs(): [SyncedPrefs, SetSyncedPrefsAction] {
|
export function useSyncedPrefs(): [SyncedPrefs, SetSyncedPrefsAction] {
|
||||||
const dispatch = useDispatch();
|
const { mutate: saveSyncedPrefs } = useSaveSyncedPrefsMutation();
|
||||||
const setPrefs = useCallback<SetSyncedPrefsAction>(
|
const { data: syncedPrefs } = useQuery(prefQueries.listSynced());
|
||||||
newValue => {
|
|
||||||
void dispatch(saveSyncedPrefs({ prefs: newValue }));
|
|
||||||
},
|
|
||||||
[dispatch],
|
|
||||||
);
|
|
||||||
const prefs = useSelector(state => state.prefs.synced);
|
|
||||||
|
|
||||||
return [prefs, setPrefs];
|
return [syncedPrefs as SyncedPrefs, saveSyncedPrefs];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|||||||
|
|
||||||
import { send } from 'loot-core/platform/client/connection';
|
import { send } from 'loot-core/platform/client/connection';
|
||||||
import { q } from 'loot-core/shared/query';
|
import { q } from 'loot-core/shared/query';
|
||||||
|
import type { Theme } from 'loot-core/types/prefs';
|
||||||
|
|
||||||
import * as accountsSlice from './accounts/accountsSlice';
|
import * as accountsSlice from './accounts/accountsSlice';
|
||||||
import * as appSlice from './app/appSlice';
|
import * as appSlice from './app/appSlice';
|
||||||
@@ -22,6 +23,7 @@ import { App } from './components/App';
|
|||||||
import { ServerProvider } from './components/ServerContext';
|
import { ServerProvider } from './components/ServerContext';
|
||||||
import * as modalsSlice from './modals/modalsSlice';
|
import * as modalsSlice from './modals/modalsSlice';
|
||||||
import * as notificationsSlice from './notifications/notificationsSlice';
|
import * as notificationsSlice from './notifications/notificationsSlice';
|
||||||
|
import { prefQueries } from './prefs';
|
||||||
import * as prefsSlice from './prefs/prefsSlice';
|
import * as prefsSlice from './prefs/prefsSlice';
|
||||||
import { aqlQuery } from './queries/aqlQuery';
|
import { aqlQuery } from './queries/aqlQuery';
|
||||||
import { configureAppStore } from './redux/store';
|
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
|
// Expose this to the main process to menu items can access it
|
||||||
window.__actionsForMenu = {
|
window.__actionsForMenu = {
|
||||||
...boundActions,
|
...boundActions,
|
||||||
@@ -75,6 +87,7 @@ window.__actionsForMenu = {
|
|||||||
redo,
|
redo,
|
||||||
appFocused,
|
appFocused,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
|
setTheme,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Expose send for fun!
|
// Expose send for fun!
|
||||||
@@ -108,6 +121,7 @@ declare global {
|
|||||||
redo: typeof redo;
|
redo: typeof redo;
|
||||||
appFocused: typeof appFocused;
|
appFocused: typeof appFocused;
|
||||||
uploadFile: typeof uploadFile;
|
uploadFile: typeof uploadFile;
|
||||||
|
setTheme: typeof setTheme;
|
||||||
};
|
};
|
||||||
|
|
||||||
$send: typeof send;
|
$send: typeof send;
|
||||||
|
|||||||
2
packages/desktop-client/src/prefs/index.ts
Normal file
2
packages/desktop-client/src/prefs/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './queries';
|
||||||
|
export * from './mutations';
|
||||||
250
packages/desktop-client/src/prefs/mutations.ts
Normal file
250
packages/desktop-client/src/prefs/mutations.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -2,134 +2,21 @@ import { createSlice } from '@reduxjs/toolkit';
|
|||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
import { send } from 'loot-core/platform/client/connection';
|
import { send } from 'loot-core/platform/client/connection';
|
||||||
import { parseNumberFormat, setNumberFormat } from 'loot-core/shared/util';
|
import type { ServerPrefs } from 'loot-core/types/prefs';
|
||||||
import type {
|
|
||||||
GlobalPrefs,
|
|
||||||
MetadataPrefs,
|
|
||||||
ServerPrefs,
|
|
||||||
SyncedPrefs,
|
|
||||||
} from 'loot-core/types/prefs';
|
|
||||||
|
|
||||||
import { resetApp } from '@desktop-client/app/appSlice';
|
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 { createAppAsyncThunk } from '@desktop-client/redux';
|
||||||
import { getUserData } from '@desktop-client/users/usersSlice';
|
import { getUserData } from '@desktop-client/users/usersSlice';
|
||||||
|
|
||||||
const sliceName = 'prefs';
|
const sliceName = 'prefs';
|
||||||
|
|
||||||
type PrefsState = {
|
type PrefsState = {
|
||||||
local: MetadataPrefs;
|
|
||||||
global: GlobalPrefs;
|
|
||||||
synced: SyncedPrefs;
|
|
||||||
server: ServerPrefs;
|
server: ServerPrefs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState: PrefsState = {
|
const initialState: PrefsState = {
|
||||||
local: {},
|
|
||||||
global: {},
|
|
||||||
synced: {},
|
|
||||||
server: {},
|
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 = {
|
type SaveServerPrefsPayload = {
|
||||||
prefs: ServerPrefs;
|
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;
|
type MergeServerPrefsPayload = ServerPrefs;
|
||||||
|
|
||||||
const prefsSlice = createSlice({
|
const prefsSlice = createSlice({
|
||||||
name: sliceName,
|
name: sliceName,
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
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>) {
|
mergeServerPrefs(state, action: PayloadAction<MergeServerPrefsPayload>) {
|
||||||
state.server = { ...state.server, ...action.payload };
|
state.server = { ...state.server, ...action.payload };
|
||||||
},
|
},
|
||||||
@@ -183,7 +47,6 @@ const prefsSlice = createSlice({
|
|||||||
extraReducers: builder => {
|
extraReducers: builder => {
|
||||||
builder.addCase(resetApp, state => ({
|
builder.addCase(resetApp, state => ({
|
||||||
...initialState,
|
...initialState,
|
||||||
global: state.global || initialState.global,
|
|
||||||
server: state.server || initialState.server,
|
server: state.server || initialState.server,
|
||||||
}));
|
}));
|
||||||
builder.addCase(getUserData.fulfilled, (state, action) => {
|
builder.addCase(getUserData.fulfilled, (state, action) => {
|
||||||
@@ -211,18 +74,7 @@ export const { name, reducer, getInitialState } = prefsSlice;
|
|||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
...prefsSlice.actions,
|
...prefsSlice.actions,
|
||||||
loadPrefs,
|
|
||||||
savePrefs,
|
|
||||||
loadGlobalPrefs,
|
|
||||||
saveGlobalPrefs,
|
|
||||||
saveSyncedPrefs,
|
|
||||||
saveServerPrefs,
|
saveServerPrefs,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const {
|
export const { mergeServerPrefs } = actions;
|
||||||
mergeGlobalPrefs,
|
|
||||||
mergeLocalPrefs,
|
|
||||||
mergeServerPrefs,
|
|
||||||
mergeSyncedPrefs,
|
|
||||||
setPrefs,
|
|
||||||
} = actions;
|
|
||||||
|
|||||||
104
packages/desktop-client/src/prefs/queries.ts
Normal file
104
packages/desktop-client/src/prefs/queries.ts
Normal 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,
|
||||||
|
}),
|
||||||
|
};
|
||||||
@@ -14,7 +14,7 @@ import { pushModal } from './modals/modalsSlice';
|
|||||||
import { addNotification } from './notifications/notificationsSlice';
|
import { addNotification } from './notifications/notificationsSlice';
|
||||||
import type { Notification } from './notifications/notificationsSlice';
|
import type { Notification } from './notifications/notificationsSlice';
|
||||||
import { payeeQueries } from './payees';
|
import { payeeQueries } from './payees';
|
||||||
import { loadPrefs } from './prefs/prefsSlice';
|
import { prefQueries } from './prefs';
|
||||||
import type { AppStore } from './redux/store';
|
import type { AppStore } from './redux/store';
|
||||||
import { signOut } from './users/usersSlice';
|
import { signOut } from './users/usersSlice';
|
||||||
|
|
||||||
@@ -37,8 +37,8 @@ export function listenForSyncEvent(store: AppStore, queryClient: QueryClient) {
|
|||||||
|
|
||||||
let attemptedSyncRepair = false;
|
let attemptedSyncRepair = false;
|
||||||
|
|
||||||
const unlistenSuccess = listen('sync-event', event => {
|
const unlistenSuccess = listen('sync-event', async event => {
|
||||||
const prefs = store.getState().prefs.local;
|
const prefs = await queryClient.ensureQueryData(prefQueries.listMetadata());
|
||||||
if (!prefs || !prefs.id) {
|
if (!prefs || !prefs.id) {
|
||||||
// Do nothing if no budget is loaded
|
// Do nothing if no budget is loaded
|
||||||
return;
|
return;
|
||||||
@@ -62,7 +62,9 @@ export function listenForSyncEvent(store: AppStore, queryClient: QueryClient) {
|
|||||||
const tables = event.tables;
|
const tables = event.tables;
|
||||||
|
|
||||||
if (tables.includes('prefs')) {
|
if (tables.includes('prefs')) {
|
||||||
void store.dispatch(loadPrefs());
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: prefQueries.lists(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -219,8 +221,10 @@ export function listenForSyncEvent(store: AppStore, queryClient: QueryClient) {
|
|||||||
title: t('Register'),
|
title: t('Register'),
|
||||||
action: async () => {
|
action: async () => {
|
||||||
await store.dispatch(uploadBudget({}));
|
await store.dispatch(uploadBudget({}));
|
||||||
void store.dispatch(sync());
|
store.dispatch(sync());
|
||||||
void store.dispatch(loadPrefs());
|
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
|
// the server does not match the local one. This can mean a
|
||||||
// few things depending on the state, and we try to show an
|
// few things depending on the state, and we try to show an
|
||||||
// appropriate message and call to action to fix it.
|
// appropriate message and call to action to fix it.
|
||||||
const { cloudFileId } = store.getState().prefs.local;
|
const { cloudFileId } = await queryClient.ensureQueryData(
|
||||||
|
prefQueries.listMetadata(),
|
||||||
|
);
|
||||||
if (!cloudFileId) {
|
if (!cloudFileId) {
|
||||||
console.error(
|
console.error(
|
||||||
'Received file-has-reset or file-has-new-key error but no cloudFileId in prefs',
|
'Received file-has-reset or file-has-new-key error but no cloudFileId in prefs',
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
closeBudget,
|
closeBudget,
|
||||||
loadAllFiles,
|
loadAllFiles,
|
||||||
} from '@desktop-client/budgetfiles/budgetfilesSlice';
|
} from '@desktop-client/budgetfiles/budgetfilesSlice';
|
||||||
import { loadGlobalPrefs } from '@desktop-client/prefs/prefsSlice';
|
import { prefQueries } from '@desktop-client/prefs';
|
||||||
import { createAppAsyncThunk } from '@desktop-client/redux';
|
import { createAppAsyncThunk } from '@desktop-client/redux';
|
||||||
|
|
||||||
const sliceName = 'user';
|
const sliceName = 'user';
|
||||||
@@ -48,11 +48,13 @@ export const loggedIn = createAppAsyncThunk(
|
|||||||
|
|
||||||
export const signOut = createAppAsyncThunk(
|
export const signOut = createAppAsyncThunk(
|
||||||
`${sliceName}/signOut`,
|
`${sliceName}/signOut`,
|
||||||
async (_, { dispatch }) => {
|
async (_, { dispatch, extra: { queryClient } }) => {
|
||||||
await send('subscribe-sign-out');
|
await send('subscribe-sign-out');
|
||||||
|
|
||||||
void dispatch(getUserData());
|
void dispatch(getUserData());
|
||||||
void dispatch(loadGlobalPrefs());
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: prefQueries.listGlobal().queryKey,
|
||||||
|
});
|
||||||
void dispatch(closeBudget());
|
void dispatch(closeBudget());
|
||||||
// Handled in budgetSlice
|
// Handled in budgetSlice
|
||||||
// dispatch({ type: constants.SIGN_OUT });
|
// dispatch({ type: constants.SIGN_OUT });
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import type {
|
|||||||
import { copy, exists, mkdir, remove } from 'fs-extra';
|
import { copy, exists, mkdir, remove } from 'fs-extra';
|
||||||
import promiseRetry from 'promise-retry';
|
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 { getMenu } from './menu';
|
||||||
import {
|
import {
|
||||||
@@ -625,11 +625,10 @@ ipcMain.on('message', (_event, msg) => {
|
|||||||
serverProcess.postMessage(msg.args);
|
serverProcess.postMessage(msg.args);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('set-theme', (_event, theme: string) => {
|
ipcMain.on('set-theme', async (_event, theme: Theme) => {
|
||||||
const obj = { theme };
|
|
||||||
if (clientWin) {
|
if (clientWin) {
|
||||||
void clientWin.webContents.executeJavaScript(
|
await clientWin.webContents.executeJavaScript(
|
||||||
`window.__actionsForMenu && window.__actionsForMenu.saveGlobalPrefs({ prefs: ${JSON.stringify(obj)} })`,
|
`window.__actionsForMenu && window.__actionsForMenu.setTheme('${theme}')`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { contextBridge, ipcRenderer } from 'electron';
|
import { contextBridge, ipcRenderer } from 'electron';
|
||||||
import type { IpcRenderer } from 'electron';
|
import type { IpcRenderer } from 'electron';
|
||||||
|
|
||||||
|
import type { Theme } from 'loot-core/types/prefs';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
GetBootstrapDataPayload,
|
GetBootstrapDataPayload,
|
||||||
OpenFileDialogPayload,
|
OpenFileDialogPayload,
|
||||||
@@ -85,7 +87,7 @@ contextBridge.exposeInMainWorld('Actual', {
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
setTheme: (theme: string) => {
|
setTheme: async (theme: Theme) => {
|
||||||
ipcRenderer.send('set-theme', theme);
|
ipcRenderer.send('set-theme', theme);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type EventEmitter from 'events';
|
import type EventEmitter from 'events';
|
||||||
|
|
||||||
|
import type { Theme } from 'loot-core/types/prefs';
|
||||||
|
|
||||||
export type IpcClient = {
|
export type IpcClient = {
|
||||||
on: EventEmitter['on'];
|
on: EventEmitter['on'];
|
||||||
emit: (name: string, data: unknown) => void;
|
emit: (name: string, data: unknown) => void;
|
||||||
@@ -34,7 +36,7 @@ type Actual = {
|
|||||||
applyAppUpdate: () => Promise<void>;
|
applyAppUpdate: () => Promise<void>;
|
||||||
ipcConnect: (callback: (client: IpcClient) => void) => void;
|
ipcConnect: (callback: (client: IpcClient) => void) => void;
|
||||||
getServerSocket: () => Promise<Worker | null>;
|
getServerSocket: () => Promise<Worker | null>;
|
||||||
setTheme: (theme: string) => void;
|
setTheme: (theme: Theme) => Promise<void>;
|
||||||
logToTerminal: (...args: unknown[]) => void;
|
logToTerminal: (...args: unknown[]) => void;
|
||||||
onEventFromMain: (
|
onEventFromMain: (
|
||||||
event: string,
|
event: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user