Compare commits

..

36 Commits

Author SHA1 Message Date
Matt Fiddaman
1f821d2849 ⬆️ bump github actions (#7234)
* actions/setup-node

* actions/cache

* actions/checkout

* docker/*

* actions/*-artifact

* actions/stale

* others

* note
2026-03-18 08:53:03 +00:00
Matt Fiddaman
beee16bc8c ⬆️ march dependency updates (#7222)
* @types/node (^22.19.10 → ^22.19.15)

* baseline-browser-mapping (^2.9.19 → ^2.10.0)

* eslint (^9.39.2 → ^9.39.3)

* lage (^2.14.17 → ^2.14.19)

* lint-staged (^16.2.7 → ^16.3.2)

* minimatch (^10.1.2 → ^10.2.4)

* oxlint (^1.47.0 → ^1.51.0)

* rollup-plugin-visualizer (^6.0.5 → ^6.0.11)

* @chromatic-com/storybook (^5.0.0 → ^5.0.1)

* @storybook/addon-a11y (^10.2.7 → ^10.2.16)

* @storybook/addon-docs (^10.2.7 → ^10.2.16)

* @storybook/react-vite (^10.2.7 → ^10.2.16)

* eslint-plugin-storybook (^10.2.7 → ^10.2.16)

* storybook (^10.2.7 → ^10.2.16)

* @codemirror/autocomplete (^6.20.0 → ^6.20.1)

* @codemirror/lang-javascript (^6.2.4 → ^6.2.5)

* @codemirror/language (^6.12.1 → ^6.12.2)

* @rolldown/plugin-babel (~0.1.7 → ~0.1.8)

* @swc/core (^1.15.11 → ^1.15.18)

* @swc/helpers (^0.5.18 → ^0.5.19)

* @tanstack/react-query (^5.90.20 → ^5.90.21)

* @uiw/react-codemirror (^4.25.4 → ^4.25.7)

* hyperformula (^3.1.1 → ^3.2.0)

* i18next (^25.8.4 → ^25.8.14)

* i18next-parser (^9.3.0 → ^9.4.0)

* react-i18next (^16.5.4 → ^16.5.6)

* react-virtualized-auto-sizer (^2.0.2 → ^2.0.3)

* fs-extra (^11.3.3 → ^11.3.4)

* @r74tech/docusaurus-plugin-panzoom (^2.4.0 → ^2.4.2)

* lru-cache (^11.2.5 → ^11.2.6)

* nodemon (^3.1.11 → ^3.1.14)

* eslint-plugin-perfectionist (^4.15.1 → ^5.6.0)

* downshift (9.0.10 → 9.3.2)

* react-router (7.13.0 → 7.13.1)

* @easyops-cn/docusaurus-search-local (^0.52.3 → ^0.55.1)

* peggy (5.0.6 → 5.1.0)

* @types/supertest (^6.0.3 → ^7.2.0)

* note
2026-03-18 08:37:04 +00:00
Karim Kodera
4cdb26f9a7 Adding Concentric Donut Pie Chart type to custom report charts (#7038)
* Initial commit for concentric donut chart implementation

* [autofix.ci] apply automated fixes

* Update upcoming-release-notes/7038.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Fix coderabbit comments (Recalculated total to avoid hidden cats, remove tooltip, add proper types)

* Update packages/desktop-client/src/components/reports/graphs/DonutGraph.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Fix zero total group

* lint issues fix

* Fix lint issues

* Empty commit to retriger the process

* [autofix.ci] apply automated fixes

* Removed line betweeen arc and label. I beleive the view is cleaner this way.

* Fixed line for outer donut

* split active shape for concentric circles to avoid impacting original chart

* [autofix.ci] apply automated fixes

* Fixing mid point to align with mid point on inner circle

* 1- make line always start inside the core circle
2- fix bug where inner circle label was showing below the outer circle

* - Fixed Dashboard issue when height too low.
- Rewrite of the activeShape part for simplicity.
- Centralize radius calculation.
- Provide differnt dimensions for compact vs standard rendering
- fix mid line point to auto fix at 70% of the inner radius
- More readable code

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #7038

* Fixed distance issue for arc on single ring.

* [autofix.ci] apply automated fixes

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #7038

* Update packages/desktop-client/src/components/reports/graphs/DonutGraph.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Fixing Code Rabbit Comments

* rerunning tests

* Added Group click through passing all categories iwth a workaorund on showActivity

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-17 20:10:30 +00:00
Matiss Janis Aboltins
15358b6b54 [AI] Remove development theme (#7232)
* [AI] Remove development theme

Delete the development theme that was only available in non-production
environments. Removes the theme file, its registration in the theme
registry, type definitions, preference validation, ThemeSelector icon
mapping, and Storybook configuration.

https://claude.ai/code/session_01A1aEippeWppuwoRSCBPwby

* Add release notes for PR #7232

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-17 19:18:47 +00:00
J B
6d9b1a1d72 Duplicate reimport fix in ui and API (#6926)
* add options to override reimportDeleted

* doc and default in ui to false

* pr note

* period

* wording

* [autofix.ci] apply automated fixes

* docs clarity

* actually test default behavior

* Update upcoming-release-notes/6926.md

Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>

* use new ImportTransactionOpts type for consistency

* Release note wording

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* [autofix.ci] apply automated fixes

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

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-17 17:25:22 +00:00
sys044
108ccc8aba Formula Card: Add budget analysis functions (#7078)
* feat(formula): Add QUERY_BUDGET function for budget-aware formula reporting

* docs(release): Add QUERY_BUDGET feature release notes

* refactor: decompose into multiple functions and support goal dimension

* [autofix.ci] apply automated fixes

* refactor: simplified code

* [autofix.ci] apply automated fixes

* updated release notes

---------

Co-authored-by: Your Name <tomgriffin@localhost>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-03-17 16:37:10 +00:00
scojo
ee8f8bfbba Add Formula Rule function to set-split-amount (#6414)
* Add Formula Rule function to set-split-amount fixed-amount and fixed-percent actions.

* Hide formula UI for remainder splits and simplify icon logic

* Guard against undefined options in set-split-amount else‑branch

* Show split amount formula in rules list

* Update tests based on changes introduced in 7fa1ff230e

* Change set-split-amount formula feature to work as a separate allocation method.

* [autofix.ci] apply automated fixes

* Update types for parent_amount

* Use a semantic non-interactive element instead of Button

* import organization

* [autofix.ci] apply automated fixes

* Fix tests

* Correctly hide when feature is disabled. Update release notes.

* Add tooltip documenting parent_amount formula variable
Change parent_amount to return in cents so it is consistent with amount
variable
Ensure balance variable is available in set-split-amount formula
Clean up parent_amount deletion to be consistent with balance

* Delete balance and parent_amount from subtransactions

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>
2026-03-17 16:28:14 +00:00
Matiss Janis Aboltins
c4ee71409e [AI] Add Yarn constraints to enforce consistent dependency versions (#7229)
* [AI] Add yarn constraints to enforce consistent dependency versions

Adds a `yarn.config.cjs` that uses Yarn 4's built-in constraints feature
to detect when the same dependency is declared with different version
ranges across workspaces. Workspace protocol references and
peerDependencies are excluded from the check.

Also adds a `yarn constraints` convenience script and the `@yarnpkg/types`
dev dependency for type-checked constraint authoring.

https://claude.ai/code/session_01B1xRjZXn6b18anZjo8cbqb

* Add release notes for PR #7229

* Add constraints job to GitHub Actions workflow

* Fix constraints

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-17 16:00:34 +00:00
Matt Fiddaman
dfd6e468a6 ⬆️ react-spring 10.0.3 (#7224)
* react-spring (10.0.0 -> ^10.0.3)

* note

* fix VRT

* fix more animations

* fix budget month colouring
2026-03-17 16:00:27 +00:00
Matiss Janis Aboltins
5b227f5fa1 feat: Add post-checkout hook to run yarn install if yarn.lock changes (#7230) 2026-03-17 15:58:02 +00:00
Julian Dominguez-Schatz
e1606b31ab Migrate get-next-package-version.js to TypeScript (#7227)
* Migrate `get-next-package-version.js` to TypeScript

* Add release notes

* Stronger type check

* Fix step ordering

* Fix typo

* Fix missed ordering
2026-03-17 13:56:47 +00:00
Michael Clark
4f7c3c51a5 🐛 Using a shared worker to coordinate multiple tabs (#7172)
* attempt to enable sync when multiple tabs are open

* allow multiple tabs to work

* release notes

* rehome the host if the tab closes

* ensure new tabs always receive failure  messages by broadcasting them on interval

* reject after retries are exhausted

* forwarding the logs from the worker to the main browser

* [autofix.ci] apply automated fixes

* add preflight fetch from main thread to server endpoint to trigger permission prompt if required

* remove the log prefix for cleaner logs

* adding heardbeat to detect closed tabs so they can be removed from the list

* store failure payload and broadcast for new tabs after timeout is cleared

* if a tab closes a budget, force other tabs to go to the budget list screen

* fix safari by detecting crossoriginisolated as a dependency for shared worker

* all ios to fallback to non-shared-worker implemenation

* coordinator and all backend work going through a leader tab to enable ios

* electing new leader tab when oone tab closes or is refreshed

* logic for standalone tabs to rejoin shared workers when on same budget

* remove the preflight request, shouldnt be needed now the code runs on the main process

* handling brand new tabs going to open budgets that are current standalone with no leader

* allowing budgets to be closed  without kickother others by transfering leadership to remaining oopened tabs

* remove unnedd comments

* change approach slightly - no more standalone, now every budget gets leader promotion automatically)

* adding tests and fixed minor bug to do with deleting budget with multiple tabs open

* fix worker not loading

* trouble with ts - moving to js

* reintroduce ts for the worker

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-03-17 09:30:34 +00:00
Matt Fiddaman
0e1fc07bf3 ⬆️ @types/react (#7223)
* bump @types/react

* note
2026-03-17 08:17:48 +00:00
Matiss Janis Aboltins
53cdc6fa48 [AI] Further hardening of "/change-password" endpoint (#7207)
* [AI] Fix OIDC privilege escalation in /change-password endpoint

Add admin role check and password auth_method session check to prevent
non-admin or OIDC-authenticated users from changing the server password.
Previously, any authenticated user could overwrite the password hash and
then login via password method to obtain an ADMIN session.

https://claude.ai/code/session_01Wne9FY2QnKp6JF7g61B1Sn

* Add release notes for PR #7207

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-17 08:16:46 +00:00
okxint
1d0281025d fix: preserve schedule link when merging transactions (#7177)
* [AI] fix: preserve schedule link when merging transactions

When merging two transactions where one is linked to a schedule,
the schedule field was not included in the merge update, causing
the schedule association to be silently dropped. This resulted in
duplicate transactions and incorrect "Due" status for scheduled
transactions.

Add `schedule: keep.schedule || drop.schedule` to both the normal
merge path and the subtransaction merge path, matching the existing
fallback pattern used for payee, category, notes, etc.

Add three test cases covering:
- Schedule preserved from dropped transaction when kept has none
- Kept transaction's schedule takes priority when both have one
- Schedule preserved when merging manual scheduled with banksynced

Fixes #6997

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add release notes for PR #7177

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 05:14:16 +00:00
Matiss Janis Aboltins
f73c5e9210 [AI] Fix adm-zip dependency resolution in loot-core (#7219) 2026-03-16 21:11:51 +00:00
Julian Dominguez-Schatz
a4eb17eff2 Upgrade to Vite 8 (#7184)
* Upgrade to Vite 8

* Add release notes

* PR feedback

* [autofix.ci] apply automated fixes

* PR feedback

* fix: inject process.env

* Restore deleted release note

* Clean up and typecheck

* Fix dev server

* Fix type error

* Fix tests

* PR feedback

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-03-15 23:16:39 +00:00
Matiss Janis Aboltins
8a3db77cff [AI] api: simplify bundling by removing loot-core type inlining (#7209) 2026-03-15 20:07:36 +00:00
Matiss Janis Aboltins
f5a62627f0 [AI] Remove @actual-app/crdt Vite aliases and redundant config (#7195)
* [AI] Remove @actual-app/crdt Vite aliases and redundant config

* Release notes

* Enhance CRDT package configuration and clean up Vite settings

* Added `publishConfig` to `crdt/package.json` to specify exports for types and default files.
* Removed unused `crdtDir` references from `vite.config.ts` and `vite.desktop.config.ts` to streamline configuration.
2026-03-15 17:41:27 +00:00
Matiss Janis Aboltins
6c150cf28a [AI] Publish loot-core (@actual-app/core) nightly first in workflow (#7200)
* [AI] Publish loot-core (@actual-app/core) nightly first in workflow

* [autofix.ci] apply automated fixes

* Refactor imports and update configuration

- Updated .oxfmtrc.json to change "parent" to ["parent", "subpath"].
- Removed unnecessary blank lines in various TypeScript files to improve code readability.
- Adjusted import order in reports and rules files for consistency.

* Add workflow steps to pack and publish the core package nightly

* Remove nightly tag from npm publish command in workflow for core package

* Update post-build script comment to reflect correct workspace command for loot-core declarations

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-03-15 17:35:01 +00:00
Asger Mogensen
e069312ac3 Feature: Add a confirmation modal when users merge payees in /payees (#7188)
* Add a confirmation model when merging payees in /payee

* Added a confirmation modal when users merge payees in /payees

* Address coderabbit comments

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-03-15 15:59:57 +00:00
Pranay S
f266b761c2 [AI] Fix Spending Analysis budget table for tracking budgeting (#7191)
* [AI] Fix Spending Analysis budget table for tracking budgeting

Made-with: Cursor

* [autofix.ci] apply automated fixes

* [AI] Add release note for Spending Analysis tracking budgeting fix

Made-with: Cursor

* [autofix.ci] apply automated fixes

* [AI] Address CodeRabbit nitpicks: use typed narrowing instead of assertion for budgetType

Made-with: Cursor

---------

Co-authored-by: Pranay Mac M1 <pranayseela@yahoo.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-03-15 15:58:22 +00:00
Matiss Janis Aboltins
328b36f124 [AI] Fix navigator is not defined error in @actual-app/api for Node.js environments (#7202)
* [AI] Fix navigator is not defined error in @actual-app/api for Node.js environments

Add platform.api.ts to provide Node.js-safe defaults for platform detection,
which the API's Vite config resolves before the browser-only platform.ts.
Also guard navigator access in environment.ts isElectron() function.

Fixes #7201

https://claude.ai/code/session_015Xz2nHC12pNkADGjGZnSXd

* Add release notes for PR #7202

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-15 08:13:55 +00:00
Derek Gaffney
8934df0cb5 fix(docs/accounts): remove duplicated content (#7199)
* fix(docs): remove duplicated content

* release note file
2026-03-14 21:39:33 +00:00
dependabot[bot]
ab269fa4ea Bump undici from 7.18.2 to 7.24.1 (#7197)
Bumps [undici](https://github.com/nodejs/undici) from 7.18.2 to 7.24.1.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v7.18.2...v7.24.1)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 7.24.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-14 21:19:47 +00:00
Matiss Janis Aboltins
9c61cfc145 [AI] Switch typecheck from tsc to tsgo and fix Menu type narrowing (#7183)
* [AI] Switch typecheck from tsc to tsgo and fix Menu type narrowing

* [autofix.ci] apply automated fixes

* Add .gitignore for dist directory, update typecheck script in package.json to use -b flag, and remove noEmit option from tsconfig.json files in ci-actions and desktop-electron packages. Introduce typesVersions in loot-core package.json for improved type handling.

* Refactor SelectedTransactionsButton to improve type safety and readability. Updated items prop to use spread operator for conditional rendering of menu items, ensuring proper type annotations with MenuItem. This change enhances the clarity of the component's structure and maintains TypeScript compliance.

* Update tsconfig.json in desktop-electron package to maintain consistent formatting for plugins section. No functional changes made.

* [autofix.ci] apply automated fixes

* Update package.json and yarn.lock to add TypeScript 5.8.0 dependency. Adjust typesVersions in loot-core package.json for improved type handling. Enhance tsconfig.json in sync-server package to enable strictFunctionTypes for better type safety.

* Enhance tsconfig.json in ci-actions package by adding composite option for improved project references and build performance.

* [AI] Revert typescript to 5.9.3 for ts-node compatibility

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

* [AI] Update yarn.lock after TypeScript version change

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

* Refactor Menu component for improved type safety and readability. Updated type assertions for Menu.line and Menu.label, simplified type checks in filtering and selection logic, and enhanced conditional rendering of menu items. This change ensures better TypeScript compliance and maintains clarity in the component's structure.

* Refactor Select and OpenIdForm components to improve type safety and simplify logic. Updated item mapping to handle Menu.line more effectively, enhancing clarity in selection processes. Adjusted SelectedTransactionsButton to streamline item creation and improve readability.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
2026-03-14 21:03:10 +00:00
Matiss Janis Aboltins
d86c9cf735 Add theme mode filtering to custom theme catalog (#7194)
* [AI] Add mode field to custom theme catalog for dark/light filtering

Each catalog theme now has a `mode: 'dark' | 'light'` field. When
installing a custom theme in auto mode, the ThemeInstaller filters the
catalog to only show themes matching the selected mode (light or dark).

https://claude.ai/code/session_01PtSEMRv3SpAEtdGzvYxzpa

* Add release notes for PR #7194

* Change category from Features to Enhancements

* [AI] Rename filter parameter to avoid shadowing useTranslation t()

Rename `t` to `catalogTheme` in the catalogItems filter to avoid
shadowing the `t` translation function from useTranslation().

https://claude.ai/code/session_01PtSEMRv3SpAEtdGzvYxzpa

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-14 14:57:47 +00:00
Stephen Brown II
f95cfbf82c Add drag&drop reordering for transactions within the same day (#6653)
* feat(desktop-client): add transaction drag-and-drop reordering

Add the ability to manually reorder transactions within a date group using
drag-and-drop in the TransactionsTable. This is useful for correcting the
order of transactions that occurred on the same day.

Prevent transaction reordering from falling back to the end of the day list
when dropping before a split parent. Parent-level moves now target parent
rows only, so regular transactions can be inserted between split parents on
the same date.

Key changes:

Frontend:
- Migrate drag-and-drop from react-dnd to react-aria hooks
- Add transaction row drag handles and drop indicators
- Implement onReorder handler in TransactionList
- Restrict reordering to same-date transactions only
- Disable drag-drop on aggregate views (categories, etc.)

Backend:
- Add transaction-move handler with validation
- Implement moveTransaction in db layer with midpoint/shove algorithm
- Add TRANSACTION_SORT_INCREMENT constant for consistent spacing
- Handle split transaction subtransactions automatically
- Inherit parent sort_order in child transactions during import

Refactoring:
- Remove useDragRef hook (replaced by react-aria)
- Remove DndProvider wrapper from App.tsx
- Update budget and sidebar components for new drag-drop API
- Fix sort.tsx @ts-strict-ignore by adding proper types

configure allowReorder for TransactionTable consumers

- Account.tsx: Enable reordering for single-account views, add sort_order
  as tiebreaker for stable ordering when sorted by other columns
- Calendar.tsx: Disable reordering in calendar report view (read-only context)

* void promises
2026-03-14 13:35:45 +00:00
Matiss Janis Aboltins
767f77fea3 [AI] Enable more lint rules as warn for gradual fix (7196) (#7196) 2026-03-14 01:47:17 +00:00
Matiss Janis Aboltins
d6dcc30e44 [AI] Promote typescript/no-for-in-array lint rule from warn to error (#7193)
* [AI] Promote typescript/no-for-in-array lint rule from warn to error

Convert the oxlint rule from "warn" to "error" as noted by the existing
TODO comment, and fix the three violations by replacing for-in loops
with for-of using .entries().

https://claude.ai/code/session_01N6F8DMzUVDxNJC56jMGknf

* Add release notes for PR #7193

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-13 21:06:06 +00:00
Matiss Janis Aboltins
541df52441 Enable restrict-template-expressions linting rule (#7181)
* [AI] Promote typescript/restrict-template-expressions to error and fix violations

Convert the oxlint rule from "warn" to "error" and fix all 42 violations
by wrapping non-string template expressions with String(). This ensures
type safety in template literals across the codebase.

https://claude.ai/code/session_01Uk8SwFbD6HuUuo3SSMwU9z

* Add release notes for PR #7181

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-13 18:36:58 +00:00
Julian Dominguez-Schatz
85e3166495 [AI] Remove deep-equal package (#7187)
* Remove `deep-equal` package

* Add release notes

* Add a few more tests

* Add release notes
2026-03-13 16:36:52 +00:00
Mats Nilsson
5d4bbc9ebb fix: mobile autocomplete modals (#6741)
When filtering for accounts in e.g. the net worth graph the modal closes
the filter the tooltip so it's impossible to add e.g. accounts to the
filter.
2026-03-13 15:03:38 +00:00
dependabot[bot]
031aac9799 bump ajv from 6.12.6 to 6.14.0 (#7044)
* Bump ajv from 6.12.6 to 6.14.0

Bumps [ajv](https://github.com/ajv-validator/ajv) from 6.12.6 to 6.14.0.
- [Release notes](https://github.com/ajv-validator/ajv/releases)
- [Commits](https://github.com/ajv-validator/ajv/compare/v6.12.6...v6.14.0)

---
updated-dependencies:
- dependency-name: ajv
  dependency-version: 6.14.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* note

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2026-03-13 01:53:32 +00:00
Stephen Brown II
c53c5c2f36 [AI] Normalize apostrophe-dot thousandsSeparator for consistency (#7179)
* Normalize apostrophe-dot thousandsSeparator for consistency

* Also normalize keyboard apostrophes on the input path
2026-03-13 01:47:53 +00:00
Matiss Janis Aboltins
e968213977 Use TypeScript project references for incremental builds (#7180)
* [AI] Fix duplicate typechecking by consolidating into lage

Previously `yarn typecheck` ran:
1. `tsc -b` (type-checks all packages via project references)
2. `tsc -p tsconfig.root.json --noEmit` (checks root bin/*.ts)
3. `lage typecheck` (runs `tsc --noEmit` per package - duplicate!)

Now it runs:
1. `tsc -p tsconfig.root.json --noEmit` (checks root bin/*.ts)
2. `lage typecheck` (handles everything via dependency ordering)

Changes:
- Remove `tsc -b` from root typecheck script
- Add `dependsOn: ["^typecheck"]` to lage config for correct ordering
- Change per-package typecheck from `tsc --noEmit` to `tsc -b` so
  declarations are emitted for dependent packages

https://claude.ai/code/session_01P7mtAHphD6f1FsnQRwWBaW

* Add release notes for PR #7180

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-12 21:54:29 +00:00
655 changed files with 10926 additions and 4809 deletions

View File

@@ -15,7 +15,7 @@ runs:
using: composite
steps:
- name: Install node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
- name: Install yarn
@@ -27,7 +27,7 @@ runs:
run: echo "version=$(node -v)" >> "$GITHUB_OUTPUT"
shell: bash
- name: Cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
id: cache
with:
path: ${{ format('{0}/**/node_modules', inputs.working-directory) }}
@@ -36,7 +36,7 @@ runs:
run: mkdir -p ${{ format('{0}/.lage', inputs.working-directory) }}
shell: bash
- name: Cache Lage
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: ${{ format('{0}/.lage', inputs.working-directory) }}
key: lage-${{ runner.os }}-${{ github.sha }}
@@ -48,7 +48,7 @@ runs:
shell: bash
if: steps.cache.outputs.cache-hit != 'true'
- name: Download translations
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: actualbudget/translations
path: ${{ inputs.working-directory }}/packages/desktop-client/locale

View File

@@ -17,7 +17,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:

View File

@@ -15,11 +15,11 @@ jobs:
autofix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Format code
run: yarn lint:fix
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27
- uses: autofix-ci/action@7a166d7532b277f34e16238930461bf77f9d7ed8 # v1.3.3

View File

@@ -22,7 +22,7 @@ jobs:
api:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:
@@ -34,12 +34,12 @@ jobs:
- name: Prepare bundle stats artifact
run: cp packages/api/app/stats.json api-stats.json
- name: Upload Build
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: actual-api
path: packages/api/actual-api.tgz
- name: Upload API bundle stats
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: api-build-stats
path: api-stats.json
@@ -47,7 +47,7 @@ jobs:
crdt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:
@@ -57,7 +57,7 @@ jobs:
- name: Create package tgz
run: cd packages/crdt && yarn pack && mv package.tgz actual-crdt.tgz
- name: Upload Build
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: actual-crdt
path: packages/crdt/actual-crdt.tgz
@@ -65,18 +65,18 @@ jobs:
web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
- name: Build Web
run: yarn build:browser
- name: Upload Build
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: actual-web
path: packages/desktop-client/build
- name: Upload Build Stats
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: build-stats
path: packages/desktop-client/build-stats
@@ -84,7 +84,7 @@ jobs:
server:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:
@@ -92,7 +92,7 @@ jobs:
- name: Build Server
run: yarn workspace @actual-app/sync-server build
- name: Upload Build
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: sync-server
path: packages/sync-server/build

View File

@@ -12,10 +12,20 @@ concurrency:
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
jobs:
constraints:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Check dependency version consistency
run: yarn constraints
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:
@@ -25,7 +35,7 @@ jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:
@@ -35,7 +45,7 @@ jobs:
validate-cli:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:
@@ -47,7 +57,7 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:
@@ -59,7 +69,7 @@ jobs:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:

View File

@@ -22,14 +22,14 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
with:
languages: javascript
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
with:
category: '/language:javascript'

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:

View File

@@ -36,17 +36,17 @@ jobs:
matrix:
os: [ubuntu, alpine]
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Docker meta
id: meta
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
# Push to both Docker Hub and Github Container Registry
images: ${{ env.IMAGES }}
@@ -54,14 +54,14 @@ jobs:
tags: ${{ env.TAGS }}
- name: Login to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
if: github.event_name != 'pull_request' && !github.event.repository.fork
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
@@ -76,7 +76,7 @@ jobs:
run: yarn build:server
- name: Build image for testing
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
context: .
push: false
@@ -93,7 +93,7 @@ jobs:
# This will use the cache from the earlier build step and not rebuild the image
# https://docs.docker.com/build/ci/github-actions/test-before-push/
- name: Build and push images
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
context: .
push: ${{ github.event_name != 'pull_request' }}

View File

@@ -28,17 +28,17 @@ jobs:
name: Build Docker image
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Docker meta
id: meta
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
# Push to both Docker Hub and Github Container Registry
images: ${{ env.IMAGES }}
@@ -48,7 +48,7 @@ jobs:
- name: Docker meta for Alpine image
id: alpine-meta
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: ${{ env.IMAGES }}
# Automatically update :latest
@@ -58,13 +58,13 @@ jobs:
tags: ${{ env.TAGS }}
- name: Login to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -78,7 +78,7 @@ jobs:
run: yarn build:server
- name: Build and push ubuntu image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
context: .
push: true
@@ -87,7 +87,7 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
- name: Build and push alpine image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
context: .
push: true

View File

@@ -32,7 +32,7 @@ jobs:
container:
image: mcr.microsoft.com/playwright:v1.58.2-jammy
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:
@@ -41,7 +41,7 @@ jobs:
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Run E2E Tests
run: yarn e2e --shard=${{ matrix.shard }}/5
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
if: always()
with:
name: desktop-client-test-results-shard-${{ matrix.shard }}
@@ -55,7 +55,7 @@ jobs:
container:
image: mcr.microsoft.com/playwright:v1.58.2-jammy
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:
@@ -65,7 +65,7 @@ jobs:
- name: Run Desktop app E2E Tests
run: |
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- yarn e2e:desktop
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
if: always()
with:
name: desktop-app-test-results
@@ -83,14 +83,14 @@ jobs:
container:
image: mcr.microsoft.com/playwright:v1.58.2-jammy
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Run VRT Tests
run: yarn vrt --shard=${{ matrix.shard }}/5
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
if: always()
with:
name: vrt-blob-report-${{ matrix.shard }}
@@ -106,11 +106,11 @@ jobs:
container:
image: mcr.microsoft.com/playwright:v1.58.2-jammy
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
- name: Download all blob reports
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: packages/desktop-client/all-blob-reports
pattern: vrt-blob-report-*
@@ -118,7 +118,7 @@ jobs:
- name: Merge reports
id: merge-reports
run: yarn workspace @actual-app/web run playwright merge-reports --reporter html ./all-blob-reports
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
id: playwright-report-vrt
with:
name: html-report--attempt-${{ github.run_attempt }}
@@ -134,7 +134,7 @@ jobs:
echo "${{ steps.playwright-report-vrt.outputs.artifact-url }}" > vrt-metadata/artifact-url.txt
- name: Upload VRT metadata
if: github.event_name == 'pull_request'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: vrt-comment-metadata
path: vrt-metadata/

View File

@@ -18,7 +18,7 @@ jobs:
if: github.event.workflow_run.event == 'pull_request'
steps:
- name: Download VRT metadata
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
@@ -53,7 +53,7 @@ jobs:
- name: Comment on PR with VRT report link
if: steps.metadata.outputs.should_comment == 'true'
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
uses: marocchino/sticky-pull-request-comment@70d2764d1a7d5d9560b100cbea0077fc8f633987 # v3.0.2
with:
number: ${{ steps.metadata.outputs.pr_number }}
header: vrt-comment

View File

@@ -29,7 +29,7 @@ jobs:
- macos-latest
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- if: ${{ startsWith(matrix.os, 'windows') }}
run: pip.exe install setuptools
- if: ${{ ! startsWith(matrix.os, 'windows') }}
@@ -74,7 +74,7 @@ jobs:
if: ${{ ! startsWith(matrix.os, 'macos') }}
run: ./bin/package-electron
- name: Upload Build
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: actual-electron-${{ matrix.os }}
path: |
@@ -85,13 +85,13 @@ jobs:
packages/desktop-electron/dist/*.flatpak
- name: Upload Windows Store Build
if: ${{ startsWith(matrix.os, 'windows') }}
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: actual-electron-${{ matrix.os }}-appx
path: |
packages/desktop-electron/dist/*.appx
- name: Add to new release
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
with:
draft: true
body: |
@@ -126,7 +126,7 @@ jobs:
Install-Module -Name StoreBroker -AcceptLicense -Force -Scope CurrentUser -Verbose
- name: Download Microsoft Store artifacts
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: actual-electron-windows-latest-appx

View File

@@ -33,7 +33,7 @@ jobs:
- macos-latest
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- if: ${{ startsWith(matrix.os, 'windows') }}
run: pip.exe install setuptools
- if: ${{ ! startsWith(matrix.os, 'windows') }}
@@ -42,6 +42,8 @@ jobs:
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install setuptools
- name: Set up environment
uses: ./.github/actions/setup
- if: ${{ startsWith(matrix.os, 'ubuntu') }}
name: Setup Flatpak dependencies
run: |
@@ -56,65 +58,63 @@ jobs:
METAINFO_FILE="packages/desktop-electron/extra-resources/linux/com.actualbudget.actual.metainfo.xml"
TODAY=$(date +%Y-%m-%d)
VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/desktop-electron/package.json --type nightly)
VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/desktop-electron/package.json --type nightly)
sed -i "s/%RELEASE_VERSION%/$VERSION/g; s/%RELEASE_DATE%/$TODAY/g" "$METAINFO_FILE"
flatpak run --command=flatpak-builder-lint org.flatpak.Builder appstream "$METAINFO_FILE"
- name: Set up environment
uses: ./.github/actions/setup
- name: Build Electron
run: ./bin/package-electron
- name: Upload Linux x64 AppImage
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-linux-x86_64.AppImage
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-linux-x86_64.AppImage
- name: Upload Linux arm64 AppImage
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-linux-arm64.AppImage
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-linux-arm64.AppImage
- name: Upload Linux x64 flatpak
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-linux-x86_64.flatpak
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-linux-x86_64.flatpak
- name: Upload Windows x32 exe
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-windows-ia32.exe
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-windows-ia32.exe
- name: Upload Windows x64 exe
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-windows-x64.exe
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-windows-x64.exe
- name: Upload Windows arm64 exe
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-windows-arm64.exe
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-windows-arm64.exe
- name: Upload Mac x64 dmg
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-mac-x64.dmg
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-mac-x64.dmg
- name: Upload Mac arm64 dmg
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-mac-arm64.dmg
if-no-files-found: ignore
@@ -122,7 +122,7 @@ jobs:
- name: Upload Windows Store Build
if: ${{ startsWith(matrix.os, 'windows') }}
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: actual-electron-${{ matrix.os }}-appx
path: |

View File

@@ -25,7 +25,7 @@ jobs:
if: github.event.pull_request.head.repo.full_name != github.repository
steps:
- name: Post welcome comment
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
uses: marocchino/sticky-pull-request-comment@70d2764d1a7d5d9560b100cbea0077fc8f633987 # v3.0.2
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
number: ${{ github.event.pull_request.number }}

View File

@@ -17,9 +17,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.inputs.ref }}
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Bump package versions
id: bump_package_versions
shell: bash
@@ -35,12 +39,12 @@ jobs:
pkg="${packages[$key]}"
if [[ -n "${{ github.event.inputs.version }}" ]]; then
version=$(node ./packages/ci-actions/bin/get-next-package-version.js \
version=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts \
--package-json "./packages/$pkg/package.json" \
--version "${{ github.event.inputs.version }}" \
--update)
else
version=$(node ./packages/ci-actions/bin/get-next-package-version.js \
version=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts \
--package-json "./packages/$pkg/package.json" \
--type auto \
--update)
@@ -51,7 +55,7 @@ jobs:
echo "version=$NEW_WEB_VERSION" >> "$GITHUB_OUTPUT"
- name: Create PR
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.ACTIONS_UPDATE_TOKEN }}
commit-message: '🔖 (${{ steps.bump_package_versions.outputs.version }})'

View File

@@ -12,7 +12,7 @@ jobs:
if: github.repository == 'actualbudget/actual'
steps:
- name: Check out main repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
path: actual
- name: Set up environment
@@ -44,7 +44,7 @@ jobs:
push \
actualbudget/actual
- name: Check out updated translations
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ssh-key: ${{ secrets.STRING_IMPORT_DEPLOY_KEY }}
repository: actualbudget/translations

View File

@@ -24,8 +24,8 @@ jobs:
runs-on: ubuntu-latest
steps:
# This is not a security concern because we have approved & merged the PR
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
- name: Handle feature requests

View File

@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Repository Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup

View File

@@ -92,7 +92,7 @@ jobs:
echo "APPIMAGE_ARM64_SHA256=$APPIMAGE_ARM64_SHA256" >> "$GITHUB_ENV"
- name: Checkout Flathub repo
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: flathub/com.actualbudget.actual
token: ${{ secrets.FLATHUB_GITHUB_TOKEN }}
@@ -113,7 +113,7 @@ jobs:
cat com.actualbudget.actual.yml
- name: Create PR in Flathub repo
uses: peter-evans/create-pull-request@v7
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.FLATHUB_GITHUB_TOKEN }}
commit-message: 'Update Actual flatpak to version ${{ steps.resolve_version.outputs.version }}'

View File

@@ -28,7 +28,7 @@ jobs:
runs-on: ${{ matrix.os }}
if: github.event.repository.fork == false
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- if: ${{ startsWith(matrix.os, 'windows') }}
run: pip.exe install setuptools
@@ -39,6 +39,9 @@ jobs:
source .venv/bin/activate
python3 -m pip install setuptools
- name: Set up environment
uses: ./.github/actions/setup
- if: ${{ startsWith(matrix.os, 'ubuntu') }}
name: Setup Flatpak dependencies
run: |
@@ -53,16 +56,14 @@ jobs:
METAINFO_FILE="packages/desktop-electron/extra-resources/linux/com.actualbudget.actual.metainfo.xml"
TODAY=$(date +%Y-%m-%d)
VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/desktop-electron/package.json --type nightly)
VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/desktop-electron/package.json --type nightly)
sed -i "s/%RELEASE_VERSION%/$VERSION/g; s/%RELEASE_DATE%/$TODAY/g" "$METAINFO_FILE"
flatpak run --command=flatpak-builder-lint org.flatpak.Builder appstream "$METAINFO_FILE"
- name: Set up environment
uses: ./.github/actions/setup
- name: Update package versions
run: |
# Get new nightly version
NEW_DESKTOP_APP_VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/desktop-electron/package.json --type nightly)
NEW_DESKTOP_APP_VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/desktop-electron/package.json --type nightly)
# Set package version
npm version $NEW_DESKTOP_APP_VERSION --no-git-tag-version --workspace=desktop-electron --no-workspaces-update
@@ -82,49 +83,49 @@ jobs:
run: ./bin/package-electron
- name: Upload Linux x64 AppImage
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-linux-x86_64.AppImage
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-linux-x86_64.AppImage
- name: Upload Linux arm64 AppImage
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-linux-arm64.AppImage
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-linux-arm64.AppImage
- name: Upload Windows x32 exe
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-windows-ia32.exe
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-windows-ia32.exe
- name: Upload Windows x64 exe
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-windows-x64.exe
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-windows-x64.exe
- name: Upload Windows arm64 exe
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-windows-arm64.exe
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-windows-arm64.exe
- name: Upload Mac x64 dmg
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-mac-x64.dmg
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-mac-x64.dmg
- name: Upload Mac arm64 dmg
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-mac-arm64.dmg
if-no-files-found: ignore
@@ -132,7 +133,7 @@ jobs:
- name: Upload Windows Store Build
if: ${{ startsWith(matrix.os, 'windows') }}
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: actual-electron-${{ matrix.os }}-appx
path: |

View File

@@ -12,7 +12,7 @@ jobs:
name: Build and pack npm packages
if: github.event.repository.fork == false
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
@@ -20,11 +20,13 @@ jobs:
- name: Update package versions
run: |
# Get new nightly versions
NEW_WEB_VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/desktop-client/package.json --type nightly)
NEW_SYNC_VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/sync-server/package.json --type nightly)
NEW_API_VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/api/package.json --type nightly)
NEW_CORE_VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/loot-core/package.json --type nightly)
NEW_WEB_VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/desktop-client/package.json --type nightly)
NEW_SYNC_VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/sync-server/package.json --type nightly)
NEW_API_VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/api/package.json --type nightly)
# Set package versions
npm version $NEW_CORE_VERSION --no-git-tag-version --workspace=@actual-app/core --no-workspaces-update
npm version $NEW_WEB_VERSION --no-git-tag-version --workspace=@actual-app/web --no-workspaces-update
npm version $NEW_SYNC_VERSION --no-git-tag-version --workspace=@actual-app/sync-server --no-workspaces-update
npm version $NEW_API_VERSION --no-git-tag-version --workspace=@actual-app/api --no-workspaces-update
@@ -33,6 +35,10 @@ jobs:
run: |
yarn install
- name: Pack the core package
run: |
yarn workspace @actual-app/core pack --filename @actual-app/core.tgz
- name: Build Server & Web
run: yarn build:server
@@ -49,10 +55,11 @@ jobs:
yarn workspace @actual-app/api pack --filename @actual-app/api.tgz
- name: Upload package artifacts
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: npm-packages
path: |
packages/loot-core/@actual-app/core.tgz
packages/desktop-client/@actual-app/web.tgz
packages/sync-server/@actual-app/sync-server.tgz
packages/api/@actual-app/api.tgz
@@ -66,16 +73,22 @@ jobs:
packages: write
steps:
- name: Download the artifacts
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: npm-packages
- name: Setup node and npm registry
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
registry-url: 'https://registry.npmjs.org'
- name: Publish Core
run: |
npm publish loot-core/@actual-app/core.tgz --access public --tag nightly
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish Web
run: |
npm publish desktop-client/@actual-app/web.tgz --access public --tag nightly

View File

@@ -11,11 +11,15 @@ jobs:
runs-on: ubuntu-latest
name: Build and pack npm packages
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
- name: Pack the core package
run: |
yarn workspace @actual-app/core pack --filename @actual-app/core.tgz
- name: Build Web
run: yarn build:server
@@ -32,10 +36,11 @@ jobs:
yarn workspace @actual-app/api pack --filename @actual-app/api.tgz
- name: Upload package artifacts
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: npm-packages
path: |
packages/loot-core/@actual-app/core.tgz
packages/desktop-client/@actual-app/web.tgz
packages/sync-server/@actual-app/sync-server.tgz
packages/api/@actual-app/api.tgz
@@ -49,16 +54,22 @@ jobs:
packages: write
steps:
- name: Download the artifacts
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: npm-packages
- name: Setup node and npm registry
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
registry-url: 'https://registry.npmjs.org'
- name: Publish Core
run: |
npm publish loot-core/@actual-app/core.tgz --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish Web
run: |
npm publish desktop-client/@actual-app/web.tgz --access public

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Get changed files

View File

@@ -35,7 +35,7 @@ jobs:
contents: read
steps:
- name: Checkout base branch
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.base_ref }}
- name: Set up environment
@@ -80,7 +80,7 @@ jobs:
exit 1
- name: Download web build artifact from ${{github.base_ref}}
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
id: pr-web-build
with:
branch: ${{github.base_ref}}
@@ -89,7 +89,7 @@ jobs:
name: build-stats
path: base
- name: Download API build artifact from ${{github.base_ref}}
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
id: pr-api-build
with:
branch: ${{github.base_ref}}
@@ -98,7 +98,7 @@ jobs:
name: api-build-stats
path: base
- name: Download build stats from PR
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
with:
pr: ${{github.event.pull_request.number}}
workflow: build.yml
@@ -107,7 +107,7 @@ jobs:
path: head
allow_forks: true
- name: Download API stats from PR
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
with:
pr: ${{github.event.pull_request.number}}
workflow: build.yml

View File

@@ -8,7 +8,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
close-pr-message: 'This PR was closed because it has been stalled for 5 days with no activity.'
@@ -18,7 +18,7 @@ jobs:
stale-wip:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
stale-pr-message: ':wave: Hi! It looks like this PR has not had any changes for a week now. Would you like someone to review this PR? If so - please remove the "[WIP]" prefix from the PR title. That will let the community know that this PR is open for a review.'
days-before-stale: 7
@@ -29,7 +29,7 @@ jobs:
stale-needs-info:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
stale-issue-label: 'needs info'
days-before-stale: -1

View File

@@ -19,7 +19,7 @@ jobs:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Download patch artifact
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
@@ -27,7 +27,7 @@ jobs:
path: /tmp/artifacts
- name: Download metadata artifact
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
@@ -54,7 +54,7 @@ jobs:
- name: Checkout fork branch
if: steps.metadata.outputs.pr_number != ''
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: ${{ steps.metadata.outputs.head_repo }}
ref: ${{ steps.metadata.outputs.head_ref }}

View File

@@ -60,7 +60,7 @@ jobs:
core.setOutput('head_ref', pr.head.ref);
core.setOutput('head_repo', pr.head.repo.full_name);
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ steps.pr.outputs.head_sha }}
@@ -113,7 +113,7 @@ jobs:
- name: Upload patch artifact
if: steps.create-patch.outputs.has_changes == 'true'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: vrt-patch-${{ github.event.issue.number }}
path: vrt-update.patch
@@ -129,7 +129,7 @@ jobs:
- name: Upload PR metadata
if: steps.create-patch.outputs.has_changes == 'true'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: vrt-metadata-${{ github.event.issue.number }}
path: pr-metadata/

13
.husky/post-checkout Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/sh
# Run yarn install when switching branches (if yarn.lock changed)
# $3 is 1 for branch checkout, 0 for file checkout
if [ "$3" != "1" ]; then
exit 0
fi
# Check if yarn.lock changed between the old and new HEAD
if git diff --name-only "$1" "$2" | grep -q "^yarn.lock$"; then
echo "yarn.lock changed — running yarn install..."
yarn install
fi

View File

@@ -10,7 +10,7 @@
"builtin",
"external",
"loot-core",
"parent",
["parent", "subpath"],
"sibling",
"index",
"desktop-client"
@@ -22,11 +22,11 @@
},
{
"groupName": "loot-core",
"elementNamePattern": ["loot-core/**"]
"elementNamePattern": ["loot-core/**", "@actual-app/core/**"]
},
{
"groupName": "desktop-client",
"elementNamePattern": ["@desktop-client/**", "#**", "#**/**"]
"elementNamePattern": ["@desktop-client/**"]
}
],
"newlinesBetween": true

