diff --git a/packages/desktop-client/src/components/App.tsx b/packages/desktop-client/src/components/App.tsx
index fafac793317..44964abd946 100644
--- a/packages/desktop-client/src/components/App.tsx
+++ b/packages/desktop-client/src/components/App.tsx
@@ -16,10 +16,9 @@ import {
closeBudget,
loadBudget,
loadGlobalPrefs,
- setAppState,
signOut,
- sync,
} from 'loot-core/client/actions';
+import { setAppState, sync } from 'loot-core/client/app/appSlice';
import { SpreadsheetProvider } from 'loot-core/client/SpreadsheetProvider';
import * as Platform from 'loot-core/src/client/platform';
import {
diff --git a/packages/desktop-client/src/components/FatalError.tsx b/packages/desktop-client/src/components/FatalError.tsx
index 682ba97aa24..6ce18aecd46 100644
--- a/packages/desktop-client/src/components/FatalError.tsx
+++ b/packages/desktop-client/src/components/FatalError.tsx
@@ -217,7 +217,7 @@ export function FatalError({ error }: FatalErrorProps) {
)}
-
diff --git a/packages/desktop-client/src/components/FinancesApp.tsx b/packages/desktop-client/src/components/FinancesApp.tsx
index a1074fbf3e2..8941874c1d6 100644
--- a/packages/desktop-client/src/components/FinancesApp.tsx
+++ b/packages/desktop-client/src/components/FinancesApp.tsx
@@ -9,7 +9,8 @@ import {
useHref,
} from 'react-router-dom';
-import { addNotification, sync } from 'loot-core/client/actions';
+import { addNotification } from 'loot-core/client/actions';
+import { sync } from 'loot-core/client/app/appSlice';
import * as undo from 'loot-core/src/platform/client/undo';
import { ProtectedRoute } from '../auth/ProtectedRoute';
diff --git a/packages/desktop-client/src/components/HelpMenu.tsx b/packages/desktop-client/src/components/HelpMenu.tsx
index 11c292add30..40a4208ed41 100644
--- a/packages/desktop-client/src/components/HelpMenu.tsx
+++ b/packages/desktop-client/src/components/HelpMenu.tsx
@@ -5,7 +5,6 @@ import { useLocation } from 'react-router-dom';
import { useToggle } from 'usehooks-ts';
-import { openDocsForCurrentPage } from 'loot-core/client/actions';
import { pushModal } from 'loot-core/client/actions/modals';
import { useFeatureFlag } from '../hooks/useFeatureFlag';
@@ -17,6 +16,30 @@ import { Menu } from './common/Menu';
import { Popover } from './common/Popover';
import { SpaceBetween } from './common/SpaceBetween';
+const getPageDocs = (page: string) => {
+ switch (page) {
+ case '/budget':
+ return 'https://actualbudget.org/docs/getting-started/envelope-budgeting';
+ case '/reports':
+ return 'https://actualbudget.org/docs/reports/';
+ case '/schedules':
+ return 'https://actualbudget.org/docs/schedules';
+ case '/payees':
+ return 'https://actualbudget.org/docs/transactions/payees';
+ case '/rules':
+ return 'https://actualbudget.org/docs/budgeting/rules';
+ case '/settings':
+ return 'https://actualbudget.org/docs/settings';
+ default:
+ // All pages under /accounts, plus any missing pages
+ return 'https://actualbudget.org/docs';
+ }
+};
+
+function openDocsForCurrentPage() {
+ window.Actual.openURLInBrowser(getPageDocs(window.location.pathname));
+}
+
type HelpMenuItem = 'docs' | 'keyboard-shortcuts' | 'goal-templates';
type HelpButtonProps = {
@@ -58,7 +81,7 @@ export const HelpMenu = () => {
const handleItemSelect = (item: HelpMenuItem) => {
switch (item) {
case 'docs':
- dispatch(openDocsForCurrentPage());
+ openDocsForCurrentPage();
break;
case 'keyboard-shortcuts':
dispatch(pushModal('keyboard-shortcuts'));
diff --git a/packages/desktop-client/src/components/Titlebar.tsx b/packages/desktop-client/src/components/Titlebar.tsx
index 03b231f4f30..073496a0dfa 100644
--- a/packages/desktop-client/src/components/Titlebar.tsx
+++ b/packages/desktop-client/src/components/Titlebar.tsx
@@ -5,7 +5,7 @@ import { Routes, Route, useLocation } from 'react-router-dom';
import { css } from '@emotion/css';
-import { sync } from 'loot-core/client/actions';
+import { sync } from 'loot-core/client/app/appSlice';
import * as Platform from 'loot-core/src/client/platform';
import * as queries from 'loot-core/src/client/queries';
import { listen } from 'loot-core/src/platform/client/fetch';
diff --git a/packages/desktop-client/src/components/UpdateNotification.tsx b/packages/desktop-client/src/components/UpdateNotification.tsx
index 469b76b98b8..6d5fbba55d9 100644
--- a/packages/desktop-client/src/components/UpdateNotification.tsx
+++ b/packages/desktop-client/src/components/UpdateNotification.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
-import { setAppState, updateApp } from 'loot-core/client/actions';
+import { setAppState, updateApp } from 'loot-core/client/app/appSlice';
import { SvgClose } from '../icons/v1';
import { useSelector, useDispatch } from '../redux';
@@ -69,7 +69,7 @@ export function UpdateNotification() {
textDecoration: 'underline',
}}
onClick={() =>
- window.Actual?.openURLInBrowser(
+ window.Actual.openURLInBrowser(
'https://actualbudget.org/docs/releases',
)
}
diff --git a/packages/desktop-client/src/components/accounts/Account.tsx b/packages/desktop-client/src/components/accounts/Account.tsx
index 08b6b397f12..643a11286bb 100644
--- a/packages/desktop-client/src/components/accounts/Account.tsx
+++ b/packages/desktop-client/src/components/accounts/Account.tsx
@@ -19,8 +19,8 @@ import {
openAccountCloseModal,
pushModal,
replaceModal,
- syncAndDownload,
} from 'loot-core/client/actions';
+import { syncAndDownload } from 'loot-core/client/app/appSlice';
import {
createPayee,
initiallyLoadPayees,
@@ -624,7 +624,7 @@ class AccountInternal extends PureComponent<
const account = this.props.accounts.find(acct => acct.id === accountId);
await this.props.dispatch(
- syncAndDownload(account ? account.id : undefined),
+ syncAndDownload({ accountId: account ? account.id : undefined }),
);
};
@@ -633,7 +633,7 @@ class AccountInternal extends PureComponent<
const account = this.props.accounts.find(acct => acct.id === accountId);
if (account) {
- const res = await window.Actual?.openFileDialog({
+ const res = await window.Actual.openFileDialog({
filters: [
{
name: t('Financial Files'),
@@ -668,7 +668,7 @@ class AccountInternal extends PureComponent<
accountName && accountName.replace(/[()]/g, '').replace(/\s+/g, '-');
const filename = `${normalizedName || 'transactions'}.csv`;
- window.Actual?.saveFile(
+ window.Actual.saveFile(
exportedTransactions,
filename,
t('Export Transactions'),
diff --git a/packages/desktop-client/src/components/manager/ConfigServer.tsx b/packages/desktop-client/src/components/manager/ConfigServer.tsx
index a8020f083dc..107676551bb 100644
--- a/packages/desktop-client/src/components/manager/ConfigServer.tsx
+++ b/packages/desktop-client/src/components/manager/ConfigServer.tsx
@@ -88,7 +88,7 @@ export function ConfigServer() {
}
async function onSelectSelfSignedCertificate() {
- const selfSignedCertificateLocation = await window.Actual?.openFileDialog({
+ const selfSignedCertificateLocation = await window.Actual.openFileDialog({
properties: ['openFile'],
filters: [
{
diff --git a/packages/desktop-client/src/components/manager/ManagementApp.tsx b/packages/desktop-client/src/components/manager/ManagementApp.tsx
index 872bf98363a..12d2c79ffe0 100644
--- a/packages/desktop-client/src/components/manager/ManagementApp.tsx
+++ b/packages/desktop-client/src/components/manager/ManagementApp.tsx
@@ -1,7 +1,8 @@
import React, { useEffect } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
-import { loggedIn, setAppState } from 'loot-core/client/actions';
+import { loggedIn } from 'loot-core/client/actions';
+import { setAppState } from 'loot-core/client/app/appSlice';
import { ProtectedRoute } from '../../auth/ProtectedRoute';
import { Permissions } from '../../auth/types';
@@ -51,7 +52,7 @@ function Version() {
},
}}
>
- {`App: v${window.Actual?.ACTUAL_VERSION} | Server: ${version}`}
+ {`App: v${window.Actual.ACTUAL_VERSION} | Server: ${version}`}
);
}
diff --git a/packages/desktop-client/src/components/mobile/accounts/AccountTransactions.tsx b/packages/desktop-client/src/components/mobile/accounts/AccountTransactions.tsx
index afd617fc4b3..87e1051761d 100644
--- a/packages/desktop-client/src/components/mobile/accounts/AccountTransactions.tsx
+++ b/packages/desktop-client/src/components/mobile/accounts/AccountTransactions.tsx
@@ -10,8 +10,8 @@ import {
collapseModals,
openAccountCloseModal,
pushModal,
- syncAndDownload,
} from 'loot-core/client/actions';
+import { syncAndDownload } from 'loot-core/client/app/appSlice';
import {
accountSchedulesQuery,
SchedulesProvider,
@@ -258,7 +258,7 @@ function TransactionListWithPreviews({
const onRefresh = useCallback(() => {
if (accountId) {
- dispatch(syncAndDownload(accountId));
+ dispatch(syncAndDownload({ accountId }));
}
}, [accountId, dispatch]);
diff --git a/packages/desktop-client/src/components/mobile/accounts/Accounts.tsx b/packages/desktop-client/src/components/mobile/accounts/Accounts.tsx
index 16cb2f958dd..2a5ff6cede8 100644
--- a/packages/desktop-client/src/components/mobile/accounts/Accounts.tsx
+++ b/packages/desktop-client/src/components/mobile/accounts/Accounts.tsx
@@ -3,7 +3,8 @@ import { useTranslation } from 'react-i18next';
import { css } from '@emotion/css';
-import { replaceModal, syncAndDownload } from 'loot-core/src/client/actions';
+import { syncAndDownload } from 'loot-core/client/app/appSlice';
+import { replaceModal } from 'loot-core/src/client/actions';
import * as queries from 'loot-core/src/client/queries';
import { type AccountEntity } from 'loot-core/types/models';
@@ -323,7 +324,7 @@ export function Accounts() {
}, [dispatch]);
const onSync = useCallback(async () => {
- dispatch(syncAndDownload());
+ dispatch(syncAndDownload({}));
}, [dispatch]);
return (
diff --git a/packages/desktop-client/src/components/mobile/budget/index.tsx b/packages/desktop-client/src/components/mobile/budget/index.tsx
index 2e38a0da76e..86d96330f66 100644
--- a/packages/desktop-client/src/components/mobile/budget/index.tsx
+++ b/packages/desktop-client/src/components/mobile/budget/index.tsx
@@ -1,7 +1,8 @@
// @ts-strict-ignore
import React, { useCallback, useEffect, useState } from 'react';
-import { collapseModals, pushModal, sync } from 'loot-core/client/actions';
+import { collapseModals, pushModal } from 'loot-core/client/actions';
+import { sync } from 'loot-core/client/app/appSlice';
import {
applyBudgetAction,
createCategory,
diff --git a/packages/desktop-client/src/components/modals/CreateEncryptionKeyModal.tsx b/packages/desktop-client/src/components/modals/CreateEncryptionKeyModal.tsx
index 1e9c3bbcba2..f13fdd8da87 100644
--- a/packages/desktop-client/src/components/modals/CreateEncryptionKeyModal.tsx
+++ b/packages/desktop-client/src/components/modals/CreateEncryptionKeyModal.tsx
@@ -5,7 +5,8 @@ import { useTranslation, Trans } from 'react-i18next';
import { css } from '@emotion/css';
-import { loadAllFiles, loadGlobalPrefs, sync } from 'loot-core/client/actions';
+import { loadAllFiles, loadGlobalPrefs } from 'loot-core/client/actions';
+import { sync } from 'loot-core/client/app/appSlice';
import { send } from 'loot-core/src/platform/client/fetch';
import { getCreateKeyError } from 'loot-core/src/shared/errors';
diff --git a/packages/desktop-client/src/components/modals/ImportTransactionsModal/ImportTransactionsModal.jsx b/packages/desktop-client/src/components/modals/ImportTransactionsModal/ImportTransactionsModal.jsx
index dceb8312640..1abef1d39d8 100644
--- a/packages/desktop-client/src/components/modals/ImportTransactionsModal/ImportTransactionsModal.jsx
+++ b/packages/desktop-client/src/components/modals/ImportTransactionsModal/ImportTransactionsModal.jsx
@@ -471,7 +471,7 @@ export function ImportTransactionsModal({ options }) {
}
async function onNewFile() {
- const res = await window.Actual?.openFileDialog({
+ const res = await window.Actual.openFileDialog({
filters: [
{
name: 'Financial Files',
diff --git a/packages/desktop-client/src/components/modals/manager/ConfirmChangeDocumentDir.tsx b/packages/desktop-client/src/components/modals/manager/ConfirmChangeDocumentDir.tsx
index adf3e5dfe35..c3c303dc039 100644
--- a/packages/desktop-client/src/components/modals/manager/ConfirmChangeDocumentDir.tsx
+++ b/packages/desktop-client/src/components/modals/manager/ConfirmChangeDocumentDir.tsx
@@ -51,7 +51,7 @@ export function ConfirmChangeDocumentDirModal({
const dispatch = useDispatch();
const restartElectronServer = useCallback(() => {
- globalThis.window.Actual?.restartElectronServer();
+ globalThis.window.Actual.restartElectronServer();
}, []);
const [_documentDir, setDocumentDirPref] = useGlobalPref(
@@ -64,7 +64,7 @@ export function ConfirmChangeDocumentDirModal({
setLoading(true);
try {
if (moveFiles) {
- await globalThis.window.Actual?.moveBudgetDirectory(
+ await globalThis.window.Actual.moveBudgetDirectory(
currentBudgetDirectory,
newDirectory,
);
diff --git a/packages/desktop-client/src/components/modals/manager/FilesSettingsModal.tsx b/packages/desktop-client/src/components/modals/manager/FilesSettingsModal.tsx
index ee3c1107def..d6275e817fd 100644
--- a/packages/desktop-client/src/components/modals/manager/FilesSettingsModal.tsx
+++ b/packages/desktop-client/src/components/modals/manager/FilesSettingsModal.tsx
@@ -19,7 +19,7 @@ function FileLocationSettings() {
const dispatch = useDispatch();
async function onChooseDocumentDir() {
- const chosenDirectory = await window.Actual?.openFileDialog({
+ const chosenDirectory = await window.Actual.openFileDialog({
properties: ['openDirectory'],
});
diff --git a/packages/desktop-client/src/components/modals/manager/ImportActualModal.tsx b/packages/desktop-client/src/components/modals/manager/ImportActualModal.tsx
index 995154b13e7..87421f22886 100644
--- a/packages/desktop-client/src/components/modals/manager/ImportActualModal.tsx
+++ b/packages/desktop-client/src/components/modals/manager/ImportActualModal.tsx
@@ -38,7 +38,7 @@ export function ImportActualModal() {
const [importing, setImporting] = useState(false);
async function onImport() {
- const res = await window.Actual?.openFileDialog({
+ const res = await window.Actual.openFileDialog({
properties: ['openFile'],
filters: [{ name: 'actual', extensions: ['zip', 'blob'] }],
});
diff --git a/packages/desktop-client/src/components/modals/manager/ImportYNAB4Modal.tsx b/packages/desktop-client/src/components/modals/manager/ImportYNAB4Modal.tsx
index 49a80721bf9..65020cb063e 100644
--- a/packages/desktop-client/src/components/modals/manager/ImportYNAB4Modal.tsx
+++ b/packages/desktop-client/src/components/modals/manager/ImportYNAB4Modal.tsx
@@ -30,7 +30,7 @@ export function ImportYNAB4Modal() {
const [importing, setImporting] = useState(false);
async function onImport() {
- const res = await window.Actual?.openFileDialog({
+ const res = await window.Actual.openFileDialog({
properties: ['openFile'],
filters: [{ name: 'ynab', extensions: ['zip'] }],
});
diff --git a/packages/desktop-client/src/components/modals/manager/ImportYNAB5Modal.tsx b/packages/desktop-client/src/components/modals/manager/ImportYNAB5Modal.tsx
index 8945278e80a..3b8e4752e31 100644
--- a/packages/desktop-client/src/components/modals/manager/ImportYNAB5Modal.tsx
+++ b/packages/desktop-client/src/components/modals/manager/ImportYNAB5Modal.tsx
@@ -33,7 +33,7 @@ export function ImportYNAB5Modal() {
const [importing, setImporting] = useState(false);
async function onImport() {
- const res = await window.Actual?.openFileDialog({
+ const res = await window.Actual.openFileDialog({
properties: ['openFile'],
filters: [{ name: 'ynab', extensions: ['json'] }],
});
diff --git a/packages/desktop-client/src/components/reports/Overview.tsx b/packages/desktop-client/src/components/reports/Overview.tsx
index 36023b89b6a..f3e467c7473 100644
--- a/packages/desktop-client/src/components/reports/Overview.tsx
+++ b/packages/desktop-client/src/components/reports/Overview.tsx
@@ -211,14 +211,14 @@ export function Overview() {
}),
} satisfies ExportImportDashboard;
- window.Actual?.saveFile(
+ window.Actual.saveFile(
JSON.stringify(data, null, 2),
'dashboard.json',
'Export Dashboard',
);
};
const onImport = async () => {
- const openFileDialog = window.Actual?.openFileDialog;
+ const openFileDialog = window.Actual.openFileDialog;
if (!openFileDialog) {
dispatch(
diff --git a/packages/desktop-client/src/components/settings/Export.tsx b/packages/desktop-client/src/components/settings/Export.tsx
index 393752a7b2d..2f2a33dbb11 100644
--- a/packages/desktop-client/src/components/settings/Export.tsx
+++ b/packages/desktop-client/src/components/settings/Export.tsx
@@ -33,7 +33,7 @@ export function ExportBudget() {
return;
}
- window.Actual?.saveFile(
+ window.Actual.saveFile(
response.data,
`${format(new Date(), 'yyyy-MM-dd')}-${budgetName}.zip`,
t('Export budget'),
diff --git a/packages/desktop-client/src/components/settings/Reset.tsx b/packages/desktop-client/src/components/settings/Reset.tsx
index a9ef6e293b9..0179e400096 100644
--- a/packages/desktop-client/src/components/settings/Reset.tsx
+++ b/packages/desktop-client/src/components/settings/Reset.tsx
@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { Trans } from 'react-i18next';
-import { resetSync } from 'loot-core/client/actions';
+import { resetSync } from 'loot-core/client/app/appSlice';
import { send } from 'loot-core/src/platform/client/fetch';
import { useMetadataPref } from '../../hooks/useMetadataPref';
diff --git a/packages/desktop-client/src/global-events.ts b/packages/desktop-client/src/global-events.ts
index 3bd98bd6c4b..ed8d7b3311d 100644
--- a/packages/desktop-client/src/global-events.ts
+++ b/packages/desktop-client/src/global-events.ts
@@ -6,10 +6,9 @@ import {
closeModal,
loadPrefs,
pushModal,
- reloadApp,
replaceModal,
- setAppState,
} from 'loot-core/client/actions';
+import { setAppState } from 'loot-core/client/app/appSlice';
import {
getAccounts,
getCategories,
@@ -163,6 +162,6 @@ export function handleGlobalEvents(store: AppStore) {
});
listen('api-fetch-redirected', () => {
- store.dispatch(reloadApp());
+ window.Actual.reload();
});
}
diff --git a/packages/desktop-client/src/gocardless.ts b/packages/desktop-client/src/gocardless.ts
index dc4d48b6332..fc92c403f36 100644
--- a/packages/desktop-client/src/gocardless.ts
+++ b/packages/desktop-client/src/gocardless.ts
@@ -25,7 +25,7 @@ function _authorize(
if ('error' in resp) return resp;
const { link, requisitionId } = resp;
- window.Actual?.openURLInBrowser(link);
+ window.Actual.openURLInBrowser(link);
return send('gocardless-poll-web-token', {
upgradingAccountId,
diff --git a/packages/desktop-client/src/index.tsx b/packages/desktop-client/src/index.tsx
index 9ed73b8f0bf..0ebe143d9cb 100644
--- a/packages/desktop-client/src/index.tsx
+++ b/packages/desktop-client/src/index.tsx
@@ -15,6 +15,7 @@ import { createRoot } from 'react-dom/client';
import * as accountsSlice from 'loot-core/src/client/accounts/accountsSlice';
import * as actions from 'loot-core/src/client/actions';
+import * as appSlice from 'loot-core/src/client/app/appSlice';
import * as queriesSlice from 'loot-core/src/client/queries/queriesSlice';
import { runQuery } from 'loot-core/src/client/query-helpers';
import { store } from 'loot-core/src/client/store';
@@ -35,6 +36,7 @@ const boundActions = bindActionCreators(
{
...actions,
...accountsSlice.actions,
+ ...appSlice.actions,
...queriesSlice.actions,
},
store.dispatch,
@@ -43,19 +45,15 @@ const boundActions = bindActionCreators(
// Listen for global events from the server or main process
handleGlobalEvents(store);
-declare global {
- // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
- interface Window {
- __actionsForMenu: typeof boundActions & {
- undo: typeof undo;
- redo: typeof redo;
- inputFocused: typeof inputFocused;
- };
+async function appFocused() {
+ await send('app-focused');
+}
- $send: typeof send;
- $query: typeof runQuery;
- $q: typeof q;
- }
+async function uploadFile(filename: string, contents: ArrayBuffer) {
+ send('upload-file-web', {
+ filename,
+ contents,
+ });
}
function inputFocused() {
@@ -67,7 +65,14 @@ function inputFocused() {
}
// Expose this to the main process to menu items can access it
-window.__actionsForMenu = { ...boundActions, undo, redo, inputFocused };
+window.__actionsForMenu = {
+ ...boundActions,
+ undo,
+ redo,
+ appFocused,
+ inputFocused,
+ uploadFile,
+};
// Expose send for fun!
window.$send = send;
@@ -85,3 +90,20 @@ root.render(
,
);
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
+ interface Window {
+ __actionsForMenu: typeof boundActions & {
+ undo: typeof undo;
+ redo: typeof redo;
+ appFocused: typeof appFocused;
+ inputFocused: typeof inputFocused;
+ uploadFile: typeof uploadFile;
+ };
+
+ $send: typeof send;
+ $query: typeof runQuery;
+ $q: typeof q;
+ }
+}
diff --git a/packages/desktop-client/src/util/versions.ts b/packages/desktop-client/src/util/versions.ts
index 09427e15f2c..291d236403f 100644
--- a/packages/desktop-client/src/util/versions.ts
+++ b/packages/desktop-client/src/util/versions.ts
@@ -30,7 +30,7 @@ export async function getLatestVersion(): Promise {
const json = await response.json();
const tags = json
.map(t => t.name)
- .concat([`v${window.Actual?.ACTUAL_VERSION}`]);
+ .concat([`v${window.Actual.ACTUAL_VERSION}`]);
tags.sort(cmpSemanticVersion);
return tags[tags.length - 1];
@@ -41,7 +41,7 @@ export async function getLatestVersion(): Promise {
}
export async function getIsOutdated(latestVersion: string): Promise {
- const clientVersion = window.Actual?.ACTUAL_VERSION;
+ const clientVersion = window.Actual.ACTUAL_VERSION;
if (latestVersion === 'unknown') {
return Promise.resolve(false);
}
diff --git a/packages/desktop-electron/index.ts b/packages/desktop-electron/index.ts
index 3a910e05573..885d6f71dfc 100644
--- a/packages/desktop-electron/index.ts
+++ b/packages/desktop-electron/index.ts
@@ -229,7 +229,7 @@ async function createWindow() {
const url = clientWin.webContents.getURL();
if (url.includes('app://') || url.includes('localhost:')) {
clientWin.webContents.executeJavaScript(
- 'window.__actionsForMenu.focused()',
+ 'window.__actionsForMenu.appFocused()',
);
}
}
@@ -435,7 +435,7 @@ ipcMain.handle(
export type SaveFileDialogPayload = {
title: SaveDialogOptions['title'];
defaultPath?: SaveDialogOptions['defaultPath'];
- fileContents: string | NodeJS.ArrayBufferView;
+ fileContents: string | Buffer;
};
ipcMain.handle(
@@ -448,7 +448,11 @@ ipcMain.handle(
return new Promise((resolve, reject) => {
if (fileLocation) {
- fs.writeFile(fileLocation.filePath, fileContents, error => {
+ const contents =
+ typeof fileContents === 'string'
+ ? fileContents
+ : new Uint8Array(fileContents.buffer);
+ fs.writeFile(fileLocation.filePath, contents, error => {
return reject(error);
});
}
diff --git a/packages/desktop-electron/preload.ts b/packages/desktop-electron/preload.ts
index 85b282dcb12..63f491133da 100644
--- a/packages/desktop-electron/preload.ts
+++ b/packages/desktop-electron/preload.ts
@@ -71,7 +71,7 @@ contextBridge.exposeInMainWorld('Actual', {
isUpdateReadyForDownload: () => false,
waitForUpdateReadyForDownload: () => new Promise(() => {}),
- getServerSocket: () => {
+ getServerSocket: async () => {
return null;
},
@@ -89,4 +89,12 @@ contextBridge.exposeInMainWorld('Actual', {
newDirectory,
);
},
-});
+
+ reload: async () => {
+ throw new Error('Reload not implemented in electron app');
+ },
+
+ applyAppUpdate: async () => {
+ throw new Error('applyAppUpdate not implemented in electron app');
+ },
+} satisfies typeof global.Actual);
diff --git a/packages/loot-core/src/client/actions/app.ts b/packages/loot-core/src/client/actions/app.ts
deleted file mode 100644
index bc199564039..00000000000
--- a/packages/loot-core/src/client/actions/app.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-// @ts-strict-ignore
-import { send } from '../../platform/client/fetch';
-import * as constants from '../constants';
-import type { AppState, SetAppStateAction } from '../state-types/app';
-import { type AppDispatch } from '../store';
-
-export function setAppState(state: Partial): SetAppStateAction {
- return {
- type: constants.SET_APP_STATE,
- state,
- };
-}
-
-export function updateApp() {
- return async (dispatch: AppDispatch) => {
- await global.Actual.applyAppUpdate();
- dispatch(setAppState({ updateInfo: null }));
- };
-}
-
-// This is only used in the fake web version where everything runs in
-// the browser. It's a way to send a file to the backend to be
-// imported into the virtual filesystem.
-export function uploadFile(filename: string, contents: ArrayBuffer) {
- return () => {
- return send('upload-file-web', { filename, contents });
- };
-}
-
-export function focused() {
- return () => {
- return send('app-focused');
- };
-}
-
-export function reloadApp() {
- return () => {
- global.Actual.reload();
- };
-}
-
-const getPageDocs = (page: string) => {
- switch (page) {
- case '/budget':
- return 'https://actualbudget.org/docs/getting-started/envelope-budgeting';
- case '/reports':
- return 'https://actualbudget.org/docs/reports/';
- case '/schedules':
- return 'https://actualbudget.org/docs/schedules';
- case '/payees':
- return 'https://actualbudget.org/docs/transactions/payees';
- case '/rules':
- return 'https://actualbudget.org/docs/budgeting/rules';
- case '/settings':
- return 'https://actualbudget.org/docs/settings';
- default:
- // All pages under /accounts, plus any missing pages
- return 'https://actualbudget.org/docs';
- }
-};
-
-export function openDocsForCurrentPage() {
- return () => {
- global.Actual.openURLInBrowser(getPageDocs(window.location.pathname));
- };
-}
diff --git a/packages/loot-core/src/client/actions/budgets.ts b/packages/loot-core/src/client/actions/budgets.ts
index cac82a0f76d..f95bf260e04 100644
--- a/packages/loot-core/src/client/actions/budgets.ts
+++ b/packages/loot-core/src/client/actions/budgets.ts
@@ -4,10 +4,10 @@ import { t } from 'i18next';
import { send } from '../../platform/client/fetch';
import { getDownloadError, getSyncError } from '../../shared/errors';
import type { Handlers } from '../../types/handlers';
+import { setAppState } from '../app/appSlice';
import * as constants from '../constants';
import { type AppDispatch, type GetRootState } from '../store';
-import { setAppState } from './app';
import { closeModal, pushModal } from './modals';
import { loadPrefs, loadGlobalPrefs } from './prefs';
diff --git a/packages/loot-core/src/client/actions/index.ts b/packages/loot-core/src/client/actions/index.ts
index 51ebdfd4ea1..9d758dfb9d8 100644
--- a/packages/loot-core/src/client/actions/index.ts
+++ b/packages/loot-core/src/client/actions/index.ts
@@ -2,7 +2,5 @@ export * from './modals';
export * from './notifications';
export * from './prefs';
export * from './budgets';
-export * from './app';
export * from './backups';
-export * from './sync';
export * from './user';
diff --git a/packages/loot-core/src/client/actions/sync.ts b/packages/loot-core/src/client/app/appSlice.ts
similarity index 50%
rename from packages/loot-core/src/client/actions/sync.ts
rename to packages/loot-core/src/client/app/appSlice.ts
index f421b98575b..819027feb0f 100644
--- a/packages/loot-core/src/client/actions/sync.ts
+++ b/packages/loot-core/src/client/app/appSlice.ts
@@ -1,13 +1,43 @@
+import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
+
import { send } from '../../platform/client/fetch';
import { getUploadError } from '../../shared/errors';
+import { type AccountEntity } from '../../types/models';
import { syncAccounts } from '../accounts/accountsSlice';
-import { type AppDispatch, type GetRootState } from '../store';
+import { loadPrefs, pushModal } from '../actions';
+import { createAppAsyncThunk } from '../redux';
+
+const sliceName = 'app';
+
+type AppState = {
+ loadingText: string | null;
+ updateInfo: {
+ version: string;
+ releaseDate: string;
+ releaseNotes: string;
+ } | null;
+ showUpdateNotification: boolean;
+ managerHasInitialized: boolean;
+};
-import { pushModal } from './modals';
-import { loadPrefs } from './prefs';
+const initialState: AppState = {
+ loadingText: null,
+ updateInfo: null,
+ showUpdateNotification: true,
+ managerHasInitialized: false,
+};
-export function resetSync() {
- return async (dispatch: AppDispatch) => {
+export const updateApp = createAppAsyncThunk(
+ `${sliceName}/updateApp`,
+ async (_, { dispatch }) => {
+ await global.Actual.applyAppUpdate();
+ dispatch(setAppState({ updateInfo: null }));
+ },
+);
+
+export const resetSync = createAppAsyncThunk(
+ `${sliceName}/resetSync`,
+ async (_, { dispatch }) => {
const { error } = await send('sync-reset');
if (error) {
@@ -32,11 +62,12 @@ export function resetSync() {
} else {
await dispatch(sync());
}
- };
-}
+ },
+);
-export function sync() {
- return async (dispatch: AppDispatch, getState: GetRootState) => {
+export const sync = createAppAsyncThunk(
+ `${sliceName}/sync`,
+ async (_, { dispatch, getState }) => {
const prefs = getState().prefs.local;
if (prefs && prefs.id) {
const result = await send('sync');
@@ -49,17 +80,22 @@ export function sync() {
}
return {};
- };
-}
+ },
+);
+
+type SyncAndDownloadPayload = {
+ accountId?: AccountEntity['id'] | string;
+};
-export function syncAndDownload(accountId?: string) {
- return async (dispatch: AppDispatch) => {
+export const syncAndDownload = createAppAsyncThunk(
+ `${sliceName}/syncAndDownload`,
+ async ({ accountId }: SyncAndDownloadPayload, { dispatch }) => {
// It is *critical* that we sync first because of transaction
// reconciliation. We want to get all transactions that other
// clients have already made, so that imported transactions can be
// reconciled against them. Otherwise, two clients will each add
// new transactions from the bank and create duplicate ones.
- const syncState = await dispatch(sync());
+ const syncState = await dispatch(sync()).unwrap();
if (syncState.error) {
return { error: syncState.error };
}
@@ -68,7 +104,7 @@ export function syncAndDownload(accountId?: string) {
if (hasDownloaded) {
// Sync again afterwards if new transactions were created
- const syncState = await dispatch(sync());
+ const syncState = await dispatch(sync()).unwrap();
if (syncState.error) {
return { error: syncState.error };
}
@@ -78,5 +114,32 @@ export function syncAndDownload(accountId?: string) {
return true;
}
return { hasUpdated: hasDownloaded };
- };
-}
+ },
+);
+
+type SetAppStatePayload = Partial;
+
+const appSlice = createSlice({
+ name: sliceName,
+ initialState,
+ reducers: {
+ setAppState(state, action: PayloadAction) {
+ return {
+ ...state,
+ ...action.payload,
+ };
+ },
+ },
+});
+
+export const { name, reducer, getInitialState } = appSlice;
+
+export const actions = {
+ ...appSlice.actions,
+ updateApp,
+ resetSync,
+ sync,
+ syncAndDownload,
+};
+
+export const { setAppState } = actions;
diff --git a/packages/loot-core/src/client/reducers/app.ts b/packages/loot-core/src/client/reducers/app.ts
deleted file mode 100644
index db1161f12c1..00000000000
--- a/packages/loot-core/src/client/reducers/app.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import * as constants from '../constants';
-import type { Action } from '../state-types';
-import type { AppState } from '../state-types/app';
-
-export const initialState: AppState = {
- loadingText: null,
- updateInfo: null,
- showUpdateNotification: true,
- managerHasInitialized: false,
-};
-
-export function update(state = initialState, action: Action): AppState {
- switch (action.type) {
- case constants.SET_APP_STATE:
- return {
- ...state,
- ...action.state,
- };
- default:
- }
- return state;
-}
diff --git a/packages/loot-core/src/client/reducers/index.ts b/packages/loot-core/src/client/reducers/index.ts
index d87b61a7534..3cdfc844992 100644
--- a/packages/loot-core/src/client/reducers/index.ts
+++ b/packages/loot-core/src/client/reducers/index.ts
@@ -1,4 +1,3 @@
-import { update as app } from './app';
import { update as budgets } from './budgets';
import { update as modals } from './modals';
import { update as notifications } from './notifications';
@@ -6,7 +5,6 @@ import { update as prefs } from './prefs';
import { update as user } from './user';
export const reducers = {
- app,
prefs,
modals,
notifications,
diff --git a/packages/loot-core/src/client/reducers/modals.ts b/packages/loot-core/src/client/reducers/modals.ts
index 89f882f49c2..bcf1db0e041 100644
--- a/packages/loot-core/src/client/reducers/modals.ts
+++ b/packages/loot-core/src/client/reducers/modals.ts
@@ -7,7 +7,18 @@ export const initialState: ModalsState = {
isHidden: false,
};
-export function update(state = initialState, action: Action): ModalsState {
+type ModalsAction =
+ | Action
+ // Temporary until we migrate to redux toolkit.
+ | {
+ type: 'app/setAppState';
+ payload: { loadingText: string | null };
+ };
+
+export function update(
+ state = initialState,
+ action: ModalsAction,
+): ModalsState {
switch (action.type) {
case constants.PUSH_MODAL:
// special case: don't show the keyboard shortcuts modal if there's already a modal open
@@ -44,11 +55,12 @@ export function update(state = initialState, action: Action): ModalsState {
...state,
modalStack: idx < 0 ? state.modalStack : state.modalStack.slice(0, idx),
};
- case constants.SET_APP_STATE:
- if ('loadingText' in action.state) {
+ // Temporary until we migrate to redux toolkit.
+ case 'app/setAppState':
+ if (action.payload.loadingText) {
return {
...state,
- isHidden: action.state.loadingText != null,
+ isHidden: action.payload.loadingText != null,
};
}
break;
diff --git a/packages/loot-core/src/client/shared-listeners.ts b/packages/loot-core/src/client/shared-listeners.ts
index 06ea0ab453e..f4dcfcc5f0d 100644
--- a/packages/loot-core/src/client/shared-listeners.ts
+++ b/packages/loot-core/src/client/shared-listeners.ts
@@ -8,11 +8,10 @@ import {
closeAndDownloadBudget,
loadPrefs,
pushModal,
- resetSync,
signOut,
- sync,
uploadBudget,
} from './actions';
+import { resetSync, sync } from './app/appSlice';
import { getAccounts, getCategories, getPayees } from './queries/queriesSlice';
import type { Notification } from './state-types/notifications';
import { type AppStore } from './store';
@@ -82,7 +81,9 @@ export function listenForSyncEvent(store: AppStore) {
id: 'reset-sync',
button: {
title: t('Reset sync'),
- action: () => store.dispatch(resetSync()),
+ action: () => {
+ store.dispatch(resetSync());
+ },
},
};
} else {
@@ -131,7 +132,9 @@ export function listenForSyncEvent(store: AppStore) {
id: 'old-file',
button: {
title: t('Reset sync'),
- action: () => store.dispatch(resetSync()),
+ action: () => {
+ store.dispatch(resetSync());
+ },
},
};
break;
@@ -197,7 +200,9 @@ export function listenForSyncEvent(store: AppStore) {
id: 'upload-file',
button: {
title: t('Upload'),
- action: () => store.dispatch(resetSync()),
+ action: () => {
+ store.dispatch(resetSync());
+ },
},
};
break;
diff --git a/packages/loot-core/src/client/state-types/app.d.ts b/packages/loot-core/src/client/state-types/app.d.ts
deleted file mode 100644
index c0923f568cb..00000000000
--- a/packages/loot-core/src/client/state-types/app.d.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import type * as constants from '../constants';
-
-export type AppState = {
- loadingText: string | null;
- updateInfo: {
- version: string;
- releaseDate: string;
- releaseNotes: string;
- } | null;
- showUpdateNotification: boolean;
- managerHasInitialized: boolean;
-};
-
-export type SetAppStateAction = {
- type: typeof constants.SET_APP_STATE;
- state: Partial;
-};
-
-export type AppActions = SetAppStateAction;
diff --git a/packages/loot-core/src/client/state-types/index.d.ts b/packages/loot-core/src/client/state-types/index.d.ts
index ead231f695b..a7665c59a83 100644
--- a/packages/loot-core/src/client/state-types/index.d.ts
+++ b/packages/loot-core/src/client/state-types/index.d.ts
@@ -1,6 +1,5 @@
import type * as constants from '../constants';
-import type { AppActions, AppState } from './app';
import type { BudgetsActions, BudgetsState } from './budgets';
import type { ModalsActions, ModalsState } from './modals';
import type { NotificationsActions, NotificationsState } from './notifications';
@@ -12,7 +11,6 @@ export type CloseBudgetAction = {
};
export type Action =
- | AppActions
| BudgetsActions
| ModalsActions
| NotificationsActions
@@ -21,7 +19,6 @@ export type Action =
| CloseBudgetAction;
export type State = {
- app: AppState;
budgets: BudgetsState;
modals: ModalsState;
notifications: NotificationsState;
diff --git a/packages/loot-core/src/client/store/index.ts b/packages/loot-core/src/client/store/index.ts
index d124832bd0f..8ad3d8f4663 100644
--- a/packages/loot-core/src/client/store/index.ts
+++ b/packages/loot-core/src/client/store/index.ts
@@ -11,6 +11,11 @@ import {
getInitialState as getInitialAccountsState,
} from '../accounts/accountsSlice';
import { addNotification } from '../actions';
+import {
+ name as appSliceName,
+ reducer as appSliceReducer,
+ getInitialState as getInitialAppState,
+} from '../app/appSlice';
import * as constants from '../constants';
import {
name as queriesSliceName,
@@ -18,7 +23,6 @@ import {
getInitialState as getInitialQueriesState,
} from '../queries/queriesSlice';
import { reducers } from '../reducers';
-import { initialState as initialAppState } from '../reducers/app';
import { initialState as initialBudgetsState } from '../reducers/budgets';
import { initialState as initialModalsState } from '../reducers/modals';
import { initialState as initialNotificationsState } from '../reducers/notifications';
@@ -28,6 +32,7 @@ import { initialState as initialUserState } from '../reducers/user';
const appReducer = combineReducers({
...reducers,
[accountsSliceName]: accountsSliceReducer,
+ [appSliceName]: appSliceReducer,
[queriesSliceName]: queriesSliceReducer,
});
const rootReducer: typeof appReducer = (state, action) => {
@@ -47,7 +52,7 @@ const rootReducer: typeof appReducer = (state, action) => {
synced: initialPrefsState.synced,
},
app: {
- ...initialAppState,
+ ...getInitialAppState(),
managerHasInitialized: state?.app?.managerHasInitialized || false,
loadingText: state?.app?.loadingText || null,
},
diff --git a/packages/loot-core/src/client/store/mock.ts b/packages/loot-core/src/client/store/mock.ts
index 4d604573a1c..c2342bea301 100644
--- a/packages/loot-core/src/client/store/mock.ts
+++ b/packages/loot-core/src/client/store/mock.ts
@@ -4,6 +4,10 @@ import {
name as accountsSliceName,
reducer as accountsSliceReducer,
} from '../accounts/accountsSlice';
+import {
+ name as appSliceName,
+ reducer as appSliceReducer,
+} from '../app/appSlice';
import {
name as queriesSliceName,
reducer as queriesSliceReducer,
@@ -15,6 +19,7 @@ import { type store as realStore } from './index';
const appReducer = combineReducers({
...reducers,
[accountsSliceName]: accountsSliceReducer,
+ [appSliceName]: appSliceReducer,
[queriesSliceName]: queriesSliceReducer,
});
diff --git a/packages/loot-core/typings/window.d.ts b/packages/loot-core/typings/window.d.ts
index 3a63353e776..afe10ea6224 100644
--- a/packages/loot-core/typings/window.d.ts
+++ b/packages/loot-core/typings/window.d.ts
@@ -1,29 +1,44 @@
export {};
+type Actual = {
+ IS_DEV: boolean;
+ ACTUAL_VERSION: string;
+ openURLInBrowser: (url: string) => void;
+ saveFile: (
+ contents: string | Buffer,
+ filename: string,
+ dialogTitle?: string,
+ ) => Promise;
+ openFileDialog: (options) => Promise;
+ relaunch: () => void;
+ reload: (() => Promise) | undefined;
+ restartElectronServer: () => void;
+ moveBudgetDirectory: (
+ currentBudgetDirectory: string,
+ newDirectory: string,
+ ) => Promise;
+ applyAppUpdate: () => Promise;
+ updateAppMenu: (budgetId: string) => void;
+ ipcConnect: (callback: (client) => void) => void;
+ getServerSocket: () => Promise;
+ setTheme: (theme: string) => void;
+ logToTerminal: (...args: unknown[]) => void;
+ onEventFromMain: (
+ event: string,
+ listener: (...args: unknown[]) => void,
+ ) => void;
+ isUpdateReadyForDownload: () => boolean;
+ waitForUpdateReadyForDownload: () => Promise;
+ startOAuthServer: () => Promise;
+};
+
declare global {
interface Window {
- Actual?: {
- IS_FAKE_WEB: boolean;
- ACTUAL_VERSION: string;
- openURLInBrowser: (url: string) => void;
- saveFile: (
- contents: string | Buffer,
- filename: string,
- dialogTitle: string,
- ) => void;
- openFileDialog: (
- opts: Parameters[0],
- ) => Promise;
- relaunch: () => void;
- reload: (() => Promise) | undefined;
- restartElectronServer: () => void;
- startOAuthServer: () => Promise;
- moveBudgetDirectory: (
- currentBudgetDirectory: string,
- newDirectory: string,
- ) => Promise;
- };
-
__navigate?: import('react-router').NavigateFunction;
}
+
+ // eslint-disable-next-line no-var
+ var Actual: Actual;
+ // eslint-disable-next-line no-var
+ var IS_TESTING: boolean;
}
diff --git a/upcoming-release-notes/4018.md b/upcoming-release-notes/4018.md
new file mode 100644
index 00000000000..8d105830628
--- /dev/null
+++ b/upcoming-release-notes/4018.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [joel-jeremy]
+---
+
+Phase 2 - Redux Toolkit Migration - app slice