Compare commits

...

162 Commits

Author SHA1 Message Date
github-actions[bot]
1f828b6562 🔖 (25.7.0) (#5260)
* 🔖 (25.7.0)

* Empty commit to bump ci

* Remove used release notes

* Empty commit to bump CI

---------

Co-authored-by: jfdoming <9922514+jfdoming@users.noreply.github.com>
Co-authored-by: Julian Dominguez-Schatz <julian.dominguezschatz@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-01 14:35:37 -04:00
Mauritz Schild
ad9a84ddf1 Fixes #5238 last synced button text not visible on light theme. (#5241)
* Changed the buttonMenuText color for the light theme so that text is visible on light background as well.

* Updated the release notes.

* Change file name of release notes

* Remove button varient from use last synced total and restore light theme
values to previous.
2025-06-30 07:40:12 -07:00
Matiss Janis Aboltins
d9a171b249 fix: Tracking budget income budget fields missing in mobile view (#5251) 2025-06-29 18:51:04 +01:00
Matiss Janis Aboltins
e5c84d4ae0 Fix switching budget type requiring hard reload to take effect (#5253)
Fixes #5252
2025-06-29 17:43:32 +01:00
An Hoang
94a76a008d fix: reapply thousand separators before passing input to appendDecimals (#5220)
* fix: reapply thousand separators before passing input to appendDecimals

This ensures that the input going into `appendDecimals` is not malformed when the  `hideFraction` option is On, otherwise when hitting delete on the text `"1,234,567"`, it will result in the text `"1,234,56"` which the formatter will parse as `1234.56`. This doesn't happen when `hideFraction` is off since hitting delete on the text `"12,345.67"` results in `"12,345.6"`, which `appendDecimals` will happily handle in a separate case to provide `"1234.56"` as the input into `currencyToAmount`.

* Handle edge cases for reapplyThousandSeparators

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

* [autofix.ci] apply automated fixes

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>
2025-06-28 12:49:30 -07:00
Matiss Janis Aboltins
432c2b6165 Patch mobile issues - hold for next month, transfer (#5245) 2025-06-28 14:37:38 +01:00
0x4d4e
46eb2a7c38 Added a gocardless bank parser for Raiffeisen AT bank (#5244)
* Added a gocardless bank parser for Raiffeisen AT bank

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-06-28 09:13:45 -04:00
Jeevan Shikaram
3214d5dd53 Update links pointing to /budgeting/users/. (#5246)
* Update links pointing to /budgeting/users/.

* Add release notes.
2025-06-28 00:27:27 -04:00
youngcw
66d8f1a631 Speed up balance history graph (#5229)
* speedy

* bunny, fill in missing data

* small cleanup

* Update packages/desktop-client/src/components/sidebar/BalanceHistoryGraph.tsx

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

* handle some edge cases, fix the month check

* better date formatting

* one more edge case

* fix

---------

Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
2025-06-27 12:59:32 -07:00
youngcw
e3aa63d1fa don't import deleted split lines ynab4 (#5226)
* don't import deleted split lines

* Update upcoming-release-notes/5226.md

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

---------

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2025-06-23 14:28:24 -07:00
Michael Clark
9478707ebb Prevent forks from running nightly worfkflows (#5213)
* dont run nightly worfkflow on forks

* dont run nightly worfkflow on forks

* release notes
2025-06-21 19:04:24 +01:00
Matt Fiddaman
9952412e1d [WIP] 🌍 Mark more files for translation (#5209)
* translate more files

* prefer Trans component in JSX where possible

* note

* [autofix.ci] apply automated fixes

* aria-label

* more...

* [autofix.ci] apply automated fixes

* review

* [autofix.ci] apply automated fixes

* more review

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-06-21 02:11:49 -04:00
Matt Fiddaman
8231bbbf5a 🌍 mark titlebar strings for translation (#5206)
* translate title bar

* note

* Update VRT

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-21 01:45:20 -04:00
Julian Dominguez-Schatz
eadd88ce31 Move more .d.ts files to .ts (#5204)
* Rename .d.ts files to .ts

* Fix type and lint errors

* Add release notes

* CodeRabbit feedback
2025-06-21 00:16:55 -04:00
youngcw
6c61cf6a8d Revert "Mobile running balance (#4809)" (#5205)
This reverts commit 2c87c44168.
2025-06-20 20:37:24 -07:00
Matt Fiddaman
0fb9c252ca improve compatibility with OFX/QFX files (#5203)
* support ofx files with space in opening tag

* note
2025-06-20 22:21:32 -04:00
Guillaume Taquet Gasperini
d73ead135e Improve Boursobank Gocardless transaction parsing (#5202)
* Fix Boursobank transfer parsing

As per the comments on https://github.com/actualbudget/actual/pull/4958#issuecomment-2988814739,
the Boursobank transfer parsing was not working correctly.

Indeed, the array returned by GoCardless for transfers is randomly
ordered. So we cannot rely on the first line to know its type.

To work around this, the code gets a bit more complex by:
- Checking the first line for known types (card, loan, atm withdrawal)
  and handling them accordingly.
- If it's not one of these, we iterate through all the lines by checking
  if the array contains a line with the transfer type.

* Add credit note type for BoursoBank

An `Avoir` is a refund made to the credit card.
Adds the proper payee / notes parsing for it.

* Improve Boursobank card transaction parsing

Some Boursobank transactions have an unknown number attached to the
payee name. Remove it from the payee name to ensure consistency across
transactions.

For instance, `CARTE 19/03/25 Github 4 CB*0494` (notice the `4`).

* Improve Boursobank payee name backslashes handling

After more testing, I found that the backslashes present are for the
pending transactions, and indicating the localization of the payment.

To keep the payee name consistent, remove what follows the backslashes.

For instance `PICARD SA 1234\\PARIS\\ FR` is for a pending transaction
whereas `PICARD SA 1234` is what we receive for a completed one.

* Set notes with date for Boursobank card transactions

@mistyque requested to see the data of the card transaction in this
comment: https://github.com/actualbudget/actual/pull/4958#issuecomment-2981459622

* Add 5202 release note

* [autofix.ci] apply automated fixes

* refactor loops and match

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2025-06-20 19:48:22 -04:00
youngcw
2c87c44168 Mobile running balance (#4809)
* start

* small fix

* clean

* working for regular transactions

* working for schedules

* cleanup

* typing

* cleanup

* cleanup

* vrt

* bunny

* use pref

* use pref right, lint

* more lint

* vrt

* pass hasInitialBalances to isLoading

* remove comment

* Add option to calculate running balances in useTransactions hook

* Fix typecheck error

* Fix lint error

* use the updated hook

* typecheck

* simplify

* don't show balances when searching

* Add runningBalances to usePreviewTransactions and an option to set the starting balance to start running balance calculation from

* Add filter to usePreviewTransactions and set startingBalance to account and category preview transaction hooks

* use runningbalance from preview transactions hook

* lint

* lint;typecheck

* remove initial from preview balances

* remove unneeded type

* Apply suggestions from code review

Co-authored-by: Joel Jeremy Marquez <joeljeremy.marquez@gmail.com>

* typecheck; align right; change color

* types

* add a menu item

* cleanup

* fix for loot-core migrated files

* lint;type

* fix import

* only schedules need fixed

* lint

* it works

* cleanup

* make lint happy

* [autofix.ci] apply automated fixes

* simplify a bit

* fix import

* feedback

---------

Co-authored-by: Joel Jeremy Marquez <joeljeremy.marquez@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-06-19 17:54:00 -07:00
lelemm
15beba2ca3 🐛 Fix to AI generated release note (#5200)
* AI Generated release notes

* lint

* removedverbose console.log

* wrong permission to PR's

* missing write to issues

* Add release notes for PR #5200

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-19 20:46:22 -03:00
Matt Fiddaman
667cc24fac ⬆️ bump react-i18next from 14.1.3 -> 15.5.3 (#5196)
* bump react-i18next from 14.1.3 -> 15.5.3

* note
2025-06-19 19:17:20 -04:00
Matt Fiddaman
4cc542a658 ⬆️ bump csv-parse and csv-stringify to latest (#5198)
* bump csv-parse and csv-stringify

* note

* fix imports

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-06-19 19:17:10 -04:00
lelemm
093d799ba0 Auto generate Release Notes (#5182)
* AI Generated release notes

* lint

* removedverbose console.log
2025-06-19 17:22:44 -03:00
Matt Fiddaman
68d10f6b29 ⬆️ bump vitest from 3.1.4 -> 3.2.4 (#5195)
* bump vitest from 3.1.4 -> 3.2.4

* note
2025-06-19 15:30:25 -04:00
Matt Fiddaman
252f04e02c ♻️ remove body-parser dependency (#5197)
* bodyParser

* note
2025-06-19 15:30:12 -04:00
wachkyri
13cb85835b fix: wrong payeeName for KBC Brussels (#5193) 2025-06-18 20:49:24 -04:00
wachkyri
39cf04c74d add Belfius and KBC to banks with limited history (#5183)
* add Belfius and KBC to banks with limited history

* Update upcoming-release-notes/5183.md

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

---------

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2025-06-18 16:40:07 -04:00
Jeremy Tan
562b5e2afd [Feature] Store and use last synced account balances (#4799)
* Update `processBankSyncDownload` to store synced balance in `balance_current`

* Display last synced balance in `MoreBalances`

* Add "Use last synced balance" to reconcile

* Remove logs

* Release notes

* Fix lint

* Add missing useEffect dep

* Restore console.log and fix type for id

* Last synced total

* Fix lint

* lint

* Made requested changes from maintainers.

* Added my name to authors note and updated description of feature.

---------

Co-authored-by: Spencer Sawyer <spencer@spencersawyer.com>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
Co-authored-by: youngcw <calebyoung94@gmail.com>
2025-06-18 14:51:43 -04:00
Tim Nielens
4b4e32d0e2 add BNP_BE_GEBABEBB in gocardless bank-factory.js (#5187)
* add BNP_BE_GEBABEBB in gocardless bank-factory.js

* Create release note 5187.md

* Update 5187.md
2025-06-18 14:50:11 -04:00
Matt Farrell
d821f1cebc fix parsing schedule templates with brackets in the name (#5189) 2025-06-18 11:38:17 -07:00
Joel Jeremy Marquez
53e3694a38 Move spreadsheet bindings to spreadsheets feature folder and move hooks in src/components/spreadsheets to src/hooks (#5007)
* Move loot-core/client/modals code over to desktop-client package

* Fix lint error

* Move loot-core/client/data-hooks over to desktop-client package

* Fix typecheck and lint errors

* Fix lint error

* Fix typecheck error

* Fix test

* Move spreadsheet bindings to spreadsheets feature folder and move hooks from src/components/spreadsheets to src/hooks

* Move NamespaceContext to useSheetName

* Rename NamespaceContext to SheetNameContext and use SheetNameProvider

* Fix lint errors

* Fix import and provider

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-06-17 14:34:00 -07:00
Michael Clark
8647452ccc 🔧 Add reset passsword to cli tool (#5185)
* add reset passsword to cli tool

* release notes

* added the script properly
2025-06-17 22:05:45 +01:00
Julian Dominguez-Schatz
5a40b017f0 Move more .d.ts files to .ts (#5060)
* Move more .d.ts files to .ts

* Add release notes

* Some errors with templates snuck in

* Fix API build

* CodeRabbit feedback

* Move budget templates to new directory

* Fix type errors in library module
2025-06-16 21:05:36 -04:00
Matiss Janis Aboltins
8ccc1af77e Clean up configuration files (#5181) 2025-06-16 22:22:38 +01:00
Matiss Janis Aboltins
07904c209e Enhance PullToRefresh component to ensure full height usage on mobile devices (#5179) 2025-06-16 21:30:52 +01:00
Matiss Janis Aboltins
234f008dcf Update feature request management message to clarify voting system and community contributions (#5180) 2025-06-16 20:07:49 +01:00
Matiss Janis Aboltins
d130b427b3 Upgrade react router to v7 (#5172) 2025-06-16 18:23:26 +01:00
Matiss Janis Aboltins
39cd71aa48 Fix: display notifications below modal windows (#5165) 2025-06-15 16:44:03 +01:00
Matiss Janis Aboltins
100711ccfb Fix bottom navbar overlaying content in mobile (#5166) 2025-06-14 20:55:14 +01:00
Matiss Janis Aboltins
7c9f3f241d Added unit tests for CategorySelector and Change components (#5118) 2025-06-14 15:54:13 +01:00
Matiss Janis Aboltins
6509e80061 Update version notification message for Pikapods instances (#5146) 2025-06-14 14:54:46 +01:00
Matiss Janis Aboltins
fbd6989a18 Add GitHub workflow and script for counting monthly contributor points (#5147) 2025-06-14 13:18:40 +01:00
Rob Jackson
180431f9ed Fix account properties being overriden with empty values from metadata (#5115)
My Monzo bank appears to return 'name' through the GoCardless getDetails and getMetadata calls, but the value in the getMetadata response is an empty string! Merging metadata values over detail values therefore risks losing values, that are useful to have & to present to the user.

To solve this, we can make getDetailedAccount smarter about merging the two objects. We could swap the object splattening around so details has priority over metadata, but I'm not at all confident there won't be other banks doing strange things with additional properties that might suffer similar bugs in the reverse scenario.

Instead, we loop through all keys in both objects and construct a new merged object, continuing to prioritise metadata over details but changing it to a truthy-based comparison (with the `||` operator).

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2025-06-14 08:03:59 -04:00
Matiss Janis Aboltins
d27d62b5fc Add GitHub Actions workflow for automatic code formatting (#5157) 2025-06-14 11:34:08 +01:00
Matiss Janis Aboltins
b5f29ccb4a show account historical balance change in side-nav hover tooltip (#5085) 2025-06-13 20:07:16 +01:00
Matt Fiddaman
0a5acebeaf use fallback linking parameters if initial GoCardless linking process fails (#5150) 2025-06-13 12:19:59 -04:00
Leandro Menezes
fa544d9c08 removed wrong md from master 2025-06-13 12:41:01 -03:00
github-actions[bot]
8976a59c3a Add release notes for PR #5155 2025-06-13 12:34:15 -03:00
Elijah Olmos
9713d09603 feat: add command bar (#5076)
* deps(desktop-client): add cmdk

* feat(desktop-client): create and integrate CommandBar

* hover on selection

* Ctrl+K only opens, ESC closes

* add custom reports

* add navigation items to cmdk

* fix: mouse hovering can interfere with keyboard navigation

* reset search state when CommandBar closes

* revert import order changes

* deps(desktop-client): readd cmdk

* fix vite error

* add item icons

* move navigation items into their own section

* hide scrollbar and release notes

* style: run yarn lint:fix

* fix: infinite loop opening commandbar with active modal

* fix: infinite error loop bc focus conflicts

* fix: kebab case console warning

* chore: update yarn.lock

* refactor: use autoFocus prop

* feat: add i18next

* style: relocate eslint-disable comment
2025-06-13 09:41:16 +01:00
youngcw
814f4fe955 release context menus (#5142) 2025-06-11 14:59:24 -07:00
Michael Clark
dbe6b27d9f :octocat: Prevent nightly npm packages publishing on push (#5144)
* prevent nightly npm packages publishing on push

* release ntoes

* Update upcoming-release-notes/5144.md

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

---------

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2025-06-11 22:54:18 +01:00
Michael Clark
31a7902a08 :octocat: Add workflow for publishing nightly npm packages (#5047)
* add workflow for publishing edge npm packages

* release notes

* alright mr rabbit

* never trust the wabbit

* changing tag to nightly as per maintainer feedback

* fix hotfix script version

* rename workflow

* wabbit

* exit process

* fix reference to package json

* variable scoping

* change nightly version number to yyyymmdd (#17)

---------

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2025-06-11 21:37:54 +01:00
Matt Fiddaman
eb35b41c6d fix preview transactions not showing on multi-account views (#5135) 2025-06-11 10:46:40 -04:00
Danish Joseph
a025d2b621 fix: bottom UI overlap with iOS Home Indicator (#5121) 2025-06-11 09:08:01 +01:00
Olicorne
d72140b8b6 add LCL to the list of bank with only 90 days of sync (#5087)
* add LCL to the list of bank with only 90 days of sync

Signed-off-by: thiswillbeyourgithub <26625900+thiswillbeyourgithub@users.noreply.github.com>

* add missing release note

Signed-off-by: thiswillbeyourgithub <26625900+thiswillbeyourgithub@users.noreply.github.com>

* Update upcoming-release-notes/5087.md

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

---------

Signed-off-by: thiswillbeyourgithub <26625900+thiswillbeyourgithub@users.noreply.github.com>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2025-06-10 14:04:46 -04:00
Olivier Kamers
254059d4c8 fix: ensure correct PR number is used for release notes file (#5134) 2025-06-10 11:25:09 -04:00
Olivier Kamers
92bc1e8ec9 fix: add missing 'enabled' in sentence. (#5133) 2025-06-10 12:59:40 +01:00
Matiss Janis Aboltins
b211b67f5e Fix: Only sync off-budget accounts when syncing from off-budget page (#5124) 2025-06-08 19:30:06 +01:00
Olivier Kamers
a2abb2b2ae Fix net worth graph gradient conflict (#5129)
* Fix net worth graph gradient conflict

Ensures that the net worth graph's gradient ID is unique to prevent conflicts when multiple graphs are rendered on the same page.
Fixes #3965

* Apply review suggestion
2025-06-08 14:03:59 -04:00
Olivier Kamers
3fab1be737 chore: replace snapshot-diff and jest with jest-diff (#5127)
Unlike the name suggests, jest-diff does not have a dependency on jest.
2025-06-08 14:00:33 -04:00
youngcw
bf9fbc5137 🐛 fix goal type templates (#5120)
* fix

* note

* fix test
2025-06-08 07:38:06 -07:00
youngcw
52eced1f21 🐛 group template apply (#5112)
* fix

* Update upcoming-release-notes/5112.md

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

---------

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2025-06-08 07:36:54 -07:00
Michael Clark
359af05cc4 🐳 Fix nightly Docker edge image (#5123)
* fix nightly docker edge test

* release notes
2025-06-07 15:54:52 +01:00
Matt Fiddaman
9f1a8f6d5c ⬆️ upgrade uuid from 9.0.1 -> 11.1.0 (#5048) 2025-06-07 01:02:35 -04:00
Matiss Janis Aboltins
b22d712b4f fix: add missing dependency array to useEffect in SaveReportChoose.tsx (#5044) (#5111) 2025-06-06 18:51:11 +01:00
Michael Clark
098cacd904 :electron: Guide user to the docs on the electron configure server page (#5107)
* add link to docs on configure server page

* release note

* release note

* bump

* Update VRT

* huh

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-06 10:48:01 +01:00
Matiss Janis Aboltins
127f114914 🔧 improving the needs-info stale issue management (#5110) 2025-06-05 23:08:51 +01:00
Matiss Janis Aboltins
b56e26ee56 🔧 auto close issues with inaactivity and 'needs info' label (#5109) 2025-06-05 22:45:20 +01:00
Matiss Janis Aboltins
cd6b141117 🔧 (eslint) patch no-restriced-imports rule (#5081) 2025-06-05 19:59:51 +01:00
Roger Goldfinger
cd15aded05 Add test that docker images are working (#4952)
* use build directory in dockerfiles

* add release notes

* add test

* Add test for cli, try to fix image test

* - fail

* try again

* try again to fail

* done testing

* Code review

* publish is unecessary
2025-06-05 10:31:56 -04:00
Baruch Odem (Rothkoff)
cac318255d [Proposal] Enhance transaction import functionality with new Transaction type and type annotations (#4720)
* Enhance transaction import functionality with new Transaction type and type annotations

* release notes

* fixes for PR

* move transaction type as requested by @MatissJanis

* type importTransaction return value

---------

Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
2025-06-05 10:31:29 -04:00
dependabot[bot]
6872dd235b bump tar-fs from 2.1.2 to 2.1.3 (#5084)
* Bump tar-fs from 2.1.2 to 2.1.3

Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 2.1.2 to 2.1.3.
- [Commits](https://github.com/mafintosh/tar-fs/commits)

---
updated-dependencies:
- dependency-name: tar-fs
  dependency-version: 2.1.3
  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>
2025-06-05 10:30:09 -04:00
Will Garrison
649932b42f [bugfix] Include split transactions in select-all (#5086)
* Include parent transactions in select-all when all of their children are selected

* Added release notes

* Fix release notes file name

* Make function return type explicit
2025-06-05 10:29:54 -04:00
Albert Pedersen
d372b71f36 Trim EndToEndID from the Danish Danske Bank branch (#5101)
* Trim EndToEndID from the Danish Danske Bank

* Add release note for PR #5101

---------

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2025-06-05 10:29:45 -04:00
youngcw
47cb5e1ecf Add ability to auto hold funds for future months (#4778)
* add spreadsheet logic

* separate to own cell

* add selection cell

* fix selection

* added button

* arrow

* switches between the two hold options gracefully

* reset manual hold on apply auto hold

* working?

* fix

* lint

* better to budget menu logic

* type

* typing

* missing function pass

* some cleanup

* typecheck

* translation

* closer to good arrow

* lint

* prevent rollover arrow from clipping

* move to a menu; position is broken

* lint

* fix location

* lint

* standardize verbage

* right case

* fix import

* cleanup

* start of a mobile menu

* slightly better

* some cleanup

* lint

* Update packages/desktop-client/src/components/modals/EnvelopeIncomeBalanceMenuModal.tsx

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

* fix styling

* types

* Update packages/desktop-client/src/components/budget/envelope/BalanceMenu.tsx

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

* Apply suggestions from code review

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

---------

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2025-06-05 06:04:14 -07:00
github-actions[bot]
02c59d9a1c 🔖 (25.6.1) (#5099)
* 🔖 (25.6.1)

* Remove used release notes

---------

Co-authored-by: matt-fidd <81489167+matt-fidd@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-04 18:13:40 -04:00
Matt Fiddaman
f9f6917fcd update dockerfiles to use node 20 (#5091)
* update dockerfiles to use node 20

* note
2025-06-04 13:31:45 -04:00
Michael Clark
7441b5fa92 🐛 Fix server version on docker (#5093)
* fix server version on docker

* bit more safety

* fix lint

* release notes

* more error handling
2025-06-04 13:31:37 -04:00
Matt Fiddaman
bfb2d61286 fix crash when datepicker deselected with no date set (#5095)
* fix crash when datepicker not set

* note
2025-06-04 13:31:29 -04:00
github-actions[bot]
09b12b8218 🔖 (25.6.0) (#5083)
* 🔖 (25.6.0)

* Empty commit to bump CI

* Remove used release notes

* Empty commit to bump CI

---------

Co-authored-by: jfdoming <9922514+jfdoming@users.noreply.github.com>
Co-authored-by: Julian Dominguez-Schatz <julian.dominguezschatz@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-04 00:35:34 -04:00
Matiss Janis Aboltins
1c46655e30 🐛 (mobile) fix - show loading indicator if schedules not yet loaded (#5080) 2025-06-02 22:24:06 +01:00
dependabot[bot]
82329b7de2 Bump formidable from 2.1.2 to 2.1.5 (#5075)
* Bump formidable from 2.1.2 to 2.1.5

Bumps [formidable](https://github.com/node-formidable/formidable) from 2.1.2 to 2.1.5.
- [Release notes](https://github.com/node-formidable/formidable/releases)
- [Changelog](https://github.com/node-formidable/formidable/blob/master/CHANGELOG.md)
- [Commits](https://github.com/node-formidable/formidable/commits)

---
updated-dependencies:
- dependency-name: formidable
  dependency-version: 2.1.5
  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>
2025-06-01 19:54:47 -04:00
Shimikito
a34c94d9fe Add ING PL (ING_PL_INGBPLPW) to banks with limited history (#5073)
* Update bank-factory.js 90 days bank history

Add ING PL (ING_PL_INGBPLPW) to banks with limited history

* Adding a bank with limited history - alphabetical fix

adding alphabetical sorting

* note

---------

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2025-06-01 19:49:08 -04:00
Normen
ae6eed65f7 Add 'GLS Gemeinschaftsbank' to banks with limited history (#5052)
* chore: add GLS_GEMEINSCHAFTSBANK_GENODEM1GLS to BANKS_WITH_LIMITED_HISTORY

* chore: add release note
2025-06-01 19:48:14 -04:00
Matiss Janis Aboltins
53398624f3 🔧 added needs-triage label to all new bug issues (#5070) 2025-05-30 21:57:16 +01:00
Valentin Lorenzen
47ee6eeb51 enable GoCardless account selection if supported by the target institution (#5031)
* Update gocardless-service.js

* Allow account selection during requisition based on institution support

* lint

* note

---------

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2025-05-27 16:32:25 -04:00
Michael Süssemilch
6e3a337945 fix: save category expansion state when value is 0 (#5069) 2025-05-27 12:33:37 -07:00
Shreyas
d2d8ce2353 Fix: restrict electron-embedded sync server to bind only to configured hostname for security (#5050)
* comment sync server

* Fix: ensure electron-embedded sync server binds to localhost to limit access

* lint and release notes

* rename

* remove hostname config and revert docker yaml
2025-05-27 16:57:06 +01:00
Johnn27
7d38f6934d 🐛 Fix goals tooltip obstructing cover spending context menu (#5051)
* 🐛 Fix goals tooltip obstructing cover spending context menu

* Update release note

* Fix lint format

---------

Co-authored-by: Johnn27 <>
2025-05-24 09:23:34 -07:00
Matt Fiddaman
d4b09ecb27 fix "delete x users" translation string (#5045) 2025-05-22 20:18:47 -04:00
Matt Fiddaman
a508a8705c ⬆️ upgrade express from 4.21.2 -> 5.1.0 (#5042)
* upgrade express from 4.21.2 -> 5.1.0

* fix tests

* note

* fix wildcard route

* fix access of req.body when undefined
2025-05-22 16:21:57 -04:00
Matt Fiddaman
f3b2507516 ⬆️ upgrade date-fns from v2.30.0 -> v4.1.0 (#5041) 2025-05-22 14:37:55 -04:00
Joel Jeremy Marquez
583ddab2ac Use @desktop-client alias in all of desktop-client package (#4960)
* Use @desktop-client alias in all of desktop-client package

* Run yarn lint:fix
2025-05-22 09:01:05 -07:00
Joel Jeremy Marquez
1876ba9fe7 Update @actual-app/components/input to be based on react-aria-components Input component (#4955)
* Update @actual-app/components/input to use react-aria-components Input component

* Cleanup

* Dummy commit

* Remove marginRight being magically added by Stack component

* Update 4955.md

* Update selection background color to match current color

* Update selection background color

* Dummy commit

* Fix ConfigServer styles

* Fix lint error

* re-add accidentally removed placeholder fix

* vrt

---------

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2025-05-21 22:48:05 -07:00
Matt Fiddaman
4c15647f7f ⬆️ upgrade low risk dependencies to their newest minor version (#5025)
* vitest

* types

* eslint

* better-sqlite3

* pluggy-sdk

* globals

* google-protobuf

* fast-check

* chokidar

* i18next

* openid-client

* playwright

* lru-cache

* memfs

* redacted font

* sass

* @vitejs/plugin-basic-ssl

* re-resizable

* rollup-plugin-visualizer

* note

* update vrt screenshots

* vrt
2025-05-21 16:56:07 -04:00
Joel Jeremy Marquez
174e13b3fe Fix category schedule indicators not showing up in budget page (#5036)
* Fix category schedule indicators not showing up in budget page

* Update release notes
2025-05-21 09:30:36 -07:00
Stein Petter Tokvam
2e9a752baa if BankSyncError is caused by rate limit. display that in toast (#5038)
* if BankSyncError is caused by rate limit. display that in toast

* ran prettier
2025-05-21 12:08:22 -04:00
Joel Jeremy Marquez
83f6706020 Add types to budget template files and some cleanup (#4986)
* Add types to budget template files and some cleanup

* Rename test context

* Rename test name

* Rename goal-template to match other files

* Update imports

* Fix lint and typecheck errors

* Release notes

* Update release notes

* Rename templateContext

* Revert rename

* Redo rename

* Fix renamed file

* Fix null error

* Missed to ignore hidden categories

* Revert to forEach

* Fix priorities

* Update 4986.md

* Revert deleted file

* Coderabbit feedback
2025-05-21 08:54:38 -07:00
Matt Fiddaman
4dba95842a ⬆️ upgrade react dependencies to their newest minor version (#5027) 2025-05-20 13:46:20 -04:00
Matt Fiddaman
af499c6503 ⬆️ upgrade babel/webpack dependencies to their newest minor version (#5028) 2025-05-20 13:45:45 -04:00
Julian Dominguez-Schatz
913a2c9a68 Automatically upload to the MS store on release (#5034)
* Update electron-master GitHub action to also submit to the MS store

* Add release notes
2025-05-19 23:23:46 -04:00
Matt Fiddaman
a4b0c4a0be ⬆️ upgrade electron dependencies to their newest minor version (#5029) 2025-05-19 17:34:12 -04:00
Jared Tweed
32a04cbbcb Changed 'close file' to say 'switch file' so it is more clear that the … (#4872)
* Changed 'close file' to say 'exit file' so it is more clear that the file is not being deleted (#4852)

Resolved this issue here:

https://github.com/actualbudget/actual/issues/4852#issue-3006813465

* chore(release): add release note for PR #4872

* Update BudgetName.tsx "Close file" -> "Switch file"

* Update 4872.md

* Changed 'Close budget' to 'Switch file' on mobile

* Update VRT

---------

Co-authored-by: youngcw <calebyoung94@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-05-19 10:29:47 -04:00
Matt Fiddaman
e950bbb1df resolve loot-core peer dependency warnings (#5024)
* fix loot-core peer dependency issues

* note

* update @swc/core
2025-05-18 19:37:22 -04:00
Matt Fiddaman
188cd21cf1 ⬆️ bump adm zip to 0.5.16 (#5019)
* bump adm-zip to 0.5.16

* note

* move to native yarn patch
2025-05-18 18:29:56 -04:00
Matt Fiddaman
f7a45d2081 ⬆️ bump dependency patch versions (#5015)
* bump dependency patch versions

* note

* remove unused ts-expect-error directives

* downgrade adm-zip again
2025-05-18 16:50:32 -04:00
Matt Fiddaman
99768a9aae ⬆️ upgrade yarn from v4.7.0 to v4.9.1 (#5014)
* upgrade yarn from v4.7.0 to v4.9.1

* note
2025-05-18 14:22:37 -04:00
Matt Fiddaman
7dbb3404f1 ⬆️ bump jq (#5018)
* bump jq

* note
2025-05-18 14:22:33 -04:00
Matt Fiddaman
ca09bbb858 remove dependency on ip (#5017)
* remove transitive dependency on ip

* note
2025-05-18 14:22:18 -04:00
Roger Goldfinger
0dca8498fe Set yarn engine version (#5011)
* Set yarn engine version

* release notes

* more precise version
2025-05-18 00:34:01 -04:00
Joel Jeremy Marquez
b5ece8e221 Make InitialFocus component generic (#5008)
* Make InitialFocus component generic

* Fix lint error
2025-05-16 15:02:06 -07:00
Samuel Barnes
351e252129 Allow-return-to-trigger-default-actions (#4912)
* button being focused

* working

* tests

* text selection test

* allow for ref injection

* updated some of the usages

* release notes

* rename of prop

* docs

* fixed release note

* remove unnecessary test setup

* updated from comments

* removed selecttext references

* fix type error

* updated children type

* removed comment
2025-05-16 12:28:31 -07:00
Joel Jeremy Marquez
720d0fda6d [Final PR] Move remaining loot-core/client files to desktop-client feature folders (#4830)
* Move loot-core/client/modals code over to desktop-client package

* Fix lint error

* Move loot-core/client/data-hooks over to desktop-client package

* Fix typecheck and lint errors

* Fix lint error

* [Final PR] Move remaining loot-core/client files to desktop-client feature folders

* Fix tests

* Clear server on each run

* Fix lint errors

* Fix imports

* Fix typecheck error

* Fix lint errors

* Fix typecheck error

* Fix test

* Fix tests - unsubscribe test pending fix

* Fix lint errors

* Fix typecheck error

* Fix lint error

* Fix tests

* Fix lint error
2025-05-16 10:42:06 -07:00
Joel Jeremy Marquez
fdf213865d Move loot-core/client/data-hooks over to desktop-client package (#4828)
* Move loot-core/client/modals code over to desktop-client package

* Fix lint error

* Move loot-core/client/data-hooks over to desktop-client package

* Fix lint errors

* Fix typecheck error

* Fix typecheck and lint errors

* Fix typecheck error

* Fix lint error
2025-05-16 08:53:59 -07:00
Alec Bakholdin
b2ffa1d846 [WIP] Qif empty lines (#5002)
* fixed qif failing with empty lines

* release notes

---------

Co-authored-by: Alec Bakholdin <alecbakholdin@Alecs-Mac.local>
2025-05-16 11:45:46 -04:00
Alec Bakholdin
21fb090ddf ⚙️ Moving TransactionsTable and TransactionList over to Typescript (#4930)
* moved over some util functions

* moved over TransactionHeader

* Added the bulk of Transaction typing data

* moved PayeeCell into its own file

* moved StatusCell over

* added NotesCell file

* Moved over NewTransaction

* converted TransactionsTable

* Converted TransactionList to use new typed TransactionsTable

* Converted consumers of TransactionList to use TransactionList

* removed old TransactionsTable

* release notes

* fixed bug vrt found

* fixed one todo

* attempting to debug whatever is going on

* moved TransactionsTable tests up a directory and that fixes it for some reason

* removed type cast

* re-instated onScheduleAction types

* re-added old TransactionsTable.jsx file

* consolidated changes into just TransactionsTable

* deleted split files and finished moving over TransactionList

* typecheck

* typecheck

* yarn lint

* changed type casting to make it more specific

* changed warn to error

* added some todos to clean up type assumptions later

* yarn lint

---------

Co-authored-by: Alec Bakholdin <alecbakholdin@mac.myfiosgateway.com>
Co-authored-by: Alec Bakholdin <alecbakholdin@Alecs-Mac.local>
2025-05-16 06:15:07 -04:00
Joel Jeremy Marquez
c389c6c637 Move loot-core/client/store and loot-core/client/redux code over to desktop-client package (#4827)
* Move loot-core/client/modals code over to desktop-client package

* Fix lint error

* Move loot-core/client/store and loot-core/client/redux to desktop-client package

* Separate redux store into it's own file

* Import types from redux/store instead of redux
2025-05-15 13:09:10 -07:00
Joel Jeremy Marquez
7bb6aff756 Move loot-core/client/users code over to desktop-client package (#4823)
* Move loot-core/client/modals code over to desktop-client package

* Fix lint error

* Move loot-core/client/users code over to desktop-client package

* Fix lint errors
2025-05-15 12:45:20 -07:00
Joel Jeremy Marquez
3f4ddfdfe0 [Mobile] Fix category transactions screen not showing child transactions (#4998) 2025-05-15 11:38:50 -07:00
Joel Jeremy Marquez
7dd98c4f86 Add runningBalances to usePreviewTransactions and an option to set the starting balance to start running balance calculation from (#4994)
* Add runningBalances to usePreviewTransactions and an option to set the starting balance to start running balance calculation from

* Add filter to usePreviewTransactions and set startingBalance to account and category preview transaction hooks
2025-05-15 11:31:38 -07:00
Michael Clark
f3fc4b8d22 Remove duplicate import (#4997) 2025-05-15 19:22:31 +01:00
Joel Jeremy Marquez
644e8df3e1 Move loot-core/client/queries code over to desktop-client package (#4822)
* Move loot-core/client/modals code over to desktop-client package

* Fix lint error

* Move loot-core/client/queries code over to desktop-client package
2025-05-15 11:10:19 -07:00
Joel Jeremy Marquez
9dfbefa1d2 Fix prefsSlice import (#4995) 2025-05-15 11:09:54 -07:00
Michael Clark
516977f666 Fix pipeline build (#4996) 2025-05-15 19:06:59 +01:00
Michael Clark
70362f6801 :electron: Server config ui (#4870)
* server config ui 

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-05-15 18:52:46 +01:00
Eetu Mäenpää
d225e5d5e1 Update react-spring to v10.0.0 (#4992)
* feat: update react-spring to v10.0.0

* docs: add release notes
2025-05-15 11:34:40 -04:00
Joel Jeremy Marquez
ae00fa2841 Move loot-core/client/prefs code over to desktop-client package (#4821)
* Move loot-core/client/modals code over to desktop-client package

* Fix lint error

* Move loot-core/client/prefs code over to desktop-client package

* Dummy commit
2025-05-14 22:01:02 -07:00
musicaldesigner
b475951075 Update bank-factory.js, add DNB to list of banks with limited history (#4893) 2025-05-14 19:09:06 -07:00
Joel Jeremy Marquez
d9dd96d0de Add option to calculate running balances in useTransactions hook (#4987)
* Add option to calculate running balances in useTransactions hook

* Fix typecheck error

* Fix lint error

* Cleanup

* retrigger checks
2025-05-14 15:27:38 -07:00
Joel Jeremy Marquez
45ff94590b Move loot-core/client/notifications code over to desktop-client package (#4820)
* Move loot-core/client/modals code over to desktop-client package

* Fix lint error

* Move loot-core/client/notifications code over to desktop-client package
2025-05-14 13:25:09 -07:00
Joel Jeremy Marquez
466875a8dd Move loot-core/client/modals code over to desktop-client package (#4819)
* Move loot-core/client/modals code over to desktop-client package

* Fix lint error

* Fix imports

* Fix lint error

* Fix lint error
2025-05-14 12:09:25 -07:00
Alec Bakholdin
0431039eb6 Inconsistent date update mobile new transaction (#4910)
* converted onUpdate to onChange for better mobile ux

* release notes

---------

Co-authored-by: Alec Bakholdin <alecbakholdin@Alecs-Mac.local>
2025-05-13 10:33:05 -04:00
Alec Bakholdin
2215c131a5 Fix new transactions account notes not clearing (#4914)
* added history entry if history entry is null while entering Add Transaction widget

* release notes

* Update TransactionEdit.jsx

* Update TransactionEdit.jsx

* Update TransactionEdit.jsx

---------

Co-authored-by: Alec Bakholdin <alecbakholdin@Alecs-Mac.local>
2025-05-13 10:32:54 -04:00
Matt Fiddaman
4de5fb34b9 increase OpenID timeout (#4980)
* increase openid request timeout

* note
2025-05-13 00:20:59 -04:00
OhNoBigO
4f6b97ae4a [Mobile] 🐛 Fix viewing pie/donut charts on mobile (#4935)
* Fix viewing pie/donut charts on mobile

* Add release note

* Fix onMouseEnter firing on ios devices

* Change isTouchDevice to canDeviceHover

---------

Co-authored-by: Oli-vers <180103373+Oli-vers@users.noreply.github.com>
2025-05-12 13:57:52 -04:00
Matt Fiddaman
6d921a48b6 change minimum version of Node.js to version 20 (#4978)
* update workflows

* update supported engines in package.json

* .nvmrc

* base yarn cache on node version being used rather than .nvmrc

* note
2025-05-12 13:49:09 -04:00
Michael Clark
2d0716233f Replacing Actuator with our own implementation (#4897)
* actuator replacement experiment

* replacing actuator

* release notes

* description added

* version typo

* Update upcoming-release-notes/4897.md

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-12 18:40:15 +01:00
Joel Jeremy Marquez
6f3bfd2ad4 Move loot-core/client/budgets code over to desktop-client package (#4818)
* Move loot-core/client/budgets code over to desktop-client package

* Fix lint error

* Fix lint error
2025-05-12 09:46:33 -07:00
Joel Jeremy Marquez
10a143a3ae Add upcoming/missed/due schedules indicator on budget table (based on category schedule templates) (#4815)
* Add upcoming/missed/due schedules indicator on budget table (based on category schedule templates)

* Rename hook

* Fix typecheck error

* Update packages/desktop-client/src/components/budget/SidebarCategory.tsx

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

* Use category goal_def instead of reparsing notes

* Fix lint and typecheck error

* Fix lint error

* Update type

* Handle doal_def parse errors

* Only show schedule status on month of the schedule's next date

* Mobile - only show category schedule status on month's schedule

* Update tooltip wording

* Disable if goal templates feature is disabled

* Move schedule indicator to spent column

* Update colors

* Mobile - move indicator to spent

* Cleanup

* Cleanup

* Support multiple schedule templates

* Clearer error message

* Memo useCategoryScheduleGoalTemplates

* Fix lint and typecheck errors

* Category preview transactions for mobile

* Fix lint errors

* Fix calls to .some()

* Show most urgent schedules first

* Fix typecheck error

* Fix lint error

* Align all schedule indicators

* List all schedules in schedule indicator tooltip

* Fix typecheck error

* Fix lint errors

* Update SpentCell to show schedule indicator

* Fix lint error

* Update packages/desktop-client/src/hooks/useCategoryScheduleGoalTemplateIndicator.ts

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

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2025-05-12 09:46:20 -07:00
Joel Jeremy Marquez
82c469648e Split transactions repair tool to remove categories from parent transactions + prevent adding/updating parent transactions with categories (#4889)
* Add a functionality to split transactions repair tool to remove categories from parent transactions and prevent adding/update parent transactions with categories

* Fix lint error

* Remove console.log
2025-05-12 09:17:22 -07:00
Joel Jeremy Marquez
ab3088b6fa Update budgetType preference in DB from rollover/report to be envelope/tracking (#4879)
* Update budgetType preference from rollover/report to be envelope/tracking

* Release notes

* Fix default budgetType in BalanceCell
2025-05-12 08:27:50 -07:00
Joel Jeremy Marquez
582bb59097 Move loot-core/client/app code over to desktop-client package (#4817)
* Move loot-core/client/app code over to desktop-client package

* Fix lint

* Fix lint error
2025-05-12 08:21:06 -07:00
youngcw
01c68bcfc7 Tests for goal templates (#4882)
* start

* make week tests make sense

* fix

* copy

* average

* no call checks

* add rest.  Still need to vet runSpend, and runBy

* fix average

* tweak spend

* updates

* cleanup

* note

* add more up to tests

* previous percentage

* hitting limit tests

* remainder

* fixes

* more fixes

* fixes, and fix remainder return value

* fix amounts in limit tests

* e2e for a category

* skip checking schedules if not needed

* lost changes

* fix null

* add goal tests

* final fix?

* lint

* lint option 2
2025-05-12 06:25:01 -07:00
Johnn27
dd749483f5 [Mobile] 🐛 Fix widget order (#4949)
* Fix Dashboard Widgets Not Ordered on Mobile

* Update Release Notes

* Update formatting and comment

---------

Co-authored-by: Johnn27 <>
2025-05-12 08:56:35 -04:00
sergio10
11b86fc33c Add Abanca to the list of banks with limited history (#4970)
* Add Abanca to the list of banks with limited history

* add release notes

* change category release notes
2025-05-12 08:42:50 -04:00
Guillaume Taquet Gasperini
6e5f4bfb18 Add BoursoBank GoCardless Integration (#4958)
* Add BoursoBank GoCardless Integration

The default normalization was creating new payees for each
transaction per day, as there was no specific integration
for this bank.

e.g. `Carte dd/mm/yy Payee Name Cb*1234` was taken as payee
name.

There doesn't seem to be an easy catch-all syntax for
the BoursoBank transactions, but I have put all the
ones I could find from my transactions.

* Fix comment typo

---------

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2025-05-12 08:42:30 -04:00
Vincent Giorgi
d87b78c02a invert most of seb kort credit card transactions (#4869)
* invert most of seb kort credit card transactions

* add release-notes

* revert package.json

---------

Co-authored-by: Vincent Giorgi <8283278+vincegio@users.noreply.github.com>
2025-05-12 07:55:19 -04:00
Alec Bakholdin
3cab9a374b 🐛 fixed scrollbar being impossible to grab (#4923)
* adjusted scrollbar width

* added icons to release note generator

* release notes
2025-05-11 13:23:29 -04:00
Julian Dominguez-Schatz
96949b701e Stabilize electron build directory paths (#4864)
* Stabilize electron build directory paths

* Add release notes
2025-05-11 07:39:05 -04:00
Michael Clark
0f55c67d3e 🐛 Fix sync server build not copying sql files over (#4968)
* fix sync server build not copying sql files over

* release notes

* copy-static-assets script
2025-05-10 13:17:34 +01:00
Michael Clark
b0adcb1333 🐛 Sync server - resolving the user-files and server-files to an absolute dir (#4954)
* resolving the user-files and server-files to an absolute dir

* safety

* release notes
2025-05-09 19:39:57 +01:00
Alec Bakholdin
04da20e34c Fix title first null undefined (#4908)
* added more null checks to titleFirst

* release notes

* fixed lint and typecheck

* Update packages/loot-core/src/shared/util.ts

Co-authored-by: Julian Dominguez-Schatz <julian.dominguezschatz@gmail.com>

* removed ts-ignore after changing titleFirst

---------

Co-authored-by: Alec Bakholdin <alecbakholdin@Alecs-Mac.local>
Co-authored-by: Julian Dominguez-Schatz <julian.dominguezschatz@gmail.com>
Co-authored-by: Alec Bakholdin <alecbakholdin@mac.myfiosgateway.com>
2025-05-08 12:11:27 -04:00
Alec Bakholdin
48715bf6b4 Ofx parsing dates incorrectly (#4911)
* added trim to ofx parsing

* release notes

* removed console.log
2025-05-08 12:11:19 -04:00
Alec Bakholdin
6c278a72a5 Fixed graphical bug in report (#4917)
Co-authored-by: Alec Bakholdin <alecbakholdin@Alecs-Mac.local>
2025-05-08 12:11:11 -04:00
Alec Bakholdin
17173d3ff0 New transaction text deselection on alt tab (#4921)
* changed default amount to come from amountToCurrency to handle localized values and maintain focus

* release notes

---------

Co-authored-by: Alec Bakholdin <alecbakholdin@Alecs-Mac.local>
2025-05-08 12:11:02 -04:00
Alec Bakholdin
64caf0f28b Last reconciled date not visible mobile (#4922)
* added last reconciled note to the reconciliation popup

* release notes

* Update packages/desktop-client/src/components/accounts/Reconcile.tsx

Co-authored-by: Julian Dominguez-Schatz <julian.dominguezschatz@gmail.com>

* bumped up padding

* yarn lint

---------

Co-authored-by: Julian Dominguez-Schatz <julian.dominguezschatz@gmail.com>
Co-authored-by: Alec Bakholdin <alecbakholdin@mac.myfiosgateway.com>
2025-05-08 11:42:27 -04:00
Joel Jeremy Marquez
0a8f820652 Use cx in Button className merging (#4956) 2025-05-07 08:12:57 -07:00
Michael Clark
e30735104e 🐛 Fix cli package json after ts build change (#4957)
* fix cli package json after ts build change

* release ntoes
2025-05-07 09:06:09 +01:00
Julian Dominguez-Schatz
8878f36eaf Add basic modal for budget automations (#4810)
* Add basic modal for budget automations

* Add release notes

* Exclude TODOs from this PR for now

* Fix some rebase errors

* CodeRabbit feedback

* PR feedback: reduce nesting in automation editor

* PR feedback: reduce nesting in automation read-only state

* PR feedback: make editors feel more like part of the read-only entry
2025-05-06 20:51:08 -04:00
Julian Dominguez-Schatz
9531a57f1c Deduplicate vite versions (#4943)
* Deduplicate vite versions

* Add release notes

* Ensure plugins are compatible with the latest vite

* yarn dedupe
2025-05-06 18:10:51 -04:00
Michael Clark
9d99fe2838 🐛 Fixing docker edge (#4953)
* instead of referencing the build dir directly reference using relative path

* release notes
2025-05-06 22:56:32 +01:00
Joel Jeremy Marquez
3a18718fa0 [Mobile] Show transactions upon clicking income categories (#4940)
* [Mobile] Show transactions upon clicking income categories

* Fix lint error
2025-05-06 14:09:40 -07:00
Roger Goldfinger
090345bd95 Enable Typescript in sync-server (#4887)
* attempt at running with typescript

* release notes

* working jest tests for TS files

* working docker image build

* remaining docker images

* cleanup

* ensure vitest is working

* get tests passing in ci

* less strict

* update release notes

* use tsc compiled assets in the published package

* scripts

* update yarn.lock

* Use build path for electron app

* PR feedback: move sync-server build out of bin/build-browser

* PR feedback: undo moduleResolution change

* extend main tsconfig and fix types

* PR feedback on scripts and when the sync-server build runs

* fix lint (unrelated change)

---------

Co-authored-by: alecbakholdin <alecbakholdin@gmail.com>
2025-05-05 23:45:49 -04:00
882 changed files with 20682 additions and 12067 deletions

View File

@@ -0,0 +1,24 @@
---
description:
globs:
alwaysApply: true
---
Before pushing code changes or opening a pull request, follow these steps:
1. Check if your branch already has a changelog file in the "upcoming-release-notes" folder.
2. If there is no changelog file for your branch:
a. Find the number of the most recent (highest-numbered) open issue or pull request on GitHub.
b. Increment that number by 1. Use this as the filename for your new changelog file.
c. Create a new markdown file in the "upcoming-release-notes" folder with the following format:
```
---
category: Features OR Maintenance OR Enhancements OR Bugfix
authors: [$GithubUsername]
---
$Description
```
3. Commit the new changelog file.
4. Proceed with your push or pull request.

View File

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

View File

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

View File

@@ -1,15 +1,13 @@
name: Bug Report
description: File a bug report also known as an issue or problem.
title: '[Bug]: '
labels: ['bug']
labels: ['needs triage', 'bug']
body:
- type: markdown
id: intro-md
attributes:
value: |
Thanks for taking the time to fill out this bug report! Please ensure you provide as much information as possible to better assist in confirming and identifying a fix for the bug.
- type: markdown
id: intro-md
attributes:
value: |
**IMPORTANT:** we use GitHub Issues only for BUG REPORTS and FEATURE REQUESTS. If you are looking for help/support - please reach out to the [community on Discord](https://discord.gg/pRYNYr4W5A). All non-bug and non-feature-request issues will be closed.
@@ -23,8 +21,6 @@ body:
options:
- label: 'I have searched and found no existing issue'
required: true
validations:
required: true
- type: textarea
id: what-happened
attributes:
@@ -43,7 +39,6 @@ body:
validations:
required: true
- type: markdown
id: env-info
attributes:
value: '## Environment Details'
- type: dropdown

View File

@@ -4,7 +4,6 @@ title: '[Feature] '
labels: ['feature']
body:
- type: markdown
id: intro-md
attributes:
value: |
Thanks for taking the time to fill out this feature request! Please ensure you provide as much information as possible so we can better understand what youre proposing so we can come up with the best solution for everyone.
@@ -16,8 +15,6 @@ body:
options:
- label: 'I have searched and found no existing issue'
required: true
validations:
required: true
- type: checkboxes
attributes:
label: '💻'

View File

@@ -0,0 +1,75 @@
#!/usr/bin/env node
import { Octokit } from '@octokit/rest';
import fs from 'fs';
const token = process.env.GITHUB_TOKEN;
const repo = process.env.GITHUB_REPOSITORY;
const issueNumber = process.env.GITHUB_EVENT_ISSUE_NUMBER;
const commentId = process.env.GITHUB_EVENT_COMMENT_ID;
if (!token || !repo || !issueNumber || !commentId) {
console.log('Missing required environment variables');
process.exit(1);
}
const [owner, repoName] = repo.split('/');
const octokit = new Octokit({ auth: token });
function setOutput(name, value) {
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`);
}
async function checkFirstComment() {
try {
console.log('Fetching comments with Octokit...');
// Get all comments with automatic pagination
const comments = await octokit.paginate(octokit.rest.issues.listComments, {
owner,
repo: repoName,
issue_number: issueNumber,
});
console.log(`Total comments found: ${comments.length}`);
// Filter for CodeRabbit summary comments (containing the specific marker)
const coderabbitSummaryComments = comments.filter(comment => {
const isCodeRabbit = comment.user.login === 'coderabbitai[bot]';
const hasSummaryMarker = comment.body.includes(
'<!-- This is an auto-generated comment: summarize by coderabbit.ai -->',
);
if (isCodeRabbit) {
console.log(
`CodeRabbit comment found (ID: ${comment.id}), has summary marker: ${hasSummaryMarker}`,
);
}
return isCodeRabbit && hasSummaryMarker;
});
const isFirstSummaryComment =
coderabbitSummaryComments.length === 1 &&
coderabbitSummaryComments[0].id == commentId;
console.log(
`CodeRabbit summary comments found: ${coderabbitSummaryComments.length}`,
);
console.log(`Current comment ID: ${commentId}`);
console.log(`Is first summary comment: ${isFirstSummaryComment}`);
setOutput('result', isFirstSummaryComment);
} catch (error) {
console.log('Error checking CodeRabbit comment:', error.message);
console.log('Stack:', error.stack);
setOutput('result', 'false');
process.exit(1);
}
}
checkFirstComment().catch(error => {
console.log('Unhandled error:', error.message);
console.log('Stack:', error.stack);
setOutput('result', 'false');
process.exit(1);
});

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env node
import { Octokit } from '@octokit/rest';
import fs from 'fs';
const token = process.env.GITHUB_TOKEN;
const repo = process.env.GITHUB_REPOSITORY;
const issueNumber = process.env.GITHUB_EVENT_ISSUE_NUMBER;
const prDetailsJson = process.env.PR_DETAILS;
if (!token || !repo || !issueNumber || !prDetailsJson) {
console.log('Missing required environment variables');
process.exit(1);
}
const [owner, repoName] = repo.split('/');
const octokit = new Octokit({ auth: token });
function setOutput(name, value) {
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`);
}
async function checkReleaseNotesExists() {
try {
const prDetails = JSON.parse(prDetailsJson);
if (!prDetails) {
console.log('No PR details available, skipping file check');
setOutput('result', 'false');
return;
}
const fileName = `upcoming-release-notes/${prDetails.number}.md`;
// Get PR info to get head SHA
const { data: pr } = await octokit.rest.pulls.get({
owner,
repo: repoName,
pull_number: issueNumber,
});
const prHeadSha = pr.head.sha;
console.log(
`Checking for file on PR branch: ${pr.head.ref} (${prHeadSha})`,
);
// Check if file exists
try {
await octokit.rest.repos.getContent({
owner,
repo: repoName,
path: fileName,
ref: prHeadSha,
});
console.log(
`Release notes file already exists on PR branch: ${fileName}`,
);
setOutput('result', 'true');
} catch (error) {
if (error.status === 404) {
console.log(
`No existing release notes file found on PR branch: ${fileName}`,
);
setOutput('result', 'false');
} else {
console.log('Error checking file existence:', error.message);
setOutput('result', 'false');
}
}
} catch (error) {
console.log('Error in file existence check:', error.message);
setOutput('result', 'false');
}
}
checkReleaseNotesExists();

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env node
import { Octokit } from '@octokit/rest';
const token = process.env.GITHUB_TOKEN;
const repo = process.env.GITHUB_REPOSITORY;
const issueNumber = process.env.GITHUB_EVENT_ISSUE_NUMBER;
const summaryDataJson = process.env.SUMMARY_DATA;
const category = process.env.CATEGORY;
if (!token || !repo || !issueNumber || !summaryDataJson || !category) {
console.log('Missing required environment variables');
process.exit(1);
}
const [owner, repoName] = repo.split('/');
const octokit = new Octokit({ auth: token });
async function commentOnPR() {
try {
const summaryData = JSON.parse(summaryDataJson);
if (!summaryData) {
console.log('No summary data available, skipping comment');
return;
}
if (!category || category === 'null') {
console.log('No valid category available, skipping comment');
return;
}
// Clean category for display
const cleanCategory =
typeof category === 'string'
? category.replace(/^["']|["']$/g, '')
: category;
// Get PR info for the file URL
const { data: pr } = await octokit.rest.pulls.get({
owner,
repo: repoName,
pull_number: issueNumber,
});
const prBranch = pr.head.ref;
const headOwner = pr.head.repo.owner.login;
const headRepo = pr.head.repo.name;
const fileUrl = `https://github.com/${headOwner}/${headRepo}/blob/${prBranch}/upcoming-release-notes/${summaryData.prNumber}.md`;
const commentBody = [
'🤖 **Auto-generated Release Notes**',
'',
`Hey @${summaryData.author}! I've automatically created a release notes file based on CodeRabbit's analysis:`,
'',
`**Category:** ${cleanCategory}`,
`**Summary:** ${summaryData.summary}`,
`**File:** [upcoming-release-notes/${summaryData.prNumber}.md](${fileUrl})`,
'',
'The release notes file has been committed to the repository. You can edit it if needed before merging.',
].join('\n');
await octokit.rest.issues.createComment({
owner,
repo: repoName,
issue_number: issueNumber,
body: commentBody,
});
console.log('✅ Successfully commented on PR');
} catch (error) {
console.log('Error commenting on PR:', error.message);
}
}
commentOnPR();

View File

@@ -0,0 +1,96 @@
#!/usr/bin/env node
import { Octokit } from '@octokit/rest';
const token = process.env.GITHUB_TOKEN;
const repo = process.env.GITHUB_REPOSITORY;
const issueNumber = process.env.GITHUB_EVENT_ISSUE_NUMBER;
const summaryDataJson = process.env.SUMMARY_DATA;
const category = process.env.CATEGORY;
if (!token || !repo || !issueNumber || !summaryDataJson || !category) {
console.log('Missing required environment variables');
process.exit(1);
}
const [owner, repoName] = repo.split('/');
const octokit = new Octokit({ auth: token });
async function createReleaseNotesFile() {
try {
const summaryData = JSON.parse(summaryDataJson);
console.log('Debug - Category value:', category);
console.log('Debug - Category type:', typeof category);
console.log('Debug - Category JSON stringified:', JSON.stringify(category));
if (!summaryData) {
console.log('No summary data available, cannot create file');
return;
}
if (!category || category === 'null') {
console.log('No valid category available, cannot create file');
return;
}
// Create file content - ensure category is not quoted
const cleanCategory =
typeof category === 'string'
? category.replace(/^["']|["']$/g, '')
: category;
console.log('Debug - Clean category:', cleanCategory);
const fileContent = `---
category: ${cleanCategory}
authors: [${summaryData.author}]
---
${summaryData.summary}`;
const fileName = `upcoming-release-notes/${summaryData.prNumber}.md`;
console.log(`Creating release notes file: ${fileName}`);
console.log('File content:');
console.log(fileContent);
// Get PR info
const { data: pr } = await octokit.rest.pulls.get({
owner,
repo: repoName,
pull_number: issueNumber,
});
const prBranch = pr.head.ref;
const headOwner = pr.head.repo.owner.login;
const headRepo = pr.head.repo.name;
console.log(
`Committing to PR branch: ${headOwner}/${headRepo}:${prBranch}`,
);
// Create the file via GitHub API on the PR branch
await octokit.rest.repos.createOrUpdateFileContents({
owner: headOwner,
repo: headRepo,
path: fileName,
message: `Add release notes for PR #${summaryData.prNumber}`,
content: Buffer.from(`${fileContent}\n\n`).toString('base64'),
branch: prBranch,
committer: {
name: 'github-actions[bot]',
email: 'github-actions[bot]@users.noreply.github.com',
},
author: {
name: 'github-actions[bot]',
email: 'github-actions[bot]@users.noreply.github.com',
},
});
console.log(`✅ Successfully created release notes file: ${fileName}`);
} catch (error) {
console.log('Error creating release notes file:', error.message);
}
}
createReleaseNotesFile();

View File

@@ -0,0 +1,118 @@
#!/usr/bin/env node
const https = require('https');
const fs = require('fs');
const commentBody = process.env.GITHUB_EVENT_COMMENT_BODY;
const prDetailsJson = process.env.PR_DETAILS;
const summaryDataJson = process.env.SUMMARY_DATA;
const openaiApiKey = process.env.OPENAI_API_KEY;
if (!commentBody || !prDetailsJson || !summaryDataJson || !openaiApiKey) {
console.log('Missing required environment variables');
process.exit(1);
}
function setOutput(name, value) {
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`);
}
try {
const prDetails = JSON.parse(prDetailsJson);
const summaryData = JSON.parse(summaryDataJson);
if (!summaryData || !prDetails) {
console.log('Missing data for categorization');
setOutput('result', 'null');
process.exit(0);
}
const data = JSON.stringify({
model: 'gpt-4o-mini',
messages: [
{
role: 'system',
content:
'You are categorizing pull requests for release notes. You must respond with exactly one of these categories: "Features", "Enhancements", "Bugfix", or "Maintenance". No other text or explanation.',
},
{
role: 'user',
content: `PR Title: ${prDetails.title}\n\nGenerated Summary: ${summaryData.summary}\n\nCodeRabbit Analysis:\n${commentBody}\n\nCategories:\n- Features: New functionality or capabilities\n- Bugfix: Fixes for broken or incorrect behavior\n- Enhancements: Improvements to existing functionality\n- Maintenance: Code cleanup, refactoring, dependencies, etc.\n\nWhat category does this PR belong to?`,
},
],
max_tokens: 10,
temperature: 0.1,
});
const options = {
hostname: 'api.openai.com',
path: '/v1/chat/completions',
method: 'POST',
headers: {
Authorization: `Bearer ${openaiApiKey}`,
'Content-Type': 'application/json',
},
};
const req = https.request(options, res => {
let responseData = '';
res.on('data', chunk => (responseData += chunk));
res.on('end', () => {
if (res.statusCode !== 200) {
console.log('OpenAI API error for categorization');
setOutput('result', 'null');
return;
}
try {
const response = JSON.parse(responseData);
console.log('OpenAI raw response:', JSON.stringify(response, null, 2));
const rawContent = response.choices[0].message.content.trim();
console.log('Raw content from OpenAI:', rawContent);
let category;
try {
category = JSON.parse(rawContent);
console.log('Parsed category:', category);
} catch (parseError) {
console.log(
'JSON parse error, using raw content:',
parseError.message,
);
category = rawContent;
}
// Validate the category response
const validCategories = [
'Features',
'Bugfix',
'Enhancements',
'Maintenance',
];
if (validCategories.includes(category)) {
console.log('OpenAI categorized as:', category);
setOutput('result', category);
} else {
console.log('Invalid category from OpenAI:', category);
console.log('Valid categories are:', validCategories);
setOutput('result', 'null');
}
} catch (error) {
console.log('Error parsing OpenAI response:', error.message);
setOutput('result', 'null');
}
});
});
req.on('error', error => {
console.log('Error in categorization:', error.message);
setOutput('result', 'null');
});
req.write(data);
req.end();
} catch (error) {
console.log('Error in categorization:', error.message);
setOutput('result', 'null');
}

View File

@@ -0,0 +1,97 @@
#!/usr/bin/env node
const https = require('https');
const fs = require('fs');
const commentBody = process.env.GITHUB_EVENT_COMMENT_BODY;
const prDetailsJson = process.env.PR_DETAILS;
const openaiApiKey = process.env.OPENAI_API_KEY;
if (!commentBody || !prDetailsJson || !openaiApiKey) {
console.log('Missing required environment variables');
process.exit(1);
}
function setOutput(name, value) {
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`);
}
try {
const prDetails = JSON.parse(prDetailsJson);
if (!prDetails) {
console.log('No PR details available, cannot generate summary');
setOutput('result', 'null');
process.exit(0);
}
console.log('CodeRabbit comment body:', commentBody);
const data = JSON.stringify({
model: 'gpt-4o-mini',
messages: [
{
role: 'system',
content:
'You are a technical writer helping to create concise release notes. Generate a maximum 15-word summary that describes what this PR does. Focus on the user-facing changes or bug fixes. Do not include "This PR" or similar phrases - just describe the change directly. Start with a base form verb (e.g., "Add" not "Adds", "Fix" not "Fixes", "Introduce" not "Introduces").',
},
{
role: 'user',
content: `PR Title: ${prDetails.title}\n\nCodeRabbit Analysis:\n${commentBody}\n\nPlease provide a concise summary (max 15 words) of what this PR accomplishes.`,
},
],
max_tokens: 50,
temperature: 0.3,
});
const options = {
hostname: 'api.openai.com',
path: '/v1/chat/completions',
method: 'POST',
headers: {
Authorization: `Bearer ${openaiApiKey}`,
'Content-Type': 'application/json',
},
};
const req = https.request(options, res => {
let responseData = '';
res.on('data', chunk => (responseData += chunk));
res.on('end', () => {
if (res.statusCode !== 200) {
console.log(`OpenAI API error: ${res.statusCode} ${res.statusMessage}`);
setOutput('result', 'null');
return;
}
try {
const response = JSON.parse(responseData);
const summary = response.choices[0].message.content.trim();
console.log('Generated summary:', summary);
const result = {
summary: summary,
prNumber: prDetails.number,
author: prDetails.author,
};
setOutput('result', JSON.stringify(result));
} catch (error) {
console.log('Error parsing OpenAI response:', error.message);
setOutput('result', 'null');
}
});
});
req.on('error', error => {
console.log('Error generating summary:', error.message);
setOutput('result', 'null');
});
req.write(data);
req.end();
} catch (error) {
console.log('Error generating summary:', error.message);
setOutput('result', 'null');
}

View File

@@ -0,0 +1,59 @@
#!/usr/bin/env node
import { Octokit } from '@octokit/rest';
import fs from 'fs';
const token = process.env.GITHUB_TOKEN;
const repo = process.env.GITHUB_REPOSITORY;
const issueNumber = process.env.GITHUB_EVENT_ISSUE_NUMBER;
if (!token || !repo || !issueNumber) {
console.log('Missing required environment variables');
process.exit(1);
}
const [owner, repoName] = repo.split('/');
const octokit = new Octokit({ auth: token });
function setOutput(name, value) {
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`);
}
async function getPRDetails() {
try {
console.log(
`Fetching PR details for ${owner}/${repoName}#${issueNumber}...`,
);
const { data: pr } = await octokit.rest.pulls.get({
owner,
repo: repoName,
pull_number: issueNumber,
});
console.log('PR details fetched successfully');
console.log('- PR Number:', pr.number);
console.log('- PR Author:', pr.user.login);
console.log('- PR Title:', pr.title);
const result = {
number: pr.number,
author: pr.user.login,
title: pr.title,
};
setOutput('result', JSON.stringify(result));
} catch (error) {
console.log('Error getting PR details:', error.message);
console.log('Stack:', error.stack);
setOutput('result', 'null');
process.exit(1);
}
}
getPRDetails().catch(error => {
console.log('Unhandled error:', error.message);
console.log('Stack:', error.stack);
setOutput('result', 'null');
process.exit(1);
});

View File

@@ -0,0 +1,94 @@
#!/usr/bin/env node
// 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.
const { parseArgs } = require('node:util');
const fs = require('node:fs');
const args = process.argv;
const options = {
'package-json': {
type: 'string',
short: 'p',
},
type: {
type: 'string', // nightly, hotfix, monthly
short: 't',
},
};
const { values } = parseArgs({
args,
options,
allowPositionals: true,
});
if (!values['package-json']) {
console.error(
'Please specify the path to package.json using --package-json or -p option.',
);
process.exit(1);
}
try {
const packageJsonPath = values['package-json'];
// Read and parse package.json
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const currentVersion = packageJson.version;
// Parse year and month from version (e.g. 25.5.1 -> year=2025, month=5)
const versionParts = currentVersion.split('.');
const versionYear = parseInt(versionParts[0]);
const versionMonth = parseInt(versionParts[1]);
const versionHotfix = parseInt(versionParts[2]);
// Create date and add 1 month
const versionDate = new Date(2000 + versionYear, versionMonth - 1, 1); // month is 0-indexed
const nextVersionMonthDate = new Date(
versionDate.getFullYear(),
versionDate.getMonth() + 1,
1,
);
// Format back to YY.M format
const nextVersionYear = nextVersionMonthDate
.getFullYear()
.toString()
.slice(-2);
const nextVersionMonth = nextVersionMonthDate.getMonth() + 1; // Convert back to 1-indexed
// Get current date string
const currentDate = new Date()
.toISOString()
.split('T')[0]
.replaceAll('-', '');
switch (values.type) {
case 'nightly': {
const newVersion = `${nextVersionYear}.${nextVersionMonth}.0-nightly.${currentDate}`;
process.stdout.write(newVersion); // return the new version to stdout
process.exit();
}
case 'hotfix': {
const bugfixVersion = `${versionYear}.${versionMonth}.${versionHotfix + 1}`;
process.stdout.write(bugfixVersion); // return the bugfix version to stdout
process.exit();
}
case 'monthly': {
const stableVersion = `${nextVersionYear}.${nextVersionMonth}.0`;
process.stdout.write(stableVersion); // return the stable version to stdout
process.exit();
}
default:
console.error(
'Invalid type specified. Use "nightly", "hotfix", or "monthly".',
);
process.exit(1);
}
} catch (error) {
console.error('Error:', error.message);
process.exit(1);
}

View File

@@ -1,4 +1,5 @@
name: Setup
description: Setup the environment for the project
inputs:
working-directory:
@@ -16,17 +17,21 @@ runs:
- name: Install node
uses: actions/setup-node@v4
with:
node-version: 18.16.0
node-version: 20
- name: Install yarn
run: npm install -g yarn
shell: bash
if: ${{ env.ACT }}
- name: Get Node version
id: get-node
run: echo "version=$(node -v)" >> "$GITHUB_OUTPUT"
shell: bash
- name: Cache
uses: actions/cache@v4
id: cache
with:
path: ${{ format('{0}/**/node_modules', inputs.working-directory) }}
key: yarn-v1-${{ runner.os }}-${{ hashFiles(format('{0}/.nvmrc', inputs.working-directory)) }}-${{ hashFiles(format('{0}/**/yarn.lock', inputs.working-directory)) }}
key: yarn-v1-${{ runner.os }}-${{ steps.get-node.outputs.version }}-${{ hashFiles(format('{0}/**/yarn.lock', inputs.working-directory)) }}
- name: Install
working-directory: ${{ inputs.working-directory }}
run: yarn --immutable

350
.github/scripts/count-points.mjs vendored Normal file
View File

@@ -0,0 +1,350 @@
import { Octokit } from '@octokit/rest';
import { minimatch } from 'minimatch';
/** Repository-specific configuration for points calculation */
const REPOSITORY_CONFIG = new Map([
[
'actual',
{
POINTS_PER_ISSUE_TRIAGE_ACTION: 1,
POINTS_PER_ISSUE_CLOSING_ACTION: 1,
POINTS_PER_RELEASE_PR: 0,
PR_REVIEW_POINT_TIERS: [
{ minChanges: 1000, points: 6 },
{ minChanges: 100, points: 4 },
{ minChanges: 0, points: 2 },
],
EXCLUDED_FILES: [
'yarn.lock',
'.yarn/**/*',
'packages/component-library/src/icons/**/*',
'release-notes/**/*',
],
},
],
[
'docs',
{
POINTS_PER_ISSUE_TRIAGE_ACTION: 1,
POINTS_PER_ISSUE_CLOSING_ACTION: 1,
POINTS_PER_RELEASE_PR: 4,
PR_REVIEW_POINT_TIERS: [
{ minChanges: 1000, points: 6 },
{ minChanges: 100, points: 4 },
{ minChanges: 0, points: 2 },
],
EXCLUDED_FILES: ['yarn.lock', '.yarn/**/*'],
},
],
]);
/**
* Get the start and end dates for the last month.
* @returns {Object} An object containing the start and end dates.
*/
function getLastMonthDates() {
// Get data relating to the last month
const now = new Date();
const firstDayOfLastMonth = new Date(
now.getFullYear(),
now.getMonth() - 1,
1,
);
const since = process.env.START_DATE
? new Date(process.env.START_DATE)
: firstDayOfLastMonth;
// Calculate the end of the month for the since date
const until = new Date(
since.getFullYear(),
since.getMonth() + 1,
0,
23,
59,
59,
999,
);
return { since, until };
}
/**
* Used for calculating the monthly points each core contributor has earned.
* These are used for payouts depending.
* @param {string} repo - The repository to analyze ('actual' or 'docs')
* @returns {number} The total points earned for the repository
*/
async function countContributorPoints(repo) {
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
const owner = 'actualbudget';
const config = REPOSITORY_CONFIG.get(repo);
const { since, until } = getLastMonthDates();
// Get organization members
const { data: orgMembers } = await octokit.orgs.listMembers({
org: owner,
});
const orgMemberLogins = new Set(orgMembers.map(member => member.login));
// Initialize stats map with all org members
const stats = new Map(
Array.from(orgMemberLogins).map(login => [
login,
{
reviews: [], // Will store objects with PR number and points
labelRemovals: [],
issueClosings: [],
points: 0,
},
]),
);
// Helper function to print statistics
const printStats = (title, getValue, formatLine) => {
console.log(`\n${title}:`);
console.log('='.repeat(title.length + 1));
const entries = Array.from(stats.entries())
.map(([user, userStats]) => [user, getValue(userStats)])
.filter(([, count]) => count > 0)
.sort((a, b) => b[1] - a[1]);
if (entries.length === 0) {
console.log(`No ${title.toLowerCase()} found in the last month.`);
} else {
entries.forEach(([user, count]) => {
console.log(formatLine(user, count));
});
}
};
// Get all PRs using search
const searchQuery = `repo:${owner}/${repo} is:pr is:merged merged:${since.toISOString()}..${until.toISOString()}`;
const recentPRs = await octokit.paginate(
octokit.search.issuesAndPullRequests,
{
q: searchQuery,
per_page: 100,
advanced_search: true,
},
response => response.data,
);
// Get reviews and PR details for each PR
for (const pr of recentPRs) {
const { data: reviews } = await octokit.pulls.listReviews({
owner,
repo,
pull_number: pr.number,
});
// Get list of modified files
const { data: modifiedFiles } = await octokit.pulls.listFiles({
owner,
repo,
pull_number: pr.number,
});
// Calculate points based on PR size, excluding specified files
const totalChanges = modifiedFiles
.filter(
file =>
!config.EXCLUDED_FILES.some(pattern =>
minimatch(file.filename, pattern),
),
)
.reduce((sum, file) => sum + file.additions + file.deletions, 0);
// Check if this is a release PR
const isReleasePR = pr.title.match(/^🔖 \(\d+\.\d+\.\d+\)/);
// Calculate points for reviewers based on PR size
const prPoints = config.PR_REVIEW_POINT_TIERS.find(
tier => totalChanges > tier.minChanges,
).points;
// Add points to the reviewers
const uniqueReviewers = new Set();
reviews
.filter(
review =>
stats.has(review.user?.login) &&
review.state === 'APPROVED' &&
!uniqueReviewers.has(review.user?.login),
)
.forEach(({ user: { login: reviewer } }) => {
uniqueReviewers.add(reviewer);
const userStats = stats.get(reviewer);
userStats.reviews.push({ pr: pr.number.toString(), points: prPoints });
userStats.points += prPoints;
});
// Award points to the PR creator if it's a release PR
if (isReleasePR && stats.has(pr.user.login)) {
const creatorStats = stats.get(pr.user.login);
creatorStats.reviews.push({
pr: pr.number.toString(),
points: config.POINTS_PER_RELEASE_PR,
isReleaseCreator: true,
});
creatorStats.points += config.POINTS_PER_RELEASE_PR;
}
}
// Get all issues with label events in the last month
const issues = await octokit.paginate(
octokit.issues.listForRepo,
{
owner,
repo,
state: 'all',
sort: 'updated',
direction: 'desc',
per_page: 100,
since: since.toISOString(),
},
(response, done) =>
response.data.filter(issue => new Date(issue.updated_at) <= until),
);
// Get label events for each issue
for (const issue of issues) {
const { data: events } = await octokit.issues.listEventsForTimeline({
owner,
repo,
issue_number: issue.number,
});
// Process events
events
.filter(
event =>
new Date(event.created_at) > since &&
new Date(event.created_at) <= until &&
stats.has(event.actor?.login),
)
.forEach(event => {
if (
event.event === 'unlabeled' &&
event.label &&
event.label.name.toLowerCase() === 'needs triage'
) {
const remover = event.actor.login;
const userStats = stats.get(remover);
userStats.labelRemovals.push(issue.number.toString());
userStats.points += config.POINTS_PER_ISSUE_TRIAGE_ACTION;
}
if (event.event === 'closed') {
const closer = event.actor.login;
const userStats = stats.get(closer);
userStats.issueClosings.push(issue.number.toString());
userStats.points += config.POINTS_PER_ISSUE_CLOSING_ACTION;
}
});
}
// Print all statistics
printStats(
`PR Review Statistics (${repo})`,
stats => stats.reviews.length,
(user, count) =>
`${user}: ${count} (PRs: ${stats
.get(user)
.reviews.map(r => {
if (r.isReleaseCreator) {
return `#${r.pr} (${r.points}pts - Release Creator)`;
}
return `#${r.pr} (${r.points}pts)`;
})
.join(', ')})`,
);
printStats(
`"Needs Triage" Label Removal Statistics (${repo})`,
stats => stats.labelRemovals.length,
(user, count) =>
`${user}: ${count} (Issues: ${stats.get(user).labelRemovals.join(', ')})`,
);
printStats(
`Issue Closing Statistics (${repo})`,
stats => stats.issueClosings.length,
(user, count) =>
`${user}: ${count} (Issues: ${stats.get(user).issueClosings.join(', ')})`,
);
// Print points summary
printStats(
`Points Summary (${repo})`,
stats => stats.points,
(user, userPoints) => `${user}: ${userPoints}`,
);
// Calculate and print total points
const totalPoints = Array.from(stats.values()).reduce(
(sum, userStats) => sum + userStats.points,
0,
);
console.log(`\nTotal points earned for ${repo}: ${totalPoints}`);
// Return the points
return new Map(
Array.from(stats.entries()).map(([login, userStats]) => [
login,
userStats.points,
]),
);
}
/**
* Calculate the points for both repositories and print cumulative results
*/
async function calculateCumulativePoints() {
// Get stats for each repository
const repoPointsResults = await Promise.all(
Array.from(REPOSITORY_CONFIG.keys()).map(countContributorPoints),
);
// Calculate cumulative stats
const cumulativeStats = new Map(repoPointsResults[0]);
// Combine stats from all repositories
for (let i = 1; i < repoPointsResults.length; i++) {
for (const [login, points] of repoPointsResults[i].entries()) {
if (!cumulativeStats.has(login)) {
cumulativeStats.set(login, 0);
}
cumulativeStats.set(login, cumulativeStats.get(login) + points);
}
}
// Print cumulative statistics
console.log('\n\nCUMULATIVE STATISTICS ACROSS ALL REPOSITORIES');
console.log('='.repeat(50));
console.log('\nCumulative Points Summary:');
console.log('='.repeat('Cumulative Points Summary'.length + 1));
const entries = Array.from(cumulativeStats.entries())
.filter(([, count]) => count > 0)
.sort((a, b) => b[1] - a[1]);
if (entries.length === 0) {
console.log('No cumulative points summary found.');
} else {
entries.forEach(([user, points]) => {
console.log(`${user}: ${points}`);
});
}
// Calculate and print total cumulative points
const totalCumulativePoints = Array.from(cumulativeStats.values()).reduce(
(sum, points) => sum + points,
0,
);
console.log('\nTotal cumulative points earned: ' + totalCumulativePoints);
}
// Run the calculations
calculateCumulativePoints().catch(console.error);

View File

@@ -0,0 +1,89 @@
name: Generate Release Notes from CodeRabbit summary
on:
issue_comment:
types: [created]
jobs:
generate-release-notes:
# Only run on PR comments from CodeRabbit bot
if: github.event.issue.pull_request && github.event.comment.user.login == 'coderabbitai[bot]'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
pull-requests: write
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up environment
uses: ./.github/actions/setup
- name: Check if this is CodeRabbit's first comment
id: check-first-comment
run: node .github/actions/ai-generated-release-notes/check-first-comment.js
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }}
- name: Get PR details
if: steps.check-first-comment.outputs.result == 'true'
id: pr-details
run: node .github/actions/ai-generated-release-notes/pr-details.js
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
- name: Check if release notes file already exists
if: steps.check-first-comment.outputs.result == 'true' && steps.pr-details.outputs.result != 'null'
id: check-release-notes-exists
run: node .github/actions/ai-generated-release-notes/check-release-notes-exists.js
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
PR_DETAILS: ${{ steps.pr-details.outputs.result }}
- name: Generate summary with OpenAI
if: steps.check-first-comment.outputs.result == 'true' && steps.check-release-notes-exists.outputs.result == 'false'
id: generate-summary
run: node .github/actions/ai-generated-release-notes/generate-summary.js
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GITHUB_EVENT_COMMENT_BODY: ${{ github.event.comment.body }}
PR_DETAILS: ${{ steps.pr-details.outputs.result }}
- name: Determine category with OpenAI
if: steps.check-first-comment.outputs.result == 'true' && steps.check-release-notes-exists.outputs.result == 'false' && steps.generate-summary.outputs.result != 'null'
id: determine-category
run: node .github/actions/ai-generated-release-notes/determine-category.js
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GITHUB_EVENT_COMMENT_BODY: ${{ github.event.comment.body }}
PR_DETAILS: ${{ steps.pr-details.outputs.result }}
SUMMARY_DATA: ${{ steps.generate-summary.outputs.result }}
- name: Create and commit release notes file via GitHub API
if: steps.check-first-comment.outputs.result == 'true' && steps.check-release-notes-exists.outputs.result == 'false' && steps.generate-summary.outputs.result != 'null' && steps.determine-category.outputs.result != 'null' && steps.determine-category.outputs.result != ''
run: node .github/actions/ai-generated-release-notes/create-release-notes-file.js
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
SUMMARY_DATA: ${{ steps.generate-summary.outputs.result }}
CATEGORY: ${{ steps.determine-category.outputs.result }}
- name: Comment on PR
if: steps.check-first-comment.outputs.result == 'true' && steps.check-release-notes-exists.outputs.result == 'false' && steps.generate-summary.outputs.result != 'null' && steps.determine-category.outputs.result != 'null' && steps.determine-category.outputs.result != ''
run: node .github/actions/ai-generated-release-notes/comment-on-pr.js
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
SUMMARY_DATA: ${{ steps.generate-summary.outputs.result }}
CATEGORY: ${{ steps.determine-category.outputs.result }}

23
.github/workflows/autofix.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: autofix.ci
defaults:
run:
shell: bash
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
permissions:
contents: read
jobs:
autofix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up environment
uses: ./.github/actions/setup
- name: Format code
run: yarn lint:fix
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27

View File

@@ -57,7 +57,7 @@ jobs:
- name: Set up environment
uses: ./.github/actions/setup
- name: Build Web
run: ./bin/package-browser
run: yarn build:browser
- name: Upload Build
uses: actions/upload-artifact@v4
with:
@@ -76,7 +76,7 @@ jobs:
- name: Set up environment
uses: ./.github/actions/setup
- name: Build Server
run: cd packages/sync-server && yarn build
run: yarn workspace @actual-app/sync-server build
- name: Upload Build
uses: actions/upload-artifact@v4
with:

View File

@@ -27,6 +27,16 @@ jobs:
uses: ./.github/actions/setup
- name: Typecheck
run: yarn typecheck
validate-cli:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up environment
uses: ./.github/actions/setup
- name: Build Web
run: yarn build:server
- name: Check that the built CLI works
run: node packages/sync-server/build/bin/actual-server.js --version
test:
runs-on: ubuntu-latest
steps:
@@ -43,6 +53,6 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '19'
node-version: 20
- name: Check migrations
run: node ./.github/actions/check-migrations.js

26
.github/workflows/count-points.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Count points
on:
schedule:
# Run at 00:00 on the first day of every month
- cron: '0 0 1 * *'
workflow_dispatch:
inputs:
startDate:
description: 'Start date for point counter (YYYY-MM-DD)'
required: true
type: string
jobs:
count-points:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up environment
uses: ./.github/actions/setup
- name: Count points
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
START_DATE: ${{ inputs.startDate }}
run: node .github/scripts/count-points.mjs

View File

@@ -78,9 +78,26 @@ jobs:
- name: Set up environment
uses: ./.github/actions/setup
- name: Build Web
run: ./bin/package-browser
run: yarn build:server
- name: Build and push image
- name: Build image for testing
uses: docker/build-push-action@v5
with:
context: .
push: false
load: true
file: packages/sync-server/docker/${{ matrix.os }}.Dockerfile
tags: actualbudget/actual-server-testing
- name: Test that the docker image boots
run: |
docker run --detach --network=host actualbudget/actual-server-testing
sleep 5
curl --fail -sS -LI -w '%{http_code}\n' --retry 10 --retry-delay 1 --retry-connrefused localhost:5006
# 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@v5
with:
context: .

View File

@@ -75,7 +75,7 @@ jobs:
- name: Set up environment
uses: ./.github/actions/setup
- name: Build Web
run: ./bin/package-browser
run: yarn build:server
- name: Build and push ubuntu image
uses: docker/build-push-action@v5

View File

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

View File

@@ -100,3 +100,45 @@ jobs:
!packages/desktop-electron/dist/Actual-windows.exe
packages/desktop-electron/dist/*.AppImage
packages/desktop-electron/dist/*.flatpak
publish-microsoft-store:
needs: build
runs-on: windows-latest
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
steps:
- name: Install StoreBroker
shell: powershell
run: |
Install-Module -Name StoreBroker -AcceptLicense -Force -Scope CurrentUser -Verbose
- name: Download Microsoft Store artifacts
uses: actions/download-artifact@v4
with:
name: actual-electron-windows-latest-appx
- name: Submit to Microsoft Store
shell: powershell
run: |
# Disable telemetry
$global:SBDisableTelemetry = $true
# Authenticate against the store
$pass = ConvertTo-SecureString -String '${{ secrets.MICROSOFT_STORE_CLIENT_SECRET }}' -AsPlainText -Force
$cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ${{ secrets.MICROSOFT_STORE_CLIENT_ID }},$pass
Set-StoreBrokerAuthentication -TenantId '${{ secrets.MICROSOFT_STORE_TENANT_ID }}' -Credential $cred
# Zip and create metadata files
$artifacts = Get-ChildItem -Path . -Filter *.appx | Select-Object -ExpandProperty FullName
New-StoreBrokerConfigFile -Path "$PWD/config.json" -AppId ${{ secrets.MICROSOFT_STORE_PRODUCT_ID }}
New-SubmissionPackage -ConfigPath "$PWD/config.json" -DisableAutoPackageNameFormatting -AppxPath $artifacts -OutPath "$PWD" -OutName submission
# Submit the app
# See https://github.com/microsoft/StoreBroker/blob/master/Documentation/USAGE.md#the-easy-way
Update-ApplicationSubmission `
-AppId ${{ secrets.MICROSOFT_STORE_PRODUCT_ID }} `
-SubmissionDataPath "submission.json" `
-PackagePath "submission.zip" `
-ReplacePackages `
-NoStatus `
-AutoCommit `
-Force

View File

@@ -3,7 +3,7 @@ name: Extract and upload i18n strings
on:
schedule:
# 4am UTC
- cron: "0 4 * * *"
- cron: '0 4 * * *'
workflow_dispatch:
jobs:
@@ -19,7 +19,7 @@ jobs:
uses: ./actual/.github/actions/setup
with:
working-directory: actual
download-translations: false # As we'll manually clone instead
download-translations: false # As we'll manually clone instead
- name: Configure Git config
run: |
git config --global user.name "github-actions[bot]"
@@ -78,7 +78,7 @@ jobs:
actualbudget/actual
- name: Unlock translations
if: always() # Clean up even on failure
if: always() # Clean up even on failure
run: |
wlc \
--url https://hosted.weblate.org/api/ \

View File

@@ -24,7 +24,7 @@ jobs:
body: |
:sparkles: Thanks for sharing your idea! :sparkles:
This repository uses lodash style issue management for enhancements. That means enhancement issues are automatically closed. This doesnt mean we dont accept feature requests, though! We will consider implementing ones that receive many upvotes, and we welcome contributions for any feature requests marked as needing votes (just post a comment first so we can help you make a successful contribution).
This repository uses a voting-based system for feature requests. While enhancement issues are automatically closed, we still welcome feature requests! The voting system helps us gauge community interest in potential features. We also encourage community contributions for any feature requests marked as needing votes (just post a comment first so we can help guide you toward a successful contribution).
The enhancement backlog can be found here: https://github.com/actualbudget/actual/issues?q=label%3A%22needs+votes%22+sort%3Areactions-%2B1-desc+

View File

@@ -27,7 +27,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '19'
node-version: 20
- name: Handle feature requests
run: node .github/actions/handle-feature-requests.js
env:

View File

@@ -22,15 +22,15 @@ jobs:
steps:
- name: Repository Checkout
uses: actions/checkout@v4
- name: Set up environment
uses: ./.github/actions/setup
- name: Install Netlify
run: npm install netlify-cli@17.10.1 -g
- name: Build Actual
run: ./bin/package-browser
run: yarn build:browser
- name: Deploy to Netlify
id: netlify_deploy
@@ -40,4 +40,4 @@ jobs:
--site ${{ secrets.NETLIFY_SITE_ID }} \
--auth ${{ secrets.NETLIFY_API_TOKEN }} \
--filter @actual-app/web \
--prod
--prod

View File

@@ -0,0 +1,95 @@
name: Publish nightly npm packages
# Nightly npm packages are built daily
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
jobs:
build-and-pack:
runs-on: ubuntu-latest
name: Build and pack npm packages
if: github.event.repository.fork == false
steps:
- uses: actions/checkout@v4
- name: Set up environment
uses: ./.github/actions/setup
- name: Update package versions
run: |
# Get new nightly versions
NEW_WEB_VERSION=$(node ./.github/actions/get-next-package-version.js --package-json ./packages/desktop-client/package.json --type nightly)
NEW_SYNC_VERSION=$(node ./.github/actions/get-next-package-version.js --package-json ./packages/sync-server/package.json --type nightly)
NEW_API_VERSION=$(node ./.github/actions/get-next-package-version.js --package-json ./packages/api/package.json --type nightly)
# Set package versions
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
- name: Yarn install
run: |
yarn install
- name: Build Server & Web
run: yarn build:server
- name: Pack the web and server packages
run: |
yarn workspace @actual-app/web pack --filename @actual-app/web.tgz
yarn workspace @actual-app/sync-server pack --filename @actual-app/sync-server.tgz
- name: Build API
run: yarn build:api
- name: Pack the api package
run: |
yarn workspace @actual-app/api pack --filename @actual-app/api.tgz
- name: Upload package artifacts
uses: actions/upload-artifact@v4
with:
name: npm-packages
path: |
packages/desktop-client/@actual-app/web.tgz
packages/sync-server/@actual-app/sync-server.tgz
packages/api/@actual-app/api.tgz
publish:
runs-on: ubuntu-latest
name: Publish Nightly npm packages
needs: build-and-pack
permissions:
contents: read
packages: write
steps:
- name: Download the artifacts
uses: actions/download-artifact@v4
with:
name: npm-packages
- name: Setup node and npm registry
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: 'https://registry.npmjs.org'
- name: Publish Web
run: |
npm publish desktop-client/@actual-app/web.tgz --access public --tag nightly
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish Sync-Server
run: |
npm publish sync-server/@actual-app/sync-server.tgz --access public --tag nightly
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish API
run: |
npm publish api/@actual-app/api.tgz --access public --tag nightly
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -17,7 +17,7 @@ jobs:
uses: ./.github/actions/setup
- name: Build Web
run: yarn build:browser
run: yarn build:server
- name: Pack the web and server packages
run: |
@@ -56,7 +56,7 @@ jobs:
- name: Setup node and npm registry
uses: actions/setup-node@v4
with:
node-version: '20.x'
node-version: 20
registry-url: 'https://registry.npmjs.org'
- name: Publish Web

View File

@@ -2,6 +2,7 @@ name: 'Close stale PRs'
on:
schedule:
- cron: '30 1 * * *'
workflow_dispatch: # Allow manual triggering
jobs:
stale:
@@ -24,3 +25,18 @@ jobs:
any-of-labels: ':construction: WIP'
days-before-close: -1
days-before-issue-stale: -1
stale-needs-info:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
stale-issue-label: 'needs info'
days-before-stale: -1
days-before-close: 7
close-issue-message: 'This issue has been automatically closed because there have been no comments for 7 days after the "needs info" label was added. If you still need help, please feel free to reopen the issue with the requested information.'
remove-stale-when-updated: false
stale-pr-message: '' # Disable PR processing
close-pr-message: '' # Disable PR processing
days-before-pr-stale: -1 # Disable PR processing
days-before-pr-close: -1 # Disable PR processing

View File

@@ -19,7 +19,7 @@ jobs:
github.event.issue.pull_request &&
contains(github.event.comment.body, '/update-vrt')
container:
image: mcr.microsoft.com/playwright:v1.41.1-jammy
image: mcr.microsoft.com/playwright:v1.52.0-jammy
steps:
- name: Get PR branch
# Until https://github.com/xt0rted/pull-request-comment-branch/issues/322 is resolved we use the forked version

2
.nvmrc
View File

@@ -1 +1 @@
v18.16.0
v20/*

View File

@@ -20,11 +20,8 @@ packages/desktop-client/playwright-report/
packages/desktop-electron/client-build/
packages/desktop-electron/build/
packages/desktop-electron/dist/
packages/import-ynab4/**/node_modules/*
packages/import-ynab5/**/node_modules/*
packages/loot-core/**/node_modules/*
packages/loot-core/**/lib-dist/*
packages/loot-core/**/proto/*
.yarn/*
.github/*
upcoming-release-notes/*

View File

@@ -0,0 +1,10 @@
diff --git a/methods/inflater.js b/methods/inflater.js
index 8769e66e82b25541aba80b1ac6429199c9a8179f..1d4402402f0e1aaf64062c1f004c3d6e6fe93e76 100644
--- a/methods/inflater.js
+++ b/methods/inflater.js
@@ -1,4 +1,4 @@
-const version = +(process.versions ? process.versions.node : "").split(".")[0] || 0;
+const version = +(process?.versions?.node ?? "").split(".")[0] || 0;
module.exports = function (/*Buffer*/ inbuf, /*number*/ expectedLength) {
var zlib = require("zlib");

File diff suppressed because one or more lines are too long

948
.yarn/releases/yarn-4.9.1.cjs vendored Executable file

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -5,7 +5,7 @@
# you are doing.
###################################################
FROM node:18-bullseye as dev
FROM node:20-bullseye as dev
RUN apt-get update -y && apt-get upgrade -y && apt-get install -y openssl
WORKDIR /app
CMD ["sh", "./bin/docker-start"]

View File

@@ -45,6 +45,7 @@ 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/web build:browser
yarn workspace @actual-app/sync-server build
yarn workspace desktop-electron update-client

View File

@@ -16,7 +16,7 @@ async function run() {
`Found potentially matching PR ${activePr.number}: ${activePr.title}`,
);
}
const prNumber = activePr?.number ?? (await getNextPrNumber());
const initialPrNumber = activePr?.number ?? (await getNextPrNumber());
const result = await prompts([
{
@@ -29,17 +29,17 @@ async function run() {
name: 'pullRequestNumber',
message: 'PR Number',
type: 'number',
initial: prNumber,
initial: initialPrNumber,
},
{
name: 'releaseNoteType',
message: 'Release Note Type',
type: 'select',
choices: [
{ title: 'Features', value: 'Features' },
{ title: 'Enhancements', value: 'Enhancements' },
{ title: 'Bugfix', value: 'Bugfix' },
{ title: 'Maintenance', value: 'Maintenance' },
{ title: 'Features', value: 'Features' },
{ title: '👍 Enhancements', value: 'Enhancements' },
{ title: '🐛 Bugfix', value: 'Bugfix' },
{ title: '⚙️ Maintenance', value: 'Maintenance' },
],
},
{
@@ -53,7 +53,8 @@ async function run() {
if (
!result.githubUsername ||
!result.oneLineSummary ||
!result.releaseNoteType
!result.releaseNoteType ||
!result.pullRequestNumber
) {
console.log('All questions must be answered. Exiting');
exit(1);
@@ -64,6 +65,7 @@ async function run() {
result.githubUsername,
result.oneLineSummary,
);
const prNumber = result.pullRequestNumber;
const filepath = `./upcoming-release-notes/${prNumber}.md`;
if (existsSync(filepath)) {
@@ -83,9 +85,7 @@ async function run() {
console.error('Failed to write release note file:', err);
exit(1);
} else {
console.log(
`Release note generated successfully: ./upcoming-release-notes/${prNumber}.md`,
);
console.log(`Release note generated successfully: ${filepath}`);
}
});
}

View File

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

View File

@@ -9,6 +9,7 @@ import pluginReact from 'eslint-plugin-react';
import pluginReactHooks from 'eslint-plugin-react-hooks';
import pluginRulesDir from 'eslint-plugin-rulesdir';
import pluginTypescript from 'typescript-eslint';
import pluginTypescriptPaths from 'eslint-plugin-typescript-paths';
import tsParser from '@typescript-eslint/parser';
@@ -84,8 +85,7 @@ const confusingBrowserGlobals = [
'top',
];
/** @type {import('eslint').Linter.Config[]} */
export default [
export default pluginTypescript.config(
{
ignores: [
'packages/api/app/bundle.api.js',
@@ -108,11 +108,10 @@ export default [
'packages/desktop-electron/client-build/',
'packages/desktop-electron/build/',
'packages/desktop-electron/dist/',
'packages/import-ynab4/**/node_modules/*',
'packages/import-ynab5/**/node_modules/*',
'packages/loot-core/**/node_modules/*',
'packages/loot-core/**/lib-dist/*',
'packages/loot-core/**/proto/*',
'packages/sync-server/build/',
'.yarn/*',
'.github/*',
],
@@ -120,8 +119,8 @@ export default [
{
// Temporary until the sync-server is migrated to TypeScript
files: [
'packages/sync-server/**/*.spec.js?(x)',
'packages/sync-server/**/*.test.js?(x)',
'packages/sync-server/**/*.spec.{js,jsx}',
'packages/sync-server/**/*.test.{js,jsx}',
],
languageOptions: {
globals: {
@@ -164,13 +163,14 @@ export default [
},
pluginReact.configs.flat.recommended,
pluginReact.configs.flat['jsx-runtime'],
...pluginTypescript.configs.recommended,
pluginTypescript.configs.recommended,
pluginImport.flatConfigs.recommended,
{
plugins: {
'react-hooks': pluginReactHooks,
'jsx-a11y': pluginJSXA11y,
rulesdir: pluginRulesDir,
'typescript-paths': pluginTypescriptPaths,
},
},
{
@@ -503,6 +503,32 @@ export default [
'no-restricted-imports': [
'warn',
{
paths: [
{
name: 'react-router',
importNames: ['useNavigate'],
message:
"Please import Actual's useNavigate() hook from `src/hooks` instead.",
},
{
name: 'react-redux',
importNames: ['useDispatch'],
message:
"Please import Actual's useDispatch() hook from `src/redux` instead.",
},
{
name: 'react-redux',
importNames: ['useSelector'],
message:
"Please import Actual's useSelector() hook from `src/redux` instead.",
},
{
name: 'react-redux',
importNames: ['useStore'],
message:
"Please import Actual's useStore() hook from `src/redux` instead.",
},
],
patterns: [
{
group: ['*.api', '*.web', '*.electron'],
@@ -518,6 +544,10 @@ export default [
importNames: ['colors'],
message: 'Please use themes instead of colors',
},
{
group: ['@actual-app/web/*'],
message: 'Please do not import `@actual-app/web` in `loot-core`',
},
],
},
],
@@ -539,7 +569,7 @@ export default [
},
},
{
files: ['**/*.ts?(x)'],
files: ['**/*.{ts,tsx}'],
languageOptions: {
parser: tsParser,
@@ -607,6 +637,16 @@ export default [
'@typescript-eslint/no-useless-constructor': 'warn',
},
},
{
files: ['packages/desktop-client/**/*.{js,ts,jsx,tsx}'],
rules: {
'typescript-paths/absolute-parent-import': [
'error',
{ preferPathOverBaseUrl: true },
],
'typescript-paths/absolute-import': ['error', { enableAlias: false }],
},
},
{
files: [
'packages/desktop-client/**/*.{ts,tsx}',
@@ -641,88 +681,6 @@ export default [
],
},
},
{
files: ['packages/desktop-client/**/*'],
ignores: ['packages/desktop-client/src/hooks/useNavigate.{ts,tsx}'],
rules: {
'no-restricted-imports': [
'warn',
{
paths: [
{
name: 'react-router-dom',
importNames: ['useNavigate'],
message:
"Please import Actual's useNavigate() hook from `src/hooks` instead.",
},
],
},
],
},
},
{
files: ['packages/desktop-client/**/*', 'packages/loot-core/**/*'],
ignores: ['packages/desktop-client/src/redux/index.{ts,tsx}'],
rules: {
'no-restricted-imports': [
'warn',
{
paths: [
{
name: 'react-redux',
importNames: ['useDispatch'],
message:
"Please import Actual's useDispatch() hook from `src/redux` instead.",
},
{
name: 'react-redux',
importNames: ['useSelector'],
message:
"Please import Actual's useSelector() hook from `src/redux` instead.",
},
{
name: 'react-redux',
importNames: ['useStore'],
message:
"Please import Actual's useStore() hook from `src/redux` instead.",
},
],
},
],
},
},
{
files: ['packages/loot-core/src/**/*'],
rules: {
'no-restricted-imports': [
'warn',
{
patterns: [
{
group: ['*.api', '*.web', '*.electron'],
message: "Don't directly reference imports from other platforms",
},
{
group: ['uuid'],
importNames: ['*'],
message: "Use `import { v4 as uuidv4 } from 'uuid'` instead",
},
{
group: ['loot-core/**'],
message:
'Please use relative imports in loot-core instead of importing from `loot-core/*`',
},
{
group: ['@actual-app/web/*'],
message: 'Please do not import `@actual-app/web` in `loot-core`',
},
],
},
],
},
},
{
files: [
'packages/loot-core/src/types/**/*',
@@ -737,27 +695,6 @@ export default [
'import/no-unused-modules': 'off',
},
},
{
files: [
'packages/desktop-client/src/style/index.*',
'packages/desktop-client/src/style/palette.*',
],
rules: {
'no-restricted-imports': [
'off',
{
patterns: [
{
group: ['**/style', '**/colors'],
importNames: ['colors'],
message: 'Please use themes instead of colors',
},
],
},
],
},
},
{
files: ['packages/api/migrations/*', 'packages/loot-core/migrations/*'],
@@ -872,4 +809,4 @@ export default [
'@typescript-eslint/no-unused-vars': 'off',
},
},
];
);

View File

@@ -31,7 +31,7 @@
"start:browser": "npm-run-all --parallel 'start:browser-*'",
"start:browser-backend": "yarn workspace loot-core watch:browser",
"start:browser-frontend": "yarn workspace @actual-app/web start:browser",
"build:server": "yarn build:browser",
"build:server": "yarn build:browser && yarn workspace @actual-app/sync-server build",
"build:browser": "./bin/package-browser",
"build:desktop": "./bin/package-electron",
"build:api": "yarn workspace @actual-app/api build",
@@ -53,45 +53,50 @@
"prepare": "husky"
},
"devDependencies": {
"@types/node": "^22.14.0",
"@octokit/rest": "^22.0.0",
"@types/node": "^22.15.18",
"@types/prompts": "^2.4.9",
"@typescript-eslint/parser": "^8.26.1",
"@typescript-eslint/parser": "^8.32.1",
"cross-env": "^7.0.3",
"eslint": "^9.22.0",
"eslint-config-prettier": "^10.1.1",
"eslint-import-resolver-typescript": "^4.2.2",
"eslint": "^9.27.0",
"eslint-config-prettier": "^10.1.5",
"eslint-import-resolver-typescript": "^4.3.5",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-rulesdir": "^0.2.2",
"globals": "^15.13.0",
"html-to-image": "^1.11.11",
"eslint-plugin-typescript-paths": "^0.0.33",
"globals": "^15.15.0",
"html-to-image": "^1.11.13",
"husky": "^9.1.7",
"lint-staged": "^15.5.0",
"node-jq": "^4.0.2",
"lint-staged": "^15.5.2",
"minimatch": "^10.0.3",
"node-jq": "^6.0.1",
"npm-run-all": "^4.1.5",
"prettier": "^3.5.3",
"prompts": "^2.4.2",
"source-map-support": "^0.5.21",
"ts-node": "^10.9.2",
"typescript": "^5.8.2",
"typescript-eslint": "^8.26.1",
"typescript": "^5.8.3",
"typescript-eslint": "^8.32.1",
"typescript-strict-plugin": "^2.4.4"
},
"resolutions": {
"rollup": "4.9.4"
"rollup": "4.40.1",
"socks": ">=2.8.3"
},
"engines": {
"node": ">=18.0.0"
"node": ">=20",
"yarn": "^4.9.1"
},
"lint-staged": {
"*.{js,jsx,ts,tsx,md,json,yml}": [
"*.{js,mjs,jsx,ts,tsx,md,json,yml}": [
"eslint --fix",
"prettier --write"
]
},
"packageManager": "yarn@4.7.0",
"packageManager": "yarn@4.9.1",
"browserslist": [
"electron 24.0",
"defaults"

View File

@@ -568,8 +568,20 @@ describe('API CRUD operations', () => {
const accountId = await api.createAccount({ name: 'test-account' }, 0);
let newTransaction = [
{ date: '2023-11-03', imported_id: '11', amount: 100, notes: 'notes' },
{ date: '2023-11-03', imported_id: '12', amount: 100, notes: '' },
{
account: accountId,
date: '2023-11-03',
imported_id: '11',
amount: 100,
notes: 'notes',
},
{
account: accountId,
date: '2023-11-03',
imported_id: '12',
amount: 100,
notes: '',
},
];
const addResult = await api.addTransactions(accountId, newTransaction, {
@@ -597,9 +609,27 @@ describe('API CRUD operations', () => {
expect(transactions).toHaveLength(2);
newTransaction = [
{ date: '2023-12-03', imported_id: '11', amount: 100, notes: 'notes' },
{ date: '2023-12-03', imported_id: '12', amount: 100, notes: 'notes' },
{ date: '2023-12-03', imported_id: '22', amount: 200, notes: '' },
{
account: accountId,
date: '2023-12-03',
imported_id: '11',
amount: 100,
notes: 'notes',
},
{
account: accountId,
date: '2023-12-03',
imported_id: '12',
amount: 100,
notes: 'notes',
},
{
account: accountId,
date: '2023-12-03',
imported_id: '22',
amount: 200,
notes: '',
},
];
const reconciled = await api.importTransactions(accountId, newTransaction);

View File

@@ -1,5 +1,6 @@
// @ts-strict-ignore
import type { Handlers } from 'loot-core/types/handlers';
import type { ImportTransactionEntity } from 'loot-core/types/models/import-transaction';
import * as injected from './injected';
@@ -98,8 +99,8 @@ export interface ImportTransactionsOpts {
}
export function importTransactions(
accountId,
transactions,
accountId: string,
transactions: ImportTransactionEntity[],
opts: ImportTransactionsOpts = {
defaultCleared: true,
},

View File

@@ -1,10 +1,10 @@
{
"name": "@actual-app/api",
"version": "25.5.0",
"version": "25.7.0",
"license": "MIT",
"description": "An API for Actual",
"engines": {
"node": ">=18.12.0"
"node": ">=20"
},
"main": "dist/index.js",
"types": "@types/index.d.ts",
@@ -24,15 +24,14 @@
},
"dependencies": {
"@actual-app/crdt": "workspace:^",
"better-sqlite3": "^11.9.1",
"better-sqlite3": "^11.10.0",
"compare-versions": "^6.1.1",
"node-fetch": "^3.3.2",
"uuid": "^9.0.1"
"uuid": "^11.1.0"
},
"devDependencies": {
"@types/uuid": "^9.0.8",
"tsc-alias": "^1.8.11",
"typescript": "^5.8.2",
"vitest": "^3.0.2"
"tsc-alias": "^1.8.16",
"typescript": "^5.8.3",
"vitest": "^3.2.4"
}
}

View File

@@ -11,7 +11,7 @@
"outDir": "dist",
"declarationDir": "@types",
"paths": {
"loot-core/*": ["./@types/loot-core/*"]
"loot-core/*": ["./@types/loot-core/src/*"]
}
},
"include": ["."],

View File

@@ -8,14 +8,15 @@
},
"dependencies": {
"@emotion/css": "^11.13.5",
"react-aria-components": "^1.7.1",
"react-aria-components": "^1.8.0",
"usehooks-ts": "^3.1.1"
},
"devDependencies": {
"@svgr/cli": "^8.1.0",
"@types/react": "^19.1.0",
"@types/react": "^19.1.4",
"react": "19.1.0",
"react-dom": "19.1.0"
"react-dom": "19.1.0",
"vitest": "^3.2.4"
},
"exports": {
"./hooks/*": "./src/hooks/*.ts",
@@ -50,6 +51,8 @@
"./view": "./src/View.tsx"
},
"scripts": {
"generate:icons": "rm src/icons/*/*.tsx; cd src/icons && svgr --template template.ts --index-template index-template.ts --typescript --expand-props start -d . ."
"generate:icons": "rm src/icons/*/*.tsx; cd src/icons && svgr --template template.ts --index-template index-template.ts --typescript --expand-props start -d . .",
"test": "npm-run-all -cp 'test:*'",
"test:web": "ENV=web vitest -c vitest.web.config.ts"
}
}

View File

@@ -7,7 +7,7 @@ import React, {
} from 'react';
import { Button as ReactAriaButton } from 'react-aria-components';
import { css } from '@emotion/css';
import { css, cx } from '@emotion/css';
import { AnimatedLoading } from './icons/AnimatedLoading';
import { styles } from './styles';
@@ -145,26 +145,24 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
const defaultButtonClassName: string = useMemo(
() =>
String(
css({
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
padding: _getPadding(variant),
margin: 0,
overflow: 'hidden',
display: 'flex',
borderRadius: 4,
backgroundColor: backgroundColor[variantWithDisabled],
border: _getBorder(variant, variantWithDisabled),
color: textColor[variantWithDisabled],
transition: 'box-shadow .25s',
WebkitAppRegion: 'no-drag',
...styles.smallText,
'&[data-hovered]': _getHoveredStyles(variant),
'&[data-pressed]': _getActiveStyles(variant, bounce),
}),
),
css({
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
padding: _getPadding(variant),
margin: 0,
overflow: 'hidden',
display: 'flex',
borderRadius: 4,
backgroundColor: backgroundColor[variantWithDisabled],
border: _getBorder(variant, variantWithDisabled),
color: textColor[variantWithDisabled],
transition: 'box-shadow .25s',
WebkitAppRegion: 'no-drag',
...styles.smallText,
'&[data-hovered]': _getHoveredStyles(variant),
'&[data-pressed]': _getActiveStyles(variant, bounce),
}),
[bounce, variant, variantWithDisabled],
);
@@ -176,9 +174,8 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
{...restProps}
className={
typeof className === 'function'
? renderProps =>
`${defaultButtonClassName} ${className(renderProps)}`
: `${defaultButtonClassName} ${className || ''}`
? renderProps => cx(defaultButtonClassName, className(renderProps))
: cx(defaultButtonClassName, className)
}
>
{children}

View File

@@ -1,36 +1,59 @@
import {
type ReactElement,
type Ref,
Children,
cloneElement,
isValidElement,
type ReactElement,
Ref,
useEffect,
useRef,
} from 'react';
type InitialFocusProps = {
children:
| ReactElement<{ inputRef: Ref<HTMLInputElement> }>
| ((node: Ref<HTMLInputElement>) => ReactElement);
type InitialFocusProps<T extends HTMLElement> = {
/**
* The child element to focus when the component mounts. This can be either a single React element or a function that returns a React element.
*/
children: ReactElement<{ ref: Ref<T> }> | ((ref: Ref<T>) => ReactElement);
};
export function InitialFocus({ children }: InitialFocusProps) {
const node = useRef<HTMLInputElement>(null);
/**
* InitialFocus sets focus on its child element
* when it mounts.
* @param {Object} props - The component props.
* @param {ReactElement | function} props.children - A single React element or a function that returns a React element.
*/
export function InitialFocus<T extends HTMLElement = HTMLElement>({
children,
}: InitialFocusProps<T>) {
const ref = useRef<T | null>(null);
useEffect(() => {
if (node.current) {
if (ref.current) {
// This is needed to avoid a strange interaction with
// `ScopeTab`, which doesn't allow it to be focused at first for
// some reason. Need to look into it.
setTimeout(() => {
if (node.current) {
node.current.focus();
node.current.setSelectionRange(0, 10000);
if (ref.current) {
ref.current.focus();
if (
ref.current instanceof HTMLInputElement ||
ref.current instanceof HTMLTextAreaElement
) {
ref.current.setSelectionRange(0, 10000);
}
}
}, 0);
}
}, []);
if (typeof children === 'function') {
return children(node);
return children(ref);
}
return cloneElement(children, { inputRef: node });
const child = Children.only(children);
if (isValidElement(child)) {
return cloneElement(child, { ref });
}
throw new Error(
'InitialFocus expects a single valid React element as its child.',
);
}

View File

@@ -0,0 +1,117 @@
import * as React from 'react';
import { forwardRef, Ref } from 'react';
import { render } from '@testing-library/react';
import { InitialFocus } from './InitialFocus';
import { View } from './View';
describe('InitialFocus', () => {
it('should focus a text input', async () => {
const component = render(
<View>
<InitialFocus>
<input type="text" title="focused" />
</InitialFocus>
<input type="text" title="unfocused" />
</View>,
);
// This is needed bc of the `setTimeout` in the `InitialFocus` component.
await new Promise(resolve => setTimeout(resolve, 0));
const input = component.getByTitle('focused') as HTMLInputElement;
const unfocusedInput = component.getByTitle(
'unfocused',
) as HTMLInputElement;
expect(document.activeElement).toBe(input);
expect(document.activeElement).not.toBe(unfocusedInput);
});
it('should focus a textarea', async () => {
const component = render(
<View>
<InitialFocus>
<textarea title="focused" />
</InitialFocus>
<textarea title="unfocused" />
</View>,
);
// This is needed bc of the `setTimeout` in the `InitialFocus` component.
await new Promise(resolve => setTimeout(resolve, 0));
const textarea = component.getByTitle('focused') as HTMLTextAreaElement;
const unfocusedTextarea = component.getByTitle(
'unfocused',
) as HTMLTextAreaElement;
expect(document.activeElement).toBe(textarea);
expect(document.activeElement).not.toBe(unfocusedTextarea);
});
it('should select text in an input', async () => {
const component = render(
<View>
<InitialFocus>
<input type="text" title="focused" defaultValue="Hello World" />
</InitialFocus>
<input type="text" title="unfocused" />
</View>,
);
// This is needed bc of the `setTimeout` in the `InitialFocus` component.
await new Promise(resolve => setTimeout(resolve, 0));
const input = component.getByTitle('focused') as HTMLInputElement;
expect(document.activeElement).toBe(input);
expect(input.selectionStart).toBe(0);
expect(input.selectionEnd).toBe(11); // Length of "Hello World"
});
it('should focus a button', async () => {
const component = render(
<View>
<InitialFocus>
<button title="focused">Click me</button>
</InitialFocus>
<button title="unfocused">Do not click me</button>
</View>,
);
// This is needed bc of the `setTimeout` in the `InitialFocus` component.
await new Promise(resolve => setTimeout(resolve, 0));
const button = component.getByTitle('focused') as HTMLButtonElement;
const unfocusedButton = component.getByTitle(
'unfocused',
) as HTMLButtonElement;
expect(document.activeElement).toBe(button);
expect(document.activeElement).not.toBe(unfocusedButton);
});
it('should focus a custom component with ref forwarding', async () => {
const CustomInput = forwardRef<HTMLInputElement>((props, ref) => (
<input type="text" ref={ref} {...props} title="focused" />
));
CustomInput.displayName = 'CustomInput';
const component = render(
<View>
<InitialFocus>
{node => <CustomInput ref={node as Ref<HTMLInputElement>} />}
</InitialFocus>
<input type="text" title="unfocused" />
</View>,
);
// This is needed bc of the `setTimeout` in the `InitialFocus` component.
await new Promise(resolve => setTimeout(resolve, 0));
const input = component.getByTitle('focused') as HTMLInputElement;
const unfocusedInput = component.getByTitle(
'unfocused',
) as HTMLInputElement;
expect(document.activeElement).toBe(input);
expect(document.activeElement).not.toBe(unfocusedInput);
});
});

View File

@@ -1,16 +1,18 @@
import React, {
type InputHTMLAttributes,
ChangeEvent,
ComponentPropsWithRef,
type KeyboardEvent,
type Ref,
type FocusEvent,
} from 'react';
import { Input as ReactAriaInput } from 'react-aria-components';
import { css, cx } from '@emotion/css';
import { useResponsive } from './hooks/useResponsive';
import { styles, type CSSProperties } from './styles';
import { styles } from './styles';
import { theme } from './theme';
export const defaultInputStyle = {
export const baseInputStyle = {
outline: 0,
backgroundColor: theme.tableBackground,
color: theme.formInputText,
@@ -20,85 +22,91 @@ export const defaultInputStyle = {
border: '1px solid ' + theme.formInputBorder,
};
export type InputProps = InputHTMLAttributes<HTMLInputElement> & {
style?: CSSProperties;
inputRef?: Ref<HTMLInputElement>;
onEnter?: (event: KeyboardEvent<HTMLInputElement>) => void;
onEscape?: (event: KeyboardEvent<HTMLInputElement>) => void;
onChangeValue?: (newValue: string) => void;
onUpdate?: (newValue: string) => void;
const defaultInputClassName = css({
...baseInputStyle,
color: theme.formInputText,
whiteSpace: 'nowrap',
overflow: 'hidden',
flexShrink: 0,
'&[data-focused]': {
border: '1px solid ' + theme.formInputBorderSelected,
boxShadow: '0 1px 1px ' + theme.formInputShadowSelected,
},
'&[data-disabled]': {
color: theme.formInputTextPlaceholder,
},
'::placeholder': { color: theme.formInputTextPlaceholder },
...styles.smallText,
});
export type InputProps = ComponentPropsWithRef<typeof ReactAriaInput> & {
onEnter?: (value: string, event: KeyboardEvent<HTMLInputElement>) => void;
onEscape?: (value: string, event: KeyboardEvent<HTMLInputElement>) => void;
onChangeValue?: (
newValue: string,
event: ChangeEvent<HTMLInputElement>,
) => void;
onUpdate?: (newValue: string, event: FocusEvent<HTMLInputElement>) => void;
};
export function Input({
style,
inputRef,
ref,
onEnter,
onEscape,
onChangeValue,
onUpdate,
className,
...nativeProps
...props
}: InputProps) {
return (
<input
ref={inputRef}
className={cx(
css(
defaultInputStyle,
{
color: nativeProps.disabled
? theme.formInputTextPlaceholder
: theme.formInputText,
whiteSpace: 'nowrap',
overflow: 'hidden',
flexShrink: 0,
':focus': {
border: '1px solid ' + theme.formInputBorderSelected,
boxShadow: '0 1px 1px ' + theme.formInputShadowSelected,
},
'::placeholder': { color: theme.formInputTextPlaceholder },
},
styles.smallText,
style,
),
className,
)}
{...nativeProps}
onKeyDown={e => {
nativeProps.onKeyDown?.(e);
<ReactAriaInput
ref={ref}
className={
typeof className === 'function'
? renderProps => cx(defaultInputClassName, className(renderProps))
: cx(defaultInputClassName, className)
}
{...props}
onKeyUp={e => {
props.onKeyUp?.(e);
if (e.key === 'Enter' && onEnter) {
onEnter(e);
onEnter(e.currentTarget.value, e);
}
if (e.key === 'Escape' && onEscape) {
onEscape(e);
onEscape(e.currentTarget.value, e);
}
}}
onBlur={e => {
onUpdate?.(e.target.value);
nativeProps.onBlur?.(e);
onUpdate?.(e.currentTarget.value, e);
props.onBlur?.(e);
}}
onChange={e => {
onChangeValue?.(e.target.value);
nativeProps.onChange?.(e);
onChangeValue?.(e.currentTarget.value, e);
props.onChange?.(e);
}}
/>
);
}
export function BigInput(props: InputProps) {
const defaultBigInputClassName = css({
padding: 10,
fontSize: 15,
border: 'none',
...styles.shadow,
'&[data-focused]': { border: 'none', ...styles.shadow },
});
export function BigInput({ className, ...props }: InputProps) {
return (
<Input
{...props}
style={{
padding: 10,
fontSize: 15,
border: 'none',
...styles.shadow,
':focus': { border: 'none', ...styles.shadow },
...props.style,
}}
className={
typeof className === 'function'
? renderProps => cx(defaultBigInputClassName, className(renderProps))
: cx(defaultBigInputClassName, className)
}
/>
);
}

View File

@@ -0,0 +1,35 @@
import path from 'path';
import peggyLoader from 'vite-plugin-peggy-loader';
import { defineConfig } from 'vitest/config';
const resolveExtensions = [
'.testing.ts',
'.web.ts',
'.mjs',
'.js',
'.mts',
'.ts',
'.jsx',
'.tsx',
'.json',
'.wasm',
];
export default defineConfig({
test: {
environment: 'jsdom',
globals: true,
include: ['src/**/*.web.test.(js|jsx|ts|tsx)'],
},
resolve: {
alias: [
{
find: /^@actual-app\/crdt(\/.*)?$/,
replacement: path.resolve('../../../crdt/src$1'),
},
],
extensions: resolveExtensions,
},
plugins: [peggyLoader()],
});

View File

@@ -15,14 +15,13 @@
"test": "vitest --globals"
},
"dependencies": {
"google-protobuf": "^3.12.4",
"google-protobuf": "^3.21.4",
"murmurhash": "^2.0.1",
"uuid": "^9.0.1"
"uuid": "^11.1.0"
},
"devDependencies": {
"@types/uuid": "^9.0.8",
"ts-protoc-gen": "^0.15.0",
"typescript": "^5.8.2",
"vitest": "^3.0.2"
"typescript": "^5.8.3",
"vitest": "^3.2.4"
}
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 30 KiB

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