View File

@@ -101,12 +101,18 @@
"typescript/no-var-requires": "error",
// we want to allow unions such as "{ name: DbAccount['name'] | DbPayee['name'] }"
"typescript/no-duplicate-type-constituents": "off",
// we want to allow unions such as "string | 'network' | 'file-key-mismatch'"
"typescript/no-redundant-type-constituents": "off",
"typescript/await-thenable": "error",
"typescript/no-floating-promises": "error",
"typescript/require-array-sort-compare": "error",
"typescript/unbound-method": "error",
"typescript/no-for-in-array": "warn", // TODO: covert to error
"typescript/restrict-template-expressions": "warn", // TODO: covert to error
"typescript/no-for-in-array": "error",
"typescript/restrict-template-expressions": "error",
"typescript/no-misused-spread": "warn", // TODO: enable this
"typescript/no-base-to-string": "warn", // TODO: enable this
"typescript/no-unsafe-unary-minus": "warn", // TODO: enable this
"typescript/no-unsafe-type-assertion": "warn", // TODO: enable this
// Import rules
"import/consistent-type-specifier-style": "error",

View File

@@ -84,7 +84,7 @@ The core application logic that runs on any platform.
```bash
# Run all loot-core tests
yarn workspace loot-core run test
yarn workspace @actual-app/core run test
# Or run tests across all packages using lage
yarn test
@@ -219,7 +219,7 @@ yarn test
yarn test:debug
# Run tests for a specific package
yarn workspace loot-core run test
yarn workspace @actual-app/core run test
```
**E2E Tests (Playwright)**
@@ -625,7 +625,7 @@ Standard commands documented in `package.json` scripts and the Quick Start secti
- `yarn lint` / `yarn lint:fix` (uses oxlint + oxfmt)
- `yarn test` (lage across all workspaces)
- `yarn typecheck` (tsc + lage typecheck)
- `yarn typecheck` (tsgo + lage typecheck)
### Testing and previewing the app

