diff --git a/.eslintrc.js b/.eslintrc.js
index c5567bd7aff..0f6a9748289 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -70,6 +70,7 @@ module.exports = {
],
'import/no-useless-path-segments': 'error',
'import/no-duplicates': ['error', { 'prefer-inline': true }],
+ 'import/no-unused-modules': ['error', { 'unusedExports': true }],
'import/order': [
'error',
{
@@ -166,5 +167,33 @@ module.exports = {
],
},
},
+ {
+ files: ['./packages/loot-core/src/**/*'],
+ rules: {
+ // defining 'src' to check all packages is slow, so only do it for loot-core
+ 'import/no-unused-modules': ['error', { 'unusedExports': true, 'src': ['../**/*.{js,ts,tsx}'] }],
+ }
+ },
+ {
+ files: [
+ '**/icons/**/*.js',
+ '**/mocks/**/*.{js,ts,tsx}',
+ '**/{mocks,__mocks__}/*.{js,ts,tsx}',
+ // can't correctly resolve usages
+ '**/*.{testing,electron,browser,web,api}.ts',
+ 'packages/loot-core/src/server/main.ts'
+ ],
+ rules: { 'import/no-unused-modules': 'off' }
+ },
],
+ settings: {
+ "import/parsers": {
+ "@typescript-eslint/parser": [".ts", ".tsx"]
+ },
+ "import/resolver": {
+ "typescript": {
+ "alwaysTryTypes": true
+ }
+ }
+ }
};
diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml
index 7d07c7b198d..42b749e6e2e 100644
--- a/.github/ISSUE_TEMPLATE/bug-report.yml
+++ b/.github/ISSUE_TEMPLATE/bug-report.yml
@@ -1,7 +1,7 @@
name: Bug Report
description: File a bug report also known as an issue or problem.
title: '[Bug]: '
-labels: ['bug', 'needs triage']
+labels: ['bug']
body:
- type: markdown
id: intro-md
diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml
index 359e37fe12c..27dc7407eb2 100644
--- a/.github/ISSUE_TEMPLATE/feature-request.yml
+++ b/.github/ISSUE_TEMPLATE/feature-request.yml
@@ -1,7 +1,7 @@
name: Feature request
description: Request a missing feature
title: '[Feature] '
-labels: ['feature', 'needs triage']
+labels: ['feature']
body:
- type: markdown
id: intro-md
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 47475a636d0..5e53e6db9a9 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -31,6 +31,22 @@ jobs:
name: actual-api
path: packages/api/actual-api.tgz
+ crdt:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up environment
+ uses: ./.github/actions/setup
+ - name: Build CRDT
+ run: cd packages/crdt && yarn build
+ - name: Create package tgz
+ run: cd packages/crdt && yarn pack && mv package.tgz actual-crdt.tgz
+ - name: Upload Build
+ uses: actions/upload-artifact@v3
+ with:
+ name: actual-crdt
+ path: packages/crdt/actual-crdt.tgz
+
web:
runs-on: ubuntu-latest
steps:
diff --git a/.github/workflows/issues-add-triage-label.yml b/.github/workflows/issues-add-triage-label.yml
deleted file mode 100644
index c82d1a5a7cb..00000000000
--- a/.github/workflows/issues-add-triage-label.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-name: Mark new issue for triage
-
-on:
- issues:
- types: [opened]
-
-jobs:
- needs-triage:
- runs-on: ubuntu-latest
- steps:
- - uses: actions-ecosystem/action-add-labels@v1
- if: github.event.issue.labels == null
- with:
- labels: needs triage
diff --git a/.github/workflows/issues-feature-implemented.yml b/.github/workflows/issues-feature-implemented.yml
index 51ffaa8893c..43012f5226e 100644
--- a/.github/workflows/issues-feature-implemented.yml
+++ b/.github/workflows/issues-feature-implemented.yml
@@ -3,6 +3,9 @@ name: Handle completed feature requests
on:
pull_request:
types: [closed]
+
+permissions:
+ issues: write
jobs:
handle-feature-requests:
diff --git a/.gitignore b/.gitignore
index e5defbf5ea8..691472708e3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
!data/.gitkeep
/data2
packages/api/dist
+packages/crdt/dist
packages/desktop-electron/client-build
packages/desktop-electron/.electron-symbols
packages/desktop-electron/dist
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 98be9397e3d..2f412f35744 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,49 +1 @@
-## Expectations
-
-For smaller improvements or features - feel free to submit a PR or an issue if you don't have the necessary skills to build it yourself. For larger features we would recommend first opening an issue to discuss it with the team.
-
-We aren't going to take every single little change. Don't be offended if we close your PR. In order for the project to stay healthy, we need to guard our bandwidth and also only take changes that align with Actual.
-
-Here are some initial guidelines for how contributions will be treated:
-
-- The mental health of the maintainers will be prioritized above all else. If this means some things get lost and PRs are unreviewed because maintainers are spending time with family or on themselves, we celebrate that.
-
-- Multiple maintainers are key to this being a healthy project. Currently a few people have maintainer rights (see list below). We are actively looking for more people to come on as maintainers. If nobody steps up, expect less activity on this project.
-
-- An open PR does not automatically deserve time for a full review and acceptance. It's up to the PR author to convince the maintainers that the change is good and worth reviewing. This involves a clear description for why the the change is being made, detailing the tradeoffs.
-
-- We especially welcome improvements in automation: creating github actions to automatically generate builds, making the release process easier, etc.
-
-## Main contributors
-
-(sorted alphabetically)
-
-- @albertogasparin
-- @j-f1
-- @jlongster
-- @MatissJanis
-- @rich-howell
-- @trevdor
-
-## Project ideas
-
-We welcome all contributions from the community. If you have an idea for a feature you want to build - please go ahead and submit a PR with the implementation or if it's a larger feature - open a new issue so we can discuss it.
-
-If you do not have ideas what to build: the issue list is always a good starting point. Look for issues labeled with "[help wanted](https://github.com/actualbudget/actual/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22)".
-
-For first time contributions you can also filter the issues labeled with "[good first issue](https://github.com/actualbudget/actual/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)".
-
-
-## Development Environment
-If you would like to contribute you can fork this repository and create a branch specific to the project you are working on.
-
-There are three options for developing:
-1. Yarn
- - This is the traditional way to get an environment stood up. Run `yarn` to install the dependencies followed by `yarn start:browser` to start the development server. You will then be able to access Actual at `localhost:3001`.
-2. Docker Compose
- - If you prefer to work with docker containers, a `docker-compose.yml` file is included. Run `docker compose up -d` to start Actual. It will be accessible at `localhost:3001`.
-3. Dev container
- - Directly integrated in some IDEs, dependencies will be installed automatically as you enter the container.
- - Use your preferred method to `npm start` the project, your IDE should expose the project on your `localhost` for you.
-
-Both options above will dynamically update as you make changes to files. If you are making changes to the front end UI, you may have to reload the page to see any changes you make.
+Please review the contributing documentation on our website: https://actualbudget.org/docs/contributing/
diff --git a/README.md b/README.md
index e48f1604055..6348da70ab0 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ Want to say thanks? Click the ⭐ at the top of the page.
## Key Links
- Actual [discord](https://discord.gg/pRYNYr4W5A) community.
-- Actual [Community Documentation](https://actualbudget.github.io/docs)
+- Actual [Community Documentation](https://actualbudget.org/docs)
## Installation
@@ -23,11 +23,11 @@ If you are only interested in running the latest version and not contributing to
The easiest way to get Actual running is to use the [actual-server](https://github.com/actualbudget/actual-server) project. That is the server for syncing changes across devices, and it comes with the latest version of Actual. The server will provide both the web project and a server for syncing.
-You can get up and running quickly and easily by following our [Running Actual Locally Guide](https://actualbudget.github.io/docs/Installing/Local/your-own-machine)
+You can get up and running quickly and easily by following our [Running Actual Locally Guide](https://actualbudget.org/docs/install/local)
## Documentation
-We have a wide range of documentation on how to use Actual, this is all available in our [Community Documentation](https://actualbudget.github.io/docs), this includes topics on Budgeting, Account Management, Tips & Tricks and some documentation for developers.
+We have a wide range of documentation on how to use Actual, this is all available in our [Community Documentation](https://actualbudget.org/docs), this includes topics on Budgeting, Account Management, Tips & Tricks and some documentation for developers.
## Code structure
@@ -37,9 +37,10 @@ The Actual app is split up into a few packages:
- desktop-client - The desktop UI
- desktop-electron - The desktop app
-More information on the project structure is available in our [community documentation](https://actualbudget.github.io/docs/Developers/project-layout).
+More information on the project structure is available in our [community documentation](https://actualbudget.org/docs/contributing/project-layout).
## Feature Requests
+
Current feature requests can be seen [here](https://github.com/actualbudget/actual/issues?q=is%3Aissue+label%3A%22needs+votes%22+sort%3Areactions-%2B1-desc).
Vote for your favorite requests by reacting :+1: to the top comment of the request.
diff --git a/package.json b/package.json
index 033ef59b173..3c87fe2b3ad 100644
--- a/package.json
+++ b/package.json
@@ -41,7 +41,9 @@
"cross-env": "^7.0.3",
"eslint": "^8.37.0",
"eslint-config-react-app": "7.0.1",
+ "eslint-import-resolver-typescript": "3.5.5",
"eslint-plugin-prettier": "4.2.1",
+ "eslint-plugin-react": "7.32.2",
"eslint-plugin-rulesdir": "^0.2.2",
"npm-run-all": "^4.1.3",
"patch-package": "^6.1.2",
diff --git a/packages/api/README.md b/packages/api/README.md
index b4870ac5ec6..fdd48bed9ad 100644
--- a/packages/api/README.md
+++ b/packages/api/README.md
@@ -2,4 +2,4 @@
npm install @actual-app/api
```
-View docs here: https://actualbudget.github.io/docs/Developers/using-the-API
+View docs here: https://actualbudget.org/docs/api/
diff --git a/packages/crdt/.eslintignore b/packages/crdt/.eslintignore
new file mode 100644
index 00000000000..1521c8b7652
--- /dev/null
+++ b/packages/crdt/.eslintignore
@@ -0,0 +1 @@
+dist
diff --git a/packages/crdt/index.ts b/packages/crdt/index.ts
new file mode 100644
index 00000000000..c1cd862ed17
--- /dev/null
+++ b/packages/crdt/index.ts
@@ -0,0 +1 @@
+export * from './src/main';
diff --git a/packages/crdt/jest.config.js b/packages/crdt/jest.config.js
new file mode 100644
index 00000000000..a3b8912d852
--- /dev/null
+++ b/packages/crdt/jest.config.js
@@ -0,0 +1,4 @@
+module.exports = {
+ preset: 'ts-jest/presets/js-with-ts-esm',
+ testEnvironment: 'node',
+};
diff --git a/packages/crdt/package.json b/packages/crdt/package.json
new file mode 100644
index 00000000000..dfb22254ccd
--- /dev/null
+++ b/packages/crdt/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "@actual-app/crdt",
+ "version": "1.0.0",
+ "license": "MIT",
+ "description": "CRDT layer of Actual",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "lint": "eslint .",
+ "build:node": "tsc --p tsconfig.dist.json",
+ "build": "rm -rf dist && yarn run build:node && cp src/proto/sync_pb.d.ts dist/src/proto/",
+ "test": "jest -c jest.config.js"
+ },
+ "dependencies": {
+ "google-protobuf": "^3.12.0-rc.1",
+ "murmurhash": "^0.0.2",
+ "uuid": "3.3.2"
+ },
+ "devDependencies": {
+ "@types/jest": "^27.5.0",
+ "jest": "^27.0.0",
+ "ts-jest": "^27.0.0",
+ "typescript": "^5.0.2"
+ }
+}
diff --git a/packages/loot-core/src/server/crdt/__snapshots__/merkle.test.ts.snap b/packages/crdt/src/crdt/__snapshots__/merkle.test.ts.snap
similarity index 100%
rename from packages/loot-core/src/server/crdt/__snapshots__/merkle.test.ts.snap
rename to packages/crdt/src/crdt/__snapshots__/merkle.test.ts.snap
diff --git a/packages/loot-core/src/server/crdt/index.ts b/packages/crdt/src/crdt/index.ts
similarity index 100%
rename from packages/loot-core/src/server/crdt/index.ts
rename to packages/crdt/src/crdt/index.ts
diff --git a/packages/loot-core/src/server/crdt/merkle.test.ts b/packages/crdt/src/crdt/merkle.test.ts
similarity index 100%
rename from packages/loot-core/src/server/crdt/merkle.test.ts
rename to packages/crdt/src/crdt/merkle.test.ts
diff --git a/packages/loot-core/src/server/crdt/merkle.ts b/packages/crdt/src/crdt/merkle.ts
similarity index 100%
rename from packages/loot-core/src/server/crdt/merkle.ts
rename to packages/crdt/src/crdt/merkle.ts
diff --git a/packages/loot-core/src/server/crdt/timestamp.test.ts b/packages/crdt/src/crdt/timestamp.test.ts
similarity index 100%
rename from packages/loot-core/src/server/crdt/timestamp.test.ts
rename to packages/crdt/src/crdt/timestamp.test.ts
diff --git a/packages/loot-core/src/server/crdt/timestamp.ts b/packages/crdt/src/crdt/timestamp.ts
similarity index 98%
rename from packages/loot-core/src/server/crdt/timestamp.ts
rename to packages/crdt/src/crdt/timestamp.ts
index 5b256560951..6d431f4381d 100644
--- a/packages/loot-core/src/server/crdt/timestamp.ts
+++ b/packages/crdt/src/crdt/timestamp.ts
@@ -1,6 +1,5 @@
import murmurhash from 'murmurhash';
-
-import * as uuid from '../../platform/uuid';
+import uuid from 'uuid';
/**
* Hybrid Unique Logical Clock (HULC) timestamp generator
@@ -65,7 +64,7 @@ export function deserializeClock(clock) {
}
export function makeClientId() {
- return uuid.v4Sync().replace(/-/g, '').slice(-16);
+ return uuid.v4().replace(/-/g, '').slice(-16);
}
let config = {
@@ -282,7 +281,7 @@ Timestamp.recv = function (msg) {
* timestamp parsing
* converts a fixed-length string timestamp to the structured value
*/
-Timestamp.parse = function (timestamp) {
+Timestamp.parse = function (timestamp: string): Timestamp | null {
if (typeof timestamp === 'string') {
let parts = timestamp.split('-');
if (parts && parts.length === 5) {
diff --git a/packages/crdt/src/main.ts b/packages/crdt/src/main.ts
new file mode 100644
index 00000000000..76c4159a53d
--- /dev/null
+++ b/packages/crdt/src/main.ts
@@ -0,0 +1,13 @@
+import * as SyncPb from './proto/sync_pb';
+export {
+ merkle,
+ getClock,
+ setClock,
+ makeClock,
+ makeClientId,
+ serializeClock,
+ deserializeClock,
+ Timestamp,
+} from './crdt';
+
+export const SyncProtoBuf = SyncPb;
diff --git a/packages/loot-core/src/server/sync/proto/sync_pb.d.ts b/packages/crdt/src/proto/sync_pb.d.ts
similarity index 100%
rename from packages/loot-core/src/server/sync/proto/sync_pb.d.ts
rename to packages/crdt/src/proto/sync_pb.d.ts
diff --git a/packages/loot-core/src/server/sync/proto/sync_pb.js b/packages/crdt/src/proto/sync_pb.js
similarity index 100%
rename from packages/loot-core/src/server/sync/proto/sync_pb.js
rename to packages/crdt/src/proto/sync_pb.js
diff --git a/packages/crdt/tsconfig.dist.json b/packages/crdt/tsconfig.dist.json
new file mode 100644
index 00000000000..81e315be117
--- /dev/null
+++ b/packages/crdt/tsconfig.dist.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ // Using ES2021 because that’s the newest version where
+ // the latest Node 16.x release supports all of the features
+ "target": "es2021",
+ "module": "CommonJS",
+ "noEmit": false,
+ "declaration": true,
+ "outDir": "dist"
+ },
+ "include": ["."],
+ "exclude": ["dist"]
+}
diff --git a/packages/desktop-client/.eslintignore b/packages/desktop-client/.eslintignore
index e4e895c18a1..e39c09a7550 100644
--- a/packages/desktop-client/.eslintignore
+++ b/packages/desktop-client/.eslintignore
@@ -1,3 +1,5 @@
bundle.browser.js
build/
public/kcab/
+**/node_modules/*
+node_modules/
\ No newline at end of file
diff --git a/packages/desktop-client/e2e/onboarding.test.js b/packages/desktop-client/e2e/onboarding.test.js
index 05bc3ff778f..92c6b084690 100644
--- a/packages/desktop-client/e2e/onboarding.test.js
+++ b/packages/desktop-client/e2e/onboarding.test.js
@@ -75,6 +75,7 @@ test.describe('Onboarding', () => {
await configurationPage.startFresh();
await navigation.clickOnNoServer();
+ await page.getByRole('button', { name: 'Start using a server' }).click();
await expect(configurationPage.heading).toHaveText('Where’s the server?');
});
diff --git a/packages/desktop-client/e2e/page-models/account-page.js b/packages/desktop-client/e2e/page-models/account-page.js
index bf4fc96ba23..37398971a19 100644
--- a/packages/desktop-client/e2e/page-models/account-page.js
+++ b/packages/desktop-client/e2e/page-models/account-page.js
@@ -91,12 +91,6 @@ export class AccountPage {
return new CloseAccountModal(this.page.locator('css=[aria-modal]'));
}
- async _clearFocusedField() {
- let isMac = process.platform === 'darwin';
- await this.page.keyboard.press(isMac ? 'Meta+A' : 'Control+A');
- await this.page.keyboard.press('Backspace');
- }
-
async _fillTransactionFields(transactionRow, transaction) {
if (transaction.payee) {
await transactionRow.getByTestId('payee').click();
@@ -123,14 +117,12 @@ export class AccountPage {
if (transaction.debit) {
await transactionRow.getByTestId('debit').click();
- await this._clearFocusedField();
await this.page.keyboard.type(transaction.debit);
await this.page.keyboard.press('Tab');
}
if (transaction.credit) {
await transactionRow.getByTestId('credit').click();
- await this._clearFocusedField();
await this.page.keyboard.type(transaction.credit);
await this.page.keyboard.press('Tab');
}
diff --git a/packages/desktop-client/e2e/page-models/navigation.js b/packages/desktop-client/e2e/page-models/navigation.js
index 00a6d801d00..248db6444b6 100644
--- a/packages/desktop-client/e2e/page-models/navigation.js
+++ b/packages/desktop-client/e2e/page-models/navigation.js
@@ -57,6 +57,9 @@ export class Navigation {
async createAccount(data) {
await this.page.getByRole('button', { name: 'Add account' }).click();
+ await this.page
+ .getByRole('button', { name: 'Create local account' })
+ .click();
// Fill the form
await this.page.getByLabel('Name:').fill(data.name);
@@ -66,7 +69,9 @@ export class Navigation {
await this.page.getByLabel('Off-budget').click();
}
- await this.page.getByRole('button', { name: 'Create' }).click();
+ await this.page
+ .getByRole('button', { name: 'Create', exact: true })
+ .click();
return new AccountPage(this.page);
}
diff --git a/packages/desktop-client/package.json b/packages/desktop-client/package.json
index 7e14e925d58..a6c58d23656 100644
--- a/packages/desktop-client/package.json
+++ b/packages/desktop-client/package.json
@@ -48,9 +48,7 @@
"react-merge-refs": "^1.1.0",
"react-modal": "3.16.1",
"react-redux": "7.2.1",
- "react-router": "5.2.0",
- "react-router-dom": "5.2.0",
- "react-router-dom-v5-compat": "^6.4.1",
+ "react-router-dom": "6.11.2",
"react-scripts": "^5.0.1",
"react-spring": "^9.7.1",
"react-virtualized-auto-sizer": "^1.0.2",
diff --git a/packages/desktop-client/src/components/FatalError.js b/packages/desktop-client/src/components/FatalError.js
index 785ecf4838f..138bd12b635 100644
--- a/packages/desktop-client/src/components/FatalError.js
+++ b/packages/desktop-client/src/components/FatalError.js
@@ -28,7 +28,7 @@ class FatalError extends Component {
function properly. If you’re seeing this error, either your browser
does not support SharedArrayBuffer, or your server is not
sending the appropriate headers, or you are not using HTTPS. See{' '}
-
+
our troubleshooting documentation
{' '}
to learn more.
@@ -70,8 +70,7 @@ class FatalError extends Component {
>
{msg}
- Please get{' '}
- in touch{' '}
+ Please get in touch{' '}
for support
@@ -95,7 +94,7 @@ class FatalError extends Component {
- )}
-
-
- You can also create a local account if you want to track
- transactions manually. You can add transactions manually or import
- QIF/OFX/QFX files.
+
+
+ Create a local account if you want to add
+ transactions manually. You can also{' '}
+
+ import QIF/OFX/QFX files into a local account
+
+ .
+
+
+
+
+ {syncServerStatus === 'online' ? (
+ <>
+
+ {isNordigenSetupComplete
+ ? 'Link bank account with Nordigen'
+ : 'Set up Nordigen for bank sync'}
+
+
+ Link a bank account to automatically download
+ transactions. Nordigen provides reliable, up-to-date
+ information from hundreds of banks.
+
+ >
+ ) : (
+ <>
+
+ Set up Nordigen for bank sync
+
+
+ >
+ )}
-
-
- Create local account
-
)}
diff --git a/packages/desktop-client/src/components/modals/CreateEncryptionKey.js b/packages/desktop-client/src/components/modals/CreateEncryptionKey.js
index 2ca47eb6ceb..82a66622824 100644
--- a/packages/desktop-client/src/components/modals/CreateEncryptionKey.js
+++ b/packages/desktop-client/src/components/modals/CreateEncryptionKey.js
@@ -69,7 +69,7 @@ export default function CreateEncryptionKey({
other devices will have to revert to this version of your data.{' '}
Learn more
@@ -108,7 +108,7 @@ export default function CreateEncryptionKey({
will take you through that process on those devices.{' '}
Learn more
diff --git a/packages/desktop-client/src/components/modals/CreateLocalAccount.js b/packages/desktop-client/src/components/modals/CreateLocalAccount.js
index 38d729b0a7b..d4b8efcdf2f 100644
--- a/packages/desktop-client/src/components/modals/CreateLocalAccount.js
+++ b/packages/desktop-client/src/components/modals/CreateLocalAccount.js
@@ -1,4 +1,5 @@
import React from 'react';
+import { useNavigate } from 'react-router-dom';
import { Formik } from 'formik';
@@ -17,7 +18,8 @@ import {
Text,
} from '../common';
-function CreateLocalAccount({ modalProps, actions, history }) {
+function CreateLocalAccount({ modalProps, actions }) {
+ let navigate = useNavigate();
return (
{() => (
@@ -43,7 +45,7 @@ function CreateLocalAccount({ modalProps, actions, history }) {
toRelaxedNumber(values.balance),
values.offbudget,
);
- history.push('/accounts/' + id);
+ navigate('/accounts/' + id);
}
}}
render={({
@@ -115,7 +117,7 @@ function CreateLocalAccount({ modalProps, actions, history }) {
This cannot be changed later. {'\n'}
See{' '}
Learn more
@@ -83,7 +83,7 @@ export default function FixEncryptionKey({
password for this file to create the key for encryption.{' '}
Learn more
diff --git a/packages/desktop-client/src/components/modals/ImportTransactions.js b/packages/desktop-client/src/components/modals/ImportTransactions.js
index b98f8fe4b68..b954e5bba04 100644
--- a/packages/desktop-client/src/components/modals/ImportTransactions.js
+++ b/packages/desktop-client/src/components/modals/ImportTransactions.js
@@ -546,7 +546,7 @@ function MultipliersField({ multiplierCB, value, onChange }) {
);
}
-export function ImportTransactions({
+function ImportTransactions({
modalProps,
options,
dateFormat = 'MM/dd/yyyy',
diff --git a/packages/desktop-client/src/components/modals/MergeUnusedPayees.js b/packages/desktop-client/src/components/modals/MergeUnusedPayees.js
index bbccf33a979..62cd244ef53 100644
--- a/packages/desktop-client/src/components/modals/MergeUnusedPayees.js
+++ b/packages/desktop-client/src/components/modals/MergeUnusedPayees.js
@@ -11,7 +11,6 @@ import { View, Text, Modal, ModalButtons, Button, P } from '../common';
let highlightStyle = { color: colors.p5 };
export default function MergeUnusedPayees({
- history,
modalProps,
payeeIds,
targetPayeeId,
diff --git a/packages/desktop-client/src/components/modals/NordigenExternalMsg.js b/packages/desktop-client/src/components/modals/NordigenExternalMsg.js
index 975a3b2bdb1..1dc7bf36e56 100644
--- a/packages/desktop-client/src/components/modals/NordigenExternalMsg.js
+++ b/packages/desktop-client/src/components/modals/NordigenExternalMsg.js
@@ -6,10 +6,11 @@ import { sendCatch } from 'loot-core/src/platform/client/fetch';
import useNordigenStatus from '../../hooks/useNordigenStatus';
import AnimatedLoading from '../../icons/AnimatedLoading';
+import DotsHorizontalTriple from '../../icons/v1/DotsHorizontalTriple';
import { colors } from '../../style';
import { Error, Warning } from '../alerts';
import Autocomplete from '../autocomplete/Autocomplete';
-import { View, Modal, Button, P, Link } from '../common';
+import { View, Modal, Button, P, Link, Menu, Tooltip } from '../common';
import { FormField, FormLabel } from '../forms';
import { COUNTRY_OPTIONS } from './countries';
@@ -77,6 +78,7 @@ export default function NordigenExternalMsg({
let [country, setCountry] = useState();
let [error, setError] = useState(null);
let [isNordigenSetupComplete, setIsNordigenSetupComplete] = useState(null);
+ let [menuOpen, setMenuOpen] = useState(false);
let data = useRef(null);
const {
@@ -141,7 +143,14 @@ export default function NordigenExternalMsg({
{isBankOptionError ? (
Failed loading available banks: Nordigen access credentials might be
- misconfigured. Please set them up again.
+ misconfigured. Please{' '}
+
+ set them up
+ {' '}
+ again.
) : (
country &&
@@ -189,18 +198,50 @@ export default function NordigenExternalMsg({
before proceeding.
-
- Link bank in browser →
-
+
+
+ Link bank in browser →
+
+ setMenuOpen(true)} aria-label="Menu">
+
+ {menuOpen && (
+ setMenuOpen(false)}
+ >
+
+ )}
+
+
);
};
diff --git a/packages/desktop-client/src/components/payees/index.js b/packages/desktop-client/src/components/payees/index.js
index ad2b91b727c..b18e53d9c36 100644
--- a/packages/desktop-client/src/components/payees/index.js
+++ b/packages/desktop-client/src/components/payees/index.js
@@ -515,7 +515,7 @@ export const ManagePayees = forwardRef(
style={{
marginRight: '10px',
}}
- disabled={!(orphanedPayees.length > 0) && !orphanedOnly}
+ disabled={!(orphanedPayees?.length > 0) && !orphanedOnly}
onClick={() => {
setOrphanedOnly(!orphanedOnly);
const filterInput = document.getElementById('filter-input');
diff --git a/packages/desktop-client/src/components/reports/Overview.js b/packages/desktop-client/src/components/reports/Overview.js
index 264180009dc..31a5f14bd93 100644
--- a/packages/desktop-client/src/components/reports/Overview.js
+++ b/packages/desktop-client/src/components/reports/Overview.js
@@ -49,7 +49,6 @@ function Card({ flex, to, style, children }) {
return (
{content}
diff --git a/packages/desktop-client/src/components/reports/index.js b/packages/desktop-client/src/components/reports/index.js
index 75af78499bc..b0ccfe63c3e 100644
--- a/packages/desktop-client/src/components/reports/index.js
+++ b/packages/desktop-client/src/components/reports/index.js
@@ -1,5 +1,5 @@
import React from 'react';
-import { Route } from 'react-router-dom';
+import { Route, Routes } from 'react-router-dom';
import { View } from '../common';
@@ -10,15 +10,11 @@ import Overview from './Overview';
export default function Reports() {
return (
-
-
-
-
-
-
-
-
-
+
+ } />
+ } />
+ } />
+
);
}
diff --git a/packages/desktop-client/src/components/reports/util.js b/packages/desktop-client/src/components/reports/util.js
index 9866fca32b4..9cd905366d6 100644
--- a/packages/desktop-client/src/components/reports/util.js
+++ b/packages/desktop-client/src/components/reports/util.js
@@ -8,10 +8,6 @@ export function fromDateReprToDay(date) {
return date;
}
-export function toDateRepr(str) {
- return parseInt(str.replace(/-/g, ''), 10);
-}
-
export async function runAll(queries, cb) {
let data = await Promise.all(
queries.map(q => {
diff --git a/packages/desktop-client/src/components/schedules/DiscoverSchedules.js b/packages/desktop-client/src/components/schedules/DiscoverSchedules.js
index c03c8106519..bea585f1eb3 100644
--- a/packages/desktop-client/src/components/schedules/DiscoverSchedules.js
+++ b/packages/desktop-client/src/components/schedules/DiscoverSchedules.js
@@ -1,5 +1,5 @@
-import React, { useState, useEffect } from 'react';
-import { useHistory } from 'react-router-dom';
+import React, { useState } from 'react';
+import { Navigate, useLocation, useNavigate } from 'react-router-dom';
import q, { runQuery } from 'loot-core/src/client/query-helpers';
import { send } from 'loot-core/src/platform/client/fetch';
@@ -10,7 +10,9 @@ import useSelected, {
useSelectedItems,
SelectedProvider,
} from '../../hooks/useSelected';
+import useSendPlatformRequest from '../../hooks/useSendPlatformRequest';
import { colors } from '../../style';
+import { getParent } from '../../util/router-tools';
import { View, Stack, ButtonWithLoading, P } from '../common';
import { Page, usePageType } from '../Page';
import { Table, TableHeader, Row, Field, SelectCell } from '../table';
@@ -73,7 +75,7 @@ function DiscoverSchedulesTable({ schedules, loading }) {
0}
onSelect={e => dispatchSelected({ type: 'select-all', event: e })}
@@ -108,18 +110,19 @@ function DiscoverSchedulesTable({ schedules, loading }) {
export default function DiscoverSchedules() {
let pageType = usePageType();
- let history = useHistory();
- let [schedules, setSchedules] = useState();
+ let navigate = useNavigate();
+ let { data: schedules, isLoading } =
+ useSendPlatformRequest('schedule/discover');
+ if (!schedules) schedules = [];
+
let [creating, setCreating] = useState(false);
let selectedInst = useSelected('discover-schedules', schedules, []);
- useEffect(() => {
- async function run() {
- setSchedules(await send('schedule/discover'));
- }
- run();
- }, []);
+ let location = useLocation();
+ if (!getParent(location)) {
+ return ;
+ }
async function onCreate() {
let selected = schedules.filter(s => selectedInst.items.has(s.id));
@@ -149,7 +152,7 @@ export default function DiscoverSchedules() {
}
setCreating(false);
- history.goBack();
+ navigate(-1);
}
return (
@@ -165,10 +168,7 @@ export default function DiscoverSchedules() {
-
+ {
return state.prefs.local.dateFormat || 'MM/dd/yyyy';
@@ -373,13 +373,13 @@ export default function ScheduleDetails() {
dispatch({
type: 'form-error',
error:
- 'An error occurred while saving. Please contact help@actualbudget.com for support.',
+ 'An error occurred while saving. Please visit https://actualbudget.org/contact/ for support.',
});
} else {
if (adding) {
await onLinkTransactions([...selectedInst.items], res.data);
}
- history.goBack();
+ navigate(-1);
}
}
@@ -769,7 +769,7 @@ export default function ScheduleDetails() {
style={{ marginTop: 20 }}
>
{state.error && {state.error}}
- history.goBack()}>
+ navigate(-1)}>
Cancel
diff --git a/packages/desktop-client/src/components/schedules/LinkSchedule.js b/packages/desktop-client/src/components/schedules/LinkSchedule.js
index e27d20df69a..0ab090f6cd9 100644
--- a/packages/desktop-client/src/components/schedules/LinkSchedule.js
+++ b/packages/desktop-client/src/components/schedules/LinkSchedule.js
@@ -1,5 +1,5 @@
import React, { useCallback, useState } from 'react';
-import { useLocation, useHistory } from 'react-router-dom';
+import { useLocation, useNavigate } from 'react-router-dom';
import { useSchedules } from 'loot-core/src/client/data-hooks/schedules';
import { send } from 'loot-core/src/platform/client/fetch';
@@ -11,7 +11,7 @@ import { SchedulesTable } from './SchedulesTable';
export default function ScheduleLink() {
let location = useLocation();
- let history = useHistory();
+ let navigate = useNavigate();
let scheduleData = useSchedules(
useCallback(query => query.filter({ completed: false }), []),
);
@@ -32,7 +32,7 @@ export default function ScheduleLink() {
updated: ids.map(id => ({ id, schedule: scheduleId })),
});
}
- history.goBack();
+ navigate(-1);
}
return (
diff --git a/packages/desktop-client/src/components/schedules/PostsOfflineNotification.js b/packages/desktop-client/src/components/schedules/PostsOfflineNotification.js
index e770b0a185a..43a6b19f245 100644
--- a/packages/desktop-client/src/components/schedules/PostsOfflineNotification.js
+++ b/packages/desktop-client/src/components/schedules/PostsOfflineNotification.js
@@ -1,5 +1,5 @@
import React from 'react';
-import { useLocation, useHistory } from 'react-router-dom';
+import { useLocation, useNavigate } from 'react-router-dom';
import { send } from 'loot-core/src/platform/client/fetch';
@@ -10,18 +10,18 @@ import DisplayId from '../util/DisplayId';
export default function PostsOfflineNotification() {
let location = useLocation();
- let history = useHistory();
+ let navigate = useNavigate();
let payees = (location.state && location.state.payees) || [];
let plural = payees.length > 1;
function onClose() {
- history.goBack();
+ navigate(-1);
}
async function onPost() {
await send('schedule/force-run-service');
- history.goBack();
+ navigate(-1);
}
return (
diff --git a/packages/desktop-client/src/components/schedules/StatusBadge.js b/packages/desktop-client/src/components/schedules/StatusBadge.js
index ca8fbea1255..99ebf9f7ec9 100644
--- a/packages/desktop-client/src/components/schedules/StatusBadge.js
+++ b/packages/desktop-client/src/components/schedules/StatusBadge.js
@@ -66,12 +66,6 @@ export function getStatusProps(status) {
return { color, backgroundColor, Icon };
}
-export function StatusIcon({ status }) {
- let { color, Icon } = getStatusProps(status);
-
- return ;
-}
-
export function StatusBadge({ status, style }) {
let { color, backgroundColor, Icon } = getStatusProps(status);
return (
diff --git a/packages/desktop-client/src/components/schedules/index.js b/packages/desktop-client/src/components/schedules/index.js
index e5449088469..90bc38270be 100644
--- a/packages/desktop-client/src/components/schedules/index.js
+++ b/packages/desktop-client/src/components/schedules/index.js
@@ -1,16 +1,16 @@
import React, { useState } from 'react';
-import { useHistory } from 'react-router-dom';
import { useSchedules } from 'loot-core/src/client/data-hooks/schedules';
import { send } from 'loot-core/src/platform/client/fetch';
+import { usePushModal } from '../../util/router-tools';
import { View, Button, Search } from '../common';
import { Page } from '../Page';
import { SchedulesTable, ROW_HEIGHT } from './SchedulesTable';
export default function Schedules() {
- let history = useHistory();
+ let pushModal = usePushModal();
let [filter, setFilter] = useState('');
@@ -23,15 +23,15 @@ export default function Schedules() {
let { schedules, statuses } = scheduleData;
function onEdit(id) {
- history.push(`/schedule/edit/${id}`, { locationPtr: history.location });
+ pushModal(`/schedule/edit/${id}`);
}
function onAdd() {
- history.push(`/schedule/edit`, { locationPtr: history.location });
+ pushModal('/schedule/edit');
}
function onDiscover() {
- history.push(`/schedule/discover`, { locationPtr: history.location });
+ pushModal('/schedule/discover');
}
async function onAction(name, id) {
diff --git a/packages/desktop-client/src/components/select/DateSelect.js b/packages/desktop-client/src/components/select/DateSelect.js
index ade52981b46..8152c7877d4 100644
--- a/packages/desktop-client/src/components/select/DateSelect.js
+++ b/packages/desktop-client/src/components/select/DateSelect.js
@@ -69,7 +69,7 @@ let pickerStyles = {
},
};
-export let DatePicker = forwardRef(
+let DatePicker = forwardRef(
({ value, firstDayOfWeekIdx, dateFormat, onUpdate, onSelect }, ref) => {
let picker = useRef(null);
let mountPoint = useRef(null);
diff --git a/packages/desktop-client/src/components/settings/Encryption.js b/packages/desktop-client/src/components/settings/Encryption.js
index 185a04da791..8d59384b0d0 100644
--- a/packages/desktop-client/src/components/settings/Encryption.js
+++ b/packages/desktop-client/src/components/settings/Encryption.js
@@ -26,7 +26,7 @@ export default function EncryptionSettings({ prefs, pushModal }) {
it out to the cloud. Local data remains unencrypted so if you forget
your password you can re-encrypt it.{' '}
@@ -42,7 +42,7 @@ export default function EncryptionSettings({ prefs, pushModal }) {
on your server to use end-to-end encryption. This problem may also occur
if your browser is too old to work with Actual.{' '}
@@ -65,7 +65,7 @@ export default function EncryptionSettings({ prefs, pushModal }) {
read it. If you want, you can use an additional password to encrypt your
data on the server.{' '}
@@ -80,7 +80,7 @@ export default function EncryptionSettings({ prefs, pushModal }) {
without a server. Budget files are always kept unencrypted locally, and
encryption is only applied when sending data to a server.{' '}
diff --git a/packages/desktop-client/src/components/settings/Experimental.js b/packages/desktop-client/src/components/settings/Experimental.js
index 61d0b9660c6..23a2821193d 100644
--- a/packages/desktop-client/src/components/settings/Experimental.js
+++ b/packages/desktop-client/src/components/settings/Experimental.js
@@ -41,16 +41,6 @@ export default function ExperimentalFeatures({ prefs, savePrefs }) {
-
@@ -64,9 +64,7 @@ function About() {
)}
-
- Release Notes
-
+ Release Notes
diff --git a/packages/desktop-client/src/components/sidebar.js b/packages/desktop-client/src/components/sidebar.js
index db1cd3c12a2..5c3efed889e 100644
--- a/packages/desktop-client/src/components/sidebar.js
+++ b/packages/desktop-client/src/components/sidebar.js
@@ -30,7 +30,6 @@ const fontWeight = 600;
function ItemContent({
style,
to,
- exact,
onClick,
activeStyle,
forceActive,
@@ -57,7 +56,6 @@ function ItemContent({
) : (
diff --git a/packages/desktop-client/src/components/sort.js b/packages/desktop-client/src/components/sort.js
index c697f7f76bc..7ae7880542d 100644
--- a/packages/desktop-client/src/components/sort.js
+++ b/packages/desktop-client/src/components/sort.js
@@ -13,7 +13,7 @@ import { colors } from '../style';
import { View } from './common';
-export function useMergedRefs(ref1, ref2) {
+function useMergedRefs(ref1, ref2) {
return useMemo(() => {
function ref(value) {
[ref1, ref2].forEach(ref => {
diff --git a/packages/desktop-client/src/components/table.tsx b/packages/desktop-client/src/components/table.tsx
index 7d50ed73eb5..5b8fe3edbef 100644
--- a/packages/desktop-client/src/components/table.tsx
+++ b/packages/desktop-client/src/components/table.tsx
@@ -39,13 +39,13 @@ import {
IntersectionBoundary,
Menu,
} from './common';
-import { FixedSizeList } from './FixedSizeList';
+import FixedSizeList from './FixedSizeList';
import { KeyHandlers } from './KeyHandlers';
import format from './spreadsheet/format';
import SheetValue from './spreadsheet/SheetValue';
export const ROW_HEIGHT = 32;
-export const TABLE_BACKGROUND_COLOR = colors.n11;
+const TABLE_BACKGROUND_COLOR = colors.n11;
function fireBlur(onBlur, e) {
if (document.hasFocus()) {
@@ -448,7 +448,7 @@ export function InputCell({
);
}
-export function shouldSaveFromKey(e) {
+function shouldSaveFromKey(e) {
switch (e.key) {
case 'Tab':
case 'Enter':
diff --git a/packages/desktop-client/src/components/tutorial/AnimateIn.js b/packages/desktop-client/src/components/tutorial/AnimateIn.js
deleted file mode 100644
index 21465e7270b..00000000000
--- a/packages/desktop-client/src/components/tutorial/AnimateIn.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Component } from 'react';
-
-class AnimateIn extends Component {
- state = { animating: false };
-
- componentDidMount() {
- setTimeout(() => this.setState({ animating: true }), this.props.delay || 0);
- }
-
- render() {
- const { children } = this.props;
- return children(this.state.animating);
- }
-}
-
-export default AnimateIn;
diff --git a/packages/desktop-client/src/components/tutorial/BudgetCategories.js b/packages/desktop-client/src/components/tutorial/BudgetCategories.js
deleted file mode 100644
index c1ff56e303c..00000000000
--- a/packages/desktop-client/src/components/tutorial/BudgetCategories.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import React from 'react';
-
-import { colors } from '../../style';
-import { Tooltip, Pointer, P } from '../common';
-
-import { Title } from './common';
-import Navigation from './Navigation';
-
-function BudgetInitial({ targetRect, navigationProps }) {
- return (
-
-
- This is where you budget money for the current month
-
-
- As money comes in, you put it in categories. As money is spent, you
- can see each categories’ balance.
-
-
-
- Don’t overthink categories. If you haven’t budgeted before, start with
- just a few.
-
-
-
-
- );
-}
-
-export default BudgetInitial;
diff --git a/packages/desktop-client/src/components/tutorial/BudgetInitial.js b/packages/desktop-client/src/components/tutorial/BudgetInitial.js
deleted file mode 100644
index 56cedf0e27e..00000000000
--- a/packages/desktop-client/src/components/tutorial/BudgetInitial.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import React from 'react';
-import { connect } from 'react-redux';
-
-import { bindActionCreators } from 'redux';
-
-import * as actions from 'loot-core/src/client/actions';
-
-import { P, Button } from '../common';
-
-import { Title, Standalone, useMinimized } from './common';
-import Navigation from './Navigation';
-
-function BudgetInitial({ accounts, navigationProps }) {
- let [minimized, toggle] = useMinimized();
-
- return (
-
- Go ahead and budget your money
- {!minimized && (
- <>
-
- You should see all of your current accounts’ balance available to
- budget. Click on the budgeted column for a category create a budget.
- Keep doing this until your “To Budget” amount is zero.
-
-
- Don’t worry too much about your initial budget. Just guess. You’ll
- learn more about your spending in the first couple months.
-
- If you added any deposit transactions, you’ll see that you have more
- money to budget. Any income becomes{' '}
- immediately available to budget. Hooray!
-
-
-
- If you’ve already budgeted all you need this month, you can click the
- “To Budget” amount and select “Hold for next month.” This puts the
- money away for next month.
-
-
-
-
- );
-}
-
-export default BudgetSummary;
diff --git a/packages/desktop-client/src/components/tutorial/BudgetNextMonth.js b/packages/desktop-client/src/components/tutorial/BudgetNextMonth.js
deleted file mode 100644
index 65bf0418d5d..00000000000
--- a/packages/desktop-client/src/components/tutorial/BudgetNextMonth.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import React from 'react';
-import { connect } from 'react-redux';
-
-import { bindActionCreators } from 'redux';
-
-import * as actions from 'loot-core/src/client/actions';
-
-import { View, P, Button } from '../common';
-
-import { Standalone, Title, useMinimized } from './common';
-import Navigation from './Navigation';
-
-function BudgetNextMonth({ stepTwo, navigationProps }) {
- let [minimized, toggle] = useMinimized();
-
- return (
-
- Budgeting the next month
- {!minimized &&
- (!stepTwo ? (
-
-
- When a new month comes around, you distribute money again to fund
- each category for the new month. Move to the next month by
- clicking the right arrow.
-
-
-
- Tip: Show multiple months at once with the control in the top left
- of the screen.
-
-
- ) : (
-
-
- It’s easier this time though! Just hover over the new month and
- click 3 dots menu and select “Copy last month’s budget” to use the
- same budget as last month.
-
-
-
- You likely need to tweak the budget for the new month, depending
- on overspending and other factors. That’s ok! Adjusting your
- budget as life happens is crucial to a realistic budget.
-
- Actual uses a system where{' '}
- you can only budget money that you currently have.
-
-
-
- This is a summary of what money you have to budget and how Actual
- calculated it. It’s your current income (including anything leftover
- from last month) minus any overspending from last month and any
- already budgeted amount.
-
-
- {fromYNAB && (
-
- Since you’re coming from YNAB 4, an important distinction is that
- money is always immediately available.
-
- With that workflow you can have peace of mind that what you are looking
- at reflects reality.{' '}
-
- 😌
- {' '}
- The amount of money in a category is cash that you can safely spend
- right now.
-
-
-
- You probably want to delete the transactions you added and clean up your
- budget. If you have any questions or feedback, please get{' '}
-
- in touch
-
- .
-
-
-
- Read{' '}
-
- How it Works
- {' '}
- for an in-depth explanation of the budgeting workflow.
-
- Welcome to Actual!{' '}
-
- 🎉
- {' '}
- Learn the basic workflow with this quick tutorial. You
- can always restart it from the File menu.
-
-
-
We also recommend reading these articles:
-
-
-
-
- Getting Started
-
- : A guide on what to do first
-
-
-
- How it Works
-
- : An in-depth explanation of the budgeting workflow
-
-
-
-
- closeTutorial()}>
- Skip
-
- {
- if (window.location.hash !== '#/budget') {
- window.location.hash = '#/budget';
- setTimeout(() => {
- nextTutorialStage();
- }, 500);
- } else {
- nextTutorialStage();
- }
- }}
- >
- Start Tutorial
-
-
-
- );
-}
-
-export default Intro;
diff --git a/packages/desktop-client/src/components/tutorial/Navigation.js b/packages/desktop-client/src/components/tutorial/Navigation.js
deleted file mode 100644
index f673d7cecb1..00000000000
--- a/packages/desktop-client/src/components/tutorial/Navigation.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import React from 'react';
-
-import { Button, ModalButtons } from '../common';
-
-function Navigation({
- nextTutorialStage,
- previousTutorialStage,
- closeTutorial,
- showBack = true,
- showNext = true,
- leftContent,
- disableNext,
-}) {
- return (
-
- closeTutorial()}>Close
- {showBack && (
- previousTutorialStage()}
- style={{ marginLeft: 10 }}
- >
- Back
-
- )}
- {showNext && (
- nextTutorialStage()}
- disabled={disableNext}
- style={{ marginLeft: 10 }}
- >
- Next
-
- )}
-
- );
-}
-
-export default Navigation;
diff --git a/packages/desktop-client/src/components/tutorial/Overspending.js b/packages/desktop-client/src/components/tutorial/Overspending.js
deleted file mode 100644
index 6ad541dca0b..00000000000
--- a/packages/desktop-client/src/components/tutorial/Overspending.js
+++ /dev/null
@@ -1,95 +0,0 @@
-import React from 'react';
-import { connect } from 'react-redux';
-
-import { bindActionCreators } from 'redux';
-
-import * as actions from 'loot-core/src/client/actions';
-import * as monthUtils from 'loot-core/src/shared/months';
-import { integerToCurrency } from 'loot-core/src/shared/util';
-
-import { P, View, Text, Button } from '../common';
-import NamespaceContext from '../spreadsheet/NamespaceContext';
-import SheetValue from '../spreadsheet/SheetValue';
-
-import { Standalone, Title, useMinimized } from './common';
-import Navigation from './Navigation';
-
-function Overspending({ navigationProps, stepTwo }) {
- let currentMonth = monthUtils.currentMonth();
- let sheetName = monthUtils.sheetForMonth(currentMonth);
- let month = monthUtils.format(currentMonth, 'MMM');
- let [minimized, toggle] = useMinimized();
-
- return (
-
-
- {({ value: spentTotal }) => {
- return (
-
- Overspending
- {!minimized &&
- (stepTwo ? (
-
-
- The category balance becomes negative. Next month will
- reset this balance to zero, and you’ll see it in
- “Overspent in {month}” in next month’s summary, which in
- turn takes it out of next month’s “To Budget” amount.{' '}
-
-
-
-
- When you overspend, it’s taken out of next month’s
- available budget.
- {' '}
- A simple workflow would be to just take it out of next
- month’s savings, or whatever you like.
-
-
- ) : (
-
-
- What happens when you overspend? Let’s find out.
- {spentTotal === 0 && (
-
- You haven’t spent any money yet so add some expenses
- in your account to see it in action.
-
- )}
-
-
-
- {spentTotal !== 0 && (
-
- You’ve spent{' '}
-
- ${integerToCurrency(Math.abs(spentTotal))}
-
- .
-
- )}{' '}
- Try zeroing out a budget for a category that already has
- spent money in it. You’ll see how overspending works.
-
- You can add transactions in two ways: import files from your bank or
- manually add individual transactions. You can usually download these
- files straight from your online bank account. (QIF/OFX/QFX is
- supported, sometimes called a “Quicken File”)
-
-
-
- Try clicking “Add New” to see how adding transactions
- affects your budget.
-
-
-
-
-
- );
-}
-
-export default TransactionAdd;
diff --git a/packages/desktop-client/src/components/tutorial/TransactionEnter.js b/packages/desktop-client/src/components/tutorial/TransactionEnter.js
deleted file mode 100644
index 6f4eae659ea..00000000000
--- a/packages/desktop-client/src/components/tutorial/TransactionEnter.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import React from 'react';
-
-import { P } from '../common';
-
-import { Standalone, Title } from './common';
-import Navigation from './Navigation';
-
-function TransactionEnter({ navigationProps }) {
- return (
-
- Add a transaction
-
- Categorize the new transaction (you can use anything, try “Food”) and
- enter any amount in the “payment” column. You’ll see how it affects the
- budget.
-
-
-
- Next, try adding an income transaction by categorizing a new transaction
- as “Income” and entering an amount in the “deposit” column.
-
-
-
-
- );
-}
-
-export default TransactionEnter;
diff --git a/packages/desktop-client/src/components/tutorial/TransactionFinalize.js b/packages/desktop-client/src/components/tutorial/TransactionFinalize.js
deleted file mode 100644
index 71a9fb9982f..00000000000
--- a/packages/desktop-client/src/components/tutorial/TransactionFinalize.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import React from 'react';
-
-import { css } from 'glamor';
-
-import { P } from '../common';
-
-import { Standalone } from './common';
-import Navigation from './Navigation';
-
-function TransactionFinalize({ navigationProps }) {
- return (
-
-
All done!
-
- You can edit transactions by clicking anywhere on the table, or move
- around with the keyboard. A few keybindings:
-
-
- Tab and enter are the same and will move right (holding shift will
- move left).
-
-
Alt or command with arrow keys will move in any direction.
-
-
-
-
-
- );
-}
-
-export default TransactionFinalize;
diff --git a/packages/desktop-client/src/components/tutorial/common.js b/packages/desktop-client/src/components/tutorial/common.js
deleted file mode 100644
index f77426e6d60..00000000000
--- a/packages/desktop-client/src/components/tutorial/common.js
+++ /dev/null
@@ -1,68 +0,0 @@
-import React, { forwardRef, useState } from 'react';
-
-import { colors } from '../../style';
-import { View, AnchorLink } from '../common';
-
-import AnimateIn from './AnimateIn';
-
-export function Title({ children }) {
- return (
-
- {children}
-
- );
-}
-
-export function Standalone({ children, width = 320, skipAnimation = false }) {
- return (
-
- {animating => (
-
- {children}
-
- )}
-
- );
-}
-
-export const ExternalLink = forwardRef((props, ref) => {
- let { href, ...linkProps } = props;
- return (
- {
- e.preventDefault();
- window.Actual.openURLInBrowser(href);
- }}
- />
- );
-});
-
-export function useMinimized() {
- let [minimized, setMinimized] = useState(false);
-
- function toggleContent() {
- setMinimized(!minimized);
- }
-
- return [minimized, toggleContent];
-}
diff --git a/packages/desktop-client/src/global-events.js b/packages/desktop-client/src/global-events.js
index d50a41b392a..05974544038 100644
--- a/packages/desktop-client/src/global-events.js
+++ b/packages/desktop-client/src/global-events.js
@@ -36,11 +36,13 @@ export function handleGlobalEvents(actions, store) {
});
listen('schedules-offline', ({ payees }) => {
- let history = window.__history;
- if (history) {
- history.push(`/schedule/posts-offline-notification`, {
- locationPtr: history.location,
- payees,
+ let navigate = window.__navigate;
+ if (navigate) {
+ navigate(`/schedule/posts-offline-notification`, {
+ state: {
+ locationPtr: navigate.location,
+ payees,
+ },
});
}
});
diff --git a/packages/desktop-client/src/hooks/index.js b/packages/desktop-client/src/hooks/index.js
index 85bd3a7dce5..8304c4bbf4e 100644
--- a/packages/desktop-client/src/hooks/index.js
+++ b/packages/desktop-client/src/hooks/index.js
@@ -1,21 +1,7 @@
-import { useEffect, useRef } from 'react';
+import { useEffect } from 'react';
import { setThemeColor } from '../util/withThemeColor';
-export function useScrollFlasher() {
- let scrollRef = useRef(null);
-
- useEffect(() => {
- setTimeout(() => {
- if (scrollRef.current) {
- scrollRef.current.flashScrollIndicators();
- }
- }, 1000);
- }, []);
-
- return scrollRef;
-}
-
export function useSetThemeColor(color) {
useEffect(() => {
setThemeColor(color);
diff --git a/packages/desktop-client/src/hooks/useFeatureFlag.ts b/packages/desktop-client/src/hooks/useFeatureFlag.ts
index 7aa96bfb456..996bf328eb5 100644
--- a/packages/desktop-client/src/hooks/useFeatureFlag.ts
+++ b/packages/desktop-client/src/hooks/useFeatureFlag.ts
@@ -2,7 +2,6 @@ import { useSelector } from 'react-redux';
const DEFAULT_FEATURE_FLAG_STATE: Record = {
reportBudget: false,
- syncAccount: false,
goalTemplatesEnabled: false,
};
diff --git a/packages/desktop-client/src/hooks/useProperFocus.js b/packages/desktop-client/src/hooks/useProperFocus.js
index 780c4d7ca76..6d8930fff13 100644
--- a/packages/desktop-client/src/hooks/useProperFocus.js
+++ b/packages/desktop-client/src/hooks/useProperFocus.js
@@ -20,7 +20,7 @@ function getFocusedKey(el) {
return null;
}
-export function focusElement(el, refocusContext) {
+function focusElement(el, refocusContext) {
if (refocusContext) {
let key = getFocusedKey(el);
el.focus({ preventScroll: key && key === refocusContext.keyRef.current });
diff --git a/packages/desktop-client/src/hooks/useSendPlatformRequest.ts b/packages/desktop-client/src/hooks/useSendPlatformRequest.ts
new file mode 100644
index 00000000000..aa621589d8b
--- /dev/null
+++ b/packages/desktop-client/src/hooks/useSendPlatformRequest.ts
@@ -0,0 +1,28 @@
+import { useEffect, useState } from 'react';
+
+import { send } from 'loot-core/src/platform/client/fetch';
+import type { Handlers } from 'loot-core/src/types/handlers';
+
+export default function useSendPlatformRequest(
+ name: K,
+ args?: Parameters[0],
+ options?: { catchErrors?: boolean },
+) {
+ const [data, setData] = useState(null);
+ const [isLoading, setIsLoading] = useState(null);
+
+ useEffect(() => {
+ async function run() {
+ setIsLoading(true);
+ setData(await send(name, args, options));
+ setIsLoading(false);
+ }
+
+ run();
+ }, [name, args, options]);
+
+ return {
+ data,
+ isLoading,
+ };
+}
diff --git a/packages/desktop-client/src/plaid.js b/packages/desktop-client/src/plaid.js
index 8da29c334d2..a4b41a43888 100644
--- a/packages/desktop-client/src/plaid.js
+++ b/packages/desktop-client/src/plaid.js
@@ -1,3 +1,4 @@
+/* eslint-disable import/no-unused-modules */
import { send } from 'loot-core/src/platform/client/fetch';
function _authorize(pushModal, plaidToken, { onSuccess, onClose }) {
diff --git a/packages/desktop-client/src/style.tsx b/packages/desktop-client/src/style.tsx
index 84fa957e759..13dbcc255f4 100644
--- a/packages/desktop-client/src/style.tsx
+++ b/packages/desktop-client/src/style.tsx
@@ -2,8 +2,6 @@ import * as Platform from 'loot-core/src/client/platform';
import tokens from './tokens';
-export const debug = { borderWidth: 1, borderColor: 'red' };
-
export const colors = {
y1: '#733309',
y2: '#87540d',
diff --git a/packages/desktop-client/src/util.ts b/packages/desktop-client/src/util.ts
deleted file mode 100644
index e506321203d..00000000000
--- a/packages/desktop-client/src/util.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export function getModalRoute(name: string): [string, string] {
- let parts = name.split('/');
- return [parts[0], parts.slice(1).join('/')];
-}
diff --git a/packages/desktop-client/src/util/location-state.js b/packages/desktop-client/src/util/location-state.js
deleted file mode 100644
index 246d034df6d..00000000000
--- a/packages/desktop-client/src/util/location-state.js
+++ /dev/null
@@ -1,12 +0,0 @@
-let VERSION = Date.now();
-
-export function makeLocationState(state) {
- return { ...state, _version: VERSION };
-}
-
-export function getLocationState(location, subfield) {
- if (location.state && location.state._version === VERSION) {
- return subfield ? location.state[subfield] : location.state;
- }
- return null;
-}
diff --git a/packages/desktop-client/src/util/router-tools.tsx b/packages/desktop-client/src/util/router-tools.tsx
new file mode 100644
index 00000000000..a8fc7ea7d9c
--- /dev/null
+++ b/packages/desktop-client/src/util/router-tools.tsx
@@ -0,0 +1,71 @@
+import { type ReactNode, useCallback, useLayoutEffect } from 'react';
+import {
+ type Location,
+ type To,
+ useLocation,
+ useNavigate,
+} from 'react-router-dom';
+
+import { ActiveLocationProvider } from '../components/ActiveLocation';
+import { PageTypeProvider } from '../components/Page';
+
+let VERSION = Date.now();
+
+export function ExposeNavigate() {
+ let navigate = useNavigate();
+ useLayoutEffect(() => {
+ window.__navigate = navigate;
+ }, [navigate]);
+ return null;
+}
+
+export function usePushModal() {
+ let navigate = useNavigate();
+ let location = useLocation();
+
+ return useCallback(
+ (path: To) =>
+ navigate(path, { state: { parent: location, _version: VERSION } }),
+ [navigate, location],
+ );
+}
+
+export function getParent(location: Location): Location | null {
+ if (location.state?._version !== VERSION) {
+ return null;
+ }
+ return location.state?.parent || null;
+}
+
+export function StackedRoutes({
+ render,
+}: {
+ render: (loc: Location) => ReactNode;
+}) {
+ let location = useLocation();
+ let parent = getParent(location);
+
+ let locations = [location];
+ while (parent) {
+ locations.unshift(parent);
+ parent = getParent(parent);
+ }
+
+ let base = locations[0];
+ let stack = locations.slice(1);
+
+ return (
+
+ {render(base)}
+ {stack.map((location, idx) => (
+
+ {render(location)}
+
+ ))}
+
+ );
+}
diff --git a/packages/desktop-client/src/util/versions.ts b/packages/desktop-client/src/util/versions.ts
index c87e00b2ba6..00dc03eab3d 100644
--- a/packages/desktop-client/src/util/versions.ts
+++ b/packages/desktop-client/src/util/versions.ts
@@ -5,7 +5,7 @@ function parseSemanticVersion(versionString): [number, number, number] {
.map(n => parseInt(n));
}
-export function cmpSemanticVersion(
+function cmpSemanticVersion(
versionStringA: string,
versionStringB: string,
): number {
diff --git a/packages/desktop-electron/index.js b/packages/desktop-electron/index.js
index 898c42aa3fc..1b1a9a3ede2 100644
--- a/packages/desktop-electron/index.js
+++ b/packages/desktop-electron/index.js
@@ -204,11 +204,7 @@ function updateMenu(isBudgetOpen) {
const file = menu.items.filter(item => item.label === 'File')[0];
const fileItems = file.submenu.items;
fileItems
- .filter(
- item =>
- item.label === 'Start Tutorial' || item.label === 'Load Backup...',
- )
-
+ .filter(item => item.label === 'Load Backup...')
.map(item => (item.enabled = isBudgetOpen));
let tools = menu.items.filter(item => item.label === 'Tools')[0];
diff --git a/packages/desktop-electron/menu.js b/packages/desktop-electron/menu.js
index 9c8e371e762..f472495991f 100644
--- a/packages/desktop-electron/menu.js
+++ b/packages/desktop-electron/menu.js
@@ -5,20 +5,6 @@ function getMenu(isDev, createWindow) {
{
label: 'File',
submenu: [
- // {
- // label: 'Start Tutorial',
- // enabled: false,
- // click(item, focusedWindow) {
- // if (
- // focusedWindow &&
- // focusedWindow.webContents.getTitle() === 'Actual'
- // ) {
- // focusedWindow.webContents.executeJavaScript(
- // '__actionsForMenu.startTutorial()'
- // );
- // }
- // }
- // },
{
label: 'Load Backup...',
enabled: false,
@@ -145,7 +131,9 @@ function getMenu(isDev, createWindow) {
enabled: false,
click: function (menuItem, focusedWin) {
focusedWin.webContents.executeJavaScript(
- '__history && __history.push("/schedule/discover", { locationPtr: __history.location })',
+ // TODO: fix
+ // '__navigate && __history.push("/schedule/discover", { locationPtr: __history.location })',
+ 'alert("Not implemented")',
);
},
},
@@ -165,7 +153,7 @@ function getMenu(isDev, createWindow) {
{
label: 'Learn More',
click() {
- shell.openExternal('https://actualbudget.github.io/docs/');
+ shell.openExternal('https://actualbudget.org/docs/');
},
},
],
diff --git a/packages/import-ynab4/.eslintignore b/packages/import-ynab4/.eslintignore
new file mode 100644
index 00000000000..abcb3667e2a
--- /dev/null
+++ b/packages/import-ynab4/.eslintignore
@@ -0,0 +1 @@
+**/node_modules/*
\ No newline at end of file
diff --git a/packages/import-ynab5/.eslintignore b/packages/import-ynab5/.eslintignore
new file mode 100644
index 00000000000..abcb3667e2a
--- /dev/null
+++ b/packages/import-ynab5/.eslintignore
@@ -0,0 +1 @@
+**/node_modules/*
\ No newline at end of file
diff --git a/packages/loot-core/package.json b/packages/loot-core/package.json
index de8a1487927..3af15475d82 100644
--- a/packages/loot-core/package.json
+++ b/packages/loot-core/package.json
@@ -31,7 +31,6 @@
"csv-parse": "^4.10.1",
"csv-stringify": "^5.3.6",
"deep-equal": "^2.0.5",
- "google-protobuf": "^3.12.0-rc.1",
"md5": "^2.3.0",
"mitt": "^3.0.0",
"node-fetch": "^2.6.9",
@@ -43,6 +42,7 @@
},
"devDependencies": {
"@actual-app/api": "*",
+ "@actual-app/crdt": "*",
"@actual-app/import-ynab4": "*",
"@babel/core": "~7.22.5",
"@babel/preset-env": "^7.22.5",
@@ -65,7 +65,6 @@
"memfs": "3.1.1",
"memoize-one": "^4.0.0",
"mockdate": "^3.0.5",
- "murmurhash": "^0.0.2",
"npm-run-all": "^4.1.3",
"peggy": "3.0.2",
"snapshot-diff": "^0.10.0",
diff --git a/packages/loot-core/src/client/actions/budgets.ts b/packages/loot-core/src/client/actions/budgets.ts
index 5eff493d104..d9b6f18dfca 100644
--- a/packages/loot-core/src/client/actions/budgets.ts
+++ b/packages/loot-core/src/client/actions/budgets.ts
@@ -165,7 +165,7 @@ export function importBudget(filepath, type) {
dispatch(closeModal());
await dispatch(loadPrefs());
- window.__history.push('/budget');
+ window.__navigate('/budget');
};
}
diff --git a/packages/loot-core/src/client/actions/notifications.ts b/packages/loot-core/src/client/actions/notifications.ts
index 7cc0ca1eb67..86c1a1ccd71 100644
--- a/packages/loot-core/src/client/actions/notifications.ts
+++ b/packages/loot-core/src/client/actions/notifications.ts
@@ -16,7 +16,7 @@ export function addGenericErrorNotification() {
type: 'error',
message:
'Something internally went wrong. You may want to restart the app if anything looks wrong. ' +
- 'We have been notified of the issue and will try to fix it soon.',
+ 'Please report this as a new issue on Github.',
});
}
diff --git a/packages/loot-core/src/client/constants.ts b/packages/loot-core/src/client/constants.ts
index 528d2769cf6..1fbe8a11aec 100644
--- a/packages/loot-core/src/client/constants.ts
+++ b/packages/loot-core/src/client/constants.ts
@@ -36,6 +36,3 @@ export const SET_ACCOUNTS_SYNCING = 'SET_ACCOUNTS_SYNCING';
export const ACCOUNT_SYNC_STATUS = 'ACCOUNT_SYNC_STATUS';
export const ACCOUNT_SYNC_FAILURES = 'ACCOUNT_SYNC_FAILURES';
export const SIGN_OUT = 'SIGN_OUT';
-
-export const SET_TUTORIAL_STAGE = 'SET_TUTORIAL_STAGE';
-export const DEACTIVATE_TUTORIAL = 'DEACTIVATE_TUTORIAL';
diff --git a/packages/loot-core/src/client/data-hooks/accounts.tsx b/packages/loot-core/src/client/data-hooks/accounts.tsx
index c19b2c4e6c3..147e545c6f8 100644
--- a/packages/loot-core/src/client/data-hooks/accounts.tsx
+++ b/packages/loot-core/src/client/data-hooks/accounts.tsx
@@ -4,7 +4,7 @@ import q from '../query-helpers';
import { useLiveQuery } from '../query-hooks';
import { getAccountsById } from '../reducers/queries';
-export function useAccounts() {
+function useAccounts() {
return useLiveQuery(() => q('accounts').select('*'), []);
}
diff --git a/packages/loot-core/src/client/data-hooks/payees.tsx b/packages/loot-core/src/client/data-hooks/payees.tsx
index c06ba9a2e39..2adba3bb71f 100644
--- a/packages/loot-core/src/client/data-hooks/payees.tsx
+++ b/packages/loot-core/src/client/data-hooks/payees.tsx
@@ -4,7 +4,7 @@ import q from '../query-helpers';
import { useLiveQuery } from '../query-hooks';
import { getPayeesById } from '../reducers/queries';
-export function usePayees() {
+function usePayees() {
return useLiveQuery(() => q('payees').select('*'), []);
}
diff --git a/packages/loot-core/src/client/query-helpers.ts b/packages/loot-core/src/client/query-helpers.ts
index 4416d3851b8..4ff9e457fe9 100644
--- a/packages/loot-core/src/client/query-helpers.ts
+++ b/packages/loot-core/src/client/query-helpers.ts
@@ -7,13 +7,13 @@ export async function runQuery(query) {
return send('query', query.serialize());
}
-export function liveQuery(query, onData?, opts?) {
+export function liveQuery(query, onData?, opts?): LiveQuery {
let q = new LiveQuery(query, onData, opts);
q.run();
return q;
}
-export function pagedQuery(query, onData?, opts?) {
+export function pagedQuery(query, onData?, opts?): PagedQuery {
let q = new PagedQuery(query, onData, opts);
q.run();
return q;
@@ -176,7 +176,7 @@ export class LiveQuery {
}
// Paging
-export class PagedQuery extends LiveQuery {
+class PagedQuery extends LiveQuery {
done;
onPageData;
pageCount;
diff --git a/packages/loot-core/src/client/query-hooks.tsx b/packages/loot-core/src/client/query-hooks.tsx
index 5828a671594..d516f011633 100644
--- a/packages/loot-core/src/client/query-hooks.tsx
+++ b/packages/loot-core/src/client/query-hooks.tsx
@@ -9,7 +9,7 @@ import React, {
import { type Query } from '../shared/query';
-import { liveQuery, LiveQuery, PagedQuery } from './query-helpers';
+import { liveQuery, LiveQuery } from './query-helpers';
function makeContext(queryState, opts, QueryClass) {
let query = new QueryClass(queryState, null, opts);
@@ -69,10 +69,6 @@ export function liveQueryContext(query, opts) {
return makeContext(query, opts, LiveQuery);
}
-export function pagedQueryContext(query, opts) {
- return makeContext(query, opts, PagedQuery);
-}
-
export function useLiveQuery(makeQuery: () => Query, deps: DependencyList) {
let [data, setData] = useState(null);
let query = useMemo(makeQuery, deps);
diff --git a/packages/loot-core/src/client/reducers/budgets.ts b/packages/loot-core/src/client/reducers/budgets.ts
index 45a7ee28fcf..ddd75630c72 100644
--- a/packages/loot-core/src/client/reducers/budgets.ts
+++ b/packages/loot-core/src/client/reducers/budgets.ts
@@ -20,7 +20,7 @@ function sortFiles(arr) {
// 4. detached - Downloaded but broken group id (reset sync state)
// 5. broken - user shouldn't have access to this file
// 6. unknown - user is offline so can't determine the status
-export function reconcileFiles(localFiles, remoteFiles) {
+function reconcileFiles(localFiles, remoteFiles) {
let reconciled = new Set();
let files = localFiles.map(localFile => {
diff --git a/packages/loot-core/src/client/reducers/index.ts b/packages/loot-core/src/client/reducers/index.ts
index 1253ce09e08..17d0f75293c 100644
--- a/packages/loot-core/src/client/reducers/index.ts
+++ b/packages/loot-core/src/client/reducers/index.ts
@@ -7,7 +7,6 @@ import notifications from './notifications';
import prefs from './prefs';
import profile from './profile';
import queries from './queries';
-import tutorial from './tutorial';
import user from './user';
const reducers = {
@@ -20,7 +19,6 @@ const reducers = {
modals,
notifications,
budgets,
- tutorial,
user,
};
export default reducers;
diff --git a/packages/loot-core/src/client/reducers/tutorial.ts b/packages/loot-core/src/client/reducers/tutorial.ts
deleted file mode 100644
index ea4bcc75718..00000000000
--- a/packages/loot-core/src/client/reducers/tutorial.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import * as constants from '../constants';
-
-const initialState = {
- stage: null,
- fromYNAB: false,
-};
-
-export default function update(state = initialState, action) {
- switch (action.type) {
- case constants.SET_TUTORIAL_STAGE:
- return {
- ...state,
- deactivated: false,
- stage: action.stage,
- fromYNAB: 'fromYNAB' in action ? action.fromYNAB : state.fromYNAB,
- };
- case constants.DEACTIVATE_TUTORIAL:
- return { ...state, stage: null };
- default:
- }
-
- return state;
-}
diff --git a/packages/loot-core/src/client/shared-listeners.ts b/packages/loot-core/src/client/shared-listeners.ts
index ddcb0615460..9253433dc80 100644
--- a/packages/loot-core/src/client/shared-listeners.ts
+++ b/packages/loot-core/src/client/shared-listeners.ts
@@ -45,9 +45,9 @@ export function listenForSyncEvent(actions, store) {
} else if (type === 'error') {
let notif = null;
let learnMore =
- '[Learn more](https://actualbudget.github.io/docs/Getting-Started/sync#debugging-sync-issues)';
+ '[Learn more](https://actualbudget.org/docs/getting-started/sync/#debugging-sync-issues)';
const githubIssueLink =
- 'https://github.com/actualbudget/actual/issues/new?assignees=&labels=bug%2Cneeds+triage&template=bug-report.yml&title=%5BBug%5D%3A+';
+ 'https://github.com/actualbudget/actual/issues/new?assignees=&labels=bug&template=bug-report.yml&title=%5BBug%5D%3A+';
switch (subtype) {
case 'out-of-sync':
@@ -97,7 +97,7 @@ export function listenForSyncEvent(actions, store) {
'This happens rarely (if ever again). The internal syncing format ' +
'has changed and you need to reset sync. This will upload data from ' +
'this device and revert all other devices. ' +
- '[Learn more about what this means](https://actualbudget.github.io/docs/Getting-Started/sync#what-does-resetting-sync-mean).' +
+ '[Learn more about what this means](https://actualbudget.org/docs/getting-started/sync/#what-does-resetting-sync-mean).' +
'\n\nOld encryption keys are not migrated. If using ' +
'encryption, [reset encryption here](#makeKey).',
messageActions: {
diff --git a/packages/loot-core/src/client/tutorial.ts b/packages/loot-core/src/client/tutorial.ts
deleted file mode 100644
index 96c513a8166..00000000000
--- a/packages/loot-core/src/client/tutorial.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export const stages = {
- BUDGET: 'budget',
-};
-
-export const order = [stages.BUDGET];
diff --git a/packages/loot-core/src/client/update-notification.ts b/packages/loot-core/src/client/update-notification.ts
index 55d9b77c2c5..3057a1c6fb1 100644
--- a/packages/loot-core/src/client/update-notification.ts
+++ b/packages/loot-core/src/client/update-notification.ts
@@ -24,7 +24,7 @@ export default async function checkForUpdateNotification(
button: {
title: 'Open changelog',
action: () => {
- window.open('https://actualbudget.github.io/docs/Release-Notes');
+ window.open('https://actualbudget.org/docs/releases');
},
},
onClose: async () => {
diff --git a/packages/loot-core/src/client/upgrade-notifications.ts b/packages/loot-core/src/client/upgrade-notifications.ts
deleted file mode 100644
index c2eca0d2a14..00000000000
--- a/packages/loot-core/src/client/upgrade-notifications.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import { send } from '../platform/client/fetch';
-
-import * as Platform from './platform';
-
-export default function checkForUpgradeNotifications(
- addNotification,
- resetSync,
- // Note: history is only available on desktop
- history,
-) {
- // TODO: Probably should only show one of these at at time?
- send('get-upgrade-notifications').then(types => {
- types.forEach(type => {
- switch (type) {
- case 'schedules': {
- let message =
- 'Track bills and subscriptions and much more with scheduled transactions. We can search all your existing transactions and try to find existing schedules.\n\n' +
- (global.IS_BETA
- ? 'NOTE: You are using the beta version, and this will not reset your sync data. This is safe to do.'
- : '');
-
- if (Platform.env === 'mobile') {
- message =
- 'Track bills and subscriptions and much more with scheduled transactions. Upcoming transactions will be shown in the accounts screen. Use the desktop app to create schedules.';
- }
-
- addNotification({
- type: 'message',
- title: 'Scheduled transactions are now available!',
- message,
- sticky: true,
- id: 'find-schedules',
- button: Platform.env !== 'mobile' && {
- title: 'Find schedules',
- action: async () => {
- window.__history &&
- window.__history.push('/schedule/discover', {
- locationPtr: window.__history.location,
- });
- },
- },
- onClose: () => {
- send('seen-upgrade-notification', { type: 'schedules' });
- },
- });
- break;
- }
-
- case 'repair-splits':
- if (history) {
- addNotification({
- type: 'message',
- title: 'Split transactions now support transfers & payees',
- message:
- 'The payee field is now available on split transactions, allowing you to perform transfers on individual split transactions.\n\nAll existing split transactions have a blank payee and we recommend using the tool below to set the payee from the parent. [View a video walkthrough](https://www.youtube.com/watch?v=5kTtAsB0Oqk)',
- sticky: true,
- id: 'repair-splits',
- button: {
- title: 'Repair splits...',
- action: () =>
- history.push('/tools/fix-splits', {
- locationPtr: history.location,
- }),
- },
- onClose: () => {
- send('seen-upgrade-notification', { type: 'repair-splits' });
- },
- });
- }
- break;
-
- default:
- }
- });
- });
-}
diff --git a/packages/loot-core/src/mocks/number-formats.ts b/packages/loot-core/src/mocks/number-formats.ts
deleted file mode 100644
index e586b490617..00000000000
--- a/packages/loot-core/src/mocks/number-formats.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-export function generateTestCases(
- configurableFormats: string[],
- inputFormats: Array<{
- name: string;
- tests: Array<{ places: number; input: string; expected: number }>;
- }>,
-) {
- let cases = [];
-
- for (let configurableFormat of configurableFormats) {
- for (let inputFormat of inputFormats) {
- for (let test of inputFormat.tests) {
- cases.push([
- configurableFormat,
- inputFormat.name,
- test.places,
- test.input,
- test.expected,
- ]);
- }
- }
- }
-
- return cases;
-}
diff --git a/packages/loot-core/src/mocks/setup.ts b/packages/loot-core/src/mocks/setup.ts
index 7da62ee461b..431b0a47871 100644
--- a/packages/loot-core/src/mocks/setup.ts
+++ b/packages/loot-core/src/mocks/setup.ts
@@ -52,9 +52,11 @@ global.resetRandomId = () => {
_id = 1;
};
-global.randomId = () => {
- return 'id' + _id++;
-};
+jest.mock('uuid', () => ({
+ v4: () => {
+ return 'id' + _id++;
+ },
+}));
global.getDatabaseDump = async function (tables) {
if (!tables) {
diff --git a/packages/loot-core/src/platform/client/fetch/index.browser.ts b/packages/loot-core/src/platform/client/fetch/index.browser.ts
index ee81c1c4cad..98122be9f4f 100644
--- a/packages/loot-core/src/platform/client/fetch/index.browser.ts
+++ b/packages/loot-core/src/platform/client/fetch/index.browser.ts
@@ -116,7 +116,7 @@ function connectWorker(worker, onOpen, onError) {
if (msg.message && msg.message.includes('indexeddb-quota-error')) {
alert(
- 'We hit a limit on the local storage available. Edits may not be saved. Please get in touch https://actualbudget.github.io/docs/Contact/ so we can help debug this.',
+ 'We hit a limit on the local storage available. Edits may not be saved. Please get in touch https://actualbudget.org/contact/ so we can help debug this.',
);
}
} else if (msg.type === 'capture-breadcrumb') {
diff --git a/packages/loot-core/src/platform/client/fetch/index.testing.ts b/packages/loot-core/src/platform/client/fetch/index.testing.ts
index e96d886381a..45722e05fb6 100644
--- a/packages/loot-core/src/platform/client/fetch/index.testing.ts
+++ b/packages/loot-core/src/platform/client/fetch/index.testing.ts
@@ -1,2 +1,2 @@
-// eslint-disable-next-line no-restricted-imports, import/extensions
+// eslint-disable-next-line no-restricted-imports
export * from './__mocks__/index.web';
diff --git a/packages/loot-core/src/platform/server/sqlite/index.web.test.ts b/packages/loot-core/src/platform/server/sqlite/index.web.test.ts
index ab64c064e46..09eaadae0ab 100644
--- a/packages/loot-core/src/platform/server/sqlite/index.web.test.ts
+++ b/packages/loot-core/src/platform/server/sqlite/index.web.test.ts
@@ -5,7 +5,6 @@ import {
transaction,
execQuery,
runQuery,
- // eslint-disable-next-line import/extensions
} from './index.web';
beforeAll(() => {
diff --git a/packages/loot-core/src/platform/uuid/index.testing.ts b/packages/loot-core/src/platform/uuid/index.testing.ts
deleted file mode 100644
index 0905063888a..00000000000
--- a/packages/loot-core/src/platform/uuid/index.testing.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import type * as T from '.';
-
-export const v4: T.V4 = function () {
- return Promise.resolve(global.randomId());
-};
-
-export const v4Sync: T.V4Sync = function () {
- return global.randomId();
-};
diff --git a/packages/loot-core/src/server/accounts/payees.ts b/packages/loot-core/src/server/accounts/payees.ts
index 9c4c55d0c3d..de612e0d013 100644
--- a/packages/loot-core/src/server/accounts/payees.ts
+++ b/packages/loot-core/src/server/accounts/payees.ts
@@ -1,5 +1,6 @@
import * as db from '../db';
+/* eslint-disable import/no-unused-modules */
export async function createPayee(description) {
// Check to make sure no payee already exists with exactly the same
// name
diff --git a/packages/loot-core/src/server/accounts/rules.ts b/packages/loot-core/src/server/accounts/rules.ts
index 37d547a0d71..d79ef88f875 100644
--- a/packages/loot-core/src/server/accounts/rules.ts
+++ b/packages/loot-core/src/server/accounts/rules.ts
@@ -21,7 +21,7 @@ function assert(test, type, msg) {
}
}
-export function parseRecurDate(desc) {
+function parseRecurDate(desc) {
try {
let rules = recurConfigToRSchedule(desc);
@@ -63,7 +63,7 @@ export function parseDateString(str) {
return null;
}
-export function parseBetweenAmount(between) {
+function parseBetweenAmount(between) {
let { num1, num2 } = between;
if (typeof num1 !== 'number' || typeof num2 !== 'number') {
return null;
diff --git a/packages/loot-core/src/server/accounts/transaction-rules.ts b/packages/loot-core/src/server/accounts/transaction-rules.ts
index 46c7c559364..fe81388007c 100644
--- a/packages/loot-core/src/server/accounts/transaction-rules.ts
+++ b/packages/loot-core/src/server/accounts/transaction-rules.ts
@@ -16,8 +16,7 @@ import * as db from '../db';
import { getMappings } from '../db/mappings';
import { RuleError } from '../errors';
import { requiredFields, toDateRepr } from '../models';
-import { batchMessages } from '../sync';
-import { addSyncListener } from '../sync/index';
+import { batchMessages, addSyncListener } from '../sync';
import {
Condition,
diff --git a/packages/loot-core/src/server/api-models.ts b/packages/loot-core/src/server/api-models.ts
index 1678ae15af2..53a4ae52fc9 100644
--- a/packages/loot-core/src/server/api-models.ts
+++ b/packages/loot-core/src/server/api-models.ts
@@ -1,82 +1,5 @@
import * as models from './models';
-export const transactionModel = {
- ...models.transactionModel,
-
- toExternal(transactions, idx, payees) {
- return transactions;
- // function convert(t, payee) {
- // return {
- // id: t.id,
- // account_id: t.acct,
- // amount: t.amount,
- // payee_id: payee ? payee.id : null,
- // payee: payee ? payee.name : null,
- // imported_payee: t.imported_description,
- // category_id: t.category,
- // date: t.date,
- // notes: t.notes,
- // imported_id: t.financial_id,
- // transfer_id: t.transferred_id,
- // cleared: t.cleared
- // };
- // }
-
- // let splits = getAllSplitTransactions(transactions, idx);
- // if (splits) {
- // let payee =
- // splits.parent.description && payees[splits.parent.description];
-
- // return {
- // ...convert(splits.parent, payee),
- // subtransactions: splits.children.map(child => convert(child, payee))
- // };
- // }
-
- // let transaction = transactions[idx];
- // let payee = transaction.description && payees[transaction.description];
- // return convert(transaction, payee);
- },
-
- fromExternal(transaction) {
- let result: Record = {};
- if ('id' in transaction) {
- result.id = transaction.id;
- }
- if ('account_id' in transaction) {
- result.acct = transaction.account_id;
- }
- if ('amount' in transaction) {
- result.amount = transaction.amount;
- }
- if ('payee_id' in transaction) {
- result.description = transaction.payee_id;
- }
- if ('imported_payee' in transaction) {
- result.imported_description = transaction.imported_payee;
- }
- if ('category_id' in transaction) {
- result.category = transaction.category_id;
- }
- if ('date' in transaction) {
- result.date = transaction.date;
- }
- if ('notes' in transaction) {
- result.notes = transaction.notes;
- }
- if ('imported_id' in transaction) {
- result.financial_id = transaction.imported_id;
- }
- if ('transfer_id' in transaction) {
- result.transferred_id = transaction.transfer_id;
- }
- if ('cleared' in transaction) {
- result.cleared = transaction.cleared;
- }
- return result;
- },
-};
-
export const accountModel = {
...models.accountModel,
diff --git a/packages/loot-core/src/server/api.ts b/packages/loot-core/src/server/api.ts
index f249312697d..85ad5183bf8 100644
--- a/packages/loot-core/src/server/api.ts
+++ b/packages/loot-core/src/server/api.ts
@@ -1,3 +1,5 @@
+import { getClock } from '@actual-app/crdt';
+
import * as connection from '../platform/server/connection';
import {
getDownloadError,
@@ -22,7 +24,6 @@ import {
} from './api-models';
import { runQuery as aqlQuery } from './aql';
import * as cloudStorage from './cloud-storage';
-import { getClock } from './crdt';
import * as db from './db';
import { runMutator } from './mutators';
import * as prefs from './prefs';
@@ -175,6 +176,9 @@ handlers['api/download-budget'] = async function ({ syncId, password }) {
}
} else {
let files = await handlers['get-remote-files']();
+ if (!files) {
+ throw new Error('Could not get remote files');
+ }
let file = files.find(f => f.groupId === syncId);
if (!file) {
throw new Error(
diff --git a/packages/loot-core/src/server/aql/compiler.ts b/packages/loot-core/src/server/aql/compiler.ts
index ddd858b05e7..4898e9715db 100644
--- a/packages/loot-core/src/server/aql/compiler.ts
+++ b/packages/loot-core/src/server/aql/compiler.ts
@@ -19,7 +19,7 @@ function dateToInt(date) {
return parseInt(date.replace(/-/g, ''));
}
-export function addTombstone(schema, tableName, tableId, whereStr) {
+function addTombstone(schema, tableName, tableId, whereStr) {
let hasTombstone = schema[tableName].tombstone != null;
return hasTombstone ? `${whereStr} AND ${tableId}.tombstone = 0` : whereStr;
}
@@ -1118,7 +1118,3 @@ export function generateSQLWithState(
let { sqlPieces, state } = compileQuery(queryState, schema, schemaConfig);
return { sql: defaultConstructQuery(queryState, state, sqlPieces), state };
}
-
-export function generateSQL(queryState) {
- return generateSQLWithState(queryState).sql;
-}
diff --git a/packages/loot-core/src/server/aql/schema/executors.test.ts b/packages/loot-core/src/server/aql/schema/executors.test.ts
index 4b03c8c64d0..720fc932a02 100644
--- a/packages/loot-core/src/server/aql/schema/executors.test.ts
+++ b/packages/loot-core/src/server/aql/schema/executors.test.ts
@@ -1,9 +1,9 @@
+import { setClock } from '@actual-app/crdt';
import fc from 'fast-check';
import * as arbs from '../../../mocks/arbitrary-schema';
import query from '../../../shared/query';
import { groupById } from '../../../shared/util';
-import { setClock } from '../../crdt';
import * as db from '../../db';
import { batchMessages, setSyncingMode } from '../../sync/index';
diff --git a/packages/loot-core/src/server/aql/schema/executors.ts b/packages/loot-core/src/server/aql/schema/executors.ts
index 56493a0bd07..f6fa0d17d5a 100644
--- a/packages/loot-core/src/server/aql/schema/executors.ts
+++ b/packages/loot-core/src/server/aql/schema/executors.ts
@@ -6,7 +6,7 @@ import { convertOutputType } from '../schema-helpers';
// Transactions executor
-export function toGroup(parents, children, mapper = x => x) {
+function toGroup(parents, children, mapper = x => x) {
return parents.reduce((list, parent) => {
let childs = children.get(parent.id) || [];
list.push({
diff --git a/packages/loot-core/src/server/backups.ts b/packages/loot-core/src/server/backups.ts
index 0766ee67008..2919b66df7c 100644
--- a/packages/loot-core/src/server/backups.ts
+++ b/packages/loot-core/src/server/backups.ts
@@ -14,7 +14,7 @@ import * as prefs from './prefs';
const LATEST_BACKUP_FILENAME = 'db.latest.sqlite';
let serviceInterval = null;
-export async function getBackups(id) {
+async function getBackups(id) {
const budgetDir = fs.getBudgetDir(id);
const backupDir = fs.join(budgetDir, 'backups');
@@ -46,7 +46,7 @@ export async function getBackups(id) {
return backups;
}
-export async function getLatestBackup(id) {
+async function getLatestBackup(id) {
const budgetDir = fs.getBudgetDir(id);
if (await fs.exists(fs.join(budgetDir, LATEST_BACKUP_FILENAME))) {
return {
diff --git a/packages/loot-core/src/server/budget/actions.ts b/packages/loot-core/src/server/budget/actions.ts
index e26536deb55..2b0c527e487 100644
--- a/packages/loot-core/src/server/budget/actions.ts
+++ b/packages/loot-core/src/server/budget/actions.ts
@@ -122,7 +122,7 @@ export async function copyPreviousMonth({ month }) {
let table = getBudgetTable();
let budgetData = await getBudgetData(table, prevMonth);
- await batchMessages(() => {
+ await batchMessages(async () => {
budgetData.forEach(prevBudget => {
if (prevBudget.is_income === 1 && !isReflectBudget()) {
return;
@@ -141,7 +141,7 @@ export async function setZero({ month }) {
'SELECT * FROM v_categories WHERE tombstone = 0',
);
- await batchMessages(() => {
+ await batchMessages(async () => {
categories.forEach(cat => {
if (cat.is_income === 1 && !isReflectBudget()) {
return;
@@ -266,7 +266,7 @@ export async function setCategoryCarryover({ startMonth, category, flag }) {
let table = getBudgetTable();
let months = getAllMonths(startMonth);
- await batchMessages(() => {
+ await batchMessages(async () => {
for (let month of months) {
setCarryover(table, category, dbMonth(month), flag);
}
diff --git a/packages/loot-core/src/server/cloud-storage.ts b/packages/loot-core/src/server/cloud-storage.ts
index 6c7cc80b5fc..6a4f0aceda5 100644
--- a/packages/loot-core/src/server/cloud-storage.ts
+++ b/packages/loot-core/src/server/cloud-storage.ts
@@ -348,11 +348,12 @@ export async function listRemoteFiles() {
},
});
} catch (e) {
- console.log('Error', e);
+ console.log('Unexpected error fetching file list from server', e);
return null;
}
if (res.status === 'error') {
+ console.log('Error fetching file list from server', res);
return null;
}
diff --git a/packages/loot-core/src/server/db/index.ts b/packages/loot-core/src/server/db/index.ts
index 9c7be1daeeb..ca27accf63b 100644
--- a/packages/loot-core/src/server/db/index.ts
+++ b/packages/loot-core/src/server/db/index.ts
@@ -1,3 +1,11 @@
+import {
+ makeClock,
+ setClock,
+ serializeClock,
+ deserializeClock,
+ makeClientId,
+ Timestamp,
+} from '@actual-app/crdt';
import LRU from 'lru-cache';
import * as fs from '../../platform/server/fs';
@@ -11,14 +19,6 @@ import {
convertForUpdate,
convertFromSelect,
} from '../aql';
-import {
- makeClock,
- setClock,
- serializeClock,
- deserializeClock,
- makeClientId,
- Timestamp,
-} from '../crdt';
import {
accountModel,
categoryModel,
@@ -491,7 +491,7 @@ export async function mergePayees(target, ids) {
}),
);
- return Promise.all(
+ await Promise.all(
ids.map(id =>
Promise.all([
update('payee_mapping', { id, targetId: target }),
@@ -583,7 +583,7 @@ export async function moveAccount(id, targetId) {
}
const { updates, sort_order } = shoveSortOrders(accounts, targetId);
- await batchMessages(() => {
+ await batchMessages(async () => {
for (let info of updates) {
update('accounts', info);
}
diff --git a/packages/loot-core/src/server/errors.ts b/packages/loot-core/src/server/errors.ts
index 3d1ddccc511..1450c447e66 100644
--- a/packages/loot-core/src/server/errors.ts
+++ b/packages/loot-core/src/server/errors.ts
@@ -56,7 +56,3 @@ export function FileDownloadError(reason, meta?) {
export function FileUploadError(reason, meta?) {
return { type: 'FileUploadError', reason, meta };
}
-
-export function isCodeError(err) {
- return err instanceof ReferenceError || err instanceof SyntaxError;
-}
diff --git a/packages/loot-core/src/server/main.test.ts b/packages/loot-core/src/server/main.test.ts
index 7aa48c5d8c8..bcb709f0953 100644
--- a/packages/loot-core/src/server/main.test.ts
+++ b/packages/loot-core/src/server/main.test.ts
@@ -1,3 +1,5 @@
+import { getClock, deserializeClock } from '@actual-app/crdt';
+
import { expectSnapshotWithDiffer } from '../mocks/util';
import * as connection from '../platform/server/connection';
import * as fs from '../platform/server/fs';
@@ -5,7 +7,6 @@ import * as monthUtils from '../shared/months';
import * as budgetActions from './budget/actions';
import * as budget from './budget/base';
-import { getClock, deserializeClock } from './crdt';
import * as db from './db';
import { handlers } from './main';
import {
diff --git a/packages/loot-core/src/server/main.ts b/packages/loot-core/src/server/main.ts
index f6a1e998c2f..cb8069813db 100644
--- a/packages/loot-core/src/server/main.ts
+++ b/packages/loot-core/src/server/main.ts
@@ -1,5 +1,6 @@
import './polyfills';
import * as injectAPI from '@actual-app/api/injected';
+import * as CRDT from '@actual-app/crdt';
import * as YNAB4 from '@actual-app/import-ynab4/importer';
import * as YNAB5 from '@actual-app/import-ynab5/importer';
@@ -37,16 +38,6 @@ import {
import budgetApp from './budget/app';
import * as budget from './budget/base';
import * as cloudStorage from './cloud-storage';
-import {
- getClock,
- setClock,
- makeClock,
- makeClientId,
- serializeClock,
- deserializeClock,
- Timestamp,
- merkle,
-} from './crdt';
import * as db from './db';
import * as mappings from './db/mappings';
import * as encryption from './encryption';
@@ -73,7 +64,6 @@ import {
repairSync,
} from './sync';
import * as syncMigrations from './sync/migrate';
-import * as SyncPb from './sync/proto/sync_pb';
import toolsApp from './tools/app';
import { withUndo, clearUndo, undo, redo } from './undo';
import { updateVersion } from './update';
@@ -927,7 +917,7 @@ handlers['account-close'] = mutator(async function ({
[id],
);
- await batchMessages(() => {
+ await batchMessages(async () => {
// TODO: what this should really do is send a special message that
// automatically marks the tombstone value for all transactions
// within an account... or something? This is problematic
@@ -1106,7 +1096,7 @@ handlers['accounts-sync'] = async function ({ id }) {
errors.push({
accountId: acct.id,
message:
- 'There was an internal error. Please get in touch https://actualbudget.github.io/docs/Contact for support.',
+ 'There was an internal error. Please get in touch https://actualbudget.org/contact for support.',
internal: err.stack,
});
@@ -1347,7 +1337,7 @@ handlers['nordigen-accounts-sync'] = async function ({ id }) {
errors.push({
accountId: acct.id,
message:
- 'There was an internal error. Please get in touch https://actualbudget.github.io/docs/Contact for support.',
+ 'There was an internal error. Please get in touch https://actualbudget.org/contact for support.',
internal: err.stack,
});
@@ -1485,14 +1475,12 @@ handlers['save-global-prefs'] = async function (prefs) {
handlers['load-global-prefs'] = async function () {
let [
[, floatingSidebar],
- [, seenTutorial],
[, maxMonths],
[, autoUpdate],
[, documentDir],
[, encryptKey],
] = await asyncStorage.multiGet([
'floating-sidebar',
- 'seen-tutorial',
'max-months',
'auto-update',
'document-dir',
@@ -1500,7 +1488,6 @@ handlers['load-global-prefs'] = async function () {
]);
return {
floatingSidebar: floatingSidebar === 'true' ? true : false,
- seenTutorial: seenTutorial === 'true' ? true : false,
maxMonths: stringToInteger(maxMonths || ''),
autoUpdate: autoUpdate == null || autoUpdate === 'true' ? true : false,
documentDir: documentDir || getDefaultDocumentDir(),
@@ -1776,6 +1763,8 @@ handlers['set-server-url'] = async function ({ url, validate = true }) {
if (url == null) {
await asyncStorage.removeItem('user-token');
} else {
+ url = url.replace(/\/+$/, '');
+
if (validate) {
// Validate the server is running
let { error } = await runHandler(handlers['subscribe-needs-bootstrap'], {
@@ -2049,11 +2038,6 @@ handlers['create-budget'] = async function ({
return {};
};
-handlers['set-tutorial-seen'] = async function () {
- await asyncStorage.setItem('seen-tutorial', 'true');
- return 'ok';
-};
-
handlers['import-budget'] = async function ({ filepath, type }) {
try {
if (!(await fs.exists(filepath))) {
@@ -2212,10 +2196,10 @@ async function loadBudget(id) {
//
// TODO: The client id should be stored elsewhere. It shouldn't
// work this way, but it's fine for now.
- getClock().timestamp.setNode(makeClientId());
+ CRDT.getClock().timestamp.setNode(CRDT.makeClientId());
await db.runQuery(
'INSERT OR REPLACE INTO messages_clock (id, clock) VALUES (1, ?)',
- [serializeClock(getClock())],
+ [CRDT.serializeClock(CRDT.getClock())],
);
await prefs.savePrefs({ resetClock: false });
@@ -2274,30 +2258,6 @@ async function loadBudget(id) {
return {};
}
-handlers['get-upgrade-notifications'] = async function () {
- let { id } = prefs.getPrefs();
- if (id === TEST_BUDGET_ID || id === DEMO_BUDGET_ID) {
- return [];
- }
-
- let types = ['schedules', 'repair-splits'];
- let unseen = [];
-
- for (let type of types) {
- let key = `notifications.${type}`;
- if (prefs.getPrefs()[key] == null) {
- unseen.push(type);
- }
- }
-
- return unseen;
-};
-
-handlers['seen-upgrade-notification'] = async function ({ type }) {
- let key = `notifications.${type}`;
- prefs.savePrefs({ [key]: true });
-};
-
handlers['upload-file-web'] = async function ({ filename, contents }) {
if (!Platform.isWeb) {
return null;
@@ -2500,15 +2460,7 @@ export const lib = {
db,
// Expose CRDT mechanisms so server can use them
- merkle,
- timestamp: {
- getClock,
- setClock,
- makeClock,
- makeClientId,
- serializeClock,
- deserializeClock,
- Timestamp,
- },
- SyncProtoBuf: SyncPb,
+ // Backwards compatability
+ ...CRDT,
+ timestamp: CRDT,
};
diff --git a/packages/loot-core/src/server/migrate/migrations.ts b/packages/loot-core/src/server/migrate/migrations.ts
index 46326e06264..e87794f0790 100644
--- a/packages/loot-core/src/server/migrate/migrations.ts
+++ b/packages/loot-core/src/server/migrate/migrations.ts
@@ -111,6 +111,10 @@ function checkDatabaseValidity(appliedIds, available) {
i >= available.length ||
appliedIds[i] !== getMigrationId(available[i])
) {
+ console.error('Database is out of sync with migrations:', {
+ appliedIds,
+ available,
+ });
throw new Error('out-of-sync-migrations');
}
}
diff --git a/packages/loot-core/src/server/mutators.ts b/packages/loot-core/src/server/mutators.ts
index a50472c63d3..bd5035084c7 100644
--- a/packages/loot-core/src/server/mutators.ts
+++ b/packages/loot-core/src/server/mutators.ts
@@ -84,11 +84,11 @@ export function disableGlobalMutations() {
function _runMutator Promise>(
func: T,
initialContext = {},
-) {
+): Promise>> {
currentContext = initialContext;
return func().finally(() => {
currentContext = null;
- }) as ReturnType;
+ }) as Promise>>;
}
// Type cast needed as TS looses types over nested generic returns
export const runMutator = sequential(_runMutator) as typeof _runMutator;
diff --git a/packages/loot-core/src/server/perf.ts b/packages/loot-core/src/server/perf.ts
deleted file mode 100644
index 6182c1f5a17..00000000000
--- a/packages/loot-core/src/server/perf.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-let enabled = false;
-let entries = {};
-let counters = {};
-
-export function reset() {
- entries = {};
- counters = {};
-}
-
-export function record(name) {
- const start = Date.now();
- return () => unrecord(name, start);
-}
-
-function unrecord(name, start) {
- const end = Date.now();
-
- if (enabled) {
- if (entries[name] == null) {
- entries[name] = [];
- }
- entries[name].push(end - start);
- }
-}
-
-export function increment(name) {
- if (enabled) {
- if (counters[name] == null) {
- counters[name] = 0;
- }
- counters[name]++;
- }
-}
-
-export function start() {
- enabled = true;
-}
-
-export function stop() {
- enabled = false;
-
- console.log('~~ PERFORMANCE REPORT ~~');
- for (let name in entries) {
- const records = entries[name];
- const total = records.reduce((total, n) => total + n / 1000, 0);
- const avg = total / records.length;
-
- console.log(
- `[${name}] count: ${records.length} total: ${total}s avg: ${avg}`,
- );
- }
-
- for (let name in counters) {
- console.log(`[${name}] ${counters[name]}`);
- }
- console.log('~~ END REPORT ~~');
-
- reset();
-}
diff --git a/packages/loot-core/src/server/prefs.ts b/packages/loot-core/src/server/prefs.ts
index 43d89db2e44..46ced65dda1 100644
--- a/packages/loot-core/src/server/prefs.ts
+++ b/packages/loot-core/src/server/prefs.ts
@@ -1,6 +1,7 @@
+import { Timestamp } from '@actual-app/crdt';
+
import * as fs from '../platform/server/fs';
-import { Timestamp } from './crdt';
import { sendMessages } from './sync';
let prefs = null;
@@ -22,6 +23,19 @@ export async function loadPrefs(id?) {
prefs = { id, budgetName: id };
}
+ // delete released feature flags
+ let releasedFeatures = ['syncAccount'];
+ for (const feature of releasedFeatures) {
+ delete prefs[`flags.${feature}`];
+ }
+
+ // delete legacy notifications
+ for (const key of Object.keys(prefs)) {
+ if (key.startsWith('notifications.')) {
+ delete prefs[key];
+ }
+ }
+
// No matter what is in `id` field, force it to be the current id.
// This makes it resilient to users moving around folders, etc
prefs.id = id;
@@ -68,14 +82,7 @@ export function getPrefs() {
}
export function getDefaultPrefs(id, budgetName) {
- // Add any notifications in here that new users shouldn't see.
- // Without them, a popup will show to explain a new feature.
- return {
- id,
- budgetName,
- 'notifications.schedules': true,
- 'notifications.repair-splits': true,
- };
+ return { id, budgetName };
}
export async function readPrefs(id) {
diff --git a/packages/loot-core/src/server/schedules/app.ts b/packages/loot-core/src/server/schedules/app.ts
index 962a9be5fad..c2f3cfe0775 100644
--- a/packages/loot-core/src/server/schedules/app.ts
+++ b/packages/loot-core/src/server/schedules/app.ts
@@ -100,7 +100,7 @@ export async function getRuleForSchedule(id) {
return getRules().find(rule => rule.id === ruleId);
}
-export async function fixRuleForSchedule(id) {
+async function fixRuleForSchedule(id) {
let { data: ruleId } = await aqlQuery(
q('schedules').filter({ id }).calculate('rule'),
);
@@ -186,7 +186,7 @@ export async function setNextDate({
// Methods
-export async function checkIfScheduleExists(name, scheduleId) {
+async function checkIfScheduleExists(name, scheduleId) {
let idForName = await db.first('SELECT id from schedules WHERE name = ?', [
name,
]);
@@ -349,7 +349,7 @@ export async function deleteSchedule({ id }) {
});
}
-export async function skipNextDate({ id }) {
+async function skipNextDate({ id }) {
return setNextDate({
id,
start: nextDate => {
@@ -360,13 +360,13 @@ export async function skipNextDate({ id }) {
// `schedule` here might not be a saved schedule, so it might not have
// an id
-export function getPossibleTransactions({ schedule }) {}
+function getPossibleTransactions({ schedule }) {}
-export function discoverSchedules() {
+function discoverSchedules() {
return findSchedules();
}
-export async function getUpcomingDates({ config, count }) {
+async function getUpcomingDates({ config, count }) {
let rules = recurConfigToRSchedule(config);
try {
@@ -460,7 +460,7 @@ async function postTransactionForSchedule({ id }) {
// TODO: make this sequential
-export async function advanceSchedulesService(syncSuccess) {
+async function advanceSchedulesService(syncSuccess) {
// Move all paid schedules
let { data: schedules } = await aqlQuery(
q('schedules')
diff --git a/packages/loot-core/src/server/schedules/find-schedules.ts b/packages/loot-core/src/server/schedules/find-schedules.ts
index d812cf24576..31e6ece6e92 100644
--- a/packages/loot-core/src/server/schedules/find-schedules.ts
+++ b/packages/loot-core/src/server/schedules/find-schedules.ts
@@ -48,7 +48,7 @@ function getRank(day1, day2) {
return 1 / (dayDiff + 1);
}
-export function matchSchedules(allOccurs, config, partialMatchRank = 0.5) {
+function matchSchedules(allOccurs, config, partialMatchRank = 0.5) {
allOccurs = [...allOccurs].reverse();
let baseOccur = allOccurs[0];
let occurs = allOccurs.slice(1);
diff --git a/packages/loot-core/src/server/spreadsheet/globals.ts b/packages/loot-core/src/server/spreadsheet/globals.ts
index fcbfbc66251..45b65640961 100644
--- a/packages/loot-core/src/server/spreadsheet/globals.ts
+++ b/packages/loot-core/src/server/spreadsheet/globals.ts
@@ -1,12 +1,3 @@
-export function first(arr) {
- return arr[0];
-}
-
-export function firstValue(arr) {
- const keys = Object.keys(arr[0]);
- return arr[0][keys[0]];
-}
-
export function number(v) {
if (typeof v === 'number') {
return v;
@@ -20,11 +11,3 @@ export function number(v) {
return 0;
}
-
-export function min(x, y) {
- return Math.min(x, y);
-}
-
-export function max(x, y) {
- return Math.max(x, y);
-}
diff --git a/packages/loot-core/src/server/spreadsheet/new/REQUIREMENTS.txt b/packages/loot-core/src/server/spreadsheet/new/REQUIREMENTS.txt
deleted file mode 100644
index 4836a96ff50..00000000000
--- a/packages/loot-core/src/server/spreadsheet/new/REQUIREMENTS.txt
+++ /dev/null
@@ -1,33 +0,0 @@
-* Function calls (native hooks)
-* Operators: + - / * > < >= <= =
-* Queries: from t in transactions select { amount }
-* Types
-** Boolean (true / false)
-** Integer
-** Float
-** String
-* Variables (only global lookup)
-
-Need a stack to hold temporary values since function calls can be
-nested. Instructions:
-
-MOV
-CALL
-QUERY
-BOP
-UOP
-
-Registers:
-
-PC
-SP
-REG1
-
-Query language:
-
-=from transactions
- where
- date >= 20170101 and
- date <= 20170131 and
- category.is_income = 1
- calculate sum(amount)
diff --git a/packages/loot-core/src/server/spreadsheet/new/__snapshots__/compiler.test.ts.snap b/packages/loot-core/src/server/spreadsheet/new/__snapshots__/compiler.test.ts.snap
deleted file mode 100644
index b8d56b78d17..00000000000
--- a/packages/loot-core/src/server/spreadsheet/new/__snapshots__/compiler.test.ts.snap
+++ /dev/null
@@ -1,734 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Compiler basic 1`] = `
-Array [
- Array [
- Symbol(query),
- "
- SELECT sum(amount) FROM transactions
- LEFT JOIN category_mapping __cm ON __cm.id = transactions.category
- LEFT JOIN accounts t1 ON t1.id = transactions.acct
-LEFT JOIN banks t2 ON t2.id = t1.bank
- WHERE ((((date >= 20170101) and (date <= 20170131)) and (t2.name = 1))) AND transactions.isParent = 0 AND transactions.tombstone = 0
- ",
- true,
- ],
- Array [
- Symbol(mov),
- Object {
- "index": 1,
- "type": "__reg",
- },
- Object {
- "index": 0,
- "type": "__stack",
- },
- ],
- Array [
- Symbol(call),
- Object {
- "name": "generated!number",
- "type": "__var",
- },
- Array [
- Object {
- "index": 0,
- "type": "__stack",
- },
- ],
- ],
- Array [
- Symbol(mov),
- Object {
- "index": 1,
- "type": "__reg",
- },
- Object {
- "index": 0,
- "type": "__stack",
- },
- ],
- Array [
- Symbol(call),
- Object {
- "name": "generated!first",
- "type": "__var",
- },
- Array [
- Object {
- "index": 0,
- "type": "__stack",
- },
- ],
- ],
- Array [
- Symbol(mov),
- Object {
- "index": 1,
- "type": "__reg",
- },
- Object {
- "name": "generated!result",
- "type": "__var",
- },
- ],
-]
-`;
-
-exports[`Compiler basic 2`] = `
-Array [
- Array [
- Symbol(mov),
- "",
- Object {
- "name": "generated!result",
- "type": "__var",
- },
- ],
-]
-`;
-
-exports[`Compiler compiler binary ops 1`] = `
-Array [
- Array [
- Symbol(mov),
- Object {
- "name": "generated!bar",
- "type": "__var",
- },
- Object {
- "index": 1,
- "type": "__reg",
- },
- ],
- Array [
- Symbol(bop),
- "+",
- Object {
- "name": "generated!foo",
- "type": "__var",
- },
- Object {
- "index": 1,
- "type": "__reg",
- },
- ],
- Array [
- Symbol(mov),
- Object {
- "index": 1,
- "type": "__reg",
- },
- Object {
- "index": 0,
- "type": "__stack",
- },
- ],
- Array [
- Symbol(mov),
- Object {
- "name": "generated!baz",
- "type": "__var",
- },
- Object {
- "index": 1,
- "type": "__reg",
- },
- ],
- Array [
- Symbol(bop),
- "+",
- Object {
- "index": 0,
- "type": "__stack",
- },
- Object {
- "index": 1,
- "type": "__reg",
- },
- ],
- Array [
- Symbol(mov),
- Object {
- "index": 1,
- "type": "__reg",
- },
- Object {
- "index": 0,
- "type": "__stack",
- },
- ],
- Array [
- Symbol(mov),
- Object {
- "name": "generated!boo",
- "type": "__var",
- },
- Object {
- "index": 1,
- "type": "__reg",
- },
- ],
- Array [
- Symbol(bop),
- "+",
- Object {
- "index": 0,
- "type": "__stack",
- },
- Object {
- "index": 1,
- "type": "__reg",
- },
- ],
- Array [
- Symbol(mov),
- Object {
- "index": 1,
- "type": "__reg",
- },
- Object {
- "name": "generated!result",
- "type": "__var",
- },
- ],
-]
-`;
-
-exports[`Compiler compiler nested funcs 1`] = `
-Array [
- Array [
- Symbol(mov),
- 0,
- Object {
- "index": 1,
- "type": "__reg",
- },
- ],
- Array [
- Symbol(mov),
- Object {
- "index": 1,
- "type": "__reg",
- },
- Object {
- "index": 0,
- "type": "__stack",
- },
- ],
- Array [
- Symbol(mov),
- -20000,
- Object {
- "index": 1,
- "type": "__reg",
- },
- ],
- Array [
- Symbol(mov),
- Object {
- "index": 1,
- "type": "__reg",
- },
- Object {
- "index": 1,
- "type": "__stack",
- },
- ],
- Array [
- Symbol(call),
- Object {
- "name": "generated!number",
- "type": "__var",
- },
- Array [
- Object {
- "index": 1,
- "type": "__stack",
- },
- ],
- ],
- Array [
- Symbol(mov),
- Object {
- "index": 1,
- "type": "__reg",
- },
- Object {
- "index": 1,
- "type": "__stack",
- },
- ],
- Array [
- Symbol(call),
- Object {
- "name": "generated!min",
- "type": "__var",
- },
- Array [
- Object {
- "index": 0,
- "type": "__stack",
- },
- Object {
- "index": 1,
- "type": "__stack",
- },
- ],
- ],
- Array [
- Symbol(mov),
- Object {
- "index": 1,
- "type": "__reg",
- },
- Object {
- "name": "generated!result",
- "type": "__var",
- },
- ],
-]
-`;
-
-exports[`Compiler compiles boolean types 1`] = `
-Array [
- Array [
- Symbol(mov),
- true,
- Object {
- "index": 1,
- "type": "__reg",
- },
- ],
- Array [
- Symbol(mov),
- Object {
- "index": 1,
- "type": "__reg",
- },
- Object {
- "index": 0,
- "type": "__stack",
- },
- ],
- Array [
- Symbol(mov),
- 1,
- Object {
- "index": 1,
- "type": "__reg",
- },
- ],
- Array [
- Symbol(bop),
- "and",
- Object {
- "index": 0,
- "type": "__stack",
- },
- Object {
- "index": 1,
- "type": "__reg",
- },
- ],
- Array [
- Symbol(mov),
- Object {
- "index": 1,
- "type": "__reg",
- },
- Object {
- "index": 0,
- "type": "__stack",
- },
- ],
- Array [
- Symbol(jumpf),
- Object {
- "index": 0,
- "type": "__stack",
- },
- Object {
- "get": [Function],
- "resolve": [Function],
- },
- ],
- Array [
- Symbol(mov),
- 0,
- Object {
- "index": 1,
- "type": "__reg",
- },
- ],
- Array [
- Symbol(jumpt),
- Object {
- "index": 0,
- "type": "__stack",
- },
- Object {
- "get": [Function],
- "resolve": [Function],
- },
- ],
- Array [
- Symbol(mov),
- 1,
- Object {
- "index": 1,
- "type": "__reg",
- },
- ],
- Array [
- Symbol(mov),
- Object {
- "index": 1,
- "type": "__reg",
- },
- Object {
- "name": "generated!result",
- "type": "__var",
- },
- ],
-]
-`;
-
-exports[`Compiler complex query expressions 1`] = `
-Array [
- Array [
- Symbol(query),
- "
- SELECT substr(date,0,7), sum(amount) FROM transactions
- LEFT JOIN category_mapping __cm ON __cm.id = transactions.category
-
- WHERE transactions.isParent = 0 AND transactions.tombstone = 0
- GROUP BY substr(date,0,7)",
- false,
- ],
- Array [
- Symbol(mov),
- Object {
- "index": 1,
- "type": "__reg",
- },
- Object {
- "name": "generated!result",
- "type": "__var",
- },
- ],
-]
-`;
-
-exports[`Compiler field dependencies 1`] = `
-Array [
- "acct",
- "category",
- "description",
- "isParent",
- "tombstone",
- "date",
-]
-`;
-
-exports[`Compiler parens 1`] = `
-Array [
- Array [
- Symbol(mov),
- 1,
- Object {
- "index": 1,
- "type": "__reg",
- },
- ],
- Array [
- Symbol(mov),
- Object {
- "index": 1,
- "type": "__reg",
- },
- Object {
- "index": 0,
- "type": "__stack",
- },
- ],
- Array [
- Symbol(mov),
- 2,
- Object {
- "index": 1,
- "type": "__reg",
- },
- ],
- Array [
- Symbol(bop),
- "+",
- Object {
- "index": 0,
- "type": "__stack",
- },
- Object {
- "index": 1,
- "type": "__reg",
- },
- ],
- Array [
- Symbol(mov),
- Object {
- "index": 1,
- "type": "__reg",
- },
- Object {
- "name": "generated!result",
- "type": "__var",
- },
- ],
-]
-`;
-
-exports[`Compiler parens 2`] = `
-Array [
- Array [
- Symbol(mov),
- 1232,
- Object {
- "index": 1,
- "type": "__reg",
- },
- ],
- Array [
- Symbol(mov),
- Object {
- "index": 1,
- "type": "__reg",
- },
- Object {
- "index": 0,
- "type": "__stack",
- },
- ],
- Array [
- Symbol(mov),
- 2,
- Object {
- "index": 1,
- "type": "__reg",
- },
- ],
- Array [
- Symbol(bop),
- "+",
- Object {
- "index": 0,
- "type": "__stack",
- },
- Object {
- "index": 1,
- "type": "__reg",
- },
- ],
- Array [
- Symbol(mov),
- Object {
- "index": 1,
- "type": "__reg",
- },
- Object {
- "index": 0,
- "type": "__stack",
- },
- ],
- Array [
- Symbol(mov),
- 3,
- Object {
- "index": 1,
- "type": "__reg",
- },
- ],
- Array [
- Symbol(mov),
- Object {
- "index": 1,
- "type": "__reg",
- },
- Object {
- "index": 2,
- "type": "__stack",
- },
- ],
- Array [
- Symbol(mov),
- 4,
- Object {
- "index": 1,
- "type": "__reg",
- },
- ],
- Array [
- Symbol(bop),
- "+",
- Object {
- "index": 2,
- "type": "__stack",
- },
- Object {
- "index": 1,
- "type": "__reg",
- },
- ],
- Array [
- Symbol(bop),
- "-",
- Object {
- "index": 0,
- "type": "__stack",
- },
- Object {
- "index": 1,
- "type": "__reg",
- },
- ],
- Array [
- Symbol(mov),
- Object {
- "index": 1,
- "type": "__reg",
- },
- Object {
- "name": "generated!result",
- "type": "__var",
- },
- ],
-]
-`;
-
-exports[`Compiler query expressions 1`] = `
-Array [
- Array [
- Symbol(query),
- "
- SELECT sum(amount) as a FROM transactions
- LEFT JOIN category_mapping __cm ON __cm.id = transactions.category
-
- WHERE ((amount > 0)) AND transactions.isParent = 0 AND transactions.tombstone = 0
- ",
- false,
- ],
- Array [
- Symbol(mov),
- Object {
- "index": 1,
- "type": "__reg",
- },
- Object {
- "name": "generated!result",
- "type": "__var",
- },
- ],
-]
-`;
-
-exports[`Compiler query expressions with field remapping 1`] = `
-Array [
- Array [
- Symbol(query),
- "
- SELECT id FROM transactions
- LEFT JOIN category_mapping __cm ON __cm.id = transactions.category
-
- WHERE ((__cm.transferId = \\"50\\")) AND transactions.isParent = 0 AND transactions.tombstone = 0
- ",
- false,
- ],
- Array [
- Symbol(mov),
- Object {
- "index": 1,
- "type": "__reg",
- },
- Object {
- "name": "generated!result",
- "type": "__var",
- },
- ],
-]
-`;
-
-exports[`Compiler query expressions with field remapping 2`] = `
-Array [
- Array [
- Symbol(query),
- "
- SELECT id FROM transactions
- LEFT JOIN category_mapping __cm ON __cm.id = transactions.category
- LEFT JOIN categories t1 ON __cm.transferId = t1.id
- WHERE ((t1.name = \\"foo\\")) AND transactions.isParent = 0 AND transactions.tombstone = 0
- ",
- false,
- ],
- Array [
- Symbol(mov),
- Object {
- "index": 1,
- "type": "__reg",
- },
- Object {
- "name": "generated!result",
- "type": "__var",
- },
- ],
-]
-`;
-
-exports[`Compiler query expressions with field remapping 3`] = `
-Array [
- Array [
- Symbol(query),
- "
- SELECT id, t1.name FROM transactions
- LEFT JOIN category_mapping __cm ON __cm.id = transactions.category
- LEFT JOIN categories t1 ON __cm.transferId = t1.id
- WHERE ((__cm.transferId = \\"50\\")) AND transactions.isParent = 0 AND transactions.tombstone = 0
- ",
- false,
- ],
- Array [
- Symbol(mov),
- Object {
- "index": 1,
- "type": "__reg",
- },
- Object {
- "name": "generated!result",
- "type": "__var",
- },
- ],
-]
-`;
-
-exports[`Compiler query expressions with null 1`] = `
-Array [
- Array [
- Symbol(query),
- "
- SELECT count(amount) FROM transactions
- LEFT JOIN category_mapping __cm ON __cm.id = transactions.category
- LEFT JOIN accounts t1 ON t1.id = transactions.acct
- WHERE (((t1.offbudget = 0) and (__cm.transferId IS NULL))) AND transactions.isParent = 0 AND transactions.tombstone = 0
- ",
- true,
- ],
- Array [
- Symbol(mov),
- Object {
- "index": 1,
- "type": "__reg",
- },
- Object {
- "name": "generated!result",
- "type": "__var",
- },
- ],
-]
-`;
diff --git a/packages/loot-core/src/server/spreadsheet/new/__snapshots__/lexer.test.ts.snap b/packages/loot-core/src/server/spreadsheet/new/__snapshots__/lexer.test.ts.snap
deleted file mode 100644
index f3f36d7e4de..00000000000
--- a/packages/loot-core/src/server/spreadsheet/new/__snapshots__/lexer.test.ts.snap
+++ /dev/null
@@ -1,50 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`lexer basic 1`] = `
-Array [
- Object {
- "colno": 0,
- "lineno": 0,
- "type": "whitespace",
- "value": "
- ",
- },
- Object {
- "colno": 5,
- "lineno": 1,
- "type": "symbol",
- "value": "x",
- },
- Object {
- "colno": 6,
- "lineno": 1,
- "type": "whitespace",
- "value": " ",
- },
- Object {
- "colno": 7,
- "lineno": 1,
- "type": "operator",
- "value": "!=~",
- },
- Object {
- "colno": 10,
- "lineno": 1,
- "type": "whitespace",
- "value": " ",
- },
- Object {
- "colno": 11,
- "lineno": 1,
- "type": "int",
- "value": "4",
- },
- Object {
- "colno": 12,
- "lineno": 1,
- "type": "whitespace",
- "value": "
- ",
- },
-]
-`;
diff --git a/packages/loot-core/src/server/spreadsheet/new/__snapshots__/vm.test.ts.snap b/packages/loot-core/src/server/spreadsheet/new/__snapshots__/vm.test.ts.snap
deleted file mode 100644
index 663b19a0f4c..00000000000
--- a/packages/loot-core/src/server/spreadsheet/new/__snapshots__/vm.test.ts.snap
+++ /dev/null
@@ -1,15 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`vm basic 1`] = `
-Object {
- "firstValue": [Function],
- "generated!result": -6,
- "number": [Function],
-}
-`;
-
-exports[`vm boolean types 1`] = `
-Object {
- "generated!result": 0,
-}
-`;
diff --git a/packages/loot-core/src/server/spreadsheet/new/compiler.test.ts b/packages/loot-core/src/server/spreadsheet/new/compiler.test.ts
deleted file mode 100644
index b1796ebd0a1..00000000000
--- a/packages/loot-core/src/server/spreadsheet/new/compiler.test.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import { compile } from './compiler';
-
-describe('Compiler', () => {
- test('get-query', () => {
- compile(
- '=from transactions where acct.offbudget = 0 and category = null and (description.transfer_acct.offbudget = 1 or description.transfer_acct = null) calculate { count(date) }',
- );
- });
-
- test('basic', () => {
- let ops = compile(`
- =first(number(from transactions
- where
- date >= 20170101 and
- date <= 20170131 and
- acct.bank.name = 1
- calculate { sum(amount) }))
- `).ops;
- expect(ops).toMatchSnapshot();
-
- ops = compile('').ops;
- expect(ops).toMatchSnapshot();
- });
-
- test('parens', () => {
- let ops = compile('=(1 + 2)').ops;
- expect(ops).toMatchSnapshot();
-
- ops = compile('=(1232 + 2) - (3 + 4)').ops;
- expect(ops).toMatchSnapshot();
- });
-
- test('compiler binary ops', () => {
- let ops = compile('=foo + bar + baz + boo').ops;
- expect(ops).toMatchSnapshot();
- });
-
- test('compiler nested funcs', () => {
- let ops = compile('=min(0, number(-20000))').ops;
- expect(ops).toMatchSnapshot();
- });
-
- test('compiles boolean types', () => {
- let ops = compile('=if(true and 1) { 0 } else { 1 } ').ops;
- expect(ops).toMatchSnapshot();
- });
-
- test('query expressions', () => {
- let ops = compile(`
- =from transactions
- where amount > 0
- select { sum(amount) as a }
- `).ops;
- expect(ops).toMatchSnapshot();
- });
-
- test('query expressions with null', () => {
- let ops = compile(`
- =from transactions where acct.offbudget = 0 and category = null calculate { count(amount) }
- `).ops;
- expect(ops).toMatchSnapshot();
- });
-
- test('complex query expressions', () => {
- let ops = compile(`
- =from transactions groupby substr(date, 0, 7) select { substr(date, 0, 7), sum(amount) }
- `).ops;
- expect(ops).toMatchSnapshot();
- });
-
- test('query expressions with field remapping', () => {
- let ops = compile(`
- =from transactions where category = "50" select { id }
- `).ops;
- expect(ops).toMatchSnapshot();
-
- ops = compile(`
- =from transactions where category.name = "foo" select { id }
- `).ops;
- expect(ops).toMatchSnapshot();
-
- ops = compile(`
- =from transactions where category = "50" select { id, category.name }
- `).ops;
- expect(ops).toMatchSnapshot();
- });
-
- test('field dependencies', () => {
- let sqlDependencies = compile(
- '=from transactions where acct.offbudget = 0 and category = null and (description.transfer_acct.offbudget = 1 or description.transfer_acct = null) calculate { count(date) }',
- ).sqlDependencies;
-
- expect(sqlDependencies[0].fields).toMatchSnapshot();
- });
-});
diff --git a/packages/loot-core/src/server/spreadsheet/new/compiler.ts b/packages/loot-core/src/server/spreadsheet/new/compiler.ts
deleted file mode 100644
index cfcf8942622..00000000000
--- a/packages/loot-core/src/server/spreadsheet/new/compiler.ts
+++ /dev/null
@@ -1,193 +0,0 @@
-import getSqlFields from './get-sql-fields';
-import * as nodes from './nodes';
-import {
- MOV,
- CALL,
- QUERY,
- UOP,
- BOP,
- REG1,
- SP,
- VAR,
- JUMPF,
- JUMPT,
- LABEL,
-} from './ops';
-import parse from './parser';
-import generateSql from './sqlgen';
-
-class Compiler {
- src;
- scopeName;
- binding;
- ops;
- dependencies;
- sqlDependencies;
-
- constructor() {
- this.ops = [];
- this.dependencies = [];
- this.sqlDependencies = [];
- }
-
- fail(msg, lineno, colno) {
- const lines = this.src.split('\n');
-
- let space = '';
- for (let i = 0; i < colno; i++) {
- space += ' ';
- }
-
- throw new Error(
- `[${lineno + 1}, ${colno + 1}] ${msg}:\n${lines[lineno]}\n${space}^`,
- );
- }
-
- resolveVariable(name) {
- if (name.indexOf('!') === -1) {
- return this.scopeName + '!' + name;
- }
- return name;
- }
-
- maybePushStack(node, si) {
- if (node instanceof nodes.Symbol) {
- // There's no need to push anything to the stack since it's a
- // direct variable reference. Just store the referenced variable
- // and pop the symbol operation off the stack.
- const op = this.ops.pop();
- return [si, op[1]];
- }
-
- this.ops.push([MOV, REG1, SP(si)]);
- return [si + 1, SP(si)];
- }
-
- compileLiteral(node, si) {
- this.ops.push([MOV, node.value, REG1]);
- }
-
- compileSymbol(node, si) {
- const resolved = this.resolveVariable(node.value);
- this.dependencies.push(resolved);
- this.ops.push([MOV, VAR(resolved), REG1]);
- }
-
- compileBinOp(node, si) {
- this.compile(node.left, si);
- // TODO: Get rid of all this and add a second pass which optimizes
- // the opcodes.
- let left;
- [si, left] = this.maybePushStack(node.left, si);
-
- this.compile(node.right, si + 1);
- this.ops.push([BOP, node.op, left, REG1]);
- }
-
- compileUnaryOp(node, si) {
- this.compile(node.target, si);
- this.ops.push([UOP, node.op, REG1]);
- }
-
- compileFunCall(node, si) {
- this.compile(node.callee, si);
- let callee;
- [si, callee] = this.maybePushStack(node.callee, si);
-
- const args = node.args.children.map((arg, i) => {
- this.compile(arg, si + i);
- this.ops.push([MOV, REG1, SP(si + i)]);
- return SP(si + i);
- });
-
- this.ops.push([CALL, callee, args]);
- }
-
- compileQuery(node, si) {
- let fields = getSqlFields(node.table, node.where)
- .concat(getSqlFields(node.table, node.groupby))
- .concat(...node.select.map(s => getSqlFields(node.table, s.expr)));
-
- const { sql, where } = generateSql(
- node.table,
- node.where,
- node.groupby,
- node.select,
- );
-
- // TODO: This is a hack, but I'm pretty sure we can get rid of all
- // of this. Just need to think through it.
- fields = fields.map(f => (f === '__cm.transferId' ? 'category' : f));
-
- // Uniquify them
- fields = [...new Set(fields)];
-
- this.sqlDependencies.push({ table: node.table, where, fields });
- this.ops.push([QUERY, sql, node.calculated]);
- }
-
- compileIf(node, si) {
- const L0 = LABEL();
- const L1 = LABEL();
-
- this.compile(node.cond, si);
- this.ops.push([MOV, REG1, SP(si)]);
-
- this.ops.push([JUMPF, SP(si), L0]);
- this.compile(node.body, si + 1);
-
- this.ops.push([JUMPT, SP(si), L1]);
- L0.resolve(this.ops.length - 1);
- this.compile(node.else_, si + 1);
- L1.resolve(this.ops.length - 1);
- }
-
- compileRoot(node, si) {
- node.children.forEach(node => {
- this.compile(node, si);
- });
- }
-
- compile(node, si) {
- const method = this['compile' + node.getTypeName()];
- if (!method) {
- this.fail(
- 'Unknown node type: ' + node.getTypeName(),
- node.lineno,
- node.colno,
- );
- }
- return method.call(this, node, si);
- }
-
- compileSource(binding, scopeName, src) {
- this.src = src;
- this.scopeName = scopeName;
- this.binding = binding;
-
- this.compile(parse(src), 0);
-
- const resolvedBinding = this.resolveVariable(binding);
-
- if (this.ops.length !== 0) {
- this.ops.push([MOV, REG1, VAR(resolvedBinding)]);
- } else {
- this.ops.push([MOV, '', VAR(resolvedBinding)]);
- }
-
- return {
- ops: this.ops,
- dependencies: this.dependencies,
- sqlDependencies: this.sqlDependencies,
- };
- }
-}
-
-export function compile(src) {
- return compileBinding('result', 'generated', src);
-}
-
-export function compileBinding(binding, scopeName, src) {
- const compiler = new Compiler();
- return compiler.compileSource(binding, scopeName, src);
-}
diff --git a/packages/loot-core/src/server/spreadsheet/new/get-sql-fields.ts b/packages/loot-core/src/server/spreadsheet/new/get-sql-fields.ts
deleted file mode 100644
index ff71f705cf7..00000000000
--- a/packages/loot-core/src/server/spreadsheet/new/get-sql-fields.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-function traverse(expr, fields) {
- switch (expr.getTypeName()) {
- case 'FunCall':
- expr.args.children.map(arg => traverse(arg, fields));
- break;
-
- case 'Member':
- // Right now we only track dependencies on the top-level table,
- // and not any of the joined data. This tracks that field itself
- // that is joined on, but not the joined data yet.
- traverse(expr.object, fields);
- break;
-
- case 'Literal':
- break;
-
- case 'Symbol':
- if (fields.indexOf(expr.value) === -1 && expr.value !== 'null') {
- fields.push(expr.value);
- }
- break;
-
- case 'BinOp':
- traverse(expr.left, fields);
- traverse(expr.right, fields);
- break;
- default:
- throw new Error('Unhandled node type: ' + expr.getTypeName());
- }
-}
-
-export default function getSqlFields(table, ast) {
- let fields: string[] = [];
- if (!ast) {
- return fields;
- }
-
- traverse(ast, fields);
-
- // These are implicit fields added by the sql generator. Going to
- // revisit how to track all of this.
- if (table === 'transactions') {
- fields.push('isParent');
- fields.push('tombstone');
- }
-
- return fields;
-}
diff --git a/packages/loot-core/src/server/spreadsheet/new/lexer.test.ts b/packages/loot-core/src/server/spreadsheet/new/lexer.test.ts
deleted file mode 100644
index c1feaefd439..00000000000
--- a/packages/loot-core/src/server/spreadsheet/new/lexer.test.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import lex from './lexer';
-
-function getTokens(tokens) {
- const toks = [];
- while (!tokens.is_finished()) {
- toks.push(tokens.nextToken());
- }
- return toks;
-}
-
-test('lexer basic', () => {
- const tokens = lex(`
- =x !=~ 4
- `);
-
- expect(getTokens(tokens)).toMatchSnapshot();
-});
diff --git a/packages/loot-core/src/server/spreadsheet/new/lexer.ts b/packages/loot-core/src/server/spreadsheet/new/lexer.ts
deleted file mode 100644
index aabd626fef6..00000000000
--- a/packages/loot-core/src/server/spreadsheet/new/lexer.ts
+++ /dev/null
@@ -1,310 +0,0 @@
-const whitespaceChars = new Set(' \n\t\r\u00A0');
-const delimChars = new Set('()[]{}%*-+~/#,:|.<>=!');
-const whitespaceAndDelimChars = new Set([...whitespaceChars, ...delimChars]);
-const intChars = new Set('0123456789');
-
-const complexOps = new Set(['==', '!=', '<=', '>=', '=~', '!=~']);
-
-export const TOKEN_STRING = 'string';
-export const TOKEN_WHITESPACE = 'whitespace';
-export const TOKEN_LEFT_PAREN = 'left-paren';
-export const TOKEN_RIGHT_PAREN = 'right-paren';
-export const TOKEN_LEFT_BRACKET = 'left-bracket';
-export const TOKEN_RIGHT_BRACKET = 'right-bracket';
-export const TOKEN_LEFT_CURLY = 'left-curly';
-export const TOKEN_RIGHT_CURLY = 'right-curly';
-export const TOKEN_COMMA = 'comma';
-export const TOKEN_INT = 'int';
-export const TOKEN_FLOAT = 'float';
-export const TOKEN_BOOLEAN = 'boolean';
-export const TOKEN_SYMBOL = 'symbol';
-export const TOKEN_DOT = 'dot';
-export const TOKEN_EXCLAIM = 'exclaim';
-export const TOKEN_OPERATOR = 'operator';
-
-function token(type, value, lineno, colno) {
- return {
- type: type,
- value: value,
- lineno: lineno,
- colno: colno,
- };
-}
-
-class Tokenizer {
- colno;
- hasCheckedMode;
- index;
- len;
- lineno;
- str;
-
- constructor(str, opts = {}) {
- this.str = str;
- this.index = 0;
- this.len = str.length;
- this.lineno = 0;
- this.colno = 0;
- this.hasCheckedMode = false;
- }
-
- nextToken() {
- let lineno = this.lineno;
- let colno = this.colno;
- let tok;
- let cur = this.current();
-
- if (this.is_finished()) {
- return null;
- } else if ((tok = this._extract(whitespaceChars))) {
- // We hit some whitespace
- return token(TOKEN_WHITESPACE, tok, lineno, colno);
- } else if (!this.hasCheckedMode) {
- this.hasCheckedMode = true;
-
- if (cur === '=') {
- this.forward();
- cur = this.current();
- return this.nextToken();
- } else {
- this.index = this.str.length;
- return token(TOKEN_STRING, this.str, lineno, colno);
- }
- // eslint-disable-next-line rulesdir/typography
- } else if (cur === '"' || cur === "'") {
- // We've hit a string
- return token(TOKEN_STRING, this.parseString(cur), lineno, colno);
- } else if (delimChars.has(cur)) {
- // We've hit a delimiter (a special char like a bracket)
- let type;
-
- if (complexOps.has(cur + this.next() + this.next(2))) {
- cur = cur + this.next() + this.next(2);
- this.forward();
- this.forward();
- } else if (complexOps.has(cur + this.next())) {
- cur = cur + this.next();
- this.forward();
- }
- this.forward();
-
- switch (cur) {
- case '(':
- type = TOKEN_LEFT_PAREN;
- break;
- case ')':
- type = TOKEN_RIGHT_PAREN;
- break;
- case '[':
- type = TOKEN_LEFT_BRACKET;
- break;
- case ']':
- type = TOKEN_RIGHT_BRACKET;
- break;
- case '{':
- type = TOKEN_LEFT_CURLY;
- break;
- case '}':
- type = TOKEN_RIGHT_CURLY;
- break;
- case ',':
- type = TOKEN_COMMA;
- break;
- case '.':
- type = TOKEN_DOT;
- break;
- case '!':
- type = TOKEN_EXCLAIM;
- break;
- default:
- type = TOKEN_OPERATOR;
- }
-
- return token(type, cur, lineno, colno);
- } else {
- // We are not at whitespace or a delimiter, so extract the
- // text and parse it
- tok = this._extractUntil(whitespaceAndDelimChars);
-
- if (tok.match(/^[-+]?[0-9]+$/)) {
- if (this.current() === '.') {
- this.forward();
- let dec = this._extract(intChars);
- return token(TOKEN_FLOAT, tok + '.' + dec, lineno, colno);
- } else {
- return token(TOKEN_INT, tok, lineno, colno);
- }
- } else if (tok.match(/^(true|false)$/)) {
- return token(TOKEN_BOOLEAN, tok, lineno, colno);
- } else if (tok.match(/^(or|and|not)$/)) {
- return token(TOKEN_OPERATOR, tok, lineno, colno);
- } else if (tok) {
- return token(TOKEN_SYMBOL, tok, lineno, colno);
- } else {
- throw new Error('Unexpected value while parsing: ' + tok);
- }
- }
- }
-
- parseString(delimiter) {
- this.forward();
-
- let str = '';
-
- while (!this.is_finished() && this.current() !== delimiter) {
- let cur = this.current();
-
- if (cur === '\\') {
- this.forward();
- switch (this.current()) {
- case 'n':
- str += '\n';
- break;
- case 't':
- str += '\t';
- break;
- case 'r':
- str += '\r';
- break;
- default:
- str += this.current();
- }
- this.forward();
- } else {
- str += cur;
- this.forward();
- }
- }
-
- this.forward();
- return str;
- }
-
- _matches(str) {
- if (this.index + str.length > this.len) {
- return null;
- }
-
- let m = this.str.slice(this.index, this.index + str.length);
- return m === str;
- }
-
- _extractString(str) {
- if (this._matches(str)) {
- this.index += str.length;
- return str;
- }
- return null;
- }
-
- _extractUntil(chars) {
- // Extract all non-matching chars, with the default matching set
- // to everything
- return this._extractMatching(true, chars || new Set());
- }
-
- _extract(chars) {
- // Extract all matching chars (no default, so charString must be
- // explicit)
- return this._extractMatching(false, chars);
- }
-
- _extractMatching(breakOnMatch, chars) {
- // Pull out characters until a breaking char is hit.
- // If breakOnMatch is false, a non-matching char stops it.
- // If breakOnMatch is true, a matching char stops it.
-
- if (this.is_finished()) {
- return null;
- }
-
- let matches = chars.has(this.current());
-
- // Only proceed if the first character meets our condition
- if ((breakOnMatch && !matches) || (!breakOnMatch && matches)) {
- let t = this.current();
- this.forward();
-
- // And pull out all the chars one at a time until we hit a
- // breaking char
- let isMatch = chars.has(this.current());
-
- while (
- ((breakOnMatch && !isMatch) || (!breakOnMatch && isMatch)) &&
- !this.is_finished()
- ) {
- t += this.current();
- this.forward();
-
- isMatch = chars.has(this.current());
- }
-
- return t;
- }
-
- return '';
- }
-
- is_finished() {
- return this.index >= this.len;
- }
-
- forward() {
- this.index++;
-
- if (this.previous() === '\n') {
- this.lineno++;
- this.colno = 0;
- } else {
- this.colno++;
- }
- }
-
- back() {
- this.index--;
-
- if (this.current() === '\n') {
- this.lineno--;
-
- let idx = this.str.lastIndexOf('\n', this.index - 1);
- if (idx === -1) {
- this.colno = this.index;
- } else {
- this.colno = this.index - idx;
- }
- } else {
- this.colno--;
- }
- }
-
- // current returns current character
- current() {
- if (!this.is_finished()) {
- return this.str.charAt(this.index);
- }
- return '';
- }
-
- next(idx = 1) {
- if (this.index + idx < this.str.length) {
- return this.str.charAt(this.index + idx);
- }
- return '';
- }
-
- // currentStr returns what's left of the unparsed string
- currentStr() {
- if (!this.is_finished()) {
- return this.str.substr(this.index);
- }
- return '';
- }
-
- previous() {
- return this.str.charAt(this.index - 1);
- }
-}
-
-export default function lex(src, opts?: Record) {
- return new Tokenizer(src, opts);
-}
diff --git a/packages/loot-core/src/server/spreadsheet/new/nodes.ts b/packages/loot-core/src/server/spreadsheet/new/nodes.ts
deleted file mode 100644
index 54fa0603a81..00000000000
--- a/packages/loot-core/src/server/spreadsheet/new/nodes.ts
+++ /dev/null
@@ -1,201 +0,0 @@
-class Node {
- colno;
- fieldNames;
- lineno;
-
- constructor(lineno, colno, fieldNames) {
- this.lineno = lineno;
- this.colno = colno;
- this.fieldNames = fieldNames;
- }
-
- getTypeName() {
- return 'Node';
- }
-
- traverseFields(onEnter, onExit) {
- const fieldNames = this.fieldNames;
- for (let i = 0; i < fieldNames.length; i++) {
- const val = this[fieldNames[i]];
-
- if (val instanceof Node) {
- const ret = val.traverse(onEnter, onExit);
- if (ret) {
- this[fieldNames[i]] = ret;
- }
- }
- }
- }
-
- traverse(onEnter, onExit) {
- if (onEnter) {
- const val = onEnter(this);
- if (val === true) {
- return;
- } else if (val != null) {
- return val;
- }
- }
- this.traverseFields(onEnter, onExit);
- onExit && onExit(this);
- }
-
- copy() {
- const inst = Object.assign(
- Object.create(Object.getPrototypeOf(this)),
- this,
- );
-
- for (let i = 0; i < inst.fieldNames.length; i++) {
- const field = inst.fieldNames[i];
- if (inst[field] instanceof Node) {
- inst[field] = inst[field].copy();
- }
- }
-
- return inst;
- }
-}
-
-export class NodeList extends Node {
- children;
-
- constructor(lineno, colno, nodes: unknown[] = []) {
- super(lineno, colno, ['children']);
- this.children = nodes;
- }
-
- getTypeName() {
- return 'NodeList';
- }
-
- addChild(node) {
- this.children.push(node);
- }
-
- traverseFields(onEnter, onExit) {
- for (let i = 0; i < this.children.length; i++) {
- this.children[i].traverse(onEnter, onExit);
- }
- }
-}
-
-export class Root extends NodeList {
- getTypeName() {
- return 'Root';
- }
-}
-
-export class Value extends Node {
- value;
-
- constructor(lineno, colno, value) {
- super(lineno, colno, ['value']);
- this.value = value ?? null;
- }
- getTypeName() {
- return 'Value';
- }
-}
-export class UnaryOp extends Node {
- op;
- target;
-
- constructor(lineno, colno, op, target) {
- super(lineno, colno, ['op', 'target']);
- this.op = op ?? null;
- this.target = target ?? null;
- }
- getTypeName() {
- return 'UnaryOp';
- }
-}
-export class BinOp extends Node {
- op;
- left;
- right;
-
- constructor(lineno, colno, op, left, right) {
- super(lineno, colno, ['op', 'left', 'right']);
- this.op = op ?? null;
- this.left = left ?? null;
- this.right = right ?? null;
- }
- getTypeName() {
- return 'BinOp';
- }
-}
-
-export class Literal extends Value {
- getTypeName() {
- return 'Literal';
- }
-}
-export class Symbol extends Value {
- getTypeName() {
- return 'Symbol';
- }
-}
-export class FunCall extends Node {
- callee;
- args;
-
- constructor(lineno, colno, callee, args) {
- super(lineno, colno, ['callee', 'args']);
- this.callee = callee ?? null;
- this.args = args ?? null;
- }
- getTypeName() {
- return 'FunCall';
- }
-}
-
-export class Member extends Node {
- object;
- property;
-
- constructor(lineno, colno, object, property) {
- super(lineno, colno, ['object', 'property']);
- this.object = object ?? null;
- this.property = property ?? null;
- }
- getTypeName() {
- return 'Member';
- }
-}
-
-export class Query extends Node {
- table;
- select;
- where;
- groupby;
- calculated;
-
- constructor(lineno, colno, table, select, where, groupby, calculated) {
- super(lineno, colno, ['table', 'select', 'where', 'groupby', 'calculated']);
- this.table = table ?? null;
- this.select = select ?? null;
- this.where = where ?? null;
- this.groupby = groupby ?? null;
- this.calculated = calculated ?? null;
- }
- getTypeName() {
- return 'Query';
- }
-}
-
-export class If extends Node {
- cond;
- body;
- else_;
-
- constructor(lineno, colno, cond, body, else_) {
- super(lineno, colno, ['cond', 'body', 'else_']);
- this.cond = cond ?? null;
- this.body = body ?? null;
- this.else_ = else_ ?? null;
- }
- getTypeName() {
- return 'If';
- }
-}
diff --git a/packages/loot-core/src/server/spreadsheet/new/ops.ts b/packages/loot-core/src/server/spreadsheet/new/ops.ts
deleted file mode 100644
index 897139ad7e5..00000000000
--- a/packages/loot-core/src/server/spreadsheet/new/ops.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-export const MOV = Symbol('mov');
-export const CALL = Symbol('call');
-export const QUERY = Symbol('query');
-export const UOP = Symbol('uop');
-export const BOP = Symbol('bop');
-export const JUMPF = Symbol('jumpf');
-export const JUMPT = Symbol('jumpt');
-
-export const REG1 = { type: '__reg', index: 1 };
-
-export function SP(n) {
- return { type: '__stack', index: n };
-}
-
-export function VAR(name) {
- return { type: '__var', name: name };
-}
-
-export function LABEL() {
- let idx = null;
- return {
- get() {
- if (idx === null) {
- throw new Error('Attempted access of unresolved label');
- }
- return idx;
- },
-
- resolve(n) {
- idx = n;
- },
- };
-}
diff --git a/packages/loot-core/src/server/spreadsheet/new/parser.ts b/packages/loot-core/src/server/spreadsheet/new/parser.ts
deleted file mode 100644
index 635b783f0b4..00000000000
--- a/packages/loot-core/src/server/spreadsheet/new/parser.ts
+++ /dev/null
@@ -1,462 +0,0 @@
-import lex, * as types from './lexer';
-import * as nodes from './nodes';
-
-function nextToken(state, withWhitespace?: boolean) {
- let tok;
- let { peeked, tokens } = state;
-
- if (peeked) {
- if (!withWhitespace && peeked.type === types.TOKEN_WHITESPACE) {
- state.peeked = null;
- } else {
- tok = state.peeked;
- state.peeked = null;
- return tok;
- }
- }
-
- tok = tokens.nextToken();
-
- if (!withWhitespace) {
- while (tok && tok.type === types.TOKEN_WHITESPACE) {
- tok = tokens.nextToken();
- }
- }
-
- return tok;
-}
-
-function peekToken(state) {
- state.peeked = state.peeked || nextToken(state);
- return state.peeked;
-}
-
-function pushToken(state, tok) {
- if (state.peeked) {
- throw new Error('pushToken: can only push one token on between reads');
- }
- state.peeked = tok;
-}
-
-function fail(state, msg, lineno?: number, colno?: number) {
- if (!peekToken(state)) {
- throw new Error(msg + '\n\nSource:\n' + state.src + '\n');
- }
- if (lineno === undefined || colno === undefined) {
- const tok = peekToken(state);
- lineno = tok.lineno;
- colno = tok.colno;
- }
-
- const lines = state.src.split('\n');
-
- let space = '';
- for (let i = 0; i < colno; i++) {
- space += ' ';
- }
-
- throw new Error(
- `[${lineno + 1}, ${colno + 1}] ${msg}:\n${lines[lineno]}\n${space}^`,
- );
-}
-
-function skip(state, type) {
- let tok = nextToken(state);
- if (!tok || tok.type !== type) {
- pushToken(state, tok);
- return false;
- }
- return true;
-}
-
-function expectValue(state, type, value) {
- let tok = nextToken(state);
- if (tok.type !== type || tok.value !== value) {
- fail(
- state,
- 'expected ' + value + ', got ' + tok.value,
- tok.lineno,
- tok.colno,
- );
- }
- return tok;
-}
-
-function expect(state, type) {
- let tok = nextToken(state);
- if (tok.type !== type) {
- fail(
- state,
- 'expected ' + type + ', got ' + tok.type,
- tok.lineno,
- tok.colno,
- );
- }
- return tok;
-}
-
-function skipValue(state, type, val) {
- let tok = nextToken(state);
- if (!tok || tok.type !== type || tok.value !== val) {
- pushToken(state, tok);
- return false;
- }
- return true;
-}
-
-function skipSymbol(state, val) {
- return skipValue(state, types.TOKEN_SYMBOL, val);
-}
-
-function parseExpression(state) {
- return parseOr(state);
-}
-
-function parseOr(state) {
- let left = parseAnd(state);
- while (skipValue(state, types.TOKEN_OPERATOR, 'or')) {
- const right = parseAnd(state);
- left = new nodes.BinOp(left.lineno, left.colno, 'or', left, right);
- }
- return left;
-}
-
-function parseAnd(state) {
- let left = parseNot(state);
- while (skipValue(state, types.TOKEN_OPERATOR, 'and')) {
- const right = parseNot(state);
- left = new nodes.BinOp(left.lineno, left.colno, 'and', left, right);
- }
- return left;
-}
-
-function parseNot(state) {
- let left = parseCompare(state);
- while (skipValue(state, types.TOKEN_OPERATOR, 'not')) {
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const right = parseCompare(state);
- left = new nodes.UnaryOp(left.lineno, left.colno, 'not', parseNot(state));
- }
- return left;
-}
-
-function parseCompare(state) {
- let compareOps = ['=', '!=', '<', '>', '<=', '>=', '=~', '!=~'];
- let node = parseAdd(state);
-
- while (1) {
- let tok = nextToken(state);
-
- if (!tok) {
- break;
- } else if (compareOps.indexOf(tok.value) !== -1) {
- node = new nodes.BinOp(
- tok.lineno,
- tok.colno,
- tok.value,
- node,
- parseAdd(state),
- );
- } else {
- pushToken(state, tok);
- break;
- }
- }
-
- return node;
-}
-
-function parseAdd(state) {
- let left = parseSub(state);
- while (skipValue(state, types.TOKEN_OPERATOR, '+')) {
- const right = parseSub(state);
- left = new nodes.BinOp(left.lineno, left.colno, '+', left, right);
- }
- return left;
-}
-
-function parseSub(state) {
- let left = parseMul(state);
- while (skipValue(state, types.TOKEN_OPERATOR, '-')) {
- const right = parseMul(state);
- left = new nodes.BinOp(left.lineno, left.colno, '-', left, right);
- }
- return left;
-}
-
-function parseMul(state) {
- let left = parseDiv(state);
- while (skipValue(state, types.TOKEN_OPERATOR, '*')) {
- const right = parseDiv(state);
- left = new nodes.BinOp(left.lineno, left.colno, '*', left, right);
- }
- return left;
-}
-
-function parseDiv(state) {
- let left = parseUnary(state);
- while (skipValue(state, types.TOKEN_OPERATOR, '/')) {
- const right = parseUnary(state);
- left = new nodes.BinOp(left.lineno, left.colno, '/', left, right);
- }
- return left;
-}
-
-function parseUnary(state) {
- let tok = peekToken(state);
-
- if (skipValue(state, types.TOKEN_OPERATOR, '-')) {
- const nextTok = peekToken(state);
- if (nextTok.type === types.TOKEN_INT) {
- const number = parseInt(nextToken(state).value);
- return new nodes.Literal(tok.lineno, tok.colno, -number);
- } else if (nextTok.type === types.TOKEN_FLOAT) {
- const number = parseFloat(nextToken(state).value);
- return new nodes.Literal(tok.lineno, tok.colno, -number);
- }
-
- return new nodes.UnaryOp(tok.lineno, tok.colno, '-', parseUnary(state));
- }
- return parsePrimary(state);
-}
-
-function parsePrimary(state) {
- let tok = nextToken(state);
- let val: number | boolean | null = null;
-
- if (!tok) {
- fail(state, 'expected expression, got end of file');
- } else if (tok.type === types.TOKEN_STRING) {
- val = tok.value;
- } else if (tok.type === types.TOKEN_INT) {
- val = parseInt(tok.value, 10);
- } else if (tok.type === types.TOKEN_FLOAT) {
- val = parseFloat(tok.value);
- } else if (tok.type === types.TOKEN_BOOLEAN) {
- if (tok.value === 'true') {
- val = true;
- } else if (tok.value === 'false') {
- val = false;
- }
- }
-
- if (val !== null) {
- return new nodes.Literal(tok.lineno, tok.colno, val);
- } else if (tok.type === types.TOKEN_SYMBOL) {
- if (tok.value === 'from') {
- return parseQueryExpression(state);
- } else if (tok.value === 'if') {
- return parseIfExpression(state);
- }
-
- return parsePostfix(
- state,
- new nodes.Symbol(tok.lineno, tok.colno, tok.value),
- );
- } else if (tok.type === types.TOKEN_LEFT_PAREN) {
- const node = parseExpression(state);
- expect(state, types.TOKEN_RIGHT_PAREN);
- return node;
- }
-
- fail(state, 'Unexpected token: ' + tok.value, tok.lineno, tok.colno);
-}
-
-function parseIfExpression(state) {
- const tok = expect(state, types.TOKEN_LEFT_PAREN);
- const cond = parseExpression(state);
- expect(state, types.TOKEN_RIGHT_PAREN);
-
- expect(state, types.TOKEN_LEFT_CURLY);
- const body = parseExpression(state);
- expect(state, types.TOKEN_RIGHT_CURLY);
-
- let else_;
- if (skipSymbol(state, 'else')) {
- expect(state, types.TOKEN_LEFT_CURLY);
- else_ = parseExpression(state);
- expect(state, types.TOKEN_RIGHT_CURLY);
- }
-
- return new nodes.If(tok.lineno, tok.colno, cond, body, else_);
-}
-
-function parseQueryExpression(state) {
- // The `from` keyword has already been parsed
- const tok = expect(state, types.TOKEN_SYMBOL);
- const table = tok.value;
-
- let where = null;
- if (skipSymbol(state, 'where')) {
- where = parseQuerySubExpression(state);
- }
-
- let groupby = null;
- if (skipSymbol(state, 'groupby')) {
- groupby = parseQuerySubExpression(state);
- }
-
- let select: Array<{ expr: unknown; as?: unknown }> = [];
- let calculated;
-
- if (skipSymbol(state, 'select')) {
- let checkComma = false;
- calculated = false;
-
- expectValue(state, types.TOKEN_LEFT_CURLY, '{');
-
- while (!skipValue(state, types.TOKEN_RIGHT_CURLY, '}')) {
- const tok = peekToken(state);
-
- if (checkComma && !skip(state, types.TOKEN_COMMA)) {
- fail(
- state,
- 'Unexpected token in query select: ' + tok.value,
- tok.lineno,
- tok.colno,
- );
- }
-
- const expr = parseQuerySubExpression(state);
- let as = null;
-
- if (skipSymbol(state, 'as')) {
- const tok = expect(state, types.TOKEN_SYMBOL);
- as = tok.value;
- }
-
- select.push({ expr, as });
-
- checkComma = true;
- }
- } else if (skipSymbol(state, 'calculate')) {
- calculated = true;
-
- expectValue(state, types.TOKEN_LEFT_CURLY, '{');
- select.push({ expr: parseQuerySubExpression(state) });
-
- if (!skipValue(state, types.TOKEN_RIGHT_CURLY, '}')) {
- fail(state, 'Only one expression allowed for `calculate`');
- }
- } else {
- fail(state, 'Expected either the `select` or `calculate` keyword');
- }
-
- return new nodes.Query(
- tok.lineno,
- tok.colno,
- table,
- select,
- where,
- groupby,
- calculated,
- );
-}
-
-function parseQuerySubExpression(state) {
- const node = parseExpression(state);
- return node;
-}
-
-function parsePostfix(state, node) {
- let tok;
-
- while ((tok = nextToken(state))) {
- if (tok.type === types.TOKEN_LEFT_PAREN) {
- pushToken(state, tok);
- let args = parseArgs(state);
- node = new nodes.FunCall(tok.lineno, tok.colno, node, args);
- } else if (tok.type === types.TOKEN_DOT) {
- const val = nextToken(state);
- node = new nodes.Member(
- tok.lineno,
- tok.colno,
- node,
- new nodes.Literal(val.lineno, val.colno, val.value),
- );
- } else if (tok.type === types.TOKEN_EXCLAIM) {
- const name = nextToken(state);
- if (name.type !== types.TOKEN_SYMBOL) {
- fail(
- state,
- 'Expected cell name in sheet reference',
- name.lineno,
- name.colno,
- );
- }
-
- return new nodes.Symbol(
- node.lineno,
- node.colno,
- node.value + '!' + name.value,
- );
- } else {
- pushToken(state, tok);
- break;
- }
- }
-
- return node;
-}
-
-function parseArgs(state) {
- let tok = peekToken(state);
-
- if (tok.type !== types.TOKEN_LEFT_PAREN) {
- fail(state, 'Expected arguments', tok.lineno, tok.colno);
- }
-
- nextToken(state);
-
- let args = new nodes.NodeList(tok.lineno, tok.colno);
- let checkComma = false;
-
- while (1) {
- tok = peekToken(state);
- if (tok.type === types.TOKEN_RIGHT_PAREN) {
- nextToken(state);
- break;
- }
-
- if (checkComma && !skip(state, types.TOKEN_COMMA)) {
- fail(
- state,
- 'Expected comma after function argument',
- tok.lineno,
- tok.colno,
- );
- }
-
- args.addChild(parseExpression(state));
- checkComma = true;
- }
-
- return args;
-}
-
-export default function parse(src) {
- let state = {
- src: src,
- tokens: lex(src),
- peeked: null,
- };
-
- if (state.tokens.is_finished()) {
- // If it's an empty string, return nothing
- return new nodes.Root(0, 0, []);
- } else {
- const expr = parseExpression(state);
-
- const tok = nextToken(state);
- if (tok) {
- fail(
- state,
- 'Unexpected token after expression: ' + tok.value,
- tok.lineno,
- tok.colno,
- );
- }
-
- return new nodes.Root(0, 0, [expr]);
- }
-}
diff --git a/packages/loot-core/src/server/spreadsheet/new/sqlconvert.ts b/packages/loot-core/src/server/spreadsheet/new/sqlconvert.ts
deleted file mode 100644
index 4316f1e6f94..00000000000
--- a/packages/loot-core/src/server/spreadsheet/new/sqlconvert.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { SCHEMA_PATHS } from './sqlgen';
-
-export default function convert(table, item) {
- if (SCHEMA_PATHS[table]) {
- let fields = SCHEMA_PATHS[table];
- let updates = {};
- Object.keys(item).forEach(k => {
- let mappedField = fields[k] && fields[k].field;
-
- if (mappedField) {
- updates[k] = item[mappedField];
- }
- });
-
- return { ...item, ...updates };
- }
- return item;
-}
diff --git a/packages/loot-core/src/server/spreadsheet/new/sqlgen.ts b/packages/loot-core/src/server/spreadsheet/new/sqlgen.ts
deleted file mode 100644
index 4e4cd8636c1..00000000000
--- a/packages/loot-core/src/server/spreadsheet/new/sqlgen.ts
+++ /dev/null
@@ -1,303 +0,0 @@
-import * as nodes from './nodes';
-
-type Lookup = { field: string; tableId?: string };
-
-let _uid = 0;
-function resetUid() {
- _uid = 0;
-}
-
-function uid() {
- _uid++;
- return 't' + _uid;
-}
-
-function fail(node, message) {
- const err = new Error(message);
- // @ts-expect-error We should use error.cause to pass node info
- err.node = node;
- throw err;
-}
-
-function generateExpression(expr) {
- if (typeof expr === 'string') {
- // eslint-disable-next-line rulesdir/typography
- return `"${expr}"`;
- } else if (typeof expr === 'number') {
- return expr;
- }
-
- switch (expr.getTypeName()) {
- case 'FunCall':
- return (
- generateExpression(expr.callee) +
- '(' +
- expr.args.children.map(node => generateExpression(node)).join(',') +
- ')'
- );
- case 'Member':
- return (
- generateExpression(expr.object) +
- '.' +
- generateExpression(expr.property)
- );
- case 'BinOp':
- const left = generateExpression(expr.left);
- let str;
-
- if (
- expr.op === '=' &&
- expr.right.getTypeName() === 'Symbol' &&
- expr.right.value === 'null'
- ) {
- str = left + ' IS NULL';
- } else {
- const right = generateExpression(expr.right);
-
- switch (expr.op) {
- case '=~':
- str = `${left} LIKE ${right}`;
- break;
- case '!=~':
- str = `${left} NOT LIKE ${right}`;
- break;
- default:
- str = `${left} ${expr.op} ${right}`;
- }
- }
-
- return '(' + str + ')';
- case 'Literal':
- if (typeof expr.value === 'string') {
- // eslint-disable-next-line rulesdir/typography
- return `"${expr.value}"`;
- }
- return expr.value;
- case 'Symbol':
- // if (expr.value.indexOf('!') !== -1) {
- // fail(expr, 'SQL variable cannot contain cell lookup');
- // }
- return expr.value;
- default:
- throw new Error('Unknown query node: ' + expr.getTypeName());
- }
-}
-
-function transformColumns(node, implicitTable) {
- let transformed = node.traverse(n => {
- if (n instanceof nodes.Symbol) {
- let table = implicitTable;
- let field = n.value;
-
- if (SCHEMA_PATHS[table] && SCHEMA_PATHS[table][field]) {
- let info = SCHEMA_PATHS[table][field];
- if (info.field) {
- // Map the field onto something else
- return new nodes.Symbol(n.lineno, n.colno, info.field);
- }
- }
- }
- });
-
- return transformed || node;
-}
-
-function transformLookups(node, implicitTable) {
- let paths: Lookup[][] = [];
-
- const transformed = node.traverse(n => {
- if (n instanceof nodes.Member) {
- let currentNode = n;
-
- let lookups: Lookup[] = [];
- while (currentNode instanceof nodes.Member) {
- if (!(currentNode.property instanceof nodes.Value)) {
- fail(currentNode, 'Invalid syntax for SQL reference');
- }
-
- lookups.push({ field: currentNode.property.value });
- currentNode = currentNode.object;
- }
-
- // @ts-expect-error Node refinement missing
- if (!(currentNode instanceof nodes.Symbol)) {
- fail(currentNode, 'Invalid syntax for SQL reference');
- }
- // @ts-expect-error Node refinement missing
- lookups.push({ field: currentNode.value });
- lookups.reverse();
-
- lookups = lookups.map((lookup, idx) => {
- return {
- field: lookup.field,
- tableId: uid(),
- };
- });
-
- let table = implicitTable;
-
- // Skip the last field as we don't want to resolve to that
- // table. The syntax to emit is `table.field`.
- for (let i = 0; i < lookups.length - 1; i++) {
- const lookup = lookups[i];
-
- if (!SCHEMA_PATHS[table]) {
- const err = new Error(
- `Table “${table}” not joinable for field “${lookup}”`,
- );
- // @ts-expect-error We should use error.cause to pass node info
- err.node = node;
- throw err;
- }
- if (!SCHEMA_PATHS[table][lookup.field]) {
- const err = new Error(
- `Unknown field “${lookup}” on table “${table}”`,
- );
- // @ts-expect-error We should use error.cause to pass node info
- err.node = node;
- throw err;
- }
-
- table = SCHEMA_PATHS[table][lookup.field].table;
- }
-
- paths.push(lookups);
-
- let tableId = lookups[lookups.length - 2].tableId;
- let field = lookups[lookups.length - 1].field;
-
- return new nodes.Member(
- node.lineno,
- node.colno,
- new nodes.Symbol(node.lineno, node.colno, tableId),
- new nodes.Symbol(node.lineno, node.colno, field),
- );
- }
- });
-
- return { paths, node: transformed || node };
-}
-
-export default function generate(table, where, groupby, select) {
- // Figure out the dep tables here. Return the SQL and dependent
- // tables
- let allPaths: Lookup[][] = [];
-
- resetUid();
-
- if (!tables[table]) {
- throw new Error('Table not found: ' + table);
- }
-
- const selectStr = select
- .map(s => {
- let { paths, node } = transformLookups(s.expr, table);
- let as = s.as;
- allPaths = allPaths.concat(paths);
-
- let newNode = transformColumns(node, table);
-
- // If the selected field was transformed, select it as the
- // original name
- if (node !== newNode && node instanceof nodes.Symbol && !as) {
- as = node.value;
- }
-
- const exprStr = generateExpression(newNode);
- return as ? `${exprStr} as ${as}` : exprStr;
- })
- .join(', ');
-
- let whereStr = '';
- let whereTransformed;
- if (where) {
- let { paths, node } = transformLookups(where, table);
- allPaths = allPaths.concat(paths);
- whereTransformed = node.copy();
-
- // Where clauses provide a special hook to map a column onto
- // something different, so you can represent something more
- // complex internally. You are still required to provide the
- // original name somehow; all other references use the original
- // name, so make sure you do `JOIN table ` or
- // something like that.
- node = transformColumns(node, table);
-
- whereStr = ' WHERE (' + generateExpression(node) + ')';
- }
-
- let groupByStr = '';
- if (groupby) {
- let { paths, node } = transformLookups(groupby, table);
- allPaths = allPaths.concat(paths);
- groupByStr = ' GROUP BY ' + generateExpression(node);
- }
-
- let dependencies: string[] = [];
- let joins: string[] = [];
-
- allPaths.forEach(path => {
- let currentTable = { name: table, id: table };
- for (let i = 0; i < path.length - 1; i++) {
- let lookup = path[i];
- let meta = SCHEMA_PATHS[currentTable.name][lookup.field];
-
- if (meta.sql) {
- joins.push(meta.sql(lookup.tableId));
- } else {
- joins.push(
- `LEFT JOIN ${meta.table} ${lookup.tableId} ON ${lookup.tableId}.id = ${currentTable.id}.${lookup.field}`,
- );
- }
-
- if (dependencies.indexOf(meta.table) === -1) {
- dependencies.push(meta.table);
- }
-
- currentTable = { name: meta.table, id: lookup.tableId };
- }
- });
-
- const sql =
- tables[table](selectStr, whereStr, joins.join('\n')) + ' ' + groupByStr;
-
- return {
- sql,
- where: whereTransformed,
- dependencies,
- };
-}
-
-export const SCHEMA_PATHS = {
- transactions: {
- category: {
- table: 'categories',
- sql: id => `LEFT JOIN categories ${id} ON __cm.transferId = ${id}.id`,
- field: '__cm.transferId',
- },
- acct: { table: 'accounts' },
- description: { table: 'payees' },
- },
- payees: {
- transfer_acct: { table: 'accounts' },
- },
- accounts: {
- bank: { table: 'banks' },
- },
-};
-
-const tables = {
- transactions: (select, where, join) => {
- // Never take into account parent split transactions. Their
- // children should sum up to be equal to it
- // prettier-ignore
- let whereStr = `${where === '' ? 'WHERE' : where + ' AND'} transactions.isParent = 0 AND transactions.tombstone = 0`;
-
- return `
- SELECT ${select} FROM transactions
- LEFT JOIN category_mapping __cm ON __cm.id = transactions.category
- ${join}
- ${whereStr}
- `;
- },
-};
diff --git a/packages/loot-core/src/server/spreadsheet/new/vm.test.ts b/packages/loot-core/src/server/spreadsheet/new/vm.test.ts
deleted file mode 100644
index c03fa636467..00000000000
--- a/packages/loot-core/src/server/spreadsheet/new/vm.test.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { unresolveName } from '../util';
-
-import VM from './vm';
-
-const db = {
- runQuery: sql => {
- return Promise.resolve([{ 'sum(t.amount)': 1000 }]);
- },
-};
-
-function makeScopes(vars) {
- return {
- getVariable: resolvedName => {
- const { name } = unresolveName(resolvedName);
-
- if (vars[resolvedName] !== undefined) {
- return vars[resolvedName];
- } else if (vars[name] !== undefined) {
- return vars[name];
- }
-
- throw new Error(`“${resolvedName}” is not defined`);
- },
-
- setVariable: (name, value) => {
- vars[name] = value;
- },
-
- getAll: () => vars,
- };
-}
-
-function run(src, vars = {}) {
- const scopes = makeScopes(vars);
- const vm = new VM(db, scopes);
-
- return new Promise(resolve => {
- vm.runSource(src, () => {
- expect(scopes.getAll()).toMatchSnapshot();
- resolve(undefined);
- });
- });
-}
-
-test('vm basic', async () => {
- return run(`=-(1 + 2 + 3)`, {
- number: x => {
- return x;
- },
- firstValue: arr => {
- return arr[0]['sum(t.amount)'];
- },
- });
-});
-
-test('vm boolean types', async () => {
- return run('=if(true and (1 + 2 + 3 - 5)) { 0 } else { 1 } ');
-});
diff --git a/packages/loot-core/src/server/spreadsheet/new/vm.ts b/packages/loot-core/src/server/spreadsheet/new/vm.ts
deleted file mode 100644
index 8fc2e0b32f6..00000000000
--- a/packages/loot-core/src/server/spreadsheet/new/vm.ts
+++ /dev/null
@@ -1,193 +0,0 @@
-import { compile } from './compiler';
-import { MOV, CALL, QUERY, UOP, BOP, JUMPF, JUMPT } from './ops';
-
-export default class VM {
- _onFinish;
- db;
- ops;
- paused;
- pc;
- reg1;
- scopes;
- stack;
-
- constructor(db, scopes) {
- this.stack = new Array(1000);
- this.reg1 = null;
- this.pc = 0;
- this.db = db;
- this.scopes = scopes;
- }
-
- get(ref) {
- if (ref && ref.type) {
- if (ref.type === '__reg') {
- return this.reg1;
- } else if (ref.type === '__var') {
- return this.scopes.getVariable(ref.name);
- } else if (ref.type === '__stack') {
- return this.stack[ref.index];
- }
- }
-
- return ref;
- }
-
- set(ref, value) {
- if (ref && ref.type) {
- if (ref.type === '__reg') {
- this.reg1 = this.get(value);
- } else if (ref.type === '__var') {
- this.scopes.setVariable(ref.name, this.get(value));
- } else if (ref.type === '__stack') {
- this.stack[ref.index] = this.get(value);
- }
- }
- }
-
- binaryOp(op, left, right) {
- switch (op) {
- case '+':
- // TODO: Enforce these to be numbers
- this.reg1 = this.get(left) + this.get(right);
- break;
- case '-':
- this.reg1 = this.get(left) - this.get(right);
- break;
- case '*':
- this.reg1 = this.get(left) * this.get(right);
- break;
- case '/':
- this.reg1 = this.get(left) / this.get(right);
- break;
- case 'and':
- this.reg1 = this.get(left) && this.get(right);
- break;
- case 'or':
- this.reg1 = this.get(left) || this.get(right);
- break;
- default:
- throw new Error('Unimplemented operator: ' + op);
- }
- }
-
- unaryOp(op, target) {
- switch (op) {
- case '-':
- this.reg1 = -this.get(target);
- break;
- default:
- throw new Error('Unimplemented operator: ' + op);
- }
- }
-
- call(callee, args) {
- const func = this.get(callee);
- this.reg1 = func.apply(
- null,
- args.map(arg => this.get(arg)),
- );
- }
-
- query(sql, calculated) {
- this.pause(
- this.db.runQuery(sql, [], true).then(res => {
- if (calculated) {
- const keys = Object.keys(res[0]);
- return res[0][keys[0]];
- }
- return res;
- }),
- 'Running sql: ' + sql,
- );
- }
-
- jump(value, loc, { test }) {
- const result = this.get(value);
- const falsy = result === false || result === 0 || result === '';
-
- if ((test === 'true' && !falsy) || (test === 'false' && falsy)) {
- this.pc = loc.get();
- }
- }
-
- pause(promise, activityName) {
- this.paused = true;
-
- promise.then(
- val => {
- this.resume(val);
- },
- err => {
- console.log('VM caught error during activity: ' + activityName);
- console.log(err);
- this.resume(null);
- },
- );
- }
-
- resume(val) {
- this.reg1 = val;
- this.paused = false;
- this._run();
- }
-
- _run() {
- while (this.pc < this.ops.length) {
- const op = this.ops[this.pc];
-
- switch (op[0]) {
- case MOV:
- this.set(op[2], op[1]);
- break;
- case CALL:
- this.call(op[1], op[2]);
- break;
- case QUERY:
- this.query(op[1], op[2]);
- break;
- case BOP:
- this.binaryOp(op[1], op[2], op[3]);
- break;
- case UOP:
- this.unaryOp(op[1], op[2]);
- break;
- case JUMPF:
- this.jump(op[1], op[2], { test: 'false' });
- break;
- case JUMPT:
- this.jump(op[1], op[2], { test: 'true' });
- break;
- default:
- throw new Error('Unimplemented opcode: ' + op[0].toString());
- }
-
- this.pc++;
-
- if (this.paused) {
- break;
- }
- }
-
- if (this.pc === this.ops.length && this._onFinish) {
- this._onFinish(this.reg1);
- }
- }
-
- onFinish(func) {
- this._onFinish = func;
- }
-
- run(ops, onFinish) {
- this.pc = 0;
- this.ops = ops;
- this._onFinish = onFinish;
- this._run();
- return this.reg1;
- }
-
- runSource(src, onFinish) {
- const { ops } = compile(src);
- return this.run(ops, onFinish);
- }
-}
diff --git a/packages/loot-core/src/server/spreadsheet/sqlinterp.test.ts b/packages/loot-core/src/server/spreadsheet/sqlinterp.test.ts
deleted file mode 100644
index 7f9eb2b7cc5..00000000000
--- a/packages/loot-core/src/server/spreadsheet/sqlinterp.test.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { compile } from './new/compiler';
-import sqlinterp from './sqlinterp';
-
-test('sql interpretation works', async () => {
- const transJan = {
- date: 20170106,
- amount: -5000,
- acct: 'boa',
- category: 1,
- };
- const transFeb = {
- date: 20170215,
- amount: -7620,
- acct: 'boa',
- category: 1,
- };
-
- const { sqlDependencies } = compile(`
- =from transactions
- where date >= 20170101 and date <= 20170131 and
- category = 1
- select { amount }
- `);
- const where = sqlDependencies[0].where;
-
- expect(sqlinterp(where, transJan, 'transactions')).toBe(true);
- expect(sqlinterp(where, transFeb, 'transactions')).toBe(false);
-});
diff --git a/packages/loot-core/src/server/spreadsheet/sqlinterp.ts b/packages/loot-core/src/server/spreadsheet/sqlinterp.ts
deleted file mode 100644
index b30fa984750..00000000000
--- a/packages/loot-core/src/server/spreadsheet/sqlinterp.ts
+++ /dev/null
@@ -1,187 +0,0 @@
-import * as dateFns from 'date-fns';
-
-const AlwaysTrue = Symbol('AlwaysTrue');
-let shouldLog = false;
-
-// TODO: We need to track conformance with the SQL behavior that
-// sqlite implements. Luckily we are restricted to a very small subset
-// of the language, but we still need to be careful about things like
-// type coercion.
-
-function bind(args, func) {
- // if (shouldLog) {
- // console.log(args, func.toString());
- // }
- for (let i = 0; i < args.length; i++) {
- if (args[i] === AlwaysTrue) {
- return AlwaysTrue;
- }
- }
- return func();
-}
-
-const builtinFuncs = {
- concat: function (str1, str2) {
- return str1 + str2;
- },
- date: function (str) {
- return dateFns.parseISO(str);
- },
-};
-
-function interpretExpr(expr, context) {
- switch (expr.getTypeName()) {
- case 'FunCall':
- const args = expr.args.map(arg => interpretExpr(arg, context));
- return bind(args, () => {
- return builtinFuncs[expr.name].apply(null, args);
- });
-
- case 'Member':
- // We don't support walking through table schema yet. Any joined
- // fields are always evaluated as true. These will always exist
- // as member expressions; fields from the implicit table will
- // simply be symbols.
- return AlwaysTrue;
-
- case 'Literal':
- return expr.value;
-
- case 'Symbol':
- return expr.value in context.row ? context.row[expr.value] : AlwaysTrue;
-
- case 'BinOp':
- switch (expr.op) {
- case 'or': {
- const left = interpretExpr(expr.left, context);
- const right = interpretExpr(expr.right, context);
- return bind([expr.left, expr.right], () => {
- return left || right;
- });
- }
- case 'and': {
- const left = interpretExpr(expr.left, context);
- const right = interpretExpr(expr.right, context);
- return left && right;
- }
- case '=': {
- const left = interpretExpr(expr.left, context);
- const right = interpretExpr(expr.right, context);
- return bind([left, right], () => {
- if (left instanceof Date && right instanceof Date) {
- return dateFns.isEqual(left, right);
- }
- return left === right;
- });
- }
- case '=~': {
- const left = interpretExpr(expr.left, context);
- const right = interpretExpr(expr.right, context);
- return bind([left, right], () => {
- return left && left.match(new RegExp(right.replace('%', '.*')));
- });
- }
- case '!=~': {
- const left = interpretExpr(expr.left, context);
- const right = interpretExpr(expr.right, context);
- return bind([left, right], () => {
- return left && !left.match(new RegExp(right.replace('%', '.*')));
- });
- }
- case '>': {
- const left = interpretExpr(expr.left, context);
- const right = interpretExpr(expr.right, context);
- return bind([left, right], () => {
- if (left instanceof Date) {
- return dateFns.isAfter(left, dateFns.parseISO(right));
- }
- return left > right;
- });
- }
- case '<': {
- const left = interpretExpr(expr.left, context);
- const right = interpretExpr(expr.right, context);
- return bind([left, right], () => {
- if (left instanceof Date) {
- return dateFns.isBefore(left, dateFns.parseISO(right));
- }
- return left < right;
- });
- }
- case '>=': {
- const left = interpretExpr(expr.left, context);
- const right = interpretExpr(expr.right, context);
- return bind([left, right], () => {
- if (left instanceof Date) {
- return (
- dateFns.isAfter(left, dateFns.parseISO(right)) ||
- dateFns.isEqual(left, dateFns.parseISO(right))
- );
- }
- return left >= right;
- });
- }
- case '<=': {
- const left = interpretExpr(expr.left, context);
- const right = interpretExpr(expr.right, context);
- return bind([left, right], () => {
- if (left instanceof Date) {
- return (
- dateFns.isBefore(left, dateFns.parseISO(right)) ||
- dateFns.isEqual(left, dateFns.parseISO(right))
- );
- }
- return left <= right;
- });
- }
- case '-': {
- const left = interpretExpr(expr.left, context);
- const right = interpretExpr(expr.right, context);
- return bind([left, right], () => left - right);
- }
- case '+': {
- const left = interpretExpr(expr.left, context);
- const right = interpretExpr(expr.right, context);
- return bind([left, right], () => left + right);
- }
- case '*': {
- const left = interpretExpr(expr.left, context);
- const right = interpretExpr(expr.right, context);
- return bind([left, right], () => left * right);
- }
- case '/': {
- const left = interpretExpr(expr.left, context);
- const right = interpretExpr(expr.right, context);
- return bind([left, right], () => left / right);
- }
- default:
- console.log(expr);
- throw new Error('Unable to interpret operator: ' + expr.op);
- }
-
- default:
- throw new Error('Unknown sql node: ' + expr.getTypeName());
- }
-}
-
-function interpret(where, row, table) {
- // if (where.op !== 'or' && where.op !== 'and') {
- // throw new Error(
- // 'Invalid where clause: top-level expression must be AND or OR'
- // );
- // }
-
- // Set this to `true` for debugging
- shouldLog = false;
-
- let ret = interpretExpr(where, { row, table });
-
- if (shouldLog) {
- console.log('Final', ret);
- }
- shouldLog = false;
-
- return ret;
-}
-
-export default interpret;
diff --git a/packages/loot-core/src/server/spreadsheet/util.ts b/packages/loot-core/src/server/spreadsheet/util.ts
index cc69f858942..92709cffa52 100644
--- a/packages/loot-core/src/server/spreadsheet/util.ts
+++ b/packages/loot-core/src/server/spreadsheet/util.ts
@@ -12,28 +12,3 @@ export function unresolveName(name) {
export function resolveName(sheet, name) {
return sheet + '!' + name;
}
-
-export function resolveNamesAsObjects(sheets) {
- const cells = {};
- Object.keys(sheets).forEach(sheetName => {
- const sheet = sheets[sheetName];
-
- Object.keys(sheet).forEach(name => {
- const expr = sheet[name];
- cells[resolveName(sheetName, name)] = expr;
- });
- });
- return cells;
-}
-
-export function resolveNamesAsArrays(sheets) {
- const cells = [];
- Object.keys(sheets).forEach(sheetName => {
- const sheet = sheets[sheetName];
-
- sheet.forEach(name => {
- cells.push(resolveName(sheetName, name));
- });
- });
- return cells;
-}
diff --git a/packages/loot-core/src/server/sync/encoder.ts b/packages/loot-core/src/server/sync/encoder.ts
index 46e933e9b3d..aa451564049 100644
--- a/packages/loot-core/src/server/sync/encoder.ts
+++ b/packages/loot-core/src/server/sync/encoder.ts
@@ -1,8 +1,10 @@
+import { SyncProtoBuf } from '@actual-app/crdt';
+
import * as encryption from '../encryption';
import { SyncError } from '../errors';
import * as prefs from '../prefs';
-import * as SyncPb from './proto/sync_pb';
+import { Message } from './index';
function coerceBuffer(value) {
// The web encryption APIs give us back raw Uint8Array... but our
@@ -15,24 +17,29 @@ function coerceBuffer(value) {
return value;
}
-export async function encode(groupId, fileId, since, messages) {
+export async function encode(
+ groupId: string,
+ fileId: string,
+ since: string,
+ messages: Message[],
+): Promise {
let { encryptKeyId } = prefs.getPrefs();
- let requestPb = new SyncPb.SyncRequest();
+ let requestPb = new SyncProtoBuf.SyncRequest();
for (let i = 0; i < messages.length; i++) {
let msg = messages[i];
- let envelopePb = new SyncPb.MessageEnvelope();
+ let envelopePb = new SyncProtoBuf.MessageEnvelope();
envelopePb.setTimestamp(msg.timestamp);
- let messagePb = new SyncPb.Message();
+ let messagePb = new SyncProtoBuf.Message();
messagePb.setDataset(msg.dataset);
messagePb.setRow(msg.row);
messagePb.setColumn(msg.column);
- messagePb.setValue(msg.value);
+ messagePb.setValue(msg.value as string);
let binaryMsg = messagePb.serializeBinary();
if (encryptKeyId) {
- let encrypted = new SyncPb.EncryptedData();
+ let encrypted = new SyncProtoBuf.EncryptedData();
let result;
try {
@@ -64,10 +71,12 @@ export async function encode(groupId, fileId, since, messages) {
return requestPb.serializeBinary();
}
-export async function decode(data) {
+export async function decode(
+ data: Uint8Array,
+): Promise<{ messages: Message[]; merkle: { hash: number } }> {
let { encryptKeyId } = prefs.getPrefs();
- let responsePb = SyncPb.SyncResponse.deserializeBinary(data);
+ let responsePb = SyncProtoBuf.SyncResponse.deserializeBinary(data);
let merkle = JSON.parse(responsePb.getMerkle());
let list = responsePb.getMessagesList();
let messages = [];
@@ -79,7 +88,7 @@ export async function decode(data) {
let msg;
if (encrypted) {
- let binary = SyncPb.EncryptedData.deserializeBinary(
+ let binary = SyncProtoBuf.EncryptedData.deserializeBinary(
envelopePb.getContent() as Uint8Array,
);
@@ -98,9 +107,9 @@ export async function decode(data) {
});
}
- msg = SyncPb.Message.deserializeBinary(decrypted);
+ msg = SyncProtoBuf.Message.deserializeBinary(decrypted);
} else {
- msg = SyncPb.Message.deserializeBinary(
+ msg = SyncProtoBuf.Message.deserializeBinary(
envelopePb.getContent() as Uint8Array,
);
}
diff --git a/packages/loot-core/src/server/sync/index.ts b/packages/loot-core/src/server/sync/index.ts
index 6aab81a77fd..4c017e14d6a 100644
--- a/packages/loot-core/src/server/sync/index.ts
+++ b/packages/loot-core/src/server/sync/index.ts
@@ -1,3 +1,11 @@
+import {
+ serializeClock,
+ deserializeClock,
+ getClock,
+ Timestamp,
+ merkle,
+} from '@actual-app/crdt';
+
import { captureException } from '../../platform/exceptions';
import * as asyncStorage from '../../platform/server/asyncStorage';
import * as connection from '../../platform/server/connection';
@@ -5,13 +13,6 @@ import logger from '../../platform/server/log';
import { sequential, once } from '../../shared/async';
import { setIn, getIn } from '../../shared/util';
import { triggerBudgetChanges, setType as setBudgetType } from '../budget/base';
-import {
- serializeClock,
- deserializeClock,
- getClock,
- Timestamp,
- merkle,
-} from '../crdt';
import * as db from '../db';
import { PostError, SyncError } from '../errors';
import app from '../main-app';
@@ -24,6 +25,7 @@ import * as undo from '../undo';
import * as encoder from './encoder';
import { rebuildMerkleHash } from './repair';
+import { isError } from './utils';
export { default as makeTestMessage } from './make-test-message';
export { default as resetSync } from './reset';
@@ -31,8 +33,9 @@ export { default as repairSync } from './repair';
let FULL_SYNC_DELAY = 1000;
let SYNCING_MODE = 'enabled';
+type SyncingMode = 'enabled' | 'offline' | 'disabled' | 'import';
-export function setSyncingMode(mode) {
+export function setSyncingMode(mode: SyncingMode) {
let prevMode = SYNCING_MODE;
switch (mode) {
case 'enabled':
@@ -53,7 +56,7 @@ export function setSyncingMode(mode) {
return prevMode;
}
-export function checkSyncingMode(mode) {
+export function checkSyncingMode(mode: SyncingMode): boolean {
switch (mode) {
case 'enabled':
return SYNCING_MODE === 'enabled' || SYNCING_MODE === 'offline';
@@ -68,7 +71,7 @@ export function checkSyncingMode(mode) {
}
}
-function apply(msg, prev?: unknown) {
+function apply(msg: Message, prev?: boolean) {
let { dataset, row, column, value } = msg;
if (dataset === 'prefs') {
@@ -146,7 +149,7 @@ async function fetchAll(table, ids) {
return results;
}
-export function serializeValue(value) {
+export function serializeValue(value: string | number | null): string {
if (value === null) {
return '0:';
} else if (typeof value === 'number') {
@@ -158,7 +161,7 @@ export function serializeValue(value) {
throw new Error('Unserializable value type: ' + JSON.stringify(value));
}
-export function deserializeValue(value) {
+export function deserializeValue(value: string): string | number | null {
const type = value[0];
switch (type) {
case '0':
@@ -173,9 +176,12 @@ export function deserializeValue(value) {
throw new Error('Invalid type key for value: ' + value);
}
-let _syncListeners = [];
+// TODO make this type stricter.
+type DataMap = Map;
+type SyncListener = (oldData: DataMap, newData: DataMap) => unknown;
+let _syncListeners: SyncListener[] = [];
-export function addSyncListener(func) {
+export function addSyncListener(func: SyncListener) {
_syncListeners.push(func);
return () => {
@@ -183,7 +189,7 @@ export function addSyncListener(func) {
};
}
-async function compareMessages(messages) {
+async function compareMessages(messages: Message[]): Promise {
let newMessages = [];
for (let i = 0; i < messages.length; i++) {
@@ -217,7 +223,7 @@ async function compareMessages(messages) {
// listeners importers should not rely on any functions that use any
// projected state (like rules). We can't fire those because they
// depend on having both old and new data which we don't quere here
-function applyMessagesForImport(messages) {
+function applyMessagesForImport(messages: Message[]): void {
db.transaction(() => {
for (let i = 0; i < messages.length; i++) {
let msg = messages[i];
@@ -238,13 +244,13 @@ function applyMessagesForImport(messages) {
});
}
-type Message = {
+export type Message = {
column: string;
dataset: string;
old?: unknown;
row: string;
- timestamp: number;
- value: unknown;
+ timestamp: string;
+ value: string | number | null;
};
export const applyMessages = sequential(async (messages: Message[]) => {
@@ -282,7 +288,7 @@ export const applyMessages = sequential(async (messages: Message[]) => {
idsPerTable[msg.dataset].push(msg.row);
});
- async function fetchData() {
+ async function fetchData(): Promise {
let data = new Map();
for (let table of Object.keys(idsPerTable)) {
@@ -414,7 +420,7 @@ export const applyMessages = sequential(async (messages: Message[]) => {
return messages;
});
-export function receiveMessages(messages: Message[]) {
+export function receiveMessages(messages: Message[]): Promise {
messages.forEach(msg => {
Timestamp.recv(msg.timestamp);
});
@@ -422,7 +428,7 @@ export function receiveMessages(messages: Message[]) {
return runMutator(() => applyMessages(messages));
}
-async function _sendMessages(messages) {
+async function _sendMessages(messages: Message[]): Promise {
try {
await applyMessages(messages);
} catch (e) {
@@ -449,15 +455,15 @@ async function _sendMessages(messages) {
}
let IS_BATCHING = false;
-let _BATCHED = [];
-export async function batchMessages(func) {
+let _BATCHED: Message[] = [];
+export async function batchMessages(func: () => Promise): Promise {
if (IS_BATCHING) {
await func();
return;
}
IS_BATCHING = true;
- let batched = [];
+ let batched: Message[] = [];
try {
await func();
@@ -473,7 +479,7 @@ export async function batchMessages(func) {
}
}
-export async function sendMessages(messages) {
+export async function sendMessages(messages: Message[]) {
if (IS_BATCHING) {
_BATCHED = _BATCHED.concat(messages);
} else {
@@ -481,7 +487,7 @@ export async function sendMessages(messages) {
}
}
-export function getMessagesSince(since) {
+export function getMessagesSince(since: string) {
return db.runQuery(
'SELECT timestamp, dataset, row, column, value FROM messages_crdt WHERE timestamp > ?',
[since],
@@ -489,19 +495,22 @@ export function getMessagesSince(since) {
);
}
-export async function syncAndReceiveMessages(messages, since) {
+export async function syncAndReceiveMessages(
+ messages: Message[],
+ since: string,
+): Promise {
let localMessages = await getMessagesSince(since);
await receiveMessages(
messages.map(msg => ({
...msg,
- value: deserializeValue(msg.value),
+ value: deserializeValue(msg.value as string),
timestamp: Timestamp.parse(msg.timestamp),
})),
);
return localMessages;
}
-export function clearFullSyncTimeout() {
+export function clearFullSyncTimeout(): void {
if (syncTimeout) {
clearTimeout(syncTimeout);
syncTimeout = null;
@@ -509,13 +518,15 @@ export function clearFullSyncTimeout() {
}
let syncTimeout = null;
-export function scheduleFullSync() {
+export function scheduleFullSync(): Promise<
+ { messages: Message[] } | { error: unknown }
+> {
clearFullSyncTimeout();
if (checkSyncingMode('enabled') && !checkSyncingMode('offline')) {
if (process.env.NODE_ENV === 'test') {
return fullSync().then(res => {
- if (res.error) {
+ if (isError(res)) {
throw res.error;
}
return res;
@@ -526,7 +537,7 @@ export function scheduleFullSync() {
}
}
-function getTablesFromMessages(messages) {
+function getTablesFromMessages(messages: Message[]): string[] {
return messages.reduce((acc, message) => {
let dataset =
message.dataset === 'schedules_next_date' ? 'schedules' : message.dataset;
@@ -542,15 +553,17 @@ function getTablesFromMessages(messages) {
// spreadsheet to finish any processing. This is useful if we want to
// perform a full sync and wait for everything to finish, usually if
// you're doing an initial sync before working with a file.
-export async function initialFullSync() {
+export async function initialFullSync(): Promise {
let result = await fullSync();
- if (!result.error) {
+ if (isError(result)) {
// Make sure to wait for anything in the spreadsheet to process
await sheet.waitOnSpreadsheet();
}
}
-export const fullSync = once(async function () {
+export const fullSync = once(async function (): Promise<
+ { messages: Message[] } | { error: unknown }
+> {
app.events.emit('sync', { type: 'start' });
let messages;
@@ -620,7 +633,11 @@ export const fullSync = once(async function () {
return { messages };
});
-async function _fullSync(sinceTimestamp, count, prevDiffTime) {
+async function _fullSync(
+ sinceTimestamp: string,
+ count: number,
+ prevDiffTime: number,
+): Promise {
let { cloudFileId, groupId, lastSyncedTimestamp } = prefs.getPrefs() || {};
clearFullSyncTimeout();
@@ -677,7 +694,7 @@ async function _fullSync(sinceTimestamp, count, prevDiffTime) {
receivedMessages = await receiveMessages(
res.messages.map(msg => ({
...msg,
- value: deserializeValue(msg.value),
+ value: deserializeValue(msg.value as string),
timestamp: Timestamp.parse(msg.timestamp),
})),
);
diff --git a/packages/loot-core/src/server/sync/make-test-message.ts b/packages/loot-core/src/server/sync/make-test-message.ts
index e4173925b0b..f686060fb1a 100644
--- a/packages/loot-core/src/server/sync/make-test-message.ts
+++ b/packages/loot-core/src/server/sync/make-test-message.ts
@@ -1,13 +1,13 @@
-import * as encryption from '../encryption';
+import { SyncProtoBuf } from '@actual-app/crdt';
-import * as SyncPb from './proto/sync_pb';
+import * as encryption from '../encryption';
async function randomString() {
return (await encryption.randomBytes(12)).toString();
}
export default async function makeTestMessage(keyId) {
- let messagePb = new SyncPb.Message();
+ let messagePb = new SyncProtoBuf.Message();
messagePb.setDataset(await randomString());
messagePb.setRow(await randomString());
messagePb.setColumn(await randomString());
diff --git a/packages/loot-core/src/server/sync/migrate.test.ts b/packages/loot-core/src/server/sync/migrate.test.ts
index 58a11738040..b2fd82fedfd 100644
--- a/packages/loot-core/src/server/sync/migrate.test.ts
+++ b/packages/loot-core/src/server/sync/migrate.test.ts
@@ -7,7 +7,7 @@ import * as db from '../db';
import { listen, unlisten } from './migrate';
-import { addSyncListener, sendMessages } from './index';
+import { Message, addSyncListener, sendMessages } from './index';
beforeEach(() => {
listen();
@@ -25,7 +25,7 @@ function toInternalField(publicField) {
return schemaConfig.views.transactions.fields[publicField];
}
-let messageArb = fc
+let messageArb: fc.Arbitrary = fc
.oneof(...fields.filter(f => f !== 'id').map(field => fc.constant(field)))
.chain(field => {
let value = arbs
@@ -41,7 +41,7 @@ let messageArb = fc
.noShrink()
.map(date => date.toISOString() + '-0000-0123456789ABCDEF');
- return fc.record({
+ return fc.record({
timestamp: timestamp,
dataset: fc.constant('transactions'),
column: fc.constant(toInternalField(field) || field),
@@ -61,7 +61,11 @@ describe('sync migrations', () => {
tracer.start();
let cleanup = addSyncListener((oldValues, newValues) => {
- tracer.event('applied', [...newValues.get('transactions').keys()]);
+ let transactionsMap = newValues.get('transactions') as Map<
+ string,
+ unknown
+ >;
+ tracer.event('applied', [...transactionsMap.keys()]);
});
await db.insert('transactions', {
@@ -87,7 +91,10 @@ describe('sync migrations', () => {
let tracer = execTracer();
tracer.start();
let cleanup = addSyncListener((oldValues, newValues) => {
- let ts = newValues.get('transactions');
+ let ts = newValues.get('transactions') as Map<
+ string,
+ { isChild: number; parent_id: string | null; id: string }
+ >;
if (
ts &&
[...ts.values()].find(
diff --git a/packages/loot-core/src/server/sync/migrate.ts b/packages/loot-core/src/server/sync/migrate.ts
index 378d2f7c120..6238aa41ae5 100644
--- a/packages/loot-core/src/server/sync/migrate.ts
+++ b/packages/loot-core/src/server/sync/migrate.ts
@@ -1,4 +1,4 @@
-import { Timestamp } from '../crdt';
+import { Timestamp } from '@actual-app/crdt';
import { addSyncListener, applyMessages } from './index';
diff --git a/packages/loot-core/src/server/sync/repair.ts b/packages/loot-core/src/server/sync/repair.ts
index 96df0b61893..e8736eb6929 100644
--- a/packages/loot-core/src/server/sync/repair.ts
+++ b/packages/loot-core/src/server/sync/repair.ts
@@ -1,4 +1,5 @@
-import { serializeClock, getClock, Timestamp, merkle } from '../crdt';
+import { serializeClock, getClock, Timestamp, merkle } from '@actual-app/crdt';
+
import * as db from '../db';
export function rebuildMerkleHash() {
diff --git a/packages/loot-core/src/server/sync/sync.property.test.ts b/packages/loot-core/src/server/sync/sync.property.test.ts
index 4242e3c6122..f43f63b28e1 100644
--- a/packages/loot-core/src/server/sync/sync.property.test.ts
+++ b/packages/loot-core/src/server/sync/sync.property.test.ts
@@ -1,12 +1,13 @@
+import { merkle, getClock, Timestamp } from '@actual-app/crdt';
import jsc, { type Arbitrary } from 'jsverify';
-import { merkle, getClock, Timestamp } from '../crdt';
import * as db from '../db';
import * as prefs from '../prefs';
import * as sheet from '../sheet';
import * as mockSyncServer from '../tests/mockSyncServer';
import * as encoder from './encoder';
+import { isError } from './utils';
import * as sync from './index';
@@ -278,10 +279,10 @@ async function run(msgs) {
),
);
- let { error } = await syncPromise;
- if (error) {
- console.log(error);
- throw error;
+ let result = await syncPromise;
+ if (isError(result)) {
+ console.log(result.error);
+ throw result.error;
}
let serverMerkle = mockSyncServer.getClock().merkle;
diff --git a/packages/loot-core/src/server/sync/sync.test.ts b/packages/loot-core/src/server/sync/sync.test.ts
index 749891b8632..91cc537d9ed 100644
--- a/packages/loot-core/src/server/sync/sync.test.ts
+++ b/packages/loot-core/src/server/sync/sync.test.ts
@@ -1,10 +1,12 @@
-import { getClock, Timestamp } from '../crdt';
+import { getClock, Timestamp } from '@actual-app/crdt';
+
import * as db from '../db';
import * as prefs from '../prefs';
import * as sheet from '../sheet';
import * as mockSyncServer from '../tests/mockSyncServer';
import * as encoder from './encoder';
+import { isError } from './utils';
import { setSyncingMode, sendMessages, applyMessages, fullSync } from './index';
@@ -85,9 +87,9 @@ describe('Sync', () => {
expect(mockSyncServer.getMessages().length).toBe(0);
- const { messages, error } = await fullSync();
- expect(error).toBeFalsy();
- expect(messages.length).toBe(0);
+ const result = await fullSync();
+ if (isError(result)) throw result.error;
+ expect(result.messages.length).toBe(0);
expect(mockSyncServer.getMessages().length).toBe(2);
});
@@ -132,8 +134,9 @@ describe('Sync', () => {
},
]);
- const { messages } = await fullSync();
- expect(messages.length).toBe(2);
+ const result = await fullSync();
+ if (isError(result)) throw result.error;
+ expect(result.messages.length).toBe(2);
expect(mockSyncServer.getMessages().length).toBe(3);
});
});
diff --git a/packages/loot-core/src/server/sync/utils.ts b/packages/loot-core/src/server/sync/utils.ts
new file mode 100644
index 00000000000..802dcc18967
--- /dev/null
+++ b/packages/loot-core/src/server/sync/utils.ts
@@ -0,0 +1,3 @@
+export function isError(value: unknown): value is { error: unknown } {
+ return (value as { error: unknown }).error !== undefined;
+}
diff --git a/packages/loot-core/src/server/tests/mockSyncServer.ts b/packages/loot-core/src/server/tests/mockSyncServer.ts
index cb4a3f2639b..1e64534eafb 100644
--- a/packages/loot-core/src/server/tests/mockSyncServer.ts
+++ b/packages/loot-core/src/server/tests/mockSyncServer.ts
@@ -1,5 +1,4 @@
-import { makeClock, Timestamp, merkle } from '../crdt';
-import * as SyncPb from '../sync/proto/sync_pb';
+import { makeClock, Timestamp, merkle, SyncProtoBuf } from '@actual-app/crdt';
import { basic as defaultMockData } from './mockData.json';
@@ -29,7 +28,7 @@ handlers['/'] = () => {
};
handlers['/sync/sync'] = async data => {
- let requestPb = SyncPb.SyncRequest.deserializeBinary(data);
+ let requestPb = SyncProtoBuf.SyncRequest.deserializeBinary(data);
let since = requestPb.getSince();
let messages = requestPb.getMessagesList();
@@ -52,11 +51,11 @@ handlers['/sync/sync'] = async data => {
currentClock.merkle = merkle.prune(currentClock.merkle);
- let responsePb = new SyncPb.SyncResponse();
+ let responsePb = new SyncProtoBuf.SyncResponse();
responsePb.setMerkle(JSON.stringify(currentClock.merkle));
newMessages.forEach(msg => {
- let envelopePb = new SyncPb.MessageEnvelope();
+ let envelopePb = new SyncProtoBuf.MessageEnvelope();
envelopePb.setTimestamp(msg.timestamp);
envelopePb.setIsencrypted(msg.is_encrypted);
envelopePb.setContent(msg.content);
@@ -112,7 +111,7 @@ export const getClock = () => {
export const getMessages = () => {
return currentMessages.map(msg => {
let { timestamp, content } = msg;
- let fields = SyncPb.Message.deserializeBinary(content);
+ let fields = SyncProtoBuf.Message.deserializeBinary(content);
return {
timestamp: timestamp,
diff --git a/packages/loot-core/src/server/undo.ts b/packages/loot-core/src/server/undo.ts
index 6217ea6be22..bfc67cc03b3 100644
--- a/packages/loot-core/src/server/undo.ts
+++ b/packages/loot-core/src/server/undo.ts
@@ -1,7 +1,8 @@
+import { Timestamp } from '@actual-app/crdt';
+
import * as connection from '../platform/server/connection';
import { getIn } from '../shared/util';
-import { Timestamp } from './crdt';
import { withMutatorContext, getMutatorContext } from './mutators';
import { sendMessages } from './sync';
diff --git a/packages/loot-core/src/server/update.ts b/packages/loot-core/src/server/update.ts
index 6be345e9e8c..221b4869fc3 100644
--- a/packages/loot-core/src/server/update.ts
+++ b/packages/loot-core/src/server/update.ts
@@ -10,7 +10,7 @@ async function runMigrations() {
await migrations.migrate(db.getDatabase());
}
-export async function updateViews() {
+async function updateViews() {
let hashKey = 'view-hash';
let row = await db.first('SELECT value FROM __meta__ WHERE key = ?', [
hashKey,
diff --git a/packages/loot-core/src/shared/arithmetic.test.ts b/packages/loot-core/src/shared/arithmetic.test.ts
index 4d94c606fe2..c0f5afa46c5 100644
--- a/packages/loot-core/src/shared/arithmetic.test.ts
+++ b/packages/loot-core/src/shared/arithmetic.test.ts
@@ -1,7 +1,4 @@
-import { generateTestCases } from '../mocks/number-formats';
-
import evalArithmetic from './arithmetic';
-import { setNumberFormat } from './util';
describe('arithmetic', () => {
test('handles negative numbers', () => {
@@ -44,76 +41,4 @@ describe('arithmetic', () => {
test('respects current number format', () => {
expect(evalArithmetic('1,222.45')).toEqual(1222.45);
});
-
- let configurableFormats = [
- 'dot-comma',
- 'comma-dot',
- 'space-comma',
- 'space-dot',
- ];
-
- let inputFormats = [
- {
- name: 'dot-comma',
- tests: [
- { places: 3, input: '1.234.567', expected: 1234567 },
- { places: 2, input: '1.234,56', expected: 1234.56 },
- { places: 1, input: '1.234,5', expected: 1234.5 },
- { places: 0, input: '1.234,', expected: 1234.0 },
- ],
- },
- {
- name: 'comma-dot',
- tests: [
- { places: 3, input: '1,234,567', expected: 1234567 },
- { places: 2, input: '1,234.56', expected: 1234.56 },
- { places: 1, input: '1,234.5', expected: 1234.5 },
- { places: 0, input: '1,234.', expected: 1234.0 },
- ],
- },
- {
- name: 'dot-dot',
- tests: [
- { places: 3, input: '1.234.567', expected: 1234567 },
- { places: 2, input: '1.234.56', expected: 1234.56 },
- { places: 1, input: '1.234.5', expected: 1234.5 },
- { places: 0, input: '1.234.', expected: 1234.0 },
- ],
- },
- {
- name: 'comma-comma',
- tests: [
- { places: 3, input: '1,234,567', expected: 1234567 },
- { places: 2, input: '1,234,56', expected: 1234.56 },
- { places: 1, input: '1,234,5', expected: 1234.5 },
- { places: 0, input: '1,234,', expected: 1234.0 },
- ],
- },
- {
- name: 'space-comma',
- tests: [
- { places: 3, input: '1 234 567', expected: 1234567 },
- { places: 2, input: '1 234,56', expected: 1234.56 },
- { places: 1, input: '1 234,5', expected: 1234.5 },
- { places: 0, input: '1 234,', expected: 1234.0 },
- ],
- },
- {
- name: 'space-dot',
- tests: [
- { places: 3, input: '1 234 567', expected: 1234567 },
- { places: 2, input: '1 234.56', expected: 1234.56 },
- { places: 1, input: '1 234.5', expected: 1234.5 },
- { places: 0, input: '1 234.', expected: 1234.0 },
- ],
- },
- ];
-
- test.each(generateTestCases(configurableFormats, inputFormats))(
- 'format is agnostic: %s can parse %s with %d decimal place(s)',
- (configurableFormat, inputFormat, places, input, expected) => {
- setNumberFormat({ format: configurableFormat, hideFraction: false });
- expect(evalArithmetic(input)).toEqual(expected);
- },
- );
});
diff --git a/packages/loot-core/src/shared/arithmetic.ts b/packages/loot-core/src/shared/arithmetic.ts
index 0957d10b101..4dd2b8955ab 100644
--- a/packages/loot-core/src/shared/arithmetic.ts
+++ b/packages/loot-core/src/shared/arithmetic.ts
@@ -1,3 +1,5 @@
+import { getNumberFormat } from './util';
+
function fail(state, msg) {
throw new Error(
msg + ': ' + JSON.stringify(state.str.slice(state.index, 10)),
@@ -27,24 +29,6 @@ function nextOperator(state, op) {
return false;
}
-function unifyNumberFormatForParsing(numberStr: string): string {
- let unifiedNumberStr = '';
- for (let i = 0; i < numberStr.length; i++) {
- let ch = numberStr[i];
- if (ch === ',' || ch === '.') {
- // Skip thousands separators
- let remainingChars = numberStr.length - i;
- if (remainingChars > 3) {
- continue;
- }
- // Unify decimal separator
- ch = '.';
- }
- unifiedNumberStr += ch;
- }
- return unifiedNumberStr;
-}
-
function parsePrimary(state) {
// We only support numbers
let isNegative = char(state) === '-';
@@ -60,14 +44,21 @@ function parsePrimary(state) {
// and we should do more strict parsing
let numberStr = '';
while (char(state) && char(state).match(/[0-9,.]/)) {
- numberStr += next(state);
+ let thousandsSep = getNumberFormat().separator === ',' ? '.' : ',';
+
+ // Don't include the thousands separator
+ if (char(state) === thousandsSep) {
+ next(state);
+ } else {
+ numberStr += next(state);
+ }
}
if (numberStr === '') {
fail(state, 'Unexpected character');
}
- let number = parseFloat(unifyNumberFormatForParsing(numberStr));
+ let number = parseFloat(numberStr.replace(getNumberFormat().separator, '.'));
return isNegative ? -number : number;
}
diff --git a/packages/loot-core/src/shared/async.ts b/packages/loot-core/src/shared/async.ts
index 3739a4e3d5f..a534631cd99 100644
--- a/packages/loot-core/src/shared/async.ts
+++ b/packages/loot-core/src/shared/async.ts
@@ -1,6 +1,6 @@
export function sequential unknown>(
fn: T,
-): (...args: Parameters) => Promise> {
+): (...args: Parameters) => Promise>> {
let sequenceState = {
running: null,
queue: [],
@@ -43,9 +43,11 @@ export function sequential unknown>(
};
}
-export function once(fn) {
+export function once Promise>(
+ fn: T,
+): (...args: Parameters) => Promise>> {
let promise = null;
- let onceFn = (...args) => {
+ let onceFn = (...args: Parameters): Promise>> => {
if (!promise) {
promise = fn(...args).finally(() => {
promise = null;
diff --git a/packages/loot-core/src/shared/environment.ts b/packages/loot-core/src/shared/environment.ts
index 8a6d0211071..275ddb398e1 100644
--- a/packages/loot-core/src/shared/environment.ts
+++ b/packages/loot-core/src/shared/environment.ts
@@ -1,3 +1,5 @@
+/* eslint-disable import/no-unused-modules */
+
export function isPreviewEnvironment() {
return String(process.env.REACT_APP_NETLIFY) === 'true';
}
diff --git a/packages/loot-core/src/shared/errors.ts b/packages/loot-core/src/shared/errors.ts
index 8b7f64826f5..0fec6f41676 100644
--- a/packages/loot-core/src/shared/errors.ts
+++ b/packages/loot-core/src/shared/errors.ts
@@ -19,7 +19,7 @@ export function getUploadError({ reason, meta }) {
case 'beta-version':
return 'You cannot perform this action in the beta version (resetting sync, deleting a file, etc).';
default:
- return `An internal error occurred, sorry! Contact help@actualbudget.com for support. (ref: ${reason})`;
+ return `An internal error occurred, sorry! Visit https://actualbudget.org/contact/ for support. (ref: ${reason})`;
}
}
@@ -31,7 +31,7 @@ export function getDownloadError({ reason, meta, fileName }) {
case 'not-zip-file':
case 'invalid-zip-file':
case 'invalid-meta-file':
- return 'Downloaded file is invalid, sorry! Contact help@actualbudget.com for support.';
+ return 'Downloaded file is invalid, sorry! Visit https://actualbudget.org/contact/ for support.';
case 'decrypt-failure':
return (
'Unable to decrypt file ' +
@@ -49,7 +49,7 @@ export function getDownloadError({ reason, meta, fileName }) {
default:
let info = meta && meta.fileId ? `(fileId: ${meta.fileId})` : '';
return (
- 'Something went wrong trying to download that file, sorry! Contact help@actualbudget.com for support. ' +
+ 'Something went wrong trying to download that file, sorry! Visit https://actualbudget.org/contact/ for support. ' +
info
);
}
@@ -72,20 +72,7 @@ export function getTestKeyError({ reason }) {
case 'decrypt-failure':
return 'Unable to decrypt file with this password. Please try again.';
default:
- return 'Something went wrong trying to create a key, sorry! Contact help@actualbudget.com for support.';
- }
-}
-
-export function getSubscribeError({ reason }) {
- switch (reason) {
- case 'network':
- return 'Unable to reach the server. Check your internet connection';
- case 'exists':
- return 'An account with that email already exists. Did you mean to login?';
- case 'invalid-email':
- return 'Invalid email';
- default:
- return 'An error occurred. Please try again later.';
+ return 'Something went wrong trying to create a key, sorry! Visit https://actualbudget.org/contact/ for support.';
}
}
diff --git a/packages/loot-core/src/shared/rules.ts b/packages/loot-core/src/shared/rules.ts
index 3a58c973f78..93b2727ff9d 100644
--- a/packages/loot-core/src/shared/rules.ts
+++ b/packages/loot-core/src/shared/rules.ts
@@ -25,19 +25,6 @@ export const TYPE_INFO = {
},
};
-export type FieldTypes = {
- imported_payee: string;
- payee: string;
- date: string;
- notes: string;
- amount: number;
- amountInflow: number;
- amountOutfow: number;
- category: string;
- account: string;
- cleared: boolean;
-};
-
export const FIELD_TYPES = new Map(
Object.entries({
imported_payee: 'string',
@@ -147,7 +134,7 @@ export function getFieldError(type) {
case 'invalid-field':
return 'Please choose a valid field for this type of rule';
default:
- return 'Internal error, sorry! Please get in touch https://actualbudget.github.io/docs/Contact/ for support';
+ return 'Internal error, sorry! Please get in touch https://actualbudget.org/contact/ for support';
}
}
diff --git a/packages/loot-core/src/shared/transactions.ts b/packages/loot-core/src/shared/transactions.ts
index 2653b482633..a1b2e425c88 100644
--- a/packages/loot-core/src/shared/transactions.ts
+++ b/packages/loot-core/src/shared/transactions.ts
@@ -56,7 +56,7 @@ export function recalculateSplit(trans) {
};
}
-export function findParentIndex(transactions, idx) {
+function findParentIndex(transactions, idx) {
// This relies on transactions being sorted in a way where parents
// are always before children, which is enforced in the db layer.
// Walk backwards and find the last parent;
@@ -95,7 +95,7 @@ export function ungroupTransactions(transactions) {
return x;
}
-export function groupTransaction(split) {
+function groupTransaction(split) {
return { ...split[0], subtransactions: split.slice(1) };
}
@@ -110,7 +110,7 @@ export function applyTransactionDiff(groupedTrans, diff) {
return groupTransaction(applyChanges(diff, ungroupTransaction(groupedTrans)));
}
-export function replaceTransactions(transactions, id, func) {
+function replaceTransactions(transactions, id, func) {
let idx = transactions.findIndex(t => t.id === id);
let trans = transactions[idx];
let transactionsCopy = [...transactions];
diff --git a/packages/loot-core/src/shared/util.ts b/packages/loot-core/src/shared/util.ts
index 008278f4a17..f1dec4b3332 100644
--- a/packages/loot-core/src/shared/util.ts
+++ b/packages/loot-core/src/shared/util.ts
@@ -1,50 +1,7 @@
-export function cleanUUID(uuid) {
- return uuid.replace(/-/g, '');
-}
-
export function last(arr) {
return arr[arr.length - 1];
}
-export function mergeObjects(objects) {
- return Object.assign.apply(null, [{}, ...objects]);
-}
-
-export function composeCellChanges(objects) {
- const merged = {};
- Object.keys(objects).forEach(key => {
- if (merged[key]) {
- merged[key] = { ...merged[key], ...objects[key] };
- } else {
- merged[key] = objects[key];
- }
- });
-}
-
-export function flattenArray(arrays) {
- return Array.prototype.concat.apply([], arrays);
-}
-
-export function shallowEqual(a, b) {
- if (a === b) {
- return true;
- }
-
- let numKeysA = 0,
- numKeysB = 0,
- key;
- for (key in b) {
- numKeysB++;
- if (!a.hasOwnProperty(key) || a[key] !== b[key]) {
- return false;
- }
- }
- for (key in a) {
- numKeysA++;
- }
- return numKeysA === numKeysB;
-}
-
export function getChangedValues(obj1, obj2) {
// Keep the id field because this is mostly used to diff database
// objects
@@ -132,19 +89,6 @@ export function groupBy(data, field, mapper?: (v: unknown) => unknown) {
return res;
}
-export function groupBySingle(data, field, mapper) {
- let res = new Map();
- for (let i = 0; i < data.length; i++) {
- let item = data[i];
- let key = item[field];
- if (res.has(key)) {
- throw new Error('groupBySingle found conflicting key: ' + key);
- }
- res.set(key, mapper ? mapper(item) : data[i]);
- }
- return res;
-}
-
// This should replace the existing `groupById` function, since a
// `Map` is better, but we can't swap it out because `Map` has a
// different API and we need to go through and update everywhere that
@@ -192,26 +136,22 @@ export function groupById(data) {
return res;
}
-export function debugMemoFailure(prevProps, nextProps) {
- let changed = getChangedValues(prevProps, nextProps);
- if (changed !== null) {
- console.log(changed);
- }
- return changed === null;
-}
-
-export function setIn(map, keys, item) {
+export function setIn(
+ map: Map,
+ keys: string[],
+ item: unknown,
+): void {
for (let i = 0; i < keys.length; i++) {
- let key = keys[i];
+ const key = keys[i];
if (i === keys.length - 1) {
map.set(key, item);
} else {
if (!map.has(key)) {
- map.set(key, new Map());
+ map.set(key, new Map());
}
- map = map.get(key);
+ map = map.get(key) as Map;
}
}
}
@@ -228,11 +168,6 @@ export function getIn(map, keys) {
return item;
}
-// Useful for throwing exception from expressions
-export function throwError(err) {
- throw err;
-}
-
export function fastSetMerge(set1, set2) {
let finalSet = new Set(set1);
let iter = set2.values();
@@ -339,10 +274,6 @@ export function toRelaxedNumber(value) {
return integerToAmount(currencyToInteger(value) || 0);
}
-export function toRelaxedInteger(value) {
- return stringToInteger(value) || 0;
-}
-
export function integerToCurrency(n) {
return numberFormat.formatter.format(safeNumber(n) / 100);
}
@@ -406,15 +337,3 @@ export function looselyParseAmount(amount) {
return safeNumber(parseFloat(left + '.' + right));
}
-
-export function semverToNumber(str) {
- return parseInt(
- '1' +
- str
- .split('.')
- .map(x => {
- return ('000' + x.replace(/[^0-9]/g, '')).slice(-3);
- })
- .join(''),
- );
-}
diff --git a/packages/loot-core/src/types/main-handlers.d.ts b/packages/loot-core/src/types/main-handlers.d.ts
index 669d1acbc96..3925c8d8d61 100644
--- a/packages/loot-core/src/types/main-handlers.d.ts
+++ b/packages/loot-core/src/types/main-handlers.d.ts
@@ -229,7 +229,6 @@ export interface MainHandlers {
'load-global-prefs': () => Promise<{
floatingSidebar: boolean;
- seenTutorial: boolean;
maxMonths: number;
autoUpdate: boolean;
documentDir: string;
@@ -303,16 +302,10 @@ export interface MainHandlers {
testBudgetId?;
}) => Promise;
- 'set-tutorial-seen': () => Promise<'ok'>;
-
'import-budget': (arg: { filepath; type }) => Promise<{ error }>;
'export-budget': () => Promise;
- 'get-upgrade-notifications': () => Promise;
-
- 'seen-upgrade-notification': (arg: { type }) => Promise;
-
'upload-file-web': (arg: { filename; contents }) => Promise<'ok'>;
'backups-get': (arg: { id }) => Promise;
diff --git a/packages/loot-core/typings/window.d.ts b/packages/loot-core/typings/window.d.ts
index 57e0efae4b0..3eea72bc6d3 100644
--- a/packages/loot-core/typings/window.d.ts
+++ b/packages/loot-core/typings/window.d.ts
@@ -8,9 +8,6 @@ declare global {
openURLInBrowser: (url: string) => void;
};
- __history?: {
- location;
- push(url: string, opts?: unknown): void;
- };
+ __navigate?: ReturnType;
}
}
diff --git a/upcoming-release-notes/1066.md b/upcoming-release-notes/1066.md
new file mode 100644
index 00000000000..38a23f70148
--- /dev/null
+++ b/upcoming-release-notes/1066.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [trevdor, j-f1]
+---
+
+Upgrade to react-router v6 and adopt v6 routing conventions.
diff --git a/upcoming-release-notes/1077.md b/upcoming-release-notes/1077.md
new file mode 100644
index 00000000000..4bb8529f3ee
--- /dev/null
+++ b/upcoming-release-notes/1077.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [TomAFrench]
+---
+
+Enforce proper types in server sync code
diff --git a/upcoming-release-notes/1121.md b/upcoming-release-notes/1121.md
new file mode 100644
index 00000000000..2a6117ed148
--- /dev/null
+++ b/upcoming-release-notes/1121.md
@@ -0,0 +1,6 @@
+---
+category: Enhancements
+authors: [j-f1]
+---
+
+Improve error reporting when using the API
diff --git a/upcoming-release-notes/1129.md b/upcoming-release-notes/1129.md
new file mode 100644
index 00000000000..54f032de3af
--- /dev/null
+++ b/upcoming-release-notes/1129.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [j-f1]
+---
+
+Update all links in the codebase to point to the new documentation site
diff --git a/upcoming-release-notes/1135.md b/upcoming-release-notes/1135.md
new file mode 100644
index 00000000000..dceedcf38d5
--- /dev/null
+++ b/upcoming-release-notes/1135.md
@@ -0,0 +1,6 @@
+---
+category: Features
+authors: [MatissJanis]
+---
+
+Nordigen: release as a first-party feature
diff --git a/upcoming-release-notes/1136.md b/upcoming-release-notes/1136.md
new file mode 100644
index 00000000000..7fb79894eff
--- /dev/null
+++ b/upcoming-release-notes/1136.md
@@ -0,0 +1,6 @@
+---
+category: Bugfix
+authors: [MatissJanis]
+---
+
+Fix "find schedules" page crashing if interction is made before loading data finishes
diff --git a/upcoming-release-notes/1137.md b/upcoming-release-notes/1137.md
new file mode 100644
index 00000000000..2ffeb25f8b7
--- /dev/null
+++ b/upcoming-release-notes/1137.md
@@ -0,0 +1,6 @@
+---
+category: Enhancements
+authors: [j-f1]
+---
+
+Nordigen: Update design of the “create account” flow
diff --git a/upcoming-release-notes/1139.md b/upcoming-release-notes/1139.md
new file mode 100644
index 00000000000..9f63e1f61f3
--- /dev/null
+++ b/upcoming-release-notes/1139.md
@@ -0,0 +1,6 @@
+---
+category: Bugfix
+authors: [Shazib]
+---
+
+Remove redundant usage of 'export' keyword
diff --git a/upcoming-release-notes/1140.md b/upcoming-release-notes/1140.md
new file mode 100644
index 00000000000..0659965bec8
--- /dev/null
+++ b/upcoming-release-notes/1140.md
@@ -0,0 +1,6 @@
+---
+category: Bugfix
+authors: [j-f1]
+---
+
+Automatically remove a trailing slash from server URLs before saving them
diff --git a/upcoming-release-notes/1141.md b/upcoming-release-notes/1141.md
new file mode 100644
index 00000000000..a05266c952a
--- /dev/null
+++ b/upcoming-release-notes/1141.md
@@ -0,0 +1,6 @@
+---
+category: Enhancements
+authors: [j-f1]
+---
+
+Make the behavior of the “Server” button in the top-right corner more consistent
diff --git a/upcoming-release-notes/1143.md b/upcoming-release-notes/1143.md
new file mode 100644
index 00000000000..8a6bd5fb884
--- /dev/null
+++ b/upcoming-release-notes/1143.md
@@ -0,0 +1,6 @@
+---
+category: Enhancements
+authors: [joel-jeremy]
+---
+
+Expand / collapse all categories
diff --git a/upcoming-release-notes/1144.md b/upcoming-release-notes/1144.md
new file mode 100644
index 00000000000..c3dfdae2dfe
--- /dev/null
+++ b/upcoming-release-notes/1144.md
@@ -0,0 +1,6 @@
+---
+category: Bugfix
+authors: [j-f1]
+---
+
+Revert “Make number parsing agnostic to decimal and thousands separators” because it produced undesirable behavior
diff --git a/upcoming-release-notes/1145.md b/upcoming-release-notes/1145.md
new file mode 100644
index 00000000000..252ad367a06
--- /dev/null
+++ b/upcoming-release-notes/1145.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [Shazib]
+---
+
+Remove unused functions from source
diff --git a/upcoming-release-notes/1146.md b/upcoming-release-notes/1146.md
new file mode 100644
index 00000000000..9c82768efc9
--- /dev/null
+++ b/upcoming-release-notes/1146.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [Shazib]
+---
+
+Remove all legacy 'Tutorial' code
diff --git a/upcoming-release-notes/1147.md b/upcoming-release-notes/1147.md
new file mode 100644
index 00000000000..dc46502735d
--- /dev/null
+++ b/upcoming-release-notes/1147.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [Shazib]
+---
+
+Remove redundant usage of 'export' keyword
diff --git a/upcoming-release-notes/1150.md b/upcoming-release-notes/1150.md
new file mode 100644
index 00000000000..a26e8269f39
--- /dev/null
+++ b/upcoming-release-notes/1150.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [MatissJanis]
+---
+
+Extracting CRDT functionality out to `@actual-app/crdt` package
diff --git a/upcoming-release-notes/1155.md b/upcoming-release-notes/1155.md
new file mode 100644
index 00000000000..86619f257fe
--- /dev/null
+++ b/upcoming-release-notes/1155.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [MatissJanis]
+---
+
+Remove misleading 'we have been notified' error messages
diff --git a/upcoming-release-notes/1156.md b/upcoming-release-notes/1156.md
new file mode 100644
index 00000000000..d3e9c168e1e
--- /dev/null
+++ b/upcoming-release-notes/1156.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [j-f1]
+---
+
+Remove unused code for notifying about major new features when updating
diff --git a/upcoming-release-notes/1157.md b/upcoming-release-notes/1157.md
new file mode 100644
index 00000000000..2635706e191
--- /dev/null
+++ b/upcoming-release-notes/1157.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [MatissJanis]
+---
+
+Remove 'needs triage' github label
diff --git a/upcoming-release-notes/1158.md b/upcoming-release-notes/1158.md
new file mode 100644
index 00000000000..3bd12741899
--- /dev/null
+++ b/upcoming-release-notes/1158.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [Shazib]
+---
+
+Remove unused/legacy code from codebase
diff --git a/upcoming-release-notes/1161.md b/upcoming-release-notes/1161.md
new file mode 100644
index 00000000000..b6f02c7c798
--- /dev/null
+++ b/upcoming-release-notes/1161.md
@@ -0,0 +1,6 @@
+---
+category: Enhancements
+authors: [j-f1]
+---
+
+Log more details when migrations are out of sync
diff --git a/upcoming-release-notes/1170.md b/upcoming-release-notes/1170.md
new file mode 100644
index 00000000000..c82d0d1ffcd
--- /dev/null
+++ b/upcoming-release-notes/1170.md
@@ -0,0 +1,6 @@
+---
+category: Bugfix
+authors: [j-f1]
+---
+
+Fix “delete file” modal layout
diff --git a/upcoming-release-notes/1171.md b/upcoming-release-notes/1171.md
new file mode 100644
index 00000000000..518894fb99d
--- /dev/null
+++ b/upcoming-release-notes/1171.md
@@ -0,0 +1,6 @@
+---
+category: Bugfix
+authors: [j-f1]
+---
+
+Fix transaction list page being blank on mobile
diff --git a/upcoming-release-notes/1173.md b/upcoming-release-notes/1173.md
new file mode 100644
index 00000000000..5f3b4e0e8f5
--- /dev/null
+++ b/upcoming-release-notes/1173.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [Shazib]
+---
+
+Add additional linter rules for checking imports
diff --git a/upcoming-release-notes/1174.md b/upcoming-release-notes/1174.md
new file mode 100644
index 00000000000..3ccac9bd7e1
--- /dev/null
+++ b/upcoming-release-notes/1174.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [Shazib]
+---
+
+Remove legacy tutorial code from loot-core.
diff --git a/upcoming-release-notes/1176.md b/upcoming-release-notes/1176.md
new file mode 100644
index 00000000000..f2c2fb9290e
--- /dev/null
+++ b/upcoming-release-notes/1176.md
@@ -0,0 +1,6 @@
+---
+category: Enhancements
+authors: [joel-jeremy]
+---
+
+Automatically set category when adding a transaction to the budget category transaction list
diff --git a/upcoming-release-notes/1178.md b/upcoming-release-notes/1178.md
new file mode 100644
index 00000000000..e72a5f0f4cf
--- /dev/null
+++ b/upcoming-release-notes/1178.md
@@ -0,0 +1,6 @@
+---
+category: Bugfix
+authors: [trevdor]
+---
+
+A couple patches for the React Router 6 upgrade.
diff --git a/upcoming-release-notes/1183.md b/upcoming-release-notes/1183.md
new file mode 100644
index 00000000000..9408e59d6ae
--- /dev/null
+++ b/upcoming-release-notes/1183.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [j-f1]
+---
+
+Fix automatic comment on completed feature requests
diff --git a/yarn.lock b/yarn.lock
index 857015e54db..44a4aa42b92 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -23,6 +23,20 @@ __metadata:
languageName: unknown
linkType: soft
+"@actual-app/crdt@*, @actual-app/crdt@workspace:packages/crdt":
+ version: 0.0.0-use.local
+ resolution: "@actual-app/crdt@workspace:packages/crdt"
+ dependencies:
+ "@types/jest": ^27.5.0
+ google-protobuf: ^3.12.0-rc.1
+ jest: ^27.0.0
+ murmurhash: ^0.0.2
+ ts-jest: ^27.0.0
+ typescript: ^5.0.2
+ uuid: 3.3.2
+ languageName: unknown
+ linkType: soft
+
"@actual-app/import-ynab4@*, @actual-app/import-ynab4@workspace:packages/import-ynab4":
version: 0.0.0-use.local
resolution: "@actual-app/import-ynab4@workspace:packages/import-ynab4"
@@ -97,9 +111,7 @@ __metadata:
react-merge-refs: ^1.1.0
react-modal: 3.16.1
react-redux: 7.2.1
- react-router: 5.2.0
- react-router-dom: 5.2.0
- react-router-dom-v5-compat: ^6.4.1
+ react-router-dom: 6.11.2
react-scripts: ^5.0.1
react-spring: ^9.7.1
react-virtualized-auto-sizer: ^1.0.2
@@ -1709,7 +1721,7 @@ __metadata:
languageName: node
linkType: hard
-"@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.8, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2":
+"@babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.8, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2":
version: 7.22.5
resolution: "@babel/runtime@npm:7.22.5"
dependencies:
@@ -2725,6 +2737,20 @@ __metadata:
languageName: node
linkType: hard
+"@pkgr/utils@npm:^2.3.1":
+ version: 2.4.1
+ resolution: "@pkgr/utils@npm:2.4.1"
+ dependencies:
+ cross-spawn: ^7.0.3
+ fast-glob: ^3.2.12
+ is-glob: ^4.0.3
+ open: ^9.1.0
+ picocolors: ^1.0.0
+ tslib: ^2.5.0
+ checksum: 654682860272541a40485b01e0763b155ec31faeba85b2c51e38b59c4ff1f8918c37b87b5ecbda3ff482d8486eba086e92b991fe4a8ed62efbbbdf83c0f64409
+ languageName: node
+ linkType: hard
+
"@playwright/test@npm:^1.29.1":
version: 1.33.0
resolution: "@playwright/test@npm:1.33.0"
@@ -3262,10 +3288,10 @@ __metadata:
languageName: node
linkType: hard
-"@remix-run/router@npm:1.6.1":
- version: 1.6.1
- resolution: "@remix-run/router@npm:1.6.1"
- checksum: 4ca65d9c7d6fa277227ad8fd4ef53bebab99460b714d835b609c998f9a7e7c33a964ce2b8af853b50025a60d9113968f256abc5f71f451939ff14a5187d327fe
+"@remix-run/router@npm:1.6.2":
+ version: 1.6.2
+ resolution: "@remix-run/router@npm:1.6.2"
+ checksum: 5969d313bff6ba5c75917910090cebafda84b9d3b4b453fae6b3d60fea9f938078578ffca769c532ab7ce252cd4a207b78d1024d7c727ab80dd572e62fd3b3f2
languageName: node
linkType: hard
@@ -4968,7 +4994,9 @@ __metadata:
cross-env: ^7.0.3
eslint: ^8.37.0
eslint-config-react-app: 7.0.1
+ eslint-import-resolver-typescript: 3.5.5
eslint-plugin-prettier: 4.2.1
+ eslint-plugin-react: 7.32.2
eslint-plugin-rulesdir: ^0.2.2
npm-run-all: ^4.1.3
patch-package: ^6.1.2
@@ -5756,6 +5784,13 @@ __metadata:
languageName: node
linkType: hard
+"big-integer@npm:^1.6.44":
+ version: 1.6.51
+ resolution: "big-integer@npm:1.6.51"
+ checksum: 3d444173d1b2e20747e2c175568bedeebd8315b0637ea95d75fd27830d3b8e8ba36c6af40374f36bdaea7b5de376dcada1b07587cb2a79a928fccdb6e6e3c518
+ languageName: node
+ linkType: hard
+
"big.js@npm:^5.2.2":
version: 5.2.2
resolution: "big.js@npm:5.2.2"
@@ -5859,6 +5894,15 @@ __metadata:
languageName: node
linkType: hard
+"bplist-parser@npm:^0.2.0":
+ version: 0.2.0
+ resolution: "bplist-parser@npm:0.2.0"
+ dependencies:
+ big-integer: ^1.6.44
+ checksum: d5339dd16afc51de6c88f88f58a45b72ed6a06aa31f5557d09877575f220b7c1d3fbe375da0b62e6a10d4b8ed80523567e351f24014f5bc886ad523758142cdd
+ languageName: node
+ linkType: hard
+
"brace-expansion@npm:^1.1.7":
version: 1.1.11
resolution: "brace-expansion@npm:1.1.11"
@@ -6052,6 +6096,15 @@ __metadata:
languageName: node
linkType: hard
+"bundle-name@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "bundle-name@npm:3.0.0"
+ dependencies:
+ run-applescript: ^5.0.0
+ checksum: edf2b1fbe6096ed32e7566947ace2ea937ee427391744d7510a2880c4b9a5b3543d3f6c551236a29e5c87d3195f8e2912516290e638c15bcbede7b37cc375615
+ languageName: node
+ linkType: hard
+
"bytes@npm:3.0.0":
version: 3.0.0
resolution: "bytes@npm:3.0.0"
@@ -7437,6 +7490,28 @@ __metadata:
languageName: node
linkType: hard
+"default-browser-id@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "default-browser-id@npm:3.0.0"
+ dependencies:
+ bplist-parser: ^0.2.0
+ untildify: ^4.0.0
+ checksum: 279c7ad492542e5556336b6c254a4eaf31b2c63a5433265655ae6e47301197b6cfb15c595a6fdc6463b2ff8e1a1a1ed3cba56038a60e1527ba4ab1628c6b9941
+ languageName: node
+ linkType: hard
+
+"default-browser@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "default-browser@npm:4.0.0"
+ dependencies:
+ bundle-name: ^3.0.0
+ default-browser-id: ^3.0.0
+ execa: ^7.1.1
+ titleize: ^3.0.0
+ checksum: 40c5af984799042b140300be5639c9742599bda76dc9eba5ac9ad5943c83dd36cebc4471eafcfddf8e0ec817166d5ba89d56f08e66a126c7c7908a179cead1a7
+ languageName: node
+ linkType: hard
+
"default-gateway@npm:^6.0.3":
version: 6.0.3
resolution: "default-gateway@npm:6.0.3"
@@ -7469,6 +7544,13 @@ __metadata:
languageName: node
linkType: hard
+"define-lazy-prop@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "define-lazy-prop@npm:3.0.0"
+ checksum: 54884f94caac0791bf6395a3ec530ce901cf71c47b0196b8754f3fd17edb6c0e80149c1214429d851873bb0d689dbe08dcedbb2306dc45c8534a5934723851b6
+ languageName: node
+ linkType: hard
+
"define-properties@npm:^1.1.3, define-properties@npm:^1.1.4, define-properties@npm:^1.2.0":
version: 1.2.0
resolution: "define-properties@npm:1.2.0"
@@ -8064,6 +8146,16 @@ __metadata:
languageName: node
linkType: hard
+"enhanced-resolve@npm:^5.12.0":
+ version: 5.15.0
+ resolution: "enhanced-resolve@npm:5.15.0"
+ dependencies:
+ graceful-fs: ^4.2.4
+ tapable: ^2.2.0
+ checksum: fbd8cdc9263be71cc737aa8a7d6c57b43d6aa38f6cc75dde6fcd3598a130cc465f979d2f4d01bb3bf475acb43817749c79f8eef9be048683602ca91ab52e4f11
+ languageName: node
+ linkType: hard
+
"enhanced-resolve@npm:^5.13.0":
version: 5.13.0
resolution: "enhanced-resolve@npm:5.13.0"
@@ -8346,6 +8438,25 @@ __metadata:
languageName: node
linkType: hard
+"eslint-import-resolver-typescript@npm:3.5.5":
+ version: 3.5.5
+ resolution: "eslint-import-resolver-typescript@npm:3.5.5"
+ dependencies:
+ debug: ^4.3.4
+ enhanced-resolve: ^5.12.0
+ eslint-module-utils: ^2.7.4
+ get-tsconfig: ^4.5.0
+ globby: ^13.1.3
+ is-core-module: ^2.11.0
+ is-glob: ^4.0.3
+ synckit: ^0.8.5
+ peerDependencies:
+ eslint: "*"
+ eslint-plugin-import: "*"
+ checksum: 27e6276fdff5d377c9036362ff736ac29852106e883ff589ea9092dc57d4bc2a67a82d75134221124f05045f9a7e2114a159b2c827d1f9f64d091f7afeab0f58
+ languageName: node
+ linkType: hard
+
"eslint-module-utils@npm:^2.7.4":
version: 2.8.0
resolution: "eslint-module-utils@npm:2.8.0"
@@ -8518,7 +8629,7 @@ __metadata:
languageName: node
linkType: hard
-"eslint-plugin-react@npm:^7.27.1":
+"eslint-plugin-react@npm:7.32.2, eslint-plugin-react@npm:^7.27.1":
version: 7.32.2
resolution: "eslint-plugin-react@npm:7.32.2"
dependencies:
@@ -8808,6 +8919,23 @@ __metadata:
languageName: node
linkType: hard
+"execa@npm:^7.1.1":
+ version: 7.1.1
+ resolution: "execa@npm:7.1.1"
+ dependencies:
+ cross-spawn: ^7.0.3
+ get-stream: ^6.0.1
+ human-signals: ^4.3.0
+ is-stream: ^3.0.0
+ merge-stream: ^2.0.0
+ npm-run-path: ^5.1.0
+ onetime: ^6.0.0
+ signal-exit: ^3.0.7
+ strip-final-newline: ^3.0.0
+ checksum: 21fa46fc69314ace4068cf820142bdde5b643a5d89831c2c9349479c1555bff137a291b8e749e7efca36535e4e0a8c772c11008ca2e84d2cbd6ca141a3c8f937
+ languageName: node
+ linkType: hard
+
"exenv@npm:^1.2.0":
version: 1.2.2
resolution: "exenv@npm:1.2.2"
@@ -8949,7 +9077,7 @@ __metadata:
languageName: node
linkType: hard
-"fast-glob@npm:^3.2.12, fast-glob@npm:^3.2.9":
+"fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.12, fast-glob@npm:^3.2.9":
version: 3.2.12
resolution: "fast-glob@npm:3.2.12"
dependencies:
@@ -9504,7 +9632,7 @@ __metadata:
languageName: node
linkType: hard
-"get-stream@npm:^6.0.0":
+"get-stream@npm:^6.0.0, get-stream@npm:^6.0.1":
version: 6.0.1
resolution: "get-stream@npm:6.0.1"
checksum: e04ecece32c92eebf5b8c940f51468cd53554dcbb0ea725b2748be583c9523d00128137966afce410b9b051eb2ef16d657cd2b120ca8edafcf5a65e81af63cad
@@ -9521,6 +9649,15 @@ __metadata:
languageName: node
linkType: hard
+"get-tsconfig@npm:^4.5.0":
+ version: 4.6.0
+ resolution: "get-tsconfig@npm:4.6.0"
+ dependencies:
+ resolve-pkg-maps: ^1.0.0
+ checksum: fd2589a50e21543cf416285e5c4ac605359f49209b6c2e66bb8698fac907356e060de0a681e40881f00182b6f19771377411a88adcc78fd3954732ff54f4a54d
+ languageName: node
+ linkType: hard
+
"github-from-package@npm:0.0.0":
version: 0.0.0
resolution: "github-from-package@npm:0.0.0"
@@ -9680,6 +9817,19 @@ __metadata:
languageName: node
linkType: hard
+"globby@npm:^13.1.3":
+ version: 13.2.0
+ resolution: "globby@npm:13.2.0"
+ dependencies:
+ dir-glob: ^3.0.1
+ fast-glob: ^3.2.11
+ ignore: ^5.2.0
+ merge2: ^1.4.1
+ slash: ^4.0.0
+ checksum: 0a3dd786571788adef1c894f22112834cff5bbe061ae6e0a01c5118c39d44b3f1937ef1dae3f8b9bc24756eba84a0923e565b1ad9a4ec52831d7e2a04c035e75
+ languageName: node
+ linkType: hard
+
"google-protobuf@npm:^3.12.0-rc.1, google-protobuf@npm:^3.15.5":
version: 3.21.2
resolution: "google-protobuf@npm:3.21.2"
@@ -9837,30 +9987,7 @@ __metadata:
languageName: node
linkType: hard
-"history@npm:^4.9.0":
- version: 4.10.1
- resolution: "history@npm:4.10.1"
- dependencies:
- "@babel/runtime": ^7.1.2
- loose-envify: ^1.2.0
- resolve-pathname: ^3.0.0
- tiny-invariant: ^1.0.2
- tiny-warning: ^1.0.0
- value-equal: ^1.0.1
- checksum: addd84bc4683929bae4400419b5af132ff4e4e9b311a0d4e224579ea8e184a6b80d7f72c55927e4fa117f69076a9e47ce082d8d0b422f1a9ddac7991490ca1d0
- languageName: node
- linkType: hard
-
-"history@npm:^5.3.0":
- version: 5.3.0
- resolution: "history@npm:5.3.0"
- dependencies:
- "@babel/runtime": ^7.7.6
- checksum: d73c35df49d19ac172f9547d30a21a26793e83f16a78386d99583b5bf1429cc980799fcf1827eb215d31816a6600684fba9686ce78104e23bd89ec239e7c726f
- languageName: node
- linkType: hard
-
-"hoist-non-react-statics@npm:^3.1.0, hoist-non-react-statics@npm:^3.3.0":
+"hoist-non-react-statics@npm:^3.3.0":
version: 3.3.2
resolution: "hoist-non-react-statics@npm:3.3.2"
dependencies:
@@ -10102,6 +10229,13 @@ __metadata:
languageName: node
linkType: hard
+"human-signals@npm:^4.3.0":
+ version: 4.3.1
+ resolution: "human-signals@npm:4.3.1"
+ checksum: 6f12958df3f21b6fdaf02d90896c271df00636a31e2bbea05bddf817a35c66b38a6fdac5863e2df85bd52f34958997f1f50350ff97249e1dff8452865d5235d1
+ languageName: node
+ linkType: hard
+
"humanize-ms@npm:^1.2.1":
version: 1.2.1
resolution: "humanize-ms@npm:1.2.1"
@@ -10467,6 +10601,15 @@ __metadata:
languageName: node
linkType: hard
+"is-docker@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "is-docker@npm:3.0.0"
+ bin:
+ is-docker: cli.js
+ checksum: b698118f04feb7eaf3338922bd79cba064ea54a1c3db6ec8c0c8d8ee7613e7e5854d802d3ef646812a8a3ace81182a085dfa0a71cc68b06f3fa794b9783b3c90
+ languageName: node
+ linkType: hard
+
"is-extglob@npm:^2.1.1":
version: 2.1.1
resolution: "is-extglob@npm:2.1.1"
@@ -10522,6 +10665,17 @@ __metadata:
languageName: node
linkType: hard
+"is-inside-container@npm:^1.0.0":
+ version: 1.0.0
+ resolution: "is-inside-container@npm:1.0.0"
+ dependencies:
+ is-docker: ^3.0.0
+ bin:
+ is-inside-container: cli.js
+ checksum: c50b75a2ab66ab3e8b92b3bc534e1ea72ca25766832c0623ac22d134116a98bcf012197d1caabe1d1c4bd5f84363d4aa5c36bb4b585fbcaf57be172cd10a1a03
+ languageName: node
+ linkType: hard
+
"is-interactive@npm:^1.0.0":
version: 1.0.0
resolution: "is-interactive@npm:1.0.0"
@@ -10674,6 +10828,13 @@ __metadata:
languageName: node
linkType: hard
+"is-stream@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "is-stream@npm:3.0.0"
+ checksum: 172093fe99119ffd07611ab6d1bcccfe8bc4aa80d864b15f43e63e54b7abc71e779acd69afdb854c4e2a67fdc16ae710e370eda40088d1cfc956a50ed82d8f16
+ languageName: node
+ linkType: hard
+
"is-string@npm:^1.0.5, is-string@npm:^1.0.7":
version: 1.0.7
resolution: "is-string@npm:1.0.7"
@@ -10754,13 +10915,6 @@ __metadata:
languageName: node
linkType: hard
-"isarray@npm:0.0.1":
- version: 0.0.1
- resolution: "isarray@npm:0.0.1"
- checksum: 49191f1425681df4a18c2f0f93db3adb85573bcdd6a4482539d98eac9e705d8961317b01175627e860516a2fc45f8f9302db26e5a380a97a520e272e2a40a8d4
- languageName: node
- linkType: hard
-
"isarray@npm:^2.0.5":
version: 2.0.5
resolution: "isarray@npm:2.0.5"
@@ -12161,7 +12315,7 @@ __metadata:
languageName: node
linkType: hard
-"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.2.0, loose-envify@npm:^1.3.1, loose-envify@npm:^1.4.0":
+"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0":
version: 1.4.0
resolution: "loose-envify@npm:1.4.0"
dependencies:
@@ -12177,6 +12331,7 @@ __metadata:
resolution: "loot-core@workspace:packages/loot-core"
dependencies:
"@actual-app/api": "*"
+ "@actual-app/crdt": "*"
"@actual-app/import-ynab4": "*"
"@babel/core": ~7.22.5
"@babel/preset-env": ^7.22.5
@@ -12207,7 +12362,6 @@ __metadata:
deep-equal: ^2.0.5
fake-indexeddb: ^3.1.3
fast-check: 3.7.1
- google-protobuf: ^3.12.0-rc.1
jest: ^27.0.0
jsverify: ^0.8.4
lru-cache: ^5.1.1
@@ -12216,7 +12370,6 @@ __metadata:
memoize-one: ^4.0.0
mitt: ^3.0.0
mockdate: ^3.0.5
- murmurhash: ^0.0.2
node-fetch: ^2.6.9
node-libofx: "*"
npm-run-all: ^4.1.3
@@ -12535,6 +12688,13 @@ __metadata:
languageName: node
linkType: hard
+"mimic-fn@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "mimic-fn@npm:4.0.0"
+ checksum: 995dcece15ee29aa16e188de6633d43a3db4611bcf93620e7e62109ec41c79c0f34277165b8ce5e361205049766e371851264c21ac64ca35499acb5421c2ba56
+ languageName: node
+ linkType: hard
+
"mimic-response@npm:^1.0.0":
version: 1.0.1
resolution: "mimic-response@npm:1.0.1"
@@ -12549,19 +12709,6 @@ __metadata:
languageName: node
linkType: hard
-"mini-create-react-context@npm:^0.4.0":
- version: 0.4.1
- resolution: "mini-create-react-context@npm:0.4.1"
- dependencies:
- "@babel/runtime": ^7.12.1
- tiny-warning: ^1.0.3
- peerDependencies:
- prop-types: ^15.0.0
- react: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
- checksum: f8cb2c7738aac355fe9ce7e8425f371b7fa90daddd5133edda4ccfdc18c49043b2ec04be6f3abf09b60a0f52549d54f158d5bfd81cdfb1a658531e5b9fe7bc6a
- languageName: node
- linkType: hard
-
"mini-css-extract-plugin@npm:^2.4.5":
version: 2.7.5
resolution: "mini-css-extract-plugin@npm:2.7.5"
@@ -13032,6 +13179,15 @@ __metadata:
languageName: node
linkType: hard
+"npm-run-path@npm:^5.1.0":
+ version: 5.1.0
+ resolution: "npm-run-path@npm:5.1.0"
+ dependencies:
+ path-key: ^4.0.0
+ checksum: dc184eb5ec239d6a2b990b43236845332ef12f4e0beaa9701de724aa797fe40b6bbd0157fb7639d24d3ab13f5d5cf22d223a19c6300846b8126f335f788bee66
+ languageName: node
+ linkType: hard
+
"npmlog@npm:^6.0.0":
version: 6.0.2
resolution: "npmlog@npm:6.0.2"
@@ -13223,6 +13379,15 @@ __metadata:
languageName: node
linkType: hard
+"onetime@npm:^6.0.0":
+ version: 6.0.0
+ resolution: "onetime@npm:6.0.0"
+ dependencies:
+ mimic-fn: ^4.0.0
+ checksum: 0846ce78e440841335d4e9182ef69d5762e9f38aa7499b19f42ea1c4cd40f0b4446094c455c713f9adac3f4ae86f613bb5e30c99e52652764d06a89f709b3788
+ languageName: node
+ linkType: hard
+
"open@npm:^7.4.2":
version: 7.4.2
resolution: "open@npm:7.4.2"
@@ -13244,6 +13409,18 @@ __metadata:
languageName: node
linkType: hard
+"open@npm:^9.1.0":
+ version: 9.1.0
+ resolution: "open@npm:9.1.0"
+ dependencies:
+ default-browser: ^4.0.0
+ define-lazy-prop: ^3.0.0
+ is-inside-container: ^1.0.0
+ is-wsl: ^2.2.0
+ checksum: 3993c0f61d51fed8ac290e99c9c3cf45d3b6cfb3e2aa2b74cafd312c3486c22fd81df16ac8f3ab91dd8a4e3e729a16fc2480cfc406c4833416cf908acf1ae7c9
+ languageName: node
+ linkType: hard
+
"optionator@npm:^0.8.1":
version: 0.8.3
resolution: "optionator@npm:0.8.3"
@@ -13564,6 +13741,13 @@ __metadata:
languageName: node
linkType: hard
+"path-key@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "path-key@npm:4.0.0"
+ checksum: 8e6c314ae6d16b83e93032c61020129f6f4484590a777eed709c4a01b50e498822b00f76ceaf94bc64dbd90b327df56ceadce27da3d83393790f1219e07721d7
+ languageName: node
+ linkType: hard
+
"path-parse@npm:^1.0.7":
version: 1.0.7
resolution: "path-parse@npm:1.0.7"
@@ -13578,15 +13762,6 @@ __metadata:
languageName: node
linkType: hard
-"path-to-regexp@npm:^1.7.0":
- version: 1.8.0
- resolution: "path-to-regexp@npm:1.8.0"
- dependencies:
- isarray: 0.0.1
- checksum: 709f6f083c0552514ef4780cb2e7e4cf49b0cc89a97439f2b7cc69a608982b7690fb5d1720a7473a59806508fc2dae0be751ba49f495ecf89fd8fbc62abccbcd
- languageName: node
- linkType: hard
-
"path-type@npm:^2.0.0":
version: 2.0.0
resolution: "path-type@npm:2.0.0"
@@ -14770,7 +14945,7 @@ __metadata:
languageName: node
linkType: hard
-"prop-types@npm:^15.5.10, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
+"prop-types@npm:^15.5.10, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
version: 15.8.1
resolution: "prop-types@npm:15.8.1"
dependencies:
@@ -15033,7 +15208,7 @@ __metadata:
languageName: node
linkType: hard
-"react-is@npm:^16.13.1, react-is@npm:^16.6.0, react-is@npm:^16.7.0, react-is@npm:^16.9.0":
+"react-is@npm:^16.13.1, react-is@npm:^16.7.0, react-is@npm:^16.9.0":
version: 16.13.1
resolution: "react-is@npm:16.13.1"
checksum: f7a19ac3496de32ca9ae12aa030f00f14a3d45374f1ceca0af707c831b2a6098ef0d6bdae51bd437b0a306d7f01d4677fcc8de7c0d331eb47ad0f46130e53c5f
@@ -15118,65 +15293,27 @@ __metadata:
languageName: node
linkType: hard
-"react-router-dom-v5-compat@npm:^6.4.1":
- version: 6.11.1
- resolution: "react-router-dom-v5-compat@npm:6.11.1"
+"react-router-dom@npm:6.11.2":
+ version: 6.11.2
+ resolution: "react-router-dom@npm:6.11.2"
dependencies:
- history: ^5.3.0
- react-router: 6.11.1
+ "@remix-run/router": 1.6.2
+ react-router: 6.11.2
peerDependencies:
react: ">=16.8"
react-dom: ">=16.8"
- react-router-dom: 4 || 5
- checksum: 56d79c3a14c82f57bdc564f425a48311a91cbb4bcf8e6cdfa7adb59a260074606cf59073d830381111464e345642323063c25a800e287ff527b1035381811e13
+ checksum: ba44ff37f2956bc18991f2eedb32a3fa46d35832f61ded6c5d167e853ca289868fca6635467866280c73bc3da00dce8437dbbec57da100c0a3e3e3850af00b83
languageName: node
linkType: hard
-"react-router-dom@npm:5.2.0":
- version: 5.2.0
- resolution: "react-router-dom@npm:5.2.0"
+"react-router@npm:6.11.2":
+ version: 6.11.2
+ resolution: "react-router@npm:6.11.2"
dependencies:
- "@babel/runtime": ^7.1.2
- history: ^4.9.0
- loose-envify: ^1.3.1
- prop-types: ^15.6.2
- react-router: 5.2.0
- tiny-invariant: ^1.0.2
- tiny-warning: ^1.0.0
- peerDependencies:
- react: ">=15"
- checksum: 98d2d35f9540ac4a3c14dc023623fc8411a6a6338e95d726370e07b27c3bc6e854516537c8e3f9ad2483c4bbd579ba28cce9aff843a19fe8ebff663318886335
- languageName: node
- linkType: hard
-
-"react-router@npm:5.2.0":
- version: 5.2.0
- resolution: "react-router@npm:5.2.0"
- dependencies:
- "@babel/runtime": ^7.1.2
- history: ^4.9.0
- hoist-non-react-statics: ^3.1.0
- loose-envify: ^1.3.1
- mini-create-react-context: ^0.4.0
- path-to-regexp: ^1.7.0
- prop-types: ^15.6.2
- react-is: ^16.6.0
- tiny-invariant: ^1.0.2
- tiny-warning: ^1.0.0
- peerDependencies:
- react: ">=15"
- checksum: 6fc908729110a65a5676a9e41333e0f511a3c0ff84c93c0dc704330cf3e02124c93aaeab8031b0e2c71712390d9278fff848eeebfbdda36dca3201142f309973
- languageName: node
- linkType: hard
-
-"react-router@npm:6.11.1":
- version: 6.11.1
- resolution: "react-router@npm:6.11.1"
- dependencies:
- "@remix-run/router": 1.6.1
+ "@remix-run/router": 1.6.2
peerDependencies:
react: ">=16.8"
- checksum: c5cafbaac13564d0e325f84ce6e4cbc42de5c381b0f619209f3b101d2b6eae4a8f9ee87b492875e869909dd9bb549d05d2f677085708f79622b872bd45d14bbb
+ checksum: e47f875dca70033a3b42704cb5ec076b60f9629a5cdc3be613707f3d5a5706123fb80301037455c285c6d5a1011b443e1784e0103969ebfac7071648d360c413
languageName: node
linkType: hard
@@ -15587,10 +15724,10 @@ __metadata:
languageName: node
linkType: hard
-"resolve-pathname@npm:^3.0.0":
- version: 3.0.0
- resolution: "resolve-pathname@npm:3.0.0"
- checksum: 6147241ba42c423dbe83cb067a2b4af4f60908c3af57e1ea567729cc71416c089737fe2a73e9e79e7a60f00f66c91e4b45ad0d37cd4be2d43fec44963ef14368
+"resolve-pkg-maps@npm:^1.0.0":
+ version: 1.0.0
+ resolution: "resolve-pkg-maps@npm:1.0.0"
+ checksum: 1012afc566b3fdb190a6309cc37ef3b2dcc35dff5fa6683a9d00cd25c3247edfbc4691b91078c97adc82a29b77a2660c30d791d65dab4fc78bfc473f60289977
languageName: node
linkType: hard
@@ -15778,6 +15915,15 @@ __metadata:
languageName: node
linkType: hard
+"run-applescript@npm:^5.0.0":
+ version: 5.0.0
+ resolution: "run-applescript@npm:5.0.0"
+ dependencies:
+ execa: ^5.0.0
+ checksum: d00c2dbfa5b2d774de7451194b8b125f40f65fc183de7d9dcae97f57f59433586d3c39b9001e111c38bfa24c3436c99df1bb4066a2a0c90d39a8c4cd6889af77
+ languageName: node
+ linkType: hard
+
"run-parallel@npm:^1.1.9":
version: 1.2.0
resolution: "run-parallel@npm:1.2.0"
@@ -16737,6 +16883,13 @@ __metadata:
languageName: node
linkType: hard
+"strip-final-newline@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "strip-final-newline@npm:3.0.0"
+ checksum: 23ee263adfa2070cd0f23d1ac14e2ed2f000c9b44229aec9c799f1367ec001478469560abefd00c5c99ee6f0b31c137d53ec6029c53e9f32a93804e18c201050
+ languageName: node
+ linkType: hard
+
"strip-json-comments@npm:^3.1.0, strip-json-comments@npm:^3.1.1":
version: 3.1.1
resolution: "strip-json-comments@npm:3.1.1"
@@ -16897,6 +17050,16 @@ __metadata:
languageName: node
linkType: hard
+"synckit@npm:^0.8.5":
+ version: 0.8.5
+ resolution: "synckit@npm:0.8.5"
+ dependencies:
+ "@pkgr/utils": ^2.3.1
+ tslib: ^2.5.0
+ checksum: 8a9560e5d8f3d94dc3cf5f7b9c83490ffa30d320093560a37b88f59483040771fd1750e76b9939abfbb1b5a23fd6dfbae77f6b338abffe7cae7329cd9b9bb86b
+ languageName: node
+ linkType: hard
+
"tabbable@npm:^4.0.0":
version: 4.0.0
resolution: "tabbable@npm:4.0.0"
@@ -17130,17 +17293,10 @@ __metadata:
languageName: node
linkType: hard
-"tiny-invariant@npm:^1.0.2":
- version: 1.3.1
- resolution: "tiny-invariant@npm:1.3.1"
- checksum: 872dbd1ff20a21303a2fd20ce3a15602cfa7fcf9b228bd694a52e2938224313b5385a1078cb667ed7375d1612194feaca81c4ecbe93121ca1baebe344de4f84c
- languageName: node
- linkType: hard
-
-"tiny-warning@npm:^1.0.0, tiny-warning@npm:^1.0.3":
- version: 1.0.3
- resolution: "tiny-warning@npm:1.0.3"
- checksum: da62c4acac565902f0624b123eed6dd3509bc9a8d30c06e017104bedcf5d35810da8ff72864400ad19c5c7806fc0a8323c68baf3e326af7cb7d969f846100d71
+"titleize@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "titleize@npm:3.0.0"
+ checksum: 71fbbeabbfb36ccd840559f67f21e356e1d03da2915b32d2ae1a60ddcc13a124be2739f696d2feb884983441d159a18649e8d956648d591bdad35c430a6b6d28
languageName: node
linkType: hard
@@ -17376,6 +17532,13 @@ __metadata:
languageName: node
linkType: hard
+"tslib@npm:^2.5.0":
+ version: 2.5.3
+ resolution: "tslib@npm:2.5.3"
+ checksum: 88902b309afaf83259131c1e13da1dceb0ad1682a213143a1346a649143924d78cf3760c448b84d796938fd76127183894f8d85cbb3bf9c4fddbfcc140c0003c
+ languageName: node
+ linkType: hard
+
"tsutils@npm:^3.21.0":
version: 3.21.0
resolution: "tsutils@npm:3.21.0"
@@ -17665,6 +17828,13 @@ __metadata:
languageName: node
linkType: hard
+"untildify@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "untildify@npm:4.0.0"
+ checksum: 39ced9c418a74f73f0a56e1ba4634b4d959422dff61f4c72a8e39f60b99380c1b45ed776fbaa0a4101b157e4310d873ad7d114e8534ca02609b4916bb4187fb9
+ languageName: node
+ linkType: hard
+
"upath@npm:^1.2.0":
version: 1.2.0
resolution: "upath@npm:1.2.0"
@@ -17804,13 +17974,6 @@ __metadata:
languageName: node
linkType: hard
-"value-equal@npm:^1.0.1":
- version: 1.0.1
- resolution: "value-equal@npm:1.0.1"
- checksum: bb7ae1facc76b5cf8071aeb6c13d284d023fdb370478d10a5d64508e0e6e53bb459c4bbe34258df29d82e6f561f874f0105eba38de0e61fe9edd0bdce07a77a2
- languageName: node
- linkType: hard
-
"vary@npm:~1.1.2":
version: 1.1.2
resolution: "vary@npm:1.1.2"