Compare commits

...

65 Commits

Author SHA1 Message Date
Cursor Agent
9af9dc84e9 feat: Trigger CI on VRT update and checkout PR branch
Co-authored-by: matiss <matiss@mja.lv>
2025-10-18 17:30:19 +00:00
Cursor Agent
57710006e0 Add CI checks to VRT update workflow
Co-authored-by: matiss <matiss@mja.lv>
2025-10-18 17:26:22 +00:00
Stephen Brown II
94332016e8 Adds balance variable to rule templates (#5925)
* Adds balance variable to rule templates

Enables access to the account balance within rule templates. This allows for more complex rule creation based on the current account balance.

Calculates the account balance up to the transaction being processed, including transactions on the same date with lower sort order.

Handles cases where the balance is undefined gracefully, defaulting to 0 to prevent errors.

## AI disclaimer
This PR contains code that was partially or fully generated by AI and may contain errors. All suggestions for improvement are welcome.

* Add release notes

* indexed sql params not supported

* [autofix.ci] apply automated fixes

* Skip parent transactions of splits

* Uses aql for account balance

Updates transaction rule preparation to use aql instead of  sql calculating it from past transactions.

The balance is defaulted to 0 if no account is set.

Refactor account balance calculation to build a proper query with date and sort_order filters

* Add block scoping to switch cases and ensure correct fallthrough handling in Action type conversions

* Corrects transaction rule sorting order

Reverses the sort order comparison in transaction rules
to ensure correct identification of prior transactions with the same date.

This ensures that the correct balance is used when
calculating balance-based rule conditions.

fixup! Corrects transaction rule sorting order

* Improves transaction rule balance calculation

Uses a more efficient query for calculating the account balance
up to a transaction when applying rules, improving performance.
This change reduces the complexity of the balance calculation.

* Apply coderabbit lessons learned

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-10-18 08:22:55 -07:00
Matiss Janis Aboltins
0af2c6c2fb Update default dashboard (#5924) 2025-10-18 16:34:41 +02:00
Matiss Janis Aboltins
97482a082d fix: prevent sensitive data leakage in error logs (#5948) 2025-10-18 16:33:27 +02:00
Matiss Janis Aboltins
31a9ba629b Change "/update-vrt" workflow (#5952) 2025-10-18 11:46:52 +01:00
Matiss Janis Aboltins
7c19a6333c docs: add AGENTS.md guide for AI agents (#5942) 2025-10-18 00:29:12 +02:00
Matiss Janis Aboltins
2bce9d707c Improve file decryption modal and button labels (#5943) 2025-10-17 23:47:12 +02:00
Michael Clark
4e42cda29e :electron: Prep for flathub - arm64 support and metainfo (#5947)
* prep for flathub -arm64 build and metainfo

* release note

* spelling mistake

* whitespace removal

* arch build issues

* hmm

* removing arm64 from flatpak - that would get bundled by flathub anyway
2025-10-17 21:08:59 +01:00
Matiss Janis Aboltins
0ce07d7692 Align onboarding form element sizes (#5941) 2025-10-16 22:34:51 +01:00
Matiss Janis Aboltins
0e3415c145 Hide refresh button when not connected to sync server (#5940) 2025-10-16 22:31:53 +01:00
Michael Clark
19675a7de6 ⬆️ Update flatpak sdk and platform (#5935)
* update flatpak sdk and platform

* release notes

* flatpak version config

* going to 24.08 to support as many distros as possible

* remove unneded file
2025-10-16 18:59:19 +01:00
Matt Fiddaman
5a1ceed7d9 ⬆️ bump electron (#5936)
* electron 38.3.0

* electron/notarize 3.1.0

* note
2025-10-16 16:22:48 +01:00
andreparames
86c1c30c97 enhancement: fix Cetelem bank transations (#5914)
* enhancement: fix Cetelem bank transations

The amount coming from GoCardless has the opposite sign of what we expect.

* Cetelem bank: make code more future-proof

Change based on code review suggestion

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>

---------

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2025-10-16 01:34:27 +01:00
lelemm
96ac1292f9 🐛 Fix Service Worker: New hash every build (#5928)
* New hash every build

* Add release notes for PR #5928

* trigger actions

* Changed from TS to MTS

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-10-15 13:11:54 -03:00
Matiss Janis Aboltins
328dfae8bf Update DeleteFileModal to disable delete button when server is offline (#5926) 2025-10-14 23:58:12 +02:00
Michael Süssemilch
37247395e2 feat(currency): add currency display to schedules (#5907)
* feat(currency): add currency display to schedules

* refactor: useFormat directly in cell
2025-10-14 20:10:57 +01:00
Daniel Boles
02b0e24d6e budgetfilesSlice: Fix paste-o in fallback sorting by ID (#5918)
* budgetfilesSlice: Fix paste-o in fallback sorting by ID

* Create 5918.md

* 5918.md: Clarify

* 5918.md: Ah, those square brackets don't just indicate a placeholder
2025-10-13 18:57:53 +01:00
Matiss Janis Aboltins
dfaa75f1cf Create e2e tests for mobile payees page (#5905) 2025-10-13 19:42:40 +02:00
Matiss Janis Aboltins
adae3e4352 Virtualize mobile payees list (#5904) 2025-10-13 19:04:32 +02:00
Matiss Janis Aboltins
7a3794295f Enhance mobile rules with undo notifications for save and delete actions (#5906) 2025-10-13 19:02:46 +02:00
Julian Dominguez-Schatz
c1d97fcc75 Allow size-compare action to run immediately after artifact upload (#5912)
* Allow size-compare action to run immediately after artifact upload

* Add release notes
2025-10-13 09:20:18 -07:00
Matiss Janis Aboltins
3715f16888 Add virtualizer to mobile rules list (#5899) 2025-10-11 12:57:39 +02:00
lelemm
edad7ce0e3 New Rule delete transaction action (#4603)
* Delete transaction rule action

* md

* removed debugger

* fix rule execution on csv preview

* Update 4603.md

---------

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2025-10-10 21:41:43 -03:00
Matt Fiddaman
737341ffb6 fix hotkey scope warning (#5901) 2025-10-10 19:59:20 +01:00
Matt Fiddaman
6147495003 ⬆️ upgrade recharts to v3 (#5903) 2025-10-10 19:58:59 +01:00
Matiss Janis Aboltins
614bedcfbf Trigger edge docker release on master push (#5897) 2025-10-10 09:37:54 +02:00
Matt Fiddaman
248b1034d7 fix api failing to import (#5896) 2025-10-09 22:55:48 +01:00
Claudio Spizzi
4f88afa266 Fix the Authentik external help URL for OIDC (#5891) 2025-10-08 21:41:16 -03:00
Matt Fiddaman
5c23aad3c2 ⬆️ bump yarn to 4.10.x (#5885)
* bump yarn to 4.10.3

* note

* fix flaky test
2025-10-08 23:21:37 +01:00
Michael Clark
5b9bcc94f6 ⬆️ Upgrade electron-builder (#5857)
* upgrade electron-builder

* no postinstall when yarn immutable

* downgrade to last version without the bug

* fix issue with wrappy

* release notes

* regen yarn lock

* react-sprint

* hmm

* update yarn manually...

* fixing spring?

* lock

* finally

* conflict

* put package back
2025-10-08 22:20:29 +01:00
Matt Fiddaman
87d54251cd ⬆️ bump loot-core dependencies (#5888)
* csv-stringify 6.6.0

* lru-cache 11.2.2

* ua-parser-js 2.0.5

* @reduxjs/toolkit 2.9.0

* jest-diff 30.2.0

* csv-parse 6.1.0

* yargs 18.0.0

* slash 5.1.0

* drop memfs

* peggy 5.0.6

* fast-check 4.3.0

* fake-indexeddb 6.2.2

* note
2025-10-08 21:16:34 +01:00
Matt Fiddaman
2e439aacba ⬆️ bump various dependencies (#5886)
* bump various dependencies

* note

* update playwright images

* globals 16.4.0

* react-router 7.9.4
2025-10-08 21:01:27 +01:00
gdufay
244140314c Enhancements: Use remark plugin in the report's Text widget (#5850)
* Add remark plugin to MarkdownCard

* Generate release note

* [autofix.ci] apply automated fixes

* Avoid duplicate css code

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-10-08 17:26:31 +01:00
Matiss Janis Aboltins
f7b40fca64 Add swipe to delete to mobile rules (#5871) 2025-10-07 20:33:46 +02:00
Stephen Brown II
dc811552be feat(currency): Currency-influenced initial number formats (#5797) 2025-10-07 19:05:16 +01:00
lelemm
295839ebbb 🐛 Fix for worker in dev mode (#5878)
* Fix for worker in dev mode

* Add release notes for PR #5878

* trigger actions

---------

Co-authored-by: Leandro Menezes <leandro.menezes@fusionflowsoftware.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-10-07 14:45:13 -03:00
Michael Süssemilch
99ca34458e feat(currency): add currency display to rules (#5639)
* feat(currency): add to rules

* doc: release notes

* feat: remove keydown from Input

* doc: release notes

* fix: make onEnter optional

* fix: ai remark

* refactor: remove onKeyDown from Input.tsx

* fix: handle Amount (inflow) and Amount (outflow) properly

* [autofix.ci] apply automated fixes

* fix: update AmountInput to sign and on outflow set +

* refactor: onSubmit handling of input value

* coderabbit suggestions

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-10-07 10:44:21 -07:00
lelemm
90ac8d8520 📚 More Translations (#5812)
* Translations

* linter

* Add release notes for PR #5812

* actions trigger

* md category change

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* typecheck fix

* linter

* more linter

* omg

* Fixes

* [autofix.ci] apply automated fixes

* Code review change

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Leandro Menezes <leandro.menezes@fusionflowsoftware.com>
2025-10-07 14:36:10 -03:00
Matt Fiddaman
52aeec2d59 ♻️ bump react dependencies (#5865) 2025-10-07 17:50:41 +01:00
lelemm
0c280d60f6 Frontend plugins Support [3/10]: System-Wide Feature Flag System + Frontend plugins feature flag (#5785)
Support for global feature flag and minimum custom theme prefs for plugins
2025-10-07 13:13:31 -03:00
Roque Alejandro Sosa
148ca92584 Added ARS currency (#5869)
* Added ARS currency

* Added correct release number

---------

Co-authored-by: Ras <git.boasting733@passinbox.com>
2025-10-07 08:28:38 -07:00
Ilyos Khurozov
90e848ebe8 Added support for Uzbek Soum (UZS) (#5876) 2025-10-07 08:16:43 -07:00
lelemm
b034d5039f Frontend plugins Support [2/10]: Plugin service worker (#5784)
* Plugin service worker
2025-10-07 12:14:32 -03:00
Matiss Janis Aboltins
5ac29473f2 Mobile payees - swipe to delete (#5824) 2025-10-06 19:23:52 +01:00
Matt Fiddaman
3b0db2bed7 ♻️ bump various build dependencies (#5864)
* vite 7.1.9

* typescript 5.9.3

* @types/node 22.18.8

* linting

* emscripten types

* note
2025-10-06 17:32:42 +01:00
Michael Clark
7a886810bc :electron: Hide the Electron menu (#5847)
* add retries to electron server import

* release notes

* get rid of this menu. If its an app functionality it should be available within the app

* hide the menu - update the ui

* fix function call

* Update VRT

* release notes

* spelling mistake

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-10-06 17:13:47 +01:00
Haritha Hasathcharu
8bf0997275 Add LKR and CRC currencies (#5848) 2025-10-06 08:41:59 -07:00
Matt Fiddaman
2f965266ab run schedule rules regardless of posted date (#5870)
* run schedule rules regardless of date

* note
2025-10-06 16:31:24 +01:00
Matt Fiddaman
499f24f7fd ♻️ bump non-react deps in desktop-client (#5858)
* patch/minor deps

* @vitejs/plugin-basic-ssl 2.1.0

* remove chokidar

* cross-env 10.1.0

* downshift 9.0.10

* remove focus-visible

* jsdom 27.0.0

* rollup-plugin-visualizer 6.0.4

* note
2025-10-06 16:28:04 +01:00
Matiss Janis Aboltins
4c5be62f56 Mobile payees - add loading indicator to rules count label (#5842) 2025-10-05 19:57:27 +01:00
Matiss Janis Aboltins
1446c7d93f Mobile rules - refactor to use react-aria GridList component (#5804) 2025-10-05 19:57:06 +01:00
Julian Dominguez-Schatz
ad9980307e Fix React compiler behaviour in dev mode (#5853)
* Fix React compiler behaviour in dev mode

* Add release notes

* Add comment
2025-10-05 07:14:03 -07:00
dependabot[bot]
d4ad31fb0c Bump tar-fs from 2.1.3 to 2.1.4 (#5796)
Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 2.1.3 to 2.1.4.
- [Commits](https://github.com/mafintosh/tar-fs/compare/v2.1.3...v2.1.4)

---
updated-dependencies:
- dependency-name: tar-fs
  dependency-version: 2.1.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2025-10-05 14:51:07 +01:00
Matt Fiddaman
05355788e4 ♻️ bump sync-server dependencies (#5819)
* uuid 11.1.0 -> 13.0.0

* better-sqlite3 12.2.0 -> 12.4.1

* debug 4.4.1 -> 4.4.3

* express-rate-limit 8.0.1 -> 8.1.0

* pluggy-sdk 0.74.0 -> 0.77.0

* babel/core 7.28.0 -> 7.28.4

* note
2025-10-05 14:36:57 +01:00
Stephen Brown II
805e2b1807 Align amount conversion utilities between api and loot-core (#5747)
* Align amount conversion utilities between api and loot-core

Updates api amount conversion utilities to align with loot-core, improving consistency and maintainability across the project.

Uses decimal places as parameters in conversion functions.

* Moves amount conversion utils to core

Moves amount conversion utilities to the core library.

This change consolidates these utilities for better code reuse
and maintainability across different parts of the application.
It removes the duplicate definition from the API package and
imports it from the core library where it is shared.
2025-10-05 14:23:34 +01:00
Çağdaş Şenel
e54dc0c1ca fix losing transaction amount decimals on update (#5807) 2025-10-05 14:23:15 +01:00
Çağdaş Şenel
e1c2f0a181 feat: show full decimals while editing (#5808)
* show full decimals while editing

* add changes

* handle null
2025-10-05 14:23:04 +01:00
Matt Fiddaman
cc2e329e8e show empty data points on line graph reports (#5815)
* draw zero points

* note

* Update VRT

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-10-05 14:22:51 +01:00
Matt Fiddaman
71f849d1e1 ♻️ bump eslint-plugin-actual dependencies (#5818)
* eslint 9.27.0 -> 9.36.0

* eslint-plugin-eslint-plugin 6.4.0 -> 7.0.0

* eslint-vitest-rule-tester 2.2.0 -> 2.2.2

* note

* misc eslint deps
2025-10-05 14:22:42 +01:00
Matt Fiddaman
0ea8bc1fb4 expand eslint untranslated string rule (#5827)
* expand translation rule and abstract import fix implementation

* fixes

* note

* coderabbit
2025-10-05 14:22:14 +01:00
Matt Fiddaman
f0c7953c0b ♻️ refactor rules code (#5837)
* extract handlebars helpers

* extract condition types

* extract condition class

* extract action class

* extract rule class

* extract rule indexer

* extract rule utils

* update main index

* note

* enable strict where able

* generalise assert

* coderabbit

* move condition-types into condition, move helper functions into rule-utils
2025-10-05 14:22:02 +01:00
Matt Fiddaman
4cf5f9b183 add average per year calculation to the summary report (#5838)
* add average per year to summary report

* note

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-10-05 14:21:52 +01:00
Michael Clark
80fd997540 Reports - Add an option to trim the start & end intervals (#5641)
* initial test to trim the intervals

* bit more

* got the logic

* fix table data

* add migration for trim intervals

* release notes

* nice work rabbit

* small cleanup

* not sure how major that is but yeah why not
2025-10-05 10:48:35 +01:00
Michael Clark
da93ddf63b 🛠️ Add retries to electron loot-core import (#5843)
* add retries to electron server import

* release notes
2025-10-05 10:48:05 +01:00
301 changed files with 11257 additions and 9547 deletions

View File

@@ -1,12 +0,0 @@
---
alwaysApply: true
---
When running yarn commands - always run them in the root directory. Do not run them in child workspaces.
The following commands can be useful:
- `yarn typecheck` to run typechecker
- `yarn lint` to run the code linter and formatter
- `yarn lint:fix` to fix some of the code lint issues (running this is preferred over `yarn lint`)
- `yarn test` to run all the tests

View File

@@ -1,37 +0,0 @@
---
description:
globs: *.ts,*.tsx
alwaysApply: false
---
You are an expert in TypeScript and React.
Code Style and Structure
- Write concise, technical TypeScript code.
- Use functional and declarative programming patterns; avoid classes.
- Prefer iteration and modularization over code duplication.
- Use descriptive variable names with auxiliary verbs (e.g., isLoaded, hasError).
- Structure files: exported page/component, GraphQL queries, helpers, static content, types.
- When creating a new component, place it in its own file rather than grouping multiple components in a single file.
Naming Conventions
- Favor named exports for components and utilities.
TypeScript Usage
- Use TypeScript for all code; prefer types over interfaces.
- Avoid enums; use objects or maps instead.
- Avoid using `any` or `unknown` unless absolutely necessary. Look for type definitions in the codebase instead.
- Avoid type assertions with `as` or `!`; prefer using `satisfies`.
Syntax and Formatting
- Use the "function" keyword for pure functions.
- Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements.
- Use declarative JSX, keeping JSX minimal and readable.
Change validation
- Run `yarn typecheck` in the root directory to validate that the generated TypeScript code is correct

View File

@@ -1,14 +0,0 @@
---
description:
globs:
alwaysApply: true
---
Vitest test runner is used for unit tests.
When running unit tests, always include the flag `--watch=false` to prevent watch mode.
To run unit tests for a specific package in the monorepo, use the following command:
`yarn workspace <workspaceNameFromPackageJson> run test <pathToTest>`
Recommendation: Minimize the number of dependencies you mock. The fewer dependencies you mock, the better.

View File

@@ -12,6 +12,8 @@ on:
branches:
- master
pull_request:
repository_dispatch:
types: [vrt-update-applied]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@@ -22,6 +24,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# For repository_dispatch events, checkout the PR branch
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref || github.ref }}
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_repo || github.repository }}
- name: Set up environment
uses: ./.github/actions/setup
- name: Build API
@@ -38,6 +44,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# For repository_dispatch events, checkout the PR branch
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref || github.ref }}
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_repo || github.repository }}
- name: Set up environment
uses: ./.github/actions/setup
- name: Build CRDT
@@ -54,6 +64,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# For repository_dispatch events, checkout the PR branch
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref || github.ref }}
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_repo || github.repository }}
- name: Set up environment
uses: ./.github/actions/setup
- name: Build Web
@@ -73,6 +87,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# For repository_dispatch events, checkout the PR branch
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref || github.ref }}
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_repo || github.repository }}
- name: Set up environment
uses: ./.github/actions/setup
- name: Build Server

View File

@@ -5,6 +5,8 @@ on:
branches:
- master
pull_request:
repository_dispatch:
types: [vrt-update-applied]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@@ -15,6 +17,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# For repository_dispatch events, checkout the PR branch
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref || github.ref }}
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_repo || github.repository }}
- name: Set up environment
uses: ./.github/actions/setup
- name: Lint
@@ -23,6 +29,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# For repository_dispatch events, checkout the PR branch
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref || github.ref }}
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_repo || github.repository }}
- name: Set up environment
uses: ./.github/actions/setup
- name: Typecheck
@@ -31,6 +41,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# For repository_dispatch events, checkout the PR branch
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref || github.ref }}
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_repo || github.repository }}
- name: Set up environment
uses: ./.github/actions/setup
- name: Build Web
@@ -41,16 +55,24 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# For repository_dispatch events, checkout the PR branch
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref || github.ref }}
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_repo || github.repository }}
- name: Set up environment
uses: ./.github/actions/setup
- name: Test
run: yarn test
migrations:
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request' || github.event_name == 'repository_dispatch'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# For repository_dispatch events, checkout the PR branch
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref || github.ref }}
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_repo || github.repository }}
- uses: actions/setup-node@v4
with:
node-version: 20

View File

@@ -1,21 +1,16 @@
name: Build Edge Docker Image
# Edge Docker images are built for every commit, and daily
# Edge Docker images are built for every push to master
on:
push:
branches:
- master
paths:
- 'packages/sync-server/**'
pull_request:
branches:
- master
paths:
- 'packages/sync-server/**'
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
concurrency:
group: docker-edge-build
cancel-in-progress: true
permissions:
contents: read
packages: write

View File

@@ -32,7 +32,7 @@ jobs:
needs: netlify
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.52.0-jammy
image: mcr.microsoft.com/playwright:v1.56.0-jammy
steps:
- uses: actions/checkout@v4
- name: Set up environment
@@ -53,7 +53,7 @@ jobs:
name: Functional Desktop App
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.52.0-jammy
image: mcr.microsoft.com/playwright:v1.56.0-jammy
steps:
- uses: actions/checkout@v4
- name: Set up environment
@@ -74,7 +74,7 @@ jobs:
needs: netlify
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.52.0-jammy
image: mcr.microsoft.com/playwright:v1.56.0-jammy
steps:
- uses: actions/checkout@v4
- name: Set up environment

View File

@@ -44,9 +44,9 @@ jobs:
sudo apt-get install flatpak -y
sudo apt-get install flatpak-builder -y
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
sudo flatpak install org.freedesktop.Sdk/x86_64/23.08 -y
sudo flatpak install org.freedesktop.Platform/x86_64/23.08 -y
sudo flatpak install org.electronjs.Electron2.BaseApp/x86_64/23.08 -y
sudo flatpak install org.freedesktop.Sdk//24.08 -y
sudo flatpak install org.freedesktop.Platform//24.08 -y
sudo flatpak install org.electronjs.Electron2.BaseApp//24.08 -y
- name: Set up environment
uses: ./.github/actions/setup
- name: Build Electron for Mac
@@ -57,6 +57,7 @@ jobs:
CSC_LINK: ${{ secrets.CSC_LINK }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- name: Build Electron
if: ${{ ! startsWith(matrix.os, 'macos') }}
run: ./bin/package-electron

View File

@@ -39,9 +39,9 @@ jobs:
sudo apt-get install flatpak -y
sudo apt-get install flatpak-builder -y
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
sudo flatpak install org.freedesktop.Sdk/x86_64/23.08 -y
sudo flatpak install org.freedesktop.Platform/x86_64/23.08 -y
sudo flatpak install org.electronjs.Electron2.BaseApp/x86_64/23.08 -y
sudo flatpak install org.freedesktop.Sdk//24.08 -y
sudo flatpak install org.freedesktop.Platform//24.08 -y
sudo flatpak install org.electronjs.Electron2.BaseApp//24.08 -y
- name: Set up environment
uses: ./.github/actions/setup
- name: Build Electron

View File

@@ -54,6 +54,7 @@ jobs:
with:
branch: ${{github.base_ref}}
workflow: build.yml
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
name: build-stats
path: base
@@ -62,6 +63,7 @@ jobs:
with:
pr: ${{github.event.pull_request.number}}
workflow: build.yml
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
name: build-stats
path: head
allow_forks: true

View File

@@ -1,119 +0,0 @@
name: /update-vrt
on:
issue_comment:
types: [created]
permissions:
pull-requests: read
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.issue.number }}-${{ contains(github.event.comment.body, '/update-vrt') }}
cancel-in-progress: true
jobs:
update-vrt:
name: Update VRT
runs-on: ubuntu-latest
if: |
github.event.issue.pull_request &&
contains(github.event.comment.body, '/update-vrt')
container:
image: mcr.microsoft.com/playwright:v1.52.0-jammy
steps:
- name: Get PR branch
# Until https://github.com/xt0rted/pull-request-comment-branch/issues/322 is resolved we use the forked version
uses: gotson/pull-request-comment-branch@head-repo-owner-dist
id: comment-branch
- uses: actions/checkout@v4
with:
repository: ${{ steps.comment-branch.outputs.head_owner }}/${{ steps.comment-branch.outputs.head_repo }}
ref: ${{ steps.comment-branch.outputs.head_ref }}
- name: Set up environment
uses: ./.github/actions/setup
- name: Run VRT Tests on Desktop app
continue-on-error: true
run: |
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- yarn e2e:desktop --update-snapshots
- name: Wait for Netlify build to finish
id: netlify
env:
COMMIT_SHA: ${{ steps.comment-branch.outputs.head_sha }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./.github/actions/netlify-wait-for-build
- name: Run VRT Tests on Netlify URL
run: yarn vrt --update-snapshots
env:
E2E_START_URL: ${{ steps.netlify.outputs.url }}
- name: Create patch
run: |
git config --system --add safe.directory "*"
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git reset
git add "**/*.png"
if git diff --staged --quiet; then
echo "No changes to commit"
exit 0
fi
git commit -m "Update VRT"
git format-patch -1 HEAD --stdout > Update-VRT.patch
- uses: actions/upload-artifact@v4
with:
name: patch
path: Update-VRT.patch
push-patch:
runs-on: ubuntu-latest
needs: update-vrt
permissions:
contents: write
pull-requests: write
steps:
- name: Get PR branch
# Until https://github.com/xt0rted/pull-request-comment-branch/issues/322 is resolved we use the forked version
uses: gotson/pull-request-comment-branch@head-repo-owner-dist
id: comment-branch
- uses: actions/checkout@v4
with:
repository: ${{ steps.comment-branch.outputs.head_owner }}/${{ steps.comment-branch.outputs.head_repo }}
ref: ${{ steps.comment-branch.outputs.head_ref }}
- uses: actions/download-artifact@v4
continue-on-error: true
with:
name: patch
- name: Apply patch and push
env:
BRANCH_NAME: ${{ steps.comment-branch.outputs.head_ref }}
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git apply Update-VRT.patch
git add "**/*.png"
if git diff --staged --quiet; then
echo "No changes to commit"
exit 0
fi
git commit -m "Update VRT"
git push origin HEAD:${BRANCH_NAME}
- name: Add finished reaction
uses: dkershner6/reaction-action@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
commentId: ${{ github.event.comment.id }}
reaction: 'rocket'
add-starting-reaction:
runs-on: ubuntu-latest
if: |
github.event.issue.pull_request &&
contains(github.event.comment.body, '/update-vrt')
permissions:
pull-requests: write
steps:
- name: React to comment
uses: dkershner6/reaction-action@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
commentId: ${{ github.event.comment.id }}
reaction: '+1'

180
.github/workflows/vrt-update-apply.yml vendored Normal file
View File

@@ -0,0 +1,180 @@
name: VRT Update - Apply
# SECURITY: This workflow runs in trusted base repo context.
# It treats the patch artifact as untrusted data, validates it contains only PNGs,
# and safely applies it to the contributor's fork branch.
on:
workflow_run:
workflows: ['VRT Update - Generate']
types:
- completed
permissions:
contents: write
pull-requests: write
jobs:
apply-vrt-updates:
name: Apply VRT Updates
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Download patch artifact
uses: actions/download-artifact@v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
pattern: vrt-patch-*
path: /tmp/artifacts
- name: Download metadata artifact
uses: actions/download-artifact@v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
pattern: vrt-metadata-*
path: /tmp/metadata
- name: Extract metadata
id: metadata
run: |
# Find the metadata directory (will be vrt-metadata-{PR_NUMBER})
METADATA_DIR=$(find /tmp/metadata -mindepth 1 -maxdepth 1 -type d | head -n 1)
if [ -z "$METADATA_DIR" ]; then
echo "No metadata found, skipping..."
exit 0
fi
PR_NUMBER=$(cat "$METADATA_DIR/pr-number.txt")
HEAD_REF=$(cat "$METADATA_DIR/head-ref.txt")
HEAD_REPO=$(cat "$METADATA_DIR/head-repo.txt")
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
echo "head_ref=$HEAD_REF" >> "$GITHUB_OUTPUT"
echo "head_repo=$HEAD_REPO" >> "$GITHUB_OUTPUT"
echo "Found PR #$PR_NUMBER: $HEAD_REPO @ $HEAD_REF"
- name: Checkout fork branch
if: steps.metadata.outputs.pr_number != ''
uses: actions/checkout@v4
with:
repository: ${{ steps.metadata.outputs.head_repo }}
ref: ${{ steps.metadata.outputs.head_ref }}
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
- name: Validate and apply patch
if: steps.metadata.outputs.pr_number != ''
id: apply
run: |
# Find the patch file
PATCH_DIR=$(find /tmp/artifacts -mindepth 1 -maxdepth 1 -type d | head -n 1)
PATCH_FILE="$PATCH_DIR/vrt-update.patch"
if [ ! -f "$PATCH_FILE" ]; then
echo "No patch file found"
exit 0
fi
echo "Found patch file: $PATCH_FILE"
# Validate patch only contains PNG files
echo "Validating patch contains only PNG files..."
if grep -E '^(\+\+\+|---) [ab]/' "$PATCH_FILE" | grep -v '\.png$'; then
echo "ERROR: Patch contains non-PNG files! Rejecting for security."
echo "applied=false" >> "$GITHUB_OUTPUT"
echo "error=Patch validation failed: contains non-PNG files" >> "$GITHUB_OUTPUT"
exit 1
fi
# Extract file list for verification
FILES_CHANGED=$(grep -E '^\+\+\+ b/' "$PATCH_FILE" | sed 's/^+++ b\///' | wc -l)
echo "Patch modifies $FILES_CHANGED PNG file(s)"
# Configure git
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
# Apply patch
echo "Applying patch..."
if git apply --check "$PATCH_FILE" 2>&1; then
git apply "$PATCH_FILE"
# Stage only PNG files (extra safety)
git add "**/*.png"
if git diff --staged --quiet; then
echo "No changes after applying patch"
echo "applied=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# Commit
git commit -m "Update VRT screenshots" -m "Auto-generated by VRT workflow" -m "PR: #${{ steps.metadata.outputs.pr_number }}"
echo "applied=true" >> "$GITHUB_OUTPUT"
else
echo "Patch could not be applied cleanly"
echo "applied=false" >> "$GITHUB_OUTPUT"
echo "error=Patch conflicts with current branch state" >> "$GITHUB_OUTPUT"
exit 1
fi
- name: Push changes
if: steps.apply.outputs.applied == 'true'
env:
HEAD_REF: ${{ steps.metadata.outputs.head_ref }}
HEAD_REPO: ${{ steps.metadata.outputs.head_repo }}
run: |
git push origin "HEAD:refs/heads/$HEAD_REF"
echo "Successfully pushed VRT updates to $HEAD_REPO@$HEAD_REF"
- name: Trigger CI workflows
if: steps.apply.outputs.applied == 'true'
uses: actions/github-script@v7
with:
script: |
// Dispatch a custom event to trigger CI workflows
// This will cause the CI workflows to run in the PR context
try {
await github.rest.repos.createDispatchEvent({
owner: context.repo.owner,
repo: context.repo.repo,
event_type: 'vrt-update-applied',
client_payload: {
pr_number: ${{ steps.metadata.outputs.pr_number }},
head_ref: '${{ steps.metadata.outputs.head_ref }}',
head_repo: '${{ steps.metadata.outputs.head_repo }}'
}
});
console.log('Successfully triggered CI workflows via repository_dispatch');
} catch (error) {
console.log(`Failed to trigger CI workflows: ${error.message}`);
}
- name: Comment on PR - Success
if: steps.apply.outputs.applied == 'true'
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.createComment({
issue_number: ${{ steps.metadata.outputs.pr_number }},
owner: context.repo.owner,
repo: context.repo.repo,
body: '✅ VRT screenshots have been automatically updated and CI workflows have been triggered.'
});
- name: Comment on PR - Failure
if: failure() && steps.metadata.outputs.pr_number != ''
uses: actions/github-script@v7
with:
script: |
const error = `${{ steps.apply.outputs.error }}` || 'Unknown error occurred';
await github.rest.issues.createComment({
issue_number: ${{ steps.metadata.outputs.pr_number }},
owner: context.repo.owner,
repo: context.repo.repo,
body: `❌ Failed to apply VRT updates: ${error}\n\nPlease check the workflow logs for details.`
});

View File

@@ -0,0 +1,105 @@
name: VRT Update - Generate
# SECURITY: This workflow runs in untrusted fork context with no write permissions.
# It only generates VRT patch artifacts that are later applied by vrt-update-apply.yml
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- 'packages/**'
- '.github/workflows/vrt-update-generate.yml'
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
generate-vrt-updates:
name: Generate VRT Updates
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.56.0-jammy
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up environment
uses: ./.github/actions/setup
- name: Run VRT Tests on Desktop app
continue-on-error: true
run: |
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- yarn e2e:desktop --update-snapshots
- name: Wait for Netlify build to finish
id: netlify
env:
COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./.github/actions/netlify-wait-for-build
- name: Run VRT Tests on Netlify URL
continue-on-error: true
run: yarn vrt --update-snapshots
env:
E2E_START_URL: ${{ steps.netlify.outputs.url }}
- name: Create patch with PNG changes only
id: create-patch
run: |
# Trust the repository directory (required for container environments)
git config --global --add safe.directory "$GITHUB_WORKSPACE"
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
# Stage only PNG files
git add "**/*.png"
# Check if there are any changes
if git diff --staged --quiet; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
echo "No VRT changes to commit"
exit 0
fi
echo "has_changes=true" >> "$GITHUB_OUTPUT"
# Create commit and patch
git commit -m "Update VRT screenshots"
git format-patch -1 HEAD --stdout > vrt-update.patch
# Validate patch only contains PNG files
if grep -E '^(\+\+\+|---) [ab]/' vrt-update.patch | grep -v '\.png$'; then
echo "ERROR: Patch contains non-PNG files!"
exit 1
fi
echo "Patch created successfully with PNG changes only"
- name: Upload patch artifact
if: steps.create-patch.outputs.has_changes == 'true'
uses: actions/upload-artifact@v4
with:
name: vrt-patch-${{ github.event.pull_request.number }}
path: vrt-update.patch
retention-days: 5
- name: Save PR metadata
if: steps.create-patch.outputs.has_changes == 'true'
run: |
mkdir -p pr-metadata
echo "${{ github.event.pull_request.number }}" > pr-metadata/pr-number.txt
echo "${{ github.event.pull_request.head.ref }}" > pr-metadata/head-ref.txt
echo "${{ github.event.pull_request.head.repo.full_name }}" > pr-metadata/head-repo.txt
- name: Upload PR metadata
if: steps.create-patch.outputs.has_changes == 'true'
uses: actions/upload-artifact@v4
with:
name: vrt-metadata-${{ github.event.pull_request.number }}
path: pr-metadata/
retention-days: 5

5
.gitignore vendored
View File

@@ -7,9 +7,6 @@ Actual-*
**/xcuserdata/*
export-2020-01-10.csv
# Secrets
.secret-tokens
# MacOS
.DS_Store
@@ -26,6 +23,8 @@ packages/desktop-electron/build
packages/desktop-electron/.electron-symbols
packages/desktop-electron/dist
packages/desktop-electron/loot-core
packages/desktop-client/service-worker
packages/plugins-service/dist
bundle.desktop.js
bundle.desktop.js.map
bundle.mobile.js

View File

@@ -1,2 +0,0 @@
export APPLE_ID=example@email.com
export APPLE_APP_SPECIFIC_PASSWORD=password

942
.yarn/releases/yarn-4.10.3.cjs vendored Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -6,4 +6,4 @@ enableTransparentWorkspaces: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.9.1.cjs
yarnPath: .yarn/releases/yarn-4.10.3.cjs

538
AGENTS.md Normal file
View File

@@ -0,0 +1,538 @@
# AGENTS.md - Guide for AI Agents Working with Actual Budget
This guide provides comprehensive information for AI agents (like Cursor) working with the Actual Budget codebase.
## Project Overview
**Actual Budget** is a local-first personal finance tool written in TypeScript/JavaScript. It's 100% free and open-source with synchronization capabilities across devices.
- **Repository**: https://github.com/actualbudget/actual
- **Community Docs**: https://github.com/actualbudget/docs or https://actualbudget.org/docs
- **License**: MIT
- **Primary Language**: TypeScript (with React)
- **Build System**: Yarn 4 workspaces (monorepo)
## Quick Start Commands
### Essential Commands (Run from Root)
```bash
# Type checking (ALWAYS run before committing)
yarn typecheck
# Linting and formatting (with auto-fix)
yarn lint:fix
# Run all tests
yarn test
# Start development server (browser)
yarn start
# Start with sync server
yarn start:server-dev
# Start desktop app development
yarn start:desktop
```
### Important Rules
- **ALWAYS run yarn commands from the root directory** - never run them in child workspaces
- Use `yarn workspace <workspace-name> run <command>` for workspace-specific tasks
- Include `--watch=false` flag when running unit tests to prevent watch mode
## Architecture & Package Structure
### Core Packages
#### 1. **loot-core** (`packages/loot-core/`)
The core application logic that runs on any platform.
- Business logic, database operations, and calculations
- Platform-agnostic code
- Exports for both browser and node environments
- Test commands:
```bash
yarn workspace loot-core run test --watch=false
```
#### 2. **desktop-client** (`packages/desktop-client/` - aliased as `@actual-app/web`)
The React-based UI for web and desktop.
- React components using functional programming patterns
- E2E tests using Playwright
- Vite for bundling
- Commands:
```bash
# Development
yarn workspace @actual-app/web start:browser
# Build
yarn workspace @actual-app/web build
# E2E tests
yarn workspace @actual-app/web e2e
# Visual regression tests
yarn workspace @actual-app/web vrt
```
#### 3. **desktop-electron** (`packages/desktop-electron/`)
Electron wrapper for the desktop application.
- Window management and native OS integration
- E2E tests for Electron-specific features
#### 4. **api** (`packages/api/` - aliased as `@actual-app/api`)
Public API for programmatic access to Actual.
- Node.js API
- Designed for integrations and automation
- Commands:
```bash
yarn workspace @actual-app/api build
yarn workspace @actual-app/api test --watch=false
```
#### 5. **sync-server** (`packages/sync-server/` - aliased as `@actual-app/sync-server`)
Synchronization server for multi-device support.
- Express-based server
- Currently transitioning to TypeScript (mostly JavaScript)
- Commands:
```bash
yarn workspace @actual-app/sync-server start
```
#### 6. **component-library** (`packages/component-library/` - aliased as `@actual-app/components`)
Reusable React UI components.
- Shared components like Button, Input, Menu, etc.
- Theme system and design tokens
- Icons (375+ icons in SVG/TSX format)
#### 7. **crdt** (`packages/crdt/` - aliased as `@actual-app/crdt`)
CRDT (Conflict-free Replicated Data Type) implementation for data synchronization.
- Protocol buffers for serialization
- Core sync logic
#### 8. **plugins-service** (`packages/plugins-service/`)
Service for handling plugins/extensions.
#### 9. **eslint-plugin-actual** (`packages/eslint-plugin-actual/`)
Custom ESLint rules specific to Actual.
- `no-untranslated-strings`: Enforces i18n usage
- `prefer-trans-over-t`: Prefers Trans component over t() function
- `prefer-logger-over-console`: Enforces using logger instead of console
- `typography`: Typography rules
- `prefer-if-statement`: Prefers explicit if statements
## Development Workflow
### 1. Making Changes
When implementing changes:
1. Read relevant files to understand current implementation
2. Make focused, incremental changes
3. Run type checking: `yarn typecheck`
4. Run linting: `yarn lint:fix`
5. Run relevant tests
6. Fix any linter errors that are introduced
### 2. Testing Strategy
**Unit Tests (Vitest)**
```bash
# All tests
yarn test
# Specific package
yarn workspace loot-core run test --watch=false
# Specific test file
yarn workspace loot-core run test path/to/test.test.ts --watch=false
```
**E2E Tests (Playwright)**
```bash
# Desktop client E2E
yarn workspace @actual-app/web e2e
# Desktop Electron E2E
yarn e2e:desktop
# Visual regression tests
yarn vrt
# Visual regression in Docker (consistent environment)
yarn vrt:docker
```
**Testing Best Practices:**
- Minimize mocked dependencies - prefer real implementations
- Use descriptive test names
- Vitest globals are available: `describe`, `it`, `expect`, `beforeEach`, etc.
- For sync-server tests, globals are explicitly defined in config
### 3. Type Checking
TypeScript configuration uses:
- Incremental compilation
- Strict type checking with `typescript-strict-plugin`
- Platform-specific exports in `loot-core` (node vs browser)
Always run `yarn typecheck` before committing.
### 4. Internationalization (i18n)
- Use `Trans` component instead of `t()` function when possible
- All user-facing strings must be translated
- Generate i18n files: `yarn generate:i18n`
- Custom ESLint rules enforce translation usage
## Code Style & Conventions
### TypeScript Guidelines
**Type Usage:**
- Use TypeScript for all code
- Prefer `type` over `interface`
- Avoid `enum` - use objects or maps
- Avoid `any` or `unknown` unless absolutely necessary
- Look for existing type definitions in the codebase
- Avoid type assertions (`as`, `!`) - prefer `satisfies`
- Use inline type imports: `import { type MyType } from '...'`
**Naming:**
- Use descriptive variable names with auxiliary verbs (e.g., `isLoaded`, `hasError`)
- Named exports for components and utilities (avoid default exports except in specific cases)
**Code Structure:**
- Functional and declarative programming patterns - avoid classes
- Use the `function` keyword for pure functions
- Prefer iteration and modularization over code duplication
- Structure files: exported component/page, helpers, static content, types
- Create new components in their own files
**React Patterns:**
- Don't use `React.FunctionComponent` or `React.FC` - type props directly
- Don't use `React.*` patterns - use named imports instead
- Use `<Link>` instead of `<a>` tags
- Use custom hooks from `src/hooks` (not react-router directly):
- `useNavigate()` from `src/hooks` (not react-router)
- `useDispatch()`, `useSelector()`, `useStore()` from `src/redux` (not react-redux)
- Avoid unstable nested components
- Use `satisfies` for type narrowing
**JSX Style:**
- Declarative JSX, minimal and readable
- Avoid unnecessary curly braces in conditionals
- Use concise syntax for simple statements
- Prefer explicit expressions (`condition && <Component />`)
### Import Organization
Imports are automatically organized by ESLint with the following order:
1. React imports (first)
2. Built-in Node.js modules
3. External packages
4. Actual packages (`loot-core`, `@actual-app/components` - legacy pattern `loot-design` may appear in old code)
5. Parent imports
6. Sibling imports
7. Index imports
Always maintain newlines between import groups.
### Platform-Specific Code
- Don't directly reference platform-specific imports (`.api`, `.web`, `.electron`)
- Use conditional exports in `loot-core` for platform-specific code
- Platform resolution happens at build time via package.json exports
### Restricted Patterns
**Never:**
- Use `console.*` (use logger instead - enforced by ESLint)
- Import from `uuid` without destructuring: use `import { v4 as uuidv4 } from 'uuid'`
- Import colors directly - use theme instead
- Import `@actual-app/web/*` in `loot-core`
**Git Commands:**
- Never update git config
- Never run destructive git operations (force push, hard reset) unless explicitly requested
- Never skip hooks (--no-verify, --no-gpg-sign)
- Never force push to main/master
- Never commit unless explicitly asked
## File Structure Patterns
### Typical Component File
```typescript
import { type ComponentType } from 'react';
// ... other imports
type MyComponentProps = {
// Props definition
};
export function MyComponent({ prop1, prop2 }: MyComponentProps) {
// Component logic
return (
// JSX
);
}
```
### Test File
```typescript
import { describe, it, expect, beforeEach } from 'vitest';
// ... imports
describe('ComponentName', () => {
it('should behave as expected', () => {
// Test logic
expect(result).toBe(expected);
});
});
```
## Important Directories & Files
### Configuration Files
- `/package.json` - Root workspace configuration, scripts
- `/eslint.config.mjs` - ESLint configuration (flat config format)
- `/tsconfig.json` - Root TypeScript configuration
- `/.cursorignore`, `/.gitignore` - Ignored files
- `/yarn.lock` - Dependency lockfile (Yarn 4)
### Documentation
- `/README.md` - Project overview
- `/CONTRIBUTING.md` - Points to community docs
- `/upcoming-release-notes/` - Release notes for next version
- `/CODEOWNERS` - Code ownership definitions
### Build Artifacts (Don't Edit)
- `packages/*/lib-dist/` - Built output
- `packages/*/dist/` - Built output
- `packages/*/build/` - Built output
- `packages/desktop-client/playwright-report/` - Test reports
- `packages/desktop-client/test-results/` - Test results
### Key Source Directories
- `packages/loot-core/src/client/` - Client-side core logic
- `packages/loot-core/src/server/` - Server-side core logic
- `packages/loot-core/src/shared/` - Shared utilities
- `packages/loot-core/src/types/` - Type definitions
- `packages/desktop-client/src/components/` - React components
- `packages/desktop-client/src/hooks/` - Custom React hooks
- `packages/desktop-client/e2e/` - End-to-end tests
- `packages/component-library/src/` - Reusable components
- `packages/component-library/src/icons/` - Icon components (auto-generated, don't edit)
## Common Development Tasks
### Running Specific Tests
```bash
# Unit test for a specific file in loot-core
yarn workspace loot-core run test src/path/to/file.test.ts --watch=false
# E2E test for a specific file
yarn workspace @actual-app/web run playwright test accounts.test.ts --browser=chromium
```
### Building for Production
```bash
# Browser build
yarn build:browser
# Desktop build
yarn build:desktop
# API build
yarn build:api
# Sync server build
yarn build:server
```
### Type Checking Specific Packages
TypeScript uses project references. Run `yarn typecheck` from root to check all packages.
### Debugging Tests
```bash
# Run tests in debug mode (without parallelization)
yarn test:debug
# Run specific E2E test with headed browser
yarn workspace @actual-app/web run playwright test --headed --debug accounts.test.ts
```
### Working with Icons
Icons in `packages/component-library/src/icons/` are auto-generated. Don't manually edit them.
## Troubleshooting
### Type Errors
1. Run `yarn typecheck` to see all type errors
2. Check if types are imported correctly
3. Look for existing type definitions in `packages/loot-core/src/types/`
4. Use `satisfies` instead of `as` for type narrowing
### Linter Errors
1. Run `yarn lint:fix` to auto-fix many issues
2. Check ESLint output for specific rule violations
3. Custom rules:
- `actual/no-untranslated-strings` - Add i18n
- `actual/prefer-trans-over-t` - Use Trans component
- `actual/prefer-logger-over-console` - Use logger
- Check `eslint.config.mjs` for complete rules
### Test Failures
1. Check if test is running in correct environment (node vs web)
2. For Vitest: check `vitest.config.ts` or `vitest.web.config.ts`
3. For Playwright: check `playwright.config.ts`
4. Ensure mock minimization - prefer real implementations
### Import Resolution Issues
1. Check `tsconfig.json` for path mappings
2. Check package.json `exports` field (especially for loot-core)
3. Verify platform-specific imports (`.web`, `.electron`, `.api`)
4. Use absolute imports in `desktop-client` (enforced by ESLint)
### Build Failures
1. Clean build artifacts: `rm -rf packages/*/dist packages/*/lib-dist packages/*/build`
2. Reinstall dependencies: `yarn install`
3. Check Node.js version (requires >=20)
4. Check Yarn version (requires ^4.9.1)
## Testing Patterns
### Unit Tests
- Located alongside source files or in `__tests__` directories
- Use `.test.ts`, `.test.tsx`, `.spec.js` extensions
- Vitest is the test runner
- Minimize mocking - prefer real implementations
### E2E Tests
- Located in `packages/desktop-client/e2e/`
- Use Playwright test runner
- Visual regression snapshots in `*-snapshots/` directories
- Page models in `e2e/page-models/` for reusable page interactions
- Mobile tests have `.mobile.test.ts` suffix
### Visual Regression Tests (VRT)
- Run with `VRT=true` environment variable
- Snapshots stored per test file
- Use Docker for consistent environment: `yarn vrt:docker`
## Additional Resources
- **Community Documentation**: https://actualbudget.org/docs/contributing/
- **Discord Community**: https://discord.gg/pRYNYr4W5A
- **GitHub Issues**: https://github.com/actualbudget/actual/issues
- **Feature Requests**: Label "needs votes" sorted by reactions
## Code Quality Checklist
Before committing changes, ensure:
- [ ] `yarn typecheck` passes
- [ ] `yarn lint:fix` has been run
- [ ] Relevant tests pass
- [ ] No new console.\* usage (use logger)
- [ ] User-facing strings are translated
- [ ] Prefer `type` over `interface`
- [ ] Named exports used (not default exports)
- [ ] Imports are properly ordered
- [ ] Platform-specific code uses proper exports
- [ ] No unnecessary type assertions
## Pull Request Guidelines
When creating pull requests:
- **AI-Generated PRs**: If you create a PR using AI assistance, add the **"AI generated"** label to the pull request. This helps maintainers understand the nature of the contribution.
## Performance Considerations
- **Bundle Size**: Check with rollup-plugin-visualizer
- **Type Checking**: Uses incremental compilation
- **Testing**: Tests run in parallel by default
- **Linting**: ESLint caches results for faster subsequent runs
## Workspace Commands Reference
```bash
# List all workspaces
yarn workspaces list
# Run command in specific workspace
yarn workspace <workspace-name> run <command>
# Run command in all workspaces
yarn workspaces foreach --all run <command>
# Install production dependencies only (for server deployment)
yarn install:server
```
## Environment Requirements
- **Node.js**: >=20
- **Yarn**: ^4.9.1 (managed by packageManager field)
- **Browser Targets**: Electron >= 35.0, modern browsers (see browserslist)
## Migration Notes
The codebase is actively being migrated:
- **JavaScript → TypeScript**: sync-server is in progress
- **Classes → Functions**: Prefer functional patterns
- **React.\* → Named Imports**: Legacy React.\* patterns being removed
When working with older code, follow the newer patterns described in this guide.

View File

@@ -16,6 +16,7 @@ packages/desktop-client/bin/remove-untranslated-languages
export NODE_OPTIONS="--max-old-space-size=4096"
yarn workspace plugins-service build
yarn workspace loot-core build:browser
yarn workspace @actual-app/web build:browser

View File

@@ -41,6 +41,7 @@ packages/desktop-client/bin/remove-untranslated-languages
export NODE_OPTIONS="--max-old-space-size=4096"
yarn workspace plugins-service build
yarn workspace loot-core build:node
yarn workspace @actual-app/web build --mode=desktop # electron specific build
@@ -61,14 +62,11 @@ yarn workspace desktop-electron update-client
echo "Skipping exe build"
else
if [ "$RELEASE" == "production" ]; then
if [ -f ../../.secret-tokens ]; then
source ../../.secret-tokens
fi
yarn build
echo "Created release"
else
SKIP_NOTARIZATION=true yarn build
yarn build
fi
fi
)

View File

@@ -28,5 +28,5 @@ echo "Running VRT tests with the following parameters:"
echo "E2E_START_URL: $E2E_START_URL"
echo "VRT_ARGS: $VRT_ARGS"
MSYS_NO_PATHCONV=1 docker run --rm --network host -v "$(pwd)":/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.52.0-jammy /bin/bash \
MSYS_NO_PATHCONV=1 docker run --rm --network host -v "$(pwd)":/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.56.0-jammy /bin/bash \
-c "E2E_START_URL=$E2E_START_URL yarn vrt $VRT_ARGS"

View File

@@ -83,6 +83,7 @@ export default pluginTypescript.config(
'packages/component-library/src/icons/**/*',
'packages/desktop-client/bundle.browser.js',
'packages/desktop-client/build/',
'packages/desktop-client/service-worker/*',
'packages/desktop-client/build-electron/',
'packages/desktop-client/build-stats/',
'packages/desktop-client/public/kcab/',
@@ -98,6 +99,7 @@ export default pluginTypescript.config(
'packages/loot-core/**/lib-dist/*',
'packages/loot-core/**/proto/*',
'packages/sync-server/build/',
'packages/plugins-service/dist/',
'.yarn/*',
'.github/*',
],

View File

@@ -23,17 +23,20 @@
"start:server-monitor": "yarn workspace @actual-app/sync-server start-monitor",
"start:server-dev": "NODE_ENV=development BROWSER_OPEN=localhost:5006 yarn npm-run-all --parallel 'start:server-monitor' 'start'",
"start:desktop": "yarn desktop-dependencies && npm-run-all --parallel 'start:desktop-*'",
"desktop-dependencies": "yarn rebuild-electron && yarn workspace loot-core build:browser",
"desktop-dependencies": "npm-run-all --parallel rebuild-electron build:browser-backend build:plugins-service",
"start:desktop-node": "yarn workspace loot-core watch:node",
"start:desktop-client": "yarn workspace @actual-app/web watch",
"start:desktop-server-client": "yarn workspace @actual-app/web build:browser",
"start:desktop-electron": "yarn workspace desktop-electron watch",
"start:browser": "npm-run-all --parallel 'start:browser-*'",
"start:browser": "yarn workspace plugins-service build-dev && npm-run-all --parallel 'start:browser-*'",
"start:service-plugins": "yarn workspace plugins-service watch",
"start:browser-backend": "yarn workspace loot-core watch:browser",
"start:browser-frontend": "yarn workspace @actual-app/web start:browser",
"build:browser-backend": "yarn workspace loot-core build:browser",
"build:server": "yarn build:browser && yarn workspace @actual-app/sync-server build",
"build:browser": "./bin/package-browser",
"build:desktop": "./bin/package-electron",
"build:plugins-service": "yarn workspace plugins-service build",
"build:api": "yarn workspace @actual-app/api build",
"generate:i18n": "yarn workspace @actual-app/web generate:i18n",
"generate:release-notes": "ts-node ./bin/release-note-generator.ts",
@@ -44,7 +47,7 @@
"playwright": "yarn workspace @actual-app/web run playwright",
"vrt": "yarn workspaces foreach --all --parallel --verbose run vrt",
"vrt:docker": "./bin/run-vrt",
"rebuild-electron": "./node_modules/.bin/electron-rebuild -f -m ./packages/loot-core",
"rebuild-electron": "./node_modules/.bin/electron-rebuild -m ./packages/loot-core",
"rebuild-node": "yarn workspace loot-core rebuild",
"lint": "prettier --check . && eslint . --max-warnings 0",
"lint:fix": "prettier --check --write . && eslint . --max-warnings 0 --fix",
@@ -55,32 +58,32 @@
},
"devDependencies": {
"@octokit/rest": "^22.0.0",
"@types/node": "^22.17.0",
"@types/node": "^22.18.8",
"@types/prompts": "^2.4.9",
"@typescript-eslint/parser": "^8.42.0",
"cross-env": "^7.0.3",
"eslint": "^9.34.0",
"eslint-config-prettier": "^10.1.5",
"eslint-import-resolver-typescript": "^4.3.5",
"eslint-plugin-import": "^2.31.0",
"@typescript-eslint/parser": "^8.46.0",
"cross-env": "^10.1.0",
"eslint": "^9.37.0",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^6.0.0-rc.2",
"eslint-plugin-react-hooks": "^6.1.1",
"eslint-plugin-typescript-paths": "^0.0.33",
"globals": "^15.15.0",
"globals": "^16.4.0",
"html-to-image": "^1.11.13",
"husky": "^9.1.7",
"lint-staged": "^15.5.2",
"lint-staged": "^16.2.3",
"minimatch": "^10.0.3",
"node-jq": "^6.0.1",
"node-jq": "^6.3.1",
"npm-run-all": "^4.1.5",
"p-limit": "^6.2.0",
"prettier": "^3.5.3",
"p-limit": "^7.1.1",
"prettier": "^3.6.2",
"prompts": "^2.4.2",
"source-map-support": "^0.5.21",
"ts-node": "^10.9.2",
"typescript": "^5.9.2",
"typescript-eslint": "^8.42.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.0",
"typescript-strict-plugin": "^2.4.4"
},
"resolutions": {
@@ -97,7 +100,7 @@
"prettier --write"
]
},
"packageManager": "yarn@4.9.1",
"packageManager": "yarn@4.10.3",
"browserslist": [
"electron >= 35.0",
"defaults"

View File

@@ -24,14 +24,14 @@
},
"dependencies": {
"@actual-app/crdt": "workspace:^",
"better-sqlite3": "^12.2.0",
"better-sqlite3": "^12.4.1",
"compare-versions": "^6.1.1",
"node-fetch": "^3.3.2",
"uuid": "^11.1.0"
"uuid": "^13.0.0"
},
"devDependencies": {
"tsc-alias": "^1.8.16",
"typescript": "^5.9.2",
"typescript": "^5.9.3",
"vitest": "^3.2.4"
}
}

View File

@@ -1,7 +0,0 @@
export function amountToInteger(n) {
return Math.round(n * 100);
}
export function integerToAmount(n) {
return parseFloat((n / 100).toFixed(2));
}

6
packages/api/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
// @ts-ignore: bundle not available until we build it
// eslint-disable-next-line import/extensions
import * as bundle from './app/bundle.api.js';
export const amountToInteger = bundle.lib.amountToInteger;
export const integerToAmount = bundle.lib.integerToAmount;

View File

@@ -8,14 +8,14 @@
},
"dependencies": {
"@emotion/css": "^11.13.5",
"react-aria-components": "^1.8.0",
"react-aria-components": "^1.13.0",
"usehooks-ts": "^3.1.1"
},
"devDependencies": {
"@svgr/cli": "^8.1.0",
"@types/react": "^19.1.12",
"react": "19.1.1",
"react-dom": "19.1.1",
"@types/react": "^19.2.2",
"react": "19.2.0",
"react-dom": "19.2.0",
"vitest": "^3.2.4"
},
"exports": {

View File

@@ -17,13 +17,13 @@
"dependencies": {
"google-protobuf": "^3.21.4",
"murmurhash": "^2.0.1",
"uuid": "^11.1.0"
"uuid": "^13.0.0"
},
"devDependencies": {
"@types/google-protobuf": "^3.15.12",
"protoc-gen-js": "^3.21.4-4",
"ts-protoc-gen": "^0.15.0",
"typescript": "^5.9.2",
"typescript": "^5.9.3",
"vitest": "^3.2.4"
}
}

View File

@@ -14,6 +14,9 @@ build-electron
build-stats
stats.json
# generated service worker
service-worker/
# misc
.DS_Store
.env

View File

@@ -65,10 +65,10 @@ Run manually:
```sh
# Run docker container
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.52.0-jammy /bin/bash
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.56.0-jammy /bin/bash
# If you receive an error such as "docker: invalid reference format", please instead use the following command:
docker run --rm --network host -v ${pwd}:/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.52.0-jammy /bin/bash
docker run --rm --network host -v ${pwd}:/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.56.0-jammy /bin/bash
# Once inside the docker container, run the VRT tests: important - they MUST be ran against a HTTPS server.
# Use the ip and port noted earlier

View File

@@ -9,6 +9,7 @@ rm -fr build
export IS_GENERIC_BROWSER=1
export REACT_APP_BACKEND_WORKER_HASH=`ls "$ROOT"/../public/kcab/kcab.worker.*.js | sed 's/.*kcab\.worker\.\(.*\)\.js/\1/'`
export REACT_APP_PLUGIN_SERVICE_WORKER_HASH=`ls "$ROOT"/../service-worker/plugin-sw.*.js | sed 's/.*plugin-sw\.\(.*\)\.js/\1/'`
yarn build

View File

@@ -6,5 +6,6 @@ cd "$ROOT/.."
export IS_GENERIC_BROWSER=1
export PORT=3001
export REACT_APP_BACKEND_WORKER_HASH="dev"
export REACT_APP_PLUGIN_SERVICE_WORKER_HASH="dev"
yarn start

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View File

@@ -3,6 +3,7 @@ import { type Locator, type Page } from '@playwright/test';
import { MobileAccountPage } from './mobile-account-page';
import { MobileAccountsPage } from './mobile-accounts-page';
import { MobileBudgetPage } from './mobile-budget-page';
import { MobilePayeesPage } from './mobile-payees-page';
import { MobileReportsPage } from './mobile-reports-page';
import { MobileRulesPage } from './mobile-rules-page';
import { MobileTransactionEntryPage } from './mobile-transaction-entry-page';
@@ -21,6 +22,7 @@ const ROUTES_BY_PAGE = {
Accounts: '/accounts',
Transaction: '/transactions/new',
Reports: '/reports',
Payees: '/payees',
Rules: '/rules',
Settings: '/settings',
};
@@ -166,6 +168,13 @@ export class MobileNavigation {
);
}
async goToPayeesPage() {
return await this.navigateToPage(
'Payees',
() => new MobilePayeesPage(this.page),
);
}
async goToRulesPage() {
return await this.navigateToPage(
'Rules',

View File

@@ -0,0 +1,75 @@
import { type Locator, type Page } from '@playwright/test';
export class MobilePayeesPage {
readonly page: Page;
readonly searchBox: Locator;
readonly payeesList: Locator;
readonly emptyMessage: Locator;
readonly loadingIndicator: Locator;
constructor(page: Page) {
this.page = page;
this.searchBox = page.getByPlaceholder('Filter payees…');
this.payeesList = page.getByRole('grid', { name: 'Payees' });
this.emptyMessage = page.getByText('No payees found.');
this.loadingIndicator = page.getByTestId('animated-loading');
}
async waitFor(options?: {
state?: 'attached' | 'detached' | 'visible' | 'hidden';
timeout?: number;
}) {
await this.payeesList.waitFor(options);
}
/**
* Search for payees using the search box
*/
async searchFor(text: string) {
await this.searchBox.fill(text);
}
/**
* Clear the search box
*/
async clearSearch() {
await this.searchBox.fill('');
}
/**
* Get the nth payee item (0-based index)
*/
getNthPayee(index: number) {
return this.getAllPayees().nth(index);
}
/**
* Get all visible payee items
*/
getAllPayees() {
return this.payeesList.getByRole('gridcell');
}
/**
* Click on a payee to view/edit rules
*/
async clickPayee(index: number) {
const payee = this.getNthPayee(index);
await payee.click();
}
/**
* Get the number of visible payees
*/
async getPayeeCount() {
const payees = this.getAllPayees();
return await payees.count();
}
/**
* Wait for loading to complete
*/
async waitForLoadingToComplete(timeout: number = 10000) {
await this.loadingIndicator.waitFor({ state: 'hidden', timeout });
}
}

View File

@@ -40,17 +40,14 @@ export class MobileRulesPage {
* Get the nth rule item (0-based index)
*/
getNthRule(index: number) {
return this.page
.getByRole('button')
.filter({ hasText: /IF|THEN/ })
.nth(index);
return this.getAllRules().nth(index);
}
/**
* Get all visible rule items
*/
getAllRules() {
return this.page.getByRole('button').filter({ hasText: /IF|THEN/ });
return this.page.getByRole('grid', { name: 'Rules' }).getByRole('row');
}
/**
@@ -76,28 +73,6 @@ export class MobileRulesPage {
return await rules.count();
}
/**
* Check if the search bar has a border
*/
async hasSearchBarBorder() {
const searchContainer = this.searchBox.locator('..');
const borderStyle = await searchContainer.evaluate(el => {
const style = window.getComputedStyle(el);
return style.borderBottomWidth;
});
return borderStyle === '2px';
}
/**
* Get the background color of the search box
*/
async getSearchBackgroundColor() {
return await this.searchBox.evaluate(el => {
const style = window.getComputedStyle(el);
return style.backgroundColor;
});
}
/**
* Check if a rule contains specific text
*/
@@ -112,7 +87,7 @@ export class MobileRulesPage {
*/
async getRuleStage(index: number) {
const rule = this.getNthRule(index);
const stageBadge = rule.locator('span').first();
const stageBadge = rule.getByTestId('rule-stage-badge');
return await stageBadge.textContent();
}
}

View File

@@ -122,7 +122,15 @@ export class RulesPage {
if (op && !fieldFirst) {
await row.getByTestId('op-select').getByRole('button').first().click();
await this.page.getByRole('button', { name: op, exact: true }).click();
await this.page
.getByRole('button', { name: op, exact: true })
.first()
.waitFor({ state: 'visible' });
await this.page
.getByRole('button', { name: op, exact: true })
.first()
.click({ force: true });
}
if (field) {
@@ -133,12 +141,26 @@ export class RulesPage {
.click();
await this.page
.getByRole('button', { name: field, exact: true })
.click();
.first()
.waitFor({ state: 'visible' });
await this.page
.getByRole('button', { name: field, exact: true })
.first()
.click({ force: true });
}
if (op && fieldFirst) {
await row.getByTestId('op-select').getByRole('button').first().click();
await this.page.getByRole('button', { name: op, exact: true }).click();
await this.page
.getByRole('button', { name: op, exact: true })
.first()
.waitFor({ state: 'visible' });
await this.page
.getByRole('button', { name: op, exact: true })
.first()
.click({ force: true });
}
if (value) {

View File

@@ -28,7 +28,10 @@ export class SchedulesPage {
await this._fillScheduleFields(data);
await this.page.getByRole('button', { name: 'Add' }).click();
await this.page
.getByTestId('schedule-edit-modal')
.getByRole('button', { name: 'Add' })
.click();
}
/**

View File

@@ -0,0 +1,112 @@
import { type Page } from '@playwright/test';
import { expect, test } from './fixtures';
import { ConfigurationPage } from './page-models/configuration-page';
import { MobileNavigation } from './page-models/mobile-navigation';
import { type MobilePayeesPage } from './page-models/mobile-payees-page';
test.describe('Mobile Payees', () => {
let page: Page;
let navigation: MobileNavigation;
let payeesPage: MobilePayeesPage;
let configurationPage: ConfigurationPage;
test.beforeEach(async ({ browser }) => {
page = await browser.newPage();
navigation = new MobileNavigation(page);
configurationPage = new ConfigurationPage(page);
// Set mobile viewport
await page.setViewportSize({
width: 350,
height: 600,
});
await page.goto('/');
await configurationPage.createTestFile();
// Navigate to payees page and wait for it to load
payeesPage = await navigation.goToPayeesPage();
});
test.afterEach(async () => {
await page.close();
});
test('checks the page visuals', async () => {
await payeesPage.waitForLoadingToComplete();
// Check that the header is present
await expect(page.getByRole('heading', { name: 'Payees' })).toBeVisible();
// Check that the search box is present with proper placeholder
await expect(payeesPage.searchBox).toBeVisible();
await expect(payeesPage.searchBox).toHaveAttribute(
'placeholder',
'Filter payees…',
);
const payeeCount = await payeesPage.getPayeeCount();
expect(payeeCount).toBeGreaterThan(0);
await expect(page).toMatchThemeScreenshots();
});
test('filters out unrelated payees', async () => {
await payeesPage.searchFor('asdfasdf-nonsense');
// Get the text 'No payees found.' from the page
const noPayeesMessage = page.getByText('No payees found.');
// Assert it is visible
await expect(noPayeesMessage).toBeVisible();
await expect(page).toMatchThemeScreenshots();
});
test('clicking on a payee opens rule creation form', async () => {
await payeesPage.waitForLoadingToComplete();
const payeeCount = await payeesPage.getPayeeCount();
expect(payeeCount).toBeGreaterThan(0);
await payeesPage.clickPayee(0);
// Should navigate to rules page for creating a new rule
await expect(page).toHaveURL(/\/rules/);
await expect(page).toMatchThemeScreenshots();
});
test('page handles empty state gracefully', async () => {
// Search for something that won't match to get empty state
await payeesPage.searchFor('NonExistentPayee123456789');
await page.waitForTimeout(500);
// Check that empty message is shown
const emptyMessage = page.getByText('No payees found.');
await expect(emptyMessage).toBeVisible();
// Check that no payee items are visible
const payees = payeesPage.getAllPayees();
await expect(payees).toHaveCount(0);
await expect(page).toMatchThemeScreenshots();
});
test('search functionality works correctly', async () => {
await payeesPage.waitForLoadingToComplete();
// Test searching for a specific payee
await payeesPage.searchFor('Fast Internet');
// Should show at least one result
const payeeCount = await payeesPage.getPayeeCount();
expect(payeeCount).toBeGreaterThan(0);
await expect(page).toMatchThemeScreenshots();
// Clear search
await payeesPage.clearSearch();
// Should show all payees again
const allPayeeCount = await payeesPage.getPayeeCount();
expect(allPayeeCount).toBeGreaterThan(payeeCount);
await expect(page).toMatchThemeScreenshots();
});
});

View File

@@ -33,7 +33,17 @@ test.describe.parallel('Reports', () => {
test('loads net worth and cash flow reports', async () => {
const reports = await reportsPage.getAvailableReportList();
expect(reports).toEqual(['Net Worth', 'Cash Flow', 'Monthly Spending']);
expect(reports).toEqual([
'Total Income (YTD)',
'Total Expenses (YTD)',
'Avg Per Month',
'Avg Per Transaction',
'Net Worth',
'Cash Flow',
'This Month',
'Budget Overview',
'3-Month Average',
]);
await expect(page).toMatchThemeScreenshots();
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -155,6 +155,8 @@ test.describe('Transactions', () => {
await expect(transaction.category.locator('input')).toHaveValue('Transfer');
await expect(page).toMatchThemeScreenshots();
const balanceBeforeTransaction =
await accountPage.accountBalance.textContent();
await accountPage.addEnteredTransaction();
transaction = accountPage.getNthTransaction(0);
@@ -163,6 +165,14 @@ test.describe('Transactions', () => {
await expect(transaction.category).toHaveText('Transfer');
await expect(transaction.debit).toHaveText('12.34');
await expect(transaction.credit).toHaveText('');
// Wait for balance to update after adding transaction
await expect(async () => {
const balanceAfterTransaction =
await accountPage.accountBalance.textContent();
expect(balanceAfterTransaction).not.toBe(balanceBeforeTransaction);
}).toPass();
await expect(page).toMatchThemeScreenshots();
});
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -7,7 +7,6 @@
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
/>
<title>Actual</title>
<link rel="canonical" href="/" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
@@ -108,10 +107,6 @@
min-height: 0;
min-width: 0;
}
.js-focus-visible :focus:not(.focus-visible) {
outline: 0;
}
</style>
</head>
<body>

View File

@@ -8,38 +8,36 @@
"devDependencies": {
"@actual-app/components": "workspace:*",
"@emotion/css": "^11.13.5",
"@fontsource/redacted-script": "^5.2.5",
"@fontsource/redacted-script": "^5.2.8",
"@juggle/resize-observer": "^3.4.0",
"@playwright/test": "1.52.0",
"@playwright/test": "1.56.0",
"@rollup/plugin-inject": "^5.0.5",
"@swc/core": "^1.11.24",
"@swc/core": "^1.13.5",
"@swc/helpers": "^0.5.17",
"@testing-library/dom": "10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/dom": "10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "16.3.0",
"@testing-library/user-event": "14.6.1",
"@types/lodash": "^4",
"@types/promise-retry": "^1.1.6",
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.1",
"@types/react-grid-layout": "^1",
"@types/react-modal": "^3.16.3",
"@use-gesture/react": "^10.3.1",
"@vitejs/plugin-basic-ssl": "^1.2.0",
"@vitejs/plugin-react": "^5.0.2",
"@vitejs/plugin-basic-ssl": "^2.1.0",
"@vitejs/plugin-react": "^5.0.4",
"auto-text-size": "^0.2.3",
"babel-plugin-react-compiler": "19.1.0-rc.3",
"chokidar": "^3.6.0",
"cmdk": "^1.1.1",
"cross-env": "^7.0.3",
"cross-env": "^10.1.0",
"date-fns": "^4.1.0",
"downshift": "7.6.2",
"focus-visible": "^4.1.5",
"i18next": "^25.2.1",
"downshift": "9.0.10",
"i18next": "^25.5.3",
"i18next-parser": "^9.3.0",
"i18next-resources-to-backend": "^1.2.1",
"inter-ui": "^3.19.3",
"jsdom": "^26.1.0",
"jsdom": "^27.0.0",
"lodash": "^4.17.21",
"loot-core": "workspace:*",
"mdast-util-newline-to-break": "^2.0.0",
@@ -48,35 +46,34 @@
"promise-retry": "^2.0.1",
"prop-types": "^15.8.1",
"re-resizable": "^6.11.2",
"react": "19.1.1",
"react-aria": "^3.39.0",
"react-aria-components": "^1.8.0",
"react": "19.2.0",
"react-aria": "^3.44.0",
"react-aria-components": "^1.13.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "19.1.1",
"react-error-boundary": "^5.0.0",
"react-grid-layout": "^1.5.1",
"react-hotkeys-hook": "^4.6.2",
"react-i18next": "^15.5.3",
"react-dom": "19.2.0",
"react-error-boundary": "^6.0.0",
"react-grid-layout": "^1.5.2",
"react-hotkeys-hook": "^5.1.0",
"react-i18next": "^16.0.0",
"react-markdown": "^10.1.0",
"react-modal": "3.16.3",
"react-redux": "^9.2.0",
"react-router": "7.6.2",
"react-router": "7.9.4",
"react-simple-pull-to-refresh": "^1.3.3",
"react-spring": "^10.0.0",
"react-stately": "^3.37.0",
"react-spring": "10.0.0",
"react-swipeable": "^7.0.2",
"react-virtualized-auto-sizer": "^1.0.26",
"recharts": "^2.15.3",
"recharts": "^3.2.1",
"rehype-external-links": "^3.0.0",
"remark-gfm": "^4.0.1",
"rollup-plugin-visualizer": "^5.14.0",
"sass": "^1.89.0",
"rollup-plugin-visualizer": "^6.0.4",
"sass": "^1.93.2",
"usehooks-ts": "^3.1.1",
"uuid": "^11.1.0",
"vite": "^6.3.6",
"vite-plugin-pwa": "^1.0.0",
"vite-tsconfig-paths": "^4.3.2",
"uuid": "^13.0.0",
"vite": "^7.1.9",
"vite-plugin-pwa": "^1.0.3",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4",
"xml2js": "^0.6.2"
},

View File

@@ -178,7 +178,6 @@ global.Actual = {
// Wait for the app to reload
await new Promise(() => {});
},
updateAppMenu: () => {},
ipcConnect: () => {},
getServerSocket: async () => {
@@ -191,35 +190,3 @@ global.Actual = {
moveBudgetDirectory: () => {},
};
function inputFocused(e) {
return (
e.target.tagName === 'INPUT' ||
e.target.tagName === 'TEXTAREA' ||
e.target.isContentEditable
);
}
document.addEventListener('keydown', e => {
if (e.metaKey || e.ctrlKey) {
// Cmd/Ctrl+o
if (e.key === 'o') {
e.preventDefault();
window.__actionsForMenu.closeBudget();
}
// Cmd/Ctrl+z
else if (e.key.toLowerCase() === 'z') {
if (inputFocused(e)) {
return;
}
e.preventDefault();
if (e.shiftKey) {
// Redo
window.__actionsForMenu.redo();
} else {
// Undo
window.__actionsForMenu.undo();
}
}
}
});

View File

@@ -212,10 +212,28 @@ export const moveCategoryGroup = createAppAsyncThunk(
},
);
function translateCategories(
categories: CategoryEntity[] | undefined,
): CategoryEntity[] | undefined {
return categories?.map(cat => ({
...cat,
name:
cat.name?.toLowerCase() === 'starting balances'
? t('Starting Balances')
: cat.name,
}));
}
export const getCategories = createAppAsyncThunk(
`${sliceName}/getCategories`,
async () => {
const categories: CategoryViews = await send('get-categories');
categories.list = translateCategories(categories.list) as CategoryEntity[];
categories.grouped.forEach(group => {
group.categories = translateCategories(
group.categories,
) as CategoryEntity[];
});
return categories;
},
{
@@ -233,6 +251,12 @@ export const reloadCategories = createAppAsyncThunk(
`${sliceName}/reloadCategories`,
async () => {
const categories: CategoryViews = await send('get-categories');
categories.list = translateCategories(categories.list) as CategoryEntity[];
categories.grouped.forEach(group => {
group.categories = translateCategories(
group.categories,
) as CategoryEntity[];
});
return categories;
},
);
@@ -556,6 +580,7 @@ export const getCategoriesById = memoizeOne(
res[cat.id] = cat;
});
});
return res;
},
);
@@ -584,6 +609,12 @@ function _loadCategories(
categories: BudgetState['categories'],
) {
state.categories = categories;
categories.list = translateCategories(categories.list) as CategoryEntity[];
categories.grouped.forEach(group => {
group.categories = translateCategories(
group.categories,
) as CategoryEntity[];
});
state.isCategoriesLoading = false;
state.isCategoriesLoaded = true;
state.isCategoriesDirty = false;

View File

@@ -498,7 +498,7 @@ function sortFiles(arr: File[]) {
let i = name1 < name2 ? -1 : name1 > name2 ? 1 : 0;
if (i === 0) {
const xId = x.state === 'remote' ? x.cloudFileId : x.id;
const yId = x.state === 'remote' ? x.cloudFileId : x.id;
const yId = y.state === 'remote' ? y.cloudFileId : y.id;
i = xId < yId ? -1 : xId > yId ? 1 : 0;
}
return i;

View File

@@ -135,10 +135,6 @@ function AppInner() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch, showErrorBoundary]);
useEffect(() => {
global.Actual.updateAppMenu(budgetId);
}, [budgetId]);
useEffect(() => {
if (userData?.tokenExpired) {
dispatch(
@@ -215,7 +211,7 @@ export function App() {
return (
<BrowserRouter>
<ExposeNavigate />
<HotkeysProvider initiallyActiveScopes={['*']}>
<HotkeysProvider initiallyActiveScopes={['app']}>
<SpreadsheetProvider>
<SidebarProvider>
<BudgetMonthCountProvider>

View File

@@ -22,7 +22,6 @@ export function AppBackground({ isLoading }: AppBackgroundProps) {
from: { opacity: 0, transform: 'translateY(-100px)' },
enter: { opacity: 1, transform: 'translateY(0)' },
leave: { opacity: 0, transform: 'translateY(100px)' },
unique: true,
});
return (

View File

@@ -22,7 +22,6 @@ export function BankSyncStatus() {
from: { opacity: 0, transform: 'translateY(-100px)' },
enter: { opacity: 1, transform: 'translateY(0)' },
leave: { opacity: 0, transform: 'translateY(-100px)' },
unique: true,
},
);

View File

@@ -97,7 +97,7 @@ export const HelpMenu = () => {
}
};
useHotkeys('shift+?', () => setMenuOpen(true));
useHotkeys('?', () => setMenuOpen(true), { useKey: true });
return (
<SpaceBetween>

View File

@@ -107,6 +107,8 @@ export function ruleToString(rule: RuleEntity, data: FilterData) {
} else if (action.op === 'prepend-notes' || action.op === 'append-notes') {
const noteValue = String(action.value || '');
return [friendlyOp(action.op), '\u201c' + noteValue + '\u201d'];
} else if (action.op === 'delete-transaction') {
return [friendlyOp(action.op), '(delete)'];
} else {
return [];
}

View File

@@ -14,75 +14,15 @@ import remarkGfm from 'remark-gfm';
import {
remarkBreaks,
sequentialNewlinesPlugin,
markdownBaseStyles,
} from '@desktop-client/util/markdown';
const remarkPlugins = [sequentialNewlinesPlugin, remarkGfm, remarkBreaks];
const markdownStyles = css({
const markdownStyles = css(markdownBaseStyles, {
display: 'block',
maxWidth: 350,
padding: 8,
overflowWrap: 'break-word',
'& p': {
margin: 0,
':not(:first-child)': {
marginTop: '0.25rem',
},
},
'& ul, & ol': {
listStylePosition: 'inside',
margin: 0,
paddingLeft: 0,
},
'&>* ul, &>* ol': {
marginLeft: '1.5rem',
},
'& li>p': {
display: 'contents',
},
'& blockquote': {
paddingLeft: '0.75rem',
borderLeft: '3px solid ' + theme.markdownDark,
margin: 0,
},
'& hr': {
borderTop: 'none',
borderLeft: 'none',
borderRight: 'none',
borderBottom: '1px solid ' + theme.markdownNormal,
},
'& code': {
backgroundColor: theme.markdownLight,
padding: '0.1rem 0.5rem',
borderRadius: '0.25rem',
},
'& pre': {
padding: '0.5rem',
backgroundColor: theme.markdownLight,
borderRadius: '0.5rem',
margin: 0,
':not(:first-child)': {
marginTop: '0.25rem',
},
'& code': {
background: 'inherit',
padding: 0,
borderRadius: 0,
},
},
'& table, & th, & td': {
border: '1px solid ' + theme.markdownNormal,
},
'& table': {
borderCollapse: 'collapse',
wordBreak: 'break-word',
},
'& td': {
padding: '0.25rem 0.75rem',
},
'& h3': {
fontSize: 15,
},
});
type NotesProps = {

View File

@@ -35,12 +35,14 @@ export function ThemeSelector({ style }: ThemeSelectorProps) {
development: SvgMoonStars,
} as const;
type ThemeIconKey = keyof typeof themeIcons;
function onMenuSelect(newTheme: Theme) {
setMenuOpen(false);
switchTheme(newTheme);
}
const Icon = themeIcons[theme] || SvgSun;
const Icon = themeIcons[theme as ThemeIconKey] || SvgSun;
if (isNarrowWidth) {
return null;

View File

@@ -20,10 +20,7 @@ import { View } from '@actual-app/components/view';
import { css } from '@emotion/css';
import { listen } from 'loot-core/platform/client/fetch';
import {
isDevelopmentEnvironment,
isElectron,
} from 'loot-core/shared/environment';
import { isDevelopmentEnvironment } from 'loot-core/shared/environment';
import * as Platform from 'loot-core/shared/platform';
import { AccountSyncCheck } from './accounts/AccountSyncCheck';
@@ -352,7 +349,7 @@ export function Titlebar({ style }: TitlebarProps) {
<PrivacyButton />
{serverURL ? <SyncButton /> : null}
<LoggedInUser />
{!isElectron() && <HelpMenu />}
<HelpMenu />
</SpaceBetween>
</View>
);

View File

@@ -1048,7 +1048,8 @@ class AccountInternal extends PureComponent<
// sync the reconciliation transaction
await send('transactions-batch-update', {
added: ruledTransactions,
added: ruledTransactions.filter(trans => !trans.tombstone),
deleted: ruledTransactions.filter(trans => trans.tombstone),
});
await this.refetchTransactions();
};

Some files were not shown because too many files have changed in this diff Show More