View File

@@ -17,7 +17,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/core build:browser
yarn workspace @actual-app/web build:browser
echo "packages/desktop-client/build"

View File

@@ -51,16 +51,16 @@ fi
export NODE_OPTIONS="--max-old-space-size=4096"
yarn workspace plugins-service build
yarn workspace loot-core build:node
yarn workspace @actual-app/core build:node
yarn workspace @actual-app/web build --mode=desktop # electron specific build
# required for running the sync-server server
yarn workspace loot-core build:browser
yarn workspace @actual-app/core build:browser
yarn workspace @actual-app/web build:browser
yarn workspace @actual-app/sync-server build
# Emit loot-core declarations so desktop-electron (which includes typings/window.ts) can build
yarn workspace loot-core exec tsc -p tsconfig.json
# Emit @actual-app/core declarations so desktop-electron (which includes typings/window.ts) can build
yarn workspace @actual-app/core exec tsgo -p tsconfig.json
yarn workspace desktop-electron update-client

View File

@@ -3,6 +3,7 @@ module.exports = {
pipeline: {
typecheck: {
type: 'npmScript',
dependsOn: ['^typecheck'],
},
test: {
type: 'npmScript',

View File

@@ -25,16 +25,16 @@
"start:desktop": "yarn desktop-dependencies && npm-run-all --parallel 'start:desktop-*'",
"start:docs": "yarn workspace docs start",
"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-node": "yarn workspace @actual-app/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": "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-backend": "yarn workspace @actual-app/core watch:browser",
"start:browser-frontend": "yarn workspace @actual-app/web start:browser",
"start:storybook": "yarn workspace @actual-app/components start:storybook",
"build:browser-backend": "yarn workspace loot-core build:browser",
"build:browser-backend": "yarn workspace @actual-app/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",
@@ -53,32 +53,35 @@
"vrt": "yarn workspace @actual-app/web run vrt",
"vrt:docker": "./bin/run-vrt",
"rebuild-electron": "./node_modules/.bin/electron-rebuild -m ./packages/loot-core",
"rebuild-node": "yarn workspace loot-core rebuild",
"rebuild-node": "yarn workspace @actual-app/core rebuild",
"lint": "oxfmt --check . && oxlint --type-aware --quiet",
"lint:fix": "oxfmt . && oxlint --fix --type-aware --quiet",
"install:server": "yarn workspaces focus @actual-app/sync-server --production",
"typecheck": "tsc -b && tsc -p tsconfig.root.json --noEmit && lage typecheck",
"constraints": "yarn constraints",
"typecheck": "tsgo -p tsconfig.root.json --noEmit && lage typecheck",
"jq": "./node_modules/node-jq/bin/jq",
"prepare": "husky"
},
"devDependencies": {
"@octokit/rest": "^22.0.1",
"@types/node": "^22.19.10",
"@types/node": "^22.19.15",
"@types/prompts": "^2.4.9",
"baseline-browser-mapping": "^2.9.19",
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
"@yarnpkg/types": "^4.0.1",
"baseline-browser-mapping": "^2.10.0",
"cross-env": "^10.1.0",
"eslint": "^9.39.2",
"eslint-plugin-perfectionist": "^4.15.1",
"eslint": "^9.39.3",
"eslint-plugin-perfectionist": "^5.6.0",
"eslint-plugin-typescript-paths": "^0.0.33",
"html-to-image": "^1.11.13",
"husky": "^9.1.7",
"lage": "^2.14.17",
"lint-staged": "^16.2.7",
"minimatch": "^10.1.2",
"lage": "^2.14.19",
"lint-staged": "^16.3.2",
"minimatch": "^10.2.4",
"node-jq": "^6.3.1",
"npm-run-all": "^4.1.5",
"oxfmt": "^0.32.0",
"oxlint": "^1.47.0",
"oxlint": "^1.51.0",
"oxlint-tsgolint": "^0.13.0",
"p-limit": "^7.3.0",
"prompts": "^2.4.2",
@@ -87,6 +90,7 @@
"typescript": "^5.9.3"
},
"resolutions": {
"adm-zip": "patch:adm-zip@npm%3A0.5.16#~/.yarn/patches/adm-zip-npm-0.5.16-4556fea098.patch",
"rollup": "4.40.1",
"socks": ">=2.8.3"
},

View File

@@ -3,8 +3,8 @@ import type {
RequestInit as FetchInit,
} from 'node-fetch';
import { init as initLootCore } from 'loot-core/server/main';
import type { InitConfig, lib } from 'loot-core/server/main';
import { init as initLootCore } from '@actual-app/core/server/main';
import type { InitConfig, lib } from '@actual-app/core/server/main';
import { validateNodeVersion } from './validateNodeVersion';

View File

@@ -3,7 +3,7 @@ import * as path from 'path';
import { vi } from 'vitest';
import type { RuleEntity } from 'loot-core/types/models';
import type { RuleEntity } from '@actual-app/core/types/models';
import * as api from './index';
@@ -896,6 +896,73 @@ describe('API CRUD operations', () => {
);
expect(transactions[0].notes).toBeNull();
});
test('Transactions: reimportDeleted=false prevents reimporting deleted transactions', async () => {
const accountId = await api.createAccount({ name: 'test-account' }, 0);
// Import a transaction
const result1 = await api.importTransactions(accountId, [
{
date: '2023-11-03',
imported_id: 'reimport-test-1',
amount: 100,
account: accountId,
},
]);
expect(result1.added).toHaveLength(1);
// Delete the transaction
await api.deleteTransaction(result1.added[0]);
// Reimport the same transaction with reimportDeleted=false
const result2 = await api.importTransactions(
accountId,
[
{
date: '2023-11-03',
imported_id: 'reimport-test-1',
amount: 100,
account: accountId,
},
],
{ reimportDeleted: false },
);
// Should match the deleted transaction and not create a new one
expect(result2.added).toHaveLength(0);
expect(result2.updated).toHaveLength(0);
});
test('Transactions: reimportDeleted=true reimports deleted transactions', async () => {
const accountId = await api.createAccount({ name: 'test-account' }, 0);
// Import a transaction
const result1 = await api.importTransactions(accountId, [
{
date: '2023-11-03',
imported_id: 'reimport-test-2',
amount: 200,
account: accountId,
},
]);
expect(result1.added).toHaveLength(1);
// Delete the transaction
await api.deleteTransaction(result1.added[0]);
// Reimport the same transaction relying on reimportDeleted=true default
const result2 = await api.importTransactions(accountId, [
{
date: '2023-11-03',
imported_id: 'reimport-test-2',
amount: 200,
account: accountId,
},
]);
// Should create a new transaction since deleted ones are ignored
expect(result2.added).toHaveLength(1);
});
});
//apis: createSchedule, getSchedules, updateSchedule, deleteSchedule

