diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index b6d881e7731..c61a9eedf23 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -23,8 +23,6 @@ body: options: - label: 'I have searched and found no existing issue' required: true - - label: 'I will be providing steps how to reproduce the bug (in most cases this will also mean uploading a demo budget file)' - required: true validations: required: true - type: textarea @@ -36,6 +34,14 @@ body: value: 'A bug happened!' validations: required: true + - type: textarea + id: reproduction + attributes: + label: How can we reproduce the issue? + description: Please give step-by-step instructions on how to reproduce the issue. In most cases this might also require uploading a sample budget/import file. + value: 'How can we reproduce the issue?' + validations: + required: true - type: markdown id: env-info attributes: diff --git a/packages/desktop-client/e2e/accounts.mobile.test.js-snapshots/Mobile-Accounts-opens-the-accounts-page-and-asserts-on-balances-1-chromium-linux.png b/packages/desktop-client/e2e/accounts.mobile.test.js-snapshots/Mobile-Accounts-opens-the-accounts-page-and-asserts-on-balances-1-chromium-linux.png index 568ed03a9dd..83b5875ce5b 100644 Binary files a/packages/desktop-client/e2e/accounts.mobile.test.js-snapshots/Mobile-Accounts-opens-the-accounts-page-and-asserts-on-balances-1-chromium-linux.png and b/packages/desktop-client/e2e/accounts.mobile.test.js-snapshots/Mobile-Accounts-opens-the-accounts-page-and-asserts-on-balances-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/accounts.mobile.test.js-snapshots/Mobile-Accounts-opens-the-accounts-page-and-asserts-on-balances-2-chromium-linux.png b/packages/desktop-client/e2e/accounts.mobile.test.js-snapshots/Mobile-Accounts-opens-the-accounts-page-and-asserts-on-balances-2-chromium-linux.png index 51bb6f2433d..fd439e6c7a8 100644 Binary files a/packages/desktop-client/e2e/accounts.mobile.test.js-snapshots/Mobile-Accounts-opens-the-accounts-page-and-asserts-on-balances-2-chromium-linux.png and b/packages/desktop-client/e2e/accounts.mobile.test.js-snapshots/Mobile-Accounts-opens-the-accounts-page-and-asserts-on-balances-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/accounts.mobile.test.js-snapshots/Mobile-Accounts-opens-the-accounts-page-and-asserts-on-balances-3-chromium-linux.png b/packages/desktop-client/e2e/accounts.mobile.test.js-snapshots/Mobile-Accounts-opens-the-accounts-page-and-asserts-on-balances-3-chromium-linux.png index 7577f513fc6..086e56d84a0 100644 Binary files a/packages/desktop-client/e2e/accounts.mobile.test.js-snapshots/Mobile-Accounts-opens-the-accounts-page-and-asserts-on-balances-3-chromium-linux.png and b/packages/desktop-client/e2e/accounts.mobile.test.js-snapshots/Mobile-Accounts-opens-the-accounts-page-and-asserts-on-balances-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/accounts.test.js b/packages/desktop-client/e2e/accounts.test.js index 5c9118172d6..dfe9cb47506 100644 --- a/packages/desktop-client/e2e/accounts.test.js +++ b/packages/desktop-client/e2e/accounts.test.js @@ -54,17 +54,17 @@ test.describe('Accounts', () => { await expect(page).toMatchThemeScreenshots(); }); - test.describe('Budgeted Accounts', () => { + test.describe('On Budget Accounts', () => { // Reset filters test.afterEach(async () => { await accountPage.removeFilter(0); }); test('creates a transfer from two existing transactions', async () => { - accountPage = await navigation.goToAccountPage('For budget'); + accountPage = await navigation.goToAccountPage('On budget'); await accountPage.waitFor(); - await expect(accountPage.accountName).toHaveText('Budgeted Accounts'); + await expect(accountPage.accountName).toHaveText('On Budget Accounts'); await accountPage.filterByNote('Test Acc Transfer'); diff --git a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-Import-Transactions-import-csv-file-twice-1-chromium-linux.png b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-Import-Transactions-import-csv-file-twice-1-chromium-linux.png index cc58f7f29ec..75f6d9468b1 100644 Binary files a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-Import-Transactions-import-csv-file-twice-1-chromium-linux.png and b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-Import-Transactions-import-csv-file-twice-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-Import-Transactions-import-csv-file-twice-2-chromium-linux.png b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-Import-Transactions-import-csv-file-twice-2-chromium-linux.png index 6e9e713d6ce..019872cae44 100644 Binary files a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-Import-Transactions-import-csv-file-twice-2-chromium-linux.png and b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-Import-Transactions-import-csv-file-twice-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-Import-Transactions-import-csv-file-twice-3-chromium-linux.png b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-Import-Transactions-import-csv-file-twice-3-chromium-linux.png index e7c52403655..2b339523965 100644 Binary files a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-Import-Transactions-import-csv-file-twice-3-chromium-linux.png and b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-Import-Transactions-import-csv-file-twice-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-Import-Transactions-imports-transactions-from-a-CSV-file-1-chromium-linux.png b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-Import-Transactions-imports-transactions-from-a-CSV-file-1-chromium-linux.png index 28fb5fc075a..93ad61b9bdb 100644 Binary files a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-Import-Transactions-imports-transactions-from-a-CSV-file-1-chromium-linux.png and b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-Import-Transactions-imports-transactions-from-a-CSV-file-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-Import-Transactions-imports-transactions-from-a-CSV-file-2-chromium-linux.png b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-Import-Transactions-imports-transactions-from-a-CSV-file-2-chromium-linux.png index d9b6c60afff..510fa9bf584 100644 Binary files a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-Import-Transactions-imports-transactions-from-a-CSV-file-2-chromium-linux.png and b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-Import-Transactions-imports-transactions-from-a-CSV-file-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-Import-Transactions-imports-transactions-from-a-CSV-file-3-chromium-linux.png b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-Import-Transactions-imports-transactions-from-a-CSV-file-3-chromium-linux.png index faed13bee17..42fa79651d9 100644 Binary files a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-Import-Transactions-imports-transactions-from-a-CSV-file-3-chromium-linux.png and b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-Import-Transactions-imports-transactions-from-a-CSV-file-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-closes-an-account-1-chromium-linux.png b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-closes-an-account-1-chromium-linux.png index e322b3a0110..9ace9002d92 100644 Binary files a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-closes-an-account-1-chromium-linux.png and b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-closes-an-account-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-closes-an-account-2-chromium-linux.png b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-closes-an-account-2-chromium-linux.png index c99575be0b1..485cd1f4285 100644 Binary files a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-closes-an-account-2-chromium-linux.png and b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-closes-an-account-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-closes-an-account-3-chromium-linux.png b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-closes-an-account-3-chromium-linux.png index 13ad4f0f9e7..f4ef788f1e9 100644 Binary files a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-closes-an-account-3-chromium-linux.png and b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-closes-an-account-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-closes-an-account-4-chromium-linux.png b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-closes-an-account-4-chromium-linux.png index bf4acbee5ba..c8bc05a7e86 100644 Binary files a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-closes-an-account-4-chromium-linux.png and b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-closes-an-account-4-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-closes-an-account-5-chromium-linux.png b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-closes-an-account-5-chromium-linux.png index d0d14d084d3..f545dced181 100644 Binary files a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-closes-an-account-5-chromium-linux.png and b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-closes-an-account-5-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-closes-an-account-6-chromium-linux.png b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-closes-an-account-6-chromium-linux.png index 00ed14f1d70..c9633902c80 100644 Binary files a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-closes-an-account-6-chromium-linux.png and b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-closes-an-account-6-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-creates-a-new-account-and-views-the-initial-balance-transaction-1-chromium-linux.png b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-creates-a-new-account-and-views-the-initial-balance-transaction-1-chromium-linux.png index ff0f29ec92a..4b09a36ab91 100644 Binary files a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-creates-a-new-account-and-views-the-initial-balance-transaction-1-chromium-linux.png and b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-creates-a-new-account-and-views-the-initial-balance-transaction-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-creates-a-new-account-and-views-the-initial-balance-transaction-2-chromium-linux.png b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-creates-a-new-account-and-views-the-initial-balance-transaction-2-chromium-linux.png index f98acb71ce2..71117da939c 100644 Binary files a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-creates-a-new-account-and-views-the-initial-balance-transaction-2-chromium-linux.png and b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-creates-a-new-account-and-views-the-initial-balance-transaction-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-creates-a-new-account-and-views-the-initial-balance-transaction-3-chromium-linux.png b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-creates-a-new-account-and-views-the-initial-balance-transaction-3-chromium-linux.png index b943624386b..9bf30abe716 100644 Binary files a/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-creates-a-new-account-and-views-the-initial-balance-transaction-3-chromium-linux.png and b/packages/desktop-client/e2e/accounts.test.js-snapshots/Accounts-creates-a-new-account-and-views-the-initial-balance-transaction-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Envelope-checks-that-clicking--321fd-ed-amount-opens-the-budget-summary-menu-modal-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Envelope-checks-that-clicking--321fd-ed-amount-opens-the-budget-summary-menu-modal-2-chromium-linux.png index b31314a5707..4e6252fec76 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Envelope-checks-that-clicking--321fd-ed-amount-opens-the-budget-summary-menu-modal-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Envelope-checks-that-clicking--321fd-ed-amount-opens-the-budget-summary-menu-modal-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Envelope-checks-that-clicking--4bb70-ed-amount-opens-the-budget-summary-menu-modal-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Envelope-checks-that-clicking--4bb70-ed-amount-opens-the-budget-summary-menu-modal-1-chromium-linux.png index f400d85a126..65c60199b30 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Envelope-checks-that-clicking--4bb70-ed-amount-opens-the-budget-summary-menu-modal-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Envelope-checks-that-clicking--4bb70-ed-amount-opens-the-budget-summary-menu-modal-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Envelope-checks-that-clicking--94a85-ed-amount-opens-the-budget-summary-menu-modal-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Envelope-checks-that-clicking--94a85-ed-amount-opens-the-budget-summary-menu-modal-3-chromium-linux.png index 08b9252de66..55a7c2da4a5 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Envelope-checks-that-clicking--94a85-ed-amount-opens-the-budget-summary-menu-modal-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Envelope-checks-that-clicking--94a85-ed-amount-opens-the-budget-summary-menu-modal-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--0ba04-nt-amount-opens-the-budget-summary-menu-modal-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--0ba04-nt-amount-opens-the-budget-summary-menu-modal-1-chromium-linux.png index ce81ed35619..888b559f45d 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--0ba04-nt-amount-opens-the-budget-summary-menu-modal-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--0ba04-nt-amount-opens-the-budget-summary-menu-modal-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--1ce6d-nt-amount-opens-the-budget-summary-menu-modal-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--1ce6d-nt-amount-opens-the-budget-summary-menu-modal-2-chromium-linux.png index 99485f83c0f..938fece6a6f 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--1ce6d-nt-amount-opens-the-budget-summary-menu-modal-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--1ce6d-nt-amount-opens-the-budget-summary-menu-modal-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--42062-in-the-page-header-opens-the-month-menu-modal-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--42062-in-the-page-header-opens-the-month-menu-modal-2-chromium-linux.png index 6dac5c567f2..8771e50c51a 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--42062-in-the-page-header-opens-the-month-menu-modal-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--42062-in-the-page-header-opens-the-month-menu-modal-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--49fb6-in-the-page-header-opens-the-month-menu-modal-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--49fb6-in-the-page-header-opens-the-month-menu-modal-3-chromium-linux.png index 0f9398c3873..73889255c41 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--49fb6-in-the-page-header-opens-the-month-menu-modal-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--49fb6-in-the-page-header-opens-the-month-menu-modal-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--5f098-roup-name-opens-the-category-group-menu-modal-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--5f098-roup-name-opens-the-category-group-menu-modal-1-chromium-linux.png index b3dced3da41..c4d46a75fd6 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--5f098-roup-name-opens-the-category-group-menu-modal-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--5f098-roup-name-opens-the-category-group-menu-modal-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--7c353-the-page-header-shows-the-next-month-s-budget-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--7c353-the-page-header-shows-the-next-month-s-budget-3-chromium-linux.png index 64ccf3b3e34..16b144855b5 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--7c353-the-page-header-shows-the-next-month-s-budget-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--7c353-the-page-header-shows-the-next-month-s-budget-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--929be-roup-name-opens-the-category-group-menu-modal-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--929be-roup-name-opens-the-category-group-menu-modal-3-chromium-linux.png index 6d8cb23a464..7bbb28410fb 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--929be-roup-name-opens-the-category-group-menu-modal-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--929be-roup-name-opens-the-category-group-menu-modal-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--a3783-in-the-page-header-opens-the-budget-page-menu-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--a3783-in-the-page-header-opens-the-budget-page-menu-1-chromium-linux.png index 0eb679a919e..1b22662391e 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--a3783-in-the-page-header-opens-the-budget-page-menu-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--a3783-in-the-page-header-opens-the-budget-page-menu-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--a8b5e-in-the-page-header-opens-the-budget-page-menu-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--a8b5e-in-the-page-header-opens-the-budget-page-menu-3-chromium-linux.png index 70175ca8492..cf602b52a12 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--a8b5e-in-the-page-header-opens-the-budget-page-menu-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--a8b5e-in-the-page-header-opens-the-budget-page-menu-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--b1562-in-the-page-header-opens-the-month-menu-modal-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--b1562-in-the-page-header-opens-the-month-menu-modal-1-chromium-linux.png index 6443198c425..b16c2a13db2 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--b1562-in-the-page-header-opens-the-month-menu-modal-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--b1562-in-the-page-header-opens-the-month-menu-modal-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--d5af6-the-page-header-shows-the-next-month-s-budget-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--d5af6-the-page-header-shows-the-next-month-s-budget-1-chromium-linux.png index b044c08e50b..74b81149829 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--d5af6-the-page-header-shows-the-next-month-s-budget-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--d5af6-the-page-header-shows-the-next-month-s-budget-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--dc927-roup-name-opens-the-category-group-menu-modal-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--dc927-roup-name-opens-the-category-group-menu-modal-2-chromium-linux.png index d43e779a9ce..2aeeadb6513 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--dc927-roup-name-opens-the-category-group-menu-modal-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--dc927-roup-name-opens-the-category-group-menu-modal-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--f2198-the-page-header-shows-the-next-month-s-budget-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--f2198-the-page-header-shows-the-next-month-s-budget-2-chromium-linux.png index 3f2e3397c1a..ccfa385f2f1 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--f2198-the-page-header-shows-the-next-month-s-budget-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--f2198-the-page-header-shows-the-next-month-s-budget-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--f224f-nt-amount-opens-the-budget-summary-menu-modal-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--f224f-nt-amount-opens-the-budget-summary-menu-modal-3-chromium-linux.png index 6bb02c428c4..6c93af74f17 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--f224f-nt-amount-opens-the-budget-summary-menu-modal-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--f224f-nt-amount-opens-the-budget-summary-menu-modal-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--f8a19-in-the-page-header-opens-the-budget-page-menu-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--f8a19-in-the-page-header-opens-the-budget-page-menu-2-chromium-linux.png index 2660f55f70e..ac5fb95ee8c 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--f8a19-in-the-page-header-opens-the-budget-page-menu-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking--f8a19-in-the-page-header-opens-the-budget-page-menu-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-balance-cell-opens-the-balance-menu-modal-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-balance-cell-opens-the-balance-menu-modal-1-chromium-linux.png index 839ef4511b3..69b5304e0d0 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-balance-cell-opens-the-balance-menu-modal-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-balance-cell-opens-the-balance-menu-modal-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-balance-cell-opens-the-balance-menu-modal-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-balance-cell-opens-the-balance-menu-modal-2-chromium-linux.png index 5817e79f764..b58968edc91 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-balance-cell-opens-the-balance-menu-modal-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-balance-cell-opens-the-balance-menu-modal-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-balance-cell-opens-the-balance-menu-modal-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-balance-cell-opens-the-balance-menu-modal-3-chromium-linux.png index 6c1c349ba81..5d5c9e88589 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-balance-cell-opens-the-balance-menu-modal-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-balance-cell-opens-the-balance-menu-modal-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-budgeted-cell-opens-the-budget-menu-modal-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-budgeted-cell-opens-the-budget-menu-modal-1-chromium-linux.png index 12415a1b4cf..d47bb949f4f 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-budgeted-cell-opens-the-budget-menu-modal-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-budgeted-cell-opens-the-budget-menu-modal-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-budgeted-cell-opens-the-budget-menu-modal-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-budgeted-cell-opens-the-budget-menu-modal-2-chromium-linux.png index 3036ffbf68f..0fedc45eb04 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-budgeted-cell-opens-the-budget-menu-modal-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-budgeted-cell-opens-the-budget-menu-modal-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-budgeted-cell-opens-the-budget-menu-modal-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-budgeted-cell-opens-the-budget-menu-modal-3-chromium-linux.png index 79274e17487..40d0b0f408a 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-budgeted-cell-opens-the-budget-menu-modal-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-budgeted-cell-opens-the-budget-menu-modal-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-category-name-opens-the-category-menu-modal-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-category-name-opens-the-category-menu-modal-1-chromium-linux.png index 5df130f15ee..6c748af4cd7 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-category-name-opens-the-category-menu-modal-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-category-name-opens-the-category-menu-modal-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-category-name-opens-the-category-menu-modal-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-category-name-opens-the-category-menu-modal-2-chromium-linux.png index 4195428bb4e..89b5a6cd2c9 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-category-name-opens-the-category-menu-modal-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-category-name-opens-the-category-menu-modal-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-category-name-opens-the-category-menu-modal-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-category-name-opens-the-category-menu-modal-3-chromium-linux.png index 76fe25abd17..a23bf489f0e 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-category-name-opens-the-category-menu-modal-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-category-name-opens-the-category-menu-modal-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png index bb0ac8bbf88..9b379af8aa4 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png index 631e8256296..0ed4936a89c 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-loads-the-budget-page-with-budgeted-amounts-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-loads-the-budget-page-with-budgeted-amounts-3-chromium-linux.png index b662506066c..cf697f2f37b 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-loads-the-budget-page-with-budgeted-amounts-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-loads-the-budget-page-with-budgeted-amounts-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-updates-the-budgeted-amount-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-updates-the-budgeted-amount-1-chromium-linux.png index a6c178b7a6c..bf758ff1e6b 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-updates-the-budgeted-amount-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-updates-the-budgeted-amount-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-updates-the-budgeted-amount-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-updates-the-budgeted-amount-2-chromium-linux.png index 38c4c7b2b1e..19d4e56003a 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-updates-the-budgeted-amount-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-updates-the-budgeted-amount-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-updates-the-budgeted-amount-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-updates-the-budgeted-amount-3-chromium-linux.png index 7fc874ece94..f9c38810b2e 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-updates-the-budgeted-amount-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.js-snapshots/Mobile-Budget-Tracking-updates-the-budgeted-amount-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.test.js b/packages/desktop-client/e2e/budget.test.js index 3a2df2e45fc..bb256c3beca 100644 --- a/packages/desktop-client/e2e/budget.test.js +++ b/packages/desktop-client/e2e/budget.test.js @@ -27,12 +27,12 @@ test.describe('Budget', () => { test('renders the summary information: available funds, overspent, budgeted and for next month', async () => { const summary = budgetPage.budgetSummary.first(); - await expect(summary.getByText('Available Funds')).toBeVisible({ + await expect(summary.getByText('Available funds')).toBeVisible({ timeout: 10000, }); await expect(summary.getByText(/^Overspent in /)).toBeVisible(); await expect(summary.getByText('Budgeted')).toBeVisible(); - await expect(summary.getByText('For Next Month')).toBeVisible(); + await expect(summary.getByText('For next month')).toBeVisible(); await expect(page).toMatchThemeScreenshots(); }); diff --git a/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-renders-the-summary-information-available-funds-overspent-budgeted-and-for-next-month-1-chromium-linux.png b/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-renders-the-summary-information-available-funds-overspent-budgeted-and-for-next-month-1-chromium-linux.png index 8f6e6c82469..d325f3ee5f3 100644 Binary files a/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-renders-the-summary-information-available-funds-overspent-budgeted-and-for-next-month-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-renders-the-summary-information-available-funds-overspent-budgeted-and-for-next-month-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-renders-the-summary-information-available-funds-overspent-budgeted-and-for-next-month-2-chromium-linux.png b/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-renders-the-summary-information-available-funds-overspent-budgeted-and-for-next-month-2-chromium-linux.png index e97f6f72826..20c9ddaa8eb 100644 Binary files a/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-renders-the-summary-information-available-funds-overspent-budgeted-and-for-next-month-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-renders-the-summary-information-available-funds-overspent-budgeted-and-for-next-month-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-renders-the-summary-information-available-funds-overspent-budgeted-and-for-next-month-3-chromium-linux.png b/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-renders-the-summary-information-available-funds-overspent-budgeted-and-for-next-month-3-chromium-linux.png index b4b0b86bf94..fc7aa791584 100644 Binary files a/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-renders-the-summary-information-available-funds-overspent-budgeted-and-for-next-month-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-renders-the-summary-information-available-funds-overspent-budgeted-and-for-next-month-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-transfer-funds-to-another-category-1-chromium-linux.png b/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-transfer-funds-to-another-category-1-chromium-linux.png index a1890d819e9..c29a9743f9e 100644 Binary files a/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-transfer-funds-to-another-category-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-transfer-funds-to-another-category-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-transfer-funds-to-another-category-2-chromium-linux.png b/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-transfer-funds-to-another-category-2-chromium-linux.png index ee551b2033e..a5a5b01ec57 100644 Binary files a/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-transfer-funds-to-another-category-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-transfer-funds-to-another-category-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-transfer-funds-to-another-category-3-chromium-linux.png b/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-transfer-funds-to-another-category-3-chromium-linux.png index a87364ce584..d20cceb4886 100644 Binary files a/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-transfer-funds-to-another-category-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.test.js-snapshots/Budget-transfer-funds-to-another-category-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/page-models/mobile-budget-page.js b/packages/desktop-client/e2e/page-models/mobile-budget-page.js index 7a3a63c0520..6e3689334fd 100644 --- a/packages/desktop-client/e2e/page-models/mobile-budget-page.js +++ b/packages/desktop-client/e2e/page-models/mobile-budget-page.js @@ -34,7 +34,7 @@ export class MobileBudgetPage { name: 'Saved', }); this.projectedSavingsButton = this.budgetTableHeader.getByRole('button', { - name: 'Projected Savings', + name: 'Projected savings', }); this.overspentButton = this.budgetTableHeader.getByRole('button', { name: 'Overspent', @@ -294,7 +294,7 @@ export class MobileBudgetPage { } throw new Error( - 'None of “Saved”, “Projected Savings”, or “Overspent” buttons could be located on the page', + 'None of “Saved”, “Projected savings”, or “Overspent” buttons could be located on the page', ); } diff --git a/packages/desktop-client/e2e/page-models/navigation.js b/packages/desktop-client/e2e/page-models/navigation.js index 248db6444b6..a8463d07678 100644 --- a/packages/desktop-client/e2e/page-models/navigation.js +++ b/packages/desktop-client/e2e/page-models/navigation.js @@ -66,7 +66,7 @@ export class Navigation { await this.page.getByLabel('Balance:').fill(String(data.balance)); if (data.offBudget) { - await this.page.getByLabel('Off-budget').click(); + await this.page.getByLabel('Off budget').click(); } await this.page diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Area-Graph-and-checks-the-visuals-1-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Area-Graph-and-checks-the-visuals-1-chromium-linux.png index f38bfe3dd15..5f6dce83514 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Area-Graph-and-checks-the-visuals-1-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Area-Graph-and-checks-the-visuals-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Area-Graph-and-checks-the-visuals-2-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Area-Graph-and-checks-the-visuals-2-chromium-linux.png index 9f96c1f40a6..e7446c7b27c 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Area-Graph-and-checks-the-visuals-2-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Area-Graph-and-checks-the-visuals-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Area-Graph-and-checks-the-visuals-3-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Area-Graph-and-checks-the-visuals-3-chromium-linux.png index 51699728fb0..e537cb6e233 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Area-Graph-and-checks-the-visuals-3-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Area-Graph-and-checks-the-visuals-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Bar-Graph-and-checks-the-visuals-1-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Bar-Graph-and-checks-the-visuals-1-chromium-linux.png index 93fd2a30ca7..9210a0b8dfd 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Bar-Graph-and-checks-the-visuals-1-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Bar-Graph-and-checks-the-visuals-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Bar-Graph-and-checks-the-visuals-2-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Bar-Graph-and-checks-the-visuals-2-chromium-linux.png index c1c099ed815..2b9312d930f 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Bar-Graph-and-checks-the-visuals-2-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Bar-Graph-and-checks-the-visuals-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Bar-Graph-and-checks-the-visuals-3-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Bar-Graph-and-checks-the-visuals-3-chromium-linux.png index 21c27b4c172..bae67361093 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Bar-Graph-and-checks-the-visuals-3-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Bar-Graph-and-checks-the-visuals-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Data-Table-and-checks-the-visuals-1-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Data-Table-and-checks-the-visuals-1-chromium-linux.png index 19fcbbdc5b7..f95e2069d7c 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Data-Table-and-checks-the-visuals-1-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Data-Table-and-checks-the-visuals-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Data-Table-and-checks-the-visuals-2-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Data-Table-and-checks-the-visuals-2-chromium-linux.png index ff5c6a727de..515682792ff 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Data-Table-and-checks-the-visuals-2-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Data-Table-and-checks-the-visuals-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Data-Table-and-checks-the-visuals-3-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Data-Table-and-checks-the-visuals-3-chromium-linux.png index 8695b840426..f88ba16bd12 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Data-Table-and-checks-the-visuals-3-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Data-Table-and-checks-the-visuals-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Donut-Graph-and-checks-the-visuals-1-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Donut-Graph-and-checks-the-visuals-1-chromium-linux.png index 8b6fb7dfc07..de3ebd81cd0 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Donut-Graph-and-checks-the-visuals-1-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Donut-Graph-and-checks-the-visuals-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Donut-Graph-and-checks-the-visuals-2-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Donut-Graph-and-checks-the-visuals-2-chromium-linux.png index e821884d439..163556249ac 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Donut-Graph-and-checks-the-visuals-2-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Donut-Graph-and-checks-the-visuals-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Donut-Graph-and-checks-the-visuals-3-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Donut-Graph-and-checks-the-visuals-3-chromium-linux.png index 986929ddd5d..75342c0ebc7 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Donut-Graph-and-checks-the-visuals-3-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Donut-Graph-and-checks-the-visuals-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Line-Graph-and-checks-the-visuals-1-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Line-Graph-and-checks-the-visuals-1-chromium-linux.png index 585f647ab6d..952ce325b76 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Line-Graph-and-checks-the-visuals-1-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Line-Graph-and-checks-the-visuals-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Line-Graph-and-checks-the-visuals-2-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Line-Graph-and-checks-the-visuals-2-chromium-linux.png index 6f0f75b9498..43ee73625d3 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Line-Graph-and-checks-the-visuals-2-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Line-Graph-and-checks-the-visuals-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Line-Graph-and-checks-the-visuals-3-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Line-Graph-and-checks-the-visuals-3-chromium-linux.png index 31437f9115a..47841b69da4 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Line-Graph-and-checks-the-visuals-3-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Line-Graph-and-checks-the-visuals-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-labels-button-shows-the-labels-1-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-labels-button-shows-the-labels-1-chromium-linux.png index 4fcb7ed113c..ab4600cc271 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-labels-button-shows-the-labels-1-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-labels-button-shows-the-labels-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-labels-button-shows-the-labels-2-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-labels-button-shows-the-labels-2-chromium-linux.png index 8b26edfde87..be087cd2d72 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-labels-button-shows-the-labels-2-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-labels-button-shows-the-labels-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-labels-button-shows-the-labels-3-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-labels-button-shows-the-labels-3-chromium-linux.png index 29e02b64c07..1e773b48936 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-labels-button-shows-the-labels-3-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-labels-button-shows-the-labels-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-legend-button-shows-the-legend-side-bar-1-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-legend-button-shows-the-legend-side-bar-1-chromium-linux.png index 90de19a59b4..679d6cb70be 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-legend-button-shows-the-legend-side-bar-1-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-legend-button-shows-the-legend-side-bar-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-legend-button-shows-the-legend-side-bar-2-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-legend-button-shows-the-legend-side-bar-2-chromium-linux.png index 3008b23dd03..e7a5d21d406 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-legend-button-shows-the-legend-side-bar-2-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-legend-button-shows-the-legend-side-bar-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-legend-button-shows-the-legend-side-bar-3-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-legend-button-shows-the-legend-side-bar-3-chromium-linux.png index 033344627f2..21349cff419 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-legend-button-shows-the-legend-side-bar-3-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-legend-button-shows-the-legend-side-bar-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-summary-button-shows-the-summary-1-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-summary-button-shows-the-summary-1-chromium-linux.png index 50c6f491487..8d482a07055 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-summary-button-shows-the-summary-1-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-summary-button-shows-the-summary-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-summary-button-shows-the-summary-2-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-summary-button-shows-the-summary-2-chromium-linux.png index a80fcbd03cd..5c2ba0154b6 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-summary-button-shows-the-summary-2-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-summary-button-shows-the-summary-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-summary-button-shows-the-summary-3-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-summary-button-shows-the-summary-3-chromium-linux.png index 239e9ac4f13..f308f9746f7 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-summary-button-shows-the-summary-3-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-summary-button-shows-the-summary-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-cash-flow-graph-and-checks-visuals-1-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-cash-flow-graph-and-checks-visuals-1-chromium-linux.png index a2ece8e2033..00c1b8960ad 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-cash-flow-graph-and-checks-visuals-1-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-cash-flow-graph-and-checks-visuals-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-cash-flow-graph-and-checks-visuals-2-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-cash-flow-graph-and-checks-visuals-2-chromium-linux.png index 581cb57fcef..96bea65dc0f 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-cash-flow-graph-and-checks-visuals-2-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-cash-flow-graph-and-checks-visuals-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-cash-flow-graph-and-checks-visuals-3-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-cash-flow-graph-and-checks-visuals-3-chromium-linux.png index 7319444883a..32496ff33a6 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-cash-flow-graph-and-checks-visuals-3-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-cash-flow-graph-and-checks-visuals-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-and-cash-flow-reports-1-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-and-cash-flow-reports-1-chromium-linux.png index 846d1b279fb..4dcc0d90cdd 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-and-cash-flow-reports-1-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-and-cash-flow-reports-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-and-cash-flow-reports-2-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-and-cash-flow-reports-2-chromium-linux.png index 0ab2b3e9d33..be2a1b131f1 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-and-cash-flow-reports-2-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-and-cash-flow-reports-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-and-cash-flow-reports-3-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-and-cash-flow-reports-3-chromium-linux.png index b105e11b4ac..5ffcf45bbe5 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-and-cash-flow-reports-3-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-and-cash-flow-reports-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-graph-and-checks-visuals-1-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-graph-and-checks-visuals-1-chromium-linux.png index f6185ec8f3a..8f51e4c2606 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-graph-and-checks-visuals-1-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-graph-and-checks-visuals-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-graph-and-checks-visuals-2-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-graph-and-checks-visuals-2-chromium-linux.png index ea636130ffa..65acd67269a 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-graph-and-checks-visuals-2-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-graph-and-checks-visuals-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-graph-and-checks-visuals-3-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-graph-and-checks-visuals-3-chromium-linux.png index a821b995cd9..3c30f39ac56 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-graph-and-checks-visuals-3-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-graph-and-checks-visuals-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-checks-the-page-visuals-1-chromium-linux.png b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-checks-the-page-visuals-1-chromium-linux.png index 100632aedfd..3ad3435f8e2 100644 Binary files a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-checks-the-page-visuals-1-chromium-linux.png and b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-checks-the-page-visuals-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-checks-the-page-visuals-2-chromium-linux.png b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-checks-the-page-visuals-2-chromium-linux.png index ec85acc65d9..97bc032452d 100644 Binary files a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-checks-the-page-visuals-2-chromium-linux.png and b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-checks-the-page-visuals-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-checks-the-page-visuals-3-chromium-linux.png b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-checks-the-page-visuals-3-chromium-linux.png index b10f9430cb1..fbe67464526 100644 Binary files a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-checks-the-page-visuals-3-chromium-linux.png and b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-checks-the-page-visuals-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-1-chromium-linux.png b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-1-chromium-linux.png index ec4bbe864ba..6280bf3d0d4 100644 Binary files a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-1-chromium-linux.png and b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-2-chromium-linux.png b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-2-chromium-linux.png index 7e7d4403134..5c94335278d 100644 Binary files a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-2-chromium-linux.png and b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-3-chromium-linux.png b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-3-chromium-linux.png index 08f56d60e5e..0942ab6fc0f 100644 Binary files a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-3-chromium-linux.png and b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-4-chromium-linux.png b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-4-chromium-linux.png index d4f7c7c11c0..c1fbf802415 100644 Binary files a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-4-chromium-linux.png and b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-4-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-5-chromium-linux.png b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-5-chromium-linux.png index b38d80cfc76..332e4798508 100644 Binary files a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-5-chromium-linux.png and b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-5-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-6-chromium-linux.png b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-6-chromium-linux.png index ea6d25609d6..8ab0059a62a 100644 Binary files a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-6-chromium-linux.png and b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-6-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-1-chromium-linux.png b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-1-chromium-linux.png index d2af63c1497..cc0deca56ef 100644 Binary files a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-1-chromium-linux.png and b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-2-chromium-linux.png b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-2-chromium-linux.png index cb5ef2b7ec6..f152f3ae997 100644 Binary files a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-2-chromium-linux.png and b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-3-chromium-linux.png b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-3-chromium-linux.png index c6fc88b0b75..061b63de9aa 100644 Binary files a/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-3-chromium-linux.png and b/packages/desktop-client/e2e/rules.test.js-snapshots/Rules-creates-a-split-transaction-rule-and-makes-sure-it-is-applied-when-creating-a-transaction-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-checks-the-page-visuals-1-chromium-linux.png b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-checks-the-page-visuals-1-chromium-linux.png index a802ba536e4..0c2c6cefc4e 100644 Binary files a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-checks-the-page-visuals-1-chromium-linux.png and b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-checks-the-page-visuals-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-checks-the-page-visuals-2-chromium-linux.png b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-checks-the-page-visuals-2-chromium-linux.png index d63beff8ce9..31b1bca4546 100644 Binary files a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-checks-the-page-visuals-2-chromium-linux.png and b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-checks-the-page-visuals-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-checks-the-page-visuals-3-chromium-linux.png b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-checks-the-page-visuals-3-chromium-linux.png index df2dc9c2c4c..ade6ced6832 100644 Binary files a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-checks-the-page-visuals-3-chromium-linux.png and b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-checks-the-page-visuals-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-1-chromium-linux.png b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-1-chromium-linux.png index 440f8c3b9f9..4ebe641c72d 100644 Binary files a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-1-chromium-linux.png and b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-10-chromium-linux.png b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-10-chromium-linux.png index d7fdcf4f754..4f1d5f19797 100644 Binary files a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-10-chromium-linux.png and b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-10-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-11-chromium-linux.png b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-11-chromium-linux.png index a70a6870cda..e3116aaead0 100644 Binary files a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-11-chromium-linux.png and b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-11-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-12-chromium-linux.png b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-12-chromium-linux.png index 11a107a25f3..927394477a1 100644 Binary files a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-12-chromium-linux.png and b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-12-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-2-chromium-linux.png b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-2-chromium-linux.png index 92dd17df133..a837d07eeb4 100644 Binary files a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-2-chromium-linux.png and b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-3-chromium-linux.png b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-3-chromium-linux.png index e1c80995e00..4fae2102048 100644 Binary files a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-3-chromium-linux.png and b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-4-chromium-linux.png b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-4-chromium-linux.png index eb14f152e87..e6b57d0f833 100644 Binary files a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-4-chromium-linux.png and b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-4-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-5-chromium-linux.png b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-5-chromium-linux.png index 6cee22f5a94..30e51041170 100644 Binary files a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-5-chromium-linux.png and b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-5-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-6-chromium-linux.png b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-6-chromium-linux.png index e2254859b7d..c9d5fee0948 100644 Binary files a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-6-chromium-linux.png and b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-6-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-7-chromium-linux.png b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-7-chromium-linux.png index 926b7d69301..1414d624b14 100644 Binary files a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-7-chromium-linux.png and b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-7-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-8-chromium-linux.png b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-8-chromium-linux.png index d933484319a..6a43aa588b9 100644 Binary files a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-8-chromium-linux.png and b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-8-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-9-chromium-linux.png b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-9-chromium-linux.png index 9ec263beb42..3aa8de5ec1d 100644 Binary files a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-9-chromium-linux.png and b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-9-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/settings.test.js-snapshots/Settings-checks-the-page-visuals-1-chromium-linux.png b/packages/desktop-client/e2e/settings.test.js-snapshots/Settings-checks-the-page-visuals-1-chromium-linux.png index 6edcc155e8c..f4ebfe318ad 100644 Binary files a/packages/desktop-client/e2e/settings.test.js-snapshots/Settings-checks-the-page-visuals-1-chromium-linux.png and b/packages/desktop-client/e2e/settings.test.js-snapshots/Settings-checks-the-page-visuals-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/settings.test.js-snapshots/Settings-checks-the-page-visuals-2-chromium-linux.png b/packages/desktop-client/e2e/settings.test.js-snapshots/Settings-checks-the-page-visuals-2-chromium-linux.png index 504c1144e80..d95e18307da 100644 Binary files a/packages/desktop-client/e2e/settings.test.js-snapshots/Settings-checks-the-page-visuals-2-chromium-linux.png and b/packages/desktop-client/e2e/settings.test.js-snapshots/Settings-checks-the-page-visuals-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/settings.test.js-snapshots/Settings-checks-the-page-visuals-3-chromium-linux.png b/packages/desktop-client/e2e/settings.test.js-snapshots/Settings-checks-the-page-visuals-3-chromium-linux.png index 2346fd83acb..8d85b9cf4c3 100644 Binary files a/packages/desktop-client/e2e/settings.test.js-snapshots/Settings-checks-the-page-visuals-3-chromium-linux.png and b/packages/desktop-client/e2e/settings.test.js-snapshots/Settings-checks-the-page-visuals-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-checks-the-page-visuals-1-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-checks-the-page-visuals-1-chromium-linux.png index e0b5ad7ba73..775c421f21e 100644 Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-checks-the-page-visuals-1-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-checks-the-page-visuals-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-checks-the-page-visuals-2-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-checks-the-page-visuals-2-chromium-linux.png index 1564f138dfe..be3da6d9975 100644 Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-checks-the-page-visuals-2-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-checks-the-page-visuals-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-checks-the-page-visuals-3-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-checks-the-page-visuals-3-chromium-linux.png index e4bf23f4cea..25c668e40c8 100644 Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-checks-the-page-visuals-3-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-checks-the-page-visuals-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-split-test-transaction-1-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-split-test-transaction-1-chromium-linux.png index f9dddffc720..504a8e3d15c 100644 Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-split-test-transaction-1-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-split-test-transaction-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-split-test-transaction-2-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-split-test-transaction-2-chromium-linux.png index f7ebc27b65b..3f8ecaeaf05 100644 Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-split-test-transaction-2-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-split-test-transaction-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-split-test-transaction-3-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-split-test-transaction-3-chromium-linux.png index 0c90ff92002..d80a3a0e021 100644 Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-split-test-transaction-3-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-split-test-transaction-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-test-transaction-1-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-test-transaction-1-chromium-linux.png index 32a731e7f71..f7a164b9efe 100644 Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-test-transaction-1-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-test-transaction-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-test-transaction-2-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-test-transaction-2-chromium-linux.png index 7847cdf91a7..77660b29aef 100644 Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-test-transaction-2-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-test-transaction-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-test-transaction-3-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-test-transaction-3-chromium-linux.png index f3f7c7287cb..7555f697792 100644 Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-test-transaction-3-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-test-transaction-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-transfer-test-transaction-1-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-transfer-test-transaction-1-chromium-linux.png index 47e323e93ee..1da49e54cb2 100644 Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-transfer-test-transaction-1-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-transfer-test-transaction-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-transfer-test-transaction-2-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-transfer-test-transaction-2-chromium-linux.png index d4da73c7c68..eb2ccd30916 100644 Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-transfer-test-transaction-2-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-transfer-test-transaction-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-transfer-test-transaction-3-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-transfer-test-transaction-3-chromium-linux.png index 86e303f6570..ff47298651c 100644 Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-transfer-test-transaction-3-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-transfer-test-transaction-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-transfer-test-transaction-4-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-transfer-test-transaction-4-chromium-linux.png index edda274f959..ea71505e978 100644 Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-transfer-test-transaction-4-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-transfer-test-transaction-4-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-transfer-test-transaction-5-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-transfer-test-transaction-5-chromium-linux.png index 0a866fb7c61..1f3556eb858 100644 Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-transfer-test-transaction-5-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-transfer-test-transaction-5-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-transfer-test-transaction-6-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-transfer-test-transaction-6-chromium-linux.png index 790d47d454e..23e7cdad42f 100644 Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-transfer-test-transaction-6-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-creates-a-transfer-test-transaction-6-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-7-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-7-chromium-linux.png index d9cadc56156..b5f64d82e34 100644 Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-7-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-7-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-8-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-8-chromium-linux.png index d1f12b017ab..7f0366ddc05 100644 Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-8-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-8-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-9-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-9-chromium-linux.png index 2106111fbb4..ec3895063d3 100644 Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-9-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-category-9-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-date-7-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-date-7-chromium-linux.png index a0627091062..8e3c1155e89 100644 Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-date-7-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-date-7-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-date-8-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-date-8-chromium-linux.png index ff672703b3c..d12a6f8f4fd 100644 Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-date-8-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-date-8-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-date-9-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-date-9-chromium-linux.png index f099ce4a711..71526c8ebcb 100644 Binary files a/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-date-9-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.js-snapshots/Transactions-filters-transactions-by-date-9-chromium-linux.png differ diff --git a/packages/desktop-client/src/components/LoggedInUser.tsx b/packages/desktop-client/src/components/LoggedInUser.tsx index 8ad160f2294..bba3b2e885a 100644 --- a/packages/desktop-client/src/components/LoggedInUser.tsx +++ b/packages/desktop-client/src/components/LoggedInUser.tsx @@ -1,11 +1,11 @@ // @ts-strict-ignore import React, { useState, useEffect, useRef, type CSSProperties } from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; +import { closeBudget, getUserData, signOut } from 'loot-core/client/actions'; import { type State } from 'loot-core/src/client/state-types'; -import { useActions } from '../hooks/useActions'; import { useNavigate } from '../hooks/useNavigate'; import { theme, styles } from '../style'; @@ -27,22 +27,28 @@ export function LoggedInUser({ color, }: LoggedInUserProps) { const { t } = useTranslation(); - + const dispatch = useDispatch(); + const navigate = useNavigate(); const userData = useSelector((state: State) => state.user.data); - const { getUserData, signOut, closeBudget } = useActions(); const [loading, setLoading] = useState(true); const [menuOpen, setMenuOpen] = useState(false); const serverUrl = useServerURL(); const triggerRef = useRef(null); useEffect(() => { - getUserData().then(() => setLoading(false)); + async function init() { + await dispatch(getUserData()); + } + + init().then(() => setLoading(false)); }, []); - const navigate = useNavigate(); + async function onCloseBudget() { + await dispatch(closeBudget()); + } async function onChangePassword() { - await closeBudget(); + await onCloseBudget(); navigate('/change-password'); } @@ -54,14 +60,14 @@ export function LoggedInUser({ onChangePassword(); break; case 'sign-in': - await closeBudget(); + await onCloseBudget(); navigate('/login'); break; case 'sign-out': - signOut(); + dispatch(signOut()); break; case 'config-server': - await closeBudget(); + await onCloseBudget(); navigate('/config-server'); break; default: diff --git a/packages/desktop-client/src/components/ManageRulesPage.tsx b/packages/desktop-client/src/components/ManageRulesPage.tsx index 12f76823d91..59e2327e446 100644 --- a/packages/desktop-client/src/components/ManageRulesPage.tsx +++ b/packages/desktop-client/src/components/ManageRulesPage.tsx @@ -1,11 +1,11 @@ import React from 'react'; - -import { t } from 'i18next'; +import { useTranslation } from 'react-i18next'; import { ManageRules } from './ManageRules'; import { Page } from './Page'; export function ManageRulesPage() { + const { t } = useTranslation(); return ( diff --git a/packages/desktop-client/src/components/Modals.tsx b/packages/desktop-client/src/components/Modals.tsx index a52f6fef71d..58ba6f051f4 100644 --- a/packages/desktop-client/src/components/Modals.tsx +++ b/packages/desktop-client/src/components/Modals.tsx @@ -45,6 +45,7 @@ import { KeyboardShortcutModal } from './modals/KeyboardShortcutModal'; import { LoadBackupModal } from './modals/LoadBackupModal'; import { ConfirmChangeDocumentDirModal } from './modals/manager/ConfirmChangeDocumentDir'; import { DeleteFileModal } from './modals/manager/DeleteFileModal'; +import { DuplicateFileModal } from './modals/manager/DuplicateFileModal'; import { FilesSettingsModal } from './modals/manager/FilesSettingsModal'; import { ImportActualModal } from './modals/manager/ImportActualModal'; import { ImportModal } from './modals/manager/ImportModal'; @@ -586,6 +587,16 @@ export function Modals() { return ; case 'delete-budget': return ; + case 'duplicate-budget': + return ( + + ); case 'import': return ; case 'files-settings': diff --git a/packages/desktop-client/src/components/Notes.tsx b/packages/desktop-client/src/components/Notes.tsx index 28a1ca6f8f8..1c1cf3c4076 100644 --- a/packages/desktop-client/src/components/Notes.tsx +++ b/packages/desktop-client/src/components/Notes.tsx @@ -1,9 +1,9 @@ // @ts-strict-ignore import React, { useEffect, useRef, type CSSProperties } from 'react'; +import { useTranslation } from 'react-i18next'; import ReactMarkdown from 'react-markdown'; import { css } from '@emotion/css'; -import { t } from 'i18next'; import remarkGfm from 'remark-gfm'; import { theme } from '../style'; @@ -99,6 +99,7 @@ export function Notes({ getStyle, }: NotesProps) { const { isNarrowWidth } = useResponsive(); + const { t } = useTranslation(); const textAreaRef = useRef(); diff --git a/packages/desktop-client/src/components/NotesButton.tsx b/packages/desktop-client/src/components/NotesButton.tsx index 80b2c2492d7..60874c1d9e8 100644 --- a/packages/desktop-client/src/components/NotesButton.tsx +++ b/packages/desktop-client/src/components/NotesButton.tsx @@ -5,8 +5,7 @@ import React, { type ComponentProps, type CSSProperties, } from 'react'; - -import { t } from 'i18next'; +import { useTranslation } from 'react-i18next'; import { send } from 'loot-core/src/platform/client/fetch'; @@ -36,6 +35,7 @@ export function NotesButton({ tooltipPosition = 'bottom start', style, }: NotesButtonProps) { + const { t } = useTranslation(); const triggerRef = useRef(null); const [isOpen, setIsOpen] = useState(false); const note = useNotes(id) || ''; diff --git a/packages/desktop-client/src/components/Notifications.tsx b/packages/desktop-client/src/components/Notifications.tsx index b7805d376c9..59bd7adcb7f 100644 --- a/packages/desktop-client/src/components/Notifications.tsx +++ b/packages/desktop-client/src/components/Notifications.tsx @@ -6,10 +6,10 @@ import React, { type SetStateAction, type CSSProperties, } from 'react'; +import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { css } from '@emotion/css'; -import { t } from 'i18next'; import { removeNotification } from 'loot-core/client/actions'; import { type State } from 'loot-core/src/client/state-types'; @@ -91,6 +91,7 @@ function Notification({ notification: NotificationWithId; onRemove: () => void; }) { + const { t } = useTranslation(); const { type, title, diff --git a/packages/desktop-client/src/components/ThemeSelector.tsx b/packages/desktop-client/src/components/ThemeSelector.tsx index 7993957790b..7e1c8600c96 100644 --- a/packages/desktop-client/src/components/ThemeSelector.tsx +++ b/packages/desktop-client/src/components/ThemeSelector.tsx @@ -1,6 +1,5 @@ import React, { useRef, useState, type CSSProperties } from 'react'; - -import { t } from 'i18next'; +import { useTranslation } from 'react-i18next'; import type { Theme } from 'loot-core/src/types/prefs'; @@ -22,6 +21,7 @@ export function ThemeSelector({ style }: ThemeSelectorProps) { const triggerRef = useRef(null); const { isNarrowWidth } = useResponsive(); + const { t } = useTranslation(); const themeIcons = { light: SvgSun, diff --git a/packages/desktop-client/src/components/Titlebar.tsx b/packages/desktop-client/src/components/Titlebar.tsx index d430b8ceaaf..748c29de9fc 100644 --- a/packages/desktop-client/src/components/Titlebar.tsx +++ b/packages/desktop-client/src/components/Titlebar.tsx @@ -1,10 +1,12 @@ import React, { useState, useEffect, type CSSProperties } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; import { Routes, Route, useLocation } from 'react-router-dom'; import { css } from '@emotion/css'; -import { t } from 'i18next'; +import { sync } from 'loot-core/client/actions'; import * as Platform from 'loot-core/src/client/platform'; import * as queries from 'loot-core/src/client/queries'; import { listen } from 'loot-core/src/platform/client/fetch'; @@ -13,7 +15,6 @@ import { isElectron, } from 'loot-core/src/shared/environment'; -import { useActions } from '../hooks/useActions'; import { useGlobalPref } from '../hooks/useGlobalPref'; import { useMetadataPref } from '../hooks/useMetadataPref'; import { useNavigate } from '../hooks/useNavigate'; @@ -107,9 +108,9 @@ type SyncButtonProps = { isMobile?: boolean; }; function SyncButton({ style, isMobile = false }: SyncButtonProps) { + const { t } = useTranslation(); const [cloudFileId] = useMetadataPref('cloudFileId'); - const { sync } = useActions(); - + const dispatch = useDispatch(); const [syncing, setSyncing] = useState(false); const [syncState, setSyncState] = useState< null | 'offline' | 'local' | 'disabled' | 'error' @@ -193,15 +194,17 @@ function SyncButton({ style, isMobile = false }: SyncButtonProps) { marginRight: 5, }; + const onSync = () => dispatch(sync()); + useHotkeys( 'ctrl+s, cmd+s, meta+s', - sync, + onSync, { enableOnFormTags: true, preventDefault: true, scopes: ['app'], }, - [sync], + [onSync], ); return ( @@ -223,7 +226,7 @@ function SyncButton({ style, isMobile = false }: SyncButtonProps) { '&[data-hovered]': hoveredStyle, '&[data-pressed]': activeStyle, })} - onPress={sync} + onPress={onSync} > {isMobile ? ( syncState === 'error' ? ( @@ -265,6 +268,7 @@ type TitlebarProps = { }; export function Titlebar({ style }: TitlebarProps) { + const { t } = useTranslation(); const navigate = useNavigate(); const location = useLocation(); const sidebar = useSidebar(); diff --git a/packages/desktop-client/src/components/UpdateNotification.tsx b/packages/desktop-client/src/components/UpdateNotification.tsx index acbb6ea2382..809ff797810 100644 --- a/packages/desktop-client/src/components/UpdateNotification.tsx +++ b/packages/desktop-client/src/components/UpdateNotification.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useSelector } from 'react-redux'; +import { useSelector, useDispatch } from 'react-redux'; +import { setAppState, updateApp } from 'loot-core/client/actions'; import { type State } from 'loot-core/src/client/state-types'; -import { useActions } from '../hooks/useActions'; import { SvgClose } from '../icons/v1'; import { theme } from '../style'; @@ -20,7 +20,10 @@ export function UpdateNotification() { (state: State) => state.app.showUpdateNotification, ); - const { updateApp, setAppState } = useActions(); + const dispatch = useDispatch(); + const onRestart = () => { + dispatch(updateApp()); + }; if (updateInfo && showUpdateNotification) { const notes = updateInfo.releaseNotes; @@ -51,7 +54,7 @@ export function UpdateNotification() { { // Set a flag to never show an update notification again for this session - setAppState({ - updateInfo: null, - showUpdateNotification: false, - }); + dispatch( + setAppState({ + updateInfo: null, + showUpdateNotification: false, + }), + ); }} > void; @@ -841,8 +841,8 @@ class AccountInternal extends PureComponent< } if (!account) { - if (id === 'budgeted') { - return t('Budgeted Accounts'); + if (id === 'onbudget') { + return t('On Budget Accounts'); } else if (id === 'offbudget') { return t('Off Budget Accounts'); } else if (id === 'uncategorized') { @@ -1662,7 +1662,7 @@ class AccountInternal extends PureComponent< const isNameEditable = accountId && - accountId !== 'budgeted' && + accountId !== 'onbudget' && accountId !== 'offbudget' && accountId !== 'uncategorized'; @@ -1767,7 +1767,7 @@ class AccountInternal extends PureComponent< showAccount={ !accountId || accountId === 'offbudget' || - accountId === 'budgeted' || + accountId === 'onbudget' || accountId === 'uncategorized' } isAdding={this.state.isAdding} diff --git a/packages/desktop-client/src/components/accounts/AccountSyncCheck.tsx b/packages/desktop-client/src/components/accounts/AccountSyncCheck.tsx index 04c59a44ace..0d3e24c904f 100644 --- a/packages/desktop-client/src/components/accounts/AccountSyncCheck.tsx +++ b/packages/desktop-client/src/components/accounts/AccountSyncCheck.tsx @@ -1,10 +1,8 @@ import React, { useCallback, useRef, useState } from 'react'; -import { Trans } from 'react-i18next'; +import { Trans, useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; -import { t } from 'i18next'; - import { unlinkAccount } from 'loot-core/client/actions'; import { type AccountEntity } from 'loot-core/types/models'; @@ -17,61 +15,69 @@ import { Link } from '../common/Link'; import { Popover } from '../common/Popover'; import { View } from '../common/View'; -function getErrorMessage(type: string, code: string) { - switch (type.toUpperCase()) { - case 'ITEM_ERROR': - switch (code.toUpperCase()) { - case 'NO_ACCOUNTS': - return t( - 'No open accounts could be found. Did you close the account? If so, unlink the account.', - ); - case 'ITEM_LOGIN_REQUIRED': - return t( - 'Your password or something else has changed with your bank and you need to login again.', - ); - default: - } - break; - - case 'INVALID_INPUT': - switch (code.toUpperCase()) { - case 'INVALID_ACCESS_TOKEN': - return t('Item is no longer authorized. You need to login again.'); - default: - } - break; - - case 'RATE_LIMIT_EXCEEDED': - return t('Rate limit exceeded for this item. Please try again later.'); - - case 'INVALID_ACCESS_TOKEN': - return t( - 'Your SimpleFIN Access Token is no longer valid. Please reset and generate a new token.', - ); - - case 'ACCOUNT_NEEDS_ATTENTION': - return ( - - The account needs your attention at{' '} - - SimpleFIN - - . - - ); - - default: +function useErrorMessage() { + const { t } = useTranslation(); + function getErrorMessage(type: string, code: string) { + switch (type.toUpperCase()) { + case 'ITEM_ERROR': + switch (code.toUpperCase()) { + case 'NO_ACCOUNTS': + return t( + 'No open accounts could be found. Did you close the account? If so, unlink the account.', + ); + case 'ITEM_LOGIN_REQUIRED': + return t( + 'Your password or something else has changed with your bank and you need to login again.', + ); + default: + } + break; + + case 'INVALID_INPUT': + switch (code.toUpperCase()) { + case 'INVALID_ACCESS_TOKEN': + return t('Item is no longer authorized. You need to login again.'); + default: + } + break; + + case 'RATE_LIMIT_EXCEEDED': + return t('Rate limit exceeded for this item. Please try again later.'); + + case 'INVALID_ACCESS_TOKEN': + return t( + 'Your SimpleFIN Access Token is no longer valid. Please reset and generate a new token.', + ); + + case 'ACCOUNT_NEEDS_ATTENTION': + return ( + + The account needs your attention at{' '} + + SimpleFIN + + . + + ); + + default: + } + + return ( + + An internal error occurred. Try to login again, or get{' '} + + in touch + {' '} + for support. + + ); } - return ( - - An internal error occurred. Try to login again, or get{' '} - - in touch - {' '} - for support. - - ); + return { getErrorMessage }; } export function AccountSyncCheck() { @@ -81,6 +87,7 @@ export function AccountSyncCheck() { const { id } = useParams(); const [open, setOpen] = useState(false); const triggerRef = useRef(null); + const { getErrorMessage } = useErrorMessage(); const reauth = useCallback( (acc: AccountEntity) => { diff --git a/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx b/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx index e403042878c..3a50f424e23 100644 --- a/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx +++ b/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx @@ -68,8 +68,8 @@ function AccountList({ item.closed ? t('Closed Accounts') : item.offbudget - ? t('Off Budget') - : t('For Budget') + ? t('Off budget') + : t('On budget') }`; lastItem = item; diff --git a/packages/desktop-client/src/components/budget/BudgetTable.jsx b/packages/desktop-client/src/components/budget/BudgetTable.tsx similarity index 65% rename from packages/desktop-client/src/components/budget/BudgetTable.jsx rename to packages/desktop-client/src/components/budget/BudgetTable.tsx index 785457f2ab5..66481fd64af 100644 --- a/packages/desktop-client/src/components/budget/BudgetTable.jsx +++ b/packages/desktop-client/src/components/budget/BudgetTable.tsx @@ -1,14 +1,24 @@ -import React, { useState } from 'react'; +import React, { + type ComponentPropsWithoutRef, + type KeyboardEvent, + useState, +} from 'react'; + +import { + type CategoryEntity, + type CategoryGroupEntity, +} from 'loot-core/types/models'; import { useCategories } from '../../hooks/useCategories'; import { useLocalPref } from '../../hooks/useLocalPref'; import { theme, styles } from '../../style'; import { View } from '../common/View'; +import { type DropPosition } from '../sort'; import { BudgetCategories } from './BudgetCategories'; import { BudgetSummaries } from './BudgetSummaries'; import { BudgetTotals } from './BudgetTotals'; -import { MonthsProvider } from './MonthsContext'; +import { type MonthBounds, MonthsProvider } from './MonthsContext'; import { findSortDown, findSortUp, @@ -16,7 +26,39 @@ import { separateGroups, } from './util'; -export function BudgetTable(props) { +type BudgetTableProps = { + type: string; + prewarmStartMonth: string; + startMonth: string; + numMonths: number; + monthBounds: MonthBounds; + dataComponents: { + SummaryComponent: ComponentPropsWithoutRef< + typeof BudgetSummaries + >['SummaryComponent']; + BudgetTotalsComponent: ComponentPropsWithoutRef< + typeof BudgetTotals + >['MonthComponent']; + }; + onSaveCategory: (category: CategoryEntity) => void; + onDeleteCategory: (id: CategoryEntity['id']) => void; + onSaveGroup: (group: CategoryGroupEntity) => void; + onDeleteGroup: (id: CategoryGroupEntity['id']) => void; + onApplyBudgetTemplatesInGroup: (groupId: CategoryGroupEntity['id']) => void; + onReorderCategory: (params: { + id: CategoryEntity['id']; + groupId?: CategoryGroupEntity['id']; + targetId: CategoryEntity['id'] | null; + }) => void; + onReorderGroup: (params: { + id: CategoryGroupEntity['id']; + targetId: CategoryEntity['id'] | null; + }) => void; + onShowActivity: (id: CategoryEntity['id'], month?: string) => void; + onBudgetAction: (month: string, type: string, args: unknown) => void; +}; + +export function BudgetTable(props: BudgetTableProps) { const { type, prewarmStartMonth, @@ -35,23 +77,29 @@ export function BudgetTable(props) { onBudgetAction, } = props; - const { grouped: categoryGroups } = useCategories(); + const { grouped: categoryGroups = [] } = useCategories(); const [collapsedGroupIds = [], setCollapsedGroupIdsPref] = useLocalPref('budget.collapsed'); const [showHiddenCategories, setShowHiddenCategoriesPef] = useLocalPref( 'budget.showHiddenCategories', ); - const [editing, setEditing] = useState(null); + const [editing, setEditing] = useState<{ id: string; cell: string } | null>( + null, + ); - const onEditMonth = (id, month) => { + const onEditMonth = (id: string, month: string) => { setEditing(id ? { id, cell: month } : null); }; - const onEditName = id => { + const onEditName = (id: string) => { setEditing(id ? { id, cell: 'name' } : null); }; - const _onReorderCategory = (id, dropPos, targetId) => { + const _onReorderCategory = ( + id: string, + dropPos: DropPosition, + targetId: string, + ) => { const isGroup = !!categoryGroups.find(g => g.id === targetId); if (isGroup) { @@ -63,7 +111,7 @@ export function BudgetTable(props) { const group = categoryGroups.find(g => g.id === groupId); if (group) { - const { categories } = group; + const { categories = [] } = group; onReorderCategory({ id, groupId: group.id, @@ -77,7 +125,7 @@ export function BudgetTable(props) { let targetGroup; for (const group of categoryGroups) { - if (group.categories.find(cat => cat.id === targetId)) { + if (group.categories?.find(cat => cat.id === targetId)) { targetGroup = group; break; } @@ -85,13 +133,17 @@ export function BudgetTable(props) { onReorderCategory({ id, - groupId: targetGroup.id, - ...findSortDown(targetGroup.categories, dropPos, targetId), + groupId: targetGroup?.id, + ...findSortDown(targetGroup?.categories || [], dropPos, targetId), }); } }; - const _onReorderGroup = (id, dropPos, targetId) => { + const _onReorderGroup = ( + id: string, + dropPos: DropPosition, + targetId: string, + ) => { const [expenseGroups] = separateGroups(categoryGroups); // exclude Income group from sortable groups to fix off-by-one error onReorderGroup({ id, @@ -99,13 +151,21 @@ export function BudgetTable(props) { }); }; - const moveVertically = dir => { - const flattened = categoryGroups.reduce((all, group) => { - if (collapsedGroupIds.includes(group.id)) { - return all.concat({ id: group.id, isGroup: true }); - } - return all.concat([{ id: group.id, isGroup: true }, ...group.categories]); - }, []); + const moveVertically = (dir: 1 | -1) => { + const flattened = categoryGroups.reduce( + (all, group) => { + if (collapsedGroupIds.includes(group.id)) { + return all.concat({ id: group.id, isGroup: true }); + } + return all.concat([ + { id: group.id, isGroup: true }, + ...(group?.categories || []), + ]); + }, + [] as Array< + { id: CategoryGroupEntity['id']; isGroup: boolean } | CategoryEntity + >, + ); if (editing) { const idx = flattened.findIndex(item => item.id === editing.id); @@ -114,10 +174,13 @@ export function BudgetTable(props) { while (nextIdx >= 0 && nextIdx < flattened.length) { const next = flattened[nextIdx]; - if (next.isGroup) { + if ('isGroup' in next && next.isGroup) { nextIdx += dir; continue; - } else if (type === 'report' || !next.is_income) { + } else if ( + type === 'report' || + ('is_income' in next && !next.is_income) + ) { onEditMonth(next.id, editing.cell); return; } else { @@ -127,7 +190,7 @@ export function BudgetTable(props) { } }; - const onKeyDown = e => { + const onKeyDown = (e: KeyboardEvent) => { if (!editing) { return null; } @@ -138,7 +201,7 @@ export function BudgetTable(props) { } }; - const onCollapse = collapsedIds => { + const onCollapse = (collapsedIds: string[]) => { setCollapsedGroupIdsPref(collapsedIds); }; @@ -223,6 +286,7 @@ export function BudgetTable(props) { onKeyDown={onKeyDown} > ; +type DynamicBudgetTableProps = Omit< + ComponentProps, + 'numMonths' +> & { + maxMonths: number; + onMonthSelect: (month: string, numMonths: number) => void; +}; export const DynamicBudgetTable = (props: DynamicBudgetTableProps) => { return ( diff --git a/packages/desktop-client/src/components/budget/MonthPicker.tsx b/packages/desktop-client/src/components/budget/MonthPicker.tsx index 501bafd79ff..66f44423871 100644 --- a/packages/desktop-client/src/components/budget/MonthPicker.tsx +++ b/packages/desktop-client/src/components/budget/MonthPicker.tsx @@ -7,12 +7,12 @@ import { useResizeObserver } from '../../hooks/useResizeObserver'; import { styles, theme } from '../../style'; import { View } from '../common/View'; -import { type BoundsProps } from './MonthsContext'; +import { type MonthBounds } from './MonthsContext'; type MonthPickerProps = { startMonth: string; numDisplayed: number; - monthBounds: BoundsProps; + monthBounds: MonthBounds; style: CSSProperties; onSelect: (month: string) => void; }; diff --git a/packages/desktop-client/src/components/budget/MonthsContext.tsx b/packages/desktop-client/src/components/budget/MonthsContext.tsx index 2d5e377402a..dad9b264028 100644 --- a/packages/desktop-client/src/components/budget/MonthsContext.tsx +++ b/packages/desktop-client/src/components/budget/MonthsContext.tsx @@ -3,13 +3,13 @@ import React, { createContext, type ReactNode } from 'react'; import * as monthUtils from 'loot-core/src/shared/months'; -export type BoundsProps = { +export type MonthBounds = { start: string; end: string; }; export function getValidMonthBounds( - bounds: BoundsProps, + bounds: MonthBounds, startMonth: undefined | string, endMonth: string, ) { @@ -29,7 +29,7 @@ export const MonthsContext = createContext(null); type MonthsProviderProps = { startMonth: string | undefined; numMonths: number; - monthBounds: BoundsProps; + monthBounds: MonthBounds; type: string; children: ReactNode; }; diff --git a/packages/desktop-client/src/components/budget/envelope/budgetsummary/TotalsList.tsx b/packages/desktop-client/src/components/budget/envelope/budgetsummary/TotalsList.tsx index 357ef3200b5..89f01776a44 100644 --- a/packages/desktop-client/src/components/budget/envelope/budgetsummary/TotalsList.tsx +++ b/packages/desktop-client/src/components/budget/envelope/budgetsummary/TotalsList.tsx @@ -119,10 +119,10 @@ export function TotalsList({ prevMonthName, style }: TotalsListProps) { - Available Funds + Available funds Overspent in {prevMonthName} Budgeted - For Next Month + For next month ); diff --git a/packages/desktop-client/src/components/budget/index.tsx b/packages/desktop-client/src/components/budget/index.tsx index 8abaf8de55f..0012f46f0a7 100644 --- a/packages/desktop-client/src/components/budget/index.tsx +++ b/packages/desktop-client/src/components/budget/index.tsx @@ -355,6 +355,7 @@ function BudgetInner(props: BudgetInnerProps) { onShowActivity={onShowActivity} onReorderCategory={onReorderCategory} onReorderGroup={onReorderGroup} + onApplyBudgetTemplatesInGroup={onApplyBudgetTemplatesInGroup} /> ); @@ -375,13 +376,13 @@ function BudgetInner(props: BudgetInnerProps) { onMonthSelect={onMonthSelect} onDeleteCategory={onDeleteCategory} onDeleteGroup={onDeleteGroup} - onApplyBudgetTemplatesInGroup={onApplyBudgetTemplatesInGroup} onSaveCategory={onSaveCategory} onSaveGroup={onSaveGroup} onBudgetAction={onBudgetAction} onShowActivity={onShowActivity} onReorderCategory={onReorderCategory} onReorderGroup={onReorderGroup} + onApplyBudgetTemplatesInGroup={onApplyBudgetTemplatesInGroup} /> ); diff --git a/packages/desktop-client/src/components/budget/tracking/budgetsummary/Saved.tsx b/packages/desktop-client/src/components/budget/tracking/budgetsummary/Saved.tsx index 3cb5ab658f6..270b4e85345 100644 --- a/packages/desktop-client/src/components/budget/tracking/budgetsummary/Saved.tsx +++ b/packages/desktop-client/src/components/budget/tracking/budgetsummary/Saved.tsx @@ -33,7 +33,7 @@ export function Saved({ projected, style }: SavedProps) { {projected ? ( - Projected Savings: + Projected savings: ) : ( @@ -46,7 +46,7 @@ export function Saved({ projected, style }: SavedProps) { content={ <> { + const { t } = useTranslation(); const { isNarrowWidth } = useResponsive(); const { enableScope, disableScope } = useHotkeysContext(); @@ -300,6 +301,7 @@ export function ModalHeader({ title, rightContent, }: ModalHeaderProps) { + const { t } = useTranslation(); return ( ({ {mapField(field, options)} {' '} {friendlyOp(op, null)}{' '} - + {!['onbudget', 'offbudget'].includes(op?.toLocaleLowerCase()) && ( + + )} > )} diff --git a/packages/desktop-client/src/components/filters/FiltersMenu.jsx b/packages/desktop-client/src/components/filters/FiltersMenu.jsx index c6ec6b583d7..17ce8e7a451 100644 --- a/packages/desktop-client/src/components/filters/FiltersMenu.jsx +++ b/packages/desktop-client/src/components/filters/FiltersMenu.jsx @@ -117,8 +117,18 @@ function ConfigureField({ }} /> ) : ( - titleFirst(mapField(field)) + + {titleFirst(mapField(field))} + )} + @@ -222,6 +232,7 @@ function ConfigureField({ } value={value} multi={op === 'oneOf' || op === 'notOneOf'} + op={op} style={{ marginTop: 10 }} onChange={v => { dispatch({ type: 'set-value', value: v }); diff --git a/packages/desktop-client/src/components/filters/updateFilterReducer.ts b/packages/desktop-client/src/components/filters/updateFilterReducer.ts index 97721973312..3b8d92b67b2 100644 --- a/packages/desktop-client/src/components/filters/updateFilterReducer.ts +++ b/packages/desktop-client/src/components/filters/updateFilterReducer.ts @@ -19,7 +19,9 @@ export function updateFilterReducer( action.op === 'is' || action.op === 'doesNotContain' || action.op === 'isNot' || - action.op === 'hasTags') + action.op === 'hasTags' || + action.op === 'onBudget' || + action.op === 'offBudget') ) { // Clear out the value if switching between contains or // is/oneof for the id or string type diff --git a/packages/desktop-client/src/components/manager/BudgetList.tsx b/packages/desktop-client/src/components/manager/BudgetList.tsx index 72a3ebab0c0..12defd2e4dd 100644 --- a/packages/desktop-client/src/components/manager/BudgetList.tsx +++ b/packages/desktop-client/src/components/manager/BudgetList.tsx @@ -64,9 +64,11 @@ function getFileDescription(file: File, t: (key: string) => string) { function FileMenu({ onDelete, onClose, + onDuplicate, }: { onDelete: () => void; onClose: () => void; + onDuplicate?: () => void; }) { function onMenuSelect(type: string) { onClose(); @@ -75,18 +77,30 @@ function FileMenu({ case 'delete': onDelete(); break; + case 'duplicate': + if (onDuplicate) onDuplicate(); + break; default: } } const { t } = useTranslation(); - const items = [{ name: 'delete', text: t('Delete') }]; + const items = [ + ...(onDuplicate ? [{ name: 'duplicate', text: t('Duplicate') }] : []), + { name: 'delete', text: t('Delete') }, + ]; return ; } -function FileMenuButton({ onDelete }: { onDelete: () => void }) { +function FileMenuButton({ + onDelete, + onDuplicate, +}: { + onDelete: () => void; + onDuplicate?: () => void; +}) { const triggerRef = useRef(null); const [menuOpen, setMenuOpen] = useState(false); @@ -108,7 +122,11 @@ function FileMenuButton({ onDelete }: { onDelete: () => void }) { isOpen={menuOpen} onOpenChange={() => setMenuOpen(false)} > - setMenuOpen(false)} /> + setMenuOpen(false)} + onDuplicate={onDuplicate} + /> ); @@ -169,11 +187,13 @@ function FileItem({ quickSwitchMode, onSelect, onDelete, + onDuplicate, }: { file: File; quickSwitchMode: boolean; onSelect: (file: File) => void; onDelete: (file: File) => void; + onDuplicate: (file: File) => void; }) { const { t } = useTranslation(); @@ -239,7 +259,10 @@ function FileItem({ )} {!quickSwitchMode && ( - onDelete(file)} /> + onDelete(file)} + onDuplicate={'id' in file ? () => onDuplicate(file) : undefined} + /> )} @@ -252,11 +275,13 @@ function BudgetFiles({ quickSwitchMode, onSelect, onDelete, + onDuplicate, }: { files: File[]; quickSwitchMode: boolean; onSelect: (file: File) => void; onDelete: (file: File) => void; + onDuplicate: (file: File) => void; }) { function isLocalFile(file: File): file is LocalFile { return file.state === 'local'; @@ -292,6 +317,7 @@ function BudgetFiles({ quickSwitchMode={quickSwitchMode} onSelect={onSelect} onDelete={onDelete} + onDuplicate={onDuplicate} /> )) )} @@ -467,7 +493,19 @@ export function BudgetList({ showHeader = true, quickSwitchMode = false }) { files={files} quickSwitchMode={quickSwitchMode} onSelect={onSelect} - onDelete={file => dispatch(pushModal('delete-budget', { file }))} + onDelete={(file: File) => + dispatch(pushModal('delete-budget', { file })) + } + onDuplicate={(file: File) => { + if (file && 'id' in file) { + dispatch(pushModal('duplicate-budget', { file, managePage: true })); + } else { + console.error( + 'Attempted to duplicate a cloud file - only local files are supported. Cloud file:', + file, + ); + } + }} /> {!quickSwitchMode && ( - pushModal('import')}> + dispatch(pushModal('import'))}> {t('Import my budget')} - createBudget({ testMode: true })}> + dispatch(createBudget({ testMode: true }))}> {t('View demo')} - createBudget()}> + dispatch(createBudget())} + > {t('Start fresh')} diff --git a/packages/desktop-client/src/components/mobile/MobileBackButton.tsx b/packages/desktop-client/src/components/mobile/MobileBackButton.tsx index 1338d153d93..ab528289105 100644 --- a/packages/desktop-client/src/components/mobile/MobileBackButton.tsx +++ b/packages/desktop-client/src/components/mobile/MobileBackButton.tsx @@ -1,6 +1,5 @@ import React, { type ComponentPropsWithoutRef } from 'react'; - -import { t } from 'i18next'; +import { useTranslation } from 'react-i18next'; import { useNavigate } from '../../hooks/useNavigate'; import { SvgCheveronLeft } from '../../icons/v1'; @@ -15,6 +14,7 @@ export function MobileBackButton({ style, ...props }: MobileBackButtonProps) { + const { t } = useTranslation(); const navigate = useNavigate(); return ( )); - useScrollListener(({ isScrolling }) => { - if (isScrolling('down')) { + useScrollListener(({ isScrolling, hasScrolledToEnd }) => { + if (isScrolling('down') && !hasScrolledToEnd('up')) { hide(); - } else if (isScrolling('up')) { + } else if (isScrolling('up') && !hasScrolledToEnd('down')) { openDefault(); } }); diff --git a/packages/desktop-client/src/components/mobile/accounts/Account.tsx b/packages/desktop-client/src/components/mobile/accounts/Account.tsx index 7940ad6222a..c8a3829764e 100644 --- a/packages/desktop-client/src/components/mobile/accounts/Account.tsx +++ b/packages/desktop-client/src/components/mobile/accounts/Account.tsx @@ -29,8 +29,8 @@ export function Account() { function accountNameFromId(id: string | undefined) { switch (id) { - case 'budgeted': - return 'Budgeted Accounts'; + case 'onbudget': + return 'On Budget Accounts'; case 'offbudget': return 'Off Budget Accounts'; case 'uncategorized': diff --git a/packages/desktop-client/src/components/mobile/accounts/AccountTransactions.tsx b/packages/desktop-client/src/components/mobile/accounts/AccountTransactions.tsx index d746aee4de2..3b6c6780312 100644 --- a/packages/desktop-client/src/components/mobile/accounts/AccountTransactions.tsx +++ b/packages/desktop-client/src/components/mobile/accounts/AccountTransactions.tsx @@ -53,7 +53,11 @@ export function AccountTransactions({ accountName, }: { readonly account?: AccountEntity; - readonly accountId?: string; + readonly accountId?: + | AccountEntity['id'] + | 'onbudget' + | 'offbudget' + | 'uncategorized'; readonly accountName: string; }) { const schedulesQuery = useMemo( @@ -218,7 +222,7 @@ function TransactionListWithPreviews({ readonly account?: AccountEntity; readonly accountId?: | AccountEntity['id'] - | 'budgeted' + | 'onbudget' | 'offbudget' | 'uncategorized'; readonly accountName: AccountEntity['name'] | string; @@ -236,6 +240,7 @@ function TransactionListWithPreviews({ transactions, isLoading, reload: reloadTransactions, + isLoadingMore, loadMore: loadMoreTransactions, } = useTransactions({ query: transactionsQuery, @@ -269,7 +274,7 @@ function TransactionListWithPreviews({ tables.includes('category_mapping') || tables.includes('payee_mapping') ) { - reloadTransactions?.(); + reloadTransactions(); } if (tables.includes('payees') || tables.includes('payee_mapping')) { @@ -326,6 +331,7 @@ function TransactionListWithPreviews({ balance={balanceQueries.balance} balanceCleared={balanceQueries.cleared} balanceUncleared={balanceQueries.uncleared} + isLoadingMore={isLoadingMore} onLoadMore={loadMoreTransactions} searchPlaceholder={`Search ${accountName}`} onSearch={onSearch} @@ -340,13 +346,13 @@ function queriesFromAccountId( entity: AccountEntity | undefined, ) { switch (id) { - case 'budgeted': + case 'onbudget': return { - balance: queries.budgetedAccountBalance(), + balance: queries.onBudgetAccountBalance(), }; case 'offbudget': return { - balance: queries.offbudgetAccountBalance(), + balance: queries.offBudgetAccountBalance(), }; case 'uncategorized': return { diff --git a/packages/desktop-client/src/components/mobile/accounts/Accounts.tsx b/packages/desktop-client/src/components/mobile/accounts/Accounts.tsx index f17806dc4a8..b7d0c76f41c 100644 --- a/packages/desktop-client/src/components/mobile/accounts/Accounts.tsx +++ b/packages/desktop-client/src/components/mobile/accounts/Accounts.tsx @@ -1,8 +1,7 @@ import React, { type CSSProperties, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; -import { t } from 'i18next'; - import { replaceModal, syncAndDownload } from 'loot-core/src/client/actions'; import * as queries from 'loot-core/src/client/queries'; import { type AccountEntity } from 'loot-core/types/models'; @@ -163,6 +162,7 @@ function AccountCard({ } function EmptyMessage() { + const { t } = useTranslation(); return ( @@ -178,7 +178,7 @@ type AccountListProps = { accounts: AccountEntity[]; updatedAccounts: Array; getBalanceQuery: (account: AccountEntity) => Binding<'account', 'balance'>; - getOnBudgetBalance: () => Binding<'account', 'budgeted-accounts-balance'>; + getOnBudgetBalance: () => Binding<'account', 'onbudget-accounts-balance'>; getOffBudgetBalance: () => Binding<'account', 'offbudget-accounts-balance'>; onAddAccount: () => void; onSelectAccount: (id: string) => void; @@ -195,10 +195,11 @@ function AccountList({ onSelectAccount, onSync, }: AccountListProps) { + const { t } = useTranslation(); const failedAccounts = useFailedAccounts(); const syncingAccountIds = useSelector(state => state.account.accountsSyncing); - const budgetedAccounts = accounts.filter(account => account.offbudget === 0); - const offbudgetAccounts = accounts.filter(account => account.offbudget === 1); + const onBudgetAccounts = accounts.filter(account => account.offbudget === 0); + const offBudgetAccounts = accounts.filter(account => account.offbudget === 1); return ( } - {budgetedAccounts.length > 0 && ( - + {onBudgetAccounts.length > 0 && ( + )} - {budgetedAccounts.map(acct => ( + {onBudgetAccounts.map(acct => ( ))} - {offbudgetAccounts.length > 0 && ( + {offBudgetAccounts.length > 0 && ( )} - {offbudgetAccounts.map(acct => ( + {offBudgetAccounts.map(acct => ( !account.closed)} updatedAccounts={updatedAccounts} getBalanceQuery={queries.accountBalance} - getOnBudgetBalance={queries.budgetedAccountBalance} - getOffBudgetBalance={queries.offbudgetAccountBalance} + getOnBudgetBalance={queries.onBudgetAccountBalance} + getOffBudgetBalance={queries.offBudgetAccountBalance} onAddAccount={onAddAccount} onSelectAccount={onSelectAccount} onSync={onSync} diff --git a/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx b/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx index 28ce9be58a5..c2bf9cd50b2 100644 --- a/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx +++ b/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx @@ -1,9 +1,9 @@ import React, { memo, useCallback, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { css } from '@emotion/css'; import { AutoTextSize } from 'auto-text-size'; -import { t } from 'i18next'; import memoizeOne from 'memoize-one'; import { collapseModals, pushModal } from 'loot-core/client/actions'; @@ -67,6 +67,7 @@ function getColumnWidth({ show3Cols, isSidebar = false, offset = 0 } = {}) { } function ToBudget({ toBudget, onPress, show3Cols }) { + const { t } = useTranslation(); const amount = useSheetValue(toBudget); const format = useFormat(); const sidebarColumnWidth = getColumnWidth({ show3Cols, isSidebar: true }); @@ -129,6 +130,7 @@ function ToBudget({ toBudget, onPress, show3Cols }) { } function Saved({ projected, onPress, show3Cols }) { + const { t } = useTranslation(); const binding = projected ? trackingBudget.totalBudgetedSaved : trackingBudget.totalSaved; @@ -156,7 +158,7 @@ function Saved({ projected, onPress, show3Cols }) { minFontSizePx={6} maxFontSizePx={12} mode="oneline" - title="Projected Savings" + title="Projected savings" style={{ color: theme.formInputText, textAlign: 'left', @@ -226,6 +228,7 @@ function BudgetCell({ children, ...props }) { + const { t } = useTranslation(); const columnWidth = getColumnWidth(); const dispatch = useDispatch(); const format = useFormat(); @@ -398,6 +401,7 @@ const ExpenseCategory = memo(function ExpenseCategory({ }) { const opacity = blank ? 0 : 1; + const { t } = useTranslation(); const isGoalTemplatesEnabled = useFeatureFlag('goalTemplatesEnabled'); const goalTemp = useSheetValue(goal); const goalValue = isGoalTemplatesEnabled ? goalTemp : null; @@ -1138,6 +1142,7 @@ const IncomeCategory = memo(function IncomeCategory({ onEdit, onBudgetAction, }) { + const { t } = useTranslation(); const listItemRef = useRef(); const format = useFormat(); const sidebarColumnWidth = getColumnWidth({ isSidebar: true, offset: -10 }); @@ -1413,6 +1418,7 @@ function IncomeGroup({ collapsed, onToggleCollapse, }) { + const { t } = useTranslation(); const columnWidth = getColumnWidth(); return ( @@ -1633,6 +1639,7 @@ export function BudgetTable({ onOpenBudgetPageMenu, onOpenBudgetMonthMenu, }) { + const { t } = useTranslation(); const { width } = useResponsive(); const show3Cols = width >= 360; @@ -1737,6 +1744,7 @@ function BudgetTableHeader({ showSpentColumn, toggleSpentColumn, }) { + const { t } = useTranslation(); const format = useFormat(); const buttonStyle = { padding: 0, @@ -1962,6 +1970,7 @@ function MonthSelector({ onPrevMonth, onNextMonth, }) { + const { t } = useTranslation(); const prevEnabled = month > monthBounds.start; const nextEnabled = month < monthUtils.subMonths(monthBounds.end, 1); diff --git a/packages/desktop-client/src/components/mobile/budget/CategoryTransactions.jsx b/packages/desktop-client/src/components/mobile/budget/CategoryTransactions.tsx similarity index 88% rename from packages/desktop-client/src/components/mobile/budget/CategoryTransactions.jsx rename to packages/desktop-client/src/components/mobile/budget/CategoryTransactions.tsx index c03f1676451..4eee5f6e2cb 100644 --- a/packages/desktop-client/src/components/mobile/budget/CategoryTransactions.jsx +++ b/packages/desktop-client/src/components/mobile/budget/CategoryTransactions.tsx @@ -11,6 +11,10 @@ import { listen } from 'loot-core/platform/client/fetch'; import * as monthUtils from 'loot-core/shared/months'; import { q } from 'loot-core/shared/query'; import { isPreviewId } from 'loot-core/shared/transactions'; +import { + type CategoryEntity, + type TransactionEntity, +} from 'loot-core/types/models'; import { useDateFormat } from '../../../hooks/useDateFormat'; import { useNavigate } from '../../../hooks/useNavigate'; @@ -21,7 +25,15 @@ import { MobileBackButton } from '../MobileBackButton'; import { AddTransactionButton } from '../transactions/AddTransactionButton'; import { TransactionListWithBalances } from '../transactions/TransactionListWithBalances'; -export function CategoryTransactions({ category, month }) { +type CategoryTransactionsProps = { + category: CategoryEntity; + month: string; +}; + +export function CategoryTransactions({ + category, + month, +}: CategoryTransactionsProps) { const dispatch = useDispatch(); const navigate = useNavigate(); @@ -40,6 +52,7 @@ export function CategoryTransactions({ category, month }) { const { transactions, isLoading, + isLoadingMore, loadMore: loadMoreTransactions, reload: reloadTransactions, } = useTransactions({ @@ -56,7 +69,7 @@ export function CategoryTransactions({ category, month }) { tables.includes('category_mapping') || tables.includes('payee_mapping') ) { - reloadTransactions?.(); + reloadTransactions(); } if (tables.includes('payees') || tables.includes('payee_mapping')) { @@ -73,7 +86,7 @@ export function CategoryTransactions({ category, month }) { }); const onOpenTransaction = useCallback( - transaction => { + (transaction: TransactionEntity) => { // details of how the native app used to handle preview transactions here can be found at commit 05e58279 if (!isPreviewId(transaction.id)) { navigate(`/transactions/${transaction.id}`); @@ -112,14 +125,16 @@ export function CategoryTransactions({ category, month }) { balanceUncleared={balanceUncleared} searchPlaceholder={`Search ${category.name}`} onSearch={onSearch} + isLoadingMore={isLoadingMore} onLoadMore={loadMoreTransactions} onOpenTransaction={onOpenTransaction} + onRefresh={undefined} /> ); } -function getCategoryMonthFilter(category, month) { +function getCategoryMonthFilter(category: CategoryEntity, month: string) { return { category: category.id, date: { $transform: '$month', $eq: month }, diff --git a/packages/desktop-client/src/components/mobile/transactions/AddTransactionButton.tsx b/packages/desktop-client/src/components/mobile/transactions/AddTransactionButton.tsx index 814fa6263f4..2024205c3c0 100644 --- a/packages/desktop-client/src/components/mobile/transactions/AddTransactionButton.tsx +++ b/packages/desktop-client/src/components/mobile/transactions/AddTransactionButton.tsx @@ -1,6 +1,5 @@ import React from 'react'; - -import { t } from 'i18next'; +import { useTranslation } from 'react-i18next'; import { useNavigate } from '../../../hooks/useNavigate'; import { SvgAdd } from '../../../icons/v1'; @@ -17,6 +16,7 @@ export function AddTransactionButton({ accountId, categoryId, }: AddTransactionButtonProps) { + const { t } = useTranslation(); const navigate = useNavigate(); return ( { + const { t } = useTranslation(); const { editingField, onRequestActiveEdit, onClearActiveEdit } = useSingleActiveEditForm(); const prettyPayee = getPrettyPayee({ @@ -448,6 +449,7 @@ const TransactionEditInner = memo(function TransactionEditInner({ onSplit, onAddSplit, }) { + const { t } = useTranslation(); const navigate = useNavigate(); const dispatch = useDispatch(); const transactions = useMemo( @@ -517,7 +519,7 @@ const TransactionEditInner = memo(function TransactionEditInner({ const getCategory = useCallback( (trans, isOffBudget) => { if (isOffBudget) { - return 'Off Budget'; + return 'Off budget'; } else if (isBudgetTransfer(trans)) { return 'Transfer'; } else { diff --git a/packages/desktop-client/src/components/mobile/transactions/TransactionList.jsx b/packages/desktop-client/src/components/mobile/transactions/TransactionList.jsx index a22e236f9af..f577126fdb8 100644 --- a/packages/desktop-client/src/components/mobile/transactions/TransactionList.jsx +++ b/packages/desktop-client/src/components/mobile/transactions/TransactionList.jsx @@ -6,10 +6,9 @@ import React, { useState, } from 'react'; import { ListBox, Section, Header, Collection } from 'react-aria-components'; +import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; -import { t } from 'i18next'; - import { setNotificationInset } from 'loot-core/client/actions'; import { groupById, integerToCurrency } from 'loot-core/shared/util'; import * as monthUtils from 'loot-core/src/shared/months'; @@ -41,12 +40,31 @@ import { TransactionListItem } from './TransactionListItem'; const NOTIFICATION_BOTTOM_INSET = 75; +function Loading({ style, 'aria-label': ariaLabel }) { + return ( + + + + ); +} + export function TransactionList({ isLoading, transactions, onOpenTransaction, + isLoadingMore, onLoadMore, }) { + const { t } = useTranslation(); const sections = useMemo(() => { // Group by date. We can assume transactions is ordered const sections = []; @@ -83,29 +101,19 @@ export function TransactionList({ ); useScrollListener(({ hasScrolledToEnd }) => { - if (hasScrolledToEnd('down', 5)) { + if (hasScrolledToEnd('down', 100)) { onLoadMore?.(); } }); if (isLoading) { - return ( - - - - ); + return ; } + return ( <> 0 ? 'multiple' : 'single'} selectedKeys={selectedTransactions} dependencies={[selectedTransactions]} @@ -159,6 +167,17 @@ export function TransactionList({ )} + + {isLoadingMore && ( + + )} + {selectedTransactions.size > 0 && ( )} diff --git a/packages/desktop-client/src/components/mobile/transactions/TransactionListItem.tsx b/packages/desktop-client/src/components/mobile/transactions/TransactionListItem.tsx index f46037d140a..db898ec9ef8 100644 --- a/packages/desktop-client/src/components/mobile/transactions/TransactionListItem.tsx +++ b/packages/desktop-client/src/components/mobile/transactions/TransactionListItem.tsx @@ -101,7 +101,7 @@ export function TransactionListItem({ transferAccount, }); const specialCategory = account?.offbudget - ? 'Off Budget' + ? 'Off budget' : transferAccount && !transferAccount.offbudget ? 'Transfer' : isParent diff --git a/packages/desktop-client/src/components/mobile/transactions/TransactionListWithBalances.jsx b/packages/desktop-client/src/components/mobile/transactions/TransactionListWithBalances.jsx index f884f927ac1..54cb0ee7807 100644 --- a/packages/desktop-client/src/components/mobile/transactions/TransactionListWithBalances.jsx +++ b/packages/desktop-client/src/components/mobile/transactions/TransactionListWithBalances.jsx @@ -1,6 +1,5 @@ import React, { useState } from 'react'; - -import { t } from 'i18next'; +import { useTranslation } from 'react-i18next'; import { SelectedProvider, useSelected } from '../../../hooks/useSelected'; import { SvgSearchAlternate } from '../../../icons/v2'; @@ -65,6 +64,7 @@ export function TransactionListWithBalances({ balanceUncleared, searchPlaceholder = 'Search...', onSearch, + isLoadingMore, onLoadMore, onOpenTransaction, onRefresh, @@ -104,6 +104,7 @@ export function TransactionListWithBalances({ @@ -113,6 +114,7 @@ export function TransactionListWithBalances({ } function BalanceWithCleared({ balanceUncleared, balanceCleared, balance }) { + const { t } = useTranslation(); const unclearedAmount = useSheetValue(balanceUncleared); return ( @@ -171,6 +173,7 @@ function BalanceWithCleared({ balanceUncleared, balanceCleared, balance }) { } function Balance({ balance }) { + const { t } = useTranslation(); return ( diff --git a/packages/desktop-client/src/components/modals/BudgetListModal.tsx b/packages/desktop-client/src/components/modals/BudgetListModal.tsx index d7e26cfb934..10e4e5e5829 100644 --- a/packages/desktop-client/src/components/modals/BudgetListModal.tsx +++ b/packages/desktop-client/src/components/modals/BudgetListModal.tsx @@ -1,8 +1,7 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { t } from 'i18next'; - import { useMetadataPref } from '../../hooks/useMetadataPref'; import { Modal, ModalHeader, ModalCloseButton } from '../common/Modal'; import { Text } from '../common/Text'; @@ -10,6 +9,7 @@ import { View } from '../common/View'; import { BudgetList } from '../manager/BudgetList'; export function BudgetListModal() { + const { t } = useTranslation(); const [id] = useMetadataPref('id'); const currentFile = useSelector(state => state.budgets.allFiles?.find(f => 'id' in f && f.id === id), diff --git a/packages/desktop-client/src/components/modals/CategoryAutocompleteModal.tsx b/packages/desktop-client/src/components/modals/CategoryAutocompleteModal.tsx index 18ff718f716..11840d41126 100644 --- a/packages/desktop-client/src/components/modals/CategoryAutocompleteModal.tsx +++ b/packages/desktop-client/src/components/modals/CategoryAutocompleteModal.tsx @@ -1,6 +1,5 @@ import React, { type ComponentPropsWithoutRef } from 'react'; - -import { t } from 'i18next'; +import { useTranslation } from 'react-i18next'; import * as monthUtils from 'loot-core/src/shared/months'; @@ -28,6 +27,7 @@ export function CategoryAutocompleteModal({ month, onClose, }: CategoryAutocompleteModalProps) { + const { t } = useTranslation(); const { isNarrowWidth } = useResponsive(); const defaultAutocompleteProps = { diff --git a/packages/desktop-client/src/components/modals/CategoryGroupMenuModal.tsx b/packages/desktop-client/src/components/modals/CategoryGroupMenuModal.tsx index edd835f27a4..a97e9d022e6 100644 --- a/packages/desktop-client/src/components/modals/CategoryGroupMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/CategoryGroupMenuModal.tsx @@ -5,8 +5,7 @@ import React, { useState, type CSSProperties, } from 'react'; - -import { t } from 'i18next'; +import { useTranslation } from 'react-i18next'; import { type CategoryGroupEntity } from 'loot-core/src/types/models'; @@ -47,6 +46,7 @@ export function CategoryGroupMenuModal({ onToggleVisibility, onClose, }: CategoryGroupMenuModalProps) { + const { t } = useTranslation(); const { grouped: categoryGroups } = useCategories(); const group = categoryGroups.find(g => g.id === groupId); const notes = useNotes(group.id); @@ -172,6 +172,7 @@ export function CategoryGroupMenuModal({ } function AdditionalCategoryGroupMenu({ group, onDelete, onToggleVisibility }) { + const { t } = useTranslation(); const triggerRef = useRef(null); const [menuOpen, setMenuOpen] = useState(false); const itemStyle: CSSProperties = { diff --git a/packages/desktop-client/src/components/modals/CategoryMenuModal.tsx b/packages/desktop-client/src/components/modals/CategoryMenuModal.tsx index 765a5caf061..973b018d0a9 100644 --- a/packages/desktop-client/src/components/modals/CategoryMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/CategoryMenuModal.tsx @@ -1,7 +1,6 @@ // @ts-strict-ignore import React, { useRef, useState, type CSSProperties } from 'react'; - -import { t } from 'i18next'; +import { useTranslation } from 'react-i18next'; import { type CategoryEntity } from 'loot-core/src/types/models'; @@ -40,6 +39,7 @@ export function CategoryMenuModal({ onToggleVisibility, onClose, }: CategoryMenuModalProps) { + const { t } = useTranslation(); const category = useCategory(categoryId); const categoryGroup = useCategoryGroup(category?.cat_group); const originalNotes = useNotes(category.id); @@ -158,6 +158,7 @@ function AdditionalCategoryMenu({ onDelete, onToggleVisibility, }) { + const { t } = useTranslation(); const triggerRef = useRef(null); const [menuOpen, setMenuOpen] = useState(false); const itemStyle: CSSProperties = { diff --git a/packages/desktop-client/src/components/modals/CloseAccountModal.tsx b/packages/desktop-client/src/components/modals/CloseAccountModal.tsx index d947b81c9c1..5e808a334d2 100644 --- a/packages/desktop-client/src/components/modals/CloseAccountModal.tsx +++ b/packages/desktop-client/src/components/modals/CloseAccountModal.tsx @@ -35,7 +35,7 @@ function needsCategory( const isOffBudget = acct && acct.offbudget === 1; // The user must select a category if transferring from a budgeted - // account to an off-budget account + // account to an off budget account return account.offbudget === 0 && isOffBudget; } @@ -183,8 +183,8 @@ export function CloseAccountModal({ {needsCategory(account, transferAccountId, accounts) && ( - Since you are transferring the balance from a budgeted - account to an off-budget account, this transaction must + Since you are transferring the balance from an on budget + account to an off budget account, this transaction must be categorized. Select a category: diff --git a/packages/desktop-client/src/components/modals/CreateAccountModal.tsx b/packages/desktop-client/src/components/modals/CreateAccountModal.tsx index 8f62089c913..bb73dc2a3d5 100644 --- a/packages/desktop-client/src/components/modals/CreateAccountModal.tsx +++ b/packages/desktop-client/src/components/modals/CreateAccountModal.tsx @@ -1,5 +1,5 @@ -// @ts-strict-ignore -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useState } from 'react'; +import { DialogTrigger } from 'react-aria-components'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; @@ -30,13 +30,12 @@ export function CreateAccountModal({ upgradingAccountId }: CreateAccountProps) { const { t } = useTranslation(); const syncServerStatus = useSyncServerStatus(); const dispatch = useDispatch(); - const [isGoCardlessSetupComplete, setIsGoCardlessSetupComplete] = - useState(null); - const [isSimpleFinSetupComplete, setIsSimpleFinSetupComplete] = - useState(null); - const [menuGoCardlessOpen, setGoCardlessMenuOpen] = useState(false); - const triggerRef = useRef(null); - const [menuSimplefinOpen, setSimplefinMenuOpen] = useState(false); + const [isGoCardlessSetupComplete, setIsGoCardlessSetupComplete] = useState< + boolean | null + >(null); + const [isSimpleFinSetupComplete, setIsSimpleFinSetupComplete] = useState< + boolean | null + >(null); const onConnectGoCardless = () => { if (!isGoCardlessSetupComplete) { @@ -139,7 +138,6 @@ export function CreateAccountModal({ upgradingAccountId }: CreateAccountProps) { value: null, }).then(() => { setIsGoCardlessSetupComplete(false); - setGoCardlessMenuOpen(false); }); }); }; @@ -154,7 +152,6 @@ export function CreateAccountModal({ upgradingAccountId }: CreateAccountProps) { value: null, }).then(() => { setIsSimpleFinSetupComplete(false); - setSimplefinMenuOpen(false); }); }); }; @@ -207,7 +204,7 @@ export function CreateAccountModal({ upgradingAccountId }: CreateAccountProps) { - {t('Create a local account')} + {t('Create a local account')}{' '} {t( 'if you want to add transactions manually. You can also', )}{' '} @@ -248,12 +245,10 @@ export function CreateAccountModal({ upgradingAccountId }: CreateAccountProps) { : t('Set up GoCardless for bank sync')} {isGoCardlessSetupComplete && ( - <> + setGoCardlessMenuOpen(true)} - aria-label="GoCardless menu" + aria-label={t('GoCardless menu')} > - setGoCardlessMenuOpen(false)} - > + { if (item === 'reconfigure') { @@ -281,7 +272,7 @@ export function CreateAccountModal({ upgradingAccountId }: CreateAccountProps) { ]} /> - > + )} @@ -317,24 +308,15 @@ export function CreateAccountModal({ upgradingAccountId }: CreateAccountProps) { : t('Set up SimpleFIN for bank sync')} {isSimpleFinSetupComplete && ( - <> - setSimplefinMenuOpen(true)} - aria-label="SimpleFIN menu" - > + + - setSimplefinMenuOpen(false)} - > + { if (item === 'reconfigure') { @@ -349,7 +331,7 @@ export function CreateAccountModal({ upgradingAccountId }: CreateAccountProps) { ]} /> - > + )} diff --git a/packages/desktop-client/src/components/modals/CreateEncryptionKeyModal.tsx b/packages/desktop-client/src/components/modals/CreateEncryptionKeyModal.tsx index 536b1145438..b513a58a83e 100644 --- a/packages/desktop-client/src/components/modals/CreateEncryptionKeyModal.tsx +++ b/packages/desktop-client/src/components/modals/CreateEncryptionKeyModal.tsx @@ -1,10 +1,10 @@ // @ts-strict-ignore import React, { useState } from 'react'; import { Form } from 'react-aria-components'; +import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { css } from '@emotion/css'; -import { t } from 'i18next'; import { loadAllFiles, loadGlobalPrefs, sync } from 'loot-core/client/actions'; import { send } from 'loot-core/src/platform/client/fetch'; @@ -35,6 +35,7 @@ type CreateEncryptionKeyModalProps = { export function CreateEncryptionKeyModal({ options = {}, }: CreateEncryptionKeyModalProps) { + const { t } = useTranslation(); const [password, setPassword] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); diff --git a/packages/desktop-client/src/components/modals/CreateLocalAccountModal.tsx b/packages/desktop-client/src/components/modals/CreateLocalAccountModal.tsx index 594d3d11202..d2147150b84 100644 --- a/packages/desktop-client/src/components/modals/CreateLocalAccountModal.tsx +++ b/packages/desktop-client/src/components/modals/CreateLocalAccountModal.tsx @@ -1,10 +1,9 @@ // @ts-strict-ignore import { type FormEvent, useState } from 'react'; import { Form } from 'react-aria-components'; +import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; -import { t } from 'i18next'; - import { closeModal, createAccount } from 'loot-core/client/actions'; import { toRelaxedNumber } from 'loot-core/src/shared/util'; @@ -30,6 +29,7 @@ import { Checkbox } from '../forms'; import { validateAccountName } from '../util/accountValidation'; export function CreateLocalAccountModal() { + const { t } = useTranslation(); const navigate = useNavigate(); const dispatch = useDispatch(); const accounts = useAccounts.useAccounts(); @@ -127,7 +127,7 @@ export function CreateLocalAccountModal() { verticalAlign: 'center', }} > - {t('Off-budget')} + {t('Off budget')} (null); diff --git a/packages/desktop-client/src/components/modals/EditRuleModal.jsx b/packages/desktop-client/src/components/modals/EditRuleModal.jsx index c30d9ef94ef..649a9236472 100644 --- a/packages/desktop-client/src/components/modals/EditRuleModal.jsx +++ b/packages/desktop-client/src/components/modals/EditRuleModal.jsx @@ -1,8 +1,8 @@ import React, { useState, useEffect, useRef, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { css } from '@emotion/css'; -import { t } from 'i18next'; import { v4 as uuid } from 'uuid'; import { @@ -262,6 +262,7 @@ function ConditionEditor({ field={field} type={type} value={value} + op={op} multi={op === 'oneOf' || op === 'notOneOf'} onChange={v => onChange('value', v)} numberFormatType="currency" @@ -303,6 +304,7 @@ function formatAmount(amount) { } function ScheduleDescription({ id }) { + const { t } = useTranslation(); const dateFormat = useDateFormat() || 'MM/dd/yyyy'; const scheduleQuery = useMemo( () => q('schedules').filter({ id }).select('*'), @@ -372,6 +374,7 @@ const splitActionFields = actionFields.filter( ); const allocationMethodOptions = Object.entries(ALLOCATION_METHODS); function ActionEditor({ action, editorStyle, onChange, onDelete, onAdd }) { + const { t } = useTranslation(); const { field, op, @@ -461,6 +464,7 @@ function ActionEditor({ action, editorStyle, onChange, onDelete, onAdd }) { ({ ...c, inputKey: uuid() })), ); diff --git a/packages/desktop-client/src/components/modals/EnvelopeBalanceMenuModal.tsx b/packages/desktop-client/src/components/modals/EnvelopeBalanceMenuModal.tsx index f4d9e655f13..5dc42192978 100644 --- a/packages/desktop-client/src/components/modals/EnvelopeBalanceMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/EnvelopeBalanceMenuModal.tsx @@ -2,8 +2,7 @@ import React, { type ComponentPropsWithoutRef, type CSSProperties, } from 'react'; - -import { t } from 'i18next'; +import { useTranslation } from 'react-i18next'; import { envelopeBudget } from 'loot-core/client/queries'; @@ -41,6 +40,7 @@ export function EnvelopeBalanceMenuModal({ borderTop: `1px solid ${theme.pillBorder}`, }; + const { t } = useTranslation(); const category = useCategory(categoryId); if (!category) { diff --git a/packages/desktop-client/src/components/modals/EnvelopeBudgetMenuModal.tsx b/packages/desktop-client/src/components/modals/EnvelopeBudgetMenuModal.tsx index 5dcb473172a..a7f1d9eb5a3 100644 --- a/packages/desktop-client/src/components/modals/EnvelopeBudgetMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/EnvelopeBudgetMenuModal.tsx @@ -4,8 +4,7 @@ import React, { useEffect, type CSSProperties, } from 'react'; - -import { t } from 'i18next'; +import { useTranslation } from 'react-i18next'; import { envelopeBudget } from 'loot-core/client/queries'; import { amountToInteger, integerToAmount } from 'loot-core/shared/util'; @@ -45,6 +44,7 @@ export function EnvelopeBudgetMenuModal({ borderTop: `1px solid ${theme.pillBorder}`, }; + const { t } = useTranslation(); const budgeted = useEnvelopeSheetValue( envelopeBudget.catBudgeted(categoryId), ); diff --git a/packages/desktop-client/src/components/modals/FixEncryptionKeyModal.tsx b/packages/desktop-client/src/components/modals/FixEncryptionKeyModal.tsx index ce048809010..c1e25425928 100644 --- a/packages/desktop-client/src/components/modals/FixEncryptionKeyModal.tsx +++ b/packages/desktop-client/src/components/modals/FixEncryptionKeyModal.tsx @@ -1,8 +1,7 @@ // @ts-strict-ignore import React, { useState } from 'react'; import { Form } from 'react-aria-components'; - -import { t } from 'i18next'; +import { useTranslation } from 'react-i18next'; import { type FinanceModals } from 'loot-core/src/client/state-types/modals'; import { send } from 'loot-core/src/platform/client/fetch'; @@ -33,6 +32,7 @@ export function FixEncryptionKeyModal({ }: FixEncryptionKeyModalProps) { const { hasExistingKey, cloudFileId, onSuccess } = options; + const { t } = useTranslation(); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); diff --git a/packages/desktop-client/src/components/modals/GoCardlessInitialiseModal.tsx b/packages/desktop-client/src/components/modals/GoCardlessInitialiseModal.tsx index 188b5d57331..a2c01a79448 100644 --- a/packages/desktop-client/src/components/modals/GoCardlessInitialiseModal.tsx +++ b/packages/desktop-client/src/components/modals/GoCardlessInitialiseModal.tsx @@ -1,7 +1,6 @@ // @ts-strict-ignore import React, { useState } from 'react'; - -import { t } from 'i18next'; // Ensure this import is correct +import { useTranslation } from 'react-i18next'; import { send } from 'loot-core/src/platform/client/fetch'; @@ -27,6 +26,7 @@ type GoCardlessInitialiseProps = { export const GoCardlessInitialiseModal = ({ onSuccess, }: GoCardlessInitialiseProps) => { + const { t } = useTranslation(); const [secretId, setSecretId] = useState(''); const [secretKey, setSecretKey] = useState(''); const [isValid, setIsValid] = useState(true); diff --git a/packages/desktop-client/src/components/modals/ImportTransactionsModal/ImportTransactionsModal.jsx b/packages/desktop-client/src/components/modals/ImportTransactionsModal/ImportTransactionsModal.jsx index 7ed84edca8e..530b2d10324 100644 --- a/packages/desktop-client/src/components/modals/ImportTransactionsModal/ImportTransactionsModal.jsx +++ b/packages/desktop-client/src/components/modals/ImportTransactionsModal/ImportTransactionsModal.jsx @@ -1,11 +1,17 @@ import React, { useState, useEffect, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; import deepEqual from 'deep-equal'; -import { t } from 'i18next'; +import { + getPayees, + importPreviewTransactions, + importTransactions, + parseTransactions, +} from 'loot-core/client/actions'; import { amountToInteger } from 'loot-core/src/shared/util'; -import { useActions } from '../../../hooks/useActions'; import { useDateFormat } from '../../../hooks/useDateFormat'; import { useSyncedPrefs } from '../../../hooks/useSyncedPrefs'; import { theme } from '../../../style'; @@ -135,14 +141,10 @@ function parseCategoryFields(trans, categories) { } export function ImportTransactionsModal({ options }) { + const { t } = useTranslation(); const dateFormat = useDateFormat() || 'MM/dd/yyyy'; const [prefs, savePrefs] = useSyncedPrefs(); - const { - parseTransactions, - importTransactions, - importPreviewTransactions, - getPayees, - } = useActions(); + const dispatch = useDispatch(); const [multiplierAmount, setMultiplierAmount] = useState(''); const [loadingState, setLoadingState] = useState('parsing'); @@ -263,9 +265,8 @@ export function ImportTransactionsModal({ options }) { } // Retreive the transactions that would be updated (along with the existing trx) - const previewTrx = await importPreviewTransactions( - accountId, - previewTransactions, + const previewTrx = await dispatch( + importPreviewTransactions(accountId, previewTransactions), ); const matchedUpdateMap = previewTrx.reduce((map, entry) => { map[entry.transaction.trx_id] = entry; @@ -309,7 +310,7 @@ export function ImportTransactionsModal({ options }) { return next; }, []); }, - [accountId, categories.list, clearOnImport, importPreviewTransactions], + [accountId, categories.list, clearOnImport, dispatch], ); const parse = useCallback( @@ -320,8 +321,9 @@ export function ImportTransactionsModal({ options }) { setFilename(filename); setFileType(filetype); - const { errors, transactions: parsedTransactions = [] } = - await parseTransactions(filename, options); + const { errors, transactions: parsedTransactions = [] } = await dispatch( + parseTransactions(filename, options), + ); let index = 0; const transactions = parsedTransactions.map(trans => { @@ -399,11 +401,11 @@ export function ImportTransactionsModal({ options }) { }, [ accountId, + dispatch, getImportPreview, inOutMode, multiplierAmount, outValue, - parseTransactions, prefs, ], ); @@ -427,7 +429,6 @@ export function ImportTransactionsModal({ options }) { parse(options.filename, parseOptions); }, [ - parseTransactions, options.filename, delimiter, hasHeaderRow, @@ -653,13 +654,11 @@ export function ImportTransactionsModal({ options }) { }); } - const didChange = await importTransactions( - accountId, - finalTransactions, - reconcile, + const didChange = await dispatch( + importTransactions(accountId, finalTransactions, reconcile), ); if (didChange) { - await getPayees(); + await dispatch(getPayees()); } if (onImported) { diff --git a/packages/desktop-client/src/components/modals/ManageRulesModal.tsx b/packages/desktop-client/src/components/modals/ManageRulesModal.tsx index 9b52fda9bc7..fe61ce7f984 100644 --- a/packages/desktop-client/src/components/modals/ManageRulesModal.tsx +++ b/packages/desktop-client/src/components/modals/ManageRulesModal.tsx @@ -1,9 +1,8 @@ // @ts-strict-ignore import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; -import { t } from 'i18next'; - import { isNonProductionEnvironment } from 'loot-core/src/shared/environment'; import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal'; @@ -14,6 +13,7 @@ type ManageRulesModalProps = { }; export function ManageRulesModal({ payeeId }: ManageRulesModalProps) { + const { t } = useTranslation(); const [loading, setLoading] = useState(true); const location = useLocation(); if (isNonProductionEnvironment()) { diff --git a/packages/desktop-client/src/components/modals/MergeUnusedPayeesModal.tsx b/packages/desktop-client/src/components/modals/MergeUnusedPayeesModal.tsx index 164143cd73e..acd5ae9b388 100644 --- a/packages/desktop-client/src/components/modals/MergeUnusedPayeesModal.tsx +++ b/packages/desktop-client/src/components/modals/MergeUnusedPayeesModal.tsx @@ -1,8 +1,7 @@ import React, { useState, useRef, useEffect, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; -import { t } from 'i18next'; - import { replaceModal } from 'loot-core/src/client/actions/modals'; import { send } from 'loot-core/src/platform/client/fetch'; import { type PayeeEntity } from 'loot-core/types/models'; @@ -27,6 +26,7 @@ export function MergeUnusedPayeesModal({ payeeIds, targetPayeeId, }: MergeUnusedPayeesModalProps) { + const { t } = useTranslation(); const allPayees = usePayees(); const modalStack = useSelector(state => state.modals.modalStack); const isEditingRule = !!modalStack.find(m => m.name === 'edit-rule'); diff --git a/packages/desktop-client/src/components/modals/PayeeAutocompleteModal.tsx b/packages/desktop-client/src/components/modals/PayeeAutocompleteModal.tsx index 7fab5513fe7..77a22652546 100644 --- a/packages/desktop-client/src/components/modals/PayeeAutocompleteModal.tsx +++ b/packages/desktop-client/src/components/modals/PayeeAutocompleteModal.tsx @@ -1,6 +1,5 @@ import React, { type ComponentPropsWithoutRef } from 'react'; - -import { t } from 'i18next'; +import { useTranslation } from 'react-i18next'; import { useAccounts } from '../../hooks/useAccounts'; import { useNavigate } from '../../hooks/useNavigate'; @@ -24,6 +23,7 @@ export function PayeeAutocompleteModal({ autocompleteProps, onClose, }: PayeeAutocompleteModalProps) { + const { t } = useTranslation(); const payees = usePayees() || []; const accounts = useAccounts() || []; const navigate = useNavigate(); diff --git a/packages/desktop-client/src/components/modals/ScheduledTransactionMenuModal.tsx b/packages/desktop-client/src/components/modals/ScheduledTransactionMenuModal.tsx index a2ba2396df2..2866152c142 100644 --- a/packages/desktop-client/src/components/modals/ScheduledTransactionMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/ScheduledTransactionMenuModal.tsx @@ -3,8 +3,7 @@ import React, { type ComponentPropsWithoutRef, type CSSProperties, } from 'react'; - -import { t } from 'i18next'; +import { useTranslation } from 'react-i18next'; import { useSchedules } from 'loot-core/client/data-hooks/schedules'; import { format } from 'loot-core/shared/months'; @@ -28,6 +27,7 @@ export function ScheduledTransactionMenuModal({ onSkip, onPost, }: ScheduledTransactionMenuModalProps) { + const { t } = useTranslation(); const defaultMenuItemStyle: CSSProperties = { ...styles.mobileMenuItem, color: theme.menuItemText, @@ -98,6 +98,7 @@ function ScheduledTransactionMenu({ onPost, ...props }: ScheduledTransactionMenuProps) { + const { t } = useTranslation(); return ( a.name.localeCompare(b.name)); + const { t } = useTranslation(); const dispatch = useDispatch(); const localAccounts = useAccounts().filter(a => a.closed === 0); const [chosenAccounts, setChosenAccounts] = useState(() => { @@ -201,6 +201,7 @@ function TableRow({ unlinkedAccounts, onSetLinkedAccount, }) { + const { t } = useTranslation(); const [focusedField, setFocusedField] = useState(null); const availableAccountOptions = [ diff --git a/packages/desktop-client/src/components/modals/TrackingBalanceMenuModal.tsx b/packages/desktop-client/src/components/modals/TrackingBalanceMenuModal.tsx index cd14bd9f1b0..2a070152c53 100644 --- a/packages/desktop-client/src/components/modals/TrackingBalanceMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/TrackingBalanceMenuModal.tsx @@ -2,8 +2,7 @@ import React, { type ComponentPropsWithoutRef, type CSSProperties, } from 'react'; - -import { t } from 'i18next'; +import { useTranslation } from 'react-i18next'; import { trackingBudget } from 'loot-core/client/queries'; @@ -39,6 +38,7 @@ export function TrackingBalanceMenuModal({ borderTop: `1px solid ${theme.pillBorder}`, }; + const { t } = useTranslation(); const category = useCategory(categoryId); if (!category) { diff --git a/packages/desktop-client/src/components/modals/TrackingBudgetMenuModal.tsx b/packages/desktop-client/src/components/modals/TrackingBudgetMenuModal.tsx index 87e07e9e98a..33d8738caf7 100644 --- a/packages/desktop-client/src/components/modals/TrackingBudgetMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/TrackingBudgetMenuModal.tsx @@ -4,8 +4,7 @@ import React, { useEffect, type CSSProperties, } from 'react'; - -import { t } from 'i18next'; +import { useTranslation } from 'react-i18next'; import { trackingBudget } from 'loot-core/client/queries'; import { amountToInteger, integerToAmount } from 'loot-core/shared/util'; @@ -45,6 +44,7 @@ export function TrackingBudgetMenuModal({ borderTop: `1px solid ${theme.pillBorder}`, }; + const { t } = useTranslation(); const budgeted = useTrackingSheetValue( trackingBudget.catBudgeted(categoryId), ); diff --git a/packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx b/packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx new file mode 100644 index 00000000000..ed1d705521e --- /dev/null +++ b/packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx @@ -0,0 +1,240 @@ +import React, { useEffect, useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; + +import { + addNotification, + duplicateBudget, + uniqueBudgetName, + validateBudgetName, +} from 'loot-core/client/actions'; +import { type File } from 'loot-core/src/types/file'; + +import { theme } from '../../../style'; +import { Button, ButtonWithLoading } from '../../common/Button2'; +import { FormError } from '../../common/FormError'; +import { InitialFocus } from '../../common/InitialFocus'; +import { InlineField } from '../../common/InlineField'; +import { Input } from '../../common/Input'; +import { + Modal, + ModalButtons, + ModalCloseButton, + ModalHeader, +} from '../../common/Modal'; +import { Text } from '../../common/Text'; +import { View } from '../../common/View'; + +type DuplicateFileProps = { + file: File; + managePage?: boolean; + loadBudget?: 'none' | 'original' | 'copy'; + onComplete?: (event: { + status: 'success' | 'failed' | 'canceled'; + error?: object; + }) => void; +}; + +export function DuplicateFileModal({ + file, + managePage, + loadBudget = 'none', + onComplete, +}: DuplicateFileProps) { + const { t } = useTranslation(); + const fileEndingTranslation = t(' - copy'); + const [newName, setNewName] = useState(file.name + fileEndingTranslation); + const [nameError, setNameError] = useState(null); + + // If the state is "broken" that means it was created by another user. + const isCloudFile = 'cloudFileId' in file && file.state !== 'broken'; + const isLocalFile = 'id' in file; + const dispatch = useDispatch(); + + const [loadingState, setLoadingState] = useState<'cloud' | 'local' | null>( + null, + ); + + useEffect(() => { + (async () => { + setNewName(await uniqueBudgetName(file.name + fileEndingTranslation)); + })(); + }, [file.name, fileEndingTranslation]); + + const validateAndSetName = async (name: string) => { + const trimmedName = name.trim(); + const { valid, message } = await validateBudgetName(trimmedName); + if (valid) { + setNewName(trimmedName); + setNameError(null); + } else { + // The "Unknown error" should never happen, but this satifies type checking + setNameError(message ?? t('Unknown error with budget name')); + } + }; + + const handleDuplicate = async (sync: 'localOnly' | 'cloudSync') => { + const { valid, message } = await validateBudgetName(newName); + if (valid) { + setLoadingState(sync === 'cloudSync' ? 'cloud' : 'local'); + + try { + await dispatch( + duplicateBudget({ + id: 'id' in file ? file.id : undefined, + cloudId: + sync === 'cloudSync' && 'cloudFileId' in file + ? file.cloudFileId + : undefined, + oldName: file.name, + newName, + cloudSync: sync === 'cloudSync', + managePage, + loadBudget, + }), + ); + dispatch( + addNotification({ + type: 'message', + message: t('Duplicate file “{{newName}}” created.', { newName }), + }), + ); + if (onComplete) onComplete({ status: 'success' }); + } catch (e) { + const newError = new Error(t('Failed to duplicate budget')); + if (onComplete) onComplete({ status: 'failed', error: newError }); + else console.error('Failed to duplicate budget:', e); + dispatch( + addNotification({ + type: 'error', + message: t('Failed to duplicate budget file.'), + }), + ); + } finally { + setLoadingState(null); + } + } else { + const failError = new Error( + message ?? t('Unknown error with budget name'), + ); + if (onComplete) onComplete({ status: 'failed', error: failError }); + } + }; + + return ( + + {({ state: { close } }) => ( + + { + close(); + if (onComplete) onComplete({ status: 'canceled' }); + }} + /> + } + /> + + + + + setNewName(event.target.value)} + onBlur={event => validateAndSetName(event.target.value)} + style={{ flex: 1 }} + /> + + + {nameError && ( + + {nameError} + + )} + + {isLocalFile ? ( + isCloudFile && ( + + + Your budget is hosted on a server, making it accessible for + download on your devices. + + Would you like to duplicate this budget for all your devices + or keep it stored locally on this device? + + + ) + ) : ( + + + Unable to duplicate a budget that is not located on your + device. + + Please download the budget from the server before duplicating. + + + )} + + { + close(); + if (onComplete) onComplete({ status: 'canceled' }); + }} + > + Cancel + + {isLocalFile && isCloudFile && ( + handleDuplicate('cloudSync')} + > + Duplicate for all devices + + )} + {isLocalFile && ( + handleDuplicate('localOnly')} + > + Duplicate + {isCloudFile && locally} + + )} + + + + )} + + ); +} diff --git a/packages/desktop-client/src/components/reports/CategorySelector.tsx b/packages/desktop-client/src/components/reports/CategorySelector.tsx index b07b9d05be4..3b6268edd3a 100644 --- a/packages/desktop-client/src/components/reports/CategorySelector.tsx +++ b/packages/desktop-client/src/components/reports/CategorySelector.tsx @@ -1,7 +1,6 @@ // @ts-strict-ignore import React, { Fragment, useMemo, useState } from 'react'; - -import { t } from 'i18next'; +import { useTranslation } from 'react-i18next'; import { type CategoryEntity, @@ -34,6 +33,7 @@ export function CategorySelector({ setSelectedCategories, showHiddenCategories = true, }: CategorySelectorProps) { + const { t } = useTranslation(); const [uncheckedHidden, setUncheckedHidden] = useState(false); const filteredGroup = (categoryGroup: CategoryGroupEntity) => { return categoryGroup.categories.filter(f => { diff --git a/packages/desktop-client/src/components/reports/Overview.tsx b/packages/desktop-client/src/components/reports/Overview.tsx index ba32c5020e0..4ddf903b8e4 100644 --- a/packages/desktop-client/src/components/reports/Overview.tsx +++ b/packages/desktop-client/src/components/reports/Overview.tsx @@ -21,6 +21,7 @@ import { import { useAccounts } from '../../hooks/useAccounts'; import { useNavigate } from '../../hooks/useNavigate'; +import { useSyncedPref } from '../../hooks/useSyncedPref'; import { breakpoints } from '../../tokens'; import { Button } from '../common/Button2'; import { Menu } from '../common/Menu'; @@ -33,6 +34,7 @@ import { useResponsive } from '../responsive/ResponsiveProvider'; import { NON_DRAGGABLE_AREA_CLASS_NAME } from './constants'; import { LoadingIndicator } from './LoadingIndicator'; +import { CalendarCard } from './reports/CalendarCard'; import { CashFlowCard } from './reports/CashFlowCard'; import { CustomReportListCards } from './reports/CustomReportListCards'; import { MarkdownCard } from './reports/MarkdownCard'; @@ -50,6 +52,8 @@ function isCustomReportWidget(widget: Widget): widget is CustomReportWidget { export function Overview() { const { t } = useTranslation(); const dispatch = useDispatch(); + const [_firstDayOfWeekIdx] = useSyncedPref('firstDayOfWeekIdx'); + const firstDayOfWeekIdx = _firstDayOfWeekIdx || '0'; const triggerRef = useRef(null); const extraMenuTriggerRef = useRef(null); @@ -385,6 +389,10 @@ export function Overview() { name: 'summary-card' as const, text: t('Summary card'), }, + { + name: 'calendar-card' as const, + text: t('Calendar card'), + }, { name: 'custom-report' as const, text: t('New custom report'), @@ -534,6 +542,15 @@ export function Overview() { onMetaChange={newMeta => onMetaChange(item, newMeta)} onRemove={() => onRemoveWidget(item.i)} /> + ) : item.type === 'calendar-card' ? ( + onMetaChange(item, newMeta)} + onRemove={() => onRemoveWidget(item.i)} + /> ) : null} ))} diff --git a/packages/desktop-client/src/components/reports/ReportOptions.ts b/packages/desktop-client/src/components/reports/ReportOptions.ts index d8f2ed7fce6..953771f1f65 100644 --- a/packages/desktop-client/src/components/reports/ReportOptions.ts +++ b/packages/desktop-client/src/components/reports/ReportOptions.ts @@ -257,7 +257,7 @@ const transferCategory: UncategorizedEntity = { }; const offBudgetCategory: UncategorizedEntity = { id: '', - name: t('Off Budget'), + name: t('Off budget'), uncategorized_id: 'off_budget', hidden: false, }; @@ -271,7 +271,7 @@ type UncategorizedGroupEntity = Pick< }; const uncategorizedGroup: UncategorizedGroupEntity = { - name: t('Uncategorized & Off Budget'), + name: t('Uncategorized & Off budget'), id: 'uncategorized', hidden: false, uncategorized_id: 'all', diff --git a/packages/desktop-client/src/components/reports/ReportRouter.tsx b/packages/desktop-client/src/components/reports/ReportRouter.tsx index 7e98d4f4cb1..abccd452a8b 100644 --- a/packages/desktop-client/src/components/reports/ReportRouter.tsx +++ b/packages/desktop-client/src/components/reports/ReportRouter.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Route, Routes } from 'react-router-dom'; import { Overview } from './Overview'; +import { Calendar } from './reports/Calendar'; import { CashFlow } from './reports/CashFlow'; import { CustomReport } from './reports/CustomReport'; import { NetWorth } from './reports/NetWorth'; @@ -22,6 +23,8 @@ export function ReportRouter() { } /> } /> } /> + } /> + } /> ); } diff --git a/packages/desktop-client/src/components/reports/ReportSidebar.tsx b/packages/desktop-client/src/components/reports/ReportSidebar.tsx index a03b9c9b925..85f81d4007c 100644 --- a/packages/desktop-client/src/components/reports/ReportSidebar.tsx +++ b/packages/desktop-client/src/components/reports/ReportSidebar.tsx @@ -1,6 +1,5 @@ import React, { useMemo, useRef, useState } from 'react'; - -import { t } from 'i18next'; +import { useTranslation } from 'react-i18next'; import * as monthUtils from 'loot-core/src/shared/months'; import { type CategoryEntity } from 'loot-core/types/models/category'; @@ -90,6 +89,7 @@ export function ReportSidebar({ firstDayOfWeekIdx, isComplexCategoryCondition = false, }: ReportSidebarProps) { + const { t } = useTranslation(); const [menuOpen, setMenuOpen] = useState(false); const triggerRef = useRef(null); diff --git a/packages/desktop-client/src/components/reports/ReportTopbar.tsx b/packages/desktop-client/src/components/reports/ReportTopbar.tsx index 6242f563d73..2a973e44de9 100644 --- a/packages/desktop-client/src/components/reports/ReportTopbar.tsx +++ b/packages/desktop-client/src/components/reports/ReportTopbar.tsx @@ -1,6 +1,5 @@ import React, { type ComponentProps } from 'react'; - -import { t } from 'i18next'; +import { useTranslation } from 'react-i18next'; import { type CustomReportEntity } from 'loot-core/types/models/reports'; import { type RuleConditionEntity } from 'loot-core/types/models/rule'; @@ -53,6 +52,7 @@ export function ReportTopbar({ isItemDisabled, defaultItems, }: ReportTopbarProps) { + const { t } = useTranslation(); const onChangeGraph = (cond: string) => { setSessionReport('graphType', cond); onReportChange({ type: 'modify' }); diff --git a/packages/desktop-client/src/components/reports/SaveReportName.tsx b/packages/desktop-client/src/components/reports/SaveReportName.tsx index 84f9d66f93f..85ea6436585 100644 --- a/packages/desktop-client/src/components/reports/SaveReportName.tsx +++ b/packages/desktop-client/src/components/reports/SaveReportName.tsx @@ -1,7 +1,6 @@ import React, { type RefObject, useEffect } from 'react'; import { Form } from 'react-aria-components'; - -import { t } from 'i18next'; +import { useTranslation } from 'react-i18next'; import { type CustomReportEntity } from 'loot-core/types/models/reports'; @@ -38,6 +37,8 @@ export function SaveReportName({ err, report, }: SaveReportNameProps) { + const { t } = useTranslation(); + useEffect(() => { if (inputRef.current) { inputRef.current.focus(); diff --git a/packages/desktop-client/src/components/reports/graphs/CalendarGraph.tsx b/packages/desktop-client/src/components/reports/graphs/CalendarGraph.tsx new file mode 100644 index 00000000000..c2985176829 --- /dev/null +++ b/packages/desktop-client/src/components/reports/graphs/CalendarGraph.tsx @@ -0,0 +1,317 @@ +import { type Ref, useEffect, useState } from 'react'; +import { Trans } from 'react-i18next'; + +import { + addDays, + format, + getDate, + isSameMonth, + startOfMonth, + startOfWeek, +} from 'date-fns'; + +import { amountToCurrency } from 'loot-core/shared/util'; +import { type SyncedPrefs } from 'loot-core/types/prefs'; + +import { useResizeObserver } from '../../../hooks/useResizeObserver'; +import { styles, theme } from '../../../style'; +import { Button } from '../../common/Button2'; +import { Tooltip } from '../../common/Tooltip'; +import { View } from '../../common/View'; +import { PrivacyFilter } from '../../PrivacyFilter'; +import { chartTheme } from '../chart-theme'; + +type CalendarGraphProps = { + data: { + date: Date; + incomeValue: number; + expenseValue: number; + incomeSize: number; + expenseSize: number; + }[]; + start: Date; + firstDayOfWeekIdx?: SyncedPrefs['firstDayOfWeekIdx']; + onDayClick: (date: Date | null) => void; +}; +export function CalendarGraph({ + data, + start, + firstDayOfWeekIdx, + onDayClick, +}: CalendarGraphProps) { + const startingDate = startOfWeek(new Date(), { + weekStartsOn: + firstDayOfWeekIdx !== undefined && + !Number.isNaN(parseInt(firstDayOfWeekIdx)) && + parseInt(firstDayOfWeekIdx) >= 0 && + parseInt(firstDayOfWeekIdx) <= 6 + ? (parseInt(firstDayOfWeekIdx) as 0 | 1 | 2 | 3 | 4 | 5 | 6) + : 0, + }); + const [fontSize, setFontSize] = useState(14); + + const buttonRef = useResizeObserver(rect => { + const newValue = Math.floor(rect.height / 2); + if (newValue > 14) { + setFontSize(14); + } else { + setFontSize(newValue); + } + }); + + return ( + <> + onDayClick(null)} + > + {Array.from({ length: 7 }, (_, index) => ( + + {format(addDays(startingDate, index), 'EEEEE')} + + ))} + + + {data.map((day, index) => + !isSameMonth(day.date, startOfMonth(start)) ? ( + onDayClick(null)} + /> + ) : day.incomeValue !== 0 || day.expenseValue !== 0 ? ( + + + {format(day.date, 'MMM dd')} + + + + + Income: + + + {day.incomeValue !== 0 ? ( + + {amountToCurrency(day.incomeValue)} + + ) : ( + '' + )} + + + ( + + {Math.round(day.incomeSize * 100) / 100 + '%'} + + ) + + + Expenses: + + + {day.expenseValue !== 0 ? ( + + {amountToCurrency(day.expenseValue)} + + ) : ( + '' + )} + + + ( + + {Math.round(day.expenseSize * 100) / 100 + '%'} + + ) + + + + + } + placement="bottom end" + style={{ + ...styles.tooltip, + lineHeight: 1.5, + padding: '6px 10px', + }} + > + { + if (index === 15 && el) { + buttonRef(el); + } + }} + fontSize={fontSize} + day={day} + onPress={() => onDayClick(day.date)} + /> + + ) : ( + { + if (index === 15 && el) { + buttonRef(el); + } + }} + fontSize={fontSize} + day={day} + onPress={() => onDayClick(day.date)} + /> + ), + )} + + > + ); +} + +type DayButtonProps = { + fontSize: number; + resizeRef: Ref; + day: { + date: Date; + incomeSize: number; + expenseSize: number; + }; + onPress: () => void; +}; +function DayButton({ day, onPress, fontSize, resizeRef }: DayButtonProps) { + const [currentFontSize, setCurrentFontSize] = useState(fontSize); + + useEffect(() => { + setCurrentFontSize(fontSize); + }, [fontSize]); + + return ( + onPress()} + > + {day.expenseSize !== 0 && ( + + )} + {day.incomeSize !== 0 && ( + + )} + + + + + {getDate(day.date)} + + + ); +} diff --git a/packages/desktop-client/src/components/reports/reportRanges.ts b/packages/desktop-client/src/components/reports/reportRanges.ts index 1fa289b5f72..e909ab6752e 100644 --- a/packages/desktop-client/src/components/reports/reportRanges.ts +++ b/packages/desktop-client/src/components/reports/reportRanges.ts @@ -161,7 +161,10 @@ export function getFullRange(start: string) { export function getLatestRange(offset: number) { const end = monthUtils.currentMonth(); - const start = monthUtils.subMonths(end, offset); + let start = end; + if (offset !== 1) { + start = monthUtils.subMonths(end, offset); + } return [start, end, 'sliding-window'] as const; } diff --git a/packages/desktop-client/src/components/reports/reports/Calendar.tsx b/packages/desktop-client/src/components/reports/reports/Calendar.tsx new file mode 100644 index 00000000000..f064616fbc7 --- /dev/null +++ b/packages/desktop-client/src/components/reports/reports/Calendar.tsx @@ -0,0 +1,961 @@ +import React, { + useState, + useEffect, + useMemo, + useRef, + type Ref, + useCallback, +} from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; +import { useParams, useSearchParams } from 'react-router-dom'; +import { useSpring, animated, config } from 'react-spring'; + +import { css } from '@emotion/css'; +import { useDrag } from '@use-gesture/react'; +import { format, parseISO } from 'date-fns'; + +import { SchedulesProvider } from 'loot-core/client/data-hooks/schedules'; +import { useTransactions } from 'loot-core/client/data-hooks/transactions'; +import { useWidget } from 'loot-core/client/data-hooks/widget'; +import { send } from 'loot-core/platform/client/fetch'; +import { q, type Query } from 'loot-core/shared/query'; +import { ungroupTransactions } from 'loot-core/shared/transactions'; +import { amountToCurrency } from 'loot-core/shared/util'; +import { addNotification } from 'loot-core/src/client/actions'; +import * as monthUtils from 'loot-core/src/shared/months'; +import { + type RuleConditionEntity, + type CalendarWidget, + type TimeFrame, + type TransactionEntity, +} from 'loot-core/types/models'; + +import { useAccounts } from '../../../hooks/useAccounts'; +import { useCategories } from '../../../hooks/useCategories'; +import { useDateFormat } from '../../../hooks/useDateFormat'; +import { useFilters } from '../../../hooks/useFilters'; +import { useMergedRefs } from '../../../hooks/useMergedRefs'; +import { useNavigate } from '../../../hooks/useNavigate'; +import { usePayees } from '../../../hooks/usePayees'; +import { useResizeObserver } from '../../../hooks/useResizeObserver'; +import { SelectedProviderWithItems } from '../../../hooks/useSelected'; +import { SplitsExpandedProvider } from '../../../hooks/useSplitsExpanded'; +import { useSyncedPref } from '../../../hooks/useSyncedPref'; +import { + SvgArrowThickDown, + SvgArrowThickUp, + SvgCheveronDown, + SvgCheveronUp, +} from '../../../icons/v1'; +import { styles, theme } from '../../../style'; +import { Button } from '../../common/Button2'; +import { View } from '../../common/View'; +import { EditablePageHeaderTitle } from '../../EditablePageHeaderTitle'; +import { MobileBackButton } from '../../mobile/MobileBackButton'; +import { TransactionList as TransactionListMobile } from '../../mobile/transactions/TransactionList'; +import { MobilePageHeader, Page, PageHeader } from '../../Page'; +import { PrivacyFilter } from '../../PrivacyFilter'; +import { useResponsive } from '../../responsive/ResponsiveProvider'; +import { TransactionList } from '../../transactions/TransactionList'; +import { chartTheme } from '../chart-theme'; +import { DateRange } from '../DateRange'; +import { CalendarGraph } from '../graphs/CalendarGraph'; +import { Header } from '../Header'; +import { LoadingIndicator } from '../LoadingIndicator'; +import { calculateTimeRange } from '../reportRanges'; +import { + type CalendarDataType, + calendarSpreadsheet, +} from '../spreadsheets/calendar-spreadsheet'; +import { useReport } from '../useReport'; +import { fromDateRepr } from '../util'; + +const CHEVRON_HEIGHT = 42; +const SUMMARY_HEIGHT = 140; + +export function Calendar() { + const params = useParams(); + const [searchParams] = useSearchParams(); + const { data: widget, isLoading } = useWidget( + params.id ?? '', + 'calendar-card', + ); + + if (isLoading) { + return ; + } + + return ; +} + +type CalendarInnerProps = { + widget?: CalendarWidget; + parameters: URLSearchParams; +}; + +function CalendarInner({ widget, parameters }: CalendarInnerProps) { + const { t } = useTranslation(); + const [initialStart, initialEnd, initialMode] = calculateTimeRange( + widget?.meta?.timeFrame, + { + start: monthUtils.dayFromDate(monthUtils.currentMonth()), + end: monthUtils.currentDay(), + mode: 'full', + }, + ); + const [start, setStart] = useState(initialStart); + const [end, setEnd] = useState(initialEnd); + const [mode, setMode] = useState(initialMode); + const [query, setQuery] = useState(undefined); + const [dirty, setDirty] = useState(false); + + const { transactions: transactionsGrouped, loadMore: loadMoreTransactions } = + useTransactions({ query }); + + const allTransactions = useMemo( + () => ungroupTransactions(transactionsGrouped as TransactionEntity[]), + [transactionsGrouped], + ); + + const accounts = useAccounts(); + const payees = usePayees(); + const { grouped: categoryGroups } = useCategories(); + + const [_firstDayOfWeekIdx] = useSyncedPref('firstDayOfWeekIdx'); + const firstDayOfWeekIdx = _firstDayOfWeekIdx || '0'; + const { + conditions, + conditionsOp, + onApply: onApplyFilter, + onDelete: onDeleteFilter, + onUpdate: onUpdateFilter, + onConditionsOpChange, + } = useFilters(widget?.meta?.conditions, widget?.meta?.conditionsOp); + + useEffect(() => { + const day = parameters.get('day'); + const month = parameters.get('month'); + + if (day && onApplyFilter) { + onApplyFilter({ + conditions: [ + ...(widget?.meta?.conditions || []), + { + op: 'is', + field: 'date', + value: day, + } as RuleConditionEntity, + ], + conditionsOp: 'and', + id: [], + }); + } + + if (month && onApplyFilter) { + onApplyFilter({ + conditions: [ + ...(widget?.meta?.conditions || []), + { + field: 'date', + op: 'is', + value: month, + options: { + month: true, + }, + }, + ], + conditionsOp: 'and', + id: [], + }); + } + }, [widget?.meta?.conditions, onApplyFilter, parameters]); + + const params = useMemo(() => { + if (dirty === true) { + setDirty(false); + } + + return calendarSpreadsheet( + start, + end, + conditions, + conditionsOp, + firstDayOfWeekIdx, + ); + }, [start, end, conditions, conditionsOp, firstDayOfWeekIdx, dirty]); + + const [sortField, setSortField] = useState(''); + const [ascDesc, setAscDesc] = useState('desc'); + + useEffect(() => { + const conditionsOpKey = conditionsOp === 'or' ? '$or' : '$and'; + + send('make-filters-from-conditions', { + conditions: conditions.filter(cond => !cond.customName), + }) + .then((data: { filters: unknown[] }) => { + let query = q('transactions') + .filter({ + [conditionsOpKey]: data.filters, + }) + .filter({ + $and: [ + { date: { $gte: monthUtils.firstDayOfMonth(start) } }, + { date: { $lte: monthUtils.lastDayOfMonth(end) } }, + ], + }) + .select('*'); + + if (sortField) { + query = query.orderBy({ + [getField(sortField)]: ascDesc, + }); + } + + setQuery(query.options({ splits: 'grouped' })); + }) + .catch((error: unknown) => { + console.error('Error generating filters:', error); + }); + }, [start, end, conditions, conditionsOp, sortField, ascDesc]); + + const [flexAlignment, setFlexAlignment] = useState('center'); + const scrollbarContainer = useRef(null); + const ref = useResizeObserver(() => { + setFlexAlignment( + scrollbarContainer.current && + scrollbarContainer.current.scrollWidth > + scrollbarContainer.current.clientWidth + ? 'flex-start' + : 'center', + ); + }); + const mergedRef = useMergedRefs( + ref, + scrollbarContainer, + ) as Ref; + + const data = useReport('calendar', params); + + const [allMonths, setAllMonths] = useState< + Array<{ + name: string; + pretty: string; + }> + >([]); + + useEffect(() => { + async function run() { + try { + const trans = await send('get-earliest-transaction'); + const currentMonth = monthUtils.currentMonth(); + let earliestMonth = trans + ? monthUtils.monthFromDate(parseISO(fromDateRepr(trans.date))) + : currentMonth; + + // Make sure the month selects are at least populates with a + // year's worth of months. We can undo this when we have fancier + // date selects. + const yearAgo = monthUtils.subMonths(monthUtils.currentMonth(), 12); + if (earliestMonth > yearAgo) { + earliestMonth = yearAgo; + } + + const allMonths = monthUtils + .rangeInclusive(earliestMonth, monthUtils.currentMonth()) + .map(month => ({ + name: month, + pretty: monthUtils.format(month, 'MMMM, yyyy'), + })) + .reverse(); + + setAllMonths(allMonths); + } catch (error) { + console.error('Error fetching earliest transaction:', error); + } + } + run(); + }, []); + + const dispatch = useDispatch(); + const navigate = useNavigate(); + const { isNarrowWidth } = useResponsive(); + const title = widget?.meta?.name || t('Calendar'); + const table = useRef(null); + const dateFormat = useDateFormat(); + + const onSaveWidgetName = async (newName: string) => { + if (!widget) { + throw new Error('No widget that could be saved.'); + } + + const name = newName || t('Calendar'); + await send('dashboard-update-widget', { + id: widget.id, + meta: { + ...(widget.meta ?? {}), + name, + }, + }); + }; + + function onChangeDates(start: string, end: string, mode: TimeFrame['mode']) { + setStart(start); + setEnd(end); + setMode(mode); + } + + async function onSaveWidget() { + if (!widget) { + throw new Error('No widget that could be saved.'); + } + + try { + await send('dashboard-update-widget', { + id: widget.id, + meta: { + ...(widget.meta ?? {}), + conditions, + conditionsOp, + timeFrame: { + start, + end, + mode, + }, + }, + }); + dispatch( + addNotification({ + type: 'message', + message: t('Dashboard widget successfully saved.'), + }), + ); + } catch (error) { + dispatch( + addNotification({ + type: 'error', + message: t('Failed to save dashboard widget.'), + }), + ); + console.error('Error saving widget:', error); + } + } + const { totalIncome, totalExpense } = useMemo(() => { + if (!data || !data.calendarData) { + return { totalIncome: 0, totalExpense: 0 }; + } + return { + totalIncome: data.calendarData.reduce( + (prev, cur) => prev + cur.totalIncome, + 0, + ), + totalExpense: data.calendarData.reduce( + (prev, cur) => prev + cur.totalExpense, + 0, + ), + }; + }, [data]); + + const onSort = useCallback( + (headerClicked: string, ascDesc: 'asc' | 'desc') => { + if (headerClicked === sortField) { + setAscDesc(ascDesc); + } else { + setSortField(headerClicked); + setAscDesc('desc'); + } + }, + [sortField], + ); + + const onOpenTransaction = useCallback( + (transaction: TransactionEntity) => { + navigate(`/transactions/${transaction.id}`); + }, + [navigate], + ); + + const refContainer = useRef(null); + + useEffect(() => { + if (refContainer.current) { + setTotalHeight(refContainer.current.clientHeight - SUMMARY_HEIGHT); + } + }, [query]); + + const [totalHeight, setTotalHeight] = useState(0); + const closeY = useRef(3000); + + const openY = 0; + const [mobileTransactionsOpen, setMobileTransactionsOpen] = useState(false); + + const [{ y }, api] = useSpring(() => ({ + y: closeY.current, + immediate: false, + })); + + useEffect(() => { + closeY.current = totalHeight; + api.start({ + y: mobileTransactionsOpen ? openY : closeY.current, + immediate: false, + }); + }, [totalHeight, mobileTransactionsOpen, api]); + + const open = useCallback( + ({ canceled }: { canceled: boolean }) => { + api.start({ + y: openY, + immediate: false, + config: canceled ? config.wobbly : config.stiff, + }); + setMobileTransactionsOpen(true); + }, + [api], + ); + + const close = useCallback( + (velocity = 0) => { + api.start({ + y: closeY.current, + config: { ...config.stiff, velocity }, + }); + setMobileTransactionsOpen(false); + }, + [api], + ); + + const bind = useDrag( + ({ offset: [, oy], cancel }) => { + if (oy < 0) { + cancel(); + api.start({ y: 0, immediate: true }); + return; + } + + if (oy > totalHeight * 0.05 && mobileTransactionsOpen) { + cancel(); + close(); + setMobileTransactionsOpen(false); + } else if (!mobileTransactionsOpen) { + if (oy / totalHeight > 0.05) { + cancel(); + open({ canceled: true }); + setMobileTransactionsOpen(true); + } else { + api.start({ y: oy, immediate: true }); + } + } + }, + { + from: () => [0, y.get()], + filterTaps: true, + bounds: { + top: -totalHeight + CHEVRON_HEIGHT, + bottom: totalHeight - CHEVRON_HEIGHT, + }, + axis: 'y', + rubberband: true, + }, + ); + + return ( + navigate('/reports')} /> + } + /> + ) : ( + + ) : ( + title + ) + } + /> + ) + } + padding={0} + > + + + {widget && ( + + Save widget + + )} + + + } style={{ flexGrow: 1 }}> + + + {data && ( + + {data.calendarData.map((calendar, index) => ( + + ))} + + )} + + + + []} + registerDispatch={() => {}} + selectAllFilter={(item: TransactionEntity) => + !item._unmatched && !item.is_parent + } + > + + + {!isNarrowWidth ? ( + + false} + isMatched={() => false} + isFiltered={() => true} + dateFormat={dateFormat} + hideFraction={false} + addNotification={addNotification} + renderEmpty={() => ( + + No transactions + + )} + onSort={onSort} + sortField={sortField} + ascDesc={ascDesc} + onChange={() => {}} + onRefetch={() => setDirty(true)} + onCloseAddTransaction={() => {}} + onCreatePayee={() => {}} + onApplyFilter={() => {}} + onBatchDelete={() => {}} + onBatchDuplicate={() => {}} + onBatchLinkSchedule={() => {}} + onBatchUnlinkSchedule={() => {}} + onCreateRule={() => {}} + onScheduleAction={() => {}} + onMakeAsNonSplitTransactions={() => {}} + showSelection={false} + allowSplitTransaction={false} + /> + + ) : ( + + + !mobileTransactionsOpen + ? open({ canceled: false }) + : close() + } + className={css({ + color: theme.pageTextSubdued, + height: 42, + '&[data-pressed]': { backgroundColor: 'transparent' }, + })} + > + {!mobileTransactionsOpen && ( + <> + + Show transactions + > + )} + {mobileTransactionsOpen && ( + <> + + Hide transactions + > + )} + + + + + + )} + + + + + + ); +} + +type CalendarWithHeaderProps = { + calendar: { + start: Date; + end: Date; + data: CalendarDataType[]; + totalExpense: number; + totalIncome: number; + }; + onApplyFilter: ( + conditions: + | null + | RuleConditionEntity + | { + conditions: RuleConditionEntity[]; + conditionsOp: 'and' | 'or'; + id: RuleConditionEntity[]; + }, + ) => void; + firstDayOfWeekIdx: string; + conditions: RuleConditionEntity[]; + conditionsOp: 'and' | 'or'; +}; + +function CalendarWithHeader({ + calendar, + onApplyFilter, + firstDayOfWeekIdx, + conditions, + conditionsOp, +}: CalendarWithHeaderProps) { + const { t } = useTranslation(); + + return ( + + onApplyFilter({ + conditions: [...conditions.filter(f => f.field !== 'date')], + conditionsOp, + id: [], + }) + } + > + + { + onApplyFilter({ + conditions: [ + ...conditions.filter(f => f.field !== 'date'), + { + field: 'date', + op: 'is', + value: format(calendar.start, 'yyyy-MM'), + options: { + month: true, + }, + }, + ], + conditionsOp: 'and', + id: [], + }); + }} + > + {format(calendar.start, 'MMMM yyyy')} + + + + + + {amountToCurrency(calendar.totalIncome)} + + + + + + {amountToCurrency(calendar.totalExpense)} + + + + + + { + if (date) { + onApplyFilter({ + conditions: [ + ...conditions.filter(f => f.field !== 'date'), + { + field: 'date', + op: 'is', + value: format(date, 'yyyy-MM-dd'), + }, + ], + conditionsOp: 'and', + id: [], + }); + } else { + onApplyFilter({ + conditions: [...conditions.filter(f => f.field !== 'date')], + conditionsOp: 'and', + id: [], + }); + } + }} + firstDayOfWeekIdx={firstDayOfWeekIdx} + /> + + + ); +} + +type CalendarCardHeaderProps = { + start: string; + end: string; + totalIncome: number; + totalExpense: number; + isNarrowWidth: boolean; +}; + +function CalendarCardHeader({ + start, + end, + totalIncome, + totalExpense, + isNarrowWidth, +}: CalendarCardHeaderProps) { + return ( + + + + + + + Income: + + + {amountToCurrency(totalIncome)} + + + + Expenses: + + + {amountToCurrency(totalExpense)} + + + + + + ); +} + +function getField(field?: string) { + if (!field) { + return 'date'; + } + + switch (field) { + case 'account': + return 'account.name'; + case 'payee': + return 'payee.name'; + case 'category': + return 'category.name'; + case 'payment': + return 'amount'; + case 'deposit': + return 'amount'; + default: + return field; + } +} diff --git a/packages/desktop-client/src/components/reports/reports/CalendarCard.tsx b/packages/desktop-client/src/components/reports/reports/CalendarCard.tsx new file mode 100644 index 00000000000..c49d07af18e --- /dev/null +++ b/packages/desktop-client/src/components/reports/reports/CalendarCard.tsx @@ -0,0 +1,543 @@ +import React, { + useState, + useMemo, + useRef, + type Ref, + useEffect, + type Dispatch, + type SetStateAction, + useCallback, +} from 'react'; +import { Trans, useTranslation } from 'react-i18next'; + +import { format } from 'date-fns'; +import { debounce } from 'debounce'; + +import { amountToCurrency } from 'loot-core/shared/util'; +import * as monthUtils from 'loot-core/src/shared/months'; +import { type CalendarWidget } from 'loot-core/types/models'; +import { type SyncedPrefs } from 'loot-core/types/prefs'; + +import { useMergedRefs } from '../../../hooks/useMergedRefs'; +import { useNavigate } from '../../../hooks/useNavigate'; +import { useResizeObserver } from '../../../hooks/useResizeObserver'; +import { SvgArrowThickDown, SvgArrowThickUp } from '../../../icons/v1'; +import { styles, theme } from '../../../style'; +import { Block } from '../../common/Block'; +import { Button } from '../../common/Button2'; +import { Tooltip } from '../../common/Tooltip'; +import { View } from '../../common/View'; +import { PrivacyFilter } from '../../PrivacyFilter'; +import { useResponsive } from '../../responsive/ResponsiveProvider'; +import { chartTheme } from '../chart-theme'; +import { DateRange } from '../DateRange'; +import { CalendarGraph } from '../graphs/CalendarGraph'; +import { LoadingIndicator } from '../LoadingIndicator'; +import { ReportCard } from '../ReportCard'; +import { ReportCardName } from '../ReportCardName'; +import { calculateTimeRange } from '../reportRanges'; +import { + type CalendarDataType, + calendarSpreadsheet, +} from '../spreadsheets/calendar-spreadsheet'; +import { useReport } from '../useReport'; + +type CalendarCardProps = { + widgetId: string; + isEditing?: boolean; + meta?: CalendarWidget['meta']; + onMetaChange: (newMeta: CalendarWidget['meta']) => void; + onRemove: () => void; + firstDayOfWeekIdx?: SyncedPrefs['firstDayOfWeekIdx']; +}; + +export function CalendarCard({ + widgetId, + isEditing, + meta = {}, + onMetaChange, + onRemove, + firstDayOfWeekIdx, +}: CalendarCardProps) { + const { t } = useTranslation(); + const [start, end] = calculateTimeRange(meta?.timeFrame, { + start: monthUtils.dayFromDate(monthUtils.currentMonth()), + end: monthUtils.currentDay(), + mode: 'full', + }); + const params = useMemo( + () => + calendarSpreadsheet( + start, + end, + meta?.conditions, + meta?.conditionsOp, + firstDayOfWeekIdx, + ), + [start, end, meta?.conditions, meta?.conditionsOp, firstDayOfWeekIdx], + ); + + const [cardOrientation, setCardOrientation] = useState<'row' | 'column'>( + 'row', + ); + const { isNarrowWidth } = useResponsive(); + + const cardRef = useResizeObserver(rect => { + if (rect.height > rect.width) { + setCardOrientation('column'); + } else { + setCardOrientation('row'); + } + }); + + const data = useReport('calendar', params); + + const [nameMenuOpen, setNameMenuOpen] = useState(false); + + const { totalIncome, totalExpense } = useMemo(() => { + if (!data) { + return { totalIncome: 0, totalExpense: 0 }; + } + return { + totalIncome: data.calendarData.reduce( + (prev, cur) => prev + cur.totalIncome, + 0, + ), + totalExpense: data.calendarData.reduce( + (prev, cur) => prev + cur.totalExpense, + 0, + ), + }; + }, [data]); + + const [monthNameFormats, setMonthNameFormats] = useState([]); + const [selectedMonthNameFormat, setSelectedMonthNameFormat] = + useState('MMMM yyyy'); + + useEffect(() => { + if (data) { + setMonthNameFormats( + Array(data.calendarData.length).map(() => 'MMMM yyyy'), + ); + } else { + setMonthNameFormats([]); + } + }, [data]); + + useEffect(() => { + if (monthNameFormats.length) { + setSelectedMonthNameFormat( + monthNameFormats.reduce( + (a, b) => ((a?.length ?? 0) <= (b?.length ?? 0) ? a : b), + 'MMMM yyyy', + ), + ); + } else { + setSelectedMonthNameFormat('MMMM yyyy'); + } + }, [monthNameFormats]); + + const calendarLenSize = useMemo(() => { + if (!data) { + return 0; + } + + return data?.calendarData.length; + }, [data]); + + return ( + { + switch (item) { + case 'rename': + setNameMenuOpen(true); + break; + case 'remove': + onRemove(); + break; + default: + throw new Error(`Unrecognized selection: ${item}`); + } + }} + > + el && cardRef(el)} + style={{ flex: 1, margin: 2, overflow: 'hidden', width: '100%' }} + > + + + { + onMetaChange({ + ...meta, + name: newName, + }); + setNameMenuOpen(false); + }} + onClose={() => setNameMenuOpen(false)} + /> + + + + + + {totalIncome !== 0 && ( + <> + + Income: + + + {totalIncome !== 0 ? ( + + {amountToCurrency(totalIncome)} + + ) : ( + '' + )} + + > + )} + {totalExpense !== 0 && ( + <> + + Expenses: + + + {totalExpense !== 0 ? ( + + {amountToCurrency(totalExpense)} + + ) : ( + '' + )} + + > + )} + + + } + > + + + + + + 4 + ? 'auto' + : 'hidden' + : 'hidden', + ...styles.horizontalScrollbar, + }} + > + 4 + ? `${100 + ((calendarLenSize - 4) % 4) * 25}%` + : 'auto' + : 'auto', + }} + > + {data ? ( + data.calendarData.map((calendar, index) => ( + + )) + ) : ( + + )} + + + + + ); +} + +type CalendarCardInnerProps = { + calendar: { + start: Date; + end: Date; + data: CalendarDataType[]; + totalExpense: number; + totalIncome: number; + }; + firstDayOfWeekIdx: string; + setMonthNameFormats: Dispatch>; + selectedMonthNameFormat: string; + index: number; + widgetId: string; +}; +function CalendarCardInner({ + calendar, + firstDayOfWeekIdx, + setMonthNameFormats, + selectedMonthNameFormat, + index, + widgetId, +}: CalendarCardInnerProps) { + const [monthNameVisible, setMonthNameVisible] = useState(true); + const monthFormatSizeContainers = useRef<(HTMLSpanElement | null)[]>( + new Array(5), + ); + const monthNameContainerRef = useRef(null); + + const measureMonthFormats = useCallback(() => { + const measurements = monthFormatSizeContainers.current.map(container => ({ + width: container?.clientWidth ?? 0, + format: container?.getAttribute('data-format') ?? '', + })); + return measurements; + }, []); + + const debouncedResizeCallback = useMemo( + () => + debounce(() => { + const measurements = measureMonthFormats(); + const containerWidth = monthNameContainerRef.current?.clientWidth ?? 0; + + const suitableFormat = measurements.find(m => containerWidth > m.width); + if (suitableFormat) { + if ( + monthNameContainerRef.current && + containerWidth > suitableFormat.width + ) { + setMonthNameFormats(prev => { + const newArray = [...prev]; + newArray[index] = suitableFormat.format; + return newArray; + }); + + setMonthNameVisible(true); + return; + } + } + + if ( + monthNameContainerRef.current && + monthNameContainerRef.current.scrollWidth > + monthNameContainerRef.current.clientWidth + ) { + setMonthNameVisible(false); + } else { + setMonthNameVisible(true); + } + }, 20), + [measureMonthFormats, monthNameContainerRef, index, setMonthNameFormats], + ); + + const monthNameResizeRef = useResizeObserver(debouncedResizeCallback); + + useEffect(() => { + return () => { + debouncedResizeCallback?.clear(); + }; + }, [debouncedResizeCallback]); + + const mergedRef = useMergedRefs( + monthNameContainerRef, + monthNameResizeRef, + ) as Ref; + + const navigate = useNavigate(); + + const monthFormats = [ + { format: 'MMMM yyyy', text: format(calendar.start, 'MMMM yyyy') }, + { format: 'MMM yyyy', text: format(calendar.start, 'MMM yyyy') }, + { format: 'MMM yy', text: format(calendar.start, 'MMM yy') }, + { format: 'MMM', text: format(calendar.start, 'MMM') }, + { format: '', text: '' }, + ]; + + return ( + + + + { + navigate( + `/reports/calendar/${widgetId}?month=${format(calendar.start, 'yyyy-MM')}`, + ); + }} + > + {selectedMonthNameFormat && + format(calendar.start, selectedMonthNameFormat)} + + + + + {calendar.totalIncome !== 0 ? ( + <> + + + {amountToCurrency(calendar.totalIncome)} + + > + ) : ( + '' + )} + + + {calendar.totalExpense !== 0 ? ( + <> + + + {amountToCurrency(calendar.totalExpense)} + + > + ) : ( + '' + )} + + + + { + if (date) { + navigate( + `/reports/calendar/${widgetId}?day=${format(date, 'yyyy-MM-dd')}`, + ); + } else { + navigate(`/reports/calendar/${widgetId}`); + } + }} + /> + + {monthFormats.map((item, idx) => ( + { + if (node) monthFormatSizeContainers.current[idx] = node; + }} + style={{ position: 'fixed', top: -9999, left: -9999 }} + data-format={item.format} + > + {item.text} + {item.text && ':'} + + ))} + + + ); +} diff --git a/packages/desktop-client/src/components/reports/reports/CustomReportListCards.tsx b/packages/desktop-client/src/components/reports/reports/CustomReportListCards.tsx index 149e5e16ae3..118bd0d252d 100644 --- a/packages/desktop-client/src/components/reports/reports/CustomReportListCards.tsx +++ b/packages/desktop-client/src/components/reports/reports/CustomReportListCards.tsx @@ -1,8 +1,7 @@ import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; -import { t } from 'i18next'; - import { send, sendCatch } from 'loot-core/platform/client/fetch/index'; import { addNotification } from 'loot-core/src/client/actions'; import { calculateHasWarning } from 'loot-core/src/client/reports'; @@ -37,6 +36,8 @@ export function CustomReportListCards({ report, onRemove, }: CustomReportListCardsProps) { + const { t } = useTranslation(); + // It's possible for a dashboard to reference a non-existing // custom report if (!report) { diff --git a/packages/desktop-client/src/components/reports/reports/GetCardData.tsx b/packages/desktop-client/src/components/reports/reports/GetCardData.tsx index c9b50d71163..8c51b203c1b 100644 --- a/packages/desktop-client/src/components/reports/reports/GetCardData.tsx +++ b/packages/desktop-client/src/components/reports/reports/GetCardData.tsx @@ -1,7 +1,6 @@ import React, { useMemo } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; - -import { t } from 'i18next'; +import { useTranslation } from 'react-i18next'; import * as monthUtils from 'loot-core/src/shared/months'; import { type AccountEntity } from 'loot-core/types/models/account'; @@ -24,6 +23,7 @@ import { createGroupedSpreadsheet } from '../spreadsheets/grouped-spreadsheet'; import { useReport } from '../useReport'; function ErrorFallback() { + const { t } = useTranslation(); return ( <> diff --git a/packages/desktop-client/src/components/reports/spreadsheets/calendar-spreadsheet.ts b/packages/desktop-client/src/components/reports/spreadsheets/calendar-spreadsheet.ts new file mode 100644 index 00000000000..eb50c66fdf3 --- /dev/null +++ b/packages/desktop-client/src/components/reports/spreadsheets/calendar-spreadsheet.ts @@ -0,0 +1,267 @@ +import * as d from 'date-fns'; + +import { runQuery } from 'loot-core/src/client/query-helpers'; +import { type useSpreadsheet } from 'loot-core/src/client/SpreadsheetProvider'; +import { send } from 'loot-core/src/platform/client/fetch'; +import * as monthUtils from 'loot-core/src/shared/months'; +import { q } from 'loot-core/src/shared/query'; +import { type RuleConditionEntity } from 'loot-core/types/models'; +import { type SyncedPrefs } from 'loot-core/types/prefs'; + +export type CalendarDataType = { + date: Date; + incomeValue: number; + expenseValue: number; + incomeSize: number; + expenseSize: number; +}; +export function calendarSpreadsheet( + start: string, + end: string, + conditions: RuleConditionEntity[] = [], + conditionsOp: 'and' | 'or' = 'and', + firstDayOfWeekIdx?: SyncedPrefs['firstDayOfWeekIdx'], +) { + return async ( + spreadsheet: ReturnType, + setData: (data: { + calendarData: { + start: Date; + end: Date; + data: CalendarDataType[]; + totalExpense: number; + totalIncome: number; + }[]; + }) => void, + ) => { + let filters; + + try { + const { filters: filtersLocal } = await send( + 'make-filters-from-conditions', + { + conditions: conditions.filter(cond => !cond.customName), + }, + ); + filters = filtersLocal; + } catch (error) { + console.error('Failed to make filters from conditions:', error); + filters = []; + } + const conditionsOpKey = conditionsOp === 'or' ? '$or' : '$and'; + + let startDay: Date; + try { + startDay = d.parse( + monthUtils.firstDayOfMonth(start), + 'yyyy-MM-dd', + new Date(), + ); + } catch (error) { + console.error('Failed to parse start date:', error); + throw new Error('Invalid start date format'); + } + + let endDay: Date; + try { + endDay = d.parse( + monthUtils.lastDayOfMonth(end), + 'yyyy-MM-dd', + new Date(), + ); + } catch (error) { + console.error('Failed to parse end date:', error); + throw new Error('Invalid end date format'); + } + + const makeRootQuery = () => + q('transactions') + .filter({ + $and: [ + { date: { $gte: d.format(startDay, 'yyyy-MM-dd') } }, + { date: { $lte: d.format(endDay, 'yyyy-MM-dd') } }, + ], + }) + .filter({ + [conditionsOpKey]: filters, + }) + .groupBy(['date']) + .select(['date', { amount: { $sum: '$amount' } }]); + + let expenseData; + try { + expenseData = await runQuery( + makeRootQuery().filter({ + $and: { amount: { $lt: 0 } }, + }), + ); + } catch (error) { + console.error('Failed to fetch expense data:', error); + expenseData = { data: [] }; + } + + let incomeData; + try { + incomeData = await runQuery( + makeRootQuery().filter({ + $and: { amount: { $gt: 0 } }, + }), + ); + } catch (error) { + console.error('Failed to fetch income data:', error); + incomeData = { data: [] }; + } + + const getOneDatePerMonth = (start: Date, end: Date) => { + const months = []; + let currentDate = d.startOfMonth(start); + + while (!d.isSameMonth(currentDate, end)) { + months.push(currentDate); + currentDate = d.addMonths(currentDate, 1); + } + months.push(end); + + return months; + }; + + setData( + recalculate( + incomeData.data, + expenseData.data, + getOneDatePerMonth(startDay, endDay), + start, + firstDayOfWeekIdx, + ), + ); + }; +} + +function recalculate( + incomeData: Array<{ + date: string; + amount: number; + }>, + expenseData: Array<{ + date: string; + amount: number; + }>, + months: Date[], + start: string, + firstDayOfWeekIdx?: SyncedPrefs['firstDayOfWeekIdx'], +) { + const incomeDataMap = new Map(); + incomeData.forEach(item => { + incomeDataMap.set(item.date, item.amount); + }); + + const expenseDataMap = new Map(); + expenseData.forEach(item => { + expenseDataMap.set(item.date, item.amount); + }); + + const parseAndCacheDate = (() => { + const cache = new Map(); + return (dateStr: string) => { + if (!cache.has(dateStr)) { + cache.set(dateStr, d.parse(dateStr, 'yyyy-MM-dd', new Date())); + } + return cache.get(dateStr)!; + }; + })(); + + const getDaysArray = (month: Date) => { + const expenseValues = expenseData + .filter(f => d.isSameMonth(parseAndCacheDate(f.date), month)) + .map(m => Math.abs(m.amount)); + const incomeValues = incomeData + .filter(f => d.isSameMonth(parseAndCacheDate(f.date), month)) + .map(m => Math.abs(m.amount)); + + const totalExpenseValue = expenseValues.length + ? expenseValues.reduce((acc, val) => acc + val, 0) + : null; + + const totalIncomeValue = incomeValues.length + ? incomeValues.reduce((acc, val) => acc + val, 0) + : null; + + const getBarLength = (value: number) => { + if ( + value < 0 && + typeof totalExpenseValue === 'number' && + totalExpenseValue > 0 + ) { + const result = (Math.abs(value) / totalExpenseValue) * 100; + return Number.isFinite(result) ? result : 0; + } else if ( + value > 0 && + typeof totalIncomeValue === 'number' && + totalIncomeValue > 0 + ) { + const result = (value / totalIncomeValue) * 100; + return Number.isFinite(result) ? result : 0; + } else { + return 0; + } + }; + + const firstDay = d.startOfMonth(month); + const beginDay = d.startOfWeek(firstDay, { + weekStartsOn: + firstDayOfWeekIdx !== undefined && + !Number.isNaN(parseInt(firstDayOfWeekIdx)) && + parseInt(firstDayOfWeekIdx) >= 0 && + parseInt(firstDayOfWeekIdx) <= 6 + ? (parseInt(firstDayOfWeekIdx) as 0 | 1 | 2 | 3 | 4 | 5 | 6) + : 0, + }); + let totalDays = + d.differenceInDays(firstDay, beginDay) + d.getDaysInMonth(firstDay); + if (totalDays % 7 !== 0) { + totalDays += 7 - (totalDays % 7); + } + const daysArray = []; + + for (let i = 0; i < totalDays; i++) { + const currentDate = d.addDays(beginDay, i); + if (!d.isSameMonth(currentDate, firstDay)) { + daysArray.push({ + date: currentDate, + incomeValue: 0, + expenseValue: 0, + incomeSize: 0, + expenseSize: 0, + }); + } else { + const dateKey = d.format(currentDate, 'yyyy-MM-dd'); + const currentIncome = incomeDataMap.get(dateKey) ?? 0; + const currentExpense = expenseDataMap.get(dateKey) ?? 0; + + daysArray.push({ + date: currentDate, + incomeSize: getBarLength(currentIncome), + incomeValue: Math.abs(currentIncome) / 100, + expenseSize: getBarLength(currentExpense), + expenseValue: Math.abs(currentExpense) / 100, + }); + } + } + + return { + data: daysArray as CalendarDataType[], + totalExpense: (totalExpenseValue ?? 0) / 100, + totalIncome: (totalIncomeValue ?? 0) / 100, + }; + }; + + return { + calendarData: months.map(m => { + return { + ...getDaysArray(m), + start: d.startOfMonth(m), + end: d.endOfMonth(m), + }; + }), + }; +} diff --git a/packages/desktop-client/src/components/rules/ActionExpression.tsx b/packages/desktop-client/src/components/rules/ActionExpression.tsx index 0466be5895a..997a2af25aa 100644 --- a/packages/desktop-client/src/components/rules/ActionExpression.tsx +++ b/packages/desktop-client/src/components/rules/ActionExpression.tsx @@ -1,6 +1,5 @@ import React, { type CSSProperties } from 'react'; - -import { t } from 'i18next'; +import { useTranslation } from 'react-i18next'; import { mapField, @@ -68,6 +67,7 @@ function SetActionExpression({ value, options, }: SetRuleActionEntity) { + const { t } = useTranslation(); return ( <> {friendlyOp(op)}{' '} diff --git a/packages/desktop-client/src/components/rules/ConditionExpression.tsx b/packages/desktop-client/src/components/rules/ConditionExpression.tsx index 229868a338f..cc2d092bcc5 100644 --- a/packages/desktop-client/src/components/rules/ConditionExpression.tsx +++ b/packages/desktop-client/src/components/rules/ConditionExpression.tsx @@ -49,7 +49,11 @@ export function ConditionExpression({ {prefix && {prefix} } {mapField(field, options)}{' '} {friendlyOp(op)}{' '} - + {!['onbudget', 'offbudget'].includes( + (op as string)?.toLocaleLowerCase(), + ) && ( + + )} ); } diff --git a/packages/desktop-client/src/components/rules/RulesHeader.tsx b/packages/desktop-client/src/components/rules/RulesHeader.tsx index 07d034a0956..a4d6d5ffa74 100644 --- a/packages/desktop-client/src/components/rules/RulesHeader.tsx +++ b/packages/desktop-client/src/components/rules/RulesHeader.tsx @@ -1,11 +1,11 @@ import React from 'react'; - -import { t } from 'i18next'; +import { useTranslation } from 'react-i18next'; import { useSelectedItems, useSelectedDispatch } from '../../hooks/useSelected'; import { SelectCell, Cell, TableHeader } from '../table'; export function RulesHeader() { + const { t } = useTranslation(); const selectedItems = useSelectedItems(); const dispatchSelected = useSelectedDispatch(); diff --git a/packages/desktop-client/src/components/rules/Value.tsx b/packages/desktop-client/src/components/rules/Value.tsx index 7cd6e7dca14..9db0afde350 100644 --- a/packages/desktop-client/src/components/rules/Value.tsx +++ b/packages/desktop-client/src/components/rules/Value.tsx @@ -1,8 +1,8 @@ // @ts-strict-ignore import React, { useState, type CSSProperties } from 'react'; +import { useTranslation } from 'react-i18next'; import { format as formatDate, parseISO } from 'date-fns'; -import { t } from 'i18next'; import { getMonthYearFormat } from 'loot-core/src/shared/months'; import { getRecurringDescription } from 'loot-core/src/shared/schedules'; @@ -36,6 +36,7 @@ export function Value({ describe = x => x.name, style, }: ValueProps) { + const { t } = useTranslation(); const dateFormat = useDateFormat() || 'MM/dd/yyyy'; const payees = usePayees(); const { list: categories } = useCategories(); diff --git a/packages/desktop-client/src/components/select/RecurringSchedulePicker.tsx b/packages/desktop-client/src/components/select/RecurringSchedulePicker.tsx index aa0f302a46d..12aeca89cad 100644 --- a/packages/desktop-client/src/components/select/RecurringSchedulePicker.tsx +++ b/packages/desktop-client/src/components/select/RecurringSchedulePicker.tsx @@ -6,8 +6,7 @@ import { useRef, useState, } from 'react'; - -import { t } from 'i18next'; +import { useTranslation } from 'react-i18next'; import { sendCatch } from 'loot-core/src/platform/client/fetch'; import * as monthUtils from 'loot-core/src/shared/months'; @@ -271,6 +270,7 @@ function MonthlyPatterns({ config: StateConfig; dispatch: Dispatch; }) { + const { t } = useTranslation(); return ( {config.patterns.map((recurrence, idx) => ( @@ -351,6 +351,7 @@ function RecurringScheduleTooltip({ onClose: () => void; onSave: (config: RecurConfig) => void; }) { + const { t } = useTranslation(); const [previewDates, setPreviewDates] = useState(null); const [state, dispatch] = useReducer(reducer, { @@ -554,6 +555,7 @@ export function RecurringSchedulePicker({ buttonStyle, onChange, }: RecurringSchedulePickerProps) { + const { t } = useTranslation(); const triggerRef = useRef(null); const [isOpen, setIsOpen] = useState(false); const dateFormat = useDateFormat() || 'MM/dd/yyyy'; diff --git a/packages/desktop-client/src/components/settings/BudgetTypeSettings.tsx b/packages/desktop-client/src/components/settings/BudgetTypeSettings.tsx index 8edf78716c2..20da70a91ad 100644 --- a/packages/desktop-client/src/components/settings/BudgetTypeSettings.tsx +++ b/packages/desktop-client/src/components/settings/BudgetTypeSettings.tsx @@ -1,6 +1,5 @@ import React from 'react'; - -import { t } from 'i18next'; +import { useTranslation } from 'react-i18next'; import { useSyncedPref } from '../../hooks/useSyncedPref'; import { Button } from '../common/Button2'; @@ -10,6 +9,7 @@ import { Text } from '../common/Text'; import { Setting } from './UI'; export function BudgetTypeSettings() { + const { t } = useTranslation(); const [budgetType = 'rollover', setBudgetType] = useSyncedPref('budgetType'); function onSwitchType() { diff --git a/packages/desktop-client/src/components/settings/Export.tsx b/packages/desktop-client/src/components/settings/Export.tsx index 9557cde8218..393752a7b2d 100644 --- a/packages/desktop-client/src/components/settings/Export.tsx +++ b/packages/desktop-client/src/components/settings/Export.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { format } from 'date-fns'; -import { t } from 'i18next'; import { send } from 'loot-core/src/platform/client/fetch'; @@ -14,6 +14,7 @@ import { Text } from '../common/Text'; import { Setting } from './UI'; export function ExportBudget() { + const { t } = useTranslation(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [budgetName] = useMetadataPref('budgetName'); diff --git a/packages/desktop-client/src/components/settings/FixSplits.tsx b/packages/desktop-client/src/components/settings/FixSplits.tsx index b7d65af70ac..2897ee431a0 100644 --- a/packages/desktop-client/src/components/settings/FixSplits.tsx +++ b/packages/desktop-client/src/components/settings/FixSplits.tsx @@ -1,6 +1,5 @@ import React, { useState } from 'react'; - -import { t } from 'i18next'; +import { useTranslation } from 'react-i18next'; import { send } from 'loot-core/src/platform/client/fetch'; import { type Handlers } from 'loot-core/src/types/handlers'; @@ -55,6 +54,7 @@ function renderResults(results: Results) { } export function FixSplits() { + const { t } = useTranslation(); const [loading, setLoading] = useState(false); const [results, setResults] = useState(null); diff --git a/packages/desktop-client/src/components/settings/Format.tsx b/packages/desktop-client/src/components/settings/Format.tsx index 131a57c4e31..f5b78195050 100644 --- a/packages/desktop-client/src/components/settings/Format.tsx +++ b/packages/desktop-client/src/components/settings/Format.tsx @@ -1,8 +1,8 @@ // @ts-strict-ignore import React, { type ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; import { css } from '@emotion/css'; -import { t } from 'i18next'; import { numberFormats } from 'loot-core/src/shared/util'; import { type SyncedPrefs } from 'loot-core/src/types/prefs'; @@ -57,6 +57,7 @@ function Column({ title, children }: { title: string; children: ReactNode }) { } export function FormatSettings() { + const { t } = useTranslation(); const sidebar = useSidebar(); const [_firstDayOfWeekIdx, setFirstDayOfWeekIdxPref] = useSyncedPref('firstDayOfWeekIdx'); // Sunday; diff --git a/packages/desktop-client/src/components/settings/Reset.tsx b/packages/desktop-client/src/components/settings/Reset.tsx index 193bb2565e6..de0418b3603 100644 --- a/packages/desktop-client/src/components/settings/Reset.tsx +++ b/packages/desktop-client/src/components/settings/Reset.tsx @@ -1,10 +1,10 @@ import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; -import { t } from 'i18next'; - +import { resetSync } from 'loot-core/client/actions'; import { send } from 'loot-core/src/platform/client/fetch'; -import { useActions } from '../../hooks/useActions'; import { useMetadataPref } from '../../hooks/useMetadataPref'; import { ButtonWithLoading } from '../common/Button2'; import { Text } from '../common/Text'; @@ -12,6 +12,7 @@ import { Text } from '../common/Text'; import { Setting } from './UI'; export function ResetCache() { + const { t } = useTranslation(); const [resetting, setResetting] = useState(false); async function onResetCache() { @@ -39,15 +40,16 @@ export function ResetCache() { } export function ResetSync() { + const { t } = useTranslation(); const [groupId] = useMetadataPref('groupId'); const isEnabled = !!groupId; - const { resetSync } = useActions(); + const dispatch = useDispatch(); const [resetting, setResetting] = useState(false); async function onResetSync() { setResetting(true); - await resetSync(); + await dispatch(resetSync()); setResetting(false); } diff --git a/packages/desktop-client/src/components/settings/Themes.tsx b/packages/desktop-client/src/components/settings/Themes.tsx index 838f5a0a684..2bf5da6a3a2 100644 --- a/packages/desktop-client/src/components/settings/Themes.tsx +++ b/packages/desktop-client/src/components/settings/Themes.tsx @@ -1,7 +1,7 @@ import React, { type ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; import { css } from '@emotion/css'; -import { t } from 'i18next'; import { type DarkTheme, type Theme } from 'loot-core/types/prefs'; @@ -37,6 +37,7 @@ function Column({ title, children }: { title: string; children: ReactNode }) { } export function ThemeSettings() { + const { t } = useTranslation(); const sidebar = useSidebar(); const [theme, switchTheme] = useTheme(); const [darkTheme, switchDarkTheme] = usePreferredDarkTheme(); diff --git a/packages/desktop-client/src/components/settings/UI.tsx b/packages/desktop-client/src/components/settings/UI.tsx index a7162e08965..8bbfbf8a9e2 100644 --- a/packages/desktop-client/src/components/settings/UI.tsx +++ b/packages/desktop-client/src/components/settings/UI.tsx @@ -1,8 +1,8 @@ import React, { useState, type ReactNode, type CSSProperties } from 'react'; +import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; import { css } from '@emotion/css'; -import { t } from 'i18next'; import { theme } from '../../style'; import { tokens } from '../../tokens'; @@ -50,6 +50,7 @@ type AdvancedToggleProps = { }; export const AdvancedToggle = ({ children }: AdvancedToggleProps) => { + const { t } = useTranslation(); const location = useLocation(); const [expanded, setExpanded] = useState(location.hash === '#advanced'); diff --git a/packages/desktop-client/src/components/settings/index.tsx b/packages/desktop-client/src/components/settings/index.tsx index 4a7852e12b0..962ce69f999 100644 --- a/packages/desktop-client/src/components/settings/index.tsx +++ b/packages/desktop-client/src/components/settings/index.tsx @@ -1,12 +1,13 @@ import React, { type ReactNode, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; import { css } from '@emotion/css'; -import { t } from 'i18next'; +import { closeBudget, loadPrefs } from 'loot-core/client/actions'; import { isElectron } from 'loot-core/shared/environment'; import { listen } from 'loot-core/src/platform/client/fetch'; -import { useActions } from '../../hooks/useActions'; import { useGlobalPref } from '../../hooks/useGlobalPref'; import { useIsOutdated, useLatestVersion } from '../../hooks/useLatestVersion'; import { useMetadataPref } from '../../hooks/useMetadataPref'; @@ -35,6 +36,7 @@ import { ThemeSettings } from './Themes'; import { AdvancedToggle, Setting } from './UI'; function About() { + const { t } = useTranslation(); const version = useServerVersion(); const latestVersion = useLatestVersion(); const isOutdated = useIsOutdated(); @@ -95,6 +97,7 @@ function IDName({ children }: { children: ReactNode }) { } function AdvancedAbout() { + const { t } = useTranslation(); const [budgetId] = useMetadataPref('id'); const [groupId] = useMetadataPref('groupId'); @@ -124,19 +127,23 @@ function AdvancedAbout() { } export function Settings() { + const { t } = useTranslation(); const [floatingSidebar] = useGlobalPref('floatingSidebar'); const [budgetName] = useMetadataPref('budgetName'); + const dispatch = useDispatch(); - const { loadPrefs, closeBudget } = useActions(); + const onCloseBudget = () => { + dispatch(closeBudget()); + }; useEffect(() => { const unlisten = listen('prefs-updated', () => { - loadPrefs(); + dispatch(loadPrefs()); }); - loadPrefs(); + dispatch(loadPrefs()); return () => unlisten(); - }, [loadPrefs]); + }, [dispatch]); const { isNarrowWidth } = useResponsive(); @@ -169,7 +176,7 @@ export function Settings() { style={{ color: theme.buttonNormalDisabledText }} /> - {t('Close Budget')} + {t('Close Budget')} )} diff --git a/packages/desktop-client/src/components/sidebar/Account.tsx b/packages/desktop-client/src/components/sidebar/Account.tsx index 77cecc6cf7a..a913b497934 100644 --- a/packages/desktop-client/src/components/sidebar/Account.tsx +++ b/packages/desktop-client/src/components/sidebar/Account.tsx @@ -199,7 +199,7 @@ export function Account>({ state.account.accountsSyncing, @@ -100,11 +100,11 @@ export function Accounts() { style={{ fontWeight, marginTop: 15 }} /> - {budgetedAccounts.length > 0 && ( + {onBudgetAccounts.length > 0 && ( )} - {budgetedAccounts.map((account, i) => ( + {onBudgetAccounts.map((account, i) => ( ['value'] { const { sheetName, fullSheetName } = useSheetName(binding); - const bindingObj = - typeof binding === 'string' - ? { name: binding, value: null, query: undefined } - : binding; + const bindingObj = useMemo( + () => + typeof binding === 'string' + ? { name: binding, value: null, query: undefined } + : binding, + [binding], + ); const spreadsheet = useSpreadsheet(); const [result, setResult] = useState>({ name: fullSheetName, - value: bindingObj.value === undefined ? null : bindingObj.value, + value: bindingObj.value ? bindingObj.value : null, query: bindingObj.query, }); const latestOnChange = useRef(onChange); @@ -48,15 +51,16 @@ export function useSheetValue< latestValue.current = result.value; useLayoutEffect(() => { - if (bindingObj.query) { - spreadsheet.createQuery(sheetName, bindingObj.name, bindingObj.query); - } + let isMounted = true; - return spreadsheet.bind( + const unbind = spreadsheet.bind( sheetName, - binding, - null, + bindingObj, (newResult: SheetValueResult) => { + if (!isMounted) { + return; + } + if (latestOnChange.current) { latestOnChange.current(newResult); } @@ -66,7 +70,17 @@ export function useSheetValue< } }, ); - }, [sheetName, bindingObj.name, JSON.stringify(bindingObj.query)]); + + return () => { + isMounted = false; + unbind(); + }; + }, [ + spreadsheet, + sheetName, + bindingObj.name, + bindingObj.query?.serializeAsString(), + ]); return result.value; } diff --git a/packages/desktop-client/src/components/transactions/SimpleTransactionsTable.tsx b/packages/desktop-client/src/components/transactions/SimpleTransactionsTable.tsx index 40a5b99f538..69f38793354 100644 --- a/packages/desktop-client/src/components/transactions/SimpleTransactionsTable.tsx +++ b/packages/desktop-client/src/components/transactions/SimpleTransactionsTable.tsx @@ -5,13 +5,13 @@ import React, { type CSSProperties, type ReactNode, } from 'react'; +import { useTranslation } from 'react-i18next'; import { format as formatDate, isValid as isDateValid, parseISO, } from 'date-fns'; -import { t } from 'i18next'; import * as monthUtils from 'loot-core/src/shared/months'; import { integerToCurrency } from 'loot-core/src/shared/util'; @@ -165,6 +165,7 @@ export function SimpleTransactionsTable({ fields = ['date', 'payee', 'amount'], style, }: SimpleTransactionsTableProps) { + const { t } = useTranslation(); const dateFormat = useDateFormat() || 'MM/dd/yyyy'; const selectedItems = useSelectedItems(); const dispatchSelected = useSelectedDispatch(); diff --git a/packages/desktop-client/src/components/transactions/TransactionList.jsx b/packages/desktop-client/src/components/transactions/TransactionList.jsx index 41b4be1c0e4..849c6daf497 100644 --- a/packages/desktop-client/src/components/transactions/TransactionList.jsx +++ b/packages/desktop-client/src/components/transactions/TransactionList.jsx @@ -88,6 +88,8 @@ export function TransactionList({ onCloseAddTransaction, onCreatePayee, onApplyFilter, + showSelection = true, + allowSplitTransaction = true, onBatchDelete, onBatchDuplicate, onBatchLinkSchedule, @@ -251,6 +253,8 @@ export function TransactionList({ onCreateRule={onCreateRule} onScheduleAction={onScheduleAction} onMakeAsNonSplitTransactions={onMakeAsNonSplitTransactions} + showSelection={showSelection} + allowSplitTransaction={allowSplitTransaction} /> ); } diff --git a/packages/desktop-client/src/components/transactions/TransactionsTable.jsx b/packages/desktop-client/src/components/transactions/TransactionsTable.jsx index 34c684b4721..69afdb478f0 100644 --- a/packages/desktop-client/src/components/transactions/TransactionsTable.jsx +++ b/packages/desktop-client/src/components/transactions/TransactionsTable.jsx @@ -175,6 +175,7 @@ const TransactionHeader = memo( onSort, ascDesc, field, + showSelection, }) => { const dispatchSelected = useSelectedDispatch(); const { t } = useTranslation(); @@ -202,19 +203,32 @@ const TransactionHeader = memo( borderColor: theme.tableBorder, }} > - - dispatchSelected({ type: 'select-all', isRangeSelect: e.shiftKey }) - } - /> + {showSelection && ( + + dispatchSelected({ + type: 'select-all', + isRangeSelect: e.shiftKey, + }) + } + /> + )} + {!showSelection && ( + + )} ) - ) : isPreview && isChild ? ( + ) : (isPreview && isChild) || !showSelection ? ( ) : ( ) : isBudgetTransfer || isOffBudget ? ( ))} ); }; @@ -1989,6 +2011,7 @@ function TransactionTableInner({ onSort={props.onSort} ascDesc={props.ascDesc} field={props.sortField} + showSelection={props.showSelection} /> {props.isAdding && ( @@ -2094,9 +2117,9 @@ export const TransactionTable = forwardRef((props, ref) => { result = props.transactions.filter((t, idx) => { if (t.parent_id) { if (idx >= index) { - return splitsExpanded.expanded(t.parent_id); + return splitsExpanded.isExpanded(t.parent_id); } else if (prevSplitsExpanded.current) { - return prevSplitsExpanded.current.expanded(t.parent_id); + return prevSplitsExpanded.current.isExpanded(t.parent_id); } } return true; @@ -2113,7 +2136,7 @@ export const TransactionTable = forwardRef((props, ref) => { result = props.transactions.filter(t => { if (t.parent_id) { - return splitsExpanded.expanded(t.parent_id); + return splitsExpanded.isExpanded(t.parent_id); } return true; }); @@ -2584,7 +2607,7 @@ export const TransactionTable = forwardRef((props, ref) => { transactionsByParent={transactionsByParent} transferAccountsByTransaction={transferAccountsByTransaction} selectedItems={selectedItems} - isExpanded={splitsExpanded.expanded} + isExpanded={splitsExpanded.isExpanded} onSave={onSave} onDelete={onDelete} onDuplicate={onDuplicate} @@ -2604,6 +2627,8 @@ export const TransactionTable = forwardRef((props, ref) => { newTransactions={newTransactions} tableNavigator={tableNavigator} newNavigator={newNavigator} + showSelection={props.showSelection} + allowSplitTransaction={props.allowSplitTransaction} /> ); }); diff --git a/packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx b/packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx index 95c73607e91..a654e329d26 100644 --- a/packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx +++ b/packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx @@ -168,6 +168,8 @@ function LiveTransactionTable(props) { onAdd={onAdd} onAddSplit={onAddSplit} onCreatePayee={onCreatePayee} + showSelection={true} + allowSplitTransaction={true} /> diff --git a/packages/desktop-client/src/components/util/AmountInput.tsx b/packages/desktop-client/src/components/util/AmountInput.tsx index 1cc0bc5c766..d83d558ab06 100644 --- a/packages/desktop-client/src/components/util/AmountInput.tsx +++ b/packages/desktop-client/src/components/util/AmountInput.tsx @@ -8,8 +8,7 @@ import React, { type KeyboardEventHandler, type CSSProperties, } from 'react'; - -import { t } from 'i18next'; +import { useTranslation } from 'react-i18next'; import { evalArithmetic } from 'loot-core/src/shared/arithmetic'; import { amountToInteger, appendDecimals } from 'loot-core/src/shared/util'; @@ -156,6 +155,7 @@ export function AmountInput({ } export function BetweenAmountInput({ defaultValue, onChange }) { + const { t } = useTranslation(); const [num1, setNum1] = useState(defaultValue.num1); const [num2, setNum2] = useState(defaultValue.num2); diff --git a/packages/desktop-client/src/components/util/GenericInput.jsx b/packages/desktop-client/src/components/util/GenericInput.jsx index ed59442c0a2..f718cdcb412 100644 --- a/packages/desktop-client/src/components/util/GenericInput.jsx +++ b/packages/desktop-client/src/components/util/GenericInput.jsx @@ -32,6 +32,7 @@ export function GenericInput({ inputRef, style, onChange, + op = undefined, }) { const { grouped: categoryGroups } = useCategories(); const { data: savedReports } = useReports(); @@ -100,18 +101,26 @@ export function GenericInput({ break; case 'account': - content = ( - - ); + switch (op) { + case 'onBudget': + case 'offBudget': + content = null; + break; + default: + content = ( + + ); + break; + } break; case 'category': diff --git a/packages/desktop-client/src/hooks/useBudgetedAccounts.ts b/packages/desktop-client/src/hooks/useOnBudgetAccounts.ts similarity index 86% rename from packages/desktop-client/src/hooks/useBudgetedAccounts.ts rename to packages/desktop-client/src/hooks/useOnBudgetAccounts.ts index dbd8e1f53db..31fd6cf10c0 100644 --- a/packages/desktop-client/src/hooks/useBudgetedAccounts.ts +++ b/packages/desktop-client/src/hooks/useOnBudgetAccounts.ts @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import { useAccounts } from './useAccounts'; -export function useBudgetedAccounts() { +export function useOnBudgetAccounts() { const accounts = useAccounts(); return useMemo( () => diff --git a/packages/desktop-client/src/hooks/useSplitsExpanded.jsx b/packages/desktop-client/src/hooks/useSplitsExpanded.tsx similarity index 64% rename from packages/desktop-client/src/hooks/useSplitsExpanded.jsx rename to packages/desktop-client/src/hooks/useSplitsExpanded.tsx index 8b8bb4ed249..25386d2dcec 100644 --- a/packages/desktop-client/src/hooks/useSplitsExpanded.jsx +++ b/packages/desktop-client/src/hooks/useSplitsExpanded.tsx @@ -4,10 +4,68 @@ import React, { useEffect, useContext, useReducer, + type Dispatch, + type ReactNode, } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -const SplitsExpandedContext = createContext(null); +import { + type SplitMode, + type SplitState, +} from 'loot-core/client/state-types/app'; + +type ToggleSplitAction = { + type: 'toggle-split'; + id: string; +}; + +type OpenSplitAction = { + type: 'open-split'; + id: string; +}; + +type CloseSplitsAction = { + type: 'close-splits'; + ids: string[]; +}; + +type SetModeAction = { + type: 'set-mode'; + mode: SplitMode; +}; + +type SwitchModeAction = { + type: 'switch-mode'; + id: string; +}; + +type FinishSwitchModeAction = { + type: 'finish-switch-mode'; +}; + +type Actions = + | ToggleSplitAction + | OpenSplitAction + | CloseSplitsAction + | SetModeAction + | SwitchModeAction + | FinishSwitchModeAction; + +type SplitsStateContext = { + state: SplitState; + dispatch: Dispatch; +}; + +const SplitsExpandedContext = createContext({ + state: { + mode: 'collapse', + ids: new Set(), + transitionId: null, + }, + dispatch: () => { + throw new Error('Unitialised context method called: dispatch'); + }, +}); export function useSplitsExpanded() { const data = useContext(SplitsExpandedContext); @@ -15,7 +73,7 @@ export function useSplitsExpanded() { return useMemo( () => ({ ...data, - expanded: id => + isExpanded: (id: string) => data.state.mode === 'collapse' ? !data.state.ids.has(id) : data.state.ids.has(id), @@ -24,12 +82,20 @@ export function useSplitsExpanded() { ); } -export function SplitsExpandedProvider({ children, initialMode = 'expand' }) { +type SplitsExpandedProviderProps = { + children?: ReactNode; + initialMode: SplitMode; +}; + +export function SplitsExpandedProvider({ + children, + initialMode = 'expand', +}: SplitsExpandedProviderProps) { const cachedState = useSelector(state => state.app.lastSplitState); const reduxDispatch = useDispatch(); const [state, dispatch] = useReducer( - (state, action) => { + (state: SplitState, action: Actions): SplitState => { switch (action.type) { case 'toggle-split': { const ids = new Set([...state.ids]); @@ -66,7 +132,7 @@ export function SplitsExpandedProvider({ children, initialMode = 'expand' }) { return { ...state, mode: action.mode, - ids: new Set(), + ids: new Set(), transitionId: null, }; } @@ -80,15 +146,17 @@ export function SplitsExpandedProvider({ children, initialMode = 'expand' }) { ...state, mode: state.mode === 'expand' ? 'collapse' : 'expand', transitionId: action.id, - ids: new Set(), + ids: new Set(), }; case 'finish-switch-mode': return { ...state, transitionId: null }; - default: - throw new Error('Unknown action type: ' + action.type); } }, - cachedState.current || { ids: new Set(), mode: initialMode }, + cachedState.current || { + ids: new Set(), + mode: initialMode, + transitionId: null, + }, ); useEffect(() => { diff --git a/packages/desktop-client/src/style/themes/dark.ts b/packages/desktop-client/src/style/themes/dark.ts index 56fd5a7b130..893ba14df72 100644 --- a/packages/desktop-client/src/style/themes/dark.ts +++ b/packages/desktop-client/src/style/themes/dark.ts @@ -213,3 +213,5 @@ export const floatingActionBarText = colorPalette.navy150; export const tooltipText = colorPalette.navy100; export const tooltipBackground = colorPalette.navy800; export const tooltipBorder = colorPalette.navy700; + +export const calendarCellBackground = colorPalette.navy900; diff --git a/packages/desktop-client/src/style/themes/development.ts b/packages/desktop-client/src/style/themes/development.ts index 099b1d7b491..8cb2678fc5d 100644 --- a/packages/desktop-client/src/style/themes/development.ts +++ b/packages/desktop-client/src/style/themes/development.ts @@ -213,3 +213,5 @@ export const floatingActionBarText = colorPalette.navy50; export const tooltipText = colorPalette.navy900; export const tooltipBackground = colorPalette.navy50; export const tooltipBorder = colorPalette.navy150; + +export const calendarCellBackground = colorPalette.navy900; diff --git a/packages/desktop-client/src/style/themes/light.ts b/packages/desktop-client/src/style/themes/light.ts index aa2b0eed205..df648c74f12 100644 --- a/packages/desktop-client/src/style/themes/light.ts +++ b/packages/desktop-client/src/style/themes/light.ts @@ -215,3 +215,5 @@ export const floatingActionBarText = colorPalette.navy50; export const tooltipText = colorPalette.navy900; export const tooltipBackground = colorPalette.white; export const tooltipBorder = colorPalette.navy150; + +export const calendarCellBackground = colorPalette.navy100; diff --git a/packages/desktop-client/src/style/themes/midnight.ts b/packages/desktop-client/src/style/themes/midnight.ts index 23ff68108dd..71400907ea5 100644 --- a/packages/desktop-client/src/style/themes/midnight.ts +++ b/packages/desktop-client/src/style/themes/midnight.ts @@ -215,3 +215,5 @@ export const floatingActionBarText = colorPalette.purple200; export const tooltipText = colorPalette.gray100; export const tooltipBackground = colorPalette.gray800; export const tooltipBorder = colorPalette.gray600; + +export const calendarCellBackground = colorPalette.navy900; diff --git a/packages/loot-core/src/client/SpreadsheetProvider.tsx b/packages/loot-core/src/client/SpreadsheetProvider.tsx index 7c53ca515d4..6f8cfafbd78 100644 --- a/packages/loot-core/src/client/SpreadsheetProvider.tsx +++ b/packages/loot-core/src/client/SpreadsheetProvider.tsx @@ -62,22 +62,26 @@ function makeSpreadsheet() { }); } - bind(sheetName = '__global', binding, fields, cb) { + bind(sheetName = '__global', binding, callback) { binding = typeof binding === 'string' ? { name: binding, value: null } : binding; + if (binding.query) { + this.createQuery(sheetName, binding.name, binding.query); + } + const resolvedName = `${sheetName}!${binding.name}`; - const cleanup = this.observeCell(resolvedName, cb); + const cleanup = this.observeCell(resolvedName, callback); // Always synchronously call with the existing value if it has one. // This is a display optimization to avoid flicker. The LRU cache // will keep a number of recent nodes in memory. if (LRUValueCache.has(resolvedName)) { - cb(LRUValueCache.get(resolvedName)); + callback(LRUValueCache.get(resolvedName)); } if (cellCache[resolvedName] != null) { - cellCache[resolvedName].then(cb); + cellCache[resolvedName].then(callback); } else { const req = this.get(sheetName, binding.name); cellCache[resolvedName] = req; @@ -90,7 +94,7 @@ function makeSpreadsheet() { // with an old value depending on the order of messages) if (cellCache[resolvedName] === req) { LRUValueCache.set(resolvedName, result); - cb(result); + callback(result); } }); } diff --git a/packages/loot-core/src/client/actions/budgets.ts b/packages/loot-core/src/client/actions/budgets.ts index 5ce240a64b8..8e0a5bfebd1 100644 --- a/packages/loot-core/src/client/actions/budgets.ts +++ b/packages/loot-core/src/client/actions/budgets.ts @@ -148,6 +148,73 @@ export function createBudget({ testMode = false, demoMode = false } = {}) { }; } +export function validateBudgetName(name: string): { + valid: boolean; + message?: string; +} { + return send('validate-budget-name', { name }); +} + +export function uniqueBudgetName(name: string): string { + return send('unique-budget-name', { name }); +} + +export function duplicateBudget({ + id, + cloudId, + oldName, + newName, + managePage, + loadBudget = 'none', + cloudSync, +}: { + id?: string; + cloudId?: string; + oldName: string; + newName: string; + managePage?: boolean; + loadBudget: 'none' | 'original' | 'copy'; + /** + * cloudSync is used to determine if the duplicate budget + * should be synced to the server + */ + cloudSync?: boolean; +}) { + return async (dispatch: Dispatch) => { + try { + dispatch( + setAppState({ + loadingText: t('Duplicating: {{oldName}} -- to: {{newName}}', { + oldName, + newName, + }), + }), + ); + + await send('duplicate-budget', { + id, + cloudId, + newName, + cloudSync, + open: loadBudget, + }); + + dispatch(closeModal()); + + if (managePage) { + await dispatch(loadAllFiles()); + } + } catch (error) { + console.error('Error duplicating budget:', error); + throw error instanceof Error + ? error + : new Error('Error duplicating budget: ' + String(error)); + } finally { + dispatch(setAppState({ loadingText: null })); + } + }; +} + export function importBudget( filepath: string, type: Parameters[0]['type'], diff --git a/packages/loot-core/src/client/data-hooks/schedules.tsx b/packages/loot-core/src/client/data-hooks/schedules.tsx index 6207b6304a8..03b95b42b30 100644 --- a/packages/loot-core/src/client/data-hooks/schedules.tsx +++ b/packages/loot-core/src/client/data-hooks/schedules.tsx @@ -161,7 +161,7 @@ export function useCachedSchedules() { } export function accountSchedulesQuery( - accountId?: AccountEntity['id'] | 'budgeted' | 'offbudget' | 'uncategorized', + accountId?: AccountEntity['id'] | 'onbudget' | 'offbudget' | 'uncategorized', ) { const filterByAccount = accountFilter(accountId, '_account'); const filterByPayee = accountFilter(accountId, '_payee.transfer_acct'); diff --git a/packages/loot-core/src/client/data-hooks/transactions.ts b/packages/loot-core/src/client/data-hooks/transactions.ts index 60ec93e1f2d..096fe2836ea 100644 --- a/packages/loot-core/src/client/data-hooks/transactions.ts +++ b/packages/loot-core/src/client/data-hooks/transactions.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState, useMemo } from 'react'; +import { useEffect, useRef, useState, useMemo, useCallback } from 'react'; import debounce from 'lodash/debounce'; @@ -24,10 +24,11 @@ type UseTransactionsProps = { type UseTransactionsResult = { transactions: ReadonlyArray; - isLoading?: boolean; + isLoading: boolean; error?: Error; - reload?: () => void; - loadMore?: () => void; + reload: () => void; + loadMore: () => void; + isLoadingMore: boolean; }; export function useTransactions({ @@ -35,6 +36,7 @@ export function useTransactions({ options = { pageCount: 50 }, }: UseTransactionsProps): UseTransactionsResult { const [isLoading, setIsLoading] = useState(true); + const [isLoadingMore, setIsLoadingMore] = useState(false); const [error, setError] = useState(undefined); const [transactions, setTransactions] = useState< ReadonlyArray @@ -88,12 +90,32 @@ export function useTransactions({ }; }, [query]); + const loadMore = useCallback(async () => { + if (!pagedQueryRef.current) { + return; + } + + setIsLoadingMore(true); + + await pagedQueryRef.current + .fetchNext() + .catch(setError) + .finally(() => { + setIsLoadingMore(false); + }); + }, []); + + const reload = useCallback(() => { + pagedQueryRef.current?.run(); + }, []); + return { transactions, isLoading, error, - reload: pagedQueryRef.current?.run, - loadMore: pagedQueryRef.current?.fetchNext, + reload, + loadMore, + isLoadingMore, }; } diff --git a/packages/loot-core/src/client/queries.ts b/packages/loot-core/src/client/queries.ts index c36659a1378..a8b1ff9b368 100644 --- a/packages/loot-core/src/client/queries.ts +++ b/packages/loot-core/src/client/queries.ts @@ -29,11 +29,11 @@ const envelopeParametrizedField = parametrizedField<'envelope-budget'>(); const trackingParametrizedField = parametrizedField<'tracking-budget'>(); export function accountFilter( - accountId?: AccountEntity['id'] | 'budgeted' | 'offbudget' | 'uncategorized', + accountId?: AccountEntity['id'] | 'onbudget' | 'offbudget' | 'uncategorized', field = 'account', ) { if (accountId) { - if (accountId === 'budgeted') { + if (accountId === 'onbudget') { return { $and: [ { [`${field}.offbudget`]: false }, @@ -68,7 +68,7 @@ export function accountFilter( } export function transactions( - accountId?: AccountEntity['id'] | 'budgeted' | 'offbudget' | 'uncategorized', + accountId?: AccountEntity['id'] | 'onbudget' | 'offbudget' | 'uncategorized', ) { let query = q('transactions').options({ splits: 'grouped' }); @@ -159,16 +159,16 @@ export function allAccountBalance() { } satisfies Binding<'account', 'accounts-balance'>; } -export function budgetedAccountBalance() { +export function onBudgetAccountBalance() { return { - name: `budgeted-accounts-balance`, + name: `onbudget-accounts-balance`, query: q('transactions') .filter({ 'account.offbudget': false, 'account.closed': false }) .calculate({ $sum: '$amount' }), - } satisfies Binding<'account', 'budgeted-accounts-balance'>; + } satisfies Binding<'account', 'onbudget-accounts-balance'>; } -export function offbudgetAccountBalance() { +export function offBudgetAccountBalance() { return { name: `offbudget-accounts-balance`, query: q('transactions') diff --git a/packages/loot-core/src/client/state-types/app.d.ts b/packages/loot-core/src/client/state-types/app.d.ts index 7420aa7251b..fd4e4f34414 100644 --- a/packages/loot-core/src/client/state-types/app.d.ts +++ b/packages/loot-core/src/client/state-types/app.d.ts @@ -1,7 +1,12 @@ import type { UndoState } from '../../server/undo'; import type * as constants from '../constants'; -export type SplitState = { ids: Set; mode: 'collapse' | 'expand' }; +export type SplitMode = 'collapse' | 'expand'; +export type SplitState = { + ids: Set; + mode: SplitMode; + transitionId: string | null; +}; export type AppState = { loadingText: string | null; diff --git a/packages/loot-core/src/client/state-types/modals.d.ts b/packages/loot-core/src/client/state-types/modals.d.ts index 11450cf4775..9a415988fcf 100644 --- a/packages/loot-core/src/client/state-types/modals.d.ts +++ b/packages/loot-core/src/client/state-types/modals.d.ts @@ -78,6 +78,37 @@ type FinanceModals = { 'delete-budget': { file: File }; + 'duplicate-budget': { + /** The budget file to be duplicated */ + file: File; + /** + * Indicates whether the duplication is initiated from the budget + * management page. This may affect the behavior or UI of the + * duplication process. + */ + managePage?: boolean; + /** + * loadBudget indicates whether to open the 'original' budget, the + * new duplicated 'copy' budget, or no budget ('none'). If 'none' + * duplicate-budget stays on the same page. + */ + loadBudget?: 'none' | 'original' | 'copy'; + /** + * onComplete is called when the DuplicateFileModal is closed. + * @param event the event object will pass back the status of the + * duplicate process. + * 'success' if the budget was duplicated. + * 'failed' if the budget could not be duplicated. This will also + * pass an error on the event object. + * 'canceled' if the DuplicateFileModal was canceled. + * @returns + */ + onComplete?: (event: { + status: 'success' | 'failed' | 'canceled'; + error?: Error; + }) => void; + }; + import: null; 'import-ynab4': null; diff --git a/packages/loot-core/src/mocks/spreadsheet.ts b/packages/loot-core/src/mocks/spreadsheet.ts index cdd02751c8a..87e4456a930 100644 --- a/packages/loot-core/src/mocks/spreadsheet.ts +++ b/packages/loot-core/src/mocks/spreadsheet.ts @@ -23,7 +23,7 @@ export function makeSpreadsheet() { this._getNode(sheetName, name).value = value; }, - bind(sheetName, binding, fields, cb) { + bind(sheetName, binding, cb) { const { name } = binding; const resolvedName = `${sheetName}!${name}`; if (!this.observers[resolvedName]) { diff --git a/packages/loot-core/src/platform/server/fs/index.web.ts b/packages/loot-core/src/platform/server/fs/index.web.ts index 06eebb13ade..0d299888528 100644 --- a/packages/loot-core/src/platform/server/fs/index.web.ts +++ b/packages/loot-core/src/platform/server/fs/index.web.ts @@ -19,11 +19,11 @@ export { join }; export { getDocumentDir, getBudgetDir, _setDocumentDir } from './shared'; export const getDataDir = () => process.env.ACTUAL_DATA_DIR; -export const pathToId = function (filepath) { +export const pathToId = function (filepath: string): string { return filepath.replace(/^\//, '').replace(/\//g, '-'); }; -function _exists(filepath) { +function _exists(filepath: string): boolean { try { FS.readlink(filepath); return true; @@ -47,7 +47,7 @@ function _mkdirRecursively(dir) { } } -function _createFile(filepath) { +function _createFile(filepath: string) { // This can create the file. Check if it exists, if not create a // symlink if it's a sqlite file. Otherwise store in idb @@ -67,7 +67,7 @@ function _createFile(filepath) { return filepath; } -async function _readFile(filepath, opts?: { encoding?: string }) { +async function _readFile(filepath: string, opts?: { encoding?: string }) { // We persist stuff in /documents, but don't need to handle sqlite // file specifically because those are symlinked to a separate // filesystem and will be handled in the BlockedFS @@ -88,7 +88,7 @@ async function _readFile(filepath, opts?: { encoding?: string }) { throw new Error('File does not exist: ' + filepath); } - if (opts.encoding === 'utf8' && ArrayBuffer.isView(item.contents)) { + if (opts?.encoding === 'utf8' && ArrayBuffer.isView(item.contents)) { return String.fromCharCode.apply( null, new Uint16Array(item.contents.buffer), @@ -101,7 +101,7 @@ async function _readFile(filepath, opts?: { encoding?: string }) { } } -function resolveLink(path) { +function resolveLink(path: string): string { try { const { node } = FS.lookupPath(path, { follow: false }); return node.link ? FS.readlink(path) : path; @@ -110,7 +110,7 @@ function resolveLink(path) { } } -async function _writeFile(filepath, contents) { +async function _writeFile(filepath: string, contents): Promise { if (contents instanceof ArrayBuffer) { contents = new Uint8Array(contents); } else if (ArrayBuffer.isView(contents)) { @@ -146,9 +146,53 @@ async function _writeFile(filepath, contents) { } else { FS.writeFile(resolveLink(filepath), contents); } + return true; } -async function _removeFile(filepath) { +async function _copySqlFile( + frompath: string, + topath: string, +): Promise { + _createFile(topath); + + const { store } = await idb.getStore(await idb.getDatabase(), 'files'); + await idb.set(store, { filepath: topath, contents: '' }); + const fromitem = await idb.get(store, frompath); + const fromDbPath = pathToId(fromitem.filepath); + const toDbPath = pathToId(topath); + + const fromfile = BFS.backend.createFile(fromDbPath); + const tofile = BFS.backend.createFile(toDbPath); + + try { + fromfile.open(); + tofile.open(); + const fileSize = fromfile.meta.size; + const blockSize = fromfile.meta.blockSize; + + const buffer = new ArrayBuffer(blockSize); + const bufferView = new Uint8Array(buffer); + + for (let i = 0; i < fileSize; i += blockSize) { + const bytesToRead = Math.min(blockSize, fileSize - i); + fromfile.read(bufferView, 0, bytesToRead, i); + tofile.write(bufferView, 0, bytesToRead, i); + } + } catch (error) { + tofile.close(); + fromfile.close(); + _removeFile(toDbPath); + console.error('Failed to copy database file', error); + return false; + } finally { + tofile.close(); + fromfile.close(); + } + + return true; +} + +async function _removeFile(filepath: string) { if (!NO_PERSIST && filepath.startsWith('/documents')) { const isDb = filepath.endsWith('.sqlite'); @@ -272,22 +316,39 @@ export const size = async function (filepath) { return attrs.size; }; -export const copyFile = async function (frompath, topath) { - // TODO: This reads the whole file into memory, but that's probably - // not a problem. This could be optimized - const contents = await _readFile(frompath); - return _writeFile(topath, contents); +export const copyFile = async function ( + frompath: string, + topath: string, +): Promise { + let result = false; + try { + const contents = await _readFile(frompath); + result = await _writeFile(topath, contents); + } catch (error) { + if (frompath.endsWith('.sqlite') || topath.endsWith('.sqlite')) { + try { + result = await _copySqlFile(frompath, topath); + } catch (secondError) { + throw new Error( + `Failed to copy SQL file from ${frompath} to ${topath}: ${secondError.message}`, + ); + } + } else { + throw error; + } + } + return result; }; -export const readFile = async function (filepath, encoding = 'utf8') { +export const readFile = async function (filepath: string, encoding = 'utf8') { return _readFile(filepath, { encoding }); }; -export const writeFile = async function (filepath, contents) { +export const writeFile = async function (filepath: string, contents) { return _writeFile(filepath, contents); }; -export const removeFile = async function (filepath) { +export const removeFile = async function (filepath: string) { return _removeFile(filepath); }; diff --git a/packages/loot-core/src/server/accounts/rules.ts b/packages/loot-core/src/server/accounts/rules.ts index 20f9ea2d219..449590370fb 100644 --- a/packages/loot-core/src/server/accounts/rules.ts +++ b/packages/loot-core/src/server/accounts/rules.ts @@ -201,6 +201,8 @@ const CONDITION_TYPES = { 'doesNotContain', 'notOneOf', 'and', + 'onBudget', + 'offBudget', ], nullable: true, parse(op, value, fieldName) { @@ -518,6 +520,21 @@ export class Condition { console.log('invalid regexp in matches condition', e); return false; } + + case 'onBudget': + if (!object._account) { + return false; + } + + return object._account.offbudget === 0; + + case 'offBudget': + if (!object._account) { + return false; + } + + return object._account.offbudget === 1; + default: } @@ -948,6 +965,8 @@ const OP_SCORES: Record = { doesNotContain: 0, matches: 0, hasTags: 0, + onBudget: 0, + offBudget: 0, }; function computeScore(rule: Rule): number { diff --git a/packages/loot-core/src/server/accounts/sync.ts b/packages/loot-core/src/server/accounts/sync.ts index aedf1e4f80f..42ff79b628b 100644 --- a/packages/loot-core/src/server/accounts/sync.ts +++ b/packages/loot-core/src/server/accounts/sync.ts @@ -523,6 +523,9 @@ export async function matchTransactions( ); // The first pass runs the rules, and preps data for fuzzy matching + const accounts: AccountEntity[] = await db.getAccounts(); + const accountsMap = new Map(accounts.map(account => [account.id, account])); + const transactionsStep1 = []; for (const { payee_name, @@ -530,7 +533,7 @@ export async function matchTransactions( subtransactions, } of normalized) { // Run the rules - const trans = await runRules(originalTrans); + const trans = await runRules(originalTrans, accountsMap); let match = null; let fuzzyDataset = null; @@ -673,9 +676,12 @@ export async function addTransactions( { rawPayeeName: true }, ); + const accounts: AccountEntity[] = await db.getAccounts(); + const accountsMap = new Map(accounts.map(account => [account.id, account])); + for (const { trans: originalTrans, subtransactions } of normalized) { // Run the rules - const trans = await runRules(originalTrans); + const trans = await runRules(originalTrans, accountsMap); const finalTransaction = { id: uuidv4(), diff --git a/packages/loot-core/src/server/accounts/transaction-rules.ts b/packages/loot-core/src/server/accounts/transaction-rules.ts index 2c2569a5fc8..8a8d52c72bd 100644 --- a/packages/loot-core/src/server/accounts/transaction-rules.ts +++ b/packages/loot-core/src/server/accounts/transaction-rules.ts @@ -13,10 +13,11 @@ import { type TransactionEntity, type RuleActionEntity, type RuleEntity, + AccountEntity, } from '../../types/models'; import { schemaConfig } from '../aql'; import * as db from '../db'; -import { getPayee, getPayeeByName, insertPayee } from '../db'; +import { getPayee, getPayeeByName, insertPayee, getAccount } from '../db'; import { getMappings } from '../db/mappings'; import { RuleError } from '../errors'; import { requiredFields, toDateRepr } from '../models'; @@ -274,8 +275,20 @@ function onApplySync(oldValues, newValues) { } // Runner -export async function runRules(trans) { - let finalTrans = await prepareTransactionForRules({ ...trans }); +export async function runRules( + trans, + accounts: Map | null = null, +) { + let accountsMap = null; + if (accounts === null) { + accountsMap = new Map( + (await db.getAccounts()).map(account => [account.id, account]), + ); + } else { + accountsMap = accounts; + } + + let finalTrans = await prepareTransactionForRules({ ...trans }, accountsMap); const rules = rankRules( fastSetMerge( @@ -291,7 +304,11 @@ export async function runRules(trans) { return await finalizeTransactionForRules(finalTrans); } -function conditionSpecialCases(cond: Condition): Condition { +function conditionSpecialCases(cond: Condition | null): Condition | null { + if (!cond) { + return cond; + } + //special cases that require multiple conditions if (cond.op === 'is' && cond.field === 'category' && cond.value === null) { return new Condition( @@ -555,6 +572,12 @@ export function conditionsToAQL(conditions, { recurDateBounds = 100 } = {}) { return { $and: getValue(value).map(subExpr => mapConditionToActualQL(subExpr)), }; + + case 'onBudget': + return { 'account.offbudget': false }; + case 'offBudget': + return { 'account.offbudget': true }; + default: throw new Error('Unhandled operator: ' + op); } @@ -604,8 +627,14 @@ export async function applyActions( return null; } + const accounts: AccountEntity[] = await db.getAccounts(); const transactionsForRules = await Promise.all( - transactions.map(prepareTransactionForRules), + transactions.map(transactions => + prepareTransactionForRules( + transactions, + new Map(accounts.map(account => [account.id, account])), + ), + ), ); const updated = transactionsForRules.flatMap(trans => { @@ -836,10 +865,12 @@ export async function updateCategoryRules(transactions) { export type TransactionForRules = TransactionEntity & { payee_name?: string; + _account?: AccountEntity; }; export async function prepareTransactionForRules( trans: TransactionEntity, + accounts: Map | null = null, ): Promise { const r: TransactionForRules = { ...trans }; if (trans.payee) { @@ -849,6 +880,14 @@ export async function prepareTransactionForRules( } } + if (trans.account) { + if (accounts !== null && accounts.has(trans.account)) { + r._account = accounts.get(trans.account); + } else { + r._account = await getAccount(trans.account); + } + } + return r; } diff --git a/packages/loot-core/src/server/accounts/transfer.ts b/packages/loot-core/src/server/accounts/transfer.ts index f9c81048378..fa8969667e0 100644 --- a/packages/loot-core/src/server/accounts/transfer.ts +++ b/packages/loot-core/src/server/accounts/transfer.ts @@ -26,7 +26,7 @@ async function clearCategory(transaction, transferAcct) { [transferAcct], ); - // If the transfer is between two on-budget or two off-budget accounts, + // If the transfer is between two on budget or two off budget accounts, // we should clear the category, because the category is not relevant if (fromOffBudget === toOffBudget) { await db.updateTransaction({ id: transaction.id, category: null }); diff --git a/packages/loot-core/src/server/dashboard/app.ts b/packages/loot-core/src/server/dashboard/app.ts index b4fbef11130..8b456f280c8 100644 --- a/packages/loot-core/src/server/dashboard/app.ts +++ b/packages/loot-core/src/server/dashboard/app.ts @@ -82,6 +82,7 @@ const exportModel = { 'custom-report', 'markdown-card', 'summary-card', + 'calendar-card', ].includes(widget.type) ) { throw new ValidationError( diff --git a/packages/loot-core/src/server/db/index.ts b/packages/loot-core/src/server/db/index.ts index 0893c2aadc8..dc324fca85f 100644 --- a/packages/loot-core/src/server/db/index.ts +++ b/packages/loot-core/src/server/db/index.ts @@ -473,6 +473,10 @@ export async function getPayee(id) { return first(`SELECT * FROM payees WHERE id = ?`, [id]); } +export async function getAccount(id) { + return first(`SELECT * FROM accounts WHERE id = ?`, [id]); +} + export async function insertPayee(payee) { payee = payeeModel.validate(payee); let id; diff --git a/packages/loot-core/src/server/main.ts b/packages/loot-core/src/server/main.ts index aadde848273..89a29484f7a 100644 --- a/packages/loot-core/src/server/main.ts +++ b/packages/loot-core/src/server/main.ts @@ -73,7 +73,11 @@ import * as syncMigrations from './sync/migrate'; import { app as toolsApp } from './tools/app'; import { withUndo, clearUndo, undo, redo } from './undo'; import { updateVersion } from './update'; -import { uniqueFileName, idFromFileName } from './util/budget-name'; +import { + uniqueBudgetName, + idFromBudgetName, + validateBudgetName, +} from './util/budget-name'; const DEMO_BUDGET_ID = '_demo-budget'; const TEST_BUDGET_ID = '_test-budget'; @@ -512,22 +516,8 @@ handlers['make-filters-from-conditions'] = async function ({ conditions }) { }; handlers['getCell'] = async function ({ sheetName, name }) { - // Fields is no longer used - hardcode - const fields = ['name', 'value']; const node = sheet.get()._getNode(resolveName(sheetName, name)); - if (fields) { - const res = {}; - fields.forEach(field => { - if (field === 'run') { - res[field] = node._run ? node._run.toString() : null; - } else { - res[field] = node[field]; - } - }); - return res; - } else { - return node; - } + return { name: node.name, value: node.value }; }; handlers['getCells'] = async function ({ names }) { @@ -1107,7 +1097,7 @@ handlers['accounts-bank-sync'] = async function ({ ids = [] }) { const accounts = await db.runQuery( ` - SELECT a.*, b.bank_id as bankId + SELECT a.*, b.bank_id as bankId FROM accounts a LEFT JOIN banks b ON a.bank = b.id WHERE a.tombstone = 0 AND a.closed = 0 @@ -1710,6 +1700,14 @@ handlers['sync'] = async function () { return fullSync(); }; +handlers['validate-budget-name'] = async function ({ name }) { + return validateBudgetName(name); +}; + +handlers['unique-budget-name'] = async function ({ name }) { + return uniqueBudgetName(name); +}; + handlers['get-budgets'] = async function () { const paths = await fs.listDir(fs.getDocumentDir()); const budgets = ( @@ -1879,7 +1877,7 @@ handlers['close-budget'] = async function () { } prefs.unloadPrefs(); - stopBackupService(); + await stopBackupService(); return 'ok'; }; @@ -1892,13 +1890,102 @@ handlers['delete-budget'] = async function ({ id, cloudFileId }) { // If a local file exists, you can delete it by passing its local id if (id) { - const budgetDir = fs.getBudgetDir(id); - await fs.removeDirRecursively(budgetDir); + // opening and then closing the database is a hack to be able to delete + // the budget file if it hasn't been opened yet. This needs a better + // way, but works for now. + try { + await db.openDatabase(id); + await db.closeDatabase(); + const budgetDir = fs.getBudgetDir(id); + await fs.removeDirRecursively(budgetDir); + } catch (e) { + return 'fail'; + } } return 'ok'; }; +handlers['duplicate-budget'] = async function ({ + id, + newName, + cloudSync, + open, +}): Promise { + if (!id) throw new Error('Unable to duplicate a budget that is not local.'); + + const { valid, message } = await validateBudgetName(newName); + if (!valid) throw new Error(message); + + const budgetDir = fs.getBudgetDir(id); + + const newId = await idFromBudgetName(newName); + + // copy metadata from current budget + // replace id with new budget id and budgetName with new budget name + const metadataText = await fs.readFile(fs.join(budgetDir, 'metadata.json')); + const metadata = JSON.parse(metadataText); + metadata.id = newId; + metadata.budgetName = newName; + [ + 'cloudFileId', + 'groupId', + 'lastUploaded', + 'encryptKeyId', + 'lastSyncedTimestamp', + ].forEach(item => { + if (metadata[item]) delete metadata[item]; + }); + + try { + const newBudgetDir = fs.getBudgetDir(newId); + await fs.mkdir(newBudgetDir); + + // write metadata for new budget + await fs.writeFile( + fs.join(newBudgetDir, 'metadata.json'), + JSON.stringify(metadata), + ); + + await fs.copyFile( + fs.join(budgetDir, 'db.sqlite'), + fs.join(newBudgetDir, 'db.sqlite'), + ); + } catch (error) { + // Clean up any partially created files + try { + const newBudgetDir = fs.getBudgetDir(newId); + if (await fs.exists(newBudgetDir)) { + await fs.removeDirRecursively(newBudgetDir); + } + } catch {} // Ignore cleanup errors + throw new Error(`Failed to duplicate budget: ${error.message}`); + } + + // load in and validate + const { error } = await loadBudget(newId); + if (error) { + console.log('Error duplicating budget: ' + error); + return error; + } + + if (cloudSync) { + try { + await cloudStorage.upload(); + } catch (error) { + console.warn('Failed to sync duplicated budget to cloud:', error); + // Ignore any errors uploading. If they are offline they should + // still be able to create files. + } + } + + handlers['close-budget'](); + if (open === 'original') await loadBudget(id); + if (open === 'copy') await loadBudget(newId); + + return newId; +}; + handlers['create-budget'] = async function ({ budgetName, avoidUpload, @@ -1921,13 +2008,10 @@ handlers['create-budget'] = async function ({ } else { // Generate budget name if not given if (!budgetName) { - // Unfortunately we need to load all of the existing files first - // so we can detect conflicting names. - const files = await handlers['get-budgets'](); - budgetName = await uniqueFileName(files); + budgetName = await uniqueBudgetName(); } - id = await idFromFileName(budgetName); + id = await idFromBudgetName(budgetName); } const budgetDir = fs.getBudgetDir(id); @@ -1993,8 +2077,8 @@ handlers['export-budget'] = async function () { } }; -async function loadBudget(id) { - let dir; +async function loadBudget(id: string) { + let dir: string; try { dir = fs.getBudgetDir(id); } catch (e) { @@ -2071,7 +2155,7 @@ async function loadBudget(id) { !Platform.isMobile && process.env.NODE_ENV !== 'test' ) { - startBackupService(id); + await startBackupService(id); } try { diff --git a/packages/loot-core/src/server/util/budget-name.ts b/packages/loot-core/src/server/util/budget-name.ts index 3c94888f0da..dfe492e5c51 100644 --- a/packages/loot-core/src/server/util/budget-name.ts +++ b/packages/loot-core/src/server/util/budget-name.ts @@ -1,16 +1,18 @@ -// @ts-strict-ignore import { v4 as uuidv4 } from 'uuid'; import * as fs from '../../platform/server/fs'; +import { handlers } from '../main'; -export async function uniqueFileName(existingFiles) { - const initialName = 'My Finances'; +export async function uniqueBudgetName( + initialName: string = 'My Finances', +): Promise { + const budgets = await handlers['get-budgets'](); let idx = 1; // If there is a conflict, keep appending an index until there is no // conflict and we have a unique name let newName = initialName; - while (existingFiles.find(file => file.name === newName)) { + while (budgets.find(file => file.name === newName)) { newName = `${initialName} ${idx}`; idx++; } @@ -18,7 +20,25 @@ export async function uniqueFileName(existingFiles) { return newName; } -export async function idFromFileName(name) { +export async function validateBudgetName( + name: string, +): Promise<{ valid: boolean; message?: string }> { + const trimmedName = name.trim(); + const uniqueName = await uniqueBudgetName(trimmedName); + let message: string | null = null; + + if (trimmedName === '') message = 'Budget name cannot be blank'; + if (trimmedName.length > 100) { + message = 'Budget name is too long (max length 100)'; + } + if (uniqueName !== trimmedName) { + message = `“${name}” already exists, try “${uniqueName}” instead`; + } + + return message ? { valid: false, message } : { valid: true }; +} + +export async function idFromBudgetName(name: string): Promise { let id = name.replace(/( |[^A-Za-z0-9])/g, '-') + '-' + uuidv4().slice(0, 7); // Make sure the id is unique. There's a chance one could already diff --git a/packages/loot-core/src/shared/query.ts b/packages/loot-core/src/shared/query.ts index 4b38e62ac8c..b53ad149867 100644 --- a/packages/loot-core/src/shared/query.ts +++ b/packages/loot-core/src/shared/query.ts @@ -5,18 +5,18 @@ type ObjectExpression = { }; export type QueryState = { - table: string; - tableOptions: Record; - filterExpressions: Array; - selectExpressions: Array; - groupExpressions: Array; - orderExpressions: Array; - calculation: boolean; - rawMode: boolean; - withDead: boolean; - validateRefs: boolean; - limit: number | null; - offset: number | null; + get table(): string; + get tableOptions(): Readonly>; + get filterExpressions(): ReadonlyArray; + get selectExpressions(): ReadonlyArray; + get groupExpressions(): ReadonlyArray; + get orderExpressions(): ReadonlyArray; + get calculation(): boolean; + get rawMode(): boolean; + get withDead(): boolean; + get validateRefs(): boolean; + get limit(): number | null; + get offset(): number | null; }; export class Query { @@ -76,15 +76,19 @@ export class Query { exprs = [exprs]; } - const query = new Query({ ...this.state, selectExpressions: exprs }); - query.state.calculation = false; - return query; + return new Query({ + ...this.state, + selectExpressions: exprs, + calculation: false, + }); } calculate(expr: ObjectExpression | string) { - const query = this.select({ result: expr }); - query.state.calculation = true; - return query; + return new Query({ + ...this.state, + selectExpressions: [{ result: expr }], + calculation: true, + }); } groupBy(exprs: ObjectExpression | string | Array) { @@ -140,6 +144,10 @@ export class Query { serialize() { return this.state; } + + serializeAsString() { + return JSON.stringify(this.serialize()); + } } export function getPrimaryOrderBy( diff --git a/packages/loot-core/src/shared/rules.ts b/packages/loot-core/src/shared/rules.ts index cbfe591964f..82c837fe177 100644 --- a/packages/loot-core/src/shared/rules.ts +++ b/packages/loot-core/src/shared/rules.ts @@ -21,6 +21,8 @@ const TYPE_INFO = { 'isNot', 'doesNotContain', 'notOneOf', + 'onBudget', + 'offBudget', ], nullable: true, }, @@ -65,12 +67,16 @@ const FIELD_INFO = { type: 'string', disallowedOps: new Set(['hasTags']), }, - payee: { type: 'id' }, + payee: { type: 'id', disallowedOps: new Set(['onBudget', 'offBudget']) }, payee_name: { type: 'string' }, date: { type: 'date' }, notes: { type: 'string' }, amount: { type: 'number' }, - category: { type: 'id', internalOps: new Set(['and']) }, + category: { + type: 'id', + disallowedOps: new Set(['onBudget', 'offBudget']), + internalOps: new Set(['and']), + }, account: { type: 'id' }, cleared: { type: 'boolean' }, reconciled: { type: 'boolean' }, @@ -199,6 +205,10 @@ export function friendlyOp(op, type?) { return t('and'); case 'or': return 'or'; + case 'onBudget': + return 'is on budget'; + case 'offBudget': + return 'is off budget'; default: return ''; } diff --git a/packages/loot-core/src/types/models/dashboard.d.ts b/packages/loot-core/src/types/models/dashboard.d.ts index 8092fb57acc..79c2a58b52b 100644 --- a/packages/loot-core/src/types/models/dashboard.d.ts +++ b/packages/loot-core/src/types/models/dashboard.d.ts @@ -66,7 +66,8 @@ type SpecializedWidget = | CashFlowWidget | SpendingWidget | MarkdownWidget - | SummaryWidget; + | SummaryWidget + | CalendarWidget; export type Widget = SpecializedWidget | CustomReportWidget; export type NewWidget = Omit; @@ -115,3 +116,13 @@ export type PercentageSummaryContent = { }; export type SummaryContent = BaseSummaryContent | PercentageSummaryContent; + +export type CalendarWidget = AbstractWidget< + 'calendar-card', + { + name?: string; + conditions?: RuleConditionEntity[]; + conditionsOp?: 'and' | 'or'; + timeFrame?: TimeFrame; + } | null +>; diff --git a/packages/loot-core/src/types/models/rule.d.ts b/packages/loot-core/src/types/models/rule.d.ts index 5cbbe7d3697..8d85f2c6634 100644 --- a/packages/loot-core/src/types/models/rule.d.ts +++ b/packages/loot-core/src/types/models/rule.d.ts @@ -27,7 +27,9 @@ export type RuleConditionOp = | 'doesNotContain' | 'hasTags' | 'and' - | 'matches'; + | 'matches' + | 'onBudget' + | 'offBudget'; type FieldValueTypes = { account: string; @@ -76,6 +78,8 @@ export type RuleConditionEntity = | 'contains' | 'doesNotContain' | 'matches' + | 'onBudget' + | 'offBudget' > | BaseConditionEntity< 'category', diff --git a/packages/loot-core/src/types/server-handlers.d.ts b/packages/loot-core/src/types/server-handlers.d.ts index 4afbc72a3e8..92b872e54f5 100644 --- a/packages/loot-core/src/types/server-handlers.d.ts +++ b/packages/loot-core/src/types/server-handlers.d.ts @@ -178,7 +178,7 @@ export interface ServerHandlers { 'account-move': (arg: { id; targetId }) => Promise; - 'secret-set': (arg: { name: string; value: string }) => Promise; + 'secret-set': (arg: { name: string; value: string | null }) => Promise; 'secret-check': (arg: string) => Promise; 'gocardless-poll-web-token': (arg: { @@ -304,6 +304,12 @@ export interface ServerHandlers { | { messages: Message[] } >; + 'validate-budget-name': (arg: { + name: string; + }) => Promise<{ valid: boolean; message?: string }>; + + 'unique-budget-name': (arg: { name: string }) => Promise; + 'get-budgets': () => Promise; 'get-remote-files': () => Promise; @@ -327,7 +333,24 @@ export interface ServerHandlers { 'delete-budget': (arg: { id?: string; cloudFileId?: string; - }) => Promise<'ok'>; + }) => Promise<'ok' | 'fail'>; + + /** + * Duplicates a budget file. + * @param {Object} arg - The arguments for duplicating a budget. + * @param {string} [arg.id] - The ID of the local budget to duplicate. + * @param {string} [arg.cloudId] - The ID of the cloud-synced budget to duplicate. + * @param {string} arg.newName - The name for the duplicated budget. + * @param {boolean} [arg.cloudSync] - Whether to sync the duplicated budget to the cloud. + * @returns {Promise} The ID of the newly created budget. + */ + 'duplicate-budget': (arg: { + id?: string; + cloudId?: string; + newName: string; + cloudSync?: boolean; + open: 'none' | 'original' | 'copy'; + }) => Promise; 'create-budget': (arg: { budgetName?; diff --git a/upcoming-release-notes/3828.md b/upcoming-release-notes/3828.md new file mode 100644 index 00000000000..130e4380ba7 --- /dev/null +++ b/upcoming-release-notes/3828.md @@ -0,0 +1,6 @@ +--- +category: Features +authors: [lelemm] +--- + +Added Calendar report diff --git a/upcoming-release-notes/3847.md b/upcoming-release-notes/3847.md new file mode 100644 index 00000000000..785e81f7abe --- /dev/null +++ b/upcoming-release-notes/3847.md @@ -0,0 +1,6 @@ +--- +category: Features +authors: [tlesicka] +--- + +Added ability to duplicate budgets. diff --git a/upcoming-release-notes/3879.md b/upcoming-release-notes/3879.md new file mode 100644 index 00000000000..89887d8dedf --- /dev/null +++ b/upcoming-release-notes/3879.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [joel-jeremy] +--- + +Optimize useSheetValue hook diff --git a/upcoming-release-notes/3891.md b/upcoming-release-notes/3891.md new file mode 100644 index 00000000000..034344b1d13 --- /dev/null +++ b/upcoming-release-notes/3891.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [lelemm] +--- + +Filter accounts when on budget or off budget diff --git a/upcoming-release-notes/3893.md b/upcoming-release-notes/3893.md new file mode 100644 index 00000000000..b0bbe2178cb --- /dev/null +++ b/upcoming-release-notes/3893.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [joel-jeremy] +--- + +Use useTranslation hook instead of directly importing t function from i18next diff --git a/upcoming-release-notes/3899.md b/upcoming-release-notes/3899.md new file mode 100644 index 00000000000..f330782869e --- /dev/null +++ b/upcoming-release-notes/3899.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [joel-jeremy] +--- + +Convert BudgetTable.jsx to TypeScript diff --git a/upcoming-release-notes/3900.md b/upcoming-release-notes/3900.md new file mode 100644 index 00000000000..6482218bc0c --- /dev/null +++ b/upcoming-release-notes/3900.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [joel-jeremy] +--- + +Add loading indicator when loading more transactions in mobile transaction list. diff --git a/upcoming-release-notes/3903.md b/upcoming-release-notes/3903.md new file mode 100644 index 00000000000..14f2456f37c --- /dev/null +++ b/upcoming-release-notes/3903.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [joel-jeremy] +--- + +Use consistent terms for on budget accounts i.e. `For budget`/`Budgeted` --> `On budget`. diff --git a/upcoming-release-notes/3911.md b/upcoming-release-notes/3911.md new file mode 100644 index 00000000000..01f7348949c --- /dev/null +++ b/upcoming-release-notes/3911.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [joel-jeremy] +--- + +Remove usage of useActions hook diff --git a/upcoming-release-notes/3942.md b/upcoming-release-notes/3942.md new file mode 100644 index 00000000000..c66d2b9326b --- /dev/null +++ b/upcoming-release-notes/3942.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [MatissJanis] +--- + +Fix misaligned gocardless credential popover. diff --git a/upcoming-release-notes/3943.md b/upcoming-release-notes/3943.md new file mode 100644 index 00000000000..988e0ac840c --- /dev/null +++ b/upcoming-release-notes/3943.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [MatissJanis] +--- + +Fix rule creation throwing error for "notes contains (nothing)" condition. diff --git a/upcoming-release-notes/3945.md b/upcoming-release-notes/3945.md new file mode 100644 index 00000000000..219e2d8618a --- /dev/null +++ b/upcoming-release-notes/3945.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [dkhalife] +--- + +Migrate useSplitsExpanded to TypeScript \ No newline at end of file diff --git a/upcoming-release-notes/3959.md b/upcoming-release-notes/3959.md new file mode 100644 index 00000000000..3d33f1f6154 --- /dev/null +++ b/upcoming-release-notes/3959.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [dkhalife] +--- + +Migrate CategoryTransactions to TypeScript \ No newline at end of file diff --git a/upcoming-release-notes/3962.md b/upcoming-release-notes/3962.md new file mode 100644 index 00000000000..be7f74b03cb --- /dev/null +++ b/upcoming-release-notes/3962.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [mbaeuerle] +--- + +Fix iOS mobile navigation tabs disappearing on bouncing top and appearing on bouncing bottom. diff --git a/upcoming-release-notes/3985.md b/upcoming-release-notes/3985.md new file mode 100644 index 00000000000..827aec6910b --- /dev/null +++ b/upcoming-release-notes/3985.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [talkintomato] +--- + +Fix space missing on create local account copy