View File

@@ -6,16 +6,16 @@ import type {
APIPayeeEntity,
APIScheduleEntity,
APITagEntity,
} from 'loot-core/server/api-models';
import { lib } from 'loot-core/server/main';
import type { Query } from 'loot-core/shared/query';
import type { ImportTransactionsOpts } from 'loot-core/types/api-handlers';
import type { Handlers } from 'loot-core/types/handlers';
} from '@actual-app/core/server/api-models';
import { lib } from '@actual-app/core/server/main';
import type { Query } from '@actual-app/core/shared/query';
import type { ImportTransactionsOpts } from '@actual-app/core/types/api-handlers';
import type { Handlers } from '@actual-app/core/types/handlers';
import type {
ImportTransactionEntity,
RuleEntity,
TransactionEntity,
} from 'loot-core/types/models';
} from '@actual-app/core/types/models';
export { q } from './app/query';

View File

@@ -10,26 +10,26 @@
"main": "dist/index.js",
"types": "@types/index.d.ts",
"scripts": {
"build": "yarn workspace loot-core exec tsc && vite build && node scripts/inline-loot-core-types.mjs",
"build": "vite build",
"test": "vitest --run",
"typecheck": "tsc --noEmit && tsc-strict"
"typecheck": "tsgo -b && tsc-strict"
},
"dependencies": {
"@actual-app/crdt": "workspace:^",
"@actual-app/core": "workspace:*",
"@actual-app/crdt": "workspace:*",
"better-sqlite3": "^12.6.2",
"compare-versions": "^6.1.1",
"loot-core": "workspace:^",
"node-fetch": "^3.3.2",
"uuid": "^13.0.0"
},
"devDependencies": {
"rollup-plugin-visualizer": "^6.0.5",
"typescript": "^5.9.3",
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
"rollup-plugin-visualizer": "^6.0.11",
"typescript-strict-plugin": "^2.4.4",
"vite": "^7.3.1",
"vite": "^8.0.0",
"vite-plugin-dts": "^4.5.4",
"vite-plugin-peggy-loader": "^2.0.1",
"vitest": "^4.0.18"
"vitest": "^4.1.0"
},
"engines": {
"node": ">=20"

View File

@@ -1,60 +0,0 @@
/**
* Post-build script: copies loot-core declaration tree into @types/loot-core
* and rewrites index.d.ts to reference it so the published package is self-contained.
* Run after vite build; requires loot-core declarations (yarn workspace loot-core exec tsc).
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const apiRoot = path.resolve(__dirname, '..');
const typesDir = path.join(apiRoot, '@types');
const indexDts = path.join(typesDir, 'index.d.ts');
const lootCoreDeclRoot = path.resolve(apiRoot, '../loot-core/lib-dist/decl');
const lootCoreDeclSrc = path.join(lootCoreDeclRoot, 'src');
const lootCoreDeclTypings = path.join(lootCoreDeclRoot, 'typings');
const lootCoreTypesDir = path.join(typesDir, 'loot-core');
function main() {
if (!fs.existsSync(indexDts)) {
console.error('Missing @types/index.d.ts; run vite build first.');
process.exit(1);
}
if (!fs.existsSync(lootCoreDeclSrc)) {
console.error(
'Missing loot-core declarations; run: yarn workspace loot-core exec tsc',
);
process.exit(1);
}
// Remove existing loot-core output (dir or legacy single file)
if (fs.existsSync(lootCoreTypesDir)) {
fs.rmSync(lootCoreTypesDir, { recursive: true });
}
const legacyDts = path.join(typesDir, 'loot-core.d.ts');
if (fs.existsSync(legacyDts)) {
fs.rmSync(legacyDts);
}
// Copy declaration tree: src (main exports) plus emitted typings so no declarations are dropped
fs.cpSync(lootCoreDeclSrc, lootCoreTypesDir, { recursive: true });
if (fs.existsSync(lootCoreDeclTypings)) {
fs.cpSync(lootCoreDeclTypings, path.join(lootCoreTypesDir, 'typings'), {
recursive: true,
});
}
// Rewrite index.d.ts: remove reference, point imports at local ./loot-core/
let indexContent = fs.readFileSync(indexDts, 'utf8');
indexContent = indexContent.replace(
/\/\/\/ <reference path="\.\/loot-core\.d\.ts" \/>\n?/,
'',
);
indexContent = indexContent
.replace(/'loot-core\//g, "'./loot-core/")
.replace(/"loot-core\//g, '"./loot-core/');
fs.writeFileSync(indexDts, indexContent, 'utf8');
}
main();

View File

@@ -1,4 +1,4 @@
import { lib } from 'loot-core/server/main';
import { lib } from '@actual-app/core/server/main';
export const amountToInteger = lib.amountToInteger;
export const integerToAmount = lib.integerToAmount;

View File

@@ -81,12 +81,6 @@ export default defineConfig({
],
resolve: {
extensions: ['.api.ts', '.js', '.ts', '.tsx', '.json'],
alias: [
{
find: /^@actual-app\/crdt(\/.*)?$/,
replacement: path.resolve(__dirname, '../crdt/src') + '$1',
},
],
},
test: {
globals: true,

1
packages/ci-actions/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
dist/*

View File

@@ -2,13 +2,13 @@
// This script is used in GitHub Actions to get the next version based on the current package.json version.
// It supports three types of versioning: nightly, hotfix, and monthly.
import fs from 'node:fs';
import { parseArgs } from 'node:util';
import { getNextVersion } from '../src/versions/get-next-package-version.js';
const args = process.argv;
import {
getNextVersion,
isValidVersionType,
} from '../src/versions/get-next-package-version';
const options = {
'package-json': {
@@ -28,40 +28,53 @@ const options = {
short: 'u',
default: false,
},
};
} as const;
function fail(message: string): never {
console.error(message);
process.exit(1);
}
const { values } = parseArgs({
args,
options,
allowPositionals: true,
});
if (!values['package-json']) {
console.error(
const packageJsonPath = values['package-json'];
if (!packageJsonPath) {
fail(
'Please specify the path to package.json using --package-json or -p option.',
);
process.exit(1);
}
try {
const packageJsonPath = values['package-json'];
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
if (!('version' in packageJson) || typeof packageJson.version !== 'string') {
fail('The specified package.json does not contain a valid version field.');
}
const currentVersion = packageJson.version;
const explicitVersion = values.version;
let newVersion;
if (explicitVersion) {
newVersion = explicitVersion;
} else {
const type = values.type;
if (!type || !isValidVersionType(type)) {
fail('Please specify the release type using --type or -t.');
}
try {
newVersion = getNextVersion({
currentVersion,
type: values.type,
type,
currentDate: new Date(),
});
} catch (e) {
console.error(e.message);
process.exit(1);
} catch (error) {
fail(error instanceof Error ? error.message : String(error));
}
}
@@ -76,6 +89,5 @@ try {
);
}
} catch (error) {
console.error('Error:', error.message);
process.exit(1);
fail(`Error: ${error instanceof Error ? error.message : String(error)}`);
}

8
packages/ci-actions/bin/tsx Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/bash
set -euo pipefail
cd ../../
script="$1"
shift
exec node --import=extensionless/register --experimental-strip-types packages/ci-actions/"$script" "$@"

View File

@@ -3,14 +3,14 @@
"private": true,
"type": "module",
"scripts": {
"tsx": "node --import=extensionless/register --experimental-strip-types",
"tsx": "bin/tsx",
"test": "vitest --run",
"typecheck": "tsc --noEmit"
"typecheck": "tsgo -b"
},
"devDependencies": {
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
"extensionless": "^2.0.6",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
"vitest": "^4.1.0"
},
"extensionless": {
"lookFor": [

View File

@@ -77,7 +77,7 @@ describe('getNextVersion (lib)', () => {
expect(() =>
getNextVersion({
currentVersion: '25.8.4',
type: 'unknown',
type: 'unknown' as never,
currentDate: new Date('2025-08-10'),
}),
).toThrow(/Invalid type/);

View File

@@ -1,35 +1,69 @@
function parseVersion(version) {
export const versionTypeArray = [
'auto',
'hotfix',
'monthly',
'nightly',
] as const;
export type VersionType = (typeof versionTypeArray)[number];
type ParsedVersion = {
versionYear: number;
versionMonth: number;
versionHotfix: number;
};
type GetNextVersionOptions = {
currentVersion: string;
type: VersionType;
currentDate?: Date;
};
function parseVersion(version: string): ParsedVersion {
const [y, m, p] = version.split('.');
return {
versionYear: parseInt(y, 10),
versionMonth: parseInt(m, 10),
versionHotfix: parseInt(p, 10),
versionYear: Number.parseInt(y, 10),
versionMonth: Number.parseInt(m, 10),
versionHotfix: Number.parseInt(p, 10),
};
}
function computeNextMonth(versionYear, versionMonth) {
// Create date and add 1 month
const versionDate = new Date(2000 + versionYear, versionMonth - 1, 1); // month is 0-indexed
function computeNextMonth(versionYear: number, versionMonth: number) {
const versionDate = new Date(2000 + versionYear, versionMonth - 1, 1);
const nextVersionMonthDate = new Date(
versionDate.getFullYear(),
versionDate.getMonth() + 1,
1,
);
// Format back to YY.M format
const fullYear = nextVersionMonthDate.getFullYear();
const nextVersionYear = fullYear.toString().slice(fullYear < 2100 ? -2 : -3);
const nextVersionMonth = nextVersionMonthDate.getMonth() + 1; // Convert back to 1-indexed
const nextVersionMonth = nextVersionMonthDate.getMonth() + 1;
return { nextVersionYear, nextVersionMonth };
}
// Determine logical type from 'auto' based on the current date and version
function resolveType(type, currentDate, versionYear, versionMonth) {
if (type !== 'auto') return type;
export function isValidVersionType(value: string): value is VersionType {
return versionTypeArray.includes(value as VersionType);
}
function resolveType(
type: VersionType,
currentDate: Date,
versionYear: number,
versionMonth: number,
) {
if (type !== 'auto') {
return type;
}
const inPatchMonth =
currentDate.getFullYear() === 2000 + versionYear &&
currentDate.getMonth() + 1 === versionMonth;
if (inPatchMonth && currentDate.getDate() <= 25) return 'hotfix';
if (inPatchMonth && currentDate.getDate() <= 25) {
return 'hotfix';
}
return 'monthly';
}
@@ -37,7 +71,7 @@ export function getNextVersion({
currentVersion,
type,
currentDate = new Date(),
}) {
}: GetNextVersionOptions) {
const { versionYear, versionMonth, versionHotfix } =
parseVersion(currentVersion);
const { nextVersionYear, nextVersionMonth } = computeNextMonth(
@@ -51,11 +85,10 @@ export function getNextVersion({
versionMonth,
);
// Format date stamp once for nightly
const currentDateString = currentDate
.toISOString()
.split('T')[0]
.replaceAll('-', '');
.replace(/-/g, '');
switch (resolvedType) {
case 'nightly':
@@ -66,7 +99,7 @@ export function getNextVersion({
return `${nextVersionYear}.${nextVersionMonth}.0`;
default:
throw new Error(
'Invalid type specified. Use "auto", "nightly", "hotfix", or "monthly".',
`Invalid type ${String(resolvedType satisfies never)} specified. Use "auto", "nightly", "hotfix", or "monthly".`,
);
}
}

View File

@@ -2,13 +2,14 @@
"compilerOptions": {
"target": "ES2022",
"lib": [],
"module": "nodenext",
"moduleResolution": "nodenext",
"module": "es2022",
"moduleResolution": "bundler",
"skipLibCheck": true,
"strict": true,
"types": ["node"],
"outDir": "dist",
"rootDir": "."
"rootDir": ".",
"composite": true
},
"include": ["src/**/*", "bin/**/*"],
"exclude": ["node_modules"]

View File

@@ -2,7 +2,7 @@ import { dirname } from 'path';
import { fileURLToPath } from 'url';
import type { StorybookConfig } from '@storybook/react-vite';
import viteTsconfigPaths from 'vite-tsconfig-paths';
import react from '@vitejs/plugin-react';
/**
* This function is used to resolve the absolute path of a package.
@@ -32,11 +32,9 @@ const config: StorybookConfig = {
const { mergeConfig } = await import('vite');
return mergeConfig(config, {
// Telling Vite how to resolve path aliases
plugins: [viteTsconfigPaths({ root: '../..' })],
esbuild: {
// Needed to handle JSX in .ts/.tsx files
jsx: 'automatic',
plugins: [react()],
resolve: {
tsconfigPaths: true,
},
});
},

View File

@@ -5,7 +5,6 @@ import type { Preview } from '@storybook/react-vite';
// Not ideal to import from desktop-client, but we need a source of truth for theme variables
// TODO: this needs refactoring
import * as darkTheme from '../../desktop-client/src/style/themes/dark';
import * as developmentTheme from '../../desktop-client/src/style/themes/development';
import * as lightTheme from '../../desktop-client/src/style/themes/light';
import * as midnightTheme from '../../desktop-client/src/style/themes/midnight';
@@ -13,7 +12,6 @@ const THEMES = {
light: lightTheme,
dark: darkTheme,
midnight: midnightTheme,
development: developmentTheme,
} as const;
type ThemeName = keyof typeof THEMES;
@@ -64,7 +62,6 @@ const preview: Preview = {
{ value: 'light', title: 'Light' },
{ value: 'dark', title: 'Dark' },
{ value: 'midnight', title: 'Midnight' },
{ value: 'development', title: 'Development' },
],
},
},

View File

@@ -40,7 +40,7 @@
"test:web": "ENV=web vitest --run -c vitest.web.config.ts",
"start:storybook": "storybook dev -p 6006",
"build:storybook": "storybook build",
"typecheck": "tsc --noEmit"
"typecheck": "tsgo -b"
},
"dependencies": {
"@emotion/css": "^11.13.5",
@@ -48,18 +48,20 @@
"usehooks-ts": "^3.1.1"
},
"devDependencies": {
"@chromatic-com/storybook": "^5.0.0",
"@storybook/addon-a11y": "^10.2.7",
"@storybook/addon-docs": "^10.2.7",
"@storybook/react-vite": "^10.2.7",
"@chromatic-com/storybook": "^5.0.1",
"@storybook/addon-a11y": "^10.2.16",
"@storybook/addon-docs": "^10.2.16",
"@storybook/react-vite": "^10.2.16",
"@svgr/cli": "^8.1.0",
"@types/react": "^19.2.5",
"eslint-plugin-storybook": "^10.2.7",
"@types/react": "^19.2.14",
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
"@vitejs/plugin-react": "^6.0.0",
"eslint-plugin-storybook": "^10.2.16",
"react": "19.2.4",
"react-dom": "19.2.4",
"storybook": "^10.2.7",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^4.0.18"
"storybook": "^10.2.16",
"vite": "^8.0.0",
"vitest": "^4.1.0"
},
"peerDependencies": {
"react": ">=19.2",

View File

@@ -4,7 +4,7 @@ import { css, cx } from '@emotion/css';
import type { CSSProperties } from './styles';
type BlockProps = HTMLProps<HTMLDivElement> & {
type BlockProps = Omit<HTMLProps<HTMLDivElement>, 'style'> & {
innerRef?: Ref<HTMLDivElement>;
style?: CSSProperties;
};

View File

@@ -16,8 +16,8 @@ import { View } from './View';
const MenuLine: unique symbol = Symbol('menu-line');
const MenuLabel: unique symbol = Symbol('menu-label');
Menu.line = MenuLine;
Menu.label = MenuLabel;
Menu.line = MenuLine as typeof MenuLine;
Menu.label = MenuLabel as typeof MenuLabel;
type KeybindingProps = {
keyName: ReactNode;

View File

@@ -4,7 +4,7 @@ import { css } from '@emotion/css';
import type { CSSProperties } from './styles';
type ParagraphProps = HTMLProps<HTMLDivElement> & {
type ParagraphProps = Omit<HTMLProps<HTMLDivElement>, 'style'> & {
style?: CSSProperties;
isLast?: boolean;
};

View File

@@ -5,7 +5,7 @@ import { css, cx } from '@emotion/css';
import type { CSSProperties } from './styles';
type TextProps = HTMLProps<HTMLSpanElement> & {
type TextProps = Omit<HTMLProps<HTMLSpanElement>, 'style'> & {
innerRef?: Ref<HTMLSpanElement>;
className?: string;
children?: ReactNode;

View File

@@ -5,7 +5,7 @@ import { css, cx } from '@emotion/css';
import type { CSSProperties } from './styles';
type ViewProps = HTMLProps<HTMLDivElement> & {
type ViewProps = Omit<HTMLProps<HTMLDivElement>, 'style'> & {
className?: string;
style?: CSSProperties;
nativeStyle?: CSSProperties;

View File

@@ -1,5 +1,6 @@
import path from 'path';
import react from '@vitejs/plugin-react';
import peggyLoader from 'vite-plugin-peggy-loader';
import { defineConfig } from 'vitest/config';
@@ -23,13 +24,7 @@ export default defineConfig({
maxWorkers: 2,
},
resolve: {
alias: [
{
find: /^@actual-app\/crdt(\/.*)?$/,
replacement: path.resolve('../../../crdt/src$1'),
},
],
extensions: resolveExtensions,
},
plugins: [peggyLoader()],
plugins: [react(), peggyLoader()],
});

View File

@@ -8,12 +8,23 @@
],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": "./src/index.ts"
},
"publishConfig": {
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
},
"scripts": {
"build:node": "tsc",
"build:node": "tsgo",
"proto:generate": "./bin/generate-proto",
"build": "rm -rf dist && yarn run build:node",
"test": "vitest --run",
"typecheck": "tsc --noEmit"
"typecheck": "tsgo -b"
},
"dependencies": {
"google-protobuf": "^3.21.4",
@@ -22,9 +33,9 @@
},
"devDependencies": {
"@types/google-protobuf": "3.15.12",
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
"protoc-gen-js": "3.21.4-4",
"ts-protoc-gen": "0.15.0",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
"vitest": "^4.1.0"
}
}

View File

@@ -61,7 +61,7 @@ export class ConfigurationPage {
break;
default:
throw new Error(`Unrecognized import type: ${type}`);
throw new Error(`Unrecognized import type: ${String(type)}`);
}
const fileChooser = await fileChooserPromise;

View File

@@ -39,7 +39,7 @@ export class CustomReportPage {
.click();
break;
default:
throw new Error(`Unrecognized mode: ${mode}`);
throw new Error(`Unrecognized mode: ${String(mode)}`);
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 121 KiB

View File

@@ -5,14 +5,6 @@
"files": [
"build"
],
"imports": {
"#*": [
"./src/*.ts",
"./src/*.tsx",
"./src/*/index.ts",
"./src/*/index.tsx"
]
},
"scripts": {
"start": "cross-env PORT=3001 vite",
"start:browser": "cross-env ./bin/watch-browser",
@@ -24,13 +16,15 @@
"e2e": "npx playwright test --browser=chromium",
"vrt": "cross-env VRT=true npx playwright test --browser=chromium",
"playwright": "playwright",
"typecheck": "tsc --noEmit && tsc-strict"
"typecheck": "tsgo -b && tsc-strict"
},
"devDependencies": {
"@actual-app/components": "workspace:*",
"@codemirror/autocomplete": "^6.20.0",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/language": "^6.12.1",
"@actual-app/core": "workspace:*",
"@babel/core": "^7.29.0",
"@codemirror/autocomplete": "^6.20.1",
"@codemirror/lang-javascript": "^6.2.5",
"@codemirror/language": "^6.12.2",
"@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.38.7",
"@emotion/css": "^11.13.5",
@@ -39,10 +33,11 @@
"@juggle/resize-observer": "^3.4.0",
"@lezer/highlight": "^1.2.3",
"@playwright/test": "1.58.2",
"@rolldown/plugin-babel": "~0.1.8",
"@rollup/plugin-inject": "^5.0.5",
"@swc/core": "^1.15.11",
"@swc/helpers": "^0.5.18",
"@tanstack/react-query": "^5.90.20",
"@swc/core": "^1.15.18",
"@swc/helpers": "^0.5.19",
"@tanstack/react-query": "^5.90.21",
"@testing-library/dom": "10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "16.3.2",
@@ -50,26 +45,26 @@
"@types/lodash": "^4",
"@types/pikaday": "^1.7.10",
"@types/promise-retry": "^1.1.6",
"@types/react": "^19.2.5",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/react-modal": "^3.16.3",
"@uiw/react-codemirror": "^4.25.4",
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
"@uiw/react-codemirror": "^4.25.7",
"@use-gesture/react": "^10.3.1",
"@vitejs/plugin-basic-ssl": "^2.1.4",
"@vitejs/plugin-react": "^5.1.3",
"@vitejs/plugin-basic-ssl": "^2.2.0",
"@vitejs/plugin-react": "^6.0.0",
"auto-text-size": "^0.2.3",
"babel-plugin-react-compiler": "^1.0.0",
"cmdk": "^1.1.1",
"cross-env": "^10.1.0",
"date-fns": "^4.1.0",
"downshift": "9.0.10",
"hyperformula": "^3.1.1",
"i18next": "^25.8.4",
"i18next-parser": "^9.3.0",
"downshift": "9.3.2",
"hyperformula": "^3.2.0",
"i18next": "^25.8.14",
"i18next-parser": "^9.4.0",
"i18next-resources-to-backend": "^1.2.1",
"jsdom": "^27.4.0",
"lodash": "^4.17.23",
"loot-core": "workspace:*",
"mdast-util-newline-to-break": "^2.0.0",
"memoize-one": "^6.0.0",
"pikaday": "1.8.2",
@@ -85,27 +80,26 @@
"react-error-boundary": "^6.0.3",
"react-grid-layout": "^2.2.2",
"react-hotkeys-hook": "^5.2.4",
"react-i18next": "^16.5.4",
"react-i18next": "^16.5.6",
"react-markdown": "^10.1.0",
"react-modal": "3.16.3",
"react-redux": "^9.2.0",
"react-router": "7.13.0",
"react-router": "7.13.1",
"react-simple-pull-to-refresh": "^1.3.4",
"react-spring": "10.0.0",
"react-spring": "^10.0.3",
"react-swipeable": "^7.0.2",
"react-virtualized-auto-sizer": "^2.0.2",
"react-virtualized-auto-sizer": "^2.0.3",
"recharts": "^3.7.0",
"rehype-external-links": "^3.0.0",
"remark-gfm": "^4.0.1",
"rollup-plugin-visualizer": "^6.0.5",
"rollup-plugin-visualizer": "^6.0.11",
"sass": "^1.97.3",
"typescript": "^5.9.3",
"typescript-strict-plugin": "^2.4.4",
"usehooks-ts": "^3.1.1",
"uuid": "^13.0.0",
"vite": "^7.3.1",
"vite": "^8.0.0",
"vite-plugin-pwa": "^1.2.0",
"vitest": "^4.0.18",
"vitest": "^4.1.0",
"xml2js": "^0.6.2"
}
}

View File

@@ -5,7 +5,7 @@ import memoizeOne from 'memoize-one';
import { groupById } from 'loot-core/shared/util';
import type { AccountEntity } from 'loot-core/types/models';
import { resetApp } from '#app/appSlice';
import { resetApp } from '@desktop-client/app/appSlice';
const sliceName = 'account';

View File

@@ -23,13 +23,13 @@ import {
} from './accountsSlice';
import { accountQueries } from './queries';
import { sync } from '#app/appSlice';
import { useAccounts } from '#hooks/useAccounts';
import { addNotification } from '#notifications/notificationsSlice';
import { payeeQueries } from '#payees';
import { useDispatch, useStore } from '#redux';
import type { AppDispatch } from '#redux/store';
import { setNewTransactions } from '#transactions/transactionsSlice';
import { sync } from '@desktop-client/app/appSlice';
import { useAccounts } from '@desktop-client/hooks/useAccounts';
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
import { payeeQueries } from '@desktop-client/payees';
import { useDispatch, useStore } from '@desktop-client/redux';
import type { AppDispatch } from '@desktop-client/redux/store';
import { setNewTransactions } from '@desktop-client/transactions/transactionsSlice';
const invalidateQueries = (queryClient: QueryClient, queryKey?: QueryKey) => {
void queryClient.invalidateQueries({
@@ -207,6 +207,7 @@ export function useMoveAccountMutation() {
type ImportPreviewTransactionsPayload = {
accountId: string;
transactions: TransactionEntity[];
reimportDeleted?: boolean;
};
export function useImportPreviewTransactionsMutation() {
@@ -218,6 +219,7 @@ export function useImportPreviewTransactionsMutation() {
mutationFn: async ({
accountId,
transactions,
reimportDeleted,
}: ImportPreviewTransactionsPayload) => {
const { errors = [], updatedPreview } = await send(
'transactions-import',
@@ -225,6 +227,7 @@ export function useImportPreviewTransactionsMutation() {
accountId,
transactions,
isPreview: true,
opts: reimportDeleted !== undefined ? { reimportDeleted } : undefined,
},
);
@@ -259,6 +262,7 @@ type ImportTransactionsPayload = {
accountId: string;
transactions: TransactionEntity[];
reconcile: boolean;
reimportDeleted?: boolean;
};
export function useImportTransactionsMutation() {
@@ -271,6 +275,7 @@ export function useImportTransactionsMutation() {
accountId,
transactions,
reconcile,
reimportDeleted,
}: ImportTransactionsPayload) => {
if (!reconcile) {
await send('api/transactions-add', {
@@ -289,6 +294,7 @@ export function useImportTransactionsMutation() {
accountId,
transactions,
isPreview: false,
opts: reimportDeleted !== undefined ? { reimportDeleted } : undefined,
});
errors.forEach(error => {

View File

@@ -5,10 +5,10 @@ import { send } from 'loot-core/platform/client/connection';
import { getUploadError } from 'loot-core/shared/errors';
import type { AtLeastOne } from 'loot-core/types/util';
import { pushModal } from '#modals/modalsSlice';
import { loadPrefs } from '#prefs/prefsSlice';
import { createAppAsyncThunk } from '#redux';
import { getIsOutdated, getLatestVersion } from '#util/versions';
import { pushModal } from '@desktop-client/modals/modalsSlice';
import { loadPrefs } from '@desktop-client/prefs/prefsSlice';
import { createAppAsyncThunk } from '@desktop-client/redux';
import { getIsOutdated, getLatestVersion } from '@desktop-client/util/versions';
const sliceName = 'app';

View File

@@ -3,8 +3,8 @@ import type { ReactNode } from 'react';
import type { Permissions } from './types';
import { useServerURL } from '#components/ServerContext';
import { useSelector } from '#redux';
import { useServerURL } from '@desktop-client/components/ServerContext';
import { useSelector } from '@desktop-client/redux';
type AuthContextType = {
hasPermission: (permission?: Permissions) => boolean;

View File

@@ -9,8 +9,8 @@ import type { RemoteFile, SyncedLocalFile } from 'loot-core/types/file';
import { useAuth } from './AuthProvider';
import type { Permissions } from './types';
import { useMetadataPref } from '#hooks/useMetadataPref';
import { useSelector } from '#redux';
import { useMetadataPref } from '@desktop-client/hooks/useMetadataPref';
import { useSelector } from '@desktop-client/redux';
type ProtectedRouteProps = {
permission: Permissions;

View File

@@ -6,6 +6,8 @@ import * as Platform from 'loot-core/shared/platform';
// oxlint-disable-next-line typescript-paths/absolute-parent-import
import packageJson from '../package.json';
import SharedBrowserServerWorker from './shared-browser-server.ts?sharedworker';
const backendWorkerUrl = new URL('./browser-server.js', import.meta.url);
// This file installs global variables that the app expects.
@@ -21,9 +23,235 @@ const ACTUAL_VERSION = Platform.isPlaywright
: packageJson.version;
// *** Start the backend ***
let worker = null;
// The regular Worker running the backend, created only on the leader tab
let localBackendWorker = null;
/**
* WorkerBridge wraps a SharedWorker port and presents a Worker-like interface
* (onmessage, postMessage, addEventListener, start) to the connection layer.
*
* The SharedWorker coordinator assigns each tab a role per budget:
* - LEADER: this tab runs the backend in a dedicated Worker
* - FOLLOWER: this tab routes messages through the SharedWorker to the leader
*
* Multiple budgets can be open simultaneously — each has its own leader.
*/
class WorkerBridge {
constructor(sharedPort) {
this._sharedPort = sharedPort;
this._onmessage = null;
this._listeners = [];
this._started = false;
// Listen for all messages from the SharedWorker port
sharedPort.addEventListener('message', e => this._onSharedMessage(e));
}
set onmessage(handler) {
this._onmessage = handler;
// Setting onmessage on a real MessagePort implicitly starts it.
// We need to do this explicitly on the underlying port.
if (!this._started) {
this._started = true;
this._sharedPort.start();
}
}
get onmessage() {
return this._onmessage;
}
postMessage(msg) {
// All messages go through the SharedWorker for coordination.
// The SharedWorker forwards to the leader's Worker via __to-worker.
this._sharedPort.postMessage(msg);
}
addEventListener(type, handler) {
this._listeners.push({ type, handler });
}
start() {
if (!this._started) {
this._started = true;
this._sharedPort.start();
}
}
_dispatch(event) {
if (this._onmessage) this._onmessage(event);
for (const { type, handler } of this._listeners) {
if (type === 'message') handler(event);
}
}
_onSharedMessage(event) {
const msg = event.data;
// Elected as leader: create the real backend Worker on this tab
if (msg && msg.type === '__become-leader') {
this._createLocalWorker(msg.initMsg, msg.budgetToRestore, msg.pendingMsg);
return;
}
// Forward requests from SharedWorker to our local Worker
if (msg && msg.type === '__to-worker') {
if (localBackendWorker) {
localBackendWorker.postMessage(msg.msg);
}
return;
}
// Leadership transfer: this tab is closing the budget but other tabs
// still need it. Terminate our Worker (don't actually close-budget on
// the backend) and dispatch a synthetic reply so the UI navigates to
// show-budgets normally.
if (msg && msg.type === '__close-and-transfer') {
console.log('[WorkerBridge] Leadership transferred — terminating Worker');
if (localBackendWorker) {
localBackendWorker.terminate();
localBackendWorker = null;
}
// Only dispatch a synthetic reply if there's an actual close-budget
// request to complete. When requestId is null the eviction was
// triggered externally (e.g. another tab deleted this budget).
if (msg.requestId) {
this._dispatch({
data: { type: 'reply', id: msg.requestId, data: {} },
});
}
return;
}
// Role change notification
if (msg && msg.type === '__role-change') {
console.log(
`[WorkerBridge] Role: ${msg.role}${msg.budgetId ? ` (budget: ${msg.budgetId})` : ''}`,
);
return;
}
// Surface SharedWorker console output in this tab's DevTools
if (msg && msg.type === '__shared-worker-console') {
const method = console[msg.level] || console.log;
method(...msg.args);
return;
}
// Respond to heartbeat pings
if (msg && msg.type === '__heartbeat-ping') {
this._sharedPort.postMessage({ type: '__heartbeat-pong' });
return;
}
// Everything else goes to the connection layer
this._dispatch(event);
}
_createLocalWorker(initMsg, budgetToRestore, pendingMsg) {
if (localBackendWorker) {
localBackendWorker.terminate();
}
localBackendWorker = new Worker(backendWorkerUrl);
initSQLBackend(localBackendWorker);
const sharedPort = this._sharedPort;
localBackendWorker.onmessage = workerEvent => {
const workerMsg = workerEvent.data;
// absurd-sql internal messages are handled by initSQLBackend
if (
workerMsg &&
workerMsg.type &&
workerMsg.type.startsWith('__absurd:')
) {
return;
}
// After the backend connects, automatically reload the budget that was
// open before the leader left (e.g. page refresh). This lets other tabs
// continue working without being sent to the budget list.
if (workerMsg.type === 'connect') {
if (budgetToRestore) {
console.log(
`[WorkerBridge] Backend connected, restoring budget "${budgetToRestore}"`,
);
const id = budgetToRestore;
budgetToRestore = null;
localBackendWorker.postMessage({
id: '__restore-budget',
name: 'load-budget',
args: { id },
catchErrors: true,
});
// Tell SharedWorker to track the restore request so
// currentBudgetId gets updated when the reply arrives.
sharedPort.postMessage({
type: '__track-restore',
requestId: '__restore-budget',
budgetId: id,
});
} else if (pendingMsg) {
const toSend = pendingMsg;
pendingMsg = null;
localBackendWorker.postMessage(toSend);
}
}
sharedPort.postMessage({ type: '__from-worker', msg: workerMsg });
};
localBackendWorker.postMessage(initMsg);
}
}
function createBackendWorker() {
// Use SharedWorker as a coordinator for multi-tab, multi-budget support.
// Each budget gets its own leader tab running a dedicated Worker. All other
// tabs on the same budget are followers — their messages are routed through
// the SharedWorker to the leader's Worker.
// The SharedWorker never touches SharedArrayBuffer, so this works on all
// platforms including iOS/Safari.
if (typeof SharedWorker !== 'undefined' && !Platform.isPlaywright) {
try {
const sharedWorker = new SharedBrowserServerWorker({
name: 'actual-backend',
});
const sharedPort = sharedWorker.port;
worker = new WorkerBridge(sharedPort);
console.log('[WorkerBridge] Connected to SharedWorker coordinator');
// Don't call start() here. The port must remain un-started so that
// messages (especially 'connect') are queued until connectWorker()
// sets onmessage, which implicitly starts the port via the bridge.
if (window.SharedArrayBuffer) {
localStorage.removeItem('SharedArrayBufferOverride');
}
sharedPort.postMessage({
type: 'init',
version: ACTUAL_VERSION,
isDev: IS_DEV,
publicUrl: process.env.PUBLIC_URL,
hash: process.env.REACT_APP_BACKEND_WORKER_HASH,
isSharedArrayBufferOverrideEnabled: localStorage.getItem(
'SharedArrayBufferOverride',
),
});
window.addEventListener('beforeunload', () => {
sharedPort.postMessage({ type: 'tab-closing' });
});
return;
} catch (e) {
console.log('SharedWorker failed, falling back to Worker:', e);
}
}
// Fallback: regular Worker (Playwright, no SharedWorker support, or failure)
console.log('[WorkerBridge] No SharedWorker available, using direct Worker');
worker = new Worker(backendWorkerUrl);
initSQLBackend(worker);
@@ -37,6 +265,7 @@ function createBackendWorker() {
isDev: IS_DEV,
publicUrl: process.env.PUBLIC_URL,
hash: process.env.REACT_APP_BACKEND_WORKER_HASH,
hasSharedArrayBuffer: !!window.SharedArrayBuffer,
isSharedArrayBufferOverrideEnabled: localStorage.getItem(
'SharedArrayBufferOverride',
),

View File

@@ -14,10 +14,10 @@ import type {
import { categoryQueries } from './queries';
import { pushModal } from '#modals/modalsSlice';
import { addNotification } from '#notifications/notificationsSlice';
import { useDispatch } from '#redux';
import type { AppDispatch } from '#redux/store';
import { pushModal } from '@desktop-client/modals/modalsSlice';
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
import { useDispatch } from '@desktop-client/redux';
import type { AppDispatch } from '@desktop-client/redux/store';
function invalidateQueries(queryClient: QueryClient, queryKey?: QueryKey) {
void queryClient.invalidateQueries({
@@ -779,7 +779,7 @@ export function useBudgetActions() {
});
return null;
default:
throw new Error(`Unknown budget action type: ${type}`);
throw new Error(`Unknown budget action type: ${String(type)}`);
}
},
onSuccess: notification => {

View File

@@ -9,11 +9,11 @@ import type { Budget } from 'loot-core/types/budget';
import type { File } from 'loot-core/types/file';
import type { Handlers } from 'loot-core/types/handlers';
import { resetApp, setAppState } from '#app/appSlice';
import { closeModal, pushModal } from '#modals/modalsSlice';
import { loadGlobalPrefs, loadPrefs } from '#prefs/prefsSlice';
import { createAppAsyncThunk } from '#redux';
import { signOut } from '#users/usersSlice';
import { resetApp, setAppState } from '@desktop-client/app/appSlice';
import { closeModal, pushModal } from '@desktop-client/modals/modalsSlice';
import { loadGlobalPrefs, loadPrefs } from '@desktop-client/prefs/prefsSlice';
import { createAppAsyncThunk } from '@desktop-client/redux';
import { signOut } from '@desktop-client/users/usersSlice';
const sliceName = 'budgetfiles';

View File

@@ -26,26 +26,29 @@ import { Modals } from './Modals';
import { SidebarProvider } from './sidebar/SidebarProvider';
import { UpdateNotification } from './UpdateNotification';
import { setAppState, sync } from '#app/appSlice';
import { closeBudget, loadBudget } from '#budgetfiles/budgetfilesSlice';
import { handleGlobalEvents } from '#global-events';
import { useIsTestEnv } from '#hooks/useIsTestEnv';
import { useMetadataPref } from '#hooks/useMetadataPref';
import { useOnVisible } from '#hooks/useOnVisible';
import { SpreadsheetProvider } from '#hooks/useSpreadsheet';
import { setI18NextLanguage } from '#i18n';
import { addNotification } from '#notifications/notificationsSlice';
import { installPolyfills } from '#polyfills';
import { loadGlobalPrefs } from '#prefs/prefsSlice';
import { useDispatch, useSelector, useStore } from '#redux';
import { setAppState, sync } from '@desktop-client/app/appSlice';
import {
closeBudget,
loadBudget,
} from '@desktop-client/budgetfiles/budgetfilesSlice';
import { handleGlobalEvents } from '@desktop-client/global-events';
import { useIsTestEnv } from '@desktop-client/hooks/useIsTestEnv';
import { useMetadataPref } from '@desktop-client/hooks/useMetadataPref';
import { useOnVisible } from '@desktop-client/hooks/useOnVisible';
import { SpreadsheetProvider } from '@desktop-client/hooks/useSpreadsheet';
import { setI18NextLanguage } from '@desktop-client/i18n';
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
import { installPolyfills } from '@desktop-client/polyfills';
import { loadGlobalPrefs } from '@desktop-client/prefs/prefsSlice';
import { useDispatch, useSelector, useStore } from '@desktop-client/redux';
import {
CustomThemeStyle,
hasHiddenScrollbars,
ThemeStyle,
useTheme,
} from '#style';
import { signOut } from '#users/usersSlice';
import { ExposeNavigate } from '#util/router-tools';
} from '@desktop-client/style';
import { signOut } from '@desktop-client/users/usersSlice';
import { ExposeNavigate } from '@desktop-client/util/router-tools';
function AppInner() {
const [budgetId] = useMetadataPref('id');

View File

@@ -9,7 +9,7 @@ import { css } from '@emotion/css';
import { Background } from './Background';
import { useSelector } from '#redux';
import { useSelector } from '@desktop-client/redux';
type AppBackgroundProps = {
isLoading?: boolean;

View File

@@ -9,7 +9,7 @@ import { View } from '@actual-app/components/view';
import { AnimatedRefresh } from './AnimatedRefresh';
import { useSelector } from '#redux';
import { useSelector } from '@desktop-client/redux';
export function BankSyncStatus() {
const accountsSyncing = useSelector(state => state.account.accountsSyncing);

View File

@@ -24,19 +24,23 @@ import { Command } from 'cmdk';
import { CellValue, CellValueText } from './spreadsheet/CellValue';
import { useAccounts } from '#hooks/useAccounts';
import { useDashboardPages } from '#hooks/useDashboardPages';
import { useMetadataPref } from '#hooks/useMetadataPref';
import { useModalState } from '#hooks/useModalState';
import { useNavigate } from '#hooks/useNavigate';
import { useReports } from '#hooks/useReports';
import type { Binding, SheetFields, SheetNames } from '#spreadsheet';
import { useAccounts } from '@desktop-client/hooks/useAccounts';
import { useDashboardPages } from '@desktop-client/hooks/useDashboardPages';
import { useMetadataPref } from '@desktop-client/hooks/useMetadataPref';
import { useModalState } from '@desktop-client/hooks/useModalState';
import { useNavigate } from '@desktop-client/hooks/useNavigate';
import { useReports } from '@desktop-client/hooks/useReports';
import type {
Binding,
SheetFields,
SheetNames,
} from '@desktop-client/spreadsheet';
import {
accountBalance,
allAccountBalance,
offBudgetAccountBalance,
onBudgetAccountBalance,
} from '#spreadsheet/bindings';
} from '@desktop-client/spreadsheet/bindings';
type SearchableItem = {
id: string;

View File

@@ -15,7 +15,7 @@ import { Link } from './common/Link';
import { Modal, ModalHeader } from './common/Modal';
import { Checkbox } from './forms';
import { useModalState } from '#hooks/useModalState';
import { useModalState } from '@desktop-client/hooks/useModalState';
type AppError = Error & {
type?: string;

View File

@@ -28,18 +28,18 @@ import { FloatableSidebar } from './sidebar';
import { ManageTagsPage } from './tags/ManageTagsPage';
import { Titlebar } from './Titlebar';
import { accountQueries } from '#accounts';
import { getLatestAppVersion, sync } from '#app/appSlice';
import { ProtectedRoute } from '#auth/ProtectedRoute';
import { Permissions } from '#auth/types';
import { useAccounts } from '#hooks/useAccounts';
import { useGlobalPref } from '#hooks/useGlobalPref';
import { useLocalPref } from '#hooks/useLocalPref';
import { useMetaThemeColor } from '#hooks/useMetaThemeColor';
import { useNavigate } from '#hooks/useNavigate';
import { ScrollProvider } from '#hooks/useScrollListener';
import { addNotification } from '#notifications/notificationsSlice';
import { useDispatch, useSelector } from '#redux';
import { accountQueries } from '@desktop-client/accounts';
import { getLatestAppVersion, sync } from '@desktop-client/app/appSlice';
import { ProtectedRoute } from '@desktop-client/auth/ProtectedRoute';
import { Permissions } from '@desktop-client/auth/types';
import { useAccounts } from '@desktop-client/hooks/useAccounts';
import { useGlobalPref } from '@desktop-client/hooks/useGlobalPref';
import { useLocalPref } from '@desktop-client/hooks/useLocalPref';
import { useMetaThemeColor } from '@desktop-client/hooks/useMetaThemeColor';
import { useNavigate } from '@desktop-client/hooks/useNavigate';
import { ScrollProvider } from '@desktop-client/hooks/useScrollListener';
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
import { useDispatch, useSelector } from '@desktop-client/redux';
function NarrowNotSupported({
redirectTo = '/budget',

View File

@@ -2,7 +2,7 @@ import { useEffect } from 'react';
import * as Platform from 'loot-core/shared/platform';
import { useNavigate } from '#hooks/useNavigate';
import { useNavigate } from '@desktop-client/hooks/useNavigate';
export function GlobalKeys() {
const navigate = useNavigate();

View File

@@ -10,9 +10,9 @@ import { Popover } from '@actual-app/components/popover';
import { SpaceBetween } from '@actual-app/components/space-between';
import { useToggle } from 'usehooks-ts';
import { useFeatureFlag } from '#hooks/useFeatureFlag';
import { pushModal } from '#modals/modalsSlice';
import { useDispatch } from '#redux';
import { useFeatureFlag } from '@desktop-client/hooks/useFeatureFlag';
import { pushModal } from '@desktop-client/modals/modalsSlice';
import { useDispatch } from '@desktop-client/redux';
const getPageDocs = (page: string) => {
switch (page) {
@@ -95,7 +95,7 @@ export const HelpMenu = () => {
dispatch(pushModal({ modal: { name: 'goal-templates' } }));
break;
default:
throw new Error(`Unrecognized menu option: ${item}`);
throw new Error(`Unrecognized menu option: ${String(item)}`);
}
};

View File

@@ -18,13 +18,13 @@ import type { TransObjectLiteral } from 'loot-core/types/util';
import { PrivacyFilter } from './PrivacyFilter';
import { useMultiuserEnabled, useServerURL } from './ServerContext';
import { useAuth } from '#auth/AuthProvider';
import { Permissions } from '#auth/types';
import { closeBudget } from '#budgetfiles/budgetfilesSlice';
import { useMetadataPref } from '#hooks/useMetadataPref';
import { useNavigate } from '#hooks/useNavigate';
import { useDispatch, useSelector } from '#redux';
import { getUserData, signOut } from '#users/usersSlice';
import { useAuth } from '@desktop-client/auth/AuthProvider';
import { Permissions } from '@desktop-client/auth/types';
import { closeBudget } from '@desktop-client/budgetfiles/budgetfilesSlice';
import { useMetadataPref } from '@desktop-client/hooks/useMetadataPref';
import { useNavigate } from '@desktop-client/hooks/useNavigate';
import { useDispatch, useSelector } from '@desktop-client/redux';
import { getUserData, signOut } from '@desktop-client/users/usersSlice';
type LoggedInUserProps = {
hideIfNoServer?: boolean;

View File

@@ -28,13 +28,16 @@ import { Search } from './common/Search';
import { RulesHeader } from './rules/RulesHeader';
import { RulesList } from './rules/RulesList';
import { useAccounts } from '#hooks/useAccounts';
import { useCategories } from '#hooks/useCategories';
import { usePayees } from '#hooks/usePayees';
import { useSchedules } from '#hooks/useSchedules';
import { SelectedProvider, useSelected } from '#hooks/useSelected';
import { pushModal } from '#modals/modalsSlice';
import { useDispatch } from '#redux';
import { useAccounts } from '@desktop-client/hooks/useAccounts';
import { useCategories } from '@desktop-client/hooks/useCategories';
import { usePayees } from '@desktop-client/hooks/usePayees';
import { useSchedules } from '@desktop-client/hooks/useSchedules';
import {
SelectedProvider,
useSelected,
} from '@desktop-client/hooks/useSelected';
import { pushModal } from '@desktop-client/modals/modalsSlice';
import { useDispatch } from '@desktop-client/redux';
export type FilterData = {
payees?: Array<{ id: string; name: string }>;

View File

@@ -17,6 +17,7 @@ import { CategoryMenuModal } from './modals/CategoryMenuModal';
import { CloseAccountModal } from './modals/CloseAccountModal';
import { ConfirmCategoryDeleteModal } from './modals/ConfirmCategoryDeleteModal';
import { ConfirmDeleteModal } from './modals/ConfirmDeleteModal';
import { ConfirmPayeesMergeModal } from './modals/ConfirmPayeesMergeModal';
import { ConfirmTransactionEditModal } from './modals/ConfirmTransactionEditModal';
import { ConfirmUnlinkAccountModal } from './modals/ConfirmUnlinkAccountModal';
import { ConvertToScheduleModal } from './modals/ConvertToScheduleModal';
@@ -78,11 +79,11 @@ import { ScheduleEditModal } from './schedules/ScheduleEditModal';
import { ScheduleLink } from './schedules/ScheduleLink';
import { UpcomingLength } from './schedules/UpcomingLength';
import { useMetadataPref } from '#hooks/useMetadataPref';
import { useModalState } from '#hooks/useModalState';
import { SheetNameProvider } from '#hooks/useSheetName';
import { closeModal } from '#modals/modalsSlice';
import { useDispatch } from '#redux';
import { useMetadataPref } from '@desktop-client/hooks/useMetadataPref';
import { useModalState } from '@desktop-client/hooks/useModalState';
import { SheetNameProvider } from '@desktop-client/hooks/useSheetName';
import { closeModal } from '@desktop-client/modals/modalsSlice';
import { useDispatch } from '@desktop-client/redux';
export function Modals() {
const location = useLocation();
@@ -140,6 +141,9 @@ export function Modals() {
case 'confirm-category-delete':
return <ConfirmCategoryDeleteModal key={key} {...modal.options} />;
case 'confirm-payees-merge':
return <ConfirmPayeesMergeModal key={key} {...modal.options} />;
case 'confirm-unlink-account':
return <ConfirmUnlinkAccountModal key={key} {...modal.options} />;

View File

@@ -14,7 +14,7 @@ import {
markdownBaseStyles,
remarkBreaks,
sequentialNewlinesPlugin,
} from '#util/markdown';
} from '@desktop-client/util/markdown';
const remarkPlugins = [sequentialNewlinesPlugin, remarkGfm, remarkBreaks];

View File

@@ -1,10 +1,11 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { ComponentProps, CSSProperties } from 'react';
import type { ComponentProps } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from '@actual-app/components/button';
import { SvgCustomNotesPaper } from '@actual-app/components/icons/v2';
import { Popover } from '@actual-app/components/popover';
import type { CSSProperties } from '@actual-app/components/styles';
import { theme } from '@actual-app/components/theme';
import { Tooltip } from '@actual-app/components/tooltip';
import { View } from '@actual-app/components/view';
@@ -14,7 +15,7 @@ import { send } from 'loot-core/platform/client/connection';
import { Notes } from './Notes';
import { useNotes } from '#hooks/useNotes';
import { useNotes } from '@desktop-client/hooks/useNotes';
type NotesButtonProps = {
id: string;

View File

@@ -18,9 +18,9 @@ import { css } from '@emotion/css';
import { Link } from './common/Link';
import { MODAL_Z_INDEX } from './common/Modal';
import { removeNotification } from '#notifications/notificationsSlice';
import type { NotificationWithId } from '#notifications/notificationsSlice';
import { useDispatch, useSelector } from '#redux';
import { removeNotification } from '@desktop-client/notifications/notificationsSlice';
import type { NotificationWithId } from '@desktop-client/notifications/notificationsSlice';
import { useDispatch, useSelector } from '@desktop-client/redux';
// Notification stacking configuration
const MAX_VISIBLE_NOTIFICATIONS = 3; // Maximum number of notifications visible in the stack
@@ -153,22 +153,27 @@ function Notification({
const yOffset = index * Y_OFFSET_PER_LEVEL;
const [isSwiped, setIsSwiped] = useState(false);
const [spring, api] = useSpring(() => ({
x: 0,
y: yOffset,
opacity: stackOpacity,
scale,
}));
const [spring, api] = useSpring(
() => ({
from: {
x: 0,
y: yOffset,
opacity: stackOpacity,
scale,
},
}),
[],
);
// Update scale, opacity, and y-position when index changes
useEffect(() => {
void api.start({ scale, opacity: stackOpacity, y: yOffset });
void api.start({ to: { scale, opacity: stackOpacity, y: yOffset } });
}, [index, scale, stackOpacity, yOffset, api]);
const swipeHandlers = useSwipeable({
onSwiping: ({ deltaX }) => {
if (!isSwiped) {
void api.start({ x: deltaX });
void api.start({ to: { x: deltaX } });
}
},
onSwiped: ({ velocity, deltaX }) => {
@@ -179,14 +184,13 @@ function Notification({
if (Math.abs(deltaX) > threshold || velocity > 0.5) {
// Animate out & remove item after animation
void api.start({
x: direction * 1000,
opacity: 0,
to: { x: direction * 1000, opacity: 0 },
onRest: onRemove,
});
setIsSwiped(true);
} else {
// Reset position if not swiped far enough
void api.start({ x: 0 });
void api.start({ to: { x: 0 } });
}
},
trackMouse: true,

View File

@@ -6,7 +6,7 @@ import { useResponsive } from '@actual-app/components/hooks/useResponsive';
import { View } from '@actual-app/components/view';
import { css } from '@emotion/css';
import { usePrivacyMode } from '#hooks/usePrivacyMode';
import { usePrivacyMode } from '@desktop-client/hooks/usePrivacyMode';
type ConditionalPrivacyFilterProps = {
children: ReactNode;

View File

@@ -12,9 +12,9 @@ import { t } from 'i18next';
import { send } from 'loot-core/platform/client/connection';
import type { Handlers } from 'loot-core/types/handlers';
import { useOnVisible } from '#hooks/useOnVisible';
import { addNotification } from '#notifications/notificationsSlice';
import { useDispatch } from '#redux';
import { useOnVisible } from '@desktop-client/hooks/useOnVisible';
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
import { useDispatch } from '@desktop-client/redux';
type LoginMethod = {
method: string;

View File

@@ -14,7 +14,7 @@ import { Popover } from '@actual-app/components/popover';
import type { Theme } from 'loot-core/types/prefs';
import { themeOptions, useTheme } from '#style';
import { themeOptions, useTheme } from '@desktop-client/style';
type ThemeSelectorProps = {
style?: CSSProperties;
@@ -33,7 +33,6 @@ export function ThemeSelector({ style }: ThemeSelectorProps) {
dark: SvgMoonStars,
auto: SvgSystem,
midnight: SvgMoonStars,
development: SvgMoonStars,
} as const;
type ThemeIconKey = keyof typeof themeIcons;

View File

@@ -34,15 +34,15 @@ import { useServerURL } from './ServerContext';
import { useSidebar } from './sidebar/SidebarProvider';
import { ThemeSelector } from './ThemeSelector';
import { sync } from '#app/appSlice';
import { useGlobalPref } from '#hooks/useGlobalPref';
import { useIsTestEnv } from '#hooks/useIsTestEnv';
import { useMetadataPref } from '#hooks/useMetadataPref';
import { useNavigate } from '#hooks/useNavigate';
import { useSheetValue } from '#hooks/useSheetValue';
import { useSyncedPref } from '#hooks/useSyncedPref';
import { useDispatch } from '#redux';
import * as bindings from '#spreadsheet/bindings';
import { sync } from '@desktop-client/app/appSlice';
import { useGlobalPref } from '@desktop-client/hooks/useGlobalPref';
import { useIsTestEnv } from '@desktop-client/hooks/useIsTestEnv';
import { useMetadataPref } from '@desktop-client/hooks/useMetadataPref';
import { useNavigate } from '@desktop-client/hooks/useNavigate';
import { useSheetValue } from '@desktop-client/hooks/useSheetValue';
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
import { useDispatch } from '@desktop-client/redux';
import * as bindings from '@desktop-client/spreadsheet/bindings';
function UncategorizedButton() {
const count: number | null = useSheetValue(bindings.uncategorizedCount());

View File

@@ -9,8 +9,8 @@ import { View } from '@actual-app/components/view';
import { Link } from './common/Link';
import { setAppState, updateApp } from '#app/appSlice';
import { useDispatch, useSelector } from '#redux';
import { setAppState, updateApp } from '@desktop-client/app/appSlice';
import { useDispatch, useSelector } from '@desktop-client/redux';
export function UpdateNotification() {
const { t } = useTranslation();

View File

@@ -46,44 +46,44 @@ import {
useSyncAndDownloadMutation,
useUnlinkAccountMutation,
useUpdateAccountMutation,
} from '#accounts';
import { markAccountRead } from '#accounts/accountsSlice';
import type { SavedFilter } from '#components/filters/SavedFilterMenuButton';
import { TransactionList } from '#components/transactions/TransactionList';
import { validateAccountName } from '#components/util/accountValidation';
import { useAccountPreviewTransactions } from '#hooks/useAccountPreviewTransactions';
import { useAccounts } from '#hooks/useAccounts';
import { SchedulesProvider } from '#hooks/useCachedSchedules';
import { useCategories } from '#hooks/useCategories';
import { useDateFormat } from '#hooks/useDateFormat';
import { useFailedAccounts } from '#hooks/useFailedAccounts';
import { useLocalPref } from '#hooks/useLocalPref';
import { usePayees } from '#hooks/usePayees';
import { getSchedulesQuery } from '#hooks/useSchedules';
import { SelectedProviderWithItems } from '#hooks/useSelected';
import type { Actions } from '#hooks/useSelected';
} from '@desktop-client/accounts';
import { markAccountRead } from '@desktop-client/accounts/accountsSlice';
import type { SavedFilter } from '@desktop-client/components/filters/SavedFilterMenuButton';
import { TransactionList } from '@desktop-client/components/transactions/TransactionList';
import { validateAccountName } from '@desktop-client/components/util/accountValidation';
import { useAccountPreviewTransactions } from '@desktop-client/hooks/useAccountPreviewTransactions';
import { useAccounts } from '@desktop-client/hooks/useAccounts';
import { SchedulesProvider } from '@desktop-client/hooks/useCachedSchedules';
import { useCategories } from '@desktop-client/hooks/useCategories';
import { useDateFormat } from '@desktop-client/hooks/useDateFormat';
import { useFailedAccounts } from '@desktop-client/hooks/useFailedAccounts';
import { useLocalPref } from '@desktop-client/hooks/useLocalPref';
import { usePayees } from '@desktop-client/hooks/usePayees';
import { getSchedulesQuery } from '@desktop-client/hooks/useSchedules';
import { SelectedProviderWithItems } from '@desktop-client/hooks/useSelected';
import type { Actions } from '@desktop-client/hooks/useSelected';
import {
SplitsExpandedProvider,
useSplitsExpanded,
} from '#hooks/useSplitsExpanded';
import { useSyncedPref } from '#hooks/useSyncedPref';
import { useTransactionBatchActions } from '#hooks/useTransactionBatchActions';
import { useTransactionFilters } from '#hooks/useTransactionFilters';
import { calculateRunningBalancesBottomUp } from '#hooks/useTransactions';
} from '@desktop-client/hooks/useSplitsExpanded';
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
import { useTransactionBatchActions } from '@desktop-client/hooks/useTransactionBatchActions';
import { useTransactionFilters } from '@desktop-client/hooks/useTransactionFilters';
import { calculateRunningBalancesBottomUp } from '@desktop-client/hooks/useTransactions';
import {
openAccountCloseModal,
pushModal,
replaceModal,
} from '#modals/modalsSlice';
import { addNotification } from '#notifications/notificationsSlice';
import { useCreatePayeeMutation } from '#payees';
import * as queries from '#queries';
import { aqlQuery } from '#queries/aqlQuery';
import { pagedQuery } from '#queries/pagedQuery';
import type { PagedQuery } from '#queries/pagedQuery';
import { useDispatch, useSelector } from '#redux';
import type { AppDispatch } from '#redux/store';
import { updateNewTransactions } from '#transactions/transactionsSlice';
} from '@desktop-client/modals/modalsSlice';
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
import { useCreatePayeeMutation } from '@desktop-client/payees';
import * as queries from '@desktop-client/queries';
import { aqlQuery } from '@desktop-client/queries/aqlQuery';
import { pagedQuery } from '@desktop-client/queries/pagedQuery';
import type { PagedQuery } from '@desktop-client/queries/pagedQuery';
import { useDispatch, useSelector } from '@desktop-client/redux';
import type { AppDispatch } from '@desktop-client/redux/store';
import { updateNewTransactions } from '@desktop-client/transactions/transactionsSlice';
type ConditionEntity = Partial<RuleConditionEntity> | TransactionFilterEntity;
@@ -1661,6 +1661,11 @@ class AccountInternal extends PureComponent<
}
maybeSortByPreviousField(this, sortPrevField, sortPrevAscDesc);
// Always add sort_order as a final tiebreaker to maintain stable ordering
// when transactions have the same values in the sorted column(s)
this.currentQuery = this.currentQuery.orderBy({ sort_order: sortAscDesc });
this.updateQuery(this.currentQuery, isFiltered);
};
@@ -1861,6 +1866,12 @@ class AccountInternal extends PureComponent<
accountId === 'onbudget' ||
accountId === 'uncategorized'
}
allowReorder={
!!accountId &&
accountId !== 'offbudget' &&
accountId !== 'onbudget' &&
accountId !== 'uncategorized'
}
isAdding={this.state.isAdding}
isNew={this.isNew}
isMatched={this.isMatched}

View File

@@ -10,12 +10,12 @@ import { View } from '@actual-app/components/view';
import type { AccountEntity } from 'loot-core/types/models';
import { useUnlinkAccountMutation } from '#accounts';
import { Link } from '#components/common/Link';
import { authorizeBank } from '#gocardless';
import { useAccounts } from '#hooks/useAccounts';
import { useFailedAccounts } from '#hooks/useFailedAccounts';
import { useDispatch } from '#redux';
import { useUnlinkAccountMutation } from '@desktop-client/accounts';
import { Link } from '@desktop-client/components/common/Link';
import { authorizeBank } from '@desktop-client/gocardless';
import { useAccounts } from '@desktop-client/hooks/useAccounts';
import { useFailedAccounts } from '@desktop-client/hooks/useFailedAccounts';
import { useDispatch } from '@desktop-client/redux';
function useErrorMessage() {
const { t } = useTranslation();

View File

@@ -15,14 +15,17 @@ import { getScheduledAmount } from 'loot-core/shared/schedules';
import { isPreviewId } from 'loot-core/shared/transactions';
import type { AccountEntity } from 'loot-core/types/models';
import { FinancialText } from '#components/FinancialText';
import { PrivacyFilter } from '#components/PrivacyFilter';
import { CellValue, CellValueText } from '#components/spreadsheet/CellValue';
import { useCachedSchedules } from '#hooks/useCachedSchedules';
import { useFormat } from '#hooks/useFormat';
import { useSelectedItems } from '#hooks/useSelected';
import { useSheetValue } from '#hooks/useSheetValue';
import type { Binding } from '#spreadsheet';
import { FinancialText } from '@desktop-client/components/FinancialText';
import { PrivacyFilter } from '@desktop-client/components/PrivacyFilter';
import {
CellValue,
CellValueText,
} from '@desktop-client/components/spreadsheet/CellValue';
import { useCachedSchedules } from '@desktop-client/hooks/useCachedSchedules';
import { useFormat } from '@desktop-client/hooks/useFormat';
import { useSelectedItems } from '@desktop-client/hooks/useSelected';
import { useSheetValue } from '@desktop-client/hooks/useSheetValue';
import type { Binding } from '@desktop-client/spreadsheet';
type DetailedBalanceProps = {
name: string;

View File

@@ -13,12 +13,12 @@ import { Area, AreaChart, Tooltip as RechartsTooltip, YAxis } from 'recharts';
import * as monthUtils from 'loot-core/shared/months';
import { integerToCurrency } from 'loot-core/shared/util';
import { PrivacyFilter } from '#components/PrivacyFilter';
import { useRechartsAnimation } from '#components/reports/chart-theme';
import { LoadingIndicator } from '#components/reports/LoadingIndicator';
import { useLocale } from '#hooks/useLocale';
import * as query from '#queries';
import { liveQuery } from '#queries/liveQuery';
import { PrivacyFilter } from '@desktop-client/components/PrivacyFilter';
import { useRechartsAnimation } from '@desktop-client/components/reports/chart-theme';
import { LoadingIndicator } from '@desktop-client/components/reports/LoadingIndicator';
import { useLocale } from '@desktop-client/hooks/useLocale';
import * as query from '@desktop-client/queries';
import { liveQuery } from '@desktop-client/queries/liveQuery';
const LABEL_WIDTH = 70;

View File

@@ -41,19 +41,19 @@ import { Balances } from './Balance';
import { BalanceHistoryGraph } from './BalanceHistoryGraph';
import { ReconcileMenu, ReconcilingMessage } from './Reconcile';
import { AnimatedRefresh } from '#components/AnimatedRefresh';
import { Search } from '#components/common/Search';
import { FilterButton } from '#components/filters/FiltersMenu';
import { FiltersStack } from '#components/filters/FiltersStack';
import type { SavedFilter } from '#components/filters/SavedFilterMenuButton';
import { NotesButton } from '#components/NotesButton';
import { SelectedTransactionsButton } from '#components/transactions/SelectedTransactionsButton';
import { useDateFormat } from '#hooks/useDateFormat';
import { useLocale } from '#hooks/useLocale';
import { useLocalPref } from '#hooks/useLocalPref';
import { useSplitsExpanded } from '#hooks/useSplitsExpanded';
import { useSyncedPref } from '#hooks/useSyncedPref';
import { useSyncServerStatus } from '#hooks/useSyncServerStatus';
import { AnimatedRefresh } from '@desktop-client/components/AnimatedRefresh';
import { Search } from '@desktop-client/components/common/Search';
import { FilterButton } from '@desktop-client/components/filters/FiltersMenu';
import { FiltersStack } from '@desktop-client/components/filters/FiltersStack';
import type { SavedFilter } from '@desktop-client/components/filters/SavedFilterMenuButton';
import { NotesButton } from '@desktop-client/components/NotesButton';
import { SelectedTransactionsButton } from '@desktop-client/components/transactions/SelectedTransactionsButton';
import { useDateFormat } from '@desktop-client/hooks/useDateFormat';
import { useLocale } from '@desktop-client/hooks/useLocale';
import { useLocalPref } from '@desktop-client/hooks/useLocalPref';
import { useSplitsExpanded } from '@desktop-client/hooks/useSplitsExpanded';
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
import { useSyncServerStatus } from '@desktop-client/hooks/useSyncServerStatus';
type AccountHeaderProps = {
tableRef: TableRef;

View File

@@ -9,10 +9,10 @@ import type { AccountEntity } from 'loot-core/types/models';
import { ReconcileMenu, ReconcilingMessage } from './Reconcile';
import { useSheetValue } from '#hooks/useSheetValue';
import { TestProviders } from '#mocks';
import { useSheetValue } from '@desktop-client/hooks/useSheetValue';
import { TestProviders } from '@desktop-client/mocks';
vi.mock('#hooks/useSheetValue', () => ({
vi.mock('@desktop-client/hooks/useSheetValue', () => ({
useSheetValue: vi.fn(),
}));

View File

@@ -19,11 +19,11 @@ import { tsToRelativeTime } from 'loot-core/shared/util';
import type { AccountEntity } from 'loot-core/types/models';
import type { TransObjectLiteral } from 'loot-core/types/util';
import { useDateFormat } from '#hooks/useDateFormat';
import { useFormat } from '#hooks/useFormat';
import { useLocale } from '#hooks/useLocale';
import { useSheetValue } from '#hooks/useSheetValue';
import * as bindings from '#spreadsheet/bindings';
import { useDateFormat } from '@desktop-client/hooks/useDateFormat';
import { useFormat } from '@desktop-client/hooks/useFormat';
import { useLocale } from '@desktop-client/hooks/useLocale';
import { useSheetValue } from '@desktop-client/hooks/useSheetValue';
import * as bindings from '@desktop-client/spreadsheet/bindings';
type ReconcilingMessageProps = {
balanceQuery: { name: `balance-query-${string}`; query: Query };

View File

@@ -19,13 +19,13 @@ import type { UserAccessEntity, UserAvailable } from 'loot-core/types/models';
import { UserAccessHeader } from './UserAccessHeader';
import { UserAccessRow } from './UserAccessRow';
import { InfiniteScrollWrapper } from '#components/common/InfiniteScrollWrapper';
import { Link } from '#components/common/Link';
import { Search } from '#components/common/Search';
import { useMetadataPref } from '#hooks/useMetadataPref';
import { pushModal } from '#modals/modalsSlice';
import { addNotification } from '#notifications/notificationsSlice';
import { useDispatch } from '#redux';
import { InfiniteScrollWrapper } from '@desktop-client/components/common/InfiniteScrollWrapper';
import { Link } from '@desktop-client/components/common/Link';
import { Search } from '@desktop-client/components/common/Search';
import { useMetadataPref } from '@desktop-client/hooks/useMetadataPref';
import { pushModal } from '@desktop-client/modals/modalsSlice';
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
import { useDispatch } from '@desktop-client/redux';
type ManageUserAccessContentProps = {
isModal: boolean;

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