diff --git a/eslint.config.mjs b/eslint.config.mjs index 7f065e79b8e..96509d58c46 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -790,20 +790,7 @@ export default [ 'packages/desktop-client/src/components/sidebar/Tools.tsx', 'packages/desktop-client/src/components/sort.tsx', 'packages/desktop-client/src/components/spreadsheet/useSheetValue.ts', - 'packages/desktop-client/src/components/table.tsx', - 'packages/desktop-client/src/components/Titlebar.tsx', - 'packages/desktop-client/src/components/transactions/MobileTransaction.jsx', - 'packages/desktop-client/src/components/transactions/SelectedTransactions.jsx', - 'packages/desktop-client/src/components/transactions/SimpleTransactionsTable.jsx', 'packages/desktop-client/src/components/transactions/TransactionList.jsx', - 'packages/desktop-client/src/components/transactions/TransactionsTable.jsx', - 'packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx', - 'packages/desktop-client/src/hooks/useAccounts.ts', - 'packages/desktop-client/src/hooks/useCategories.ts', - 'packages/desktop-client/src/hooks/usePayees.ts', - 'packages/desktop-client/src/hooks/useProperFocus.tsx', - 'packages/desktop-client/src/hooks/useSelected.tsx', - 'packages/loot-core/src/client/query-hooks.tsx', ], rules: { diff --git a/package.json b/package.json index 4d1d66729a4..bd8048a0fb6 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,9 @@ }, "scripts": { "start": "yarn start:browser", - "start:server": "yarn workspace actual-sync start", + "start:server": "yarn workspace @actual-app/sync-server start", + "start:server-monitor": "yarn workspace @actual-app/sync-server start-monitor", + "start:server-dev": "NODE_ENV=development BROWSER_OPEN=localhost:5006 yarn npm-run-all --parallel 'start:server-monitor' 'start'", "start:desktop": "yarn rebuild-electron && npm-run-all --parallel 'start:desktop-*'", "start:desktop-node": "yarn workspace loot-core watch:node", "start:desktop-client": "yarn workspace @actual-app/web watch", @@ -41,7 +43,7 @@ "rebuild-node": "yarn workspace loot-core rebuild", "lint": "eslint . --max-warnings 0", "lint:verbose": "DEBUG=eslint:cli-engine eslint . --max-warnings 0", - "install:server": "yarn workspaces focus actual-sync --production", + "install:server": "yarn workspaces focus @actual-app/sync-server --production", "typecheck": "yarn tsc && tsc-strict", "jq": "./node_modules/node-jq/bin/jq", "prepare": "husky" diff --git a/packages/component-library/src/styles.ts b/packages/component-library/src/styles.ts index c5ac0e22b56..ea21ddd0a9c 100644 --- a/packages/component-library/src/styles.ts +++ b/packages/component-library/src/styles.ts @@ -148,4 +148,10 @@ export const styles: Record = { lightScrollbar: null as CSSProperties | null, darkScrollbar: null as CSSProperties | null, scrollbarWidth: null as number | null, + editorPill: { + color: theme.pillText, + backgroundColor: theme.pillBackground, + borderRadius: 4, + padding: '3px 5px', + }, }; diff --git a/packages/desktop-client/e2e/accounts.mobile.test.ts-snapshots/Mobile-Accounts-opens-individual-account-page-and-checks-that-filtering-is-working-1-chromium-linux.png b/packages/desktop-client/e2e/accounts.mobile.test.ts-snapshots/Mobile-Accounts-opens-individual-account-page-and-checks-that-filtering-is-working-1-chromium-linux.png index cb84ec53392..1ad2d39d322 100644 Binary files a/packages/desktop-client/e2e/accounts.mobile.test.ts-snapshots/Mobile-Accounts-opens-individual-account-page-and-checks-that-filtering-is-working-1-chromium-linux.png and b/packages/desktop-client/e2e/accounts.mobile.test.ts-snapshots/Mobile-Accounts-opens-individual-account-page-and-checks-that-filtering-is-working-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/accounts.mobile.test.ts-snapshots/Mobile-Accounts-opens-individual-account-page-and-checks-that-filtering-is-working-2-chromium-linux.png b/packages/desktop-client/e2e/accounts.mobile.test.ts-snapshots/Mobile-Accounts-opens-individual-account-page-and-checks-that-filtering-is-working-2-chromium-linux.png index 2e33859da81..abf12e0eff4 100644 Binary files a/packages/desktop-client/e2e/accounts.mobile.test.ts-snapshots/Mobile-Accounts-opens-individual-account-page-and-checks-that-filtering-is-working-2-chromium-linux.png and b/packages/desktop-client/e2e/accounts.mobile.test.ts-snapshots/Mobile-Accounts-opens-individual-account-page-and-checks-that-filtering-is-working-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/accounts.mobile.test.ts-snapshots/Mobile-Accounts-opens-individual-account-page-and-checks-that-filtering-is-working-3-chromium-linux.png b/packages/desktop-client/e2e/accounts.mobile.test.ts-snapshots/Mobile-Accounts-opens-individual-account-page-and-checks-that-filtering-is-working-3-chromium-linux.png index 03cc182961c..59e18cee676 100644 Binary files a/packages/desktop-client/e2e/accounts.mobile.test.ts-snapshots/Mobile-Accounts-opens-individual-account-page-and-checks-that-filtering-is-working-3-chromium-linux.png and b/packages/desktop-client/e2e/accounts.mobile.test.ts-snapshots/Mobile-Accounts-opens-individual-account-page-and-checks-that-filtering-is-working-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts b/packages/desktop-client/e2e/budget.mobile.test.ts index 6464c525e65..e8440744ab4 100644 --- a/packages/desktop-client/e2e/budget.mobile.test.ts +++ b/packages/desktop-client/e2e/budget.mobile.test.ts @@ -60,6 +60,9 @@ async function setBudgetAverage( await budgetPage.goToPreviousMonth(); const spentButton = await budgetPage.getButtonForSpent(categoryName); const spent = await spentButton.textContent(); + if (!spent) { + throw new Error('Failed to get spent amount'); + } totalSpent += currencyToAmount(spent) ?? 0; } @@ -280,6 +283,10 @@ budgetTypes.forEach(budgetType => { const lastMonthBudget = await budgetedButton.textContent(); + if (!lastMonthBudget) { + throw new Error('Failed to get last month budget'); + } + await budgetPage.goToNextMonth(); await copyLastMonthBudget(budgetPage, categoryName); diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-applies-budget-template-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-applies-budget-template-1-chromium-linux.png index 15255672a53..a09a8a2dce3 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-applies-budget-template-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-applies-budget-template-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-applies-budget-template-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-applies-budget-template-2-chromium-linux.png index 563cb12d538..4c31f8f3bdf 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-applies-budget-template-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-applies-budget-template-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-applies-budget-template-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-applies-budget-template-3-chromium-linux.png index f5330ca0bdc..c17afe1bf9b 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-applies-budget-template-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-applies-budget-template-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--14404-in-the-page-header-opens-the-month-menu-modal-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--14404-in-the-page-header-opens-the-month-menu-modal-3-chromium-linux.png index d5954385694..d2659616376 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--14404-in-the-page-header-opens-the-month-menu-modal-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--14404-in-the-page-header-opens-the-month-menu-modal-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--321fd-ed-amount-opens-the-budget-summary-menu-modal-2-chromium-linux.png index 4e6252fec76..e877ef6e32d 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-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.ts-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.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--4bb70-ed-amount-opens-the-budget-summary-menu-modal-1-chromium-linux.png index 65c60199b30..5a916e40e7c 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-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.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--589a6-the-page-header-shows-the-next-month-s-budget-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--589a6-the-page-header-shows-the-next-month-s-budget-2-chromium-linux.png index 058a5667394..f234dfd9f50 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--589a6-the-page-header-shows-the-next-month-s-budget-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--589a6-the-page-header-shows-the-next-month-s-budget-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--6ab37-roup-name-opens-the-category-group-menu-modal-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--6ab37-roup-name-opens-the-category-group-menu-modal-1-chromium-linux.png index 488a1a1eef8..4e288662e02 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--6ab37-roup-name-opens-the-category-group-menu-modal-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--6ab37-roup-name-opens-the-category-group-menu-modal-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--6dbdb-page-header-shows-the-previous-month-s-budget-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--6dbdb-page-header-shows-the-previous-month-s-budget-1-chromium-linux.png index 96bba7fcc06..840c615edcb 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--6dbdb-page-header-shows-the-previous-month-s-budget-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--6dbdb-page-header-shows-the-previous-month-s-budget-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--7bd8f-the-page-header-shows-the-next-month-s-budget-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--7bd8f-the-page-header-shows-the-next-month-s-budget-3-chromium-linux.png index 988870579d7..ba95fa68fd9 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--7bd8f-the-page-header-shows-the-next-month-s-budget-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--7bd8f-the-page-header-shows-the-next-month-s-budget-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--884ac-the-page-header-shows-the-next-month-s-budget-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--884ac-the-page-header-shows-the-next-month-s-budget-1-chromium-linux.png index 460e95731b2..d9bcaf20707 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--884ac-the-page-header-shows-the-next-month-s-budget-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--884ac-the-page-header-shows-the-next-month-s-budget-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--94a79-roup-name-opens-the-category-group-menu-modal-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--94a79-roup-name-opens-the-category-group-menu-modal-2-chromium-linux.png index 864a20db262..a0c256d2f68 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--94a79-roup-name-opens-the-category-group-menu-modal-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--94a79-roup-name-opens-the-category-group-menu-modal-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--94a85-ed-amount-opens-the-budget-summary-menu-modal-3-chromium-linux.png index 55a7c2da4a5..fb15769e771 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-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.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--96ebb-page-header-shows-the-previous-month-s-budget-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--96ebb-page-header-shows-the-previous-month-s-budget-3-chromium-linux.png index 0d7aac39903..3dceb48d7fd 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--96ebb-page-header-shows-the-previous-month-s-budget-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--96ebb-page-header-shows-the-previous-month-s-budget-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--9e6aa-in-the-page-header-opens-the-budget-page-menu-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--9e6aa-in-the-page-header-opens-the-budget-page-menu-1-chromium-linux.png index 8095e242ede..a290f3445b6 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--9e6aa-in-the-page-header-opens-the-budget-page-menu-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--9e6aa-in-the-page-header-opens-the-budget-page-menu-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--bbde3-roup-name-opens-the-category-group-menu-modal-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--bbde3-roup-name-opens-the-category-group-menu-modal-3-chromium-linux.png index fe047e927c7..a776161a5eb 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--bbde3-roup-name-opens-the-category-group-menu-modal-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--bbde3-roup-name-opens-the-category-group-menu-modal-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--bed18-in-the-page-header-opens-the-month-menu-modal-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--bed18-in-the-page-header-opens-the-month-menu-modal-1-chromium-linux.png index 702e3ee8ff2..10737edb3ae 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--bed18-in-the-page-header-opens-the-month-menu-modal-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--bed18-in-the-page-header-opens-the-month-menu-modal-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--ceb3a-in-the-page-header-opens-the-month-menu-modal-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--ceb3a-in-the-page-header-opens-the-month-menu-modal-2-chromium-linux.png index 41cced4df73..ce2728cbc87 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--ceb3a-in-the-page-header-opens-the-month-menu-modal-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--ceb3a-in-the-page-header-opens-the-month-menu-modal-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--d270d-in-the-page-header-opens-the-budget-page-menu-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--d270d-in-the-page-header-opens-the-budget-page-menu-2-chromium-linux.png index 36d8a1a9b35..eebcb154084 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--d270d-in-the-page-header-opens-the-budget-page-menu-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--d270d-in-the-page-header-opens-the-budget-page-menu-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--d7184-page-header-shows-the-previous-month-s-budget-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--d7184-page-header-shows-the-previous-month-s-budget-2-chromium-linux.png index b1807e1f4c5..1efe67bb6ac 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--d7184-page-header-shows-the-previous-month-s-budget-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--d7184-page-header-shows-the-previous-month-s-budget-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--fdd57-in-the-page-header-opens-the-budget-page-menu-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--fdd57-in-the-page-header-opens-the-budget-page-menu-3-chromium-linux.png index ac2b51b1a84..52e2f6f1224 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--fdd57-in-the-page-header-opens-the-budget-page-menu-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking--fdd57-in-the-page-header-opens-the-budget-page-menu-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking-the-balance-cell-opens-the-balance-menu-modal-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking-the-balance-cell-opens-the-balance-menu-modal-1-chromium-linux.png index 9a4018ca1cc..965fbe10a63 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking-the-balance-cell-opens-the-balance-menu-modal-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-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.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking-the-balance-cell-opens-the-balance-menu-modal-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking-the-balance-cell-opens-the-balance-menu-modal-2-chromium-linux.png index 26354764c8e..b8ecd517062 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking-the-balance-cell-opens-the-balance-menu-modal-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-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.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking-the-balance-cell-opens-the-balance-menu-modal-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking-the-balance-cell-opens-the-balance-menu-modal-3-chromium-linux.png index 6aaaed2de0d..8432e50d7a1 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking-the-balance-cell-opens-the-balance-menu-modal-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-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.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking-the-budgeted-cell-opens-the-budget-menu-modal-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking-the-budgeted-cell-opens-the-budget-menu-modal-1-chromium-linux.png index e2cdde0d388..bd2ba94d3d4 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking-the-budgeted-cell-opens-the-budget-menu-modal-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-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.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking-the-budgeted-cell-opens-the-budget-menu-modal-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking-the-budgeted-cell-opens-the-budget-menu-modal-2-chromium-linux.png index e349b84c89a..637688a2133 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking-the-budgeted-cell-opens-the-budget-menu-modal-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-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.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking-the-budgeted-cell-opens-the-budget-menu-modal-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking-the-budgeted-cell-opens-the-budget-menu-modal-3-chromium-linux.png index 66c6fb6ea03..f445bf810f8 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking-the-budgeted-cell-opens-the-budget-menu-modal-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-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.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking-the-category-name-opens-the-category-menu-modal-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking-the-category-name-opens-the-category-menu-modal-1-chromium-linux.png index f6ee99e7e85..c1ff14681a0 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking-the-category-name-opens-the-category-menu-modal-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-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.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking-the-category-name-opens-the-category-menu-modal-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking-the-category-name-opens-the-category-menu-modal-2-chromium-linux.png index f43f528db36..ff93ade188a 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking-the-category-name-opens-the-category-menu-modal-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-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.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking-the-category-name-opens-the-category-menu-modal-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking-the-category-name-opens-the-category-menu-modal-3-chromium-linux.png index afab8bed374..18612390e0b 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-checks-that-clicking-the-category-name-opens-the-category-menu-modal-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-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.ts-snapshots/Mobile-Budget-Envelope-copies-last-month-s-budget-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-copies-last-month-s-budget-1-chromium-linux.png index 868fde3b913..d2fc215a77c 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-copies-last-month-s-budget-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-copies-last-month-s-budget-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-copies-last-month-s-budget-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-copies-last-month-s-budget-2-chromium-linux.png index 2734b1732e2..756810dac33 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-copies-last-month-s-budget-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-copies-last-month-s-budget-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-copies-last-month-s-budget-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-copies-last-month-s-budget-3-chromium-linux.png index 3d73f699537..50254cba2d7 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-copies-last-month-s-budget-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-copies-last-month-s-budget-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png index e8b9fcc0a78..6c3647f527b 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png index e035685b377..208274acea0 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-loads-the-budget-page-with-budgeted-amounts-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-loads-the-budget-page-with-budgeted-amounts-3-chromium-linux.png index 03e5347f49c..1efba4b14a0 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-loads-the-budget-page-with-budgeted-amounts-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-loads-the-budget-page-with-budgeted-amounts-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-12-month-average-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-12-month-average-1-chromium-linux.png index cfb7676153c..eb65be1c244 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-12-month-average-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-12-month-average-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-12-month-average-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-12-month-average-2-chromium-linux.png index 2d90739b65b..f09ea6eb707 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-12-month-average-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-12-month-average-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-12-month-average-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-12-month-average-3-chromium-linux.png index c1fe2134fc8..369837c5258 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-12-month-average-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-12-month-average-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-3-month-average-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-3-month-average-1-chromium-linux.png index 6c727d9aed0..688d04023ae 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-3-month-average-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-3-month-average-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-3-month-average-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-3-month-average-2-chromium-linux.png index 5779b5a4803..5679a056b32 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-3-month-average-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-3-month-average-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-3-month-average-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-3-month-average-3-chromium-linux.png index 726c57331af..684af02564a 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-3-month-average-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-3-month-average-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-6-month-average-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-6-month-average-1-chromium-linux.png index 6707abb9169..7532fff00ed 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-6-month-average-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-6-month-average-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-6-month-average-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-6-month-average-2-chromium-linux.png index b84640ca13c..a99d70c5e20 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-6-month-average-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-6-month-average-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-6-month-average-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-6-month-average-3-chromium-linux.png index 1445a7f3912..fa852315aa7 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-6-month-average-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-set-budget-to-6-month-average-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-updates-the-budgeted-amount-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-updates-the-budgeted-amount-1-chromium-linux.png index 8d1410f1419..cdd4a4df820 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-updates-the-budgeted-amount-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-updates-the-budgeted-amount-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-updates-the-budgeted-amount-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-updates-the-budgeted-amount-2-chromium-linux.png index 5eee10c8c07..c7c991acab6 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-updates-the-budgeted-amount-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-updates-the-budgeted-amount-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-updates-the-budgeted-amount-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-updates-the-budgeted-amount-3-chromium-linux.png index 01cf9184a29..de2dadbce3f 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-updates-the-budgeted-amount-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Envelope-updates-the-budgeted-amount-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-applies-budget-template-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-applies-budget-template-1-chromium-linux.png index 74f34fcd050..97fa9a3c9ae 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-applies-budget-template-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-applies-budget-template-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-applies-budget-template-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-applies-budget-template-2-chromium-linux.png index ea66cde7373..f09a12bc770 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-applies-budget-template-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-applies-budget-template-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-applies-budget-template-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-applies-budget-template-3-chromium-linux.png index d02580c3f5f..a71d5ece56e 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-applies-budget-template-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-applies-budget-template-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking--0ba04-nt-amount-opens-the-budget-summary-menu-modal-1-chromium-linux.png index 35aa12cd8cf..35962825af5 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-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.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking--0dfe7-page-header-shows-the-previous-month-s-budget-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking--0dfe7-page-header-shows-the-previous-month-s-budget-1-chromium-linux.png index 73e12fc76ea..f08aae653bf 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking--0dfe7-page-header-shows-the-previous-month-s-budget-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking--0dfe7-page-header-shows-the-previous-month-s-budget-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking--1ce6d-nt-amount-opens-the-budget-summary-menu-modal-2-chromium-linux.png index 5a748dfd7ee..7129a1c3a95 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-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.ts-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.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking--42062-in-the-page-header-opens-the-month-menu-modal-2-chromium-linux.png index 8771e50c51a..60e086f309c 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-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.ts-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.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking--49fb6-in-the-page-header-opens-the-month-menu-modal-3-chromium-linux.png index 73889255c41..5e0e887facc 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-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.ts-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.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking--5f098-roup-name-opens-the-category-group-menu-modal-1-chromium-linux.png index c4d46a75fd6..c835fb4b9a9 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-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.ts-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.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking--7c353-the-page-header-shows-the-next-month-s-budget-3-chromium-linux.png index 16b144855b5..2dc65951651 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-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.ts-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.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking--929be-roup-name-opens-the-category-group-menu-modal-3-chromium-linux.png index 7bbb28410fb..9fa793e405d 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-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.ts-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.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking--a3783-in-the-page-header-opens-the-budget-page-menu-1-chromium-linux.png index 1b22662391e..0b024176507 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-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.ts-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.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking--a8b5e-in-the-page-header-opens-the-budget-page-menu-3-chromium-linux.png index cf602b52a12..c285e167145 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-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.ts-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.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking--b1562-in-the-page-header-opens-the-month-menu-modal-1-chromium-linux.png index b16c2a13db2..dbf8c2f8e98 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-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.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking--cfb69-page-header-shows-the-previous-month-s-budget-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking--cfb69-page-header-shows-the-previous-month-s-budget-2-chromium-linux.png index a7fc68c3c29..6c386b9c4e1 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking--cfb69-page-header-shows-the-previous-month-s-budget-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking--cfb69-page-header-shows-the-previous-month-s-budget-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking--d5af6-the-page-header-shows-the-next-month-s-budget-1-chromium-linux.png index 74b81149829..c87750ded37 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-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.ts-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.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking--dc927-roup-name-opens-the-category-group-menu-modal-2-chromium-linux.png index 2aeeadb6513..a7e4624b552 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-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.ts-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.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking--f2198-the-page-header-shows-the-next-month-s-budget-2-chromium-linux.png index ccfa385f2f1..aa744a601bd 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-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.ts-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.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking--f224f-nt-amount-opens-the-budget-summary-menu-modal-3-chromium-linux.png index 2bc864ccd16..5012ebed6af 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-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.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking--f7fa3-page-header-shows-the-previous-month-s-budget-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking--f7fa3-page-header-shows-the-previous-month-s-budget-3-chromium-linux.png index bce1db2c68e..a7a4b942a7f 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking--f7fa3-page-header-shows-the-previous-month-s-budget-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking--f7fa3-page-header-shows-the-previous-month-s-budget-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking--f8a19-in-the-page-header-opens-the-budget-page-menu-2-chromium-linux.png index ac5fb95ee8c..e0dc12c4fdb 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-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.ts-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.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-balance-cell-opens-the-balance-menu-modal-1-chromium-linux.png index 69b5304e0d0..a8367743e84 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-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.ts-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.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-balance-cell-opens-the-balance-menu-modal-2-chromium-linux.png index b58968edc91..98d654c05be 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-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.ts-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.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-balance-cell-opens-the-balance-menu-modal-3-chromium-linux.png index 5d5c9e88589..3ddd24ae4f2 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-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.ts-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.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-budgeted-cell-opens-the-budget-menu-modal-1-chromium-linux.png index d47bb949f4f..b56d301b2dc 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-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.ts-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.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-budgeted-cell-opens-the-budget-menu-modal-2-chromium-linux.png index 0fedc45eb04..b5220fd98be 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-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.ts-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.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-budgeted-cell-opens-the-budget-menu-modal-3-chromium-linux.png index 40d0b0f408a..edf317632d0 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-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.ts-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.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-category-name-opens-the-category-menu-modal-1-chromium-linux.png index 6c748af4cd7..717c11c0f7e 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-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.ts-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.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-category-name-opens-the-category-menu-modal-2-chromium-linux.png index 89b5a6cd2c9..c55e4caa729 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-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.ts-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.ts-snapshots/Mobile-Budget-Tracking-checks-that-clicking-the-category-name-opens-the-category-menu-modal-3-chromium-linux.png index a23bf489f0e..89331cc0a6d 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-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.ts-snapshots/Mobile-Budget-Tracking-copies-last-month-s-budget-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-copies-last-month-s-budget-1-chromium-linux.png index 7ccff58c183..3d8924bb61e 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-copies-last-month-s-budget-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-copies-last-month-s-budget-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-copies-last-month-s-budget-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-copies-last-month-s-budget-2-chromium-linux.png index 94d0d64dc2f..108ad326b72 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-copies-last-month-s-budget-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-copies-last-month-s-budget-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-copies-last-month-s-budget-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-copies-last-month-s-budget-3-chromium-linux.png index 18ff1d4e89e..1fa98576de2 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-copies-last-month-s-budget-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-copies-last-month-s-budget-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png index 9b379af8aa4..1e7f594b648 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-snapshots/Mobile-Budget-Tracking-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png index 0ed4936a89c..27ac56d0afa 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-snapshots/Mobile-Budget-Tracking-loads-the-budget-page-with-budgeted-amounts-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-loads-the-budget-page-with-budgeted-amounts-3-chromium-linux.png index cf697f2f37b..8e54cfd1843 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-loads-the-budget-page-with-budgeted-amounts-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-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.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-12-month-average-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-12-month-average-1-chromium-linux.png index fb5f9a10041..6b0042f0228 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-12-month-average-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-12-month-average-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-12-month-average-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-12-month-average-2-chromium-linux.png index 48021d7b6fd..7970352a7d5 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-12-month-average-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-12-month-average-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-12-month-average-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-12-month-average-3-chromium-linux.png index 8aea9621129..68b8adc8421 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-12-month-average-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-12-month-average-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-3-month-average-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-3-month-average-1-chromium-linux.png index f0339368f86..a9046b593db 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-3-month-average-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-3-month-average-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-3-month-average-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-3-month-average-2-chromium-linux.png index 548c941757d..79ed5662f19 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-3-month-average-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-3-month-average-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-3-month-average-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-3-month-average-3-chromium-linux.png index c4df2d4cccd..54d41f5900b 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-3-month-average-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-3-month-average-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-6-month-average-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-6-month-average-1-chromium-linux.png index fdf1c3efc04..40e7a978f40 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-6-month-average-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-6-month-average-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-6-month-average-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-6-month-average-2-chromium-linux.png index 6abaecbf418..98a27ebe2a2 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-6-month-average-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-6-month-average-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-6-month-average-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-6-month-average-3-chromium-linux.png index a4795b81bd4..789f2fb7ca0 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-6-month-average-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-set-budget-to-6-month-average-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-updates-the-budgeted-amount-1-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-updates-the-budgeted-amount-1-chromium-linux.png index 2e23c82f7e6..b02611e69eb 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-updates-the-budgeted-amount-1-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-updates-the-budgeted-amount-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-updates-the-budgeted-amount-2-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-updates-the-budgeted-amount-2-chromium-linux.png index 0be2162edd0..a2c7a8d91a5 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-updates-the-budgeted-amount-2-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-updates-the-budgeted-amount-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-updates-the-budgeted-amount-3-chromium-linux.png b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-updates-the-budgeted-amount-3-chromium-linux.png index a1e56532afe..5a3872f1c35 100644 Binary files a/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-updates-the-budgeted-amount-3-chromium-linux.png and b/packages/desktop-client/e2e/budget.mobile.test.ts-snapshots/Mobile-Budget-Tracking-updates-the-budgeted-amount-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/page-models/account-page.js b/packages/desktop-client/e2e/page-models/account-page.ts similarity index 72% rename from packages/desktop-client/e2e/page-models/account-page.js rename to packages/desktop-client/e2e/page-models/account-page.ts index cbdf4efef41..32fd6072229 100644 --- a/packages/desktop-client/e2e/page-models/account-page.js +++ b/packages/desktop-client/e2e/page-models/account-page.ts @@ -1,7 +1,33 @@ +import { type Locator, type Page } from '@playwright/test'; + import { CloseAccountModal } from './close-account-modal'; +type TransactionEntry = { + debit?: string; + credit?: string; + account?: string; + payee?: string; + notes?: string; + category?: string; +}; + export class AccountPage { - constructor(page) { + readonly page: Page; + readonly accountName: Locator; + readonly accountBalance: Locator; + readonly addNewTransactionButton: Locator; + readonly newTransactionRow: Locator; + readonly addTransactionButton: Locator; + readonly cancelTransactionButton: Locator; + readonly accountMenuButton: Locator; + readonly transactionTable: Locator; + readonly transactionTableRow: Locator; + readonly filterButton: Locator; + readonly filterSelectTooltip: Locator; + readonly selectButton: Locator; + readonly selectTooltip: Locator; + + constructor(page: Page) { this.page = page; this.accountName = this.page.getByTestId('account-name'); @@ -30,14 +56,14 @@ export class AccountPage { this.selectTooltip = this.page.getByTestId('transactions-select-tooltip'); } - async waitFor() { - await this.transactionTable.waitFor(); + async waitFor(...options: Parameters) { + await this.transactionTable.waitFor(...options); } /** * Enter details of a transaction */ - async enterSingleTransaction(transaction) { + async enterSingleTransaction(transaction: TransactionEntry) { await this.addNewTransactionButton.click(); await this._fillTransactionFields(this.newTransactionRow, transaction); } @@ -53,7 +79,7 @@ export class AccountPage { /** * Create a single transaction */ - async createSingleTransaction(transaction) { + async createSingleTransaction(transaction: TransactionEntry) { await this.enterSingleTransaction(transaction); await this.addEnteredTransaction(); } @@ -61,7 +87,10 @@ export class AccountPage { /** * Create split transactions */ - async createSplitTransaction([rootTransaction, ...transactions]) { + async createSplitTransaction([ + rootTransaction, + ...transactions + ]: TransactionEntry[]) { await this.addNewTransactionButton.click(); // Root transaction @@ -87,7 +116,7 @@ export class AccountPage { await this.cancelTransactionButton.click(); } - async selectNthTransaction(index) { + async selectNthTransaction(index: number) { const row = this.transactionTableRow.nth(index); await row.getByTestId('select').click(); } @@ -96,7 +125,7 @@ export class AccountPage { * Retrieve the data for the nth-transaction. * 0-based index */ - getNthTransaction(index) { + getNthTransaction(index: number) { const row = this.transactionTableRow.nth(index); return this._getTransactionDetails(row); @@ -106,11 +135,9 @@ export class AccountPage { return this._getTransactionDetails(this.newTransactionRow); } - _getTransactionDetails(row) { - const account = row.getByTestId('account'); - + _getTransactionDetails(row: Locator) { return { - ...(account ? { account } : {}), + account: row.getByTestId('account'), payee: row.getByTestId('payee'), notes: row.getByTestId('notes'), category: row.getByTestId('category'), @@ -119,7 +146,7 @@ export class AccountPage { }; } - async clickSelectAction(action) { + async clickSelectAction(action: string | RegExp) { await this.selectButton.click(); await this.selectTooltip.getByRole('button', { name: action }).click(); } @@ -130,16 +157,13 @@ export class AccountPage { async clickCloseAccount() { await this.accountMenuButton.click(); await this.page.getByRole('button', { name: 'Close Account' }).click(); - return new CloseAccountModal( - this.page, - this.page.getByTestId('close-account-modal'), - ); + return new CloseAccountModal(this.page.getByTestId('close-account-modal')); } /** * Open the filtering popover. */ - async filterBy(field) { + async filterBy(field: string | RegExp) { await this.filterButton.click(); await this.filterSelectTooltip.getByRole('button', { name: field }).click(); @@ -149,7 +173,7 @@ export class AccountPage { /** * Filter to a specific note */ - async filterByNote(note) { + async filterByNote(note: string) { const filterTooltip = await this.filterBy('Note'); await this.page.keyboard.type(note); await filterTooltip.applyButton.click(); @@ -158,16 +182,20 @@ export class AccountPage { /** * Remove the nth filter */ - async removeFilter(idx) { + async removeFilter(idx: number) { await this.page .getByRole('button', { name: 'Delete filter' }) .nth(idx) .click(); } - async _fillTransactionFields(transactionRow, transaction) { + async _fillTransactionFields( + transactionRow: Locator, + transaction: TransactionEntry, + ) { if (transaction.debit) { - await transactionRow.getByTestId('debit').click(); + // double click to ensure the content is selected when adding split transactions + await transactionRow.getByTestId('debit').dblclick(); await this.page.keyboard.type(transaction.debit); await this.page.keyboard.press('Tab'); } @@ -210,8 +238,11 @@ export class AccountPage { } class FilterTooltip { - constructor(page) { - this.page = page; - this.applyButton = page.getByRole('button', { name: 'Apply' }); + readonly locator: Locator; + readonly applyButton: Locator; + + constructor(locator: Locator) { + this.locator = locator; + this.applyButton = locator.getByRole('button', { name: 'Apply' }); } } diff --git a/packages/desktop-client/e2e/page-models/budget-page.js b/packages/desktop-client/e2e/page-models/budget-page.js deleted file mode 100644 index 86b168a2e1d..00000000000 --- a/packages/desktop-client/e2e/page-models/budget-page.js +++ /dev/null @@ -1,89 +0,0 @@ -import { AccountPage } from './account-page'; - -export class BudgetPage { - constructor(page) { - this.page = page; - - this.budgetSummary = page.getByTestId('budget-summary'); - this.budgetTable = page.getByTestId('budget-table'); - this.budgetTableTotals = this.budgetTable.getByTestId('budget-totals'); - } - - async getTableTotals() { - return { - budgeted: parseInt( - await this.budgetTableTotals - .getByTestId(/total-budgeted$/) - .textContent(), - 10, - ), - spent: parseInt( - await this.budgetTableTotals.getByTestId(/total-spent$/).textContent(), - 10, - ), - balance: parseInt( - await this.budgetTableTotals - .getByTestId(/total-leftover$/) - .textContent(), - 10, - ), - }; - } - - async showMoreMonths() { - await this.page.getByTestId('calendar-icon').first().click(); - } - - async getBalanceForRow(idx) { - return Math.round( - parseFloat( - ( - await this.budgetTable - .getByTestId('row') - .nth(idx) - .getByTestId('balance') - .textContent() - ).replace(/,/g, ''), - ) * 100, - ); - } - - async getCategoryNameForRow(idx) { - return this.budgetTable - .getByTestId('row') - .nth(idx) - .getByTestId('category-name') - .textContent(); - } - - async clickOnSpentAmountForRow(idx) { - await this.budgetTable - .getByTestId('row') - .nth(idx) - .getByTestId('category-month-spent') - .click(); - return new AccountPage(this.page); - } - - async transferAllBalance(fromIdx, toIdx) { - const toName = await this.getCategoryNameForRow(toIdx); - - await this.budgetTable - .getByTestId('row') - .nth(fromIdx) - .getByTestId('balance') - .getByTestId(/^budget/) - .click(); - - await this.page - .getByRole('button', { name: 'Transfer to another category' }) - .click(); - - await this.page.getByPlaceholder('(none)').click(); - - await this.page.keyboard.type(toName); - await this.page.keyboard.press('Enter'); - - await this.page.getByRole('button', { name: 'Transfer' }).click(); - } -} diff --git a/packages/desktop-client/e2e/page-models/budget-page.ts b/packages/desktop-client/e2e/page-models/budget-page.ts new file mode 100644 index 00000000000..99f90242950 --- /dev/null +++ b/packages/desktop-client/e2e/page-models/budget-page.ts @@ -0,0 +1,128 @@ +import { type Locator, type Page } from '@playwright/test'; + +import { AccountPage } from './account-page'; + +export class BudgetPage { + readonly page: Page; + readonly budgetSummary: Locator; + readonly budgetTable: Locator; + readonly budgetTableTotals: Locator; + + constructor(page: Page) { + this.page = page; + + this.budgetSummary = page.getByTestId('budget-summary'); + this.budgetTable = page.getByTestId('budget-table'); + this.budgetTableTotals = this.budgetTable.getByTestId('budget-totals'); + } + + async getTotalBudgeted() { + const totalBudgetedText = await this.budgetTableTotals + .getByTestId(/total-budgeted$/) + .textContent(); + + if (!totalBudgetedText) { + throw new Error('Failed to get total budgeted.'); + } + + return parseInt(totalBudgetedText, 10); + } + + async getTotalSpent() { + const totalSpentText = await this.budgetTableTotals + .getByTestId(/total-spent$/) + .textContent(); + + if (!totalSpentText) { + throw new Error('Failed to get total spent.'); + } + + return parseInt(totalSpentText, 10); + } + + async getTotalLeftover() { + const totalLeftoverText = await this.budgetTableTotals + .getByTestId(/total-leftover$/) + .textContent(); + + if (!totalLeftoverText) { + throw new Error('Failed to get total leftover.'); + } + + return parseInt(totalLeftoverText, 10); + } + + async getTableTotals() { + return { + budgeted: await this.getTotalBudgeted(), + spent: await this.getTotalSpent(), + balance: await this.getTotalLeftover(), + }; + } + + async showMoreMonths() { + await this.page.getByTestId('calendar-icon').first().click(); + } + + async getBalanceForRow(idx: number) { + const balanceText = await this.budgetTable + .getByTestId('row') + .nth(idx) + .getByTestId('balance') + .textContent(); + + if (!balanceText) { + throw new Error(`Failed to get balance on row index ${idx}.`); + } + + return Math.round(parseFloat(balanceText.replace(/,/g, '')) * 100); + } + + async getCategoryNameForRow(idx: number) { + const categoryNameText = this.budgetTable + .getByTestId('row') + .nth(idx) + .getByTestId('category-name') + .textContent(); + + if (!categoryNameText) { + throw new Error(`Failed to get category name on row index ${idx}.`); + } + + return categoryNameText; + } + + async clickOnSpentAmountForRow(idx: number) { + await this.budgetTable + .getByTestId('row') + .nth(idx) + .getByTestId('category-month-spent') + .click(); + return new AccountPage(this.page); + } + + async transferAllBalance(fromIdx: number, toIdx: number) { + const toName = await this.getCategoryNameForRow(toIdx); + if (!toName) { + throw new Error(`Unable to get category name of row index ${toIdx}.`); + } + + await this.budgetTable + .getByTestId('row') + .nth(fromIdx) + .getByTestId('balance') + .getByTestId(/^budget/) + .click(); + + await this.page + .getByRole('button', { name: 'Transfer to another category' }) + .click(); + + await this.page.getByPlaceholder('(none)').click(); + + await this.page.keyboard.type(toName); + await this.page.keyboard.press('Enter'); + + await this.page.getByRole('button', { name: 'Transfer' }).click(); + } +} diff --git a/packages/desktop-client/e2e/page-models/close-account-modal.js b/packages/desktop-client/e2e/page-models/close-account-modal.ts similarity index 56% rename from packages/desktop-client/e2e/page-models/close-account-modal.js rename to packages/desktop-client/e2e/page-models/close-account-modal.ts index dc975a1d71f..36b2a062e32 100644 --- a/packages/desktop-client/e2e/page-models/close-account-modal.js +++ b/packages/desktop-client/e2e/page-models/close-account-modal.ts @@ -1,10 +1,15 @@ +import { type Locator, type Page } from '@playwright/test'; + export class CloseAccountModal { - constructor(page, locator) { - this.page = page; + readonly locator: Locator; + readonly page: Page; + + constructor(locator: Locator) { this.locator = locator; + this.page = locator.page(); } - async selectTransferAccount(accountName) { + async selectTransferAccount(accountName: string) { await this.locator.getByPlaceholder('Select account...').fill(accountName); await this.page.keyboard.press('Enter'); } diff --git a/packages/desktop-client/e2e/page-models/configuration-page.js b/packages/desktop-client/e2e/page-models/configuration-page.ts similarity index 89% rename from packages/desktop-client/e2e/page-models/configuration-page.js rename to packages/desktop-client/e2e/page-models/configuration-page.ts index 903ec5dfdd9..daa2c9efbac 100644 --- a/packages/desktop-client/e2e/page-models/configuration-page.js +++ b/packages/desktop-client/e2e/page-models/configuration-page.ts @@ -1,8 +1,13 @@ +import { type Locator, type Page } from '@playwright/test'; + import { AccountPage } from './account-page'; import { BudgetPage } from './budget-page'; export class ConfigurationPage { - constructor(page) { + readonly page: Page; + readonly heading: Locator; + + constructor(page: Page) { this.page = page; this.heading = page.getByRole('heading'); @@ -23,7 +28,7 @@ export class ConfigurationPage { return new AccountPage(this.page); } - async importBudget(type, file) { + async importBudget(type: 'YNAB4' | 'nYNAB' | 'Actual', file: string) { const fileChooserPromise = this.page.waitForEvent('filechooser'); await this.page.getByRole('button', { name: 'Import my budget' }).click(); diff --git a/packages/desktop-client/e2e/page-models/custom-report-page.js b/packages/desktop-client/e2e/page-models/custom-report-page.ts similarity index 70% rename from packages/desktop-client/e2e/page-models/custom-report-page.js rename to packages/desktop-client/e2e/page-models/custom-report-page.ts index 493160b5590..2de241afeb3 100644 --- a/packages/desktop-client/e2e/page-models/custom-report-page.js +++ b/packages/desktop-client/e2e/page-models/custom-report-page.ts @@ -1,5 +1,13 @@ +import { type Locator, type Page } from '@playwright/test'; + export class CustomReportPage { - constructor(page) { + readonly page: Page; + readonly pageContent: Locator; + readonly showLegendButton: Locator; + readonly showSummaryButton: Locator; + readonly showLabelsButton: Locator; + + constructor(page: Page) { this.page = page; this.pageContent = page.getByTestId('reports-page'); @@ -14,11 +22,11 @@ export class CustomReportPage { }); } - async selectViz(vizName) { + async selectViz(vizName: string | RegExp) { await this.pageContent.getByRole('button', { name: vizName }).click(); } - async selectMode(mode) { + async selectMode(mode: 'total' | 'time') { switch (mode) { case 'total': await this.pageContent.getByRole('button', { name: 'Total' }).click(); diff --git a/packages/desktop-client/e2e/page-models/mobile-account-page.js b/packages/desktop-client/e2e/page-models/mobile-account-page.ts similarity index 60% rename from packages/desktop-client/e2e/page-models/mobile-account-page.js rename to packages/desktop-client/e2e/page-models/mobile-account-page.ts index 009d3b556bd..4c7e312e776 100644 --- a/packages/desktop-client/e2e/page-models/mobile-account-page.js +++ b/packages/desktop-client/e2e/page-models/mobile-account-page.ts @@ -1,7 +1,18 @@ +import { type Locator, type Page } from '@playwright/test'; + import { MobileTransactionEntryPage } from './mobile-transaction-entry-page'; export class MobileAccountPage { - constructor(page) { + readonly page: Page; + readonly heading: Locator; + readonly balance: Locator; + readonly noTransactionsMessage: Locator; + readonly searchBox: Locator; + readonly transactionList: Locator; + readonly transactions: Locator; + readonly createTransactionButton: Locator; + + constructor(page: Page) { this.page = page; this.heading = page.getByRole('heading'); @@ -15,21 +26,25 @@ export class MobileAccountPage { }); } - async waitFor() { - await this.transactionList.waitFor(); + async waitFor(...options: Parameters) { + await this.transactionList.waitFor(...options); } /** * Retrieve the balance of the account as a number */ async getBalance() { - return parseInt(await this.balance.textContent(), 10); + const balanceText = await this.balance.textContent(); + if (!balanceText) { + throw new Error('Failed to get balance.'); + } + return parseInt(balanceText, 10); } /** * Search by the given term */ - async searchByText(term) { + async searchByText(term: string) { await this.searchBox.fill(term); } diff --git a/packages/desktop-client/e2e/page-models/mobile-accounts-page.js b/packages/desktop-client/e2e/page-models/mobile-accounts-page.ts similarity index 64% rename from packages/desktop-client/e2e/page-models/mobile-accounts-page.js rename to packages/desktop-client/e2e/page-models/mobile-accounts-page.ts index 365584e73e0..571244535fc 100644 --- a/packages/desktop-client/e2e/page-models/mobile-accounts-page.js +++ b/packages/desktop-client/e2e/page-models/mobile-accounts-page.ts @@ -1,21 +1,27 @@ +import { type Locator, type Page } from '@playwright/test'; + import { MobileAccountPage } from './mobile-account-page'; export class MobileAccountsPage { - constructor(page) { + readonly page: Page; + readonly accountList: Locator; + readonly accountListItems: Locator; + + constructor(page: Page) { this.page = page; this.accountList = this.page.getByLabel('Account list'); this.accountListItems = this.accountList.getByTestId('account-list-item'); } - async waitFor() { - await this.accountList.waitFor(); + async waitFor(...options: Parameters) { + await this.accountList.waitFor(...options); } /** * Get the name and balance of the nth account */ - async getNthAccount(idx) { + async getNthAccount(idx: number) { const accountRow = this.accountListItems.nth(idx); return { @@ -27,7 +33,7 @@ export class MobileAccountsPage { /** * Click on the n-th account to open it up */ - async openNthAccount(idx) { + async openNthAccount(idx: number) { await this.accountListItems.nth(idx).click(); return new MobileAccountPage(this.page); diff --git a/packages/desktop-client/e2e/page-models/mobile-balance-menu-modal.js b/packages/desktop-client/e2e/page-models/mobile-balance-menu-modal.ts similarity index 62% rename from packages/desktop-client/e2e/page-models/mobile-balance-menu-modal.js rename to packages/desktop-client/e2e/page-models/mobile-balance-menu-modal.ts index e802354013f..8c292271a4f 100644 --- a/packages/desktop-client/e2e/page-models/mobile-balance-menu-modal.js +++ b/packages/desktop-client/e2e/page-models/mobile-balance-menu-modal.ts @@ -1,7 +1,18 @@ +import { type Page, type Locator } from '@playwright/test'; + export class BalanceMenuModal { - constructor(page, locator) { - this.page = page; + readonly page: Page; + readonly locator: Locator; + readonly heading: Locator; + readonly balanceAmountInput: Locator; + readonly transferToAnotherCategoryButton: Locator; + readonly coverOverspendingButton: Locator; + readonly rolloverOverspendingButton: Locator; + readonly removeOverspendingRolloverButton: Locator; + + constructor(locator: Locator) { this.locator = locator; + this.page = locator.page(); this.heading = locator.getByRole('heading'); this.balanceAmountInput = locator.getByTestId('amount-input'); diff --git a/packages/desktop-client/e2e/page-models/mobile-budget-menu-modal.js b/packages/desktop-client/e2e/page-models/mobile-budget-menu-modal.ts similarity index 69% rename from packages/desktop-client/e2e/page-models/mobile-budget-menu-modal.js rename to packages/desktop-client/e2e/page-models/mobile-budget-menu-modal.ts index 774448c6968..5f39f117213 100644 --- a/packages/desktop-client/e2e/page-models/mobile-budget-menu-modal.js +++ b/packages/desktop-client/e2e/page-models/mobile-budget-menu-modal.ts @@ -1,7 +1,19 @@ +import { type Locator, type Page } from '@playwright/test'; + export class BudgetMenuModal { - constructor(page, locator) { - this.page = page; + readonly page: Page; + readonly locator: Locator; + readonly heading: Locator; + readonly budgetAmountInput: Locator; + readonly copyLastMonthBudgetButton: Locator; + readonly setTo3MonthAverageButton: Locator; + readonly setTo6MonthAverageButton: Locator; + readonly setToYearlyAverageButton: Locator; + readonly applyBudgetTemplateButton: Locator; + + constructor(locator: Locator) { this.locator = locator; + this.page = locator.page(); this.heading = locator.getByRole('heading'); this.budgetAmountInput = locator.getByTestId('amount-input'); @@ -26,8 +38,8 @@ export class BudgetMenuModal { await this.heading.getByRole('button', { name: 'Close' }).click(); } - async setBudgetAmount(newAmount) { - await this.budgetAmountInput.fill(String(newAmount)); + async setBudgetAmount(newAmount: string) { + await this.budgetAmountInput.fill(newAmount); await this.budgetAmountInput.blur(); await this.close(); } diff --git a/packages/desktop-client/e2e/page-models/mobile-budget-page.js b/packages/desktop-client/e2e/page-models/mobile-budget-page.ts similarity index 66% rename from packages/desktop-client/e2e/page-models/mobile-budget-page.js rename to packages/desktop-client/e2e/page-models/mobile-budget-page.ts index 3053557e144..daa7804b27d 100644 --- a/packages/desktop-client/e2e/page-models/mobile-budget-page.js +++ b/packages/desktop-client/e2e/page-models/mobile-budget-page.ts @@ -1,3 +1,5 @@ +import { type Locator, type Page } from '@playwright/test'; + import { MobileAccountPage } from './mobile-account-page'; import { BalanceMenuModal } from './mobile-balance-menu-modal'; import { BudgetMenuModal } from './mobile-budget-menu-modal'; @@ -6,24 +8,47 @@ import { EnvelopeBudgetSummaryModal } from './mobile-envelope-budget-summary-mod import { TrackingBudgetSummaryModal } from './mobile-tracking-budget-summary-modal'; export class MobileBudgetPage { - MONTH_HEADER_DATE_FORMAT = 'MMMM ‘yy'; - - constructor(page) { + readonly MONTH_HEADER_DATE_FORMAT = 'MMMM ‘yy'; + + readonly page: Page; + readonly heading: Locator; + readonly previousMonthButton: Locator; + readonly selectedBudgetMonthButton: Locator; + readonly nextMonthButton: Locator; + readonly budgetPageMenuButton: Locator; + readonly budgetTableHeader: Locator; + readonly toBudgetButton: Locator; + readonly overbudgetedButton: Locator; + readonly savedButton: Locator; + readonly projectedSavingsButton: Locator; + readonly overspentButton: Locator; + readonly budgetedHeaderButton: Locator; + readonly spentHeaderButton: Locator; + readonly budgetTable: Locator; + readonly categoryRows: Locator; + readonly categoryNames: Locator; + readonly categoryGroupRows: Locator; + readonly categoryGroupNames: Locator; + + constructor(page: Page) { this.page = page; - this.#initializePageHeaderLocators(page); - this.#initializeBudgetTableLocators(page); - } + // Page header locators - async determineBudgetType() { - return (await this.#getButtonForEnvelopeBudgetSummary({ - throwIfNotFound: false, - })) !== null - ? 'Envelope' - : 'Tracking'; - } + this.heading = page.getByRole('heading'); + this.previousMonthButton = this.heading.getByRole('button', { + name: 'Previous month', + }); + this.selectedBudgetMonthButton = this.heading.locator('button[data-month]'); + this.nextMonthButton = this.heading.getByRole('button', { + name: 'Next month', + }); + this.budgetPageMenuButton = page.getByRole('button', { + name: 'Budget page menu', + }); + + // Budget table locators - #initializeBudgetTableLocators(page) { this.budgetTableHeader = page.getByTestId('budget-table-header'); // Envelope budget summary buttons @@ -69,25 +94,21 @@ export class MobileBudgetPage { ); } - #initializePageHeaderLocators(page) { - this.heading = page.getByRole('heading'); - this.previousMonthButton = this.heading.getByRole('button', { - name: 'Previous month', - }); - this.selectedBudgetMonthButton = this.heading.locator('button[data-month]'); - this.nextMonthButton = this.heading.getByRole('button', { - name: 'Next month', - }); - this.budgetPageMenuButton = page.getByRole('button', { - name: 'Budget page menu', - }); + async determineBudgetType() { + return (await this.#getButtonForEnvelopeBudgetSummary({ + throwIfNotFound: false, + })) !== null + ? 'Envelope' + : 'Tracking'; } - async waitFor(options) { - await this.budgetTable.waitFor(options); + async waitFor(...options: Parameters) { + await this.budgetTable.waitFor(...options); } - async toggleVisibleColumns(maxAttempts = 3) { + async toggleVisibleColumns({ + maxAttempts = 3, + }: { maxAttempts?: number } = {}) { for (let i = 0; i < maxAttempts; i++) { if (await this.budgetedHeaderButton.isVisible()) { await this.budgetedHeaderButton.click(); @@ -100,55 +121,72 @@ export class MobileBudgetPage { await this.page.waitForTimeout(1000); } - throw new Error('Budgeted/Spent columns could not be located on the page'); + throw new Error('Budgeted/Spent columns could not be located on the page.'); } async getSelectedMonth() { - return await this.heading + const selectedMonth = await this.heading .locator('[data-month]') .getAttribute('data-month'); + + if (!selectedMonth) { + throw new Error('Failed to get the selected month.'); + } + + return selectedMonth; } async openBudgetPageMenu() { await this.budgetPageMenuButton.click(); } - async getCategoryGroupNameForRow(idx) { - return this.categoryGroupNames.nth(idx).textContent(); + async getCategoryGroupNameForRow(idx: number) { + const groupNameText = await this.categoryGroupNames.nth(idx).textContent(); + if (!groupNameText) { + throw new Error(`Failed to get category group name for row ${idx}.`); + } + return groupNameText; } - #getButtonForCategoryGroup(categoryGroupName) { + #getButtonForCategoryGroup(categoryGroupName: string | RegExp) { return this.categoryGroupRows.getByRole('button', { name: categoryGroupName, exact: true, }); } - async openCategoryGroupMenu(categoryGroupName) { + async openCategoryGroupMenu(categoryGroupName: string | RegExp) { const categoryGroupButton = await this.#getButtonForCategoryGroup(categoryGroupName); await categoryGroupButton.click(); } - async getCategoryNameForRow(idx) { - return this.categoryNames.nth(idx).textContent(); + async getCategoryNameForRow(idx: number) { + const categoryNameText = await this.categoryNames.nth(idx).textContent(); + if (!categoryNameText) { + throw new Error(`Failed to get category name for row ${idx}.`); + } + return categoryNameText; } - #getButtonForCategory(categoryName) { + #getButtonForCategory(categoryName: string | RegExp) { return this.categoryRows.getByRole('button', { name: categoryName, exact: true, }); } - async openCategoryMenu(categoryName) { + async openCategoryMenu(categoryName: string | RegExp) { const categoryButton = await this.#getButtonForCategory(categoryName); await categoryButton.click(); - return new CategoryMenuModal(this.page, this.page.getByRole('dialog')); + return new CategoryMenuModal(this.page.getByRole('dialog')); } - async #getButtonForCell(buttonType, categoryName) { + async #getButtonForCell( + buttonType: 'Budgeted' | 'Spent', + categoryName: string, + ) { const buttonSelector = buttonType === 'Budgeted' ? `Open budget menu for ${categoryName} category` @@ -168,43 +206,43 @@ export class MobileBudgetPage { } throw new Error( - `${buttonType} button for category ${categoryName} could not be located on the page`, + `${buttonType} button for category ${categoryName} could not be located on the page.`, ); } - async getButtonForBudgeted(categoryName) { + async getButtonForBudgeted(categoryName: string) { return await this.#getButtonForCell('Budgeted', categoryName); } - async getButtonForSpent(categoryName) { + async getButtonForSpent(categoryName: string) { return await this.#getButtonForCell('Spent', categoryName); } - async openBudgetMenu(categoryName) { + async openBudgetMenu(categoryName: string) { const budgetedButton = await this.getButtonForBudgeted(categoryName); await budgetedButton.click(); - return new BudgetMenuModal(this.page, this.page.getByRole('dialog')); + return new BudgetMenuModal(this.page.getByRole('dialog')); } - async openSpentPage(categoryName) { + async openSpentPage(categoryName: string) { const spentButton = await this.getButtonForSpent(categoryName); await spentButton.click(); return new MobileAccountPage(this.page); } - async openBalanceMenu(categoryName) { + async openBalanceMenu(categoryName: string) { const balanceButton = this.budgetTable.getByRole('button', { name: `Open balance menu for ${categoryName} category`, }); if (await balanceButton.isVisible()) { await balanceButton.click(); - return new BalanceMenuModal(this.page, this.page.getByRole('dialog')); + return new BalanceMenuModal(this.page.getByRole('dialog')); } else { throw new Error( - `Balance button for category ${categoryName} not found or not visible`, + `Balance button for category ${categoryName} not found or not visible.`, ); } } @@ -213,7 +251,11 @@ export class MobileBudgetPage { currentMonth, errorMessage, maxAttempts = 3, - } = {}) { + }: { + currentMonth: string; + errorMessage: string; + maxAttempts: number; + }) { for (let attempt = 0; attempt < maxAttempts; attempt++) { const newMonth = await this.getSelectedMonth(); if (newMonth !== currentMonth) { @@ -225,7 +267,7 @@ export class MobileBudgetPage { throw new Error(errorMessage); } - async goToPreviousMonth({ maxAttempts = 3 } = {}) { + async goToPreviousMonth({ maxAttempts = 3 }: { maxAttempts?: number } = {}) { const currentMonth = await this.getSelectedMonth(); await this.previousMonthButton.click(); @@ -234,7 +276,7 @@ export class MobileBudgetPage { currentMonth, maxAttempts, errorMessage: - 'Failed to navigate to the previous month after maximum attempts', + 'Failed to navigate to the previous month after maximum attempts.', }); } @@ -242,7 +284,7 @@ export class MobileBudgetPage { await this.selectedBudgetMonthButton.click(); } - async goToNextMonth({ maxAttempts = 3 } = {}) { + async goToNextMonth({ maxAttempts = 3 }: { maxAttempts?: number } = {}) { const currentMonth = await this.getSelectedMonth(); await this.nextMonthButton.click(); @@ -251,11 +293,13 @@ export class MobileBudgetPage { currentMonth, maxAttempts, errorMessage: - 'Failed to navigate to the next month after maximum attempts', + 'Failed to navigate to the next month after maximum attempts.', }); } - async #getButtonForEnvelopeBudgetSummary({ throwIfNotFound = true } = {}) { + async #getButtonForEnvelopeBudgetSummary({ + throwIfNotFound = true, + }: { throwIfNotFound?: boolean } = {}) { if (await this.toBudgetButton.isVisible()) { return this.toBudgetButton; } @@ -269,21 +313,23 @@ export class MobileBudgetPage { } throw new Error( - 'Neither “To Budget” nor “Overbudgeted” button could be located on the page', + 'Neither “To Budget” nor “Overbudgeted” button could be located on the page.', ); } async openEnvelopeBudgetSummary() { const budgetSummaryButton = await this.#getButtonForEnvelopeBudgetSummary(); + if (!budgetSummaryButton) { + throw new Error('Envelope budget summary button not found.'); + } await budgetSummaryButton.click(); - return new EnvelopeBudgetSummaryModal( - this.page, - this.page.getByRole('dialog'), - ); + return new EnvelopeBudgetSummaryModal(this.page.getByRole('dialog')); } - async #getButtonForTrackingBudgetSummary({ throwIfNotFound = true } = {}) { + async #getButtonForTrackingBudgetSummary({ + throwIfNotFound = true, + }: { throwIfNotFound?: boolean } = {}) { if (await this.savedButton.isVisible()) { return this.savedButton; } @@ -301,17 +347,17 @@ 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.', ); } async openTrackingBudgetSummary() { const budgetSummaryButton = await this.#getButtonForTrackingBudgetSummary(); + if (!budgetSummaryButton) { + throw new Error('Tracking budget summary button not found.'); + } await budgetSummaryButton.click(); - return new TrackingBudgetSummaryModal( - this.page, - this.page.getByRole('dialog'), - ); + return new TrackingBudgetSummaryModal(this.page.getByRole('dialog')); } } diff --git a/packages/desktop-client/e2e/page-models/mobile-category-menu-modal.js b/packages/desktop-client/e2e/page-models/mobile-category-menu-modal.ts similarity index 58% rename from packages/desktop-client/e2e/page-models/mobile-category-menu-modal.js rename to packages/desktop-client/e2e/page-models/mobile-category-menu-modal.ts index f02ae03b8db..53319613e50 100644 --- a/packages/desktop-client/e2e/page-models/mobile-category-menu-modal.js +++ b/packages/desktop-client/e2e/page-models/mobile-category-menu-modal.ts @@ -1,9 +1,17 @@ +import { type Locator, type Page } from '@playwright/test'; + import { EditNotesModal } from './mobile-edit-notes-modal'; export class CategoryMenuModal { - constructor(page, locator) { - this.page = page; + readonly page: Page; + readonly locator: Locator; + readonly heading: Locator; + readonly budgetAmountInput: Locator; + readonly editNotesButton: Locator; + + constructor(locator: Locator) { this.locator = locator; + this.page = locator.page(); this.heading = locator.getByRole('heading'); this.budgetAmountInput = locator.getByTestId('amount-input'); @@ -17,6 +25,6 @@ export class CategoryMenuModal { async editNotes() { await this.editNotesButton.click(); - return new EditNotesModal(this.page, this.page.getByRole('dialog')); + return new EditNotesModal(this.page.getByRole('dialog')); } } diff --git a/packages/desktop-client/e2e/page-models/mobile-edit-notes-modal.js b/packages/desktop-client/e2e/page-models/mobile-edit-notes-modal.ts similarity index 57% rename from packages/desktop-client/e2e/page-models/mobile-edit-notes-modal.js rename to packages/desktop-client/e2e/page-models/mobile-edit-notes-modal.ts index c9c717df83f..845c8dbfc3f 100644 --- a/packages/desktop-client/e2e/page-models/mobile-edit-notes-modal.js +++ b/packages/desktop-client/e2e/page-models/mobile-edit-notes-modal.ts @@ -1,7 +1,15 @@ +import { type Locator, type Page } from '@playwright/test'; + export class EditNotesModal { - constructor(page, locator) { - this.page = page; + readonly page: Page; + readonly locator: Locator; + readonly heading: Locator; + readonly textArea: Locator; + readonly saveNotesButton: Locator; + + constructor(locator: Locator) { this.locator = locator; + this.page = locator.page(); this.heading = locator.getByRole('heading'); this.textArea = locator.getByRole('textbox'); @@ -12,7 +20,7 @@ export class EditNotesModal { await this.heading.getByRole('button', { name: 'Close' }).click(); } - async updateNotes(notes) { + async updateNotes(notes: string) { await this.textArea.fill(notes); await this.saveNotesButton.click(); } diff --git a/packages/desktop-client/e2e/page-models/mobile-envelope-budget-summary-modal.js b/packages/desktop-client/e2e/page-models/mobile-envelope-budget-summary-modal.ts similarity index 51% rename from packages/desktop-client/e2e/page-models/mobile-envelope-budget-summary-modal.js rename to packages/desktop-client/e2e/page-models/mobile-envelope-budget-summary-modal.ts index b0615c04d60..2f6efed0ea0 100644 --- a/packages/desktop-client/e2e/page-models/mobile-envelope-budget-summary-modal.js +++ b/packages/desktop-client/e2e/page-models/mobile-envelope-budget-summary-modal.ts @@ -1,7 +1,13 @@ +import { type Locator, type Page } from '@playwright/test'; + export class EnvelopeBudgetSummaryModal { - constructor(page, locator) { - this.page = page; + readonly page: Page; + readonly locator: Locator; + readonly heading: Locator; + + constructor(locator: Locator) { this.locator = locator; + this.page = locator.page(); this.heading = locator.getByRole('heading'); } diff --git a/packages/desktop-client/e2e/page-models/mobile-navigation.js b/packages/desktop-client/e2e/page-models/mobile-navigation.ts similarity index 70% rename from packages/desktop-client/e2e/page-models/mobile-navigation.js rename to packages/desktop-client/e2e/page-models/mobile-navigation.ts index 258ec5a77d4..14fba95cffa 100644 --- a/packages/desktop-client/e2e/page-models/mobile-navigation.js +++ b/packages/desktop-client/e2e/page-models/mobile-navigation.ts @@ -1,3 +1,5 @@ +import { type Locator, type Page } from '@playwright/test'; + import { MobileAccountPage } from './mobile-account-page'; import { MobileAccountsPage } from './mobile-accounts-page'; import { MobileBudgetPage } from './mobile-budget-page'; @@ -5,8 +7,30 @@ import { MobileReportsPage } from './mobile-reports-page'; import { MobileTransactionEntryPage } from './mobile-transaction-entry-page'; import { SettingsPage } from './settings-page'; +const NAVBAR_ROWS = 3; +const NAV_LINKS_HIDDEN_BY_DEFAULT = [ + 'Reports', + 'Schedules', + 'Payees', + 'Rules', + 'Settings', +]; +const ROUTES_BY_PAGE = { + Budget: '/budget', + Accounts: '/accounts', + Transaction: '/transactions/new', + Reports: '/reports', + Settings: '/settings', +}; + export class MobileNavigation { - constructor(page) { + readonly page: Page; + readonly heading: Locator; + readonly navbar: Locator; + readonly mainContentSelector: string; + readonly navbarSelector: string; + + constructor(page: Page) { this.page = page; this.heading = page.getByRole('heading'); this.navbar = page.getByRole('navigation'); @@ -14,31 +38,23 @@ export class MobileNavigation { this.navbarSelector = '[role=navigation]'; } - static #NAVBAR_ROWS = 3; - static #NAV_LINKS_HIDDEN_BY_DEFAULT = [ - 'Reports', - 'Schedules', - 'Payees', - 'Rules', - 'Settings', - ]; - static #ROUTES_BY_PAGE = { - Budget: '/budget', - Accounts: '/accounts', - Transactions: '/transactions/new', - Reports: '/reports', - Settings: '/settings', - }; - async dragNavbarUp() { const mainContentBoundingBox = await this.page .locator(this.mainContentSelector) .boundingBox(); + if (!mainContentBoundingBox) { + throw new Error('Unable to get bounding box of main content.'); + } + const navbarBoundingBox = await this.page .locator(this.navbarSelector) .boundingBox(); + if (!navbarBoundingBox) { + throw new Error('Unable to get bounding box of navbar.'); + } + await this.page.dragAndDrop(this.navbarSelector, this.mainContentSelector, { sourcePosition: { x: 1, y: 0 }, targetPosition: { @@ -53,39 +69,47 @@ export class MobileNavigation { .locator(this.navbarSelector) .boundingBox(); + if (!boundingBox) { + throw new Error('Unable to get bounding box of navbar.'); + } + await this.page.dragAndDrop(this.navbarSelector, this.navbarSelector, { sourcePosition: { x: 1, y: 0 }, targetPosition: { x: 1, // Only scroll until bottom of screen i.e. bottom of first navbar row. - y: boundingBox.height / MobileNavigation.#NAVBAR_ROWS, + y: boundingBox.height / NAVBAR_ROWS, }, }); } - async hasNavbarState(...states) { + async hasNavbarState(...states: string[]) { if ((await this.navbar.count()) === 0) { // No navbar on page. return false; } const dataNavbarState = await this.navbar.getAttribute('data-navbar-state'); + if (!dataNavbarState) { + throw new Error('Navbar does not have data-navbar-state attribute.'); + } return states.includes(dataNavbarState); } - async navigateToPage(pageName, pageModelFactory) { + async navigateToPage( + pageName: keyof typeof ROUTES_BY_PAGE, + pageModelFactory: () => T, + ): Promise { const pageInstance = pageModelFactory(); - if (this.page.url().endsWith(MobileNavigation.#ROUTES_BY_PAGE[pageName])) { + if (this.page.url().endsWith(ROUTES_BY_PAGE[pageName])) { // Already on the page. return pageInstance; } await this.navbar.waitFor(); - const navbarStates = MobileNavigation.#NAV_LINKS_HIDDEN_BY_DEFAULT.includes( - pageName, - ) + const navbarStates = NAV_LINKS_HIDDEN_BY_DEFAULT.includes(pageName) ? ['default', 'hidden'] : ['hidden']; diff --git a/packages/desktop-client/e2e/page-models/mobile-reports-page.js b/packages/desktop-client/e2e/page-models/mobile-reports-page.js deleted file mode 100644 index 76e317ab3c0..00000000000 --- a/packages/desktop-client/e2e/page-models/mobile-reports-page.js +++ /dev/null @@ -1,11 +0,0 @@ -export class MobileReportsPage { - constructor(page) { - this.page = page; - - this.overview = page.getByTestId('reports-overview'); - } - - async waitFor(options) { - await this.overview.waitFor(options); - } -} diff --git a/packages/desktop-client/e2e/page-models/mobile-reports-page.ts b/packages/desktop-client/e2e/page-models/mobile-reports-page.ts new file mode 100644 index 00000000000..74b5684426c --- /dev/null +++ b/packages/desktop-client/e2e/page-models/mobile-reports-page.ts @@ -0,0 +1,16 @@ +import { type Locator, type Page } from '@playwright/test'; + +export class MobileReportsPage { + readonly page: Page; + readonly overview: Locator; + + constructor(page: Page) { + this.page = page; + + this.overview = page.getByTestId('reports-overview'); + } + + async waitFor(...options: Parameters) { + await this.overview.waitFor(...options); + } +} diff --git a/packages/desktop-client/e2e/page-models/mobile-tracking-budget-summary-modal.js b/packages/desktop-client/e2e/page-models/mobile-tracking-budget-summary-modal.ts similarity index 51% rename from packages/desktop-client/e2e/page-models/mobile-tracking-budget-summary-modal.js rename to packages/desktop-client/e2e/page-models/mobile-tracking-budget-summary-modal.ts index f6afab7b209..0a4de8bf281 100644 --- a/packages/desktop-client/e2e/page-models/mobile-tracking-budget-summary-modal.js +++ b/packages/desktop-client/e2e/page-models/mobile-tracking-budget-summary-modal.ts @@ -1,7 +1,13 @@ +import { type Locator, type Page } from '@playwright/test'; + export class TrackingBudgetSummaryModal { - constructor(page, locator) { - this.page = page; + readonly page: Page; + readonly locator: Locator; + readonly heading: Locator; + + constructor(locator: Locator) { this.locator = locator; + this.page = locator.page(); this.heading = locator.getByRole('heading'); } diff --git a/packages/desktop-client/e2e/page-models/mobile-transaction-entry-page.js b/packages/desktop-client/e2e/page-models/mobile-transaction-entry-page.ts similarity index 62% rename from packages/desktop-client/e2e/page-models/mobile-transaction-entry-page.js rename to packages/desktop-client/e2e/page-models/mobile-transaction-entry-page.ts index b6de03dd92a..8937fd0d187 100644 --- a/packages/desktop-client/e2e/page-models/mobile-transaction-entry-page.js +++ b/packages/desktop-client/e2e/page-models/mobile-transaction-entry-page.ts @@ -1,7 +1,16 @@ +import { type Locator, type Page } from '@playwright/test'; + import { MobileAccountPage } from './mobile-account-page'; export class MobileTransactionEntryPage { - constructor(page) { + readonly page: Page; + readonly header: Locator; + readonly amountField: Locator; + readonly transactionForm: Locator; + readonly footer: Locator; + readonly addTransactionButton: Locator; + + constructor(page: Page) { this.page = page; this.header = page.getByRole('heading'); this.transactionForm = page.getByTestId('transaction-form'); @@ -12,11 +21,11 @@ export class MobileTransactionEntryPage { }); } - async waitFor(options) { - await this.transactionForm.waitFor(options); + async waitFor(...options: Parameters) { + await this.transactionForm.waitFor(...options); } - async fillField(fieldLocator, content) { + async fillField(fieldLocator: Locator, content: string) { await fieldLocator.click(); await this.page.locator('css=[role=combobox] input').fill(content); await this.page.keyboard.press('Enter'); diff --git a/packages/desktop-client/e2e/page-models/navigation.js b/packages/desktop-client/e2e/page-models/navigation.ts similarity index 88% rename from packages/desktop-client/e2e/page-models/navigation.js rename to packages/desktop-client/e2e/page-models/navigation.ts index 8e468847cd0..f665a2e8b77 100644 --- a/packages/desktop-client/e2e/page-models/navigation.js +++ b/packages/desktop-client/e2e/page-models/navigation.ts @@ -1,15 +1,25 @@ +import { type Page } from '@playwright/test'; + import { AccountPage } from './account-page'; import { ReportsPage } from './reports-page'; import { RulesPage } from './rules-page'; import { SchedulesPage } from './schedules-page'; import { SettingsPage } from './settings-page'; +type AccountEntry = { + name: string; + balance: number; + offBudget: boolean; +}; + export class Navigation { - constructor(page) { + readonly page: Page; + + constructor(page: Page) { this.page = page; } - async goToAccountPage(accountName) { + async goToAccountPage(accountName: string) { await this.page .getByRole('link', { name: new RegExp(`^${accountName}`) }) .click(); @@ -55,7 +65,7 @@ export class Navigation { return new SettingsPage(this.page); } - async createAccount(data) { + async createAccount(data: AccountEntry) { await this.page.getByRole('button', { name: 'Add account' }).click(); await this.page .getByRole('button', { name: 'Create a local account' }) diff --git a/packages/desktop-client/e2e/page-models/reports-page.js b/packages/desktop-client/e2e/page-models/reports-page.ts similarity index 87% rename from packages/desktop-client/e2e/page-models/reports-page.js rename to packages/desktop-client/e2e/page-models/reports-page.ts index c5c070f3cb8..a499c8692cd 100644 --- a/packages/desktop-client/e2e/page-models/reports-page.js +++ b/packages/desktop-client/e2e/page-models/reports-page.ts @@ -1,7 +1,12 @@ +import { type Locator, type Page } from '@playwright/test'; + import { CustomReportPage } from './custom-report-page'; export class ReportsPage { - constructor(page) { + readonly page: Page; + readonly pageContent: Locator; + + constructor(page: Page) { this.page = page; this.pageContent = page.getByTestId('reports-page'); } diff --git a/packages/desktop-client/e2e/page-models/rules-page.js b/packages/desktop-client/e2e/page-models/rules-page.ts similarity index 74% rename from packages/desktop-client/e2e/page-models/rules-page.js rename to packages/desktop-client/e2e/page-models/rules-page.ts index 28f0c06fb42..be2d359c306 100644 --- a/packages/desktop-client/e2e/page-models/rules-page.js +++ b/packages/desktop-client/e2e/page-models/rules-page.ts @@ -1,5 +1,35 @@ +import { type Locator, type Page } from '@playwright/test'; + +type ConditionsEntry = { + field: string; + op: string; + value: string; +}; + +type ActionsEntry = { + field: string; + op?: string; + value: string; +}; + +type SplitsEntry = { + field: string; + op?: string; + value?: string; +}; + +type RuleEntry = { + conditionsOp?: string | RegExp; + conditions?: ConditionsEntry[]; + actions?: ActionsEntry[]; + splits?: Array; +}; + export class RulesPage { - constructor(page) { + readonly page: Page; + readonly searchBox: Locator; + + constructor(page: Page) { this.page = page; this.searchBox = page.getByPlaceholder('Filter rules...'); } @@ -7,7 +37,7 @@ export class RulesPage { /** * Create a new rule */ - async createRule(data) { + async createRule(data: RuleEntry) { await this.page .getByRole('button', { name: 'Create new rule', @@ -23,7 +53,7 @@ export class RulesPage { * Retrieve the data for the nth-rule. * 0-based index */ - getNthRule(index) { + getNthRule(index: number) { const row = this.page.getByTestId('table').getByTestId('row').nth(index); return { @@ -32,11 +62,11 @@ export class RulesPage { }; } - async searchFor(text) { + async searchFor(text: string) { await this.searchBox.fill(text); } - async _fillRuleFields(data) { + async _fillRuleFields(data: RuleEntry) { if (data.conditionsOp) { await this.page .getByTestId('conditions-op') @@ -76,9 +106,13 @@ export class RulesPage { } } - async _fillEditorFields(data, rootElement, fieldFirst = false) { - for (const idx in data) { - const { field, op, value } = data[idx]; + async _fillEditorFields( + data: Array, + rootElement: Locator, + fieldFirst = false, + ) { + for (const [idx, entry] of data.entries()) { + const { field, op, value } = entry; const row = rootElement.getByTestId('editor-row').nth(idx); diff --git a/packages/desktop-client/e2e/page-models/schedules-page.js b/packages/desktop-client/e2e/page-models/schedules-page.ts similarity index 78% rename from packages/desktop-client/e2e/page-models/schedules-page.js rename to packages/desktop-client/e2e/page-models/schedules-page.ts index 3166b96509a..81ce61c09e5 100644 --- a/packages/desktop-client/e2e/page-models/schedules-page.js +++ b/packages/desktop-client/e2e/page-models/schedules-page.ts @@ -1,5 +1,17 @@ +import { type Locator, type Page } from '@playwright/test'; + +type ScheduleEntry = { + payee?: string; + account?: string; + amount?: number; +}; + export class SchedulesPage { - constructor(page) { + readonly page: Page; + readonly addNewScheduleButton: Locator; + readonly schedulesTableRow: Locator; + + constructor(page: Page) { this.page = page; this.addNewScheduleButton = this.page.getByRole('button', { @@ -11,7 +23,7 @@ export class SchedulesPage { /** * Add a new schedule */ - async addNewSchedule(data) { + async addNewSchedule(data: ScheduleEntry) { await this.addNewScheduleButton.click(); await this._fillScheduleFields(data); @@ -23,7 +35,7 @@ export class SchedulesPage { * Retrieve the row element for the nth-schedule. * 0-based index */ - getNthScheduleRow(index) { + getNthScheduleRow(index: number) { return this.schedulesTableRow.nth(index); } @@ -31,7 +43,7 @@ export class SchedulesPage { * Retrieve the data for the nth-schedule. * 0-based index */ - getNthSchedule(index) { + getNthSchedule(index: number) { const row = this.getNthScheduleRow(index); return { @@ -47,7 +59,7 @@ export class SchedulesPage { * Create a transaction for the nth-schedule. * 0-based index */ - async postNthSchedule(index) { + async postNthSchedule(index: number) { await this._performNthAction(index, 'Post transaction today'); await this.page.waitForTimeout(1000); } @@ -56,12 +68,12 @@ export class SchedulesPage { * Complete the nth-schedule. * 0-based index */ - async completeNthSchedule(index) { + async completeNthSchedule(index: number) { await this._performNthAction(index, 'Complete'); await this.page.waitForTimeout(1000); } - async _performNthAction(index, actionName) { + async _performNthAction(index: number, actionName: string | RegExp) { const row = this.getNthScheduleRow(index); const actions = row.getByTestId('actions'); @@ -69,7 +81,7 @@ export class SchedulesPage { await this.page.getByRole('button', { name: actionName }).click(); } - async _fillScheduleFields(data) { + async _fillScheduleFields(data: ScheduleEntry) { if (data.payee) { await this.page.getByRole('textbox', { name: 'Payee' }).fill(data.payee); await this.page.keyboard.press('Enter'); diff --git a/packages/desktop-client/e2e/page-models/settings-page.js b/packages/desktop-client/e2e/page-models/settings-page.ts similarity index 66% rename from packages/desktop-client/e2e/page-models/settings-page.js rename to packages/desktop-client/e2e/page-models/settings-page.ts index 96045c30043..42c1f329191 100644 --- a/packages/desktop-client/e2e/page-models/settings-page.js +++ b/packages/desktop-client/e2e/page-models/settings-page.ts @@ -1,5 +1,14 @@ +import { type Locator, type Page } from '@playwright/test'; + export class SettingsPage { - constructor(page) { + readonly page: Page; + readonly settings: Locator; + readonly exportDataButton: Locator; + readonly switchBudgetTypeButton: Locator; + readonly advancedSettingsButton: Locator; + readonly experimentalSettingsButton: Locator; + + constructor(page: Page) { this.page = page; this.settings = page.getByTestId('settings'); this.exportDataButton = this.settings.getByRole('button', { @@ -15,24 +24,24 @@ export class SettingsPage { ); } - async waitFor(options) { - await this.settings.waitFor(options); + async waitFor(...options: Parameters) { + await this.settings.waitFor(...options); } async exportData() { await this.exportDataButton.click(); } - async useBudgetType(budgetType) { + async useBudgetType(budgetType: 'Envelope' | 'Tracking') { await this.switchBudgetTypeButton.waitFor(); const buttonText = await this.switchBudgetTypeButton.textContent(); - if (buttonText.includes(budgetType.toLowerCase())) { + if (buttonText?.includes(budgetType.toLowerCase())) { await this.switchBudgetTypeButton.click(); } } - async enableExperimentalFeature(featureName) { + async enableExperimentalFeature(featureName: string) { if (await this.advancedSettingsButton.isVisible()) { await this.advancedSettingsButton.click(); } diff --git a/packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-loads-cash-flow-graph-and-checks-visuals-1-chromium-linux.png b/packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-loads-cash-flow-graph-and-checks-visuals-1-chromium-linux.png index 85a97c2032f..85f9c02fe53 100644 Binary files a/packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-loads-cash-flow-graph-and-checks-visuals-1-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-loads-cash-flow-graph-and-checks-visuals-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.mobile.test.ts-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-7-chromium-linux.png b/packages/desktop-client/e2e/transactions.mobile.test.ts-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-7-chromium-linux.png index 5d6a84f5a7a..8d885268885 100644 Binary files a/packages/desktop-client/e2e/transactions.mobile.test.ts-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-7-chromium-linux.png and b/packages/desktop-client/e2e/transactions.mobile.test.ts-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-7-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.mobile.test.ts-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-8-chromium-linux.png b/packages/desktop-client/e2e/transactions.mobile.test.ts-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-8-chromium-linux.png index c8ed30e1d63..c56186c0171 100644 Binary files a/packages/desktop-client/e2e/transactions.mobile.test.ts-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-8-chromium-linux.png and b/packages/desktop-client/e2e/transactions.mobile.test.ts-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-8-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.mobile.test.ts-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-9-chromium-linux.png b/packages/desktop-client/e2e/transactions.mobile.test.ts-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-9-chromium-linux.png index d10b3d513df..5ea220ed44a 100644 Binary files a/packages/desktop-client/e2e/transactions.mobile.test.ts-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-9-chromium-linux.png and b/packages/desktop-client/e2e/transactions.mobile.test.ts-snapshots/Mobile-Transactions-creates-a-transaction-via-footer-button-9-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.ts b/packages/desktop-client/e2e/transactions.test.ts index 1b444b707d5..375605324ef 100644 --- a/packages/desktop-client/e2e/transactions.test.ts +++ b/packages/desktop-client/e2e/transactions.test.ts @@ -40,7 +40,7 @@ test.describe('Transactions', () => { test('by date', async () => { const filterTooltip = await accountPage.filterBy('Date'); - await expect(filterTooltip.page).toMatchThemeScreenshots(); + await expect(filterTooltip.locator).toMatchThemeScreenshots(); // Open datepicker await page.keyboard.press('Space'); @@ -58,7 +58,7 @@ test.describe('Transactions', () => { test('by category', async () => { const filterTooltip = await accountPage.filterBy('Category'); - await expect(filterTooltip.page).toMatchThemeScreenshots(); + await expect(filterTooltip.locator).toMatchThemeScreenshots(); // Type in the autocomplete box const autocomplete = page.getByTestId('autocomplete'); diff --git a/packages/desktop-client/src/auth/ProtectedRoute.tsx b/packages/desktop-client/src/auth/ProtectedRoute.tsx index 0dad71a1c38..202980b9b21 100644 --- a/packages/desktop-client/src/auth/ProtectedRoute.tsx +++ b/packages/desktop-client/src/auth/ProtectedRoute.tsx @@ -1,8 +1,9 @@ import { useEffect, useState, type ReactElement } from 'react'; +import { View } from '@actual-app/components/view'; + import { type RemoteFile, type SyncedLocalFile } from 'loot-core/types/file'; -import { View } from '../components/common/View'; import { useMetadataPref } from '../hooks/useMetadataPref'; import { useSelector } from '../redux'; diff --git a/packages/desktop-client/src/components/AnimatedRefresh.tsx b/packages/desktop-client/src/components/AnimatedRefresh.tsx index b9273bf9556..d794c34f704 100644 --- a/packages/desktop-client/src/components/AnimatedRefresh.tsx +++ b/packages/desktop-client/src/components/AnimatedRefresh.tsx @@ -1,12 +1,11 @@ // @ts-strict-ignore import React, { type CSSProperties } from 'react'; +import { View } from '@actual-app/components/view'; import { keyframes } from '@emotion/css'; import { SvgRefresh } from '../icons/v1'; -import { View } from './common/View'; - const spin = keyframes({ '0%': { transform: 'rotateZ(0deg)' }, '100%': { transform: 'rotateZ(360deg)' }, diff --git a/packages/desktop-client/src/components/App.tsx b/packages/desktop-client/src/components/App.tsx index 6e39c77d9b4..5b7033f9788 100644 --- a/packages/desktop-client/src/components/App.tsx +++ b/packages/desktop-client/src/components/App.tsx @@ -11,6 +11,9 @@ import { HotkeysProvider } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { BrowserRouter } from 'react-router-dom'; +import { styles } from '@actual-app/components/styles'; +import { View } from '@actual-app/components/view'; + import { addNotification, closeBudget, @@ -28,12 +31,11 @@ import { useMetadataPref } from '../hooks/useMetadataPref'; import { setI18NextLanguage } from '../i18n'; import { installPolyfills } from '../polyfills'; import { useDispatch, useSelector, useStore } from '../redux'; -import { styles, hasHiddenScrollbars, ThemeStyle, useTheme } from '../style'; +import { hasHiddenScrollbars, ThemeStyle, useTheme } from '../style'; import { ExposeNavigate } from '../util/router-tools'; import { AppBackground } from './AppBackground'; import { BudgetMonthCountProvider } from './budget/BudgetMonthCountContext'; -import { View } from './common/View'; import { DevelopmentTopBar } from './DevelopmentTopBar'; import { FatalError } from './FatalError'; import { FinancesApp } from './FinancesApp'; diff --git a/packages/desktop-client/src/components/BankSyncStatus.tsx b/packages/desktop-client/src/components/BankSyncStatus.tsx index 843b62d3e3f..554a37c2748 100644 --- a/packages/desktop-client/src/components/BankSyncStatus.tsx +++ b/packages/desktop-client/src/components/BankSyncStatus.tsx @@ -2,12 +2,14 @@ import React from 'react'; import { Trans } from 'react-i18next'; import { useTransition, animated } from 'react-spring'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { useSelector } from '../redux'; -import { theme, styles } from '../style'; +import { theme } from '../style'; import { AnimatedRefresh } from './AnimatedRefresh'; -import { Text } from './common/Text'; -import { View } from './common/View'; export function BankSyncStatus() { const accountsSyncing = useSelector(state => state.account.accountsSyncing); diff --git a/packages/desktop-client/src/components/DevelopmentTopBar.tsx b/packages/desktop-client/src/components/DevelopmentTopBar.tsx index c92375a402d..9fd121551e2 100644 --- a/packages/desktop-client/src/components/DevelopmentTopBar.tsx +++ b/packages/desktop-client/src/components/DevelopmentTopBar.tsx @@ -1,7 +1,8 @@ +import { View } from '@actual-app/components/view'; + import { theme } from '../style'; import { Link } from './common/Link'; -import { View } from './common/View'; export function DevelopmentTopBar() { return ( diff --git a/packages/desktop-client/src/components/EditablePageHeaderTitle.tsx b/packages/desktop-client/src/components/EditablePageHeaderTitle.tsx index 0552bb4bb33..2e883ddd89e 100644 --- a/packages/desktop-client/src/components/EditablePageHeaderTitle.tsx +++ b/packages/desktop-client/src/components/EditablePageHeaderTitle.tsx @@ -1,12 +1,13 @@ import React, { useCallback, useEffect, useState } from 'react'; +import { Button } from '@actual-app/components/button'; +import { InitialFocus } from '@actual-app/components/initial-focus'; +import { View } from '@actual-app/components/view'; + import { SvgPencil1 } from '../icons/v2'; import { theme } from '../style'; -import { Button } from './common/Button2'; -import { InitialFocus } from './common/InitialFocus'; import { Input } from './common/Input'; -import { View } from './common/View'; type EditablePageHeaderTitleProps = { title: string; diff --git a/packages/desktop-client/src/components/FinancesApp.tsx b/packages/desktop-client/src/components/FinancesApp.tsx index 5245becb5df..156cc95cf02 100644 --- a/packages/desktop-client/src/components/FinancesApp.tsx +++ b/packages/desktop-client/src/components/FinancesApp.tsx @@ -9,6 +9,8 @@ import { useHref, } from 'react-router-dom'; +import { View } from '@actual-app/components/view'; + import { addNotification } from 'loot-core/client/actions'; import { sync } from 'loot-core/client/app/appSlice'; import * as undo from 'loot-core/platform/client/undo'; @@ -24,8 +26,8 @@ import { theme } from '../style'; import { getIsOutdated, getLatestVersion } from '../util/versions'; import { UserAccessPage } from './admin/UserAccess/UserAccessPage'; +import { BankSync } from './banksync'; import { BankSyncStatus } from './BankSyncStatus'; -import { View } from './common/View'; import { GlobalKeys } from './GlobalKeys'; import { ManageRulesPage } from './ManageRulesPage'; import { Category } from './mobile/budget/Category'; @@ -248,6 +250,7 @@ export function FinancesApp() { } /> } /> + } /> } /> index; diff --git a/packages/desktop-client/src/components/HelpMenu.tsx b/packages/desktop-client/src/components/HelpMenu.tsx index 40a4208ed41..8b28ccafac9 100644 --- a/packages/desktop-client/src/components/HelpMenu.tsx +++ b/packages/desktop-client/src/components/HelpMenu.tsx @@ -3,6 +3,10 @@ import { useHotkeys } from 'react-hotkeys-hook'; import { Trans, useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; +import { Button } from '@actual-app/components/button'; +import { Menu } from '@actual-app/components/menu'; +import { Popover } from '@actual-app/components/popover'; +import { SpaceBetween } from '@actual-app/components/space-between'; import { useToggle } from 'usehooks-ts'; import { pushModal } from 'loot-core/client/actions/modals'; @@ -11,11 +15,6 @@ import { useFeatureFlag } from '../hooks/useFeatureFlag'; import { SvgHelp } from '../icons/v2/Help'; import { useDispatch } from '../redux'; -import { Button } from './common/Button2'; -import { Menu } from './common/Menu'; -import { Popover } from './common/Popover'; -import { SpaceBetween } from './common/SpaceBetween'; - const getPageDocs = (page: string) => { switch (page) { case '/budget': diff --git a/packages/desktop-client/src/components/LoggedInUser.tsx b/packages/desktop-client/src/components/LoggedInUser.tsx index 0f4b6f7fc36..8998642e8dc 100644 --- a/packages/desktop-client/src/components/LoggedInUser.tsx +++ b/packages/desktop-client/src/components/LoggedInUser.tsx @@ -2,6 +2,13 @@ import React, { useState, useEffect, useRef, type CSSProperties } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; +import { Button } from '@actual-app/components/button'; +import { Menu } from '@actual-app/components/menu'; +import { Popover } from '@actual-app/components/popover'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { closeBudget, getUserData, signOut } from 'loot-core/client/actions'; import { listen } from 'loot-core/platform/client/fetch'; import { type RemoteFile, type SyncedLocalFile } from 'loot-core/types/file'; @@ -12,13 +19,8 @@ import { Permissions } from '../auth/types'; import { useMetadataPref } from '../hooks/useMetadataPref'; import { useNavigate } from '../hooks/useNavigate'; import { useSelector, useDispatch } from '../redux'; -import { theme, styles } from '../style'; +import { theme } from '../style'; -import { Button } from './common/Button2'; -import { Menu } from './common/Menu'; -import { Popover } from './common/Popover'; -import { Text } from './common/Text'; -import { View } from './common/View'; import { PrivacyFilter } from './PrivacyFilter'; import { useMultiuserEnabled, useServerURL } from './ServerContext'; diff --git a/packages/desktop-client/src/components/ManageRules.tsx b/packages/desktop-client/src/components/ManageRules.tsx index 5963995aab1..7e8e9aeabeb 100644 --- a/packages/desktop-client/src/components/ManageRules.tsx +++ b/packages/desktop-client/src/components/ManageRules.tsx @@ -9,6 +9,11 @@ import React, { } from 'react'; import { useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Stack } from '@actual-app/components/stack'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { pushModal } from 'loot-core/client/actions/modals'; import { useSchedules } from 'loot-core/client/data-hooks/schedules'; import { initiallyLoadPayees } from 'loot-core/client/queries/queriesSlice'; @@ -27,13 +32,9 @@ import { useSelected, SelectedProvider } from '../hooks/useSelected'; import { useDispatch } from '../redux'; import { theme } from '../style'; -import { Button } from './common/Button2'; import { Link } from './common/Link'; import { Search } from './common/Search'; import { SimpleTable } from './common/SimpleTable'; -import { Stack } from './common/Stack'; -import { Text } from './common/Text'; -import { View } from './common/View'; import { RulesHeader } from './rules/RulesHeader'; import { RulesList } from './rules/RulesList'; diff --git a/packages/desktop-client/src/components/Modals.tsx b/packages/desktop-client/src/components/Modals.tsx index b3ccc043a13..30055290261 100644 --- a/packages/desktop-client/src/components/Modals.tsx +++ b/packages/desktop-client/src/components/Modals.tsx @@ -11,6 +11,7 @@ import { useMetadataPref } from '../hooks/useMetadataPref'; import { useModalState } from '../hooks/useModalState'; import { useDispatch } from '../redux'; +import { EditSyncAccount } from './banksync/EditSyncAccount'; import { ModalTitle, ModalHeader } from './common/Modal'; import { AccountAutocompleteModal } from './modals/AccountAutocompleteModal'; import { AccountMenuModal } from './modals/AccountMenuModal'; @@ -384,6 +385,9 @@ export function Modals() { case 'schedule-posts-offline-notification': return ; + case 'synced-account-edit': + return ; + case 'account-menu': return ( - No transactions + No transactions ) : null } diff --git a/packages/desktop-client/src/components/accounts/AccountSyncCheck.tsx b/packages/desktop-client/src/components/accounts/AccountSyncCheck.tsx index f91425bbaec..cf1143ccd2b 100644 --- a/packages/desktop-client/src/components/accounts/AccountSyncCheck.tsx +++ b/packages/desktop-client/src/components/accounts/AccountSyncCheck.tsx @@ -2,6 +2,10 @@ import React, { useCallback, useRef, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; +import { Button } from '@actual-app/components/button'; +import { Popover } from '@actual-app/components/popover'; +import { View } from '@actual-app/components/view'; + import { unlinkAccount } from 'loot-core/client/accounts/accountsSlice'; import { type AccountEntity } from 'loot-core/types/models'; @@ -11,10 +15,7 @@ import { useFailedAccounts } from '../../hooks/useFailedAccounts'; import { SvgExclamationOutline } from '../../icons/v1'; import { useDispatch } from '../../redux'; import { theme } from '../../style'; -import { Button } from '../common/Button2'; import { Link } from '../common/Link'; -import { Popover } from '../common/Popover'; -import { View } from '../common/View'; function useErrorMessage() { const { t } = useTranslation(); diff --git a/packages/desktop-client/src/components/accounts/Balance.tsx b/packages/desktop-client/src/components/accounts/Balance.tsx index b0c37a5de40..136a0c0fc7b 100644 --- a/packages/desktop-client/src/components/accounts/Balance.tsx +++ b/packages/desktop-client/src/components/accounts/Balance.tsx @@ -1,6 +1,9 @@ import React, { useRef } from 'react'; import { useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; import { useHover } from 'usehooks-ts'; import { useCachedSchedules } from 'loot-core/client/data-hooks/schedules'; @@ -12,9 +15,6 @@ import { type AccountEntity } from 'loot-core/types/models'; import { useSelectedItems } from '../../hooks/useSelected'; import { SvgArrowButtonRight1 } from '../../icons/v2'; import { theme } from '../../style'; -import { Button } from '../common/Button2'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; import { PrivacyFilter } from '../PrivacyFilter'; import { type Binding } from '../spreadsheet'; import { CellValue, CellValueText } from '../spreadsheet/CellValue'; diff --git a/packages/desktop-client/src/components/accounts/Header.tsx b/packages/desktop-client/src/components/accounts/Header.tsx index 08e7147a416..59534cab9a1 100644 --- a/packages/desktop-client/src/components/accounts/Header.tsx +++ b/packages/desktop-client/src/components/accounts/Header.tsx @@ -8,6 +8,14 @@ import React, { import { useHotkeys } from 'react-hotkeys-hook'; import { Trans, useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { InitialFocus } from '@actual-app/components/initial-focus'; +import { Menu } from '@actual-app/components/menu'; +import { Popover } from '@actual-app/components/popover'; +import { Stack } from '@actual-app/components/stack'; +import { styles } from '@actual-app/components/styles'; +import { View } from '@actual-app/components/view'; + import { type AccountEntity, type RuleConditionEntity, @@ -27,17 +35,11 @@ import { SvgLockClosed, SvgPencil1, } from '../../icons/v2'; -import { theme, styles } from '../../style'; +import { theme } from '../../style'; import { AnimatedRefresh } from '../AnimatedRefresh'; -import { Button } from '../common/Button2'; -import { InitialFocus } from '../common/InitialFocus'; import { Input } from '../common/Input'; -import { Menu } from '../common/Menu'; import { MenuButton } from '../common/MenuButton'; -import { Popover } from '../common/Popover'; import { Search } from '../common/Search'; -import { Stack } from '../common/Stack'; -import { View } from '../common/View'; import { FilterButton } from '../filters/FiltersMenu'; import { FiltersStack } from '../filters/FiltersStack'; import { type SavedFilter } from '../filters/SavedFilterMenuButton'; diff --git a/packages/desktop-client/src/components/accounts/Reconcile.tsx b/packages/desktop-client/src/components/accounts/Reconcile.tsx index b98eedbddd4..a05a943a58c 100644 --- a/packages/desktop-client/src/components/accounts/Reconcile.tsx +++ b/packages/desktop-client/src/components/accounts/Reconcile.tsx @@ -1,6 +1,12 @@ import React, { useState } from 'react'; import { Trans } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { InitialFocus } from '@actual-app/components/initial-focus'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import * as queries from 'loot-core/client/queries'; import { type Query } from 'loot-core/shared/query'; import { currencyToInteger } from 'loot-core/shared/util'; @@ -8,12 +14,8 @@ import { type AccountEntity } from 'loot-core/types/models'; import { type TransObjectLiteral } from 'loot-core/types/util'; import { SvgCheckCircle1 } from '../../icons/v2'; -import { styles, theme } from '../../style'; -import { Button } from '../common/Button2'; -import { InitialFocus } from '../common/InitialFocus'; +import { theme } from '../../style'; import { Input } from '../common/Input'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; import { useFormat } from '../spreadsheet/useFormat'; import { useSheetValue } from '../spreadsheet/useSheetValue'; diff --git a/packages/desktop-client/src/components/admin/UserAccess/UserAccess.tsx b/packages/desktop-client/src/components/admin/UserAccess/UserAccess.tsx index 38290d3d939..64996f470a8 100644 --- a/packages/desktop-client/src/components/admin/UserAccess/UserAccess.tsx +++ b/packages/desktop-client/src/components/admin/UserAccess/UserAccess.tsx @@ -10,6 +10,10 @@ import React, { } from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { addNotification, pushModal } from 'loot-core/client/actions'; import { send } from 'loot-core/platform/client/fetch'; import * as undo from 'loot-core/platform/client/undo'; @@ -22,12 +26,9 @@ import { SvgLockOpen } from '../../../icons/v1'; import { SvgLockClosed } from '../../../icons/v2'; import { useDispatch } from '../../../redux'; import { theme } from '../../../style'; -import { Button } from '../../common/Button2'; import { Link } from '../../common/Link'; import { Search } from '../../common/Search'; import { SimpleTable } from '../../common/SimpleTable'; -import { Text } from '../../common/Text'; -import { View } from '../../common/View'; import { UserAccessHeader } from './UserAccessHeader'; import { UserAccessRow } from './UserAccessRow'; diff --git a/packages/desktop-client/src/components/admin/UserAccess/UserAccessRow.tsx b/packages/desktop-client/src/components/admin/UserAccess/UserAccessRow.tsx index ced6c563f69..ae4c38f043a 100644 --- a/packages/desktop-client/src/components/admin/UserAccess/UserAccessRow.tsx +++ b/packages/desktop-client/src/components/admin/UserAccess/UserAccessRow.tsx @@ -2,6 +2,8 @@ import React, { memo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { View } from '@actual-app/components/view'; + import { addNotification, signOut } from 'loot-core/client/actions'; import { send } from 'loot-core/platform/client/fetch'; import { getUserAccessErrors } from 'loot-core/shared/errors'; @@ -10,7 +12,6 @@ import { type UserAvailable } from 'loot-core/types/models'; import { useMetadataPref } from '../../../hooks/useMetadataPref'; import { useDispatch } from '../../../redux'; import { theme } from '../../../style'; -import { View } from '../../common/View'; import { Checkbox } from '../../forms'; import { Row, Cell } from '../../table'; diff --git a/packages/desktop-client/src/components/admin/UserDirectory/UserDirectory.tsx b/packages/desktop-client/src/components/admin/UserDirectory/UserDirectory.tsx index cdb4135f470..b1e158bc8cc 100644 --- a/packages/desktop-client/src/components/admin/UserDirectory/UserDirectory.tsx +++ b/packages/desktop-client/src/components/admin/UserDirectory/UserDirectory.tsx @@ -10,6 +10,11 @@ import { } from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Stack } from '@actual-app/components/stack'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { addNotification, signOut } from 'loot-core/client/actions'; import { pushModal } from 'loot-core/client/actions/modals'; import { send } from 'loot-core/platform/client/fetch'; @@ -22,13 +27,9 @@ import { import { SelectedProvider, useSelected } from '../../../hooks/useSelected'; import { useDispatch } from '../../../redux'; import { theme } from '../../../style'; -import { Button } from '../../common/Button2'; import { Link } from '../../common/Link'; import { Search } from '../../common/Search'; import { SimpleTable } from '../../common/SimpleTable'; -import { Stack } from '../../common/Stack'; -import { Text } from '../../common/Text'; -import { View } from '../../common/View'; import { UserDirectoryHeader } from './UserDirectoryHeader'; import { UserDirectoryRow } from './UserDirectoryRow'; @@ -41,37 +42,40 @@ type ManageUserDirectoryContentProps = { function useGetUserDirectoryErrors() { const { t } = useTranslation(); - function getUserDirectoryErrors(reason) { - switch (reason) { - case 'unauthorized': - return t('You are not logged in.'); - case 'token-expired': - return t('Login expired, please log in again.'); - case 'user-cant-be-empty': - return t( - 'Please enter a value for the username; the field cannot be empty.', - ); - case 'role-cant-be-empty': - return t('Select a role; the field cannot be empty.'); - case 'user-already-exists': - return t( - 'The username you entered already exists. Please choose a different username.', - ); - case 'not-all-deleted': - return t( - 'Not all users were deleted. Check if one of the selected users is the server owner.', - ); - case 'role-does-not-exists': - return t( - 'Selected role does not exists, possibly a bug? Visit https://actualbudget.org/contact/ for support.', - ); - default: - return t( - 'An internal error occurred, sorry! Visit https://actualbudget.org/contact/ for support. (ref: {{reason}})', - { reason }, - ); - } - } + const getUserDirectoryErrors = useCallback( + reason => { + switch (reason) { + case 'unauthorized': + return t('You are not logged in.'); + case 'token-expired': + return t('Login expired, please log in again.'); + case 'user-cant-be-empty': + return t( + 'Please enter a value for the username; the field cannot be empty.', + ); + case 'role-cant-be-empty': + return t('Select a role; the field cannot be empty.'); + case 'user-already-exists': + return t( + 'The username you entered already exists. Please choose a different username.', + ); + case 'not-all-deleted': + return t( + 'Not all users were deleted. Check if one of the selected users is the server owner.', + ); + case 'role-does-not-exists': + return t( + 'Selected role does not exist, possibly a bug? Visit https://actualbudget.org/contact/ for support.', + ); + default: + return t( + 'An internal error occurred, sorry! Visit https://actualbudget.org/contact/ for support. (ref: {{reason}})', + { reason }, + ); + } + }, + [t], + ); return { getUserDirectoryErrors }; } diff --git a/packages/desktop-client/src/components/admin/UserDirectory/UserDirectoryPage.tsx b/packages/desktop-client/src/components/admin/UserDirectory/UserDirectoryPage.tsx index ec8fd223f6e..58cf88afa01 100644 --- a/packages/desktop-client/src/components/admin/UserDirectory/UserDirectoryPage.tsx +++ b/packages/desktop-client/src/components/admin/UserDirectory/UserDirectoryPage.tsx @@ -1,9 +1,10 @@ import React, { type ReactNode } from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { View } from '@actual-app/components/view'; + import { useNavigate } from '../../../hooks/useNavigate'; -import { Button } from '../../common/Button2'; -import { View } from '../../common/View'; import { Page } from '../../Page'; import { UserDirectory } from './UserDirectory'; diff --git a/packages/desktop-client/src/components/admin/UserDirectory/UserDirectoryRow.tsx b/packages/desktop-client/src/components/admin/UserDirectory/UserDirectoryRow.tsx index 391b9f33912..a26c7050c2f 100644 --- a/packages/desktop-client/src/components/admin/UserDirectory/UserDirectoryRow.tsx +++ b/packages/desktop-client/src/components/admin/UserDirectory/UserDirectoryRow.tsx @@ -2,12 +2,13 @@ import React, { memo } from 'react'; import { Trans } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { View } from '@actual-app/components/view'; + import { PossibleRoles, type UserEntity } from 'loot-core/types/models/user'; import { useSelectedDispatch } from '../../../hooks/useSelected'; import { theme } from '../../../style'; -import { Button } from '../../common/Button2'; -import { View } from '../../common/View'; import { Checkbox } from '../../forms'; import { SelectCell, Row, Cell } from '../../table'; diff --git a/packages/desktop-client/src/components/alerts.tsx b/packages/desktop-client/src/components/alerts.tsx index ee04560d1ff..7ab8b324004 100644 --- a/packages/desktop-client/src/components/alerts.tsx +++ b/packages/desktop-client/src/components/alerts.tsx @@ -5,11 +5,12 @@ import React, { type CSSProperties, } from 'react'; -import { SvgExclamationOutline, SvgInformationOutline } from '../icons/v1'; -import { styles, theme } from '../style'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; -import { Text } from './common/Text'; -import { View } from './common/View'; +import { SvgExclamationOutline, SvgInformationOutline } from '../icons/v1'; +import { theme } from '../style'; type AlertProps = { icon?: ComponentType<{ width?: number; style?: CSSProperties }>; diff --git a/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx b/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx index 967e2630705..a4786f9576b 100644 --- a/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx +++ b/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx @@ -8,14 +8,15 @@ import React, { } from 'react'; import { useTranslation } from 'react-i18next'; +import { styles } from '@actual-app/components/styles'; +import { TextOneLine } from '@actual-app/components/text-one-line'; +import { View } from '@actual-app/components/view'; import { css, cx } from '@emotion/css'; import { type AccountEntity } from 'loot-core/types/models'; import { useAccounts } from '../../hooks/useAccounts'; -import { theme, styles } from '../../style'; -import { TextOneLine } from '../common/TextOneLine'; -import { View } from '../common/View'; +import { theme } from '../../style'; import { useResponsive } from '../responsive/ResponsiveProvider'; import { Autocomplete } from './Autocomplete'; diff --git a/packages/desktop-client/src/components/autocomplete/Autocomplete.tsx b/packages/desktop-client/src/components/autocomplete/Autocomplete.tsx index 914cb0db7c3..c79698da2de 100644 --- a/packages/desktop-client/src/components/autocomplete/Autocomplete.tsx +++ b/packages/desktop-client/src/components/autocomplete/Autocomplete.tsx @@ -11,17 +11,18 @@ import React, { useState, } from 'react'; +import { Popover } from '@actual-app/components/popover'; +import { styles } from '@actual-app/components/styles'; +import { View } from '@actual-app/components/view'; import { css, cx } from '@emotion/css'; import Downshift, { type StateChangeTypes } from 'downshift'; import { getNormalisedString } from 'loot-core/shared/normalisation'; import { SvgRemove } from '../../icons/v2'; -import { styles, theme } from '../../style'; +import { theme } from '../../style'; import { Button } from '../common/Button'; import { Input } from '../common/Input'; -import { Popover } from '../common/Popover'; -import { View } from '../common/View'; import { useResponsive } from '../responsive/ResponsiveProvider'; type CommonAutocompleteProps = { diff --git a/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx b/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx index c3ba3d990b0..913a7bc19a9 100644 --- a/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx +++ b/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx @@ -12,6 +12,10 @@ import React, { } from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { TextOneLine } from '@actual-app/components/text-one-line'; +import { View } from '@actual-app/components/view'; import { css, cx } from '@emotion/css'; import { trackingBudget, envelopeBudget } from 'loot-core/client/queries'; @@ -25,12 +29,9 @@ import { import { useCategories } from '../../hooks/useCategories'; import { useSyncedPref } from '../../hooks/useSyncedPref'; import { SvgSplit } from '../../icons/v0'; -import { theme, styles } from '../../style'; +import { theme } from '../../style'; import { useEnvelopeSheetValue } from '../budget/envelope/EnvelopeBudgetComponents'; import { makeAmountFullStyle } from '../budget/util'; -import { Text } from '../common/Text'; -import { TextOneLine } from '../common/TextOneLine'; -import { View } from '../common/View'; import { useResponsive } from '../responsive/ResponsiveProvider'; import { useSheetValue } from '../spreadsheet/useSheetValue'; diff --git a/packages/desktop-client/src/components/autocomplete/FilterList.tsx b/packages/desktop-client/src/components/autocomplete/FilterList.tsx index 1e4e0191b68..f5332b48f8a 100644 --- a/packages/desktop-client/src/components/autocomplete/FilterList.tsx +++ b/packages/desktop-client/src/components/autocomplete/FilterList.tsx @@ -1,8 +1,9 @@ import React, { type ComponentProps } from 'react'; import { useTranslation } from 'react-i18next'; +import { View } from '@actual-app/components/view'; + import { theme } from '../../style/theme'; -import { View } from '../common/View'; import { ItemHeader } from './ItemHeader'; diff --git a/packages/desktop-client/src/components/autocomplete/ItemHeader.tsx b/packages/desktop-client/src/components/autocomplete/ItemHeader.tsx index 1d9bf95c12d..18cbb9c9ed2 100644 --- a/packages/desktop-client/src/components/autocomplete/ItemHeader.tsx +++ b/packages/desktop-client/src/components/autocomplete/ItemHeader.tsx @@ -1,6 +1,8 @@ import React, { type CSSProperties } from 'react'; -import { styles, theme } from '../../style'; +import { styles } from '@actual-app/components/styles'; + +import { theme } from '../../style'; import { useResponsive } from '../responsive/ResponsiveProvider'; type ItemHeaderProps = { diff --git a/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.tsx b/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.tsx index f1113913a50..0b14526a9eb 100644 --- a/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.tsx +++ b/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.tsx @@ -13,6 +13,9 @@ import React, { } from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { styles } from '@actual-app/components/styles'; +import { TextOneLine } from '@actual-app/components/text-one-line'; +import { View } from '@actual-app/components/view'; import { css, cx } from '@emotion/css'; import { @@ -26,10 +29,8 @@ import { useAccounts } from '../../hooks/useAccounts'; import { useCommonPayees, usePayees } from '../../hooks/usePayees'; import { SvgAdd, SvgBookmark } from '../../icons/v1'; import { useDispatch } from '../../redux'; -import { theme, styles } from '../../style'; +import { theme } from '../../style'; import { Button } from '../common/Button'; -import { TextOneLine } from '../common/TextOneLine'; -import { View } from '../common/View'; import { useResponsive } from '../responsive/ResponsiveProvider'; import { diff --git a/packages/desktop-client/src/components/autocomplete/ReportList.tsx b/packages/desktop-client/src/components/autocomplete/ReportList.tsx index f4fb2cb553c..c1212931871 100644 --- a/packages/desktop-client/src/components/autocomplete/ReportList.tsx +++ b/packages/desktop-client/src/components/autocomplete/ReportList.tsx @@ -1,8 +1,9 @@ import React, { Fragment, type ComponentProps } from 'react'; import { useTranslation } from 'react-i18next'; +import { View } from '@actual-app/components/view'; + import { theme } from '../../style/theme'; -import { View } from '../common/View'; import { ItemHeader } from './ItemHeader'; diff --git a/packages/desktop-client/src/components/banksync/AccountRow.tsx b/packages/desktop-client/src/components/banksync/AccountRow.tsx new file mode 100644 index 00000000000..e98d2242f92 --- /dev/null +++ b/packages/desktop-client/src/components/banksync/AccountRow.tsx @@ -0,0 +1,93 @@ +import React, { memo } from 'react'; +import { Trans } from 'react-i18next'; + +import { Button } from '@actual-app/components/button'; + +import { format } from 'loot-core/src/shared/months'; +import { type AccountEntity } from 'loot-core/src/types/models'; + +import { useDateFormat } from '../../hooks/useDateFormat'; +import { theme } from '../../style'; +import { Row, Cell } from '../table'; + +const tsToString = (ts: string | null, dateFormat: string) => { + if (!ts) return 'Unknown'; + + const parsed = new Date(parseInt(ts, 10)); + return `${format(parsed, dateFormat)} ${format(parsed, 'HH:mm:ss')}`; +}; + +type AccountRowProps = { + account: AccountEntity; + hovered: boolean; + onHover: (id: AccountEntity['id'] | null) => void; + onAction: (account: AccountEntity, action: 'link' | 'edit') => void; +}; + +export const AccountRow = memo( + ({ account, hovered, onHover, onAction }: AccountRowProps) => { + const backgroundFocus = hovered; + + const dateFormat = useDateFormat() || 'MM/dd/yyyy'; + + const lastSync = tsToString(account.last_sync, dateFormat); + + return ( + onHover && onHover(account.id)} + onMouseLeave={() => onHover && onHover(null)} + > + + {account.name} + + + + {account.bankName} + + + + {account.account_sync_source ? lastSync : ''} + + + {account.account_sync_source ? ( + + + + ) : ( + + + + )} + + ); + }, +); + +AccountRow.displayName = 'AccountRow'; diff --git a/packages/desktop-client/src/components/banksync/AccountsHeader.tsx b/packages/desktop-client/src/components/banksync/AccountsHeader.tsx new file mode 100644 index 00000000000..c5000046705 --- /dev/null +++ b/packages/desktop-client/src/components/banksync/AccountsHeader.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { Cell, TableHeader } from '../table'; + +type AccountsHeaderProps = { + unlinked: boolean; +}; + +export function AccountsHeader({ unlinked }: AccountsHeaderProps) { + const { t } = useTranslation(); + + return ( + + + {!unlinked && ( + <> + + + + + )} + + ); +} diff --git a/packages/desktop-client/src/components/banksync/AccountsList.tsx b/packages/desktop-client/src/components/banksync/AccountsList.tsx new file mode 100644 index 00000000000..4bff9360ac4 --- /dev/null +++ b/packages/desktop-client/src/components/banksync/AccountsList.tsx @@ -0,0 +1,43 @@ +import React from 'react'; + +import { View } from '@actual-app/components/view'; + +import { type AccountEntity } from 'loot-core/src/types/models'; + +import { AccountRow } from './AccountRow'; + +type AccountsListProps = { + accounts: AccountEntity[]; + hoveredAccount?: string | null; + onHover: (id: AccountEntity['id'] | null) => void; + onAction: (account: AccountEntity, action: 'link' | 'edit') => void; +}; + +export function AccountsList({ + accounts, + hoveredAccount, + onHover, + onAction, +}: AccountsListProps) { + if (accounts.length === 0) { + return null; + } + + return ( + + {accounts.map(account => { + const hovered = hoveredAccount === account.id; + + return ( + + ); + })} + + ); +} diff --git a/packages/desktop-client/src/components/banksync/EditSyncAccount.tsx b/packages/desktop-client/src/components/banksync/EditSyncAccount.tsx new file mode 100644 index 00000000000..b13efd362f1 --- /dev/null +++ b/packages/desktop-client/src/components/banksync/EditSyncAccount.tsx @@ -0,0 +1,232 @@ +import React, { useMemo, useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; + +import { Button } from '@actual-app/components/button'; +import { Stack } from '@actual-app/components/stack'; +import { Text } from '@actual-app/components/text'; + +import { useTransactions } from 'loot-core/client/data-hooks/transactions'; +import { + defaultMappings, + type Mappings, + mappingsFromString, + mappingsToString, +} from 'loot-core/server/util/custom-sync-mapping'; +import { q } from 'loot-core/src/shared/query'; +import { + type TransactionEntity, + type AccountEntity, +} from 'loot-core/src/types/models'; + +import { useSyncedPref } from '../../hooks/useSyncedPref'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal'; +import { CheckboxOption } from '../modals/ImportTransactionsModal/CheckboxOption'; + +import { FieldMapping } from './FieldMapping'; + +export type TransactionDirection = 'payment' | 'deposit'; + +type MappableActualFields = 'date' | 'payee' | 'notes'; + +export type MappableField = { + actualField: MappableActualFields; + syncFields: string[]; +}; +export type MappableFieldWithExample = { + actualField: MappableActualFields; + syncFields: { + field: string; + example: string; + }[]; +}; + +const mappableFields: MappableField[] = [ + { + actualField: 'date', + syncFields: [ + 'date', + 'bookingDate', + 'valueDate', + 'postedDate', + 'transactedDate', + ], + }, + { + actualField: 'payee', + syncFields: [ + 'payeeName', + 'creditorName', + 'debtorName', + 'remittanceInformationUnstructured', + 'remittanceInformationUnstructuredArrayString', + 'remittanceInformationStructured', + 'remittanceInformationStructuredArrayString', + 'additionalInformation', + ], + }, + { + actualField: 'notes', + syncFields: [ + 'notes', + 'remittanceInformationUnstructured', + 'remittanceInformationUnstructuredArrayString', + 'remittanceInformationStructured', + 'remittanceInformationStructuredArrayString', + 'additionalInformation', + ], + }, +]; + +const getFields = (transaction: TransactionEntity) => + mappableFields.map(field => ({ + actualField: field.actualField, + syncFields: field.syncFields + .filter(syncField => transaction[syncField as keyof TransactionEntity]) + .map(syncField => ({ + field: syncField, + example: transaction[syncField as keyof TransactionEntity], + })), + })); + +export type EditSyncAccountProps = { + account: AccountEntity; +}; + +export function EditSyncAccount({ account }: EditSyncAccountProps) { + const { t } = useTranslation(); + + const [savedMappings = mappingsToString(defaultMappings), setSavedMappings] = + useSyncedPref(`custom-sync-mappings-${account.id}`); + const [savedImportNotes = true, setSavedImportNotes] = useSyncedPref( + `sync-import-notes-${account.id}`, + ); + const [savedImportPending = true, setSavedImportPending] = useSyncedPref( + `sync-import-pending-${account.id}`, + ); + + const [transactionDirection, setTransactionDirection] = + useState('payment'); + const [importPending, setImportPending] = useState( + String(savedImportPending) === 'true', + ); + const [importNotes, setImportNotes] = useState( + String(savedImportNotes) === 'true', + ); + const [mappings, setMappings] = useState( + mappingsFromString(savedMappings), + ); + + const transactionQuery = useMemo( + () => + q('transactions') + .filter({ + account: account.id, + amount: transactionDirection === 'payment' ? { $lte: 0 } : { $gt: 0 }, + raw_synced_data: { $ne: null }, + }) + .options({ splits: 'none' }) + .select('*'), + [account.id, transactionDirection], + ); + + const { transactions } = useTransactions({ + query: transactionQuery, + }); + + const exampleTransaction = useMemo(() => { + const data = transactions?.[0]?.raw_synced_data; + if (!data) return undefined; + try { + return JSON.parse(data); + } catch (error) { + console.error('Failed to parse transaction data:', error); + return undefined; + } + }, [transactions]); + + const onSave = async (close: () => void) => { + const mappingsStr = mappingsToString(mappings); + setSavedMappings(mappingsStr); + setSavedImportPending(String(importPending)); + setSavedImportNotes(String(importNotes)); + close(); + }; + + const setMapping = (field: string, value: string) => { + setMappings(prev => { + const updated = new Map(prev); + updated?.get(transactionDirection)?.set(field, value); + return updated; + }); + }; + + const fields = exampleTransaction ? getFields(exampleTransaction) : []; + const mapping = mappings.get(transactionDirection); + + return ( + + {({ state: { close } }) => ( + <> + } + /> + + + Field mapping + + + + + + Options + + + setImportPending(!importPending)} + > + Import pending transactions + + + setImportNotes(!importNotes)} + > + Import transaction notes + + + + + + + + )} + + ); +} diff --git a/packages/desktop-client/src/components/banksync/FieldMapping.tsx b/packages/desktop-client/src/components/banksync/FieldMapping.tsx new file mode 100644 index 00000000000..ddf0975cbcd --- /dev/null +++ b/packages/desktop-client/src/components/banksync/FieldMapping.tsx @@ -0,0 +1,167 @@ +import React from 'react'; +import { Trans, useTranslation } from 'react-i18next'; + +import { Text } from '@actual-app/components/text'; + +import { SvgRightArrow2 } from '../../icons/v0'; +import { SvgEquals } from '../../icons/v1'; +import { theme } from '../../style'; +import { Select } from '../common/Select'; +import { Row, Cell, TableHeader } from '../table'; + +import { + type MappableFieldWithExample, + type TransactionDirection, +} from './EditSyncAccount'; + +const useTransactionDirectionOptions = () => { + const { t } = useTranslation(); + + const transactionDirectionOptions = [ + { + value: 'payment', + label: t('Payment'), + }, + { + value: 'deposit', + label: t('Deposit'), + }, + ]; + + return { transactionDirectionOptions }; +}; + +type FieldMappingProps = { + transactionDirection: TransactionDirection; + setTransactionDirection: (newValue: TransactionDirection) => void; + fields: MappableFieldWithExample[]; + mapping: Map; + setMapping: (field: string, value: string) => void; +}; + +export function FieldMapping({ + transactionDirection, + setTransactionDirection, + fields, + mapping, + setMapping, +}: FieldMappingProps) { + const { t } = useTranslation(); + + const { transactionDirectionOptions } = useTransactionDirectionOptions(); + + return ( + <> + [field, field])} + value={mapping.get(field.actualField)} + style={{ + width: 290, + }} + onChange={newValue => { + if (newValue) setMapping(field.actualField, newValue); + }} + /> + + + + + + f.field === mapping.get(field.actualField), + )?.example + } + width="flex" + style={{ paddingLeft: '10px', height: '100%', border: 0 }} + /> + + ); + })} + + )} + + ); +} diff --git a/packages/desktop-client/src/components/banksync/index.tsx b/packages/desktop-client/src/components/banksync/index.tsx new file mode 100644 index 00000000000..4526920cad4 --- /dev/null +++ b/packages/desktop-client/src/components/banksync/index.tsx @@ -0,0 +1,139 @@ +import { useMemo, useState, useCallback } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; + +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + +import { pushModal } from 'loot-core/src/client/actions/modals'; +import { + type BankSyncProviders, + type AccountEntity, +} from 'loot-core/types/models'; + +import { useAccounts } from '../../hooks/useAccounts'; +import { useGlobalPref } from '../../hooks/useGlobalPref'; +import { useDispatch } from '../../redux'; +import { MOBILE_NAV_HEIGHT } from '../mobile/MobileNavTabs'; +import { Page } from '../Page'; +import { useResponsive } from '../responsive/ResponsiveProvider'; + +import { AccountsHeader } from './AccountsHeader'; +import { AccountsList } from './AccountsList'; + +type SyncProviders = BankSyncProviders | 'unlinked'; + +const useSyncSourceReadable = () => { + const { t } = useTranslation(); + + const syncSourceReadable: Record = { + goCardless: 'GoCardless', + simpleFin: 'SimpleFIN', + unlinked: t('Unlinked'), + }; + + return { syncSourceReadable }; +}; + +export function BankSync() { + const { t } = useTranslation(); + const [floatingSidebar] = useGlobalPref('floatingSidebar'); + + const { syncSourceReadable } = useSyncSourceReadable(); + + const accounts = useAccounts(); + const dispatch = useDispatch(); + const { isNarrowWidth } = useResponsive(); + + const [hoveredAccount, setHoveredAccount] = useState< + AccountEntity['id'] | null + >(null); + + const groupedAccounts = useMemo(() => { + const unsorted = accounts + .filter(a => !a.closed) + .reduce( + (acc, a) => { + const syncSource = a.account_sync_source ?? 'unlinked'; + acc[syncSource] = acc[syncSource] || []; + acc[syncSource].push(a); + return acc; + }, + {} as Record, + ); + + const sortedKeys = Object.keys(unsorted).sort((keyA, keyB) => { + if (keyA === 'unlinked') return 1; + if (keyB === 'unlinked') return -1; + return keyA.localeCompare(keyB); + }); + + return sortedKeys.reduce( + (sorted, key) => { + sorted[key as SyncProviders] = unsorted[key as SyncProviders]; + return sorted; + }, + {} as Record, + ); + }, [accounts]); + + const onAction = async (account: AccountEntity, action: 'link' | 'edit') => { + switch (action) { + case 'edit': + dispatch( + pushModal('synced-account-edit', { + account, + }), + ); + break; + case 'link': + dispatch(pushModal('add-account', { upgradingAccountId: account.id })); + break; + default: + break; + } + }; + + const onHover = useCallback((id: AccountEntity['id'] | null) => { + setHoveredAccount(id); + }, []); + + return ( + + + {accounts.length === 0 && ( + + + To use the bank syncing features, you must first add an account. + + + )} + {Object.entries(groupedAccounts).map(([syncProvider, accounts]) => { + return ( + + {Object.keys(groupedAccounts).length > 1 && ( + + {syncSourceReadable[syncProvider as SyncProviders]} + + )} + + + + ); + })} + + + ); +} diff --git a/packages/desktop-client/src/components/budget/BalanceWithCarryover.tsx b/packages/desktop-client/src/components/budget/BalanceWithCarryover.tsx index 320c29dc7be..45c0859c2c8 100644 --- a/packages/desktop-client/src/components/budget/BalanceWithCarryover.tsx +++ b/packages/desktop-client/src/components/budget/BalanceWithCarryover.tsx @@ -7,15 +7,16 @@ import React, { } from 'react'; import { useTranslation, Trans } from 'react-i18next'; +import { styles } from '@actual-app/components/styles'; +import { Tooltip } from '@actual-app/components/tooltip'; +import { View } from '@actual-app/components/view'; import { css } from '@emotion/css'; import { type TransObjectLiteral } from 'loot-core/types/util'; import { useFeatureFlag } from '../../hooks/useFeatureFlag'; import { SvgArrowThinRight } from '../../icons/v1'; -import { theme, styles } from '../../style'; -import { Tooltip } from '../common/Tooltip'; -import { View } from '../common/View'; +import { theme } from '../../style'; import { useResponsive } from '../responsive/ResponsiveProvider'; import { type Binding } from '../spreadsheet'; import { CellValue, CellValueText } from '../spreadsheet/CellValue'; @@ -186,10 +187,7 @@ export function BalanceWithCarryover({
{ { - type: - longGoalValue === 1 - ? t('Long', { context: 'noun' }) - : t('Template'), + type: longGoalValue === 1 ? t('Long') : t('Template'), } as TransObjectLiteral }
diff --git a/packages/desktop-client/src/components/budget/BudgetCategories.jsx b/packages/desktop-client/src/components/budget/BudgetCategories.jsx index 12efffd809b..b3a08e22782 100644 --- a/packages/desktop-client/src/components/budget/BudgetCategories.jsx +++ b/packages/desktop-client/src/components/budget/BudgetCategories.jsx @@ -1,8 +1,10 @@ import React, { memo, useState, useMemo } from 'react'; +import { styles } from '@actual-app/components/styles'; +import { View } from '@actual-app/components/view'; + import { useLocalPref } from '../../hooks/useLocalPref'; -import { theme, styles } from '../../style'; -import { View } from '../common/View'; +import { theme } from '../../style'; import { DropHighlightPosContext } from '../sort'; import { Row } from '../table'; diff --git a/packages/desktop-client/src/components/budget/BudgetPageHeader.tsx b/packages/desktop-client/src/components/budget/BudgetPageHeader.tsx index dfdb5844885..ef61f4c0401 100644 --- a/packages/desktop-client/src/components/budget/BudgetPageHeader.tsx +++ b/packages/desktop-client/src/components/budget/BudgetPageHeader.tsx @@ -1,7 +1,7 @@ // @ts-strict-ignore import React, { type ComponentProps, memo } from 'react'; -import { View } from '../common/View'; +import { View } from '@actual-app/components/view'; import { MonthPicker } from './MonthPicker'; import { getScrollbarWidth } from './util'; diff --git a/packages/desktop-client/src/components/budget/BudgetSummaries.tsx b/packages/desktop-client/src/components/budget/BudgetSummaries.tsx index 5308e8a36ac..8dffb677d65 100644 --- a/packages/desktop-client/src/components/budget/BudgetSummaries.tsx +++ b/packages/desktop-client/src/components/budget/BudgetSummaries.tsx @@ -7,12 +7,12 @@ import React, { } from 'react'; import { useSpring, animated } from 'react-spring'; +import { View } from '@actual-app/components/view'; import { css } from '@emotion/css'; import { addMonths, subMonths } from 'loot-core/shared/months'; import { useResizeObserver } from '../../hooks/useResizeObserver'; -import { View } from '../common/View'; import { type BudgetSummary as EnvelopeBudgetSummary } from './envelope/budgetsummary/BudgetSummary'; import { MonthsContext } from './MonthsContext'; diff --git a/packages/desktop-client/src/components/budget/BudgetTable.tsx b/packages/desktop-client/src/components/budget/BudgetTable.tsx index 66481fd64af..bcdf1719277 100644 --- a/packages/desktop-client/src/components/budget/BudgetTable.tsx +++ b/packages/desktop-client/src/components/budget/BudgetTable.tsx @@ -4,6 +4,9 @@ import React, { useState, } from 'react'; +import { styles } from '@actual-app/components/styles'; +import { View } from '@actual-app/components/view'; + import { type CategoryEntity, type CategoryGroupEntity, @@ -11,8 +14,7 @@ import { import { useCategories } from '../../hooks/useCategories'; import { useLocalPref } from '../../hooks/useLocalPref'; -import { theme, styles } from '../../style'; -import { View } from '../common/View'; +import { theme } from '../../style'; import { type DropPosition } from '../sort'; import { BudgetCategories } from './BudgetCategories'; diff --git a/packages/desktop-client/src/components/budget/BudgetTotals.tsx b/packages/desktop-client/src/components/budget/BudgetTotals.tsx index fa3e348b28f..59b9a2f4bc1 100644 --- a/packages/desktop-client/src/components/budget/BudgetTotals.tsx +++ b/packages/desktop-client/src/components/budget/BudgetTotals.tsx @@ -1,12 +1,14 @@ import React, { type ComponentProps, memo, useRef, useState } from 'react'; import { useTranslation, Trans } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Menu } from '@actual-app/components/menu'; +import { Popover } from '@actual-app/components/popover'; +import { styles } from '@actual-app/components/styles'; +import { View } from '@actual-app/components/view'; + import { SvgDotsHorizontalTriple } from '../../icons/v1'; -import { theme, styles } from '../../style'; -import { Button } from '../common/Button2'; -import { Menu } from '../common/Menu'; -import { Popover } from '../common/Popover'; -import { View } from '../common/View'; +import { theme } from '../../style'; import { RenderMonths } from './RenderMonths'; import { getScrollbarWidth } from './util'; diff --git a/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx b/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx index 416c6be8d14..0875fda55dc 100644 --- a/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx +++ b/packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx @@ -3,9 +3,9 @@ import React, { useEffect, type ComponentProps } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import AutoSizer from 'react-virtualized-auto-sizer'; -import * as monthUtils from 'loot-core/shared/months'; +import { View } from '@actual-app/components/view'; -import { View } from '../common/View'; +import * as monthUtils from 'loot-core/shared/months'; import { useBudgetMonthCount } from './BudgetMonthCountContext'; import { BudgetPageHeader } from './BudgetPageHeader'; diff --git a/packages/desktop-client/src/components/budget/ExpenseCategory.tsx b/packages/desktop-client/src/components/budget/ExpenseCategory.tsx index f11721c2063..ed6877dee96 100644 --- a/packages/desktop-client/src/components/budget/ExpenseCategory.tsx +++ b/packages/desktop-client/src/components/budget/ExpenseCategory.tsx @@ -1,13 +1,14 @@ // @ts-strict-ignore import React, { type ComponentProps } from 'react'; +import { View } from '@actual-app/components/view'; + import { type CategoryGroupEntity, type CategoryEntity, } from 'loot-core/types/models'; import { theme } from '../../style'; -import { View } from '../common/View'; import { useDraggable, useDroppable, diff --git a/packages/desktop-client/src/components/budget/ExpenseGroup.tsx b/packages/desktop-client/src/components/budget/ExpenseGroup.tsx index 5fdbf16d848..61dad6f1e23 100644 --- a/packages/desktop-client/src/components/budget/ExpenseGroup.tsx +++ b/packages/desktop-client/src/components/budget/ExpenseGroup.tsx @@ -1,8 +1,9 @@ // @ts-strict-ignore import React, { type ComponentProps } from 'react'; +import { View } from '@actual-app/components/view'; + import { theme } from '../../style'; -import { View } from '../common/View'; import { useDraggable, useDroppable, diff --git a/packages/desktop-client/src/components/budget/IncomeHeader.tsx b/packages/desktop-client/src/components/budget/IncomeHeader.tsx index 193188fcf9d..3f3fbc1db3e 100644 --- a/packages/desktop-client/src/components/budget/IncomeHeader.tsx +++ b/packages/desktop-client/src/components/budget/IncomeHeader.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { Trans } from 'react-i18next'; -import { Button } from '../common/Button2'; -import { View } from '../common/View'; +import { Button } from '@actual-app/components/button'; +import { View } from '@actual-app/components/view'; import { RenderMonths } from './RenderMonths'; diff --git a/packages/desktop-client/src/components/budget/MonthCountSelector.tsx b/packages/desktop-client/src/components/budget/MonthCountSelector.tsx index 22019451ae3..13a0163450a 100644 --- a/packages/desktop-client/src/components/budget/MonthCountSelector.tsx +++ b/packages/desktop-client/src/components/budget/MonthCountSelector.tsx @@ -1,9 +1,10 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; +import { View } from '@actual-app/components/view'; + import { SvgCalendar } from '../../icons/v2'; import { theme } from '../../style'; -import { View } from '../common/View'; import { useBudgetMonthCount } from './BudgetMonthCountContext'; diff --git a/packages/desktop-client/src/components/budget/MonthPicker.tsx b/packages/desktop-client/src/components/budget/MonthPicker.tsx index d75c9725dc7..2085f52f7f2 100644 --- a/packages/desktop-client/src/components/budget/MonthPicker.tsx +++ b/packages/desktop-client/src/components/budget/MonthPicker.tsx @@ -2,13 +2,15 @@ import React, { type CSSProperties, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { styles } from '@actual-app/components/styles'; +import { View } from '@actual-app/components/view'; + import * as monthUtils from 'loot-core/shared/months'; import { useResizeObserver } from '../../hooks/useResizeObserver'; import { SvgCalendar } from '../../icons/v2'; -import { styles, theme } from '../../style'; +import { theme } from '../../style'; import { Link } from '../common/Link'; -import { View } from '../common/View'; import { type MonthBounds } from './MonthsContext'; diff --git a/packages/desktop-client/src/components/budget/RenderMonths.tsx b/packages/desktop-client/src/components/budget/RenderMonths.tsx index 4a80049c8ce..2b761c862f0 100644 --- a/packages/desktop-client/src/components/budget/RenderMonths.tsx +++ b/packages/desktop-client/src/components/budget/RenderMonths.tsx @@ -5,10 +5,11 @@ import React, { type ComponentType, } from 'react'; +import { View } from '@actual-app/components/view'; + import * as monthUtils from 'loot-core/shared/months'; import { theme } from '../../style'; -import { View } from '../common/View'; import { NamespaceContext } from '../spreadsheet/NamespaceContext'; import { MonthsContext } from './MonthsContext'; diff --git a/packages/desktop-client/src/components/budget/SidebarCategory.tsx b/packages/desktop-client/src/components/budget/SidebarCategory.tsx index 91cf34c7986..0b08646ce7f 100644 --- a/packages/desktop-client/src/components/budget/SidebarCategory.tsx +++ b/packages/desktop-client/src/components/budget/SidebarCategory.tsx @@ -2,21 +2,25 @@ import React, { type CSSProperties, type Ref, useRef } from 'react'; import { useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Menu } from '@actual-app/components/menu'; +import { Popover } from '@actual-app/components/popover'; +import { View } from '@actual-app/components/view'; + import { type CategoryGroupEntity, type CategoryEntity, } from 'loot-core/types/models'; import { useContextMenu } from '../../hooks/useContextMenu'; +import { useFeatureFlag } from '../../hooks/useFeatureFlag'; import { SvgCheveronDown } from '../../icons/v1'; import { theme } from '../../style'; -import { Button } from '../common/Button2'; -import { Menu } from '../common/Menu'; -import { Popover } from '../common/Popover'; -import { View } from '../common/View'; import { NotesButton } from '../NotesButton'; import { InputCell } from '../table'; +import { CategoryAutomationButton } from './goals/CategoryAutomationButton'; + type SidebarCategoryProps = { innerRef: Ref; category: CategoryEntity; @@ -24,6 +28,7 @@ type SidebarCategoryProps = { dragPreview?: boolean; dragging?: boolean; editing: boolean; + goalsShown?: boolean; style?: CSSProperties; borderColor?: string; isLast?: boolean; @@ -40,6 +45,7 @@ export function SidebarCategory({ dragPreview, dragging, editing, + goalsShown = false, style, isLast, onEditName, @@ -48,6 +54,7 @@ export function SidebarCategory({ onHideNewCategory, }: SidebarCategoryProps) { const { t } = useTranslation(); + const goalTemplatesUIEnabled = useFeatureFlag('goalTemplatesUIEnabled'); const temporary = category.id === 'new'; const { setMenuOpen, menuOpen, handleContextMenu, resetPosition, position } = @@ -117,17 +124,25 @@ export function SidebarCategory({ setMenuOpen(false); }} items={[ - { name: 'rename', text: 'Rename' }, + { name: 'rename', text: t('Rename') }, !categoryGroup?.hidden && { name: 'toggle-visibility', - text: category.hidden ? 'Show' : 'Hide', + text: category.hidden ? t('Show') : t('Hide'), }, - { name: 'delete', text: 'Delete' }, + { name: 'delete', text: t('Delete') }, ]} /> + {!goalsShown && goalTemplatesUIEnabled && ( + + + + )} , diff --git a/packages/desktop-client/src/components/budget/envelope/CoverMenu.tsx b/packages/desktop-client/src/components/budget/envelope/CoverMenu.tsx index cac3186bb11..bd2fe69b306 100644 --- a/packages/desktop-client/src/components/budget/envelope/CoverMenu.tsx +++ b/packages/desktop-client/src/components/budget/envelope/CoverMenu.tsx @@ -1,13 +1,14 @@ import React, { useMemo, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { InitialFocus } from '@actual-app/components/initial-focus'; +import { View } from '@actual-app/components/view'; + import { type CategoryEntity } from 'loot-core/types/models'; import { useCategories } from '../../../hooks/useCategories'; import { CategoryAutocomplete } from '../../autocomplete/CategoryAutocomplete'; -import { Button } from '../../common/Button2'; -import { InitialFocus } from '../../common/InitialFocus'; -import { View } from '../../common/View'; import { addToBeBudgetedGroup, removeCategoriesFromGroups } from '../util'; type CoverMenuProps = { diff --git a/packages/desktop-client/src/components/budget/envelope/EnvelopeBudgetComponents.tsx b/packages/desktop-client/src/components/budget/envelope/EnvelopeBudgetComponents.tsx index 14489e8f77b..eee049d1353 100644 --- a/packages/desktop-client/src/components/budget/envelope/EnvelopeBudgetComponents.tsx +++ b/packages/desktop-client/src/components/budget/envelope/EnvelopeBudgetComponents.tsx @@ -6,6 +6,11 @@ import React, { } from 'react'; import { useTranslation, Trans } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Popover } from '@actual-app/components/popover'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; import { css } from '@emotion/css'; import { envelopeBudget } from 'loot-core/client/queries'; @@ -16,11 +21,7 @@ import { integerToCurrency, amountToInteger } from 'loot-core/shared/util'; import { useContextMenu } from '../../../hooks/useContextMenu'; import { useUndo } from '../../../hooks/useUndo'; import { SvgCheveronDown } from '../../../icons/v1'; -import { styles, theme } from '../../../style'; -import { Button } from '../../common/Button2'; -import { Popover } from '../../common/Popover'; -import { Text } from '../../common/Text'; -import { View } from '../../common/View'; +import { theme } from '../../../style'; import { type Binding, type SheetFields } from '../../spreadsheet'; import { CellValue, CellValueText } from '../../spreadsheet/CellValue'; import { useSheetName } from '../../spreadsheet/useSheetName'; diff --git a/packages/desktop-client/src/components/budget/envelope/HoldMenu.tsx b/packages/desktop-client/src/components/budget/envelope/HoldMenu.tsx index d09f101c970..57afe34d497 100644 --- a/packages/desktop-client/src/components/budget/envelope/HoldMenu.tsx +++ b/packages/desktop-client/src/components/budget/envelope/HoldMenu.tsx @@ -6,14 +6,15 @@ import React, { } from 'react'; import { Trans } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { InitialFocus } from '@actual-app/components/initial-focus'; +import { View } from '@actual-app/components/view'; + import { useSpreadsheet } from 'loot-core/client/SpreadsheetProvider'; import { evalArithmetic } from 'loot-core/shared/arithmetic'; import { integerToCurrency, amountToInteger } from 'loot-core/shared/util'; -import { Button } from '../../common/Button2'; -import { InitialFocus } from '../../common/InitialFocus'; import { Input } from '../../common/Input'; -import { View } from '../../common/View'; import { NamespaceContext } from '../../spreadsheet/NamespaceContext'; type HoldMenuProps = { diff --git a/packages/desktop-client/src/components/budget/envelope/TransferMenu.tsx b/packages/desktop-client/src/components/budget/envelope/TransferMenu.tsx index e5e893f25f1..41793ed9c12 100644 --- a/packages/desktop-client/src/components/budget/envelope/TransferMenu.tsx +++ b/packages/desktop-client/src/components/budget/envelope/TransferMenu.tsx @@ -1,16 +1,17 @@ import React, { useMemo, useState } from 'react'; import { Trans } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { InitialFocus } from '@actual-app/components/initial-focus'; +import { View } from '@actual-app/components/view'; + import { evalArithmetic } from 'loot-core/shared/arithmetic'; import { integerToCurrency, amountToInteger } from 'loot-core/shared/util'; import { type CategoryEntity } from 'loot-core/types/models'; import { useCategories } from '../../../hooks/useCategories'; import { CategoryAutocomplete } from '../../autocomplete/CategoryAutocomplete'; -import { Button } from '../../common/Button2'; -import { InitialFocus } from '../../common/InitialFocus'; import { Input } from '../../common/Input'; -import { View } from '../../common/View'; import { addToBeBudgetedGroup, removeCategoriesFromGroups } from '../util'; type TransferMenuProps = { diff --git a/packages/desktop-client/src/components/budget/envelope/budgetsummary/BudgetMonthMenu.tsx b/packages/desktop-client/src/components/budget/envelope/budgetsummary/BudgetMonthMenu.tsx index 2244bc4c6af..30016e9ce05 100644 --- a/packages/desktop-client/src/components/budget/envelope/budgetsummary/BudgetMonthMenu.tsx +++ b/packages/desktop-client/src/components/budget/envelope/budgetsummary/BudgetMonthMenu.tsx @@ -1,8 +1,9 @@ import React, { type ComponentPropsWithoutRef } from 'react'; import { useTranslation } from 'react-i18next'; +import { Menu } from '@actual-app/components/menu'; + import { useFeatureFlag } from '../../../../hooks/useFeatureFlag'; -import { Menu } from '../../../common/Menu'; type BudgetMonthMenuProps = Omit< ComponentPropsWithoutRef, diff --git a/packages/desktop-client/src/components/budget/envelope/budgetsummary/BudgetSummary.tsx b/packages/desktop-client/src/components/budget/envelope/budgetsummary/BudgetSummary.tsx index 21888cca50a..b8347ddc67d 100644 --- a/packages/desktop-client/src/components/budget/envelope/budgetsummary/BudgetSummary.tsx +++ b/packages/desktop-client/src/components/budget/envelope/budgetsummary/BudgetSummary.tsx @@ -1,6 +1,10 @@ import React, { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Popover } from '@actual-app/components/popover'; +import { styles } from '@actual-app/components/styles'; +import { View } from '@actual-app/components/view'; import { css } from '@emotion/css'; import * as monthUtils from 'loot-core/shared/months'; @@ -8,10 +12,7 @@ import * as monthUtils from 'loot-core/shared/months'; import { useUndo } from '../../../../hooks/useUndo'; import { SvgDotsHorizontalTriple } from '../../../../icons/v1'; import { SvgArrowButtonDown1, SvgArrowButtonUp1 } from '../../../../icons/v2'; -import { theme, styles } from '../../../../style'; -import { Button } from '../../../common/Button2'; -import { Popover } from '../../../common/Popover'; -import { View } from '../../../common/View'; +import { theme } from '../../../../style'; import { NotesButton } from '../../../NotesButton'; import { NamespaceContext } from '../../../spreadsheet/NamespaceContext'; import { useEnvelopeBudget } from '../EnvelopeBudgetContext'; diff --git a/packages/desktop-client/src/components/budget/envelope/budgetsummary/ToBudget.tsx b/packages/desktop-client/src/components/budget/envelope/budgetsummary/ToBudget.tsx index 42f32fac392..c3b788392de 100644 --- a/packages/desktop-client/src/components/budget/envelope/budgetsummary/ToBudget.tsx +++ b/packages/desktop-client/src/components/budget/envelope/budgetsummary/ToBudget.tsx @@ -5,11 +5,12 @@ import React, { useState, } from 'react'; +import { Popover } from '@actual-app/components/popover'; +import { View } from '@actual-app/components/view'; + import { envelopeBudget } from 'loot-core/client/queries'; import { useContextMenu } from '../../../../hooks/useContextMenu'; -import { Popover } from '../../../common/Popover'; -import { View } from '../../../common/View'; import { CoverMenu } from '../CoverMenu'; import { useEnvelopeSheetValue } from '../EnvelopeBudgetComponents'; import { HoldMenu } from '../HoldMenu'; diff --git a/packages/desktop-client/src/components/budget/envelope/budgetsummary/ToBudgetMenu.tsx b/packages/desktop-client/src/components/budget/envelope/budgetsummary/ToBudgetMenu.tsx index 3dd9b4a6857..3cb15da8dd2 100644 --- a/packages/desktop-client/src/components/budget/envelope/budgetsummary/ToBudgetMenu.tsx +++ b/packages/desktop-client/src/components/budget/envelope/budgetsummary/ToBudgetMenu.tsx @@ -1,9 +1,10 @@ import React, { type ComponentPropsWithoutRef } from 'react'; import { useTranslation } from 'react-i18next'; +import { Menu } from '@actual-app/components/menu'; + import { envelopeBudget } from 'loot-core/client/queries'; -import { Menu } from '../../../common/Menu'; import { useEnvelopeSheetValue } from '../EnvelopeBudgetComponents'; type ToBudgetMenuProps = Omit< 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 eae77e5eedc..07957345c64 100644 --- a/packages/desktop-client/src/components/budget/envelope/budgetsummary/TotalsList.tsx +++ b/packages/desktop-client/src/components/budget/envelope/budgetsummary/TotalsList.tsx @@ -1,4 +1,5 @@ import React, { type CSSProperties } from 'react'; +import { Trans } from 'react-i18next'; import { AlignedText } from '@actual-app/components/aligned-text'; import { Block } from '@actual-app/components/block'; @@ -120,10 +121,21 @@ export function TotalsList({ prevMonthName, style }: TotalsListProps) { - Available funds - Overspent in {prevMonthName} - Budgeted - For next month + + Available funds + + + + Overspent in {{ prevMonthName }} + + + + Budgeted + + + + For next month + ); diff --git a/packages/desktop-client/src/components/budget/goals/CategoryAutomationButton.tsx b/packages/desktop-client/src/components/budget/goals/CategoryAutomationButton.tsx new file mode 100644 index 00000000000..9aee1625b3b --- /dev/null +++ b/packages/desktop-client/src/components/budget/goals/CategoryAutomationButton.tsx @@ -0,0 +1,54 @@ +import React, { type CSSProperties } from 'react'; + +import { Button } from '@actual-app/components/button'; +import { theme } from '@actual-app/components/theme'; + +import { pushModal } from 'loot-core/client/actions'; +import { type Template } from 'loot-core/server/budget/types/templates'; + +import { useFeatureFlag } from '../../../hooks/useFeatureFlag'; +import { SvgChartPie } from '../../../icons/v1'; +import { useDispatch } from '../../../redux'; + +type CategoryAutomationButtonProps = { + width?: number; + height?: number; + defaultColor?: string; + style?: CSSProperties; +}; +export function CategoryAutomationButton({ + width = 12, + height = 12, + defaultColor = theme.buttonNormalText, + style, +}: CategoryAutomationButtonProps) { + const automations: Template[] = []; + const hasAutomations = !!automations.length; + + const dispatch = useDispatch(); + + const goalTemplatesEnabled = useFeatureFlag('goalTemplatesEnabled'); + const goalTemplatesUIEnabled = useFeatureFlag('goalTemplatesUIEnabled'); + + if (!goalTemplatesEnabled || !goalTemplatesUIEnabled) { + return null; + } + + return ( + + ); +} diff --git a/packages/desktop-client/src/components/budget/index.tsx b/packages/desktop-client/src/components/budget/index.tsx index 04778aa2937..e62b0de395c 100644 --- a/packages/desktop-client/src/components/budget/index.tsx +++ b/packages/desktop-client/src/components/budget/index.tsx @@ -2,6 +2,9 @@ import React, { memo, useMemo, useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; +import { styles } from '@actual-app/components/styles'; +import { View } from '@actual-app/components/view'; + import { addNotification, pushModal } from 'loot-core/client/actions'; import { applyBudgetAction, @@ -25,8 +28,6 @@ import { useLocalPref } from '../../hooks/useLocalPref'; import { useNavigate } from '../../hooks/useNavigate'; import { useSyncedPref } from '../../hooks/useSyncedPref'; import { useDispatch } from '../../redux'; -import { styles } from '../../style'; -import { View } from '../common/View'; import { NamespaceContext } from '../spreadsheet/NamespaceContext'; import { DynamicBudgetTable } from './DynamicBudgetTable'; diff --git a/packages/desktop-client/src/components/budget/tracking/BalanceMenu.tsx b/packages/desktop-client/src/components/budget/tracking/BalanceMenu.tsx index 22e3f5446bf..d5e39f3fce5 100644 --- a/packages/desktop-client/src/components/budget/tracking/BalanceMenu.tsx +++ b/packages/desktop-client/src/components/budget/tracking/BalanceMenu.tsx @@ -1,9 +1,9 @@ import React, { type ComponentPropsWithoutRef } from 'react'; import { useTranslation } from 'react-i18next'; -import { trackingBudget } from 'loot-core/client/queries'; +import { Menu } from '@actual-app/components/menu'; -import { Menu } from '../../common/Menu'; +import { trackingBudget } from 'loot-core/client/queries'; import { useTrackingSheetValue } from './TrackingBudgetComponents'; diff --git a/packages/desktop-client/src/components/budget/tracking/BudgetMenu.tsx b/packages/desktop-client/src/components/budget/tracking/BudgetMenu.tsx index b5f2f38c265..7144f409919 100644 --- a/packages/desktop-client/src/components/budget/tracking/BudgetMenu.tsx +++ b/packages/desktop-client/src/components/budget/tracking/BudgetMenu.tsx @@ -1,8 +1,9 @@ import React, { type ComponentPropsWithoutRef } from 'react'; import { useTranslation } from 'react-i18next'; +import { Menu } from '@actual-app/components/menu'; + import { useFeatureFlag } from '../../../hooks/useFeatureFlag'; -import { Menu } from '../../common/Menu'; type BudgetMenuProps = Omit< ComponentPropsWithoutRef, diff --git a/packages/desktop-client/src/components/budget/tracking/TrackingBudgetComponents.tsx b/packages/desktop-client/src/components/budget/tracking/TrackingBudgetComponents.tsx index 9bf9e9e536f..3afc6948e00 100644 --- a/packages/desktop-client/src/components/budget/tracking/TrackingBudgetComponents.tsx +++ b/packages/desktop-client/src/components/budget/tracking/TrackingBudgetComponents.tsx @@ -8,6 +8,11 @@ import React, { } from 'react'; import { Trans } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Popover } from '@actual-app/components/popover'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; import { css } from '@emotion/css'; import { trackingBudget } from 'loot-core/client/queries'; @@ -17,11 +22,7 @@ import { integerToCurrency, amountToInteger } from 'loot-core/shared/util'; import { useUndo } from '../../../hooks/useUndo'; import { SvgCheveronDown } from '../../../icons/v1'; -import { styles, theme } from '../../../style'; -import { Button } from '../../common/Button2'; -import { Popover } from '../../common/Popover'; -import { Text } from '../../common/Text'; -import { View } from '../../common/View'; +import { theme } from '../../../style'; import { type Binding, type SheetFields } from '../../spreadsheet'; import { CellValue, CellValueText } from '../../spreadsheet/CellValue'; import { useSheetValue } from '../../spreadsheet/useSheetValue'; diff --git a/packages/desktop-client/src/components/budget/tracking/budgetsummary/BudgetMonthMenu.tsx b/packages/desktop-client/src/components/budget/tracking/budgetsummary/BudgetMonthMenu.tsx index d4b5e4c9cad..d1f941b21c4 100644 --- a/packages/desktop-client/src/components/budget/tracking/budgetsummary/BudgetMonthMenu.tsx +++ b/packages/desktop-client/src/components/budget/tracking/budgetsummary/BudgetMonthMenu.tsx @@ -1,8 +1,9 @@ import React, { type ComponentPropsWithoutRef } from 'react'; import { useTranslation } from 'react-i18next'; +import { Menu } from '@actual-app/components/menu'; + import { useFeatureFlag } from '../../../../hooks/useFeatureFlag'; -import { Menu } from '../../../common/Menu'; type BudgetMonthMenuProps = Omit< ComponentPropsWithoutRef, diff --git a/packages/desktop-client/src/components/budget/tracking/budgetsummary/BudgetSummary.tsx b/packages/desktop-client/src/components/budget/tracking/budgetsummary/BudgetSummary.tsx index 8df1ec15045..16eef91be3a 100644 --- a/packages/desktop-client/src/components/budget/tracking/budgetsummary/BudgetSummary.tsx +++ b/packages/desktop-client/src/components/budget/tracking/budgetsummary/BudgetSummary.tsx @@ -2,6 +2,11 @@ import React, { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Popover } from '@actual-app/components/popover'; +import { Stack } from '@actual-app/components/stack'; +import { styles } from '@actual-app/components/styles'; +import { View } from '@actual-app/components/view'; import { css } from '@emotion/css'; import * as monthUtils from 'loot-core/shared/months'; @@ -9,11 +14,7 @@ import * as monthUtils from 'loot-core/shared/months'; import { useUndo } from '../../../../hooks/useUndo'; import { SvgDotsHorizontalTriple } from '../../../../icons/v1'; import { SvgArrowButtonDown1, SvgArrowButtonUp1 } from '../../../../icons/v2'; -import { theme, styles } from '../../../../style'; -import { Button } from '../../../common/Button2'; -import { Popover } from '../../../common/Popover'; -import { Stack } from '../../../common/Stack'; -import { View } from '../../../common/View'; +import { theme } from '../../../../style'; import { NotesButton } from '../../../NotesButton'; import { NamespaceContext } from '../../../spreadsheet/NamespaceContext'; import { useTrackingBudget } from '../TrackingBudgetContext'; diff --git a/packages/desktop-client/src/components/budget/tracking/budgetsummary/BudgetTotal.tsx b/packages/desktop-client/src/components/budget/tracking/budgetsummary/BudgetTotal.tsx index 7e65d730e25..a5da04fa886 100644 --- a/packages/desktop-client/src/components/budget/tracking/budgetsummary/BudgetTotal.tsx +++ b/packages/desktop-client/src/components/budget/tracking/budgetsummary/BudgetTotal.tsx @@ -6,9 +6,11 @@ import React, { } from 'react'; import { Trans } from 'react-i18next'; -import { theme, styles } from '../../../../style'; -import { Text } from '../../../common/Text'; -import { View } from '../../../common/View'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + +import { theme } from '../../../../style'; import { type SheetFields, type Binding } from '../../../spreadsheet'; import { CellValue, CellValueText } from '../../../spreadsheet/CellValue'; diff --git a/packages/desktop-client/src/components/budget/util.ts b/packages/desktop-client/src/components/budget/util.ts index 679d5e0c600..7d0bde3f547 100644 --- a/packages/desktop-client/src/components/budget/util.ts +++ b/packages/desktop-client/src/components/budget/util.ts @@ -1,6 +1,7 @@ // @ts-strict-ignore import { type CSSProperties } from 'react'; +import { styles } from '@actual-app/components/styles'; import { t } from 'i18next'; import { type useSpreadsheet } from 'loot-core/client/SpreadsheetProvider'; @@ -13,7 +14,7 @@ import { } from 'loot-core/types/models'; import { type SyncedPrefs } from 'loot-core/types/prefs'; -import { styles, theme } from '../../style'; +import { theme } from '../../style'; import { type DropPosition } from '../sort'; import { getValidMonthBounds } from './MonthsContext'; diff --git a/packages/desktop-client/src/components/common/Button2.ts b/packages/desktop-client/src/components/common/Button2.ts deleted file mode 100644 index 7871daccf4d..00000000000 --- a/packages/desktop-client/src/components/common/Button2.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { - Button as ActualComponentButton, - ButtonWithLoading as ActualComponentButtonWithLoading, -} from '@actual-app/components/button'; - -/** @deprecated please import Button from '@actual-app/components/button' */ -export const Button = ActualComponentButton; - -/** @deprecated please import ButtonWithLoading from '@actual-app/components/button' */ -export const ButtonWithLoading = ActualComponentButtonWithLoading; diff --git a/packages/desktop-client/src/components/common/Card.ts b/packages/desktop-client/src/components/common/Card.ts deleted file mode 100644 index ab9663033fa..00000000000 --- a/packages/desktop-client/src/components/common/Card.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Card as ActualComponentCard } from '@actual-app/components/card'; - -/** @deprecated please import Card from '@actual-app/components/card' */ -export const Card = ActualComponentCard; diff --git a/packages/desktop-client/src/components/common/FormError.ts b/packages/desktop-client/src/components/common/FormError.ts deleted file mode 100644 index 81f37afb588..00000000000 --- a/packages/desktop-client/src/components/common/FormError.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { FormError as ActualComponentFormError } from '@actual-app/components/form-error'; - -/** @deprecated please import FormError from '@actual-app/components/form-error' */ -export const FormError = ActualComponentFormError; diff --git a/packages/desktop-client/src/components/common/InfoBubble.tsx b/packages/desktop-client/src/components/common/InfoBubble.tsx index d28879d80e0..69d7f271e35 100644 --- a/packages/desktop-client/src/components/common/InfoBubble.tsx +++ b/packages/desktop-client/src/components/common/InfoBubble.tsx @@ -1,12 +1,12 @@ import React, { useState } from 'react'; import { useLocation } from 'react-router-dom'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { SvgInformationCircle } from '../../icons/v2'; import { theme } from '../../style'; -import { Text } from './Text'; -import { View } from './View'; - type InfoBubbleProps = { label: string; textWidth?: number; diff --git a/packages/desktop-client/src/components/common/InitialFocus.ts b/packages/desktop-client/src/components/common/InitialFocus.ts deleted file mode 100644 index 759add53a52..00000000000 --- a/packages/desktop-client/src/components/common/InitialFocus.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { InitialFocus as ActualComponentInitialFocus } from '@actual-app/components/initial-focus'; - -/** @deprecated please import InitialFocus from '@actual-app/components/initial-focus' */ -export const InitialFocus = ActualComponentInitialFocus; diff --git a/packages/desktop-client/src/components/common/InlineField.ts b/packages/desktop-client/src/components/common/InlineField.ts deleted file mode 100644 index 24019276be1..00000000000 --- a/packages/desktop-client/src/components/common/InlineField.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { InlineField as ActualComponentInlineField } from '@actual-app/components/inline-field'; - -/** @deprecated please import InlineField from '@actual-app/components/inline-field' */ -export const InlineField = ActualComponentInlineField; diff --git a/packages/desktop-client/src/components/common/Input.tsx b/packages/desktop-client/src/components/common/Input.tsx index fa915aa0884..158fb7251a5 100644 --- a/packages/desktop-client/src/components/common/Input.tsx +++ b/packages/desktop-client/src/components/common/Input.tsx @@ -5,11 +5,12 @@ import React, { useRef, } from 'react'; +import { styles } from '@actual-app/components/styles'; import { css, cx } from '@emotion/css'; import { useMergedRefs } from '../../hooks/useMergedRefs'; import { useProperFocus } from '../../hooks/useProperFocus'; -import { styles, theme, type CSSProperties } from '../../style'; +import { theme, type CSSProperties } from '../../style'; export const defaultInputStyle = { outline: 0, @@ -54,6 +55,9 @@ export function Input({ css( defaultInputStyle, { + color: nativeProps.disabled + ? theme.formInputTextPlaceholder + : theme.formInputText, whiteSpace: 'nowrap', overflow: 'hidden', flexShrink: 0, diff --git a/packages/desktop-client/src/components/common/InputWithContent.tsx b/packages/desktop-client/src/components/common/InputWithContent.tsx index 15f17394faa..6c60398bf83 100644 --- a/packages/desktop-client/src/components/common/InputWithContent.tsx +++ b/packages/desktop-client/src/components/common/InputWithContent.tsx @@ -1,9 +1,10 @@ import { useState, type ComponentProps, type ReactNode } from 'react'; +import { View } from '@actual-app/components/view'; + import { theme, type CSSProperties } from '../../style'; import { Input, defaultInputStyle } from './Input'; -import { View } from './View'; type InputWithContentProps = ComponentProps & { leftContent?: ReactNode; diff --git a/packages/desktop-client/src/components/common/Label.ts b/packages/desktop-client/src/components/common/Label.ts deleted file mode 100644 index c799cb06bbb..00000000000 --- a/packages/desktop-client/src/components/common/Label.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Label as ActualComponentLabel } from '@actual-app/components/label'; - -/** @deprecated please import Label from '@actual-app/components/label' */ -export const Label = ActualComponentLabel; diff --git a/packages/desktop-client/src/components/common/Link.tsx b/packages/desktop-client/src/components/common/Link.tsx index 00225844537..d79c8ddc03a 100644 --- a/packages/desktop-client/src/components/common/Link.tsx +++ b/packages/desktop-client/src/components/common/Link.tsx @@ -5,15 +5,15 @@ import React, { } from 'react'; import { NavLink, useMatch } from 'react-router-dom'; +import { Button } from '@actual-app/components/button'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; import { css } from '@emotion/css'; import { useNavigate } from '../../hooks/useNavigate'; -import { type CSSProperties, styles } from '../../style'; +import { type CSSProperties } from '../../style'; import { theme } from '../../style/theme'; -import { Button } from './Button2'; -import { Text } from './Text'; - type TextLinkProps = { style?: CSSProperties; onClick?: MouseEventHandler; diff --git a/packages/desktop-client/src/components/common/Menu.ts b/packages/desktop-client/src/components/common/Menu.ts deleted file mode 100644 index d736353e625..00000000000 --- a/packages/desktop-client/src/components/common/Menu.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { - Menu as ActualComponentMenu, - type MenuItem as ActualComponentMenuItem, -} from '@actual-app/components/menu'; - -/** @deprecated please import Menu from '@actual-app/components/menu' */ -export const Menu = ActualComponentMenu; - -/** @deprecated please import MenuItem from '@actual-app/components/menu' */ -export type MenuItem = ActualComponentMenuItem; diff --git a/packages/desktop-client/src/components/common/MenuButton.tsx b/packages/desktop-client/src/components/common/MenuButton.tsx index bf55a955cc5..acb763cab44 100644 --- a/packages/desktop-client/src/components/common/MenuButton.tsx +++ b/packages/desktop-client/src/components/common/MenuButton.tsx @@ -1,8 +1,8 @@ import React, { type ComponentPropsWithoutRef, forwardRef } from 'react'; -import { SvgDotsHorizontalTriple } from '../../icons/v1'; +import { Button } from '@actual-app/components/button'; -import { Button } from './Button2'; +import { SvgDotsHorizontalTriple } from '../../icons/v1'; type MenuButtonProps = ComponentPropsWithoutRef; diff --git a/packages/desktop-client/src/components/common/Paragraph.ts b/packages/desktop-client/src/components/common/Paragraph.ts deleted file mode 100644 index a69bd9c880c..00000000000 --- a/packages/desktop-client/src/components/common/Paragraph.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Paragraph as ActualComponentParagraph } from '@actual-app/components/paragraph'; - -/** @deprecated please import Paragraph from '@actual-app/components/paragraph' */ -export const Paragraph = ActualComponentParagraph; diff --git a/packages/desktop-client/src/components/common/Popover.ts b/packages/desktop-client/src/components/common/Popover.ts deleted file mode 100644 index c885af49d00..00000000000 --- a/packages/desktop-client/src/components/common/Popover.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Popover as ActualComponentPopover } from '@actual-app/components/popover'; - -/** @deprecated please import Popover from '@actual-app/components/popover' */ -export const Popover = ActualComponentPopover; diff --git a/packages/desktop-client/src/components/common/Search.tsx b/packages/desktop-client/src/components/common/Search.tsx index dd154c80bd8..6a90f74d0e3 100644 --- a/packages/desktop-client/src/components/common/Search.tsx +++ b/packages/desktop-client/src/components/common/Search.tsx @@ -1,12 +1,13 @@ import { type Ref } from 'react'; import { useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { View } from '@actual-app/components/view'; + import { SvgRemove, SvgSearchAlternate } from '../../icons/v2'; import { theme } from '../../style'; -import { Button } from './Button2'; import { InputWithContent } from './InputWithContent'; -import { View } from './View'; type SearchProps = { inputRef?: Ref; diff --git a/packages/desktop-client/src/components/common/Select.tsx b/packages/desktop-client/src/components/common/Select.tsx index f30dd3b3199..7f438eccc00 100644 --- a/packages/desktop-client/src/components/common/Select.tsx +++ b/packages/desktop-client/src/components/common/Select.tsx @@ -1,11 +1,11 @@ import { useRef, useState, type CSSProperties } from 'react'; -import { SvgExpandArrow } from '../../icons/v0'; +import { Button } from '@actual-app/components/button'; +import { Menu } from '@actual-app/components/menu'; +import { Popover } from '@actual-app/components/popover'; +import { View } from '@actual-app/components/view'; -import { Button } from './Button2'; -import { Menu } from './Menu'; -import { Popover } from './Popover'; -import { View } from './View'; +import { SvgExpandArrow } from '../../icons/v0'; function isValueOption( option: readonly [Value, string] | typeof Menu.line, diff --git a/packages/desktop-client/src/components/common/SimpleTable.tsx b/packages/desktop-client/src/components/common/SimpleTable.tsx index 6539d7655c0..fc059ba8385 100644 --- a/packages/desktop-client/src/components/common/SimpleTable.tsx +++ b/packages/desktop-client/src/components/common/SimpleTable.tsx @@ -6,7 +6,7 @@ import React, { type CSSProperties, } from 'react'; -import { View } from './View'; +import { View } from '@actual-app/components/view'; type SimpleTableProps = { loadMore?: () => void; diff --git a/packages/desktop-client/src/components/common/SpaceBetween.ts b/packages/desktop-client/src/components/common/SpaceBetween.ts deleted file mode 100644 index d272bf9a324..00000000000 --- a/packages/desktop-client/src/components/common/SpaceBetween.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { SpaceBetween as ActualComponentSpaceBetween } from '@actual-app/components/space-between'; - -/** @deprecated please import SpaceBetween from '@actual-app/components/space-between' */ -export const SpaceBetween = ActualComponentSpaceBetween; diff --git a/packages/desktop-client/src/components/common/Stack.ts b/packages/desktop-client/src/components/common/Stack.ts deleted file mode 100644 index e2a2f9b561f..00000000000 --- a/packages/desktop-client/src/components/common/Stack.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Stack as ActualComponentStack } from '@actual-app/components/stack'; - -/** @deprecated please import Stack from '@actual-app/components/stack' */ -export const Stack = ActualComponentStack; diff --git a/packages/desktop-client/src/components/common/Text.ts b/packages/desktop-client/src/components/common/Text.ts deleted file mode 100644 index 74a1dcead77..00000000000 --- a/packages/desktop-client/src/components/common/Text.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Text as ActualComponentText } from '@actual-app/components/text'; - -/** @deprecated please import Text from '@actual-app/components/text' */ -export const Text = ActualComponentText; diff --git a/packages/desktop-client/src/components/common/TextOneLine.ts b/packages/desktop-client/src/components/common/TextOneLine.ts deleted file mode 100644 index 3f09be34873..00000000000 --- a/packages/desktop-client/src/components/common/TextOneLine.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { TextOneLine as ActualComponentTextOneLine } from '@actual-app/components/text-one-line'; - -/** @deprecated please import TextOneLine from '@actual-app/components/text-one-line' */ -export const TextOneLine = ActualComponentTextOneLine; diff --git a/packages/desktop-client/src/components/common/Toggle.ts b/packages/desktop-client/src/components/common/Toggle.ts deleted file mode 100644 index 1e244e2c863..00000000000 --- a/packages/desktop-client/src/components/common/Toggle.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Toggle as ActualComponentToggle } from '@actual-app/components/toggle'; - -/** @deprecated please import Toggle from '@actual-app/components/toggle' */ -export const Toggle = ActualComponentToggle; diff --git a/packages/desktop-client/src/components/common/Tooltip.ts b/packages/desktop-client/src/components/common/Tooltip.ts deleted file mode 100644 index 88c52e79957..00000000000 --- a/packages/desktop-client/src/components/common/Tooltip.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Tooltip as ActualComponentTooltip } from '@actual-app/components/tooltip'; - -/** @deprecated please import Tooltip from '@actual-app/components/tooltip' */ -export const Tooltip = ActualComponentTooltip; diff --git a/packages/desktop-client/src/components/common/View.ts b/packages/desktop-client/src/components/common/View.ts deleted file mode 100644 index 1de101a9a21..00000000000 --- a/packages/desktop-client/src/components/common/View.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { View as ActualComponentView } from '@actual-app/components/view'; - -/** @deprecated please import View from '@actual-app/components/view' */ -export const View = ActualComponentView; diff --git a/packages/desktop-client/src/components/filters/AppliedFilters.tsx b/packages/desktop-client/src/components/filters/AppliedFilters.tsx index c1fa10aaf70..3f0ff1cb694 100644 --- a/packages/desktop-client/src/components/filters/AppliedFilters.tsx +++ b/packages/desktop-client/src/components/filters/AppliedFilters.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { type RuleConditionEntity } from 'loot-core/types/models'; +import { View } from '@actual-app/components/view'; -import { View } from '../common/View'; +import { type RuleConditionEntity } from 'loot-core/types/models'; import { ConditionsOpMenu } from './ConditionsOpMenu'; import { FilterExpression } from './FilterExpression'; diff --git a/packages/desktop-client/src/components/filters/CompactFiltersButton.tsx b/packages/desktop-client/src/components/filters/CompactFiltersButton.tsx index a9b1abe04fa..f94000ec147 100644 --- a/packages/desktop-client/src/components/filters/CompactFiltersButton.tsx +++ b/packages/desktop-client/src/components/filters/CompactFiltersButton.tsx @@ -1,7 +1,8 @@ import React from 'react'; +import { Button } from '@actual-app/components/button'; + import { SvgFilter } from '../../icons/v1'; -import { Button } from '../common/Button2'; export function CompactFiltersButton({ onPress }: { onPress: () => void }) { return ( diff --git a/packages/desktop-client/src/components/filters/ConditionsOpMenu.tsx b/packages/desktop-client/src/components/filters/ConditionsOpMenu.tsx index 41bf5be269b..e326c023dd8 100644 --- a/packages/desktop-client/src/components/filters/ConditionsOpMenu.tsx +++ b/packages/desktop-client/src/components/filters/ConditionsOpMenu.tsx @@ -1,10 +1,11 @@ import React from 'react'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { type RuleConditionEntity } from 'loot-core/types/models'; import { theme } from '../../style'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; import { FieldSelect } from '../modals/EditRuleModal'; export function ConditionsOpMenu({ diff --git a/packages/desktop-client/src/components/filters/FilterExpression.tsx b/packages/desktop-client/src/components/filters/FilterExpression.tsx index 47f2d9fd5c0..7906a6737d0 100644 --- a/packages/desktop-client/src/components/filters/FilterExpression.tsx +++ b/packages/desktop-client/src/components/filters/FilterExpression.tsx @@ -1,16 +1,17 @@ import React, { useRef, useState, type CSSProperties } from 'react'; import { useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Popover } from '@actual-app/components/popover'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { mapField, friendlyOp } from 'loot-core/shared/rules'; import { integerToCurrency } from 'loot-core/shared/util'; import { type RuleConditionEntity } from 'loot-core/types/models'; import { SvgDelete } from '../../icons/v0'; import { theme } from '../../style'; -import { Button } from '../common/Button2'; -import { Popover } from '../common/Popover'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; import { Value } from '../rules/Value'; import { FilterEditor } from './FiltersMenu'; diff --git a/packages/desktop-client/src/components/filters/FilterMenu.tsx b/packages/desktop-client/src/components/filters/FilterMenu.tsx index 94d389998c7..ea2e12dc7b7 100644 --- a/packages/desktop-client/src/components/filters/FilterMenu.tsx +++ b/packages/desktop-client/src/components/filters/FilterMenu.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Menu } from '../common/Menu'; +import { Menu } from '@actual-app/components/menu'; import { type SavedFilter } from './SavedFilterMenuButton'; diff --git a/packages/desktop-client/src/components/filters/FiltersButton.tsx b/packages/desktop-client/src/components/filters/FiltersButton.tsx index 1568986f0a8..91c78535c12 100644 --- a/packages/desktop-client/src/components/filters/FiltersButton.tsx +++ b/packages/desktop-client/src/components/filters/FiltersButton.tsx @@ -1,7 +1,8 @@ import React from 'react'; +import { Button } from '@actual-app/components/button'; + import { SvgFilter } from '../../icons/v1/Filter'; -import { Button } from '../common/Button2'; export function FiltersButton({ onPress }: { onPress: () => void }) { return ( diff --git a/packages/desktop-client/src/components/filters/FiltersMenu.jsx b/packages/desktop-client/src/components/filters/FiltersMenu.jsx index d0c80f27595..941bf949492 100644 --- a/packages/desktop-client/src/components/filters/FiltersMenu.jsx +++ b/packages/desktop-client/src/components/filters/FiltersMenu.jsx @@ -4,6 +4,14 @@ import { Form } from 'react-aria-components'; import { useHotkeys } from 'react-hotkeys-hook'; import { Trans, useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Menu } from '@actual-app/components/menu'; +import { Popover } from '@actual-app/components/popover'; +import { Stack } from '@actual-app/components/stack'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { Tooltip } from '@actual-app/components/tooltip'; +import { View } from '@actual-app/components/view'; import { parse as parseDate, format as formatDate, @@ -24,15 +32,8 @@ import { import { titleFirst } from 'loot-core/shared/util'; import { useDateFormat } from '../../hooks/useDateFormat'; -import { styles, theme } from '../../style'; -import { Button } from '../common/Button2'; -import { Menu } from '../common/Menu'; -import { Popover } from '../common/Popover'; +import { theme } from '../../style'; import { Select } from '../common/Select'; -import { Stack } from '../common/Stack'; -import { Text } from '../common/Text'; -import { Tooltip } from '../common/Tooltip'; -import { View } from '../common/View'; import { GenericInput } from '../util/GenericInput'; import { CompactFiltersButton } from './CompactFiltersButton'; diff --git a/packages/desktop-client/src/components/filters/FiltersStack.tsx b/packages/desktop-client/src/components/filters/FiltersStack.tsx index f145c786864..2b021b86d4b 100644 --- a/packages/desktop-client/src/components/filters/FiltersStack.tsx +++ b/packages/desktop-client/src/components/filters/FiltersStack.tsx @@ -1,11 +1,11 @@ import React from 'react'; +import { Stack } from '@actual-app/components/stack'; +import { View } from '@actual-app/components/view'; + import { type TransactionFilterEntity } from 'loot-core/types/models'; import { type RuleConditionEntity } from 'loot-core/types/models/rule'; -import { Stack } from '../common/Stack'; -import { View } from '../common/View'; - import { AppliedFilters } from './AppliedFilters'; import { type SavedFilter, diff --git a/packages/desktop-client/src/components/filters/NameFilter.tsx b/packages/desktop-client/src/components/filters/NameFilter.tsx index 827b15fe860..336bf6be247 100644 --- a/packages/desktop-client/src/components/filters/NameFilter.tsx +++ b/packages/desktop-client/src/components/filters/NameFilter.tsx @@ -2,11 +2,12 @@ import React, { useRef, useEffect } from 'react'; import { Form } from 'react-aria-components'; import { useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Stack } from '@actual-app/components/stack'; +import { Text } from '@actual-app/components/text'; + import { theme } from '../../style'; -import { Button } from '../common/Button2'; import { Input } from '../common/Input'; -import { Stack } from '../common/Stack'; -import { Text } from '../common/Text'; import { FormField, FormLabel } from '../forms'; export function NameFilter({ diff --git a/packages/desktop-client/src/components/filters/OpButton.tsx b/packages/desktop-client/src/components/filters/OpButton.tsx index 0afac5deaf2..19a8a457532 100644 --- a/packages/desktop-client/src/components/filters/OpButton.tsx +++ b/packages/desktop-client/src/components/filters/OpButton.tsx @@ -1,11 +1,11 @@ import React, { type CSSProperties } from 'react'; +import { Button } from '@actual-app/components/button'; import { css } from '@emotion/css'; import { friendlyOp } from 'loot-core/shared/rules'; import { theme } from '../../style'; -import { Button } from '../common/Button2'; type OpButtonProps = { op: string; diff --git a/packages/desktop-client/src/components/filters/SavedFilterMenuButton.tsx b/packages/desktop-client/src/components/filters/SavedFilterMenuButton.tsx index b51be581bec..57e6633d430 100644 --- a/packages/desktop-client/src/components/filters/SavedFilterMenuButton.tsx +++ b/packages/desktop-client/src/components/filters/SavedFilterMenuButton.tsx @@ -1,15 +1,16 @@ import React, { useRef, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Popover } from '@actual-app/components/popover'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { send, sendCatch } from 'loot-core/platform/client/fetch'; import { type TransactionFilterEntity } from 'loot-core/types/models'; import { type RuleConditionEntity } from 'loot-core/types/models/rule'; import { SvgExpandArrow } from '../../icons/v0'; -import { Button } from '../common/Button2'; -import { Popover } from '../common/Popover'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; import { FilterMenu } from './FilterMenu'; import { NameFilter } from './NameFilter'; diff --git a/packages/desktop-client/src/components/forms.tsx b/packages/desktop-client/src/components/forms.tsx index 439acfae812..c86688e07bb 100644 --- a/packages/desktop-client/src/components/forms.tsx +++ b/packages/desktop-client/src/components/forms.tsx @@ -1,12 +1,11 @@ import React, { type ReactNode, type ComponentProps } from 'react'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; import { css } from '@emotion/css'; import { theme, type CSSProperties } from '../style'; -import { Text } from './common/Text'; -import { View } from './common/View'; - type SectionLabelProps = { title?: string; style?: CSSProperties; diff --git a/packages/desktop-client/src/components/gocardless/GoCardlessLink.tsx b/packages/desktop-client/src/components/gocardless/GoCardlessLink.tsx index 2a3312242e9..14e0a59b7df 100644 --- a/packages/desktop-client/src/components/gocardless/GoCardlessLink.tsx +++ b/packages/desktop-client/src/components/gocardless/GoCardlessLink.tsx @@ -1,9 +1,10 @@ import React from 'react'; import { Trans } from 'react-i18next'; +import { Paragraph } from '@actual-app/components/paragraph'; +import { View } from '@actual-app/components/view'; + import { Modal, ModalHeader } from '../common/Modal'; -import { Paragraph } from '../common/Paragraph'; -import { View } from '../common/View'; export function GoCardlessLink() { window.close(); diff --git a/packages/desktop-client/src/components/manager/BudgetList.tsx b/packages/desktop-client/src/components/manager/BudgetList.tsx index 5bcc4b123f4..0925434582a 100644 --- a/packages/desktop-client/src/components/manager/BudgetList.tsx +++ b/packages/desktop-client/src/components/manager/BudgetList.tsx @@ -7,6 +7,14 @@ import React, { } from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Menu } from '@actual-app/components/menu'; +import { Popover } from '@actual-app/components/popover'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { Tooltip } from '@actual-app/components/tooltip'; +import { View } from '@actual-app/components/view'; + import { closeAndDownloadBudget, closeAndLoadBudget, @@ -43,14 +51,8 @@ import { } from '../../icons/v1'; import { SvgCloudUnknown, SvgKey, SvgRefreshArrow } from '../../icons/v2'; import { useSelector, useDispatch } from '../../redux'; -import { styles, theme } from '../../style'; +import { theme } from '../../style'; import { tokens } from '../../tokens'; -import { Button } from '../common/Button2'; -import { Menu } from '../common/Menu'; -import { Popover } from '../common/Popover'; -import { Text } from '../common/Text'; -import { Tooltip } from '../common/Tooltip'; -import { View } from '../common/View'; import { useResponsive } from '../responsive/ResponsiveProvider'; import { useMultiuserEnabled } from '../ServerContext'; diff --git a/packages/desktop-client/src/components/manager/ConfigServer.tsx b/packages/desktop-client/src/components/manager/ConfigServer.tsx index 393ecdea51b..681e10e151c 100644 --- a/packages/desktop-client/src/components/manager/ConfigServer.tsx +++ b/packages/desktop-client/src/components/manager/ConfigServer.tsx @@ -2,6 +2,10 @@ import React, { useState, useEffect, useCallback } from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { Button, ButtonWithLoading } from '@actual-app/components/button'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { createBudget, loggedIn, signOut } from 'loot-core/client/actions'; import { isNonProductionEnvironment, @@ -12,11 +16,8 @@ import { useGlobalPref } from '../../hooks/useGlobalPref'; import { useNavigate } from '../../hooks/useNavigate'; import { useDispatch } from '../../redux'; import { theme } from '../../style'; -import { Button, ButtonWithLoading } from '../common/Button2'; import { BigInput } from '../common/Input'; import { Link } from '../common/Link'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; import { useServerURL, useSetServerURL } from '../ServerContext'; import { Title } from './subscribe/common'; diff --git a/packages/desktop-client/src/components/manager/ManagementApp.tsx b/packages/desktop-client/src/components/manager/ManagementApp.tsx index 12d2c79ffe0..8be567824ea 100644 --- a/packages/desktop-client/src/components/manager/ManagementApp.tsx +++ b/packages/desktop-client/src/components/manager/ManagementApp.tsx @@ -1,6 +1,9 @@ import React, { useEffect } from 'react'; import { Navigate, Route, Routes } from 'react-router-dom'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { loggedIn } from 'loot-core/client/actions'; import { setAppState } from 'loot-core/client/app/appSlice'; @@ -15,8 +18,6 @@ import { UserDirectoryPage, } from '../admin/UserDirectory/UserDirectoryPage'; import { AppBackground } from '../AppBackground'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; import { LoggedInUser } from '../LoggedInUser'; import { Notifications } from '../Notifications'; import { useResponsive } from '../responsive/ResponsiveProvider'; diff --git a/packages/desktop-client/src/components/manager/ServerURL.tsx b/packages/desktop-client/src/components/manager/ServerURL.tsx index 47ab070727c..44c294c88e7 100644 --- a/packages/desktop-client/src/components/manager/ServerURL.tsx +++ b/packages/desktop-client/src/components/manager/ServerURL.tsx @@ -1,11 +1,12 @@ import React from 'react'; import { Trans } from 'react-i18next'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { useAuth } from '../../auth/AuthProvider'; import { Permissions } from '../../auth/types'; import { Link } from '../common/Link'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; import { useServerURL } from '../ServerContext'; export function ServerURL() { diff --git a/packages/desktop-client/src/components/manager/WelcomeScreen.tsx b/packages/desktop-client/src/components/manager/WelcomeScreen.tsx index 9c9be40a998..88ed4a5d45e 100644 --- a/packages/desktop-client/src/components/manager/WelcomeScreen.tsx +++ b/packages/desktop-client/src/components/manager/WelcomeScreen.tsx @@ -1,15 +1,17 @@ import React from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Paragraph } from '@actual-app/components/paragraph'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { createBudget, pushModal } from 'loot-core/client/actions'; import { useDispatch } from '../../redux'; -import { styles, theme } from '../../style'; -import { Button } from '../common/Button2'; +import { theme } from '../../style'; import { Link } from '../common/Link'; -import { Paragraph } from '../common/Paragraph'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; export function WelcomeScreen() { const { t } = useTranslation(); diff --git a/packages/desktop-client/src/components/manager/subscribe/Bootstrap.tsx b/packages/desktop-client/src/components/manager/subscribe/Bootstrap.tsx index 8b001f4f473..6687977c1ce 100644 --- a/packages/desktop-client/src/components/manager/subscribe/Bootstrap.tsx +++ b/packages/desktop-client/src/components/manager/subscribe/Bootstrap.tsx @@ -2,17 +2,18 @@ import React, { useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Paragraph } from '@actual-app/components/paragraph'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { createBudget } from 'loot-core/client/actions/budgets'; import { send } from 'loot-core/platform/client/fetch'; import { useNavigate } from '../../../hooks/useNavigate'; import { useDispatch } from '../../../redux'; import { theme } from '../../../style'; -import { Button } from '../../common/Button2'; import { Link } from '../../common/Link'; -import { Paragraph } from '../../common/Paragraph'; -import { Text } from '../../common/Text'; -import { View } from '../../common/View'; import { useRefreshLoginMethods } from '../../ServerContext'; import { useBootstrapped, Title } from './common'; diff --git a/packages/desktop-client/src/components/manager/subscribe/ChangePassword.tsx b/packages/desktop-client/src/components/manager/subscribe/ChangePassword.tsx index 0b4d6c0ee01..0d1c5eb7c68 100644 --- a/packages/desktop-client/src/components/manager/subscribe/ChangePassword.tsx +++ b/packages/desktop-client/src/components/manager/subscribe/ChangePassword.tsx @@ -2,13 +2,14 @@ import React, { useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { send } from 'loot-core/platform/client/fetch'; import { useNavigate } from '../../../hooks/useNavigate'; import { theme } from '../../../style'; -import { Button } from '../../common/Button2'; -import { Text } from '../../common/Text'; -import { View } from '../../common/View'; import { Title } from './common'; import { ConfirmPasswordForm } from './ConfirmPasswordForm'; diff --git a/packages/desktop-client/src/components/manager/subscribe/ConfirmPasswordForm.tsx b/packages/desktop-client/src/components/manager/subscribe/ConfirmPasswordForm.tsx index cfb64077e44..7cd4b614c56 100644 --- a/packages/desktop-client/src/components/manager/subscribe/ConfirmPasswordForm.tsx +++ b/packages/desktop-client/src/components/manager/subscribe/ConfirmPasswordForm.tsx @@ -2,10 +2,11 @@ import React, { type ChangeEvent, type ReactNode, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { ButtonWithLoading } from '@actual-app/components/button'; +import { View } from '@actual-app/components/view'; + import { theme } from '../../../style'; -import { ButtonWithLoading } from '../../common/Button2'; import { BigInput } from '../../common/Input'; -import { View } from '../../common/View'; type ConfirmPasswordFormProps = { buttons: ReactNode; diff --git a/packages/desktop-client/src/components/manager/subscribe/Error.tsx b/packages/desktop-client/src/components/manager/subscribe/Error.tsx index 9168d9fbe02..623bb5e329b 100644 --- a/packages/desktop-client/src/components/manager/subscribe/Error.tsx +++ b/packages/desktop-client/src/components/manager/subscribe/Error.tsx @@ -3,11 +3,12 @@ import React from 'react'; import { Trans } from 'react-i18next'; import { useLocation } from 'react-router-dom'; +import { Button } from '@actual-app/components/button'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { useNavigate } from '../../../hooks/useNavigate'; import { theme } from '../../../style'; -import { Button } from '../../common/Button2'; -import { Text } from '../../common/Text'; -import { View } from '../../common/View'; function getErrorMessage(reason) { switch (reason) { diff --git a/packages/desktop-client/src/components/manager/subscribe/Login.tsx b/packages/desktop-client/src/components/manager/subscribe/Login.tsx index e886d621987..856569fa0ce 100644 --- a/packages/desktop-client/src/components/manager/subscribe/Login.tsx +++ b/packages/desktop-client/src/components/manager/subscribe/Login.tsx @@ -3,6 +3,12 @@ import React, { useState, useEffect } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; +import { Button, ButtonWithLoading } from '@actual-app/components/button'; +import { Label } from '@actual-app/components/label'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { loggedIn } from 'loot-core/client/actions/user'; import { send } from 'loot-core/platform/client/fetch'; import { isElectron } from 'loot-core/shared/environment'; @@ -11,14 +17,10 @@ import { type OpenIdConfig } from 'loot-core/types/models/openid'; import { useNavigate } from '../../../hooks/useNavigate'; import { AnimatedLoading } from '../../../icons/AnimatedLoading'; import { useDispatch } from '../../../redux'; -import { styles, theme } from '../../../style'; -import { Button, ButtonWithLoading } from '../../common/Button2'; +import { theme } from '../../../style'; import { BigInput } from '../../common/Input'; -import { Label } from '../../common/Label'; import { Link } from '../../common/Link'; import { Select } from '../../common/Select'; -import { Text } from '../../common/Text'; -import { View } from '../../common/View'; import { useResponsive } from '../../responsive/ResponsiveProvider'; import { useAutoLogin, diff --git a/packages/desktop-client/src/components/manager/subscribe/OpenIdForm.tsx b/packages/desktop-client/src/components/manager/subscribe/OpenIdForm.tsx index 3a59f525b1f..ffb50ace57d 100644 --- a/packages/desktop-client/src/components/manager/subscribe/OpenIdForm.tsx +++ b/packages/desktop-client/src/components/manager/subscribe/OpenIdForm.tsx @@ -2,20 +2,22 @@ import { type ReactNode, useEffect, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { useLocation, type Location } from 'react-router-dom'; +import { ButtonWithLoading } from '@actual-app/components/button'; +import { Menu } from '@actual-app/components/menu'; +import { Stack } from '@actual-app/components/stack'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { addNotification } from 'loot-core/client/actions'; import { send } from 'loot-core/platform/client/fetch'; import { type Handlers } from 'loot-core/types/handlers'; import { type OpenIdConfig } from 'loot-core/types/models/openid'; -import { theme, styles } from '../../../style'; -import { ButtonWithLoading } from '../../common/Button2'; +import { theme } from '../../../style'; import { Input } from '../../common/Input'; import { Link } from '../../common/Link'; -import { Menu } from '../../common/Menu'; import { Select } from '../../common/Select'; -import { Stack } from '../../common/Stack'; -import { Text } from '../../common/Text'; -import { View } from '../../common/View'; import { FormField, FormLabel } from '../../forms'; import { useServerURL } from '../../ServerContext'; diff --git a/packages/desktop-client/src/components/mobile/FloatingActionBar.tsx b/packages/desktop-client/src/components/mobile/FloatingActionBar.tsx index dbee0a6cb85..fc8b1ad75b3 100644 --- a/packages/desktop-client/src/components/mobile/FloatingActionBar.tsx +++ b/packages/desktop-client/src/components/mobile/FloatingActionBar.tsx @@ -1,7 +1,8 @@ import { type PropsWithChildren, type CSSProperties } from 'react'; +import { View } from '@actual-app/components/view'; + import { theme } from '../../style'; -import { View } from '../common/View'; type FloatingActionBarProps = PropsWithChildren & { style: CSSProperties; diff --git a/packages/desktop-client/src/components/mobile/MobileBackButton.tsx b/packages/desktop-client/src/components/mobile/MobileBackButton.tsx index ab528289105..ac9cc7e0d02 100644 --- a/packages/desktop-client/src/components/mobile/MobileBackButton.tsx +++ b/packages/desktop-client/src/components/mobile/MobileBackButton.tsx @@ -1,11 +1,12 @@ import React, { type ComponentPropsWithoutRef } from 'react'; import { useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; + import { useNavigate } from '../../hooks/useNavigate'; import { SvgCheveronLeft } from '../../icons/v1'; -import { styles } from '../../style'; -import { Button } from '../common/Button2'; -import { Text } from '../common/Text'; type MobileBackButtonProps = ComponentPropsWithoutRef; diff --git a/packages/desktop-client/src/components/mobile/MobileForms.tsx b/packages/desktop-client/src/components/mobile/MobileForms.tsx index 6f3035a4c0a..58ebcd0106e 100644 --- a/packages/desktop-client/src/components/mobile/MobileForms.tsx +++ b/packages/desktop-client/src/components/mobile/MobileForms.tsx @@ -6,14 +6,15 @@ import React, { type CSSProperties, } from 'react'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { Toggle } from '@actual-app/components/toggle'; +import { View } from '@actual-app/components/view'; import { css } from '@emotion/css'; -import { theme, styles } from '../../style'; +import { theme } from '../../style'; import { Button } from '../common/Button'; import { Input } from '../common/Input'; -import { Text } from '../common/Text'; -import { Toggle } from '../common/Toggle'; -import { View } from '../common/View'; type FieldLabelProps = { title: string; diff --git a/packages/desktop-client/src/components/mobile/MobileNavTabs.tsx b/packages/desktop-client/src/components/mobile/MobileNavTabs.tsx index 1ca7c37492e..2d0498a4f2a 100644 --- a/packages/desktop-client/src/components/mobile/MobileNavTabs.tsx +++ b/packages/desktop-client/src/components/mobile/MobileNavTabs.tsx @@ -5,9 +5,12 @@ import React, { useCallback, useState, } from 'react'; +import { useTranslation } from 'react-i18next'; import { NavLink } from 'react-router-dom'; import { useSpring, animated, config } from 'react-spring'; +import { styles } from '@actual-app/components/styles'; +import { View } from '@actual-app/components/view'; import { useDrag } from '@use-gesture/react'; import { @@ -20,8 +23,7 @@ import { } from '../../icons/v1'; import { SvgReports } from '../../icons/v1/Reports'; import { SvgCalendar } from '../../icons/v2'; -import { theme, styles } from '../../style'; -import { View } from '../common/View'; +import { theme } from '../../style'; import { useResponsive } from '../responsive/ResponsiveProvider'; import { useScrollListener } from '../ScrollProvider'; @@ -36,6 +38,7 @@ const HIDDEN_Y = TOTAL_HEIGHT; export const MOBILE_NAV_HEIGHT = ROW_HEIGHT + PILL_HEIGHT; export function MobileNavTabs() { + const { t } = useTranslation(); const { isNarrowWidth } = useResponsive(); const [navbarState, setNavbarState] = useState<'default' | 'open' | 'hidden'>( 'default', @@ -89,49 +92,49 @@ export function MobileNavTabs() { const navTabs = [ { - name: 'Budget', + name: t('Budget'), path: '/budget', style: navTabStyle, Icon: SvgWallet, }, { - name: 'Transaction', + name: t('Transaction'), path: '/transactions/new', style: navTabStyle, Icon: SvgAdd, }, { - name: 'Accounts', + name: t('Accounts'), path: '/accounts', style: navTabStyle, Icon: SvgPiggyBank, }, { - name: 'Reports', + name: t('Reports'), path: '/reports', style: navTabStyle, Icon: SvgReports, }, { - name: 'Schedules (Soon)', + name: t('Schedules (Soon)'), path: '/schedules/soon', style: navTabStyle, Icon: SvgCalendar, }, { - name: 'Payees (Soon)', + name: t('Payees (Soon)'), path: '/payees/soon', style: navTabStyle, Icon: SvgStoreFront, }, { - name: 'Rules (Soon)', + name: t('Rules (Soon)'), path: '/rules/soon', style: navTabStyle, Icon: SvgTuning, }, { - name: 'Settings', + name: t('Settings'), path: '/settings', style: navTabStyle, Icon: SvgCog, diff --git a/packages/desktop-client/src/components/mobile/accounts/AccountTransactions.tsx b/packages/desktop-client/src/components/mobile/accounts/AccountTransactions.tsx index 6979ea5200e..3ac2b75af2b 100644 --- a/packages/desktop-client/src/components/mobile/accounts/AccountTransactions.tsx +++ b/packages/desktop-client/src/components/mobile/accounts/AccountTransactions.tsx @@ -5,6 +5,12 @@ import React, { useMemo, useState, } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { Button } from '@actual-app/components/button'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; import { collapseModals, @@ -39,10 +45,7 @@ import { useDateFormat } from '../../../hooks/useDateFormat'; import { useFailedAccounts } from '../../../hooks/useFailedAccounts'; import { useNavigate } from '../../../hooks/useNavigate'; import { useSelector, useDispatch } from '../../../redux'; -import { styles, theme } from '../../../style'; -import { Button } from '../../common/Button2'; -import { Text } from '../../common/Text'; -import { View } from '../../common/View'; +import { theme } from '../../../style'; import { MobilePageHeader, Page } from '../../Page'; import { MobileBackButton } from '../MobileBackButton'; import { AddTransactionButton } from '../transactions/AddTransactionButton'; @@ -228,6 +231,7 @@ function TransactionListWithPreviews({ | 'uncategorized'; readonly accountName: AccountEntity['name'] | string; }) { + const { t } = useTranslation(); const baseTransactionsQuery = useCallback( () => queries.transactions(accountId).options({ splits: 'none' }).select('*'), @@ -338,7 +342,7 @@ function TransactionListWithPreviews({ balanceUncleared={balanceQueries.uncleared} isLoadingMore={isLoadingMore} onLoadMore={loadMoreTransactions} - searchPlaceholder={`Search ${accountName}`} + searchPlaceholder={t('Search {{accountName}}', { accountName })} onSearch={onSearch} onOpenTransaction={onOpenTransaction} onRefresh={onRefresh} diff --git a/packages/desktop-client/src/components/mobile/accounts/Accounts.tsx b/packages/desktop-client/src/components/mobile/accounts/Accounts.tsx index 11cb2fece99..ae0b3b4ed63 100644 --- a/packages/desktop-client/src/components/mobile/accounts/Accounts.tsx +++ b/packages/desktop-client/src/components/mobile/accounts/Accounts.tsx @@ -1,6 +1,11 @@ import React, { type CSSProperties, useCallback } from 'react'; import { useTranslation, Trans } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { TextOneLine } from '@actual-app/components/text-one-line'; +import { View } from '@actual-app/components/view'; import { css } from '@emotion/css'; import { replaceModal } from 'loot-core/client/actions'; @@ -14,12 +19,8 @@ import { useNavigate } from '../../../hooks/useNavigate'; import { useSyncedPref } from '../../../hooks/useSyncedPref'; import { SvgAdd, SvgCheveronRight } from '../../../icons/v1'; import { useDispatch, useSelector } from '../../../redux'; -import { theme, styles } from '../../../style'; +import { theme } from '../../../style'; import { makeAmountFullStyle } from '../../budget/util'; -import { Button } from '../../common/Button2'; -import { Text } from '../../common/Text'; -import { TextOneLine } from '../../common/TextOneLine'; -import { View } from '../../common/View'; import { MobilePageHeader, Page } from '../../Page'; import { type Binding, type SheetFields } from '../../spreadsheet'; import { CellValue, CellValueText } from '../../spreadsheet/CellValue'; @@ -261,7 +262,7 @@ function AccountList({ {onBudgetAccounts.length > 0 && ( )} @@ -281,7 +282,7 @@ function AccountList({ {offBudgetAccounts.length > 0 && ( diff --git a/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx b/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx index 2f685146030..71700f44b4d 100644 --- a/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx +++ b/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx @@ -1,6 +1,12 @@ import React, { memo, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Card } from '@actual-app/components/card'; +import { Label } from '@actual-app/components/label'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; import { css } from '@emotion/css'; import { AutoTextSize } from 'auto-text-size'; import memoizeOne from 'memoize-one'; @@ -29,17 +35,12 @@ import { SvgArrowThickRight, SvgCheveronRight, } from '../../../icons/v1'; -import { SvgViewShow } from '../../../icons/v2'; +import { SvgCalendar, SvgViewShow } from '../../../icons/v2'; import { useDispatch } from '../../../redux'; -import { theme, styles } from '../../../style'; +import { theme } from '../../../style'; import { BalanceWithCarryover } from '../../budget/BalanceWithCarryover'; import { makeAmountGrey, makeBalanceAmountStyle } from '../../budget/util'; -import { Button } from '../../common/Button2'; -import { Card } from '../../common/Card'; -import { Label } from '../../common/Label'; import { Link } from '../../common/Link'; -import { Text } from '../../common/Text'; -import { View } from '../../common/View'; import { MobilePageHeader, Page } from '../../Page'; import { PrivacyFilter } from '../../PrivacyFilter'; import { useResponsive } from '../../responsive/ResponsiveProvider'; @@ -487,7 +488,13 @@ const ExpenseCategory = memo(function ExpenseCategory({ }); dispatch(collapseModals(`${modalBudgetType}-balance-menu`)); showUndoNotification({ - message: `Covered ${category.name} overspending from ${categoriesById[fromCategoryId].name}.`, + message: t( + `Covered {{toCategoryName}} overspending from {{fromCategoryName}}.`, + { + toCategoryName: category.name, + fromCategoryName: categoriesById[fromCategoryId].name, + }, + ), }); }, }), @@ -501,6 +508,7 @@ const ExpenseCategory = memo(function ExpenseCategory({ month, onBudgetAction, showUndoNotification, + t, ]); const onOpenBalanceMenu = useCallback(() => { @@ -1627,6 +1635,7 @@ export function BudgetTable({ // editMode, onPrevMonth, onNextMonth, + onCurrentMonth, onSaveGroup, onDeleteGroup, onAddCategory, @@ -1694,6 +1703,16 @@ export function BudgetTable({ /> } + rightContent={ + + } /> } > diff --git a/packages/desktop-client/src/components/mobile/budget/CategoryTransactions.tsx b/packages/desktop-client/src/components/mobile/budget/CategoryTransactions.tsx index d196d053d16..9427fd9fd8b 100644 --- a/packages/desktop-client/src/components/mobile/budget/CategoryTransactions.tsx +++ b/packages/desktop-client/src/components/mobile/budget/CategoryTransactions.tsx @@ -1,5 +1,9 @@ import React, { useCallback, useEffect, useState } from 'react'; +import { TextOneLine } from '@actual-app/components/text-one-line'; +import { View } from '@actual-app/components/view'; + +import { SchedulesProvider } from 'loot-core/client/data-hooks/schedules'; import { useTransactions, useTransactionsSearch, @@ -17,8 +21,6 @@ import { import { useDateFormat } from '../../../hooks/useDateFormat'; import { useNavigate } from '../../../hooks/useNavigate'; import { useDispatch } from '../../../redux'; -import { TextOneLine } from '../../common/TextOneLine'; -import { View } from '../../common/View'; import { MobilePageHeader, Page } from '../../Page'; import { MobileBackButton } from '../MobileBackButton'; import { AddTransactionButton } from '../transactions/AddTransactionButton'; @@ -113,19 +115,21 @@ export function CategoryTransactions({ } padding={0} > - + + + ); } diff --git a/packages/desktop-client/src/components/mobile/budget/ListItem.tsx b/packages/desktop-client/src/components/mobile/budget/ListItem.tsx index f8c7c753335..b797cf23ef0 100644 --- a/packages/desktop-client/src/components/mobile/budget/ListItem.tsx +++ b/packages/desktop-client/src/components/mobile/budget/ListItem.tsx @@ -4,8 +4,9 @@ import React, { type CSSProperties, } from 'react'; +import { View } from '@actual-app/components/view'; + import { theme } from '../../../style'; -import { View } from '../../common/View'; const ROW_HEIGHT = 50; diff --git a/packages/desktop-client/src/components/mobile/budget/index.tsx b/packages/desktop-client/src/components/mobile/budget/index.tsx index b605b720d07..b9a95363cb7 100644 --- a/packages/desktop-client/src/components/mobile/budget/index.tsx +++ b/packages/desktop-client/src/components/mobile/budget/index.tsx @@ -1,6 +1,8 @@ // @ts-strict-ignore import React, { useCallback, useEffect, useState } from 'react'; +import { View } from '@actual-app/components/view'; + import { collapseModals, pushModal } from 'loot-core/client/actions'; import { sync } from 'loot-core/client/app/appSlice'; import { @@ -25,7 +27,6 @@ import { AnimatedLoading } from '../../../icons/AnimatedLoading'; import { useDispatch } from '../../../redux'; import { theme } from '../../../style'; import { prewarmMonth } from '../../budget/util'; -import { View } from '../../common/View'; import { NamespaceContext } from '../../spreadsheet/NamespaceContext'; import { SyncRefresh } from '../../SyncRefresh'; @@ -286,6 +287,12 @@ export function Budget() { setInitialized(true); }, [budgetType, setStartMonthPref, spreadsheet, startMonth]); + const onCurrentMonth = useCallback(async () => { + await prewarmMonth(budgetType, spreadsheet, currMonth); + setStartMonthPref(currMonth); + setInitialized(true); + }, [budgetType, setStartMonthPref, spreadsheet, currMonth]); + // const onOpenMonthActionMenu = () => { // const options = [ // 'Copy last month’s budget', @@ -500,6 +507,7 @@ export function Budget() { onShowBudgetSummary={onShowBudgetSummary} onPrevMonth={onPrevMonth} onNextMonth={onNextMonth} + onCurrentMonth={onCurrentMonth} onSaveGroup={onSaveGroup} onDeleteGroup={onDeleteGroup} onAddCategory={onOpenNewCategoryModal} diff --git a/packages/desktop-client/src/components/mobile/transactions/AddTransactionButton.tsx b/packages/desktop-client/src/components/mobile/transactions/AddTransactionButton.tsx index 2024205c3c0..372c3540381 100644 --- a/packages/desktop-client/src/components/mobile/transactions/AddTransactionButton.tsx +++ b/packages/desktop-client/src/components/mobile/transactions/AddTransactionButton.tsx @@ -1,9 +1,10 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; + import { useNavigate } from '../../../hooks/useNavigate'; import { SvgAdd } from '../../../icons/v1'; -import { Button } from '../../common/Button2'; type AddTransactionButtonProps = { to?: string; diff --git a/packages/desktop-client/src/components/mobile/transactions/FocusableAmountInput.tsx b/packages/desktop-client/src/components/mobile/transactions/FocusableAmountInput.tsx index cf3fcd18027..feee7554ce4 100644 --- a/packages/desktop-client/src/components/mobile/transactions/FocusableAmountInput.tsx +++ b/packages/desktop-client/src/components/mobile/transactions/FocusableAmountInput.tsx @@ -9,6 +9,9 @@ import React, { type CSSProperties, } from 'react'; +import { Button } from '@actual-app/components/button'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; import { css } from '@emotion/css'; import { @@ -21,9 +24,6 @@ import { useMergedRefs } from '../../../hooks/useMergedRefs'; import { useSyncedPref } from '../../../hooks/useSyncedPref'; import { theme } from '../../../style'; import { makeAmountFullStyle } from '../../budget/util'; -import { Button } from '../../common/Button2'; -import { Text } from '../../common/Text'; -import { View } from '../../common/View'; type AmountInputProps = { value: number; diff --git a/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx b/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx index 4a95a2d6a40..e0f15d20dbd 100644 --- a/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx +++ b/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx @@ -7,9 +7,13 @@ import React, { useMemo, useCallback, } from 'react'; -import { useTranslation } from 'react-i18next'; +import { Trans, useTranslation } from 'react-i18next'; import { useLocation, useParams } from 'react-router-dom'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { Toggle } from '@actual-app/components/toggle'; +import { View } from '@actual-app/components/view'; import { format as formatDate, parse as parseDate, @@ -57,11 +61,8 @@ import { SvgSplit } from '../../../icons/v0'; import { SvgAdd, SvgPiggyBank, SvgTrash } from '../../../icons/v1'; import { SvgPencilWriteAlternate } from '../../../icons/v2'; import { useSelector, useDispatch } from '../../../redux'; -import { styles, theme } from '../../../style'; +import { theme } from '../../../style'; import { Button } from '../../common/Button'; -import { Text } from '../../common/Text'; -import { Toggle } from '../../common/Toggle'; -import { View } from '../../common/View'; import { MobilePageHeader, Page } from '../../Page'; import { AmountInput } from '../../util/AmountInput'; import { MobileBackButton } from '../MobileBackButton'; @@ -235,7 +236,7 @@ function Footer({ marginLeft: 6, }} > - Select account + Select account ) : isAdding ? ( @@ -253,7 +254,7 @@ function Footer({ marginLeft: 5, }} > - Add transaction + Add transaction ) : ( @@ -749,8 +750,8 @@ const TransactionEditInner = memo(function TransactionEditInner({ title={ transaction.payee == null ? isAdding - ? 'New Transaction' - : 'Transaction' + ? t('New Transaction') + : t('Transaction') : title } leftContent={} diff --git a/packages/desktop-client/src/components/mobile/transactions/TransactionList.tsx b/packages/desktop-client/src/components/mobile/transactions/TransactionList.tsx index bc983b2b246..2fe59e36af3 100644 --- a/packages/desktop-client/src/components/mobile/transactions/TransactionList.tsx +++ b/packages/desktop-client/src/components/mobile/transactions/TransactionList.tsx @@ -7,7 +7,7 @@ import React, { type CSSProperties, } from 'react'; import { ListBox, Section, Header, Collection } from 'react-aria-components'; -import { useTranslation } from 'react-i18next'; +import { Trans, useTranslation } from 'react-i18next'; import { Button } from '@actual-app/components/button'; import { Menu, type MenuItemObject } from '@actual-app/components/menu'; @@ -149,7 +149,9 @@ export function TransactionList({ backgroundColor: theme.mobilePageBackground, }} > - No transactions + + No transactions + )} items={sections} diff --git a/packages/desktop-client/src/components/mobile/transactions/TransactionListItem.tsx b/packages/desktop-client/src/components/mobile/transactions/TransactionListItem.tsx index 8d50fbae5b8..f2a0976ec9e 100644 --- a/packages/desktop-client/src/components/mobile/transactions/TransactionListItem.tsx +++ b/packages/desktop-client/src/components/mobile/transactions/TransactionListItem.tsx @@ -4,39 +4,68 @@ import React, { } from 'react'; import { mergeProps } from 'react-aria'; import { ListBoxItem } from 'react-aria-components'; +import { useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { TextOneLine } from '@actual-app/components/text-one-line'; +import { View } from '@actual-app/components/view'; import { PressResponder, usePress, useLongPress, } from '@react-aria/interactions'; -import { isPreviewId } from 'loot-core/shared/transactions'; -import { integerToCurrency } from 'loot-core/shared/util'; -import { type TransactionEntity } from 'loot-core/types/models'; +import { useCachedSchedules } from 'loot-core/client/data-hooks/schedules'; +import { isPreviewId } from 'loot-core/src/shared/transactions'; +import { integerToCurrency } from 'loot-core/src/shared/util'; +import { + type AccountEntity, + type TransactionEntity, +} from 'loot-core/types/models'; import { useAccount } from '../../../hooks/useAccount'; import { useCategories } from '../../../hooks/useCategories'; +import { useDisplayPayee } from '../../../hooks/useDisplayPayee'; import { usePayee } from '../../../hooks/usePayee'; -import { SvgSplit } from '../../../icons/v0'; +import { SvgLeftArrow2, SvgRightArrow2, SvgSplit } from '../../../icons/v0'; import { SvgArrowsSynchronize, + SvgCalendar, SvgCheckCircle1, SvgLockClosed, } from '../../../icons/v2'; import { useSelector } from '../../../redux'; -import { styles, theme } from '../../../style'; +import { theme } from '../../../style'; import { makeAmountFullStyle } from '../../budget/util'; -import { Button } from '../../common/Button2'; -import { Text } from '../../common/Text'; -import { TextOneLine } from '../../common/TextOneLine'; -import { View } from '../../common/View'; -import { getPrettyPayee } from '../utils'; import { lookupName, Status } from './TransactionEdit'; const ROW_HEIGHT = 60; +const getTextStyle = ({ + isPreview, +}: { + isPreview: boolean; +}): CSSProperties => ({ + ...styles.text, + fontSize: 14, + ...(isPreview + ? { + fontStyle: 'italic', + color: theme.pageTextLight, + } + : {}), +}); + +const getScheduleIconStyle = ({ isPreview }: { isPreview: boolean }) => ({ + width: 12, + height: 12, + marginRight: 5, + color: isPreview ? theme.pageTextLight : theme.menuItemText, +}); + type TransactionListItemProps = ComponentPropsWithoutRef< typeof ListBoxItem > & { @@ -49,11 +78,14 @@ export function TransactionListItem({ onLongPress, ...props }: TransactionListItemProps) { + const { t } = useTranslation(); const { list: categories } = useCategories(); const { value: transaction } = props; const payee = usePayee(transaction?.payee || ''); + const displayPayee = useDisplayPayee({ transaction }); + const account = useAccount(transaction?.account || ''); const transferAccount = useAccount(payee?.transfer_acct || ''); const isPreview = isPreviewId(transaction?.id || ''); @@ -90,7 +122,6 @@ export function TransactionListItem({ is_parent: isParent, is_child: isChild, notes, - schedule: scheduleId, forceUpcoming, } = transaction; @@ -98,11 +129,6 @@ export function TransactionListItem({ const isAdded = newTransactions.includes(id); const categoryName = lookupName(categories, categoryId); - const prettyPayee = getPrettyPayee({ - transaction, - payee, - transferAccount, - }); const specialCategory = account?.offbudget ? 'Off budget' : transferAccount && !transferAccount.offbudget @@ -112,17 +138,7 @@ export function TransactionListItem({ : null; const prettyCategory = specialCategory || categoryName; - - const textStyle: CSSProperties = { - ...styles.text, - fontSize: 14, - ...(isPreview - ? { - fontStyle: 'italic', - color: theme.pageTextLight, - } - : {}), - }; + const textStyle = getTextStyle({ isPreview }); return ( @@ -165,27 +181,23 @@ export function TransactionListItem({ > - {scheduleId && ( - - )} + - {prettyPayee || '(No payee)'} + {displayPayee || t('(No payee)')} {isPreview ? ( @@ -282,3 +294,39 @@ export function TransactionListItem({ ); } + +type PayeeIconsProps = { + transaction: TransactionEntity; + transferAccount?: AccountEntity; +}; + +function PayeeIcons({ transaction, transferAccount }: PayeeIconsProps) { + const { id, schedule: scheduleId } = transaction; + const { isLoading: isSchedulesLoading, schedules = [] } = + useCachedSchedules(); + const isPreview = isPreviewId(id); + const schedule = schedules.find(s => s.id === scheduleId); + const isScheduleRecurring = + schedule && schedule._date && !!schedule._date.frequency; + + if (isSchedulesLoading) { + return null; + } + + return ( + <> + {schedule && + (isScheduleRecurring ? ( + + ) : ( + + ))} + {transferAccount && + (transaction.amount > 0 ? ( + + ) : ( + + ))} + + ); +} diff --git a/packages/desktop-client/src/components/mobile/transactions/TransactionListWithBalances.tsx b/packages/desktop-client/src/components/mobile/transactions/TransactionListWithBalances.tsx index 34260f1f9ce..a8da911d1d0 100644 --- a/packages/desktop-client/src/components/mobile/transactions/TransactionListWithBalances.tsx +++ b/packages/desktop-client/src/components/mobile/transactions/TransactionListWithBalances.tsx @@ -1,14 +1,16 @@ import React, { type ComponentProps, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { Label } from '@actual-app/components/label'; +import { styles } from '@actual-app/components/styles'; +import { View } from '@actual-app/components/view'; + import { type TransactionEntity } from 'loot-core/types/models/transaction'; import { SelectedProvider, useSelected } from '../../../hooks/useSelected'; import { SvgSearchAlternate } from '../../../icons/v2'; -import { styles, theme } from '../../../style'; +import { theme } from '../../../style'; import { InputWithContent } from '../../common/InputWithContent'; -import { Label } from '../../common/Label'; -import { View } from '../../common/View'; import type { Binding, SheetNames, SheetFields } from '../../spreadsheet'; import { CellValue, CellValueText } from '../../spreadsheet/CellValue'; import { useSheetValue } from '../../spreadsheet/useSheetValue'; diff --git a/packages/desktop-client/src/components/modals/AccountAutocompleteModal.tsx b/packages/desktop-client/src/components/modals/AccountAutocompleteModal.tsx index 6e3b3170231..b1f78abb479 100644 --- a/packages/desktop-client/src/components/modals/AccountAutocompleteModal.tsx +++ b/packages/desktop-client/src/components/modals/AccountAutocompleteModal.tsx @@ -1,6 +1,8 @@ import React, { type ComponentPropsWithoutRef } from 'react'; import { useTranslation } from 'react-i18next'; +import { View } from '@actual-app/components/view'; + import { theme } from '../../style'; import { AccountAutocomplete } from '../autocomplete/AccountAutocomplete'; import { @@ -9,7 +11,6 @@ import { ModalTitle, ModalHeader, } from '../common/Modal'; -import { View } from '../common/View'; import { SectionLabel } from '../forms'; import { useResponsive } from '../responsive/ResponsiveProvider'; diff --git a/packages/desktop-client/src/components/modals/AccountMenuModal.tsx b/packages/desktop-client/src/components/modals/AccountMenuModal.tsx index a7553c384c5..b36318ea864 100644 --- a/packages/desktop-client/src/components/modals/AccountMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/AccountMenuModal.tsx @@ -7,6 +7,12 @@ import { } from 'react'; import { useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Menu } from '@actual-app/components/menu'; +import { Popover } from '@actual-app/components/popover'; +import { styles } from '@actual-app/components/styles'; +import { View } from '@actual-app/components/view'; + import { type AccountEntity } from 'loot-core/types/models'; import { useAccount } from '../../hooks/useAccount'; @@ -14,17 +20,13 @@ import { useAccounts } from '../../hooks/useAccounts'; import { useNotes } from '../../hooks/useNotes'; import { SvgClose, SvgDotsHorizontalTriple, SvgLockOpen } from '../../icons/v1'; import { SvgNotesPaper } from '../../icons/v2'; -import { styles, theme } from '../../style'; -import { Button } from '../common/Button2'; -import { Menu } from '../common/Menu'; +import { theme } from '../../style'; import { Modal, ModalCloseButton, ModalHeader, ModalTitle, } from '../common/Modal'; -import { Popover } from '../common/Popover'; -import { View } from '../common/View'; import { Notes } from '../Notes'; import { validateAccountName } from '../util/accountValidation'; diff --git a/packages/desktop-client/src/components/modals/BudgetListModal.tsx b/packages/desktop-client/src/components/modals/BudgetListModal.tsx index 1d1d5fc77f5..7e51f6b9bf6 100644 --- a/packages/desktop-client/src/components/modals/BudgetListModal.tsx +++ b/packages/desktop-client/src/components/modals/BudgetListModal.tsx @@ -1,11 +1,12 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { useMetadataPref } from '../../hooks/useMetadataPref'; import { useSelector } from '../../redux'; import { Modal, ModalHeader, ModalCloseButton } from '../common/Modal'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; import { BudgetList } from '../manager/BudgetList'; export function BudgetListModal() { diff --git a/packages/desktop-client/src/components/modals/BudgetPageMenuModal.tsx b/packages/desktop-client/src/components/modals/BudgetPageMenuModal.tsx index e96abcdc8e7..733076afdad 100644 --- a/packages/desktop-client/src/components/modals/BudgetPageMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/BudgetPageMenuModal.tsx @@ -4,9 +4,11 @@ import React, { } from 'react'; import { useTranslation } from 'react-i18next'; +import { Menu } from '@actual-app/components/menu'; +import { styles } from '@actual-app/components/styles'; + import { useLocalPref } from '../../hooks/useLocalPref'; -import { theme, styles } from '../../style'; -import { Menu } from '../common/Menu'; +import { theme } from '../../style'; import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal'; type BudgetPageMenuModalProps = ComponentPropsWithoutRef; diff --git a/packages/desktop-client/src/components/modals/CategoryAutocompleteModal.tsx b/packages/desktop-client/src/components/modals/CategoryAutocompleteModal.tsx index 269e552ee6d..d64b468dc48 100644 --- a/packages/desktop-client/src/components/modals/CategoryAutocompleteModal.tsx +++ b/packages/desktop-client/src/components/modals/CategoryAutocompleteModal.tsx @@ -1,6 +1,8 @@ import React, { type ComponentPropsWithoutRef } from 'react'; import { useTranslation } from 'react-i18next'; +import { View } from '@actual-app/components/view'; + import * as monthUtils from 'loot-core/shared/months'; import { theme } from '../../style'; @@ -11,7 +13,6 @@ import { ModalTitle, ModalHeader, } from '../common/Modal'; -import { View } from '../common/View'; import { SectionLabel } from '../forms'; import { useResponsive } from '../responsive/ResponsiveProvider'; import { NamespaceContext } from '../spreadsheet/NamespaceContext'; diff --git a/packages/desktop-client/src/components/modals/CategoryGroupMenuModal.tsx b/packages/desktop-client/src/components/modals/CategoryGroupMenuModal.tsx index bf22caa105b..f502a62f41c 100644 --- a/packages/desktop-client/src/components/modals/CategoryGroupMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/CategoryGroupMenuModal.tsx @@ -7,23 +7,25 @@ import React, { } from 'react'; import { useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Menu } from '@actual-app/components/menu'; +import { Popover } from '@actual-app/components/popover'; +import { styles } from '@actual-app/components/styles'; +import { View } from '@actual-app/components/view'; + import { type CategoryGroupEntity } from 'loot-core/types/models'; import { useCategories } from '../../hooks/useCategories'; import { useNotes } from '../../hooks/useNotes'; import { SvgDotsHorizontalTriple, SvgAdd, SvgTrash } from '../../icons/v1'; import { SvgNotesPaper, SvgViewHide, SvgViewShow } from '../../icons/v2'; -import { styles, theme } from '../../style'; -import { Button } from '../common/Button2'; -import { Menu } from '../common/Menu'; +import { theme } from '../../style'; import { Modal, ModalCloseButton, ModalHeader, ModalTitle, } from '../common/Modal'; -import { Popover } from '../common/Popover'; -import { View } from '../common/View'; import { Notes } from '../Notes'; type CategoryGroupMenuModalProps = { diff --git a/packages/desktop-client/src/components/modals/CategoryMenuModal.tsx b/packages/desktop-client/src/components/modals/CategoryMenuModal.tsx index edec24657d4..2a238c8c5cd 100644 --- a/packages/desktop-client/src/components/modals/CategoryMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/CategoryMenuModal.tsx @@ -2,6 +2,12 @@ import React, { useRef, useState, type CSSProperties } from 'react'; import { useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Menu } from '@actual-app/components/menu'; +import { Popover } from '@actual-app/components/popover'; +import { styles } from '@actual-app/components/styles'; +import { View } from '@actual-app/components/view'; + import { type CategoryEntity } from 'loot-core/types/models'; import { useCategory } from '../../hooks/useCategory'; @@ -9,17 +15,13 @@ import { useCategoryGroup } from '../../hooks/useCategoryGroup'; import { useNotes } from '../../hooks/useNotes'; import { SvgDotsHorizontalTriple, SvgTrash } from '../../icons/v1'; import { SvgNotesPaper, SvgViewHide, SvgViewShow } from '../../icons/v2'; -import { styles, theme } from '../../style'; -import { Button } from '../common/Button2'; -import { Menu } from '../common/Menu'; +import { theme } from '../../style'; import { Modal, ModalCloseButton, ModalHeader, ModalTitle, } from '../common/Modal'; -import { Popover } from '../common/Popover'; -import { View } from '../common/View'; import { Notes } from '../Notes'; type CategoryMenuModalProps = { @@ -114,7 +116,9 @@ export function CategoryMenuModal({ }} > 0 ? originalNotes : 'No notes'} + notes={ + originalNotes?.length > 0 ? originalNotes : t('No notes') + } editable={false} focused={false} getStyle={() => ({ @@ -197,14 +201,14 @@ function AdditionalCategoryMenu({ items={[ !categoryGroup?.hidden && { name: 'toggleVisibility', - text: category.hidden ? 'Show' : 'Hide', + text: category.hidden ? t('Show') : t('Hide'), icon: category.hidden ? SvgViewShow : SvgViewHide, iconSize: 16, }, !categoryGroup?.hidden && Menu.line, { name: 'delete', - text: 'Delete', + text: t('Delete'), icon: SvgTrash, iconSize: 15, }, diff --git a/packages/desktop-client/src/components/modals/CloseAccountModal.tsx b/packages/desktop-client/src/components/modals/CloseAccountModal.tsx index 1519db34919..cc698bb6361 100644 --- a/packages/desktop-client/src/components/modals/CloseAccountModal.tsx +++ b/packages/desktop-client/src/components/modals/CloseAccountModal.tsx @@ -3,6 +3,13 @@ import React, { type FormEvent, useState, type CSSProperties } from 'react'; import { Form } from 'react-aria-components'; import { useTranslation, Trans } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { FormError } from '@actual-app/components/form-error'; +import { Paragraph } from '@actual-app/components/paragraph'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { pushModal } from 'loot-core/client/actions'; import { closeAccount } from 'loot-core/client/queries/queriesSlice'; import { integerToCurrency } from 'loot-core/shared/util'; @@ -12,16 +19,11 @@ import { type TransObjectLiteral } from 'loot-core/types/util'; import { useAccounts } from '../../hooks/useAccounts'; import { useCategories } from '../../hooks/useCategories'; import { useDispatch } from '../../redux'; -import { styles, theme } from '../../style'; +import { theme } from '../../style'; import { AccountAutocomplete } from '../autocomplete/AccountAutocomplete'; import { CategoryAutocomplete } from '../autocomplete/CategoryAutocomplete'; -import { Button } from '../common/Button2'; -import { FormError } from '../common/FormError'; import { Link } from '../common/Link'; import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal'; -import { Paragraph } from '../common/Paragraph'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; import { useResponsive } from '../responsive/ResponsiveProvider'; function needsCategory( diff --git a/packages/desktop-client/src/components/modals/ConfirmTransactionDeleteModal.tsx b/packages/desktop-client/src/components/modals/ConfirmTransactionDeleteModal.tsx index 5b5315df73f..0acaea9cca8 100644 --- a/packages/desktop-client/src/components/modals/ConfirmTransactionDeleteModal.tsx +++ b/packages/desktop-client/src/components/modals/ConfirmTransactionDeleteModal.tsx @@ -1,12 +1,13 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { styles } from '../../style'; -import { Button } from '../common/Button2'; -import { InitialFocus } from '../common/InitialFocus'; +import { Button } from '@actual-app/components/button'; +import { InitialFocus } from '@actual-app/components/initial-focus'; +import { Paragraph } from '@actual-app/components/paragraph'; +import { styles } from '@actual-app/components/styles'; +import { View } from '@actual-app/components/view'; + import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal'; -import { Paragraph } from '../common/Paragraph'; -import { View } from '../common/View'; import { useResponsive } from '../responsive/ResponsiveProvider'; type ConfirmTransactionDeleteProps = { diff --git a/packages/desktop-client/src/components/modals/ConfirmUnlinkAccountModal.tsx b/packages/desktop-client/src/components/modals/ConfirmUnlinkAccountModal.tsx index d77fee203b0..a3ed1078ad1 100644 --- a/packages/desktop-client/src/components/modals/ConfirmUnlinkAccountModal.tsx +++ b/packages/desktop-client/src/components/modals/ConfirmUnlinkAccountModal.tsx @@ -1,11 +1,12 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Button } from '../common/Button2'; -import { InitialFocus } from '../common/InitialFocus'; +import { Button } from '@actual-app/components/button'; +import { InitialFocus } from '@actual-app/components/initial-focus'; +import { Paragraph } from '@actual-app/components/paragraph'; +import { View } from '@actual-app/components/view'; + import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal'; -import { Paragraph } from '../common/Paragraph'; -import { View } from '../common/View'; type ConfirmUnlinkAccountProps = { accountName: string; diff --git a/packages/desktop-client/src/components/modals/CoverModal.tsx b/packages/desktop-client/src/components/modals/CoverModal.tsx index 31b0437ad99..0c0de7ddc40 100644 --- a/packages/desktop-client/src/components/modals/CoverModal.tsx +++ b/packages/desktop-client/src/components/modals/CoverModal.tsx @@ -1,19 +1,20 @@ import React, { useCallback, useMemo, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { styles } from '@actual-app/components/styles'; +import { View } from '@actual-app/components/view'; + import { pushModal } from 'loot-core/client/actions'; import { type CategoryEntity } from 'loot-core/types/models'; import { useCategories } from '../../hooks/useCategories'; import { useDispatch } from '../../redux'; -import { styles } from '../../style'; import { addToBeBudgetedGroup, removeCategoriesFromGroups, } from '../budget/util'; -import { Button } from '../common/Button2'; import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal'; -import { View } from '../common/View'; import { FieldLabel, TapField } from '../mobile/MobileForms'; type CoverModalProps = { diff --git a/packages/desktop-client/src/components/modals/CreateAccountModal.tsx b/packages/desktop-client/src/components/modals/CreateAccountModal.tsx index 025e275f852..5d75cba5abf 100644 --- a/packages/desktop-client/src/components/modals/CreateAccountModal.tsx +++ b/packages/desktop-client/src/components/modals/CreateAccountModal.tsx @@ -2,6 +2,14 @@ import React, { useEffect, useState } from 'react'; import { DialogTrigger } from 'react-aria-components'; import { Trans, useTranslation } from 'react-i18next'; +import { Button, ButtonWithLoading } from '@actual-app/components/button'; +import { InitialFocus } from '@actual-app/components/initial-focus'; +import { Menu } from '@actual-app/components/menu'; +import { Paragraph } from '@actual-app/components/paragraph'; +import { Popover } from '@actual-app/components/popover'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { pushModal } from 'loot-core/client/actions'; import { send } from 'loot-core/platform/client/fetch'; @@ -15,15 +23,8 @@ import { SvgDotsHorizontalTriple } from '../../icons/v1'; import { useDispatch } from '../../redux'; import { theme } from '../../style'; import { Warning } from '../alerts'; -import { Button, ButtonWithLoading } from '../common/Button2'; -import { InitialFocus } from '../common/InitialFocus'; import { Link } from '../common/Link'; -import { Menu } from '../common/Menu'; import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal'; -import { Paragraph } from '../common/Paragraph'; -import { Popover } from '../common/Popover'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; import { useMultiuserEnabled } from '../ServerContext'; type CreateAccountProps = { diff --git a/packages/desktop-client/src/components/modals/CreateEncryptionKeyModal.tsx b/packages/desktop-client/src/components/modals/CreateEncryptionKeyModal.tsx index cb7353de519..a2733b37a6d 100644 --- a/packages/desktop-client/src/components/modals/CreateEncryptionKeyModal.tsx +++ b/packages/desktop-client/src/components/modals/CreateEncryptionKeyModal.tsx @@ -3,6 +3,12 @@ import React, { useState } from 'react'; import { Form } from 'react-aria-components'; import { useTranslation, Trans } from 'react-i18next'; +import { ButtonWithLoading } from '@actual-app/components/button'; +import { InitialFocus } from '@actual-app/components/initial-focus'; +import { Paragraph } from '@actual-app/components/paragraph'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; import { css } from '@emotion/css'; import { loadAllFiles, loadGlobalPrefs } from 'loot-core/client/actions'; @@ -11,9 +17,7 @@ import { send } from 'loot-core/platform/client/fetch'; import { getCreateKeyError } from 'loot-core/shared/errors'; import { useDispatch } from '../../redux'; -import { styles, theme } from '../../style'; -import { ButtonWithLoading } from '../common/Button2'; -import { InitialFocus } from '../common/InitialFocus'; +import { theme } from '../../style'; import { Input } from '../common/Input'; import { Link } from '../common/Link'; import { @@ -22,9 +26,6 @@ import { ModalCloseButton, ModalHeader, } from '../common/Modal'; -import { Paragraph } from '../common/Paragraph'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; import { useResponsive } from '../responsive/ResponsiveProvider'; type CreateEncryptionKeyModalProps = { diff --git a/packages/desktop-client/src/components/modals/CreateLocalAccountModal.tsx b/packages/desktop-client/src/components/modals/CreateLocalAccountModal.tsx index 29a43ce6410..7cf6ee47112 100644 --- a/packages/desktop-client/src/components/modals/CreateLocalAccountModal.tsx +++ b/packages/desktop-client/src/components/modals/CreateLocalAccountModal.tsx @@ -3,6 +3,13 @@ import { type FormEvent, useState } from 'react'; import { Form } from 'react-aria-components'; import { useTranslation, Trans } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { FormError } from '@actual-app/components/form-error'; +import { InitialFocus } from '@actual-app/components/initial-focus'; +import { InlineField } from '@actual-app/components/inline-field'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { closeModal } from 'loot-core/client/actions'; import { createAccount } from 'loot-core/client/queries/queriesSlice'; import { toRelaxedNumber } from 'loot-core/shared/util'; @@ -11,10 +18,6 @@ import * as useAccounts from '../../hooks/useAccounts'; import { useNavigate } from '../../hooks/useNavigate'; import { useDispatch } from '../../redux'; import { theme } from '../../style'; -import { Button } from '../common/Button2'; -import { FormError } from '../common/FormError'; -import { InitialFocus } from '../common/InitialFocus'; -import { InlineField } from '../common/InlineField'; import { Input } from '../common/Input'; import { Link } from '../common/Link'; import { @@ -24,8 +27,6 @@ import { ModalHeader, ModalTitle, } from '../common/Modal'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; import { Checkbox } from '../forms'; import { validateAccountName } from '../util/accountValidation'; @@ -85,7 +86,7 @@ export function CreateLocalAccountModal() { />
- + - + ([]); useEffect(() => { - send('access-get-available-users', defaultUserAccess.fileId).then( - (data: Awaited>) => { - if ('error' in data) { - setSetError(data.error); - } else { - setAvailableUsers( - data.map(user => [ - user.userId, - user.displayName - ? `${user.displayName} (${user.userName})` - : user.userName, - ]), - ); - } - }, - ); + send('access-get-available-users', defaultUserAccess.fileId).then(data => { + if ('error' in data) { + setSetError(data.error); + } else { + setAvailableUsers( + data.map(user => [ + user.userId, + user.displayName + ? `${user.displayName} (${user.userName})` + : user.userName, + ]), + ); + } + }); }, [defaultUserAccess.fileId]); async function onSave(close: () => void) { diff --git a/packages/desktop-client/src/components/modals/EditFieldModal.tsx b/packages/desktop-client/src/components/modals/EditFieldModal.tsx index 92503b70e89..cc6d55fe695 100644 --- a/packages/desktop-client/src/components/modals/EditFieldModal.tsx +++ b/packages/desktop-client/src/components/modals/EditFieldModal.tsx @@ -6,6 +6,8 @@ import React, { } from 'react'; import { useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { View } from '@actual-app/components/view'; import { parseISO, format as formatDate, parse as parseDate } from 'date-fns'; import { currentDay, dayFromDate } from 'loot-core/shared/months'; @@ -13,10 +15,8 @@ import { amountToInteger } from 'loot-core/shared/util'; import { useDateFormat } from '../../hooks/useDateFormat'; import { theme } from '../../style'; -import { Button } from '../common/Button2'; import { Input } from '../common/Input'; import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal'; -import { View } from '../common/View'; import { SectionLabel } from '../forms'; import { useResponsive } from '../responsive/ResponsiveProvider'; import { DateSelect } from '../select/DateSelect'; diff --git a/packages/desktop-client/src/components/modals/EditRuleModal.jsx b/packages/desktop-client/src/components/modals/EditRuleModal.jsx index 8dfb9b59473..1296aeb7bbb 100644 --- a/packages/desktop-client/src/components/modals/EditRuleModal.jsx +++ b/packages/desktop-client/src/components/modals/EditRuleModal.jsx @@ -1,6 +1,13 @@ import React, { useState, useEffect, useRef, useMemo } from 'react'; import { useTranslation, Trans } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Menu } from '@actual-app/components/menu'; +import { Stack } from '@actual-app/components/stack'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { Tooltip } from '@actual-app/components/tooltip'; +import { View } from '@actual-app/components/view'; import { css } from '@emotion/css'; import { v4 as uuid } from 'uuid'; @@ -35,15 +42,9 @@ import { useSelected, SelectedProvider } from '../../hooks/useSelected'; import { SvgDelete, SvgAdd, SvgSubtract } from '../../icons/v0'; import { SvgAlignLeft, SvgCode, SvgInformationOutline } from '../../icons/v1'; import { useDispatch } from '../../redux'; -import { styles, theme } from '../../style'; -import { Button } from '../common/Button2'; -import { Menu } from '../common/Menu'; +import { theme } from '../../style'; import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal'; import { Select } from '../common/Select'; -import { Stack } from '../common/Stack'; -import { Text } from '../common/Text'; -import { Tooltip } from '../common/Tooltip'; -import { View } from '../common/View'; import { StatusBadge } from '../schedules/StatusBadge'; import { SimpleTransactionsTable } from '../transactions/SimpleTransactionsTable'; import { BetweenAmountInput } from '../util/AmountInput'; @@ -201,14 +202,7 @@ function FieldError({ type }) { function Editor({ error, style, children }) { return ( - + {children} {error && } @@ -1008,12 +1002,6 @@ export function EditRuleModal({ defaultRule, onSave: originalOnSave }) { } } - const editorStyle = { - color: theme.pillText, - backgroundColor: theme.pillBackground, - borderRadius: 4, - }; - // Enable editing existing split rules even if the feature has since been disabled. const showSplitButton = actionSplits.length > 0; @@ -1104,7 +1092,7 @@ export function EditRuleModal({ defaultRule, onSave: originalOnSave }) { setConditions(conds)} /> @@ -1185,7 +1173,7 @@ export function EditRuleModal({ defaultRule, onSave: originalOnSave }) { 'append-notes', ]} action={action} - editorStyle={editorStyle} + editorStyle={styles.editorPill} onChange={(name, value) => { onChangeAction(action, name, value); }} diff --git a/packages/desktop-client/src/components/modals/EditUser.tsx b/packages/desktop-client/src/components/modals/EditUser.tsx index edab7b481fe..5761c98c6fd 100644 --- a/packages/desktop-client/src/components/modals/EditUser.tsx +++ b/packages/desktop-client/src/components/modals/EditUser.tsx @@ -1,19 +1,21 @@ import { useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Stack } from '@actual-app/components/stack'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { addNotification, popModal, signOut } from 'loot-core/client/actions'; import { send } from 'loot-core/platform/client/fetch'; import { PossibleRoles, type UserEntity } from 'loot-core/types/models/user'; import { useDispatch } from '../../redux'; -import { styles, theme } from '../../style'; -import { Button } from '../common/Button2'; +import { theme } from '../../style'; import { Input } from '../common/Input'; import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal'; import { Select } from '../common/Select'; -import { Stack } from '../common/Stack'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; import { Checkbox, FormField, FormLabel } from '../forms'; type User = UserEntity; diff --git a/packages/desktop-client/src/components/modals/EnvelopeBalanceMenuModal.tsx b/packages/desktop-client/src/components/modals/EnvelopeBalanceMenuModal.tsx index 5dc42192978..6625973d8a0 100644 --- a/packages/desktop-client/src/components/modals/EnvelopeBalanceMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/EnvelopeBalanceMenuModal.tsx @@ -4,10 +4,14 @@ import React, { } from 'react'; import { useTranslation } from 'react-i18next'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { envelopeBudget } from 'loot-core/client/queries'; import { useCategory } from '../../hooks/useCategory'; -import { theme, styles } from '../../style'; +import { theme } from '../../style'; import { BalanceWithCarryover, CarryoverIndicator, @@ -19,8 +23,6 @@ import { ModalHeader, ModalTitle, } from '../common/Modal'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; import { CellValueText } from '../spreadsheet/CellValue'; type EnvelopeBalanceMenuModalProps = ComponentPropsWithoutRef< diff --git a/packages/desktop-client/src/components/modals/EnvelopeBudgetMenuModal.tsx b/packages/desktop-client/src/components/modals/EnvelopeBudgetMenuModal.tsx index a7f1d9eb5a3..4d32cce5f0c 100644 --- a/packages/desktop-client/src/components/modals/EnvelopeBudgetMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/EnvelopeBudgetMenuModal.tsx @@ -6,11 +6,15 @@ import React, { } from 'react'; import { useTranslation } from 'react-i18next'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { envelopeBudget } from 'loot-core/client/queries'; import { amountToInteger, integerToAmount } from 'loot-core/shared/util'; import { useCategory } from '../../hooks/useCategory'; -import { theme, styles } from '../../style'; +import { theme } from '../../style'; import { BudgetMenu } from '../budget/envelope/BudgetMenu'; import { useEnvelopeSheetValue } from '../budget/envelope/EnvelopeBudgetComponents'; import { @@ -19,8 +23,6 @@ import { ModalHeader, ModalTitle, } from '../common/Modal'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; import { FocusableAmountInput } from '../mobile/transactions/FocusableAmountInput'; type EnvelopeBudgetMenuModalProps = ComponentPropsWithoutRef< diff --git a/packages/desktop-client/src/components/modals/EnvelopeBudgetMonthMenuModal.tsx b/packages/desktop-client/src/components/modals/EnvelopeBudgetMonthMenuModal.tsx index 543b13784fa..932b58ee216 100644 --- a/packages/desktop-client/src/components/modals/EnvelopeBudgetMonthMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/EnvelopeBudgetMonthMenuModal.tsx @@ -2,6 +2,9 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { styles } from '@actual-app/components/styles'; +import { View } from '@actual-app/components/view'; import { css } from '@emotion/css'; import * as monthUtils from 'loot-core/shared/months'; @@ -10,11 +13,9 @@ import { useNotes } from '../../hooks/useNotes'; import { useUndo } from '../../hooks/useUndo'; import { SvgCheveronDown, SvgCheveronUp } from '../../icons/v1'; import { SvgNotesPaper } from '../../icons/v2'; -import { styles, theme, type CSSProperties } from '../../style'; +import { theme, type CSSProperties } from '../../style'; import { BudgetMonthMenu } from '../budget/envelope/budgetsummary/BudgetMonthMenu'; -import { Button } from '../common/Button2'; import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal'; -import { View } from '../common/View'; import { Notes } from '../Notes'; type EnvelopeBudgetMonthMenuModalProps = { diff --git a/packages/desktop-client/src/components/modals/EnvelopeBudgetSummaryModal.tsx b/packages/desktop-client/src/components/modals/EnvelopeBudgetSummaryModal.tsx index 1acd608fe8c..f860b0950c8 100644 --- a/packages/desktop-client/src/components/modals/EnvelopeBudgetSummaryModal.tsx +++ b/packages/desktop-client/src/components/modals/EnvelopeBudgetSummaryModal.tsx @@ -1,6 +1,8 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; +import { styles } from '@actual-app/components/styles'; + import { collapseModals, pushModal } from 'loot-core/client/actions'; import { envelopeBudget } from 'loot-core/client/queries'; import { format, sheetForMonth, prevMonth } from 'loot-core/shared/months'; @@ -9,7 +11,6 @@ import { groupById, integerToCurrency } from 'loot-core/shared/util'; import { useCategories } from '../../hooks/useCategories'; import { useUndo } from '../../hooks/useUndo'; import { useDispatch } from '../../redux'; -import { styles } from '../../style'; import { ToBudgetAmount } from '../budget/envelope/budgetsummary/ToBudgetAmount'; import { TotalsList } from '../budget/envelope/budgetsummary/TotalsList'; import { useEnvelopeSheetValue } from '../budget/envelope/EnvelopeBudgetComponents'; diff --git a/packages/desktop-client/src/components/modals/EnvelopeToBudgetMenuModal.tsx b/packages/desktop-client/src/components/modals/EnvelopeToBudgetMenuModal.tsx index 3466db52d69..43371ccaff6 100644 --- a/packages/desktop-client/src/components/modals/EnvelopeToBudgetMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/EnvelopeToBudgetMenuModal.tsx @@ -3,7 +3,9 @@ import React, { type CSSProperties, } from 'react'; -import { theme, styles } from '../../style'; +import { styles } from '@actual-app/components/styles'; + +import { theme } from '../../style'; import { ToBudgetMenu } from '../budget/envelope/budgetsummary/ToBudgetMenu'; import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal'; diff --git a/packages/desktop-client/src/components/modals/FixEncryptionKeyModal.tsx b/packages/desktop-client/src/components/modals/FixEncryptionKeyModal.tsx index 91c3cd01094..904e00ba2b2 100644 --- a/packages/desktop-client/src/components/modals/FixEncryptionKeyModal.tsx +++ b/packages/desktop-client/src/components/modals/FixEncryptionKeyModal.tsx @@ -3,13 +3,18 @@ import React, { useState } from 'react'; import { Form } from 'react-aria-components'; import { useTranslation } from 'react-i18next'; +import { Button, ButtonWithLoading } from '@actual-app/components/button'; +import { InitialFocus } from '@actual-app/components/initial-focus'; +import { Paragraph } from '@actual-app/components/paragraph'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { type FinanceModals } from 'loot-core/client/state-types/modals'; import { send } from 'loot-core/platform/client/fetch'; import { getTestKeyError } from 'loot-core/shared/errors'; -import { styles, theme } from '../../style'; -import { Button, ButtonWithLoading } from '../common/Button2'; -import { InitialFocus } from '../common/InitialFocus'; +import { theme } from '../../style'; import { Input } from '../common/Input'; import { Link } from '../common/Link'; import { @@ -18,9 +23,6 @@ import { ModalCloseButton, ModalHeader, } from '../common/Modal'; -import { Paragraph } from '../common/Paragraph'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; import { useResponsive } from '../responsive/ResponsiveProvider'; type FixEncryptionKeyModalProps = { diff --git a/packages/desktop-client/src/components/modals/GoCardlessExternalMsgModal.tsx b/packages/desktop-client/src/components/modals/GoCardlessExternalMsgModal.tsx index c89f1a424f4..c477d1178c1 100644 --- a/packages/desktop-client/src/components/modals/GoCardlessExternalMsgModal.tsx +++ b/packages/desktop-client/src/components/modals/GoCardlessExternalMsgModal.tsx @@ -2,6 +2,10 @@ import React, { useEffect, useState, useRef } from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Paragraph } from '@actual-app/components/paragraph'; +import { View } from '@actual-app/components/view'; + import { pushModal } from 'loot-core/client/actions/modals'; import { sendCatch } from 'loot-core/platform/client/fetch'; import { @@ -15,11 +19,8 @@ import { useDispatch } from '../../redux'; import { theme } from '../../style'; import { Error, Warning } from '../alerts'; import { Autocomplete } from '../autocomplete/Autocomplete'; -import { Button } from '../common/Button2'; import { Link } from '../common/Link'; import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal'; -import { Paragraph } from '../common/Paragraph'; -import { View } from '../common/View'; import { FormField, FormLabel } from '../forms'; import { COUNTRY_OPTIONS } from '../util/countries'; diff --git a/packages/desktop-client/src/components/modals/GoCardlessInitialiseModal.tsx b/packages/desktop-client/src/components/modals/GoCardlessInitialiseModal.tsx index 39cac360de4..0bdd4761fd4 100644 --- a/packages/desktop-client/src/components/modals/GoCardlessInitialiseModal.tsx +++ b/packages/desktop-client/src/components/modals/GoCardlessInitialiseModal.tsx @@ -2,12 +2,15 @@ import React, { useState } from 'react'; import { useTranslation, Trans } from 'react-i18next'; +import { ButtonWithLoading } from '@actual-app/components/button'; +import { InitialFocus } from '@actual-app/components/initial-focus'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { send } from 'loot-core/platform/client/fetch'; import { getSecretsError } from 'loot-core/shared/errors'; import { Error } from '../alerts'; -import { ButtonWithLoading } from '../common/Button2'; -import { InitialFocus } from '../common/InitialFocus'; import { Input } from '../common/Input'; import { Link } from '../common/Link'; import { @@ -16,8 +19,6 @@ import { ModalCloseButton, ModalHeader, } from '../common/Modal'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; import { FormField, FormLabel } from '../forms'; type GoCardlessInitialiseProps = { diff --git a/packages/desktop-client/src/components/modals/GoalTemplateModal.tsx b/packages/desktop-client/src/components/modals/GoalTemplateModal.tsx index 12b06548ad0..6b50547378d 100644 --- a/packages/desktop-client/src/components/modals/GoalTemplateModal.tsx +++ b/packages/desktop-client/src/components/modals/GoalTemplateModal.tsx @@ -1,10 +1,11 @@ import { Trans, useTranslation } from 'react-i18next'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { theme } from '../../style'; import { Link } from '../common/Link'; import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; import { TableHeader, Row, Field } from '../table'; export function GoalTemplateModal() { diff --git a/packages/desktop-client/src/components/modals/HoldBufferModal.tsx b/packages/desktop-client/src/components/modals/HoldBufferModal.tsx index 199411c00c5..c94b3a52c6e 100644 --- a/packages/desktop-client/src/components/modals/HoldBufferModal.tsx +++ b/packages/desktop-client/src/components/modals/HoldBufferModal.tsx @@ -1,14 +1,15 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { InitialFocus } from '@actual-app/components/initial-focus'; +import { styles } from '@actual-app/components/styles'; +import { View } from '@actual-app/components/view'; + import { envelopeBudget } from 'loot-core/client/queries'; -import { styles } from '../../style'; import { useEnvelopeSheetValue } from '../budget/envelope/EnvelopeBudgetComponents'; -import { Button } from '../common/Button2'; -import { InitialFocus } from '../common/InitialFocus'; import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal'; -import { View } from '../common/View'; import { FieldLabel } from '../mobile/MobileForms'; import { AmountInput } from '../util/AmountInput'; diff --git a/packages/desktop-client/src/components/modals/ImportTransactionsModal/CheckboxOption.tsx b/packages/desktop-client/src/components/modals/ImportTransactionsModal/CheckboxOption.tsx index 5af399a4396..b456f71d637 100644 --- a/packages/desktop-client/src/components/modals/ImportTransactionsModal/CheckboxOption.tsx +++ b/packages/desktop-client/src/components/modals/ImportTransactionsModal/CheckboxOption.tsx @@ -4,8 +4,9 @@ import React, { type ReactNode, } from 'react'; +import { View } from '@actual-app/components/view'; + import { theme } from '../../../style'; -import { View } from '../../common/View'; import { Checkbox } from '../../forms'; type CheckboxOptionProps = { diff --git a/packages/desktop-client/src/components/modals/ImportTransactionsModal/DateFormatSelect.tsx b/packages/desktop-client/src/components/modals/ImportTransactionsModal/DateFormatSelect.tsx index d743c71dedc..5e543568c3d 100644 --- a/packages/desktop-client/src/components/modals/ImportTransactionsModal/DateFormatSelect.tsx +++ b/packages/desktop-client/src/components/modals/ImportTransactionsModal/DateFormatSelect.tsx @@ -1,7 +1,8 @@ import React from 'react'; +import { View } from '@actual-app/components/view'; + import { Select } from '../../common/Select'; -import { View } from '../../common/View'; import { SectionLabel } from '../../forms'; import { diff --git a/packages/desktop-client/src/components/modals/ImportTransactionsModal/FieldMappings.tsx b/packages/desktop-client/src/components/modals/ImportTransactionsModal/FieldMappings.tsx index e4a0ab4e80a..dc92a26bfd5 100644 --- a/packages/desktop-client/src/components/modals/ImportTransactionsModal/FieldMappings.tsx +++ b/packages/desktop-client/src/components/modals/ImportTransactionsModal/FieldMappings.tsx @@ -1,7 +1,8 @@ import React from 'react'; -import { Stack } from '../../common/Stack'; -import { View } from '../../common/View'; +import { Stack } from '@actual-app/components/stack'; +import { View } from '@actual-app/components/view'; + import { SectionLabel } from '../../forms'; import { SelectField } from './SelectField'; diff --git a/packages/desktop-client/src/components/modals/ImportTransactionsModal/ImportTransactionsModal.jsx b/packages/desktop-client/src/components/modals/ImportTransactionsModal/ImportTransactionsModal.jsx index aaf616cb7d0..f44026d0bf4 100644 --- a/packages/desktop-client/src/components/modals/ImportTransactionsModal/ImportTransactionsModal.jsx +++ b/packages/desktop-client/src/components/modals/ImportTransactionsModal/ImportTransactionsModal.jsx @@ -1,6 +1,10 @@ import React, { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +import { Button, ButtonWithLoading } from '@actual-app/components/button'; +import { Stack } from '@actual-app/components/stack'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; import deepEqual from 'deep-equal'; import { @@ -16,13 +20,9 @@ import { useDateFormat } from '../../../hooks/useDateFormat'; import { useSyncedPrefs } from '../../../hooks/useSyncedPrefs'; import { useDispatch } from '../../../redux'; import { theme } from '../../../style'; -import { Button, ButtonWithLoading } from '../../common/Button2'; import { Input } from '../../common/Input'; import { Modal, ModalCloseButton, ModalHeader } from '../../common/Modal'; import { Select } from '../../common/Select'; -import { Stack } from '../../common/Stack'; -import { Text } from '../../common/Text'; -import { View } from '../../common/View'; import { SectionLabel } from '../../forms'; import { TableHeader, TableWithNavigator } from '../../table'; @@ -233,7 +233,7 @@ export function ImportTransactionsModal({ options }) { const { amount } = parseAmountFields( trans, splitMode, - inOutMode, + isOfxFile(filetype) ? false : inOutMode, outValue, flipAmount, multiplierAmount, @@ -575,7 +575,7 @@ export function ImportTransactionsModal({ options }) { const { amount } = parseAmountFields( trans, splitMode, - inOutMode, + isOfxFile(filetype) ? false : inOutMode, outValue, flipAmount, multiplierAmount, diff --git a/packages/desktop-client/src/components/modals/ImportTransactionsModal/InOutOption.tsx b/packages/desktop-client/src/components/modals/ImportTransactionsModal/InOutOption.tsx index 80b565789ad..07f52985366 100644 --- a/packages/desktop-client/src/components/modals/ImportTransactionsModal/InOutOption.tsx +++ b/packages/desktop-client/src/components/modals/ImportTransactionsModal/InOutOption.tsx @@ -1,7 +1,8 @@ import React from 'react'; +import { View } from '@actual-app/components/view'; + import { Input } from '../../common/Input'; -import { View } from '../../common/View'; import { CheckboxOption } from './CheckboxOption'; diff --git a/packages/desktop-client/src/components/modals/ImportTransactionsModal/MultiplierOption.tsx b/packages/desktop-client/src/components/modals/ImportTransactionsModal/MultiplierOption.tsx index 4eb4fd23dca..4c48384f0bf 100644 --- a/packages/desktop-client/src/components/modals/ImportTransactionsModal/MultiplierOption.tsx +++ b/packages/desktop-client/src/components/modals/ImportTransactionsModal/MultiplierOption.tsx @@ -1,7 +1,8 @@ import React, { type ComponentProps } from 'react'; +import { View } from '@actual-app/components/view'; + import { Input } from '../../common/Input'; -import { View } from '../../common/View'; import { CheckboxOption } from './CheckboxOption'; diff --git a/packages/desktop-client/src/components/modals/ImportTransactionsModal/ParsedDate.tsx b/packages/desktop-client/src/components/modals/ImportTransactionsModal/ParsedDate.tsx index c32c9e09c69..7a6beded1b4 100644 --- a/packages/desktop-client/src/components/modals/ImportTransactionsModal/ParsedDate.tsx +++ b/packages/desktop-client/src/components/modals/ImportTransactionsModal/ParsedDate.tsx @@ -1,8 +1,9 @@ import React from 'react'; import { Trans } from 'react-i18next'; +import { Text } from '@actual-app/components/text'; + import { theme } from '../../../style'; -import { Text } from '../../common/Text'; import { formatDate, parseDate } from './utils'; diff --git a/packages/desktop-client/src/components/modals/ImportTransactionsModal/SubLabel.tsx b/packages/desktop-client/src/components/modals/ImportTransactionsModal/SubLabel.tsx index 7994c94fcf7..58a3490279c 100644 --- a/packages/desktop-client/src/components/modals/ImportTransactionsModal/SubLabel.tsx +++ b/packages/desktop-client/src/components/modals/ImportTransactionsModal/SubLabel.tsx @@ -1,7 +1,8 @@ import React from 'react'; +import { Text } from '@actual-app/components/text'; + import { theme } from '../../../style'; -import { Text } from '../../common/Text'; type SubLabelProps = { title: string; diff --git a/packages/desktop-client/src/components/modals/ImportTransactionsModal/Transaction.tsx b/packages/desktop-client/src/components/modals/ImportTransactionsModal/Transaction.tsx index 738ab9494ab..257c4833d8a 100644 --- a/packages/desktop-client/src/components/modals/ImportTransactionsModal/Transaction.tsx +++ b/packages/desktop-client/src/components/modals/ImportTransactionsModal/Transaction.tsx @@ -1,13 +1,15 @@ import React, { type ComponentProps, useMemo } from 'react'; +import { Stack } from '@actual-app/components/stack'; +import { styles } from '@actual-app/components/styles'; +import { Tooltip } from '@actual-app/components/tooltip'; +import { View } from '@actual-app/components/view'; + import { amountToCurrency } from 'loot-core/shared/util'; import { type CategoryEntity } from 'loot-core/types/models'; import { SvgDownAndRightArrow } from '../../../icons/v2'; -import { theme, styles } from '../../../style'; -import { Stack } from '../../common/Stack'; -import { Tooltip } from '../../common/Tooltip'; -import { View } from '../../common/View'; +import { theme } from '../../../style'; import { Checkbox } from '../../forms'; import { Row, Field } from '../../table'; diff --git a/packages/desktop-client/src/components/modals/KeyboardShortcutModal.tsx b/packages/desktop-client/src/components/modals/KeyboardShortcutModal.tsx index fb75b1a11cd..cbe97472759 100644 --- a/packages/desktop-client/src/components/modals/KeyboardShortcutModal.tsx +++ b/packages/desktop-client/src/components/modals/KeyboardShortcutModal.tsx @@ -2,11 +2,12 @@ import { type CSSProperties } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import * as Platform from 'loot-core/client/platform'; import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; type KeyIconProps = { shortcut: string; diff --git a/packages/desktop-client/src/components/modals/MergeUnusedPayeesModal.tsx b/packages/desktop-client/src/components/modals/MergeUnusedPayeesModal.tsx index 9dbf9df1036..33b79a8f88a 100644 --- a/packages/desktop-client/src/components/modals/MergeUnusedPayeesModal.tsx +++ b/packages/desktop-client/src/components/modals/MergeUnusedPayeesModal.tsx @@ -1,6 +1,11 @@ import React, { useState, useRef, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Paragraph } from '@actual-app/components/paragraph'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { replaceModal } from 'loot-core/client/actions/modals'; import { send } from 'loot-core/platform/client/fetch'; import { type PayeeEntity } from 'loot-core/types/models'; @@ -9,11 +14,7 @@ import { usePayees } from '../../hooks/usePayees'; import { useSelector, useDispatch } from '../../redux'; import { theme } from '../../style'; import { Information } from '../alerts'; -import { Button } from '../common/Button2'; import { Modal, ModalButtons } from '../common/Modal'; -import { Paragraph } from '../common/Paragraph'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; const highlightStyle = { color: theme.pageTextPositive }; diff --git a/packages/desktop-client/src/components/modals/NotesModal.tsx b/packages/desktop-client/src/components/modals/NotesModal.tsx index 716828ccdb2..de105976b62 100644 --- a/packages/desktop-client/src/components/modals/NotesModal.tsx +++ b/packages/desktop-client/src/components/modals/NotesModal.tsx @@ -2,11 +2,12 @@ import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { View } from '@actual-app/components/view'; + import { useNotes } from '../../hooks/useNotes'; import { SvgCheck } from '../../icons/v2'; -import { Button } from '../common/Button2'; import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal'; -import { View } from '../common/View'; import { Notes } from '../Notes'; type NotesModalProps = { diff --git a/packages/desktop-client/src/components/modals/OpenIDEnableModal.tsx b/packages/desktop-client/src/components/modals/OpenIDEnableModal.tsx index 18dbd7b6caf..de4729edd9a 100644 --- a/packages/desktop-client/src/components/modals/OpenIDEnableModal.tsx +++ b/packages/desktop-client/src/components/modals/OpenIDEnableModal.tsx @@ -1,6 +1,11 @@ import { useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Label } from '@actual-app/components/label'; +import { styles } from '@actual-app/components/styles'; +import { View } from '@actual-app/components/view'; + import { closeBudget, popModal } from 'loot-core/client/actions'; import { send } from 'loot-core/platform/client/fetch'; import * as asyncStorage from 'loot-core/platform/server/asyncStorage'; @@ -8,12 +13,9 @@ import { getOpenIdErrors } from 'loot-core/shared/errors'; import { type OpenIdConfig } from 'loot-core/types/models/openid'; import { useDispatch } from '../../redux'; -import { theme, styles } from '../../style'; +import { theme } from '../../style'; import { Error } from '../alerts'; -import { Button } from '../common/Button2'; -import { Label } from '../common/Label'; import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal'; -import { View } from '../common/View'; import { OpenIdForm } from '../manager/subscribe/OpenIdForm'; import { useRefreshLoginMethods } from '../ServerContext'; diff --git a/packages/desktop-client/src/components/modals/OutOfSyncMigrationsModal.tsx b/packages/desktop-client/src/components/modals/OutOfSyncMigrationsModal.tsx index 2a904f26345..a8aa2c09895 100644 --- a/packages/desktop-client/src/components/modals/OutOfSyncMigrationsModal.tsx +++ b/packages/desktop-client/src/components/modals/OutOfSyncMigrationsModal.tsx @@ -1,15 +1,16 @@ import React from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Paragraph } from '@actual-app/components/paragraph'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { closeBudget } from 'loot-core/client/actions'; import { useDispatch } from '../../redux'; -import { Button } from '../common/Button2'; import { Link } from '../common/Link'; import { Modal, ModalHeader, ModalTitle } from '../common/Modal'; -import { Paragraph } from '../common/Paragraph'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; export function OutOfSyncMigrationsModal() { const dispatch = useDispatch(); diff --git a/packages/desktop-client/src/components/modals/PasswordEnableModal.tsx b/packages/desktop-client/src/components/modals/PasswordEnableModal.tsx index 067d127fcf1..1567a775fab 100644 --- a/packages/desktop-client/src/components/modals/PasswordEnableModal.tsx +++ b/packages/desktop-client/src/components/modals/PasswordEnableModal.tsx @@ -1,17 +1,19 @@ import { useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Label } from '@actual-app/components/label'; +import { styles } from '@actual-app/components/styles'; +import { View } from '@actual-app/components/view'; + import { closeBudget, popModal } from 'loot-core/client/actions'; import { send } from 'loot-core/platform/client/fetch'; import * as asyncStorage from 'loot-core/platform/server/asyncStorage'; import { useDispatch } from '../../redux'; -import { theme, styles } from '../../style'; +import { theme } from '../../style'; import { Error as ErrorAlert } from '../alerts'; -import { Button } from '../common/Button2'; -import { Label } from '../common/Label'; import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal'; -import { View } from '../common/View'; import { FormField } from '../forms'; import { ConfirmOldPasswordForm, diff --git a/packages/desktop-client/src/components/modals/ScheduledTransactionMenuModal.tsx b/packages/desktop-client/src/components/modals/ScheduledTransactionMenuModal.tsx index 53efee0fa35..ec148781abe 100644 --- a/packages/desktop-client/src/components/modals/ScheduledTransactionMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/ScheduledTransactionMenuModal.tsx @@ -5,6 +5,11 @@ import React, { } from 'react'; import { useTranslation } from 'react-i18next'; +import { Menu } from '@actual-app/components/menu'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { useSchedules } from 'loot-core/client/data-hooks/schedules'; import { format } from 'loot-core/shared/months'; import { q } from 'loot-core/shared/query'; @@ -13,16 +18,13 @@ import { extractScheduleConds, } from 'loot-core/shared/schedules'; -import { theme, styles } from '../../style'; -import { Menu } from '../common/Menu'; +import { theme } from '../../style'; import { Modal, ModalCloseButton, ModalHeader, ModalTitle, } from '../common/Modal'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; type ScheduledTransactionMenuModalProps = ScheduledTransactionMenuProps; diff --git a/packages/desktop-client/src/components/modals/SelectLinkedAccountsModal.jsx b/packages/desktop-client/src/components/modals/SelectLinkedAccountsModal.jsx index 10c3fd8777d..545c47caefb 100644 --- a/packages/desktop-client/src/components/modals/SelectLinkedAccountsModal.jsx +++ b/packages/desktop-client/src/components/modals/SelectLinkedAccountsModal.jsx @@ -1,6 +1,10 @@ import React, { useState } from 'react'; import { useTranslation, Trans } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { linkAccount, linkAccountSimpleFin, @@ -12,10 +16,7 @@ import { useAccounts } from '../../hooks/useAccounts'; import { useDispatch } from '../../redux'; import { theme } from '../../style'; import { Autocomplete } from '../autocomplete/Autocomplete'; -import { Button } from '../common/Button2'; import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; import { PrivacyFilter } from '../PrivacyFilter'; import { TableHeader, Table, Row, Field } from '../table'; diff --git a/packages/desktop-client/src/components/modals/SimpleFinInitialiseModal.tsx b/packages/desktop-client/src/components/modals/SimpleFinInitialiseModal.tsx index d051d30f9be..c5460661436 100644 --- a/packages/desktop-client/src/components/modals/SimpleFinInitialiseModal.tsx +++ b/packages/desktop-client/src/components/modals/SimpleFinInitialiseModal.tsx @@ -2,11 +2,14 @@ import React, { useState } from 'react'; import { useTranslation, Trans } from 'react-i18next'; +import { ButtonWithLoading } from '@actual-app/components/button'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { send } from 'loot-core/platform/client/fetch'; import { getSecretsError } from 'loot-core/shared/errors'; import { Error } from '../alerts'; -import { ButtonWithLoading } from '../common/Button2'; import { Input } from '../common/Input'; import { Link } from '../common/Link'; import { @@ -15,8 +18,6 @@ import { ModalCloseButton, ModalHeader, } from '../common/Modal'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; import { FormField, FormLabel } from '../forms'; type SimpleFinInitialiseProps = { diff --git a/packages/desktop-client/src/components/modals/SingleInputModal.tsx b/packages/desktop-client/src/components/modals/SingleInputModal.tsx index c1d5a12ed68..e56fa50844b 100644 --- a/packages/desktop-client/src/components/modals/SingleInputModal.tsx +++ b/packages/desktop-client/src/components/modals/SingleInputModal.tsx @@ -7,12 +7,13 @@ import React, { } from 'react'; import { Form } from 'react-aria-components'; -import { styles } from '../../style'; -import { Button } from '../common/Button2'; -import { FormError } from '../common/FormError'; -import { InitialFocus } from '../common/InitialFocus'; +import { Button } from '@actual-app/components/button'; +import { FormError } from '@actual-app/components/form-error'; +import { InitialFocus } from '@actual-app/components/initial-focus'; +import { styles } from '@actual-app/components/styles'; +import { View } from '@actual-app/components/view'; + import { Modal, ModalCloseButton, type ModalHeader } from '../common/Modal'; -import { View } from '../common/View'; import { InputField } from '../mobile/MobileForms'; type SingleInputModalProps = { diff --git a/packages/desktop-client/src/components/modals/TrackingBalanceMenuModal.tsx b/packages/desktop-client/src/components/modals/TrackingBalanceMenuModal.tsx index 2a070152c53..9d20517b6a3 100644 --- a/packages/desktop-client/src/components/modals/TrackingBalanceMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/TrackingBalanceMenuModal.tsx @@ -4,10 +4,14 @@ import React, { } from 'react'; import { useTranslation } from 'react-i18next'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { trackingBudget } from 'loot-core/client/queries'; import { useCategory } from '../../hooks/useCategory'; -import { theme, styles } from '../../style'; +import { theme } from '../../style'; import { BalanceWithCarryover, CarryoverIndicator, @@ -19,8 +23,6 @@ import { ModalHeader, ModalTitle, } from '../common/Modal'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; import { CellValueText } from '../spreadsheet/CellValue'; type TrackingBalanceMenuModalProps = ComponentPropsWithoutRef< diff --git a/packages/desktop-client/src/components/modals/TrackingBudgetMenuModal.tsx b/packages/desktop-client/src/components/modals/TrackingBudgetMenuModal.tsx index 33d8738caf7..abceb5c7d46 100644 --- a/packages/desktop-client/src/components/modals/TrackingBudgetMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/TrackingBudgetMenuModal.tsx @@ -6,11 +6,15 @@ import React, { } from 'react'; import { useTranslation } from 'react-i18next'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { trackingBudget } from 'loot-core/client/queries'; import { amountToInteger, integerToAmount } from 'loot-core/shared/util'; import { useCategory } from '../../hooks/useCategory'; -import { theme, styles } from '../../style'; +import { theme } from '../../style'; import { BudgetMenu } from '../budget/tracking/BudgetMenu'; import { useTrackingSheetValue } from '../budget/tracking/TrackingBudgetComponents'; import { @@ -19,8 +23,6 @@ import { ModalHeader, ModalTitle, } from '../common/Modal'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; import { FocusableAmountInput } from '../mobile/transactions/FocusableAmountInput'; type TrackingBudgetMenuModalProps = ComponentPropsWithoutRef< diff --git a/packages/desktop-client/src/components/modals/TrackingBudgetMonthMenuModal.tsx b/packages/desktop-client/src/components/modals/TrackingBudgetMonthMenuModal.tsx index b2aeb325d65..f136a8ac430 100644 --- a/packages/desktop-client/src/components/modals/TrackingBudgetMonthMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/TrackingBudgetMonthMenuModal.tsx @@ -2,6 +2,9 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { styles } from '@actual-app/components/styles'; +import { View } from '@actual-app/components/view'; import { css } from '@emotion/css'; import * as monthUtils from 'loot-core/shared/months'; @@ -10,11 +13,9 @@ import { useNotes } from '../../hooks/useNotes'; import { useUndo } from '../../hooks/useUndo'; import { SvgCheveronDown, SvgCheveronUp } from '../../icons/v1'; import { SvgNotesPaper } from '../../icons/v2'; -import { type CSSProperties, styles, theme } from '../../style'; +import { type CSSProperties, theme } from '../../style'; import { BudgetMonthMenu } from '../budget/tracking/budgetsummary/BudgetMonthMenu'; -import { Button } from '../common/Button2'; import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal'; -import { View } from '../common/View'; import { Notes } from '../Notes'; type TrackingBudgetMonthMenuModalProps = { diff --git a/packages/desktop-client/src/components/modals/TrackingBudgetSummaryModal.tsx b/packages/desktop-client/src/components/modals/TrackingBudgetSummaryModal.tsx index 52309e339fa..1c1b75237e7 100644 --- a/packages/desktop-client/src/components/modals/TrackingBudgetSummaryModal.tsx +++ b/packages/desktop-client/src/components/modals/TrackingBudgetSummaryModal.tsx @@ -1,15 +1,16 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; +import { Stack } from '@actual-app/components/stack'; +import { styles } from '@actual-app/components/styles'; + import { sheetForMonth } from 'loot-core/shared/months'; import * as monthUtils from 'loot-core/shared/months'; -import { styles } from '../../style'; import { ExpenseTotal } from '../budget/tracking/budgetsummary/ExpenseTotal'; import { IncomeTotal } from '../budget/tracking/budgetsummary/IncomeTotal'; import { Saved } from '../budget/tracking/budgetsummary/Saved'; import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal'; -import { Stack } from '../common/Stack'; import { NamespaceContext } from '../spreadsheet/NamespaceContext'; type TrackingBudgetSummaryModalProps = { diff --git a/packages/desktop-client/src/components/modals/TransferModal.tsx b/packages/desktop-client/src/components/modals/TransferModal.tsx index 9cf1a7fa64b..8fdb02ba225 100644 --- a/packages/desktop-client/src/components/modals/TransferModal.tsx +++ b/packages/desktop-client/src/components/modals/TransferModal.tsx @@ -1,20 +1,21 @@ import React, { useMemo, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { InitialFocus } from '@actual-app/components/initial-focus'; +import { styles } from '@actual-app/components/styles'; +import { View } from '@actual-app/components/view'; + import { pushModal } from 'loot-core/client/actions'; import { type CategoryEntity } from 'loot-core/types/models'; import { useCategories } from '../../hooks/useCategories'; import { useDispatch } from '../../redux'; -import { styles } from '../../style'; import { addToBeBudgetedGroup, removeCategoriesFromGroups, } from '../budget/util'; -import { Button } from '../common/Button2'; -import { InitialFocus } from '../common/InitialFocus'; import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal'; -import { View } from '../common/View'; import { FieldLabel, TapField } from '../mobile/MobileForms'; import { AmountInput } from '../util/AmountInput'; diff --git a/packages/desktop-client/src/components/modals/TransferOwnership.tsx b/packages/desktop-client/src/components/modals/TransferOwnership.tsx index 72c886ba457..b03f87c13b5 100644 --- a/packages/desktop-client/src/components/modals/TransferOwnership.tsx +++ b/packages/desktop-client/src/components/modals/TransferOwnership.tsx @@ -1,6 +1,12 @@ import { useEffect, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { Stack } from '@actual-app/components/stack'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { addNotification, closeAndLoadBudget, @@ -14,13 +20,9 @@ import { type Handlers } from 'loot-core/types/handlers'; import { useMetadataPref } from '../../hooks/useMetadataPref'; import { useDispatch, useSelector } from '../../redux'; -import { styles, theme } from '../../style'; -import { Button } from '../common/Button2'; +import { theme } from '../../style'; import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal'; import { Select } from '../common/Select'; -import { Stack } from '../common/Stack'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; import { FormField, FormLabel } from '../forms'; type TransferOwnershipProps = { diff --git a/packages/desktop-client/src/components/modals/manager/ConfirmChangeDocumentDir.tsx b/packages/desktop-client/src/components/modals/manager/ConfirmChangeDocumentDir.tsx index 97f7b9aa22d..1ff4529be6b 100644 --- a/packages/desktop-client/src/components/modals/manager/ConfirmChangeDocumentDir.tsx +++ b/packages/desktop-client/src/components/modals/manager/ConfirmChangeDocumentDir.tsx @@ -1,16 +1,18 @@ import React, { useCallback, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { Button, ButtonWithLoading } from '@actual-app/components/button'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { addNotification } from 'loot-core/client/actions'; import { useGlobalPref } from '../../../hooks/useGlobalPref'; import { useDispatch } from '../../../redux'; -import { theme, styles } from '../../../style'; +import { theme } from '../../../style'; import { Information } from '../../alerts'; -import { Button, ButtonWithLoading } from '../../common/Button2'; import { Modal, ModalCloseButton, ModalHeader } from '../../common/Modal'; -import { Text } from '../../common/Text'; -import { View } from '../../common/View'; import { Checkbox } from '../../forms'; function DirectoryDisplay({ directory }: { directory: string }) { diff --git a/packages/desktop-client/src/components/modals/manager/DeleteFileModal.tsx b/packages/desktop-client/src/components/modals/manager/DeleteFileModal.tsx index fb5ffda8d32..2f927f7b2a5 100644 --- a/packages/desktop-client/src/components/modals/manager/DeleteFileModal.tsx +++ b/packages/desktop-client/src/components/modals/manager/DeleteFileModal.tsx @@ -1,15 +1,16 @@ import React, { useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { ButtonWithLoading } from '@actual-app/components/button'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { deleteBudget } from 'loot-core/client/actions'; import { type File } from 'loot-core/types/file'; import { useDispatch } from '../../../redux'; import { theme } from '../../../style'; -import { ButtonWithLoading } from '../../common/Button2'; import { Modal, ModalCloseButton, ModalHeader } from '../../common/Modal'; -import { Text } from '../../common/Text'; -import { View } from '../../common/View'; type DeleteFileProps = { file: File; diff --git a/packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx b/packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx index 7c5afeb9307..4e030163233 100644 --- a/packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx +++ b/packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx @@ -1,6 +1,13 @@ import React, { useEffect, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { Button, ButtonWithLoading } from '@actual-app/components/button'; +import { FormError } from '@actual-app/components/form-error'; +import { InitialFocus } from '@actual-app/components/initial-focus'; +import { InlineField } from '@actual-app/components/inline-field'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { addNotification, duplicateBudget, @@ -11,10 +18,6 @@ import { type File } from 'loot-core/types/file'; import { useDispatch } from '../../../redux'; 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, @@ -22,8 +25,6 @@ import { ModalCloseButton, ModalHeader, } from '../../common/Modal'; -import { Text } from '../../common/Text'; -import { View } from '../../common/View'; type DuplicateFileProps = { file: File; diff --git a/packages/desktop-client/src/components/modals/manager/FilesSettingsModal.tsx b/packages/desktop-client/src/components/modals/manager/FilesSettingsModal.tsx index 0e2a0b0d9b2..10cd1e939ae 100644 --- a/packages/desktop-client/src/components/modals/manager/FilesSettingsModal.tsx +++ b/packages/desktop-client/src/components/modals/manager/FilesSettingsModal.tsx @@ -1,16 +1,18 @@ import React, { useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { styles } from '@actual-app/components/styles'; +import { Text } from '@actual-app/components/text'; +import { View } from '@actual-app/components/view'; + import { loadAllFiles, pushModal } from 'loot-core/client/actions'; import { useGlobalPref } from '../../../hooks/useGlobalPref'; import { SvgPencil1 } from '../../../icons/v2'; import { useDispatch } from '../../../redux'; -import { theme, styles } from '../../../style'; -import { Button } from '../../common/Button2'; +import { theme } from '../../../style'; import { Modal, ModalCloseButton, ModalHeader } from '../../common/Modal'; -import { Text } from '../../common/Text'; -import { View } from '../../common/View'; function FileLocationSettings() { const [documentDir, _setDocumentDirPref] = useGlobalPref('documentDir'); diff --git a/packages/desktop-client/src/components/modals/manager/ImportModal.tsx b/packages/desktop-client/src/components/modals/manager/ImportModal.tsx index 13c1a6321b7..661a893025b 100644 --- a/packages/desktop-client/src/components/modals/manager/ImportModal.tsx +++ b/packages/desktop-client/src/components/modals/manager/ImportModal.tsx @@ -67,8 +67,10 @@ export function ImportModal() { )} - Select an app to import from, and we’ll guide you through the - process. + + Select an app to import from, and we’ll guide you through the + process. + {error ? ( - Add + Add )} @@ -1855,28 +1780,35 @@ function TransactionTableInner({ setScrollWidth(!width ? 0 : width); } + const { + onCloseAddTransaction: onCloseAddTransactionProp, + onNavigateToTransferAccount: onNavigateToTransferAccountProp, + onNavigateToSchedule: onNavigateToScheduleProp, + onNotesTagClick: onNotesTagClickProp, + } = props; + const onNavigateToTransferAccount = useCallback( accountId => { - props.onCloseAddTransaction(); - props.onNavigateToTransferAccount(accountId); + onCloseAddTransactionProp(); + onNavigateToTransferAccountProp(accountId); }, - [props.onCloseAddTransaction, props.onNavigateToTransferAccount], + [onCloseAddTransactionProp, onNavigateToTransferAccountProp], ); const onNavigateToSchedule = useCallback( scheduleId => { - props.onCloseAddTransaction(); - props.onNavigateToSchedule(scheduleId); + onCloseAddTransactionProp(); + onNavigateToScheduleProp(scheduleId); }, - [props.onCloseAddTransaction, props.onNavigateToSchedule], + [onCloseAddTransactionProp, onNavigateToScheduleProp], ); const onNotesTagClick = useCallback( noteTag => { - props.onCloseAddTransaction(); - props.onNotesTagClick(noteTag); + onCloseAddTransactionProp(); + onNotesTagClickProp(noteTag); }, - [props.onCloseAddTransaction, props.onNotesTagClick], + [onCloseAddTransactionProp, onNotesTagClickProp], ); useEffect(() => { @@ -2112,6 +2044,7 @@ export const TransactionTable = forwardRef((props, ref) => { const [newTransactions, setNewTransactions] = useState(null); const [prevIsAdding, setPrevIsAdding] = useState(false); const splitsExpanded = useSplitsExpanded(); + const splitsExpandedDispatch = splitsExpanded.dispatch; const prevSplitsExpanded = useRef(null); const tableRef = useRef(null); @@ -2190,6 +2123,8 @@ export const TransactionTable = forwardRef((props, ref) => { ); }, [props.transactions, props.payees, props.accounts]); + const hasPrevSplitsExpanded = prevSplitsExpanded.current; + useEffect(() => { // If it's anchored that means we've also disabled animations. To // reduce the chance for side effect collision, only do this if @@ -2198,7 +2133,7 @@ export const TransactionTable = forwardRef((props, ref) => { tableRef.current.unanchor(); tableRef.current.setRowAnimation(true); } - }, [prevSplitsExpanded.current]); + }, [hasPrevSplitsExpanded]); const newNavigator = useTableNavigator( newTransactions, @@ -2216,14 +2151,12 @@ export const TransactionTable = forwardRef((props, ref) => { const [_, forceRerender] = useState({}); const selectedItems = useSelectedItems(); - useLayoutEffect(() => { - latestState.current = { - newTransactions, - newNavigator, - tableNavigator, - transactions: props.transactions, - }; - }); + latestState.current = { + newTransactions, + newNavigator, + tableNavigator, + transactions: props.transactions, + }; // Derive new transactions from the `isAdding` prop if (prevIsAdding !== props.isAdding) { @@ -2238,32 +2171,30 @@ export const TransactionTable = forwardRef((props, ref) => { setPrevIsAdding(props.isAdding); } - useEffect(() => { - if (shouldAdd.current) { - if (newTransactions[0].account == null) { - dispatch( - addNotification({ - type: 'error', - message: 'Account is a required field', - }), - ); - newNavigator.onEdit('temp', 'account'); - } else { - const transactions = latestState.current.newTransactions; - const lastDate = transactions.length > 0 ? transactions[0].date : null; - setNewTransactions( - makeTemporaryTransactions( - props.currentAccountId, - props.currentCategoryId, - lastDate, - ), - ); - newNavigator.onEdit('temp', 'date'); - props.onAdd(transactions); - } - shouldAdd.current = false; + if (shouldAdd.current) { + if (newTransactions[0].account == null) { + dispatch( + addNotification({ + type: 'error', + message: 'Account is a required field', + }), + ); + newNavigator.onEdit('temp', 'account'); + } else { + const transactions = latestState.current.newTransactions; + const lastDate = transactions.length > 0 ? transactions[0].date : null; + setNewTransactions( + makeTemporaryTransactions( + props.currentAccountId, + props.currentCategoryId, + lastDate, + ), + ); + newNavigator.onEdit('temp', 'date'); + props.onAdd(transactions); } - }); + shouldAdd.current = false; + } useEffect(() => { if (savePending.current && afterSaveFunc.current) { @@ -2272,7 +2203,7 @@ export const TransactionTable = forwardRef((props, ref) => { } savePending.current = false; - }, [newTransactions, props.transactions]); + }, [newTransactions, props, props.transactions]); function getFieldsNewTransaction(item) { const fields = [ @@ -2409,7 +2340,20 @@ export const TransactionTable = forwardRef((props, ref) => { // effect we want to run. We have to wait for all updates to be // committed (the input could still be saving a value). forceRerender({}); - }, [props.onAdd, newNavigator.onEdit]); + }, []); + + const { + onSave: onSaveProp, + onApplyRules: onApplyRulesProp, + onBatchDelete, + onBatchDuplicate, + onBatchLinkSchedule, + onBatchUnlinkSchedule, + onCreateRule: onCreateRuleProp, + onScheduleAction: onScheduleActionProp, + onMakeAsNonSplitTransactions: onMakeAsNonSplitTransactionsProp, + onSplit: onSplitProp, + } = props; const onSave = useCallback( async (transaction, subtransactions = null, updatedFieldName = null) => { @@ -2420,8 +2364,8 @@ export const TransactionTable = forwardRef((props, ref) => { : transaction; if (isTemporaryId(transaction.id)) { - if (props.onApplyRules) { - groupedTransaction = await props.onApplyRules( + if (onApplyRulesProp) { + groupedTransaction = await onApplyRulesProp( groupedTransaction, updatedFieldName, ); @@ -2436,48 +2380,69 @@ export const TransactionTable = forwardRef((props, ref) => { ), ); } else { - props.onSave(groupedTransaction); + onSaveProp(groupedTransaction); } }, - [props.onSave], + [onSaveProp, onApplyRulesProp], ); - const onDelete = useCallback(id => { - const temporary = isTemporaryId(id); + const onDelete = useCallback( + id => { + const temporary = isTemporaryId(id); - if (temporary) { - const newTrans = latestState.current.newTransactions; + if (temporary) { + const newTrans = latestState.current.newTransactions; - if (id === newTrans[0].id) { - // You can never delete the parent new transaction - return; - } + if (id === newTrans[0].id) { + // You can never delete the parent new transaction + return; + } - setNewTransactions(deleteTransaction(newTrans, id).data); - } else { - props.onBatchDelete([id]); - } - }, []); + setNewTransactions(deleteTransaction(newTrans, id).data); + } else { + onBatchDelete([id]); + } + }, + [onBatchDelete], + ); - const onDuplicate = useCallback(id => { - props.onBatchDuplicate([id]); - }, []); + const onDuplicate = useCallback( + id => { + onBatchDuplicate([id]); + }, + [onBatchDuplicate], + ); - const onLinkSchedule = useCallback(id => { - props.onBatchLinkSchedule([id]); - }, []); - const onUnlinkSchedule = useCallback(id => { - props.onBatchUnlinkSchedule([id]); - }, []); - const onCreateRule = useCallback(id => { - props.onCreateRule([id]); - }, []); - const onScheduleAction = useCallback((action, id) => { - props.onScheduleAction(action, [id]); - }, []); - const onMakeAsNonSplitTransactions = useCallback(id => { - props.onMakeAsNonSplitTransactions([id]); - }, []); + const onLinkSchedule = useCallback( + id => { + onBatchLinkSchedule([id]); + }, + [onBatchLinkSchedule], + ); + const onUnlinkSchedule = useCallback( + id => { + onBatchUnlinkSchedule([id]); + }, + [onBatchUnlinkSchedule], + ); + const onCreateRule = useCallback( + id => { + onCreateRuleProp([id]); + }, + [onCreateRuleProp], + ); + const onScheduleAction = useCallback( + (action, id) => { + onScheduleActionProp(action, [id]); + }, + [onScheduleActionProp], + ); + const onMakeAsNonSplitTransactions = useCallback( + id => { + onMakeAsNonSplitTransactionsProp([id]); + }, + [onMakeAsNonSplitTransactionsProp], + ); const onSplit = useMemo(() => { return id => { @@ -2500,9 +2465,9 @@ export const TransactionTable = forwardRef((props, ref) => { } } else { const trans = latestState.current.transactions.find(t => t.id === id); - const newId = props.onSplit(id); + const newId = onSplitProp(id); - splitsExpanded.dispatch({ type: 'open-split', id: trans.id }); + splitsExpandedDispatch({ type: 'open-split', id: trans.id }); const { tableNavigator } = latestState.current; if (trans.amount === null) { @@ -2512,12 +2477,19 @@ export const TransactionTable = forwardRef((props, ref) => { } } }; - }, [props.onSplit, splitsExpanded.dispatch]); + }, [onSplitProp, splitsExpandedDispatch]); + + const { onAddSplit: onAddSplitProp } = props; const onAddSplit = useCallback( id => { + const { + tableNavigator, + newNavigator, + newTransactions: newTrans, + } = latestState.current; + if (isTemporaryId(id)) { - const newTrans = latestState.current.newTransactions; const { data, diff } = addSplitTransaction(newTrans, id); setNewTransactions(data); newNavigator.onEdit( @@ -2525,19 +2497,19 @@ export const TransactionTable = forwardRef((props, ref) => { latestState.current.newNavigator.focusedField, ); } else { - const newId = props.onAddSplit(id); + const newId = onAddSplitProp(id); tableNavigator.onEdit( newId, latestState.current.tableNavigator.focusedField, ); } }, - [props.onAddSplit], + [onAddSplitProp], ); const onDistributeRemainder = useCallback( async id => { - const { transactions, tableNavigator, newTransactions } = + const { transactions, newNavigator, tableNavigator, newTransactions } = latestState.current; const targetTransactions = isTemporaryId(id) @@ -2591,7 +2563,7 @@ export const TransactionTable = forwardRef((props, ref) => { }); } }, - [latestState], + [onSave], ); function onCloseAddTransaction() { @@ -2605,8 +2577,8 @@ export const TransactionTable = forwardRef((props, ref) => { } const onToggleSplit = useCallback( - id => splitsExpanded.dispatch({ type: 'toggle-split', id }), - [splitsExpanded.dispatch], + id => splitsExpandedDispatch({ type: 'toggle-split', id }), + [splitsExpandedDispatch], ); return ( diff --git a/packages/desktop-client/src/components/transactions/TransactionsTable.test.tsx b/packages/desktop-client/src/components/transactions/TransactionsTable.test.tsx index fff9b87b2aa..64aee992360 100644 --- a/packages/desktop-client/src/components/transactions/TransactionsTable.test.tsx +++ b/packages/desktop-client/src/components/transactions/TransactionsTable.test.tsx @@ -48,6 +48,10 @@ vi.mock('../../hooks/useFeatureFlag', () => ({ })); const accounts = [generateAccount('Bank of America')]; +vi.mock('../../hooks/useAccounts', () => ({ + useAccounts: () => accounts, +})); + const payees: PayeeEntity[] = [ { id: 'bob-id', @@ -65,6 +69,15 @@ const payees: PayeeEntity[] = [ name: 'This guy on the side of the road', }, ]; +vi.mock('../../hooks/usePayees', async importOriginal => { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + const actual = await importOriginal(); + return { + ...actual, + usePayees: () => payees, + }; +}); + const categoryGroups = generateCategoryGroups([ { name: 'Investments and Savings', @@ -79,6 +92,13 @@ const categoryGroups = generateCategoryGroups([ categories: [{ name: 'Big Projects' }, { name: 'Shed' }], }, ]); +vi.mock('../../hooks/useCategories', () => ({ + useCategories: () => ({ + list: categoryGroups.flatMap(g => g.categories), + grouped: categoryGroups, + }), +})); + const usualGroup = categoryGroups[1]; function generateTransactions( @@ -130,13 +150,14 @@ type LiveTransactionTableProps = { }; function LiveTransactionTable(props: LiveTransactionTableProps) { - const [transactions, setTransactions] = useState(props.transactions); + const { transactions: transactionsProp, onTransactionsChange } = props; + + const [transactions, setTransactions] = useState(transactionsProp); useEffect(() => { - if (transactions === props.transactions) return; - props.onTransactionsChange?.(transactions); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [transactions]); + if (transactions === transactionsProp) return; + onTransactionsChange?.(transactions); + }, [transactions, transactionsProp, onTransactionsChange]); const onSplit = (id: string) => { const { data, diff } = splitTransaction(transactions, id); @@ -212,6 +233,11 @@ function initBasicServer() { return { data: payees, dependencies: [] }; case 'accounts': return { data: accounts, dependencies: [] }; + case 'transactions': + return { + data: generateTransactions(5, [6]), + dependencies: [], + }; default: throw new Error(`queried unknown table: ${query.table}`); } diff --git a/packages/desktop-client/src/components/util/AmountInput.tsx b/packages/desktop-client/src/components/util/AmountInput.tsx index 833c0b32795..1a0e9f59069 100644 --- a/packages/desktop-client/src/components/util/AmountInput.tsx +++ b/packages/desktop-client/src/components/util/AmountInput.tsx @@ -10,6 +10,9 @@ import React, { } from 'react'; import { useTranslation } from 'react-i18next'; +import { Button } from '@actual-app/components/button'; +import { View } from '@actual-app/components/view'; + import { evalArithmetic } from 'loot-core/shared/arithmetic'; import { amountToInteger, appendDecimals } from 'loot-core/shared/util'; @@ -17,9 +20,7 @@ import { useMergedRefs } from '../../hooks/useMergedRefs'; import { useSyncedPref } from '../../hooks/useSyncedPref'; import { SvgAdd, SvgSubtract } from '../../icons/v1'; import { theme } from '../../style'; -import { Button } from '../common/Button2'; import { InputWithContent } from '../common/InputWithContent'; -import { View } from '../common/View'; import { useFormat } from '../spreadsheet/useFormat'; type AmountInputProps = { diff --git a/packages/desktop-client/src/components/util/DisplayId.tsx b/packages/desktop-client/src/components/util/DisplayId.tsx index 162744a1772..17ed79fb0a9 100644 --- a/packages/desktop-client/src/components/util/DisplayId.tsx +++ b/packages/desktop-client/src/components/util/DisplayId.tsx @@ -2,10 +2,11 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; +import { Text } from '@actual-app/components/text'; + import { useAccount } from '../../hooks/useAccount'; import { usePayee } from '../../hooks/usePayee'; import { theme } from '../../style'; -import { Text } from '../common/Text'; type DisplayIdProps = { type: 'accounts' | 'payees'; diff --git a/packages/desktop-client/src/components/util/GenericInput.jsx b/packages/desktop-client/src/components/util/GenericInput.jsx index cf6f2bee112..d1331589250 100644 --- a/packages/desktop-client/src/components/util/GenericInput.jsx +++ b/packages/desktop-client/src/components/util/GenericInput.jsx @@ -1,5 +1,7 @@ import React from 'react'; +import { View } from '@actual-app/components/view'; + import { useReports } from 'loot-core/client/data-hooks/reports'; import { getMonthYearFormat } from 'loot-core/shared/months'; import { integerToAmount, amountToInteger } from 'loot-core/shared/util'; @@ -14,7 +16,6 @@ import { FilterAutocomplete } from '../autocomplete/FilterAutocomplete'; import { PayeeAutocomplete } from '../autocomplete/PayeeAutocomplete'; import { ReportAutocomplete } from '../autocomplete/ReportAutocomplete'; import { Input } from '../common/Input'; -import { View } from '../common/View'; import { Checkbox } from '../forms'; import { DateSelect } from '../select/DateSelect'; import { RecurringSchedulePicker } from '../select/RecurringSchedulePicker'; diff --git a/packages/desktop-client/src/hooks/useAccounts.ts b/packages/desktop-client/src/hooks/useAccounts.ts index 7fdda9a51f4..f7b71197153 100644 --- a/packages/desktop-client/src/hooks/useAccounts.ts +++ b/packages/desktop-client/src/hooks/useAccounts.ts @@ -4,15 +4,18 @@ import { getAccounts } from 'loot-core/client/queries/queriesSlice'; import { useSelector, useDispatch } from '../redux'; +import { useInitialMount } from './useInitialMount'; + export function useAccounts() { const dispatch = useDispatch(); const accountsLoaded = useSelector(state => state.queries.accountsLoaded); + const isInitialMount = useInitialMount(); useEffect(() => { - if (!accountsLoaded) { + if (isInitialMount && !accountsLoaded) { dispatch(getAccounts()); } - }, []); + }, [accountsLoaded, dispatch, isInitialMount]); return useSelector(state => state.queries.accounts); } diff --git a/packages/desktop-client/src/hooks/useCategories.ts b/packages/desktop-client/src/hooks/useCategories.ts index a6936b35865..b233d77852c 100644 --- a/packages/desktop-client/src/hooks/useCategories.ts +++ b/packages/desktop-client/src/hooks/useCategories.ts @@ -4,15 +4,18 @@ import { getCategories } from 'loot-core/client/queries/queriesSlice'; import { useSelector, useDispatch } from '../redux'; +import { useInitialMount } from './useInitialMount'; + export function useCategories() { const dispatch = useDispatch(); const categoriesLoaded = useSelector(state => state.queries.categoriesLoaded); + const isInitialMount = useInitialMount(); useEffect(() => { - if (!categoriesLoaded) { + if (isInitialMount && !categoriesLoaded) { dispatch(getCategories()); } - }, []); + }, [categoriesLoaded, dispatch, isInitialMount]); return useSelector(state => state.queries.categories); } diff --git a/packages/desktop-client/src/hooks/useDisplayPayee.ts b/packages/desktop-client/src/hooks/useDisplayPayee.ts new file mode 100644 index 00000000000..ab2609b523c --- /dev/null +++ b/packages/desktop-client/src/hooks/useDisplayPayee.ts @@ -0,0 +1,138 @@ +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useTransactions } from 'loot-core/client/data-hooks/transactions'; +import { q } from 'loot-core/shared/query'; +import { + type AccountEntity, + type PayeeEntity, + type TransactionEntity, +} from 'loot-core/types/models'; + +import { useAccounts } from './useAccounts'; +import { usePayee } from './usePayee'; +import { usePayees } from './usePayees'; + +type Counts = { + counts: Record; + maxCount: number; + mostCommonPayeeTransaction: TransactionEntity | null; +}; + +type UseDisplayPayeeProps = { + transaction?: TransactionEntity | undefined; +}; + +export function useDisplayPayee({ transaction }: UseDisplayPayeeProps) { + const { t } = useTranslation(); + const subtransactionsQuery = useMemo( + () => q('transactions').filter({ parent_id: transaction?.id }).select('*'), + [transaction?.id], + ); + const { transactions: subtransactions = [] } = useTransactions({ + query: subtransactionsQuery, + }); + + const accounts = useAccounts(); + const payees = usePayees(); + const payee = usePayee(transaction?.payee || ''); + + return useMemo(() => { + if (subtransactions.length === 0) { + return getPrettyPayee({ + t, + transaction, + payee, + transferAccount: accounts.find( + a => + a.id === + payees.find(p => p.id === transaction?.payee)?.transfer_acct, + ), + }); + } + + const { counts, mostCommonPayeeTransaction } = + subtransactions?.reduce( + ({ counts, ...result }, sub) => { + if (sub.payee) { + counts[sub.payee] = (counts[sub.payee] || 0) + 1; + if (counts[sub.payee] > result.maxCount) { + return { + counts, + maxCount: counts[sub.payee], + mostCommonPayeeTransaction: sub, + }; + } + } + return { counts, ...result }; + }, + { counts: {}, maxCount: 0, mostCommonPayeeTransaction: null } as Counts, + ) || {}; + + if (!mostCommonPayeeTransaction) { + return t('Split (no payee)'); + } + + const mostCommonPayee = payees.find( + p => p.id === mostCommonPayeeTransaction.payee, + ); + + if (!mostCommonPayee) { + return t('Split (no payee)'); + } + + const numDistinctPayees = Object.keys(counts).length; + + return getPrettyPayee({ + t, + transaction: mostCommonPayeeTransaction, + payee: mostCommonPayee, + transferAccount: accounts.find( + a => + a.id === + payees.find(p => p.id === mostCommonPayeeTransaction.payee) + ?.transfer_acct, + ), + numHiddenPayees: numDistinctPayees - 1, + }); + }, [subtransactions, payees, accounts, transaction, payee, t]); +} + +type GetPrettyPayeeProps = { + t: ReturnType['t']; + transaction?: TransactionEntity | undefined; + payee?: PayeeEntity | undefined; + transferAccount?: AccountEntity | undefined; + numHiddenPayees?: number | undefined; +}; + +function getPrettyPayee({ + t, + transaction, + payee, + transferAccount, + numHiddenPayees = 0, +}: GetPrettyPayeeProps) { + if (!transaction) { + return ''; + } + + const formatPayeeName = (payeeName: string) => + numHiddenPayees > 0 + ? `${payeeName} ${t('(+{{numHiddenPayees}} more)', { + numHiddenPayees, + })}` + : payeeName; + + const { payee: payeeId } = transaction; + + if (transferAccount) { + return formatPayeeName(transferAccount.name); + } else if (payee) { + return formatPayeeName(payee.name); + } else if (payeeId && payeeId.startsWith('new:')) { + return formatPayeeName(payeeId.slice('new:'.length)); + } + + return ''; +} diff --git a/packages/desktop-client/src/hooks/useFeatureFlag.ts b/packages/desktop-client/src/hooks/useFeatureFlag.ts index 1ad9821839a..5f29af5ac85 100644 --- a/packages/desktop-client/src/hooks/useFeatureFlag.ts +++ b/packages/desktop-client/src/hooks/useFeatureFlag.ts @@ -4,6 +4,7 @@ import { useSyncedPref } from './useSyncedPref'; const DEFAULT_FEATURE_FLAG_STATE: Record = { goalTemplatesEnabled: false, + goalTemplatesUIEnabled: false, actionTemplating: false, contextMenus: false, openidAuth: false, diff --git a/packages/desktop-client/src/hooks/usePayees.ts b/packages/desktop-client/src/hooks/usePayees.ts index 27d4660799f..925bae3102a 100644 --- a/packages/desktop-client/src/hooks/usePayees.ts +++ b/packages/desktop-client/src/hooks/usePayees.ts @@ -7,17 +7,21 @@ import { import { useSelector, useDispatch } from '../redux'; +import { useInitialMount } from './useInitialMount'; + export function useCommonPayees() { const dispatch = useDispatch(); const commonPayeesLoaded = useSelector( state => state.queries.commonPayeesLoaded, ); + const isInitialMount = useInitialMount(); + useEffect(() => { - if (!commonPayeesLoaded) { + if (isInitialMount && !commonPayeesLoaded) { dispatch(getCommonPayees()); } - }, []); + }, [commonPayeesLoaded, dispatch, isInitialMount]); return useSelector(state => state.queries.commonPayees); } @@ -26,11 +30,13 @@ export function usePayees() { const dispatch = useDispatch(); const payeesLoaded = useSelector(state => state.queries.payeesLoaded); + const isInitialMount = useInitialMount(); + useEffect(() => { - if (!payeesLoaded) { + if (isInitialMount && !payeesLoaded) { dispatch(getPayees()); } - }, []); + }, [dispatch, isInitialMount, payeesLoaded]); return useSelector(state => state.queries.payees); } diff --git a/packages/desktop-client/src/hooks/useProperFocus.tsx b/packages/desktop-client/src/hooks/useProperFocus.tsx index dba487bdce5..7c0fc5c7a72 100644 --- a/packages/desktop-client/src/hooks/useProperFocus.tsx +++ b/packages/desktop-client/src/hooks/useProperFocus.tsx @@ -98,5 +98,5 @@ export function useProperFocus( } prevShouldFocus.current = shouldFocus; - }, [shouldFocus]); + }, [context, ref, shouldFocus]); } diff --git a/packages/desktop-client/src/hooks/useSelected.tsx b/packages/desktop-client/src/hooks/useSelected.tsx index 7c999e6e752..d3cfe2cf9c2 100644 --- a/packages/desktop-client/src/hooks/useSelected.tsx +++ b/packages/desktop-client/src/hooks/useSelected.tsx @@ -204,7 +204,7 @@ export function useSelected( const prevState = undo.getUndoState('selectedItems'); undo.setUndoState('selectedItems', { name, items: state.selectedItems }); return () => undo.setUndoState('selectedItems', prevState); - }, [state.selectedItems]); + }, [name, state.selectedItems]); useEffect(() => { function onUndo({ messages, undoTag }: UndoState) { @@ -233,7 +233,7 @@ export function useSelected( } return listen('undo-event', onUndo); - }, []); + }, [name]); return { items: state.selectedItems, @@ -263,37 +263,35 @@ export function SelectedProvider({ fetchAllIds, children, }: SelectedProviderProps) { - const latestItems = useRef(null); - - useEffect(() => { - latestItems.current = instance.items; - }, [instance.items]); + const { items: instanceItems, dispatch: instanceDispatch } = instance; + const latestItems = useRef(instanceItems); + latestItems.current = instanceItems; const dispatch = useCallback( async (action: Actions) => { if (action.type === 'select-all') { if (latestItems.current && latestItems.current.size > 0) { - return instance.dispatch({ + return instanceDispatch({ type: 'select-none', isRangeSelect: action.isRangeSelect, }); } else { if (fetchAllIds) { - return instance.dispatch({ + return instanceDispatch({ type: 'select-all', ids: await fetchAllIds(), isRangeSelect: action.isRangeSelect, }); } - return instance.dispatch({ + return instanceDispatch({ type: 'select-all', isRangeSelect: action.isRangeSelect, }); } } - return instance.dispatch(action); + return instanceDispatch(action); }, - [instance.dispatch, fetchAllIds], + [instanceDispatch, fetchAllIds], ); return ( @@ -335,7 +333,7 @@ export function SelectedProviderWithItems({ useEffect(() => { registerDispatch?.(selected.dispatch); - }, [registerDispatch]); + }, [registerDispatch, selected.dispatch]); return ( instance={selected} fetchAllIds={fetchAllIds}> diff --git a/packages/desktop-client/src/style/styles.ts b/packages/desktop-client/src/style/styles.ts index 3d0643cfe39..5ea78501c78 100644 --- a/packages/desktop-client/src/style/styles.ts +++ b/packages/desktop-client/src/style/styles.ts @@ -1,13 +1,10 @@ // @ts-strict-ignore -import { styles as baseStyles } from '@actual-app/components/styles'; +import { styles } from '@actual-app/components/styles'; import * as Platform from 'loot-core/client/platform'; export { type CSSProperties } from '@actual-app/components/styles'; -/** @deprecated please import styles from '@actual-app/components/styles' */ -export const styles = baseStyles; - let hiddenScrollbars = false; // need both styles defined for primary and secondary colors diff --git a/packages/desktop-client/vite.config.mts b/packages/desktop-client/vite.config.mts index fd0b9acb1a4..69ddbb80003 100644 --- a/packages/desktop-client/vite.config.mts +++ b/packages/desktop-client/vite.config.mts @@ -103,6 +103,8 @@ export default defineConfig(async ({ mode }) => { ]; } + const browserOpen = env.BROWSER_OPEN ? `//${env.BROWSER_OPEN}` : true; + return { base: '/', envPrefix: 'REACT_APP_', @@ -139,7 +141,7 @@ export default defineConfig(async ({ mode }) => { ? ['chrome', 'firefox', 'edge', 'browser', 'browserPrivate'].includes( env.BROWSER, ) - : true, + : browserOpen, watch: { disableGlobbing: false, }, diff --git a/packages/loot-core/migrations/1736640000000__custom_report_sorting.sql b/packages/loot-core/migrations/1736640000000_custom_report_sorting.sql similarity index 100% rename from packages/loot-core/migrations/1736640000000__custom_report_sorting.sql rename to packages/loot-core/migrations/1736640000000_custom_report_sorting.sql diff --git a/packages/loot-core/migrations/1738491452000__sorting_rename.sql b/packages/loot-core/migrations/1738491452000_sorting_rename.sql similarity index 100% rename from packages/loot-core/migrations/1738491452000__sorting_rename.sql rename to packages/loot-core/migrations/1738491452000_sorting_rename.sql diff --git a/packages/loot-core/migrations/1739139550000_bank_sync_page.sql b/packages/loot-core/migrations/1739139550000_bank_sync_page.sql new file mode 100644 index 00000000000..d5ff380cbae --- /dev/null +++ b/packages/loot-core/migrations/1739139550000_bank_sync_page.sql @@ -0,0 +1,7 @@ +BEGIN TRANSACTION; + +ALTER TABLE accounts ADD COLUMN last_sync text; + +ALTER TABLE transactions ADD COLUMN raw_synced_data text; + +COMMIT; diff --git a/packages/loot-core/src/client/accounts/accountsSlice.ts b/packages/loot-core/src/client/accounts/accountsSlice.ts index 6c8ce6b718a..f1ff845f4fd 100644 --- a/packages/loot-core/src/client/accounts/accountsSlice.ts +++ b/packages/loot-core/src/client/accounts/accountsSlice.ts @@ -190,6 +190,8 @@ function handleSyncResponse( resMatchedTransactions.push(...matchedTransactions); resUpdatedAccounts.push(...updatedAccounts); + dispatch(getAccounts()); + return newTransactions.length > 0 || matchedTransactions.length > 0; } diff --git a/packages/loot-core/src/client/data-hooks/schedules.tsx b/packages/loot-core/src/client/data-hooks/schedules.tsx index 372c96dc540..c122b818cdb 100644 --- a/packages/loot-core/src/client/data-hooks/schedules.tsx +++ b/packages/loot-core/src/client/data-hooks/schedules.tsx @@ -84,6 +84,7 @@ export function useSchedules({ setError(undefined); if (!query) { + console.error('No query provided to useSchedules'); return; } diff --git a/packages/loot-core/src/client/data-hooks/transactions.ts b/packages/loot-core/src/client/data-hooks/transactions.ts index 4711766c197..85386db6443 100644 --- a/packages/loot-core/src/client/data-hooks/transactions.ts +++ b/packages/loot-core/src/client/data-hooks/transactions.ts @@ -26,6 +26,10 @@ import { type PagedQuery, pagedQuery } from '../query-helpers'; import { type ScheduleStatuses, useCachedSchedules } from './schedules'; type UseTransactionsProps = { + /** + * The Query class is immutable so it is important to memoize the query object + * to prevent unnecessary re-renders i.e. `useMemo`, `useState`, etc. + */ query?: Query; options?: { pageCount?: number; 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 d2140d43cd8..7fb3c44fdad 100644 --- a/packages/loot-core/src/client/state-types/modals.d.ts +++ b/packages/loot-core/src/client/state-types/modals.d.ts @@ -186,6 +186,11 @@ type FinanceModals = { 'schedules-upcoming-length': null; 'schedule-posts-offline-notification': null; + + 'synced-account-edit': { + account: AccountEntity; + }; + 'account-menu': { accountId: string; onSave: (account: AccountEntity) => void; @@ -342,6 +347,7 @@ type FinanceModals = { }; 'keyboard-shortcuts': EmptyObject; 'goal-templates': EmptyObject; + 'category-automations-edit': EmptyObject; }; export type PushModalAction = { diff --git a/packages/loot-core/src/mocks/index.ts b/packages/loot-core/src/mocks/index.ts index 82005d29188..e008df1d7a1 100644 --- a/packages/loot-core/src/mocks/index.ts +++ b/packages/loot-core/src/mocks/index.ts @@ -16,15 +16,10 @@ export function generateAccount( name: AccountEntity['name'], isConnected?: boolean, offbudget?: boolean, -): AccountEntity & { bankId: number | null; bankName: string | null } { - const offlineAccount: AccountEntity & { - bankId: number | null; - bankName: string | null; - } = { +): AccountEntity { + const offlineAccount: AccountEntity = { id: uuidv4(), name, - bankId: null, - bankName: null, offbudget: offbudget ? 1 : 0, sort_order: 0, tombstone: 0, @@ -45,6 +40,7 @@ export function generateAccount( balance_available: 0, balance_limit: 0, account_sync_source: 'goCardless', + last_sync: new Date().getTime().toString(), }; } @@ -55,12 +51,15 @@ function emptySyncFields(): _SyncFields { return { account_id: null, bank: null, + bankId: null, + bankName: null, mask: null, official_name: null, balance_current: null, balance_available: null, balance_limit: null, account_sync_source: null, + last_sync: null, }; } diff --git a/packages/loot-core/src/mocks/setup.ts b/packages/loot-core/src/mocks/setup.ts index 91834cde9df..64ea562bce8 100644 --- a/packages/loot-core/src/mocks/setup.ts +++ b/packages/loot-core/src/mocks/setup.ts @@ -3,7 +3,6 @@ import * as nativeFs from 'fs'; import * as fetchClient from '../platform/client/fetch'; import * as sqlite from '../platform/server/sqlite'; -import * as rules from '../server/accounts/transaction-rules'; import * as db from '../server/db'; import { enableGlobalMutations, @@ -12,6 +11,7 @@ import { import { setServer } from '../server/server-config'; import * as sheet from '../server/sheet'; import { setSyncingMode } from '../server/sync'; +import * as rules from '../server/transactions/transaction-rules'; import { updateVersion } from '../server/update'; import { resetTracer, tracer } from '../shared/test-helpers'; diff --git a/packages/loot-core/src/platform/client/fetch/index.browser.ts b/packages/loot-core/src/platform/client/fetch/index.browser.ts index 6f212a34658..defc478021d 100644 --- a/packages/loot-core/src/platform/client/fetch/index.browser.ts +++ b/packages/loot-core/src/platform/client/fetch/index.browser.ts @@ -96,11 +96,15 @@ function connectWorker(worker, onOpen, onError) { // ready to handle messages. if (msg.type === 'connect') { // Send any messages that were queued while closed - if (messageQueue.length > 0) { + if (messageQueue?.length > 0) { messageQueue.forEach(msg => worker.postMessage(msg)); messageQueue = null; } + // signal to the backend that we're connected to it + globalWorker.postMessage({ + name: 'client-connected-to-backend', + }); onOpen(); } else if (msg.type === 'app-init-failure') { onError(msg); @@ -147,10 +151,9 @@ export const init: T.Init = async function (worker) { }; export const send: T.Send = function ( - name, - args, - { catchErrors = false } = {}, -) { + ...params: Parameters +): ReturnType { + const [name, args, { catchErrors = false } = {}] = params; return new Promise((resolve, reject) => { const id = uuidv4(); @@ -167,8 +170,7 @@ export const send: T.Send = function ( } else { globalWorker.postMessage(message); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - }) as any; + }); }; export const sendCatch: T.SendCatch = function (name, args) { diff --git a/packages/loot-core/src/platform/client/fetch/index.d.ts b/packages/loot-core/src/platform/client/fetch/index.d.ts index d6e4d4a31cd..dcb660ea902 100644 --- a/packages/loot-core/src/platform/client/fetch/index.d.ts +++ b/packages/loot-core/src/platform/client/fetch/index.d.ts @@ -7,25 +7,31 @@ export type Init = typeof init; export function send( name: K, - args?: Parameters[0], - options?: { catchErrors: true }, -): ReturnType< - | { data: Handlers[K] } - | { error: { type: 'APIError' | 'InternalError'; message: string } } + args: Parameters[0], + options: { catchErrors: true }, +): Promise< + | { data: Awaited>; error: undefined } + | { + data: undefined; + error: { type: 'APIError' | 'InternalError'; message: string }; + } >; export function send( name: K, args?: Parameters[0], options?: { catchErrors?: boolean }, -): ReturnType; +): Promise>>; export type Send = typeof send; export function sendCatch( name: K, args?: Parameters[0], -): ReturnType< - | { data: Handlers[K] } - | { error: { type: 'APIError' | 'InternalError'; message: string } } +): Promise< + | { data: Awaited>; error: undefined } + | { + data: undefined; + error: { type: 'APIError' | 'InternalError'; message: string }; + } >; export type SendCatch = typeof sendCatch; diff --git a/packages/loot-core/src/platform/client/fetch/index.web.ts b/packages/loot-core/src/platform/client/fetch/index.web.ts index c74ce488a6f..b28c1d680a6 100644 --- a/packages/loot-core/src/platform/client/fetch/index.web.ts +++ b/packages/loot-core/src/platform/client/fetch/index.web.ts @@ -79,10 +79,9 @@ export const init: T.Init = async function () { }; export const send: T.Send = function ( - name, - args, - { catchErrors = false } = {}, -) { + ...params: Parameters +): ReturnType { + const [name, args, { catchErrors = false } = {}] = params; return new Promise((resolve, reject) => { const id = uuidv4(); replyHandlers.set(id, { resolve, reject }); diff --git a/packages/loot-core/src/platform/server/asyncStorage/index.d.ts b/packages/loot-core/src/platform/server/asyncStorage/index.d.ts index 4a189f3a86e..88f7f415de6 100644 --- a/packages/loot-core/src/platform/server/asyncStorage/index.d.ts +++ b/packages/loot-core/src/platform/server/asyncStorage/index.d.ts @@ -1,20 +1,34 @@ +import { GlobalPrefs, GlobalPrefsJson } from '../../../types/prefs'; + export function init(opts?: { persist?: boolean }): void; export type Init = typeof init; -export function getItem(key: string): Promise; +type InferType = GlobalPrefs[K]; + +export function getItem( + key: K, +): Promise; export type GetItem = typeof getItem; -export function setItem(key: string, value: unknown): void; +export function setItem( + key: K, + value: GlobalPrefsJson[K], +): void; export type SetItem = typeof setItem; -export function removeItem(key: string): void; +export function removeItem(key: keyof GlobalPrefsJson): void; export type RemoveItem = typeof removeItem; -export function multiGet(keys: string[]): Promise<[string, string][]>; +export async function multiGet( + keys: K, +): Promise<{ [P in keyof K]: [K[P], GlobalPrefsJson[K[P]]] }>; export type MultiGet = typeof multiGet; -export function multiSet(keyValues: [string, unknown][]): void; +export function multiSet( + keyValues: Array<[K, GlobalPrefsJson[K]]>, +): void; + export type MultiSet = typeof multiSet; -export function multiRemove(keys: string[]): void; +export function multiRemove(keys: (keyof GlobalPrefsJson)[]): void; export type MultiRemove = typeof multiRemove; diff --git a/packages/loot-core/src/platform/server/asyncStorage/index.electron.ts b/packages/loot-core/src/platform/server/asyncStorage/index.electron.ts index bca8ddf73b5..d0cd503afff 100644 --- a/packages/loot-core/src/platform/server/asyncStorage/index.electron.ts +++ b/packages/loot-core/src/platform/server/asyncStorage/index.electron.ts @@ -2,12 +2,13 @@ import * as fs from 'fs'; import { join } from 'path'; +import { GlobalPrefsJson } from '../../../types/prefs'; import * as lootFs from '../fs'; import * as T from '.'; const getStorePath = () => join(lootFs.getDataDir(), 'global-store.json'); -let store; +let store: GlobalPrefsJson; let persisted = true; export const init: T.Init = function ({ persist = true } = {}) { @@ -55,15 +56,17 @@ export const removeItem: T.RemoveItem = function (key) { return _saveStore(); }; -export const multiGet: T.MultiGet = function (keys) { +export async function multiGet( + keys: K, +) { return new Promise(function (resolve) { return resolve( keys.map(function (key) { return [key, store[key]]; - }), + }) as { [P in keyof K]: [K[P], GlobalPrefsJson[K[P]]] }, ); }); -}; +} export const multiSet: T.MultiSet = function (keyValues) { keyValues.forEach(function ([key, value]) { diff --git a/packages/loot-core/src/platform/server/asyncStorage/index.testing.ts b/packages/loot-core/src/platform/server/asyncStorage/index.testing.ts index 5f96009b4d0..4de836e2040 100644 --- a/packages/loot-core/src/platform/server/asyncStorage/index.testing.ts +++ b/packages/loot-core/src/platform/server/asyncStorage/index.testing.ts @@ -1,7 +1,9 @@ // @ts-strict-ignore +import { GlobalPrefsJson } from '../../../types/prefs'; + import * as T from '.'; -const store = {}; +const store: GlobalPrefsJson = {}; export const init: T.Init = function () {}; @@ -19,15 +21,17 @@ export const removeItem: T.RemoveItem = function (key) { delete store[key]; }; -export const multiGet: T.MultiGet = function (keys) { +export async function multiGet( + keys: K, +) { return new Promise(function (resolve) { return resolve( keys.map(function (key) { return [key, store[key]]; - }), + }) as { [P in keyof K]: [K[P], GlobalPrefsJson[K[P]]] }, ); }); -}; +} export const multiSet: T.MultiSet = function (keyValues) { keyValues.forEach(function ([key, value]) { diff --git a/packages/loot-core/src/platform/server/asyncStorage/index.web.ts b/packages/loot-core/src/platform/server/asyncStorage/index.web.ts index c9bc80c7f7a..f09442e5cd8 100644 --- a/packages/loot-core/src/platform/server/asyncStorage/index.web.ts +++ b/packages/loot-core/src/platform/server/asyncStorage/index.web.ts @@ -1,4 +1,5 @@ // @ts-strict-ignore +import { GlobalPrefsJson } from '../../../types/prefs'; import { getDatabase } from '../indexeddb'; import * as T from '.'; @@ -53,7 +54,9 @@ export const removeItem: T.RemoveItem = async function (key) { }); }; -export const multiGet: T.MultiGet = async function (keys) { +export async function multiGet( + keys: K, +) { const db = await getDatabase(); const transaction = db.transaction(['asyncStorage'], 'readonly'); @@ -71,7 +74,7 @@ export const multiGet: T.MultiGet = async function (keys) { commit(transaction); return promise; -}; +} export const multiSet: T.MultiSet = async function (keyValues) { const db = await getDatabase(); diff --git a/packages/loot-core/src/platform/server/connection/index.web.ts b/packages/loot-core/src/platform/server/connection/index.web.ts index 465b8348edc..6a17684c0e5 100644 --- a/packages/loot-core/src/platform/server/connection/index.web.ts +++ b/packages/loot-core/src/platform/server/connection/index.web.ts @@ -44,6 +44,12 @@ export const init: T.Init = function (serverChn, handlers) { return; } + if (msg.name === 'client-connected-to-backend') { + // the client is indicating that it is connected to this backend. Stop attempting to connect + clearInterval(reconnectToClientInterval); + return; + } + const { id, name, args, undoTag, catchErrors } = msg; if (handlers[name]) { @@ -98,7 +104,19 @@ export const init: T.Init = function (serverChn, handlers) { false, ); - serverChannel.postMessage({ type: 'connect' }); + const RECONNECT_INTERVAL_MS = 200; + const MAX_RECONNECT_ATTEMPTS = 500; + let reconnectAttempts = 0; + + const reconnectToClientInterval = setInterval(() => { + serverChannel.postMessage({ type: 'connect' }); + reconnectAttempts++; + if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) { + // Failed to connect to client - signal server error + send('server-error'); + clearInterval(reconnectToClientInterval); + } + }, RECONNECT_INTERVAL_MS); }; export const send: T.Send = function (name, args) { diff --git a/packages/loot-core/src/server/__snapshots__/main.test.ts.snap b/packages/loot-core/src/server/__snapshots__/main.test.ts.snap index 1825a6e8480..572444f3637 100644 --- a/packages/loot-core/src/server/__snapshots__/main.test.ts.snap +++ b/packages/loot-core/src/server/__snapshots__/main.test.ts.snap @@ -19,6 +19,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -44,6 +45,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -75,7 +77,7 @@ exports[`Accounts Transfers are properly updated 2`] = ` \\"id\\": \\"test-transfer\\", \\"imported_description\\": null, \\"isChild\\": 0, -@@ -23,15 +23,15 @@ +@@ -24,15 +24,15 @@ \\"tombstone\\": 0, \\"transferred_id\\": \\"id2\\", \\"type\\": null, @@ -100,8 +102,8 @@ exports[`Accounts Transfers are properly updated 3`] = ` - First value + Second value -@@ -18,12 +18,12 @@ - \\"pending\\": 0, +@@ -19,12 +19,12 @@ + \\"raw_synced_data\\": null, \\"reconciled\\": 0, \\"schedule\\": null, \\"sort_order\\": 123456789, @@ -115,8 +117,8 @@ exports[`Accounts Transfers are properly updated 3`] = ` Object { \\"acct\\": \\"three\\", \\"amount\\": -5000, -@@ -43,10 +43,10 @@ - \\"pending\\": 0, +@@ -45,10 +45,10 @@ + \\"raw_synced_data\\": null, \\"reconciled\\": 0, \\"schedule\\": null, \\"sort_order\\": 123456789, diff --git a/packages/loot-core/src/server/accounts/__snapshots__/sync.test.ts.snap b/packages/loot-core/src/server/accounts/__snapshots__/sync.test.ts.snap index 0602bdc5574..d82a0a3b244 100644 --- a/packages/loot-core/src/server/accounts/__snapshots__/sync.test.ts.snap +++ b/packages/loot-core/src/server/accounts/__snapshots__/sync.test.ts.snap @@ -18,6 +18,7 @@ Array [ "parent_id": null, "payee": null, "payee_name": null, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -41,6 +42,7 @@ Array [ "parent_id": null, "payee": null, "payee_name": null, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, diff --git a/packages/loot-core/src/server/accounts/sync.test.ts b/packages/loot-core/src/server/accounts/sync.test.ts index b4356875c32..f4514ca4e1f 100644 --- a/packages/loot-core/src/server/accounts/sync.test.ts +++ b/packages/loot-core/src/server/accounts/sync.test.ts @@ -4,9 +4,9 @@ import * as db from '../db'; import { loadMappings } from '../db/mappings'; import { post } from '../post'; import { getServer } from '../server-config'; +import { loadRules, insertRule } from '../transactions/transaction-rules'; import { reconcileTransactions, addTransactions } from './sync'; -import { loadRules, insertRule } from './transaction-rules'; jest.mock('../../shared/months', () => ({ ...jest.requireActual('../../shared/months'), diff --git a/packages/loot-core/src/server/accounts/sync.ts b/packages/loot-core/src/server/accounts/sync.ts index 75651164212..168883ebb32 100644 --- a/packages/loot-core/src/server/accounts/sync.ts +++ b/packages/loot-core/src/server/accounts/sync.ts @@ -26,11 +26,15 @@ import { runMutator } from '../mutators'; import { post } from '../post'; import { getServer } from '../server-config'; import { batchMessages } from '../sync'; +import { batchUpdateTransactions } from '../transactions'; +import { runRules } from '../transactions/transaction-rules'; +import { + defaultMappings, + mappingsFromString, +} from '../util/custom-sync-mapping'; import { getStartingBalancePayee } from './payees'; import { title } from './title'; -import { runRules } from './transaction-rules'; -import { batchUpdateTransactions } from './transactions'; function BankSyncError(type: string, code: string, details?: object) { return { type: 'BankSyncError', category: type, code, details }; @@ -322,57 +326,83 @@ async function normalizeTransactions( async function normalizeBankSyncTransactions(transactions, acctId) { const payeesToCreate = new Map(); + const [customMappingsRaw, importPending, importNotes] = await Promise.all([ + runQuery( + q('preferences') + .filter({ id: `custom-sync-mappings-${acctId}` }) + .select('value'), + ).then(data => data?.data?.[0]?.value), + runQuery( + q('preferences') + .filter({ id: `sync-import-pending-${acctId}` }) + .select('value'), + ).then(data => String(data?.data?.[0]?.value ?? 'true') === 'true'), + runQuery( + q('preferences') + .filter({ id: `sync-import-notes-${acctId}` }) + .select('value'), + ).then(data => String(data?.data?.[0]?.value ?? 'true') === 'true'), + ]); + + const mappings = customMappingsRaw + ? mappingsFromString(customMappingsRaw) + : defaultMappings; + const normalized = []; for (const trans of transactions) { + trans.cleared = Boolean(trans.booked); + + if (!importPending && !trans.cleared) continue; + if (!trans.amount) { trans.amount = trans.transactionAmount.amount; } + const mapping = mappings.get(trans.amount <= 0 ? 'payment' : 'deposit'); + + const date = trans[mapping.get('date')] ?? trans.date; + const payeeName = trans[mapping.get('payee')]; + const notes = trans[mapping.get('notes')]; + // Validate the date because we do some stuff with it. The db // layer does better validation, but this will give nicer errors - if (trans.date == null) { + if (date == null) { throw new Error('`date` is required when adding a transaction'); } - if (trans.payeeName == null) { + if (payeeName == null) { throw new Error('`payeeName` is required when adding a transaction'); } - trans.imported_payee = trans.imported_payee || trans.payeeName; + trans.imported_payee = trans.imported_payee || payeeName; if (trans.imported_payee) { trans.imported_payee = trans.imported_payee.trim(); } - // It's important to resolve both the account and payee early so - // when rules are run, they have the right data. Resolving payees - // also simplifies the payee creation process - trans.account = acctId; - trans.payee = await resolvePayee(trans, trans.payeeName, payeesToCreate); - - trans.cleared = Boolean(trans.booked); - let imported_id = trans.transactionId; - if (trans.cleared && !trans.transactionId && trans.internalTransactionId) { imported_id = `${trans.account}-${trans.internalTransactionId}`; } - const notes = - trans.remittanceInformationUnstructured || - (trans.remittanceInformationUnstructuredArray || []).join(', '); + // It's important to resolve both the account and payee early so + // when rules are run, they have the right data. Resolving payees + // also simplifies the payee creation process + trans.account = acctId; + trans.payee = await resolvePayee(trans, payeeName, payeesToCreate); normalized.push({ - payee_name: trans.payeeName, + payee_name: payeeName, trans: { amount: amountToInteger(trans.amount), payee: trans.payee, account: trans.account, - date: trans.date, - notes: notes.trim().replace('#', '##'), + date, + notes: importNotes && notes ? notes.trim().replace(/#/g, '##') : null, category: trans.category ?? null, imported_id, imported_payee: trans.imported_payee, cleared: trans.cleared, + raw_synced_data: JSON.stringify(trans), }, }); } @@ -444,9 +474,20 @@ export async function reconcileTransactions( imported_payee: trans.imported_payee || null, notes: existing.notes || trans.notes || null, cleared: trans.cleared ?? existing.cleared, + raw_synced_data: + existing.raw_synced_data ?? trans.raw_synced_data ?? null, }; - if (hasFieldsChanged(existing, updates, Object.keys(updates))) { + const fieldsToMarkUpdated = Object.keys(updates).filter(k => { + // do not mark raw_synced_data if it's gone from falsy to falsy + if (!existing.raw_synced_data && !trans.raw_synced_data) { + return k !== 'raw_synced_data'; + } + + return true; + }); + + if (hasFieldsChanged(existing, updates, fieldsToMarkUpdated)) { updated.push({ id: existing.id, ...updates }); if (!existingPayeeMap.has(existing.payee)) { const payee = await db.getPayee(existing.payee); diff --git a/packages/loot-core/src/server/api.ts b/packages/loot-core/src/server/api.ts index 6a39dd5b5e1..63e1ec7c14e 100644 --- a/packages/loot-core/src/server/api.ts +++ b/packages/loot-core/src/server/api.ts @@ -461,12 +461,14 @@ handlers['api/transactions-export'] = async function ({ transactions, categoryGroups, payees, + accounts, }) { checkFileOpen(); return handlers['transactions-export']({ transactions, categoryGroups, payees, + accounts, }); }; diff --git a/packages/loot-core/src/server/aql/schema/index.ts b/packages/loot-core/src/server/aql/schema/index.ts index df445ab01b9..5b85c75a65c 100644 --- a/packages/loot-core/src/server/aql/schema/index.ts +++ b/packages/loot-core/src/server/aql/schema/index.ts @@ -53,6 +53,7 @@ export const schema = { reconciled: f('boolean', { default: false }), tombstone: f('boolean'), schedule: f('id', { ref: 'schedules' }), + raw_synced_data: f('string'), // subtransactions is a special field added if the table has the // `splits: grouped` option }, @@ -73,6 +74,7 @@ export const schema = { account_id: f('string'), official_name: f('string'), account_sync_source: f('string'), + last_sync: f('string'), }, categories: { id: f('id'), diff --git a/packages/loot-core/src/server/budget/base.ts b/packages/loot-core/src/server/budget/base.ts index 56505d8d6de..a3fcb075e17 100644 --- a/packages/loot-core/src/server/budget/base.ts +++ b/packages/loot-core/src/server/budget/base.ts @@ -41,7 +41,7 @@ function createCategory(cat, sheetName, prevSheetName, start, end) { initialValue: 0, run: () => { // Making this sync is faster! - const rows = db.runQuery( + const rows = db.runQuery<{ amount: number }>( `SELECT SUM(amount) as amount FROM v_transactions_internal_alive t LEFT JOIN accounts a ON a.id = t.account WHERE t.date >= ${start} AND t.date <= ${end} @@ -86,7 +86,7 @@ function createCategoryGroup(group, sheetName) { function handleAccountChange(months, oldValue, newValue) { if (!oldValue || oldValue.offbudget !== newValue.offbudget) { - const rows = db.runQuery( + const rows = db.runQuery>( ` SELECT DISTINCT(category) as category FROM transactions WHERE acct = ? diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index 9dcd5180700..0436b3ba4a4 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -30,7 +30,12 @@ export class CategoryTemplate { // Class interface // set up the class and check all templates - static async init(templates: Template[], category: CategoryEntity, month) { + static async init( + templates: Template[], + category: CategoryEntity, + month, + budgeted: number, + ) { // get all the needed setup values const lastMonthSheet = monthUtils.sheetForMonth( monthUtils.subMonths(month, 1), @@ -47,6 +52,7 @@ export class CategoryTemplate { if (lastMonthBalance < 0 && !carryover) { fromLastMonth = 0; } else if (category.is_income) { + //for tracking budget fromLastMonth = 0; } else { fromLastMonth = lastMonthBalance; @@ -55,9 +61,23 @@ export class CategoryTemplate { await CategoryTemplate.checkByAndScheduleAndSpend(templates, month); await CategoryTemplate.checkPercentage(templates); // call the private constructor - return new CategoryTemplate(templates, category, month, fromLastMonth); + return new CategoryTemplate( + templates, + category, + month, + fromLastMonth, + budgeted, + ); } + getGoalOnly(): boolean { + // if there is only a goal + return ( + this.templates.length === 0 && + this.remainder.length === 0 && + this.goals.length > 0 + ); + } getPriorities(): number[] { return this.priorities; } @@ -224,16 +244,19 @@ export class CategoryTemplate { private limitAmount = 0; private limitCheck = false; private limitHold = false; + readonly previouslyBudgeted: number = 0; private constructor( templates: Template[], category: CategoryEntity, month: string, fromLastMonth: number, + budgeted: number, ) { this.category = category; this.month = month; this.fromLastMonth = fromLastMonth; + this.previouslyBudgeted = budgeted; // sort the template lines into regular template, goals, and remainder templates if (templates) { templates.forEach(t => { @@ -279,6 +302,7 @@ export class CategoryTemplate { private runGoal() { if (this.goals.length > 0) { + if (this.getGoalOnly()) this.toBudgetAmount = this.previouslyBudgeted; this.isLongGoal = true; this.goalAmount = amountToInteger(this.goals[0].amount); return; diff --git a/packages/loot-core/src/server/budget/goal-template.pegjs b/packages/loot-core/src/server/budget/goal-template.pegjs index 3d78ada8fb6..730da339421 100644 --- a/packages/loot-core/src/server/budget/goal-template.pegjs +++ b/packages/loot-core/src/server/budget/goal-template.pegjs @@ -18,8 +18,8 @@ expr { return { type: 'simple', monthly, limit, priority: template.priority, directive: template.directive }} / template: template _ limit: limit { return { type: 'simple', monthly: null, limit, priority: template.priority, directive: template.directive }} - / template: template _ schedule _ full:full? name: name - { return { type: 'schedule', name, priority: template.priority, directive: template.directive, full }} + / template: template _ schedule:schedule _ full:full? name:rawScheduleName modifiers:modifiers? + { return { type: 'schedule', name: name.trim(), priority: template.priority, directive: template.directive, full, adjustment: modifiers?.adjustment }} / template: template _ remainder: remainder limit: limit? { return { type: 'remainder', priority: null, directive: template.directive, weight: remainder, limit }} / template: template _ 'average'i _ amount: positive _ 'months'i? @@ -28,6 +28,13 @@ expr { return { type: 'copy', priority: template.priority, directive: template.directive, lookBack: +lookBack, limit }} / goal: goal amount: amount { return {type: 'simple', amount: amount, priority: null, directive: goal }} +modifiers = _ '[' modifier:modifier ']' { return modifier } + +modifier + = op:('increase'i / 'decrease'i) _ value:percent { + const multiplier = op.toLowerCase() === 'increase' ? 1 : -1; + return { adjustment: multiplier * +value } + } repeat 'repeat interval' = 'month'i { return { annual: false }} @@ -59,24 +66,37 @@ repeatEvery = 'repeat'i _ 'every'i starting = 'starting'i upTo = 'up'i _ 'to'i hold = 'hold'i {return true} -schedule = 'schedule'i +schedule = 'schedule'i { return text() } full = 'full'i _ {return true} priority = '-'i number: number {return number} remainder = 'remainder'i _? weight: positive? { return +weight || 1 } template = '#template' priority: priority? {return {priority: +priority, directive: 'template'}} goal = '#goal'i { return 'goal'} -_ 'space' = ' '+ +_ "whitespace" = [ \t]* { return text() } +__ "mandatory whitespace" = [ \t]+ { return text() } + d 'digit' = [0-9] number 'number' = $(d+) positive = $([1-9][0-9]*) amount 'amount' = currencySymbol? _? amount: $('-'?d+ ('.' (d d?)?)?) { return +amount } -percent 'percentage' = percent: $(d+ ('.' (d+)?)?) _? '%' { return +percent } +percent 'percentage' = percent: $(d+ ('.' (d+)?)?) _? '%' { return percent } year 'year' = $(d d d d) month 'month' = $(year '-' d d) day 'day' = $(d d) date = $(month '-' day) currencySymbol 'currency symbol' = symbol: . & { return /\p{Sc}/u.test(symbol) } -name 'Name' = $([^\r\n\t]+) +// Match schedule name including spaces up until we see a [, looking ahead to make sure it's followed by increase/decrease +rawScheduleName = $( + ( + [^ \t\r\n\[] // First character can't be space or [ + ( + [^\r\n\[] // Subsequent characters can include spaces but not [ + / + (![^\r\n\[]* '['('increase'i/'decrease'i)) [ ] // Or spaces if not followed by [increase/decrease + )* + ) +) { return text() } +name 'Name' = $([^\r\n\t]+) { return text() } \ No newline at end of file diff --git a/packages/loot-core/src/server/budget/goalsSchedule.ts b/packages/loot-core/src/server/budget/goalsSchedule.ts index 9eeef419113..9705a81afc2 100644 --- a/packages/loot-core/src/server/budget/goalsSchedule.ts +++ b/packages/loot-core/src/server/budget/goalsSchedule.ts @@ -29,11 +29,16 @@ async function createScheduleList( const conditions = rule.serialize().conditions; const { date: dateConditions, amount: amountCondition } = extractScheduleConds(conditions); - const scheduleAmount = + let scheduleAmount = amountCondition.op === 'isbetween' ? Math.round(amountCondition.value.num1 + amountCondition.value.num2) / 2 : amountCondition.value; + // Apply adjustment percentage if specified + if (template[ll].adjustment) { + const adjustmentFactor = 1 + template[ll].adjustment / 100; + scheduleAmount = Math.round(scheduleAmount * adjustmentFactor); + } const { amount: postRuleAmount, subtransactions } = rule.execActions({ amount: scheduleAmount, category: category.id, diff --git a/packages/loot-core/src/server/budget/goaltemplates.ts b/packages/loot-core/src/server/budget/goaltemplates.ts index 7a4b00a30c1..f137c6190a9 100644 --- a/packages/loot-core/src/server/budget/goaltemplates.ts +++ b/packages/loot-core/src/server/budget/goaltemplates.ts @@ -164,15 +164,22 @@ async function processTemplate( // gather needed priorities // gather remainder weights try { - const obj = await CategoryTemplate.init(templates, category, month); - availBudget += budgeted; + const obj = await CategoryTemplate.init( + templates, + category, + month, + budgeted, + ); + // don't use the funds that are not from templates + if (!obj.getGoalOnly()) { + availBudget += budgeted; + } availBudget += obj.getLimitExcess(); const p = obj.getPriorities(); p.forEach(pr => priorities.push(pr)); remainderWeight += obj.getRemainderWeight(); catObjects.push(obj); } catch (e) { - //console.log(`${categories[i].name}: ${e}`); errors.push(`${categories[i].name}: ${e.message}`); } @@ -183,7 +190,6 @@ async function processTemplate( goal: null, longGoal: null, }); - //await setGoal({ month, category: id, goal: null, long_goal: null }); } } diff --git a/packages/loot-core/src/server/budget/statements.ts b/packages/loot-core/src/server/budget/statements.ts index 43a1af7d940..13e4707a5f7 100644 --- a/packages/loot-core/src/server/budget/statements.ts +++ b/packages/loot-core/src/server/budget/statements.ts @@ -1,5 +1,5 @@ import * as db from '../db'; -import { Schedule } from '../db/types'; +import { DbSchedule } from '../db'; import { GOAL_PREFIX, TEMPLATE_PREFIX } from './template-notes'; @@ -41,7 +41,7 @@ export async function getCategoriesWithTemplateNotes(): Promise< ); } -export async function getActiveSchedules(): Promise { +export async function getActiveSchedules(): Promise { return await db.all( 'SELECT id, rule, active, completed, posts_transaction, tombstone, name from schedules WHERE name NOT NULL AND tombstone = 0', ); diff --git a/packages/loot-core/src/server/budget/template-notes.test.ts b/packages/loot-core/src/server/budget/template-notes.test.ts index f2e36c7281c..81709dc63e4 100644 --- a/packages/loot-core/src/server/budget/template-notes.test.ts +++ b/packages/loot-core/src/server/budget/template-notes.test.ts @@ -1,5 +1,4 @@ import * as db from '../db'; -import { Schedule } from '../db/types'; import { CategoryWithTemplateNote, @@ -20,7 +19,7 @@ function mockGetTemplateNotesForCategories( ); } -function mockGetActiveSchedules(schedules: Schedule[]) { +function mockGetActiveSchedules(schedules: db.DbSchedule[]) { (getActiveSchedules as jest.Mock).mockResolvedValue(schedules); } @@ -227,6 +226,38 @@ describe('checkTemplates', () => { pre: 'Category 1: Schedule “Non-existent Schedule” does not exist', }, }, + { + description: 'Returns errors for invalid increase schedule adjustments', + mockTemplateNotes: [ + { + id: 'cat1', + name: 'Category 1', + note: '#template schedule Mock Schedule 1 [increase 1001%]', + }, + ], + mockSchedules: mockSchedules(), + expected: { + sticky: true, + message: 'There were errors interpreting some templates:', + pre: 'Category 1: #template schedule Mock Schedule 1 [increase 1001%]\nError: Invalid adjustment percentage (1001%). Must be between -100% and 1000%', + }, + }, + { + description: 'Returns errors for invalid decrease schedule adjustments', + mockTemplateNotes: [ + { + id: 'cat1', + name: 'Category 1', + note: '#template schedule Mock Schedule 1 [decrease 101%]', + }, + ], + mockSchedules: mockSchedules(), + expected: { + sticky: true, + message: 'There were errors interpreting some templates:', + pre: 'Category 1: #template schedule Mock Schedule 1 [decrease 101%]\nError: Invalid adjustment percentage (-101%). Must be between -100% and 1000%', + }, + }, ]; it.each(testCases)( @@ -245,7 +276,7 @@ describe('checkTemplates', () => { ); }); -function mockSchedules(): Schedule[] { +function mockSchedules(): db.DbSchedule[] { return [ { id: 'mock-schedule-1', diff --git a/packages/loot-core/src/server/budget/template-notes.ts b/packages/loot-core/src/server/budget/template-notes.ts index 7761b4f4439..e0243c7ed3c 100644 --- a/packages/loot-core/src/server/budget/template-notes.ts +++ b/packages/loot-core/src/server/budget/template-notes.ts @@ -43,7 +43,12 @@ export async function checkTemplates(): Promise { categoryWithTemplates.forEach(({ name, templates }) => { templates.forEach(template => { if (template.type === 'error') { - errors.push(`${name}: ${template.line}`); + // Only show detailed error for adjustment-related errors + if (template.error && template.error.includes('adjustment')) { + errors.push(`${name}: ${template.line}\nError: ${template.error}`); + } else { + errors.push(`${name}: ${template.line}`); + } } else if ( template.type === 'schedule' && !scheduleNames.includes(template.name) @@ -91,6 +96,21 @@ async function getCategoriesWithTemplates(): Promise { try { const parsedTemplate: Template = parse(trimmedLine); + // Validate schedule adjustments + if ( + parsedTemplate.type === 'schedule' && + parsedTemplate.adjustment !== undefined + ) { + if ( + parsedTemplate.adjustment <= -100 || + parsedTemplate.adjustment > 1000 + ) { + throw new Error( + `Invalid adjustment percentage (${parsedTemplate.adjustment}%). Must be between -100% and 1000%`, + ); + } + } + parsedTemplates.push(parsedTemplate); } catch (e: unknown) { parsedTemplates.push({ diff --git a/packages/loot-core/src/server/budget/types/templates.d.ts b/packages/loot-core/src/server/budget/types/templates.d.ts index 8b5eb6e323d..39b36f6e3df 100644 --- a/packages/loot-core/src/server/budget/types/templates.d.ts +++ b/packages/loot-core/src/server/budget/types/templates.d.ts @@ -45,6 +45,7 @@ interface ScheduleTemplate extends BaseTemplate { type: 'schedule'; name: string; full?: boolean; + adjustment?: number; } interface RemainderTemplate extends BaseTemplate { @@ -59,7 +60,7 @@ interface AverageTemplate extends BaseTemplate { } interface GoalTemplate extends BaseTemplate { - type: 'simple'; + type: 'goal'; amount: number; } diff --git a/packages/loot-core/src/server/db/index.ts b/packages/loot-core/src/server/db/index.ts index dc324fca85f..12e80e07a1d 100644 --- a/packages/loot-core/src/server/db/index.ts +++ b/packages/loot-core/src/server/db/index.ts @@ -15,11 +15,7 @@ import * as fs from '../../platform/server/fs'; import * as sqlite from '../../platform/server/sqlite'; import * as monthUtils from '../../shared/months'; import { groupById } from '../../shared/util'; -import { - CategoryEntity, - CategoryGroupEntity, - PayeeEntity, -} from '../../types/models'; +import { CategoryEntity, CategoryGroupEntity } from '../../types/models'; import { schema, schemaConfig, @@ -37,6 +33,16 @@ import { import { sendMessages, batchMessages } from '../sync'; import { shoveSortOrders, SORT_INCREMENT } from './sort'; +import { + DbAccount, + DbCategory, + DbCategoryGroup, + DbPayee, + DbTransaction, + DbViewTransaction, +} from './types'; + +export * from './types'; export { toDateRepr, fromDateRepr } from '../models'; @@ -100,17 +106,24 @@ export function runQuery( sql: string, params?: Array, fetchAll?: false, -); -export function runQuery( +): { changes: unknown }; + +export function runQuery( sql: string, params: Array | undefined, fetchAll: true, -); -export function runQuery(sql, params, fetchAll) { - // const unrecord = perf.record('sqlite'); - const result = sqlite.runQuery(db, sql, params, fetchAll); - // unrecord(); - return result; +): T[]; + +export function runQuery( + sql: string, + params: (string | number)[], + fetchAll: boolean, +) { + if (fetchAll) { + return sqlite.runQuery(db, sql, params, true); + } else { + return sqlite.runQuery(db, sql, params, false); + } } export function execQuery(sql: string) { @@ -147,19 +160,28 @@ export function asyncTransaction(fn: () => Promise) { // async. We return a promise here until we've audited all the code to // make sure nothing calls `.then` on this. export async function all(sql, params?: (string | number)[]) { - return runQuery(sql, params, true); + // TODO: In the next phase, we will make this function generic + // and pass the type of the return type to `runQuery`. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return runQuery(sql, params, true) as any[]; } export async function first(sql, params?: (string | number)[]) { const arr = await runQuery(sql, params, true); - return arr.length === 0 ? null : arr[0]; + // TODO: In the next phase, we will make this function generic + // and pass the type of the return type to `runQuery`. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return arr.length === 0 ? null : (arr[0] as any); } // The underlying sql system is now sync, but we can't update `first` yet // without auditing all uses of it export function firstSync(sql, params?: (string | number)[]) { const arr = runQuery(sql, params, true); - return arr.length === 0 ? null : arr[0]; + // TODO: In the next phase, we will make this function generic + // and pass the type of the return type to `runQuery`. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return arr.length === 0 ? null : (arr[0] as any); } // This function is marked as async because `runQuery` is no longer @@ -175,7 +197,10 @@ export async function select(table, id) { [id], true, ); - return rows[0]; + // TODO: In the next phase, we will make this function generic + // and pass the type of the return type to `runQuery`. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return rows[0] as any; } export async function update(table, params) { @@ -252,9 +277,12 @@ export async function deleteAll(table: string) { export async function selectWithSchema(table, sql, params) { const rows = await runQuery(sql, params, true); - return rows + const convertedRows = rows .map(row => convertFromSelect(schema, schemaConfig, table, row)) .filter(Boolean); + // TODO: Make convertFromSelect generic so we don't need this cast + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return convertedRows as any[]; } export async function selectFirstWithSchema(table, sql, params) { @@ -282,16 +310,18 @@ export function updateWithSchema(table, fields) { // Data-specific functions. Ideally this would be split up into // different files +// TODO: Fix return type. This should returns a DbCategory[]. export async function getCategories( - ids?: Array, + ids?: Array, ): Promise { const whereIn = ids ? `c.id IN (${toSqlQueryParameters(ids)}) AND` : ''; const query = `SELECT c.* FROM categories c WHERE ${whereIn} c.tombstone = 0 ORDER BY c.sort_order, c.id`; return ids ? await all(query, [...ids]) : await all(query); } +// TODO: Fix return type. This should returns a [DbCategoryGroup, ...DbCategory]. export async function getCategoriesGrouped( - ids?: Array, + ids?: Array, ): Promise> { const categoryGroupWhereIn = ids ? `cg.id IN (${toSqlQueryParameters(ids)}) AND` @@ -432,7 +462,11 @@ export function updateCategory(category) { return update('categories', category); } -export async function moveCategory(id, groupId, targetId?: string) { +export async function moveCategory( + id: DbCategory['id'], + groupId: DbCategoryGroup['id'], + targetId?: DbCategory['id'], +) { if (!groupId) { throw new Error('moveCategory: groupId is required'); } @@ -449,7 +483,10 @@ export async function moveCategory(id, groupId, targetId?: string) { await update('categories', { id, sort_order, cat_group: groupId }); } -export async function deleteCategory(category, transferId?: string) { +export async function deleteCategory( + category: Pick, + transferId?: DbCategory['id'], +) { if (transferId) { // We need to update all the deleted categories that currently // point to the one we're about to delete so they all are @@ -469,11 +506,11 @@ export async function deleteCategory(category, transferId?: string) { return delete_('categories', category.id); } -export async function getPayee(id) { +export async function getPayee(id: DbPayee['id']) { return first(`SELECT * FROM payees WHERE id = ?`, [id]); } -export async function getAccount(id) { +export async function getAccount(id: DbAccount['id']) { return first(`SELECT * FROM accounts WHERE id = ?`, [id]); } @@ -487,7 +524,7 @@ export async function insertPayee(payee) { return id; } -export async function deletePayee(payee) { +export async function deletePayee(payee: Pick) { const { transfer_acct } = await first('SELECT * FROM payees WHERE id = ?', [ payee.id, ]); @@ -506,7 +543,7 @@ export async function deletePayee(payee) { return delete_('payees', payee.id); } -export async function deleteTransferPayee(payee) { +export async function deleteTransferPayee(payee: Pick) { // This allows deleting transfer payees return delete_('payees', payee.id); } @@ -516,9 +553,12 @@ export function updatePayee(payee) { return update('payees', payee); } -export async function mergePayees(target: string, ids: string[]) { +export async function mergePayees( + target: DbPayee['id'], + ids: Array, +) { // Load in payees so we can check some stuff - const dbPayees: PayeeEntity[] = await all('SELECT * FROM payees'); + const dbPayees: DbPayee[] = await all('SELECT * FROM payees'); const payees = groupById(dbPayees); // Filter out any transfer payees @@ -613,7 +653,7 @@ export async function getOrphanedPayees() { return rows.map(row => row.id); } -export async function getPayeeByName(name) { +export async function getPayeeByName(name: DbPayee['name']) { return first( `SELECT * FROM payees WHERE UNICODE_LOWER(name) = ? AND tombstone = 0`, [name.toLowerCase()], @@ -632,7 +672,7 @@ export function getAccounts() { export async function insertAccount(account) { const accounts = await all( 'SELECT * FROM accounts WHERE offbudget = ? ORDER BY sort_order, name', - [account.offbudget != null ? account.offbudget : 0], + [account.offbudget ? 1 : 0], ); // Don't pass a target in, it will default to appending at the end @@ -651,7 +691,10 @@ export function deleteAccount(account) { return delete_('accounts', account.id); } -export async function moveAccount(id, targetId) { +export async function moveAccount( + id: DbAccount['id'], + targetId: DbAccount['id'], +) { const account = await first('SELECT * FROM accounts WHERE id = ?', [id]); let accounts; if (account.closed) { @@ -661,7 +704,7 @@ export async function moveAccount(id, targetId) { } else { accounts = await all( `SELECT id, sort_order FROM accounts WHERE tombstone = 0 AND offbudget = ? ORDER BY sort_order, name`, - [account.offbudget], + [account.offbudget ? 1 : 0], ); } @@ -674,7 +717,7 @@ export async function moveAccount(id, targetId) { }); } -export async function getTransaction(id) { +export async function getTransaction(id: DbViewTransaction['id']) { const rows = await selectWithSchema( 'transactions', 'SELECT * FROM v_transactions WHERE id = ?', @@ -683,7 +726,7 @@ export async function getTransaction(id) { return rows[0]; } -export async function getTransactions(accountId) { +export async function getTransactions(accountId: DbTransaction['acct']) { if (arguments.length > 1) { throw new Error( '`getTransactions` was given a second argument, it now only takes a single argument `accountId`', diff --git a/packages/loot-core/src/server/db/mappings.ts b/packages/loot-core/src/server/db/mappings.ts index 81b0c280db2..f7423cab5f0 100644 --- a/packages/loot-core/src/server/db/mappings.ts +++ b/packages/loot-core/src/server/db/mappings.ts @@ -22,14 +22,12 @@ let unlistenSync; export async function loadMappings() { // The mappings are separated into tables specific to the type of // data. But you know, we really could keep a global mapping table. - const categories = (await db.all('SELECT * FROM category_mapping')).map(r => [ - r.id, - r.transferId, - ]); - const payees = (await db.all('SELECT * FROM payee_mapping')).map(r => [ - r.id, - r.targetId, - ]); + const categories = (await db.all('SELECT * FROM category_mapping')).map( + r => [r.id, r.transferId] as const, + ); + const payees = (await db.all('SELECT * FROM payee_mapping')).map( + r => [r.id, r.targetId] as const, + ); // All ids are unique, so we can just keep a global table of mappings allMappings = new Map(categories.concat(payees)); diff --git a/packages/loot-core/src/server/db/types.d.ts b/packages/loot-core/src/server/db/types.d.ts deleted file mode 100644 index b6fdd94ec3a..00000000000 --- a/packages/loot-core/src/server/db/types.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type Schedule = { - id: string; - rule: string; - active: number; - completed: number; - posts_transaction: number; - tombstone: number; - name: string | null; -}; diff --git a/packages/loot-core/src/server/db/types/index.ts b/packages/loot-core/src/server/db/types/index.ts new file mode 100644 index 00000000000..c6accb44f00 --- /dev/null +++ b/packages/loot-core/src/server/db/types/index.ts @@ -0,0 +1,315 @@ +// These are the types that exactly match the database schema. +// The `Entity` types e.g. `TransactionEntity`, `AccountEntity`, etc +// are specific to the AQL query framework and does not necessarily +// match the actual database schema. + +type JsonString = string; + +export type DbAccount = { + id: string; + name: string; + offbudget: 1 | 0; + closed: 1 | 0; + tombstone: 1 | 0; + sort_order: number; + account_id?: string | null; + balance_current?: number | null; + balance_available?: number | null; + balance_limit?: number | null; + mask?: string | null; + official_name?: string | null; + type?: string | null; + subtype?: string | null; + bank?: string | null; + account_sync_source?: 'simpleFin' | 'goCardless' | null; +}; + +export type DbBank = { + id: string; + bank_id: string; + name: string; + tombstone: 1 | 0; +}; + +export type DbCategory = { + id: string; + name: string; + is_income: 1 | 0; + cat_group: DbCategoryGroup['id']; + sort_order: number; + hidden: 1 | 0; + goal_def?: JsonString | null; + tombstone: 1 | 0; +}; + +export type DbCategoryGroup = { + id: string; + name: string; + is_income: 1 | 0; + sort_order: number; + hidden: 1 | 0; + tombstone: 1 | 0; +}; + +export type DbCategoryMapping = { + id: DbCategory['id']; + transferId: DbCategory['id']; +}; + +export type DbKvCache = { + key: string; + value: string; +}; + +export type DbKvCacheKey = { + id: number; + key: number; +}; + +export type DbClockMessage = { + id: string; + clock: string; +}; + +export type DbCrdtMessage = { + id: string; + timestamp: string; + dataset: string; + row: string; + column: string; + value: Uint8Array; +}; + +export type DbNote = { + id: string; + note: string; +}; + +export type DbPayeeMapping = { + id: DbPayee['id']; + targetId: DbPayee['id']; +}; + +export type DbPayee = { + id: string; + name: string; + transfer_acct?: DbAccount['id'] | null; + favorite: 1 | 0; + learn_categories: 1 | 0; + tombstone: 1 | 0; + // Unused in the codebase + category?: string | null; +}; + +export type DbRule = { + id: string; + stage: string; + conditions: JsonString; + actions: JsonString; + tombstone: 1 | 0; + conditions_op: string; +}; + +export type DbSchedule = { + id: string; + name: string; + rule: DbRule['id']; + active: 1 | 0; + completed: 1 | 0; + posts_transaction: 1 | 0; + tombstone: 1 | 0; +}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type DbScheduleJsonPath = { + schedule_id: DbSchedule['id']; + payee: string; + account: string; + amount: string; + date: string; +}; + +export type DbScheduleNextDate = { + id: string; + schedule_id: DbSchedule['id']; + local_next_date: number; + local_next_date_ts: number; + base_next_date: number; + base_next_date_ts: number; +}; + +// This is unused in the codebase. +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type DbPendingTransaction = { + id: string; + acct: number; + amount: number; + description: string; + date: string; +}; + +export type DbTransaction = { + id: string; + isParent: 1 | 0; + isChild: 1 | 0; + date: number; + acct: DbAccount['id']; + amount: number; + sort_order: number; + parent_id?: DbTransaction['id'] | null; + category?: DbCategory['id'] | null; + description?: string | null; + notes?: string | null; + financial_id?: string | null; + error?: string | null; + imported_description?: string | null; + transferred_id?: DbTransaction['id'] | null; + schedule?: DbSchedule['id'] | null; + starting_balance_flag: 1 | 0; + tombstone: 1 | 0; + cleared: 1 | 0; + reconciled: 1 | 0; + // Unused in the codebase + pending?: 1 | 0 | null; + location?: string | null; + type?: string | null; +}; + +export type DbReflectBudget = { + id: string; + month: number; + category: string; + amount: number; + carryover: number; + goal: number; + long_goal: number; +}; + +export type DbZeroBudgetMonth = { + id: string; + buffered: number; +}; + +export type DbZeroBudget = { + id: string; + month: number; + category: string; + amount: number; + carryover: number; + goal: number; + long_goal: number; +}; + +export type DbTransactionFilter = { + id: string; + name: string; + conditions: JsonString; + conditions_op: string; + tombstone: 1 | 0; +}; + +export type DbPreference = { + id: string; + value: string; +}; + +export type DbCustomReport = { + id: string; + name: string; + start_date: string; + end_date: string; + date_static: number; + date_range: string; + mode: string; + group_by: string; + balance_type: string; + show_empty: 1 | 0; + show_offbudget: 1 | 0; + show_hidden: 1 | 0; + show_uncateogorized: 1 | 0; + selected_categories: string; + graph_type: string; + conditions: JsonString; + conditions_op: string; + metadata: JsonString; + interval: string; + color_scheme: string; + include_current: 1 | 0; + sort_by: string; + tombstone: 1 | 0; +}; + +export type DbDashboard = { + id: string; + type: string; + width: number; + height: number; + x: number; + y: number; + meta: JsonString; + tombstone: 1 | 0; +}; + +export type DbViewTransactionInternal = { + id: DbTransaction['id']; + is_parent: DbTransaction['isParent']; + is_child: DbTransaction['isChild']; + date: DbTransaction['date']; + account: DbAccount['id']; + amount: DbTransaction['amount']; + parent_id: DbTransaction['parent_id'] | null; + category: DbCategory['id'] | null; + payee: DbPayee['id'] | null; + notes: DbTransaction['notes'] | null; + imported_id: DbTransaction['financial_id'] | null; + error: DbTransaction['error'] | null; + imported_payee: DbTransaction['imported_description'] | null; + starting_balance_flag: DbTransaction['starting_balance_flag'] | null; + transfer_id: DbTransaction['transferred_id'] | null; + schedule: DbSchedule['id'] | null; + sort_order: DbTransaction['sort_order']; + cleared: DbTransaction['cleared']; + tombstone: DbTransaction['tombstone']; + reconciled: DbTransaction['reconciled']; +}; + +export type DbViewTransactionInternalAlive = DbViewTransactionInternal; +export type DbViewTransaction = DbViewTransactionInternalAlive; + +export type DbViewCategory = { + id: DbCategory['id']; + name: DbCategory['name']; + is_income: DbCategory['is_income']; + hidden: DbCategory['hidden']; + group: DbCategoryGroup['id']; + sort_order: DbCategory['sort_order']; + tombstone: DbCategory['tombstone']; +}; + +export type DbViewPayee = { + id: DbPayee['id']; + name: DbAccount['name'] | DbPayee['name']; + transfer_acct: DbPayee['transfer_acct']; + tombstone: DbPayee['tombstone']; +}; + +export type DbViewSchedule = { + id: DbSchedule['id']; + name: DbSchedule['name']; + rule: DbSchedule['rule']; + next_date: + | DbScheduleNextDate['local_next_date_ts'] + | DbScheduleNextDate['local_next_date'] + | DbScheduleNextDate['base_next_date']; + active: DbSchedule['active']; + completed: DbSchedule['completed']; + posts_transaction: DbSchedule['posts_transaction']; + tombstone: DbSchedule['tombstone']; + _payee: DbPayeeMapping['targetId']; + _account: DbAccount['id']; + _amount: number; + _amountOp: string; + _date: JsonString; + _conditions: JsonString; + _actions: JsonString; +}; diff --git a/packages/loot-core/src/server/filters/app.ts b/packages/loot-core/src/server/filters/app.ts index c6b4ef69c96..8fc22399b12 100644 --- a/packages/loot-core/src/server/filters/app.ts +++ b/packages/loot-core/src/server/filters/app.ts @@ -1,11 +1,11 @@ // @ts-strict-ignore import { v4 as uuidv4 } from 'uuid'; -import { parseConditionsOrActions } from '../accounts/transaction-rules'; import { createApp } from '../app'; import * as db from '../db'; import { requiredFields } from '../models'; import { mutator } from '../mutators'; +import { parseConditionsOrActions } from '../transactions/transaction-rules'; import { undoable } from '../undo'; import { FiltersHandlers } from './types/handlers'; diff --git a/packages/loot-core/src/server/main.test.ts b/packages/loot-core/src/server/main.test.ts index ea4d8713007..39a16890d5f 100644 --- a/packages/loot-core/src/server/main.test.ts +++ b/packages/loot-core/src/server/main.test.ts @@ -1,5 +1,6 @@ // @ts-strict-ignore import { getClock, deserializeClock } from '@actual-app/crdt'; +import { v4 as uuidv4 } from 'uuid'; import { expectSnapshotWithDiffer } from '../mocks/util'; import * as connection from '../platform/server/connection'; @@ -175,6 +176,7 @@ describe('Budget', () => { // budgets for the earlier months await db.runQuery("INSERT INTO accounts (id, name) VALUES ('one', 'boa')"); await runHandler(handlers['transaction-add'], { + id: uuidv4(), date: '2016-05-06', amount: 50, account: 'one', diff --git a/packages/loot-core/src/server/main.ts b/packages/loot-core/src/server/main.ts index 6ca7ab44f54..4b4a4de9d4a 100644 --- a/packages/loot-core/src/server/main.ts +++ b/packages/loot-core/src/server/main.ts @@ -15,19 +15,15 @@ import * as sqlite from '../platform/server/sqlite'; import { isNonProductionEnvironment } from '../shared/environment'; import * as monthUtils from '../shared/months'; import { dayFromDate } from '../shared/months'; -import { q, Query } from '../shared/query'; +import { q } from '../shared/query'; import { amountToInteger, stringToInteger } from '../shared/util'; import { type Budget } from '../types/budget'; import { Handlers } from '../types/handlers'; import { OpenIdConfig } from '../types/models/openid'; -import { exportToCSV, exportQueryToCSV } from './accounts/export-to-csv'; import * as link from './accounts/link'; -import { parseFile } from './accounts/parse-file'; import { getStartingBalancePayee } from './accounts/payees'; import * as bankSync from './accounts/sync'; -import * as rules from './accounts/transaction-rules'; -import { batchUpdateTransactions } from './accounts/transactions'; import { app as adminApp } from './admin/app'; import { installAPI } from './api'; import { runQuery as aqlQuery } from './aql'; @@ -73,6 +69,8 @@ import { } from './sync'; import * as syncMigrations from './sync/migrate'; import { app as toolsApp } from './tools/app'; +import { app as transactionsApp } from './transactions/app'; +import * as rules from './transactions/transaction-rules'; import { withUndo, clearUndo, undo, redo } from './undo'; import { updateVersion } from './update'; import { @@ -108,56 +106,6 @@ handlers['redo'] = mutator(function () { return redo(); }); -handlers['transactions-batch-update'] = mutator(async function ({ - added, - deleted, - updated, - learnCategories, -}) { - return withUndo(async () => { - const result = await batchUpdateTransactions({ - added, - updated, - deleted, - learnCategories, - }); - - return result; - }); -}); - -handlers['transaction-add'] = mutator(async function (transaction) { - await handlers['transactions-batch-update']({ added: [transaction] }); - return {}; -}); - -handlers['transaction-update'] = mutator(async function (transaction) { - await handlers['transactions-batch-update']({ updated: [transaction] }); - return {}; -}); - -handlers['transaction-delete'] = mutator(async function (transaction) { - await handlers['transactions-batch-update']({ deleted: [transaction] }); - return {}; -}); - -handlers['transactions-parse-file'] = async function ({ filepath, options }) { - return parseFile(filepath, options); -}; - -handlers['transactions-export'] = async function ({ - transactions, - accounts, - categoryGroups, - payees, -}) { - return exportToCSV(transactions, accounts, categoryGroups, payees); -}; - -handlers['transactions-export-query'] = async function ({ query: queryState }) { - return exportQueryToCSV(new Query(queryState)); -}; - handlers['get-categories'] = async function () { return { grouped: await db.getCategoriesGrouped(), @@ -165,17 +113,6 @@ handlers['get-categories'] = async function () { }; }; -handlers['get-earliest-transaction'] = async function () { - const { data } = await aqlQuery( - q('transactions') - .options({ splits: 'none' }) - .orderBy({ date: 'asc' }) - .select('*') - .limit(1), - ); - return data[0] || null; -}; - handlers['get-budget-bounds'] = async function () { return budget.createAllBudgets(); }; @@ -420,7 +357,7 @@ handlers['category-group-delete'] = mutator(async function ({ }); handlers['must-category-transfer'] = async function ({ id }) { - const res = await db.runQuery( + const res = await db.runQuery<{ count: number }>( `SELECT count(t.id) as count FROM transactions t LEFT JOIN category_mapping cm ON cm.id = t.category WHERE cm.transferId = ? AND t.tombstone = 0`, @@ -778,7 +715,9 @@ handlers['account-close'] = mutator(async function ({ if (numTransactions === 0) { await db.deleteAccount({ id }); } else if (forced) { - const rows = await db.runQuery( + const rows = await db.runQuery< + Pick + >( 'SELECT id, transfer_id FROM v_transactions WHERE account = ?', [id], true, @@ -1050,7 +989,7 @@ handlers['gocardless-create-web-token'] = async function ({ } }; -function handleSyncResponse( +async function handleSyncResponse( res, acct, newTransactions, @@ -1065,6 +1004,10 @@ function handleSyncResponse( if (added.length > 0) { updatedAccounts.push(acct.id); } + + const ts = new Date().getTime().toString(); + const id = acct.id; + await db.runQuery(`UPDATE accounts SET last_sync = ? WHERE id = ?`, [ts, id]); } function handleSyncError(err, acct) { @@ -1099,9 +1042,11 @@ handlers['accounts-bank-sync'] = async function ({ ids = [] }) { const [[, userId], [, userKey]] = await asyncStorage.multiGet([ 'user-id', 'user-key', - ]); + ] as const); - const accounts = await db.runQuery( + const accounts = await db.runQuery< + db.DbAccount & { bankId: db.DbBank['bank_id'] } + >( ` SELECT a.*, b.bank_id as bankId FROM accounts a @@ -1132,7 +1077,7 @@ handlers['accounts-bank-sync'] = async function ({ ids = [] }) { acct.bankId, ); - handleSyncResponse( + await handleSyncResponse( res, acct, newTransactions, @@ -1160,7 +1105,9 @@ handlers['accounts-bank-sync'] = async function ({ ids = [] }) { }; handlers['simplefin-batch-sync'] = async function ({ ids = [] }) { - const accounts = await db.runQuery( + const accounts = await db.runQuery< + db.DbAccount & { bankId: db.DbBank['bank_id'] } + >( `SELECT a.*, b.bank_id as bankId FROM accounts a LEFT JOIN banks b ON a.bank = b.id WHERE @@ -1201,7 +1148,7 @@ handlers['simplefin-batch-sync'] = async function ({ ids = [] }) { ), ); } else { - handleSyncResponse( + await handleSyncResponse( account.res, accounts.find(a => a.id === account.accountId), newTransactions, @@ -1219,7 +1166,7 @@ handlers['simplefin-batch-sync'] = async function ({ ids = [] }) { const errors = []; for (const account of accounts) { retVal.push({ - accountId: account.accountId, + accountId: account.id, res: { errors, newTransactions: [], @@ -1393,7 +1340,7 @@ handlers['load-global-prefs'] = async function () { 'theme', 'preferred-dark-theme', 'server-self-signed-cert', - ]); + ] as const); return { floatingSidebar: floatingSidebar === 'true' ? true : false, maxMonths: stringToInteger(maxMonths || ''), @@ -2464,6 +2411,7 @@ app.combine( reportsApp, rulesApp, adminApp, + transactionsApp, ); function getDefaultDocumentDir() { diff --git a/packages/loot-core/src/server/rules/app.ts b/packages/loot-core/src/server/rules/app.ts index df8b60def1d..a13de653fc6 100644 --- a/packages/loot-core/src/server/rules/app.ts +++ b/packages/loot-core/src/server/rules/app.ts @@ -1,15 +1,16 @@ // @ts-strict-ignore import { type RuleEntity } from '../../types/models'; -import { Condition, Action, rankRules } from '../accounts/rules'; -import * as rules from '../accounts/transaction-rules'; import { createApp } from '../app'; import { RuleError } from '../errors'; import { mutator } from '../mutators'; import { batchMessages } from '../sync'; +import * as rules from '../transactions/transaction-rules'; import { undoable } from '../undo'; import { RulesHandlers } from './types/handlers'; +import { Condition, Action, rankRules } from '.'; + function validateRule(rule: Partial) { // Returns an array of errors, the array is the same link as the // passed-in `array`, or null if there are no errors diff --git a/packages/loot-core/src/server/accounts/rules.test.ts b/packages/loot-core/src/server/rules/index.test.ts similarity index 99% rename from packages/loot-core/src/server/accounts/rules.test.ts rename to packages/loot-core/src/server/rules/index.test.ts index 6935cb7eeb8..e0e5325f6eb 100644 --- a/packages/loot-core/src/server/accounts/rules.test.ts +++ b/packages/loot-core/src/server/rules/index.test.ts @@ -7,7 +7,7 @@ import { Action, Rule, RuleIndexer, -} from './rules'; +} from '.'; describe('Condition', () => { test('parses date formats correctly', () => { diff --git a/packages/loot-core/src/server/accounts/rules.ts b/packages/loot-core/src/server/rules/index.ts similarity index 100% rename from packages/loot-core/src/server/accounts/rules.ts rename to packages/loot-core/src/server/rules/index.ts diff --git a/packages/loot-core/src/server/rules/types/handlers.ts b/packages/loot-core/src/server/rules/types/handlers.ts index 766c45f2be5..18b76a7ce0c 100644 --- a/packages/loot-core/src/server/rules/types/handlers.ts +++ b/packages/loot-core/src/server/rules/types/handlers.ts @@ -1,10 +1,10 @@ // @ts-strict-ignore +import { type Action } from '..'; import { type RuleEntity, type TransactionEntity, type RuleActionEntity, } from '../../../types/models'; -import { type Action } from '../../accounts/rules'; type ValidationError = { conditionErrors: string[]; diff --git a/packages/loot-core/src/server/schedules/app.test.ts b/packages/loot-core/src/server/schedules/app.test.ts index 2aa6e47c1b1..a46ea12de06 100644 --- a/packages/loot-core/src/server/schedules/app.test.ts +++ b/packages/loot-core/src/server/schedules/app.test.ts @@ -3,9 +3,9 @@ import MockDate from 'mockdate'; import { q } from '../../shared/query'; import { getNextDate } from '../../shared/schedules'; -import { loadRules, updateRule } from '../accounts/transaction-rules'; import { runQuery as aqlQuery } from '../aql'; import { loadMappings } from '../db/mappings'; +import { loadRules, updateRule } from '../transactions/transaction-rules'; import { updateConditions, diff --git a/packages/loot-core/src/server/schedules/app.ts b/packages/loot-core/src/server/schedules/app.ts index 7016d6f627a..06c8e64999f 100644 --- a/packages/loot-core/src/server/schedules/app.ts +++ b/packages/loot-core/src/server/schedules/app.ts @@ -16,21 +16,21 @@ import { getStatus, recurConfigToRSchedule, } from '../../shared/schedules'; -import { Rule } from '../accounts/rules'; import { addTransactions } from '../accounts/sync'; -import { - getRules, - insertRule, - ruleModel, - updateRule, -} from '../accounts/transaction-rules'; import { createApp } from '../app'; import { runQuery as aqlQuery } from '../aql'; import * as db from '../db'; import { toDateRepr } from '../models'; import { mutator, runMutator } from '../mutators'; import * as prefs from '../prefs'; +import { Rule } from '../rules'; import { addSyncListener, batchMessages } from '../sync'; +import { + getRules, + insertRule, + ruleModel, + updateRule, +} from '../transactions/transaction-rules'; import { undoable } from '../undo'; import { Schedule as RSchedule } from '../util/rschedule'; diff --git a/packages/loot-core/src/server/schedules/find-schedules.ts b/packages/loot-core/src/server/schedules/find-schedules.ts index 8b45406138b..8d6df7354d0 100644 --- a/packages/loot-core/src/server/schedules/find-schedules.ts +++ b/packages/loot-core/src/server/schedules/find-schedules.ts @@ -7,10 +7,10 @@ import { q } from '../../shared/query'; import { getApproxNumberThreshold } from '../../shared/rules'; import { recurConfigToRSchedule } from '../../shared/schedules'; import { groupBy } from '../../shared/util'; -import { conditionsToAQL } from '../accounts/transaction-rules'; import { runQuery as aqlQuery } from '../aql'; import * as db from '../db'; import { fromDateRepr } from '../models'; +import { conditionsToAQL } from '../transactions/transaction-rules'; import { Schedule as RSchedule } from '../util/rschedule'; import { SchedulesHandlers } from './types/handlers'; diff --git a/packages/loot-core/src/server/sync/index.ts b/packages/loot-core/src/server/sync/index.ts index 7c4b03adb58..5888fb768a8 100644 --- a/packages/loot-core/src/server/sync/index.ts +++ b/packages/loot-core/src/server/sync/index.ts @@ -198,7 +198,7 @@ async function compareMessages(messages: Message[]): Promise { const { dataset, row, column, timestamp } = message; const timestampStr = timestamp.toString(); - const res = db.runQuery( + const res = db.runQuery>( db.cache( 'SELECT timestamp FROM messages_crdt WHERE dataset = ? AND row = ? AND column = ? AND timestamp >= ?', ), diff --git a/packages/loot-core/src/server/sync/migrate.test.ts b/packages/loot-core/src/server/sync/migrate.test.ts index 6959766b23f..7fec886fcdf 100644 --- a/packages/loot-core/src/server/sync/migrate.test.ts +++ b/packages/loot-core/src/server/sync/migrate.test.ts @@ -79,7 +79,11 @@ describe('sync migrations', () => { tracer.expectNow('applied', ['trans1/child1']); await tracer.expectWait('applied', ['trans1/child1']); - const transactions = db.runQuery('SELECT * FROM transactions', [], true); + const transactions = db.runQuery( + 'SELECT * FROM transactions', + [], + true, + ); expect(transactions.length).toBe(1); expect(transactions[0].parent_id).toBe('trans1'); diff --git a/packages/loot-core/src/server/tools/app.ts b/packages/loot-core/src/server/tools/app.ts index bc6b0321c34..60d23277309 100644 --- a/packages/loot-core/src/server/tools/app.ts +++ b/packages/loot-core/src/server/tools/app.ts @@ -1,10 +1,10 @@ // @ts-strict-ignore import { q } from '../../shared/query'; -import { batchUpdateTransactions } from '../accounts/transactions'; import { createApp } from '../app'; import { runQuery } from '../aql'; import * as db from '../db'; import { runMutator } from '../mutators'; +import { batchUpdateTransactions } from '../transactions'; import { ToolsHandlers } from './types/handlers'; @@ -52,7 +52,7 @@ app.method('tools/fix-split-transactions', async () => { `); await runMutator(async () => { - const updated = deletedRows.map(row => ({ id: row.id, tombstone: 1 })); + const updated = deletedRows.map(row => ({ id: row.id, tombstone: true })); await batchUpdateTransactions({ updated }); }); diff --git a/packages/loot-core/src/server/accounts/__snapshots__/transaction-rules.test.ts.snap b/packages/loot-core/src/server/transactions/__snapshots__/transaction-rules.test.ts.snap similarity index 100% rename from packages/loot-core/src/server/accounts/__snapshots__/transaction-rules.test.ts.snap rename to packages/loot-core/src/server/transactions/__snapshots__/transaction-rules.test.ts.snap diff --git a/packages/loot-core/src/server/accounts/__snapshots__/transfer.test.ts.snap b/packages/loot-core/src/server/transactions/__snapshots__/transfer.test.ts.snap similarity index 94% rename from packages/loot-core/src/server/accounts/__snapshots__/transfer.test.ts.snap rename to packages/loot-core/src/server/transactions/__snapshots__/transfer.test.ts.snap index 6890d6ba26f..815c8e77b5a 100644 --- a/packages/loot-core/src/server/accounts/__snapshots__/transfer.test.ts.snap +++ b/packages/loot-core/src/server/transactions/__snapshots__/transfer.test.ts.snap @@ -18,6 +18,7 @@ Array [ "parent_id": null, "payee": "id3", "payee_name": "", + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -41,6 +42,7 @@ Array [ "parent_id": null, "payee": "id2", "payee_name": "", + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -68,7 +70,7 @@ exports[`Transfer split transfers are retained on child transactions 2`] = ` \\"parent_id\\": null, \\"payee\\": \\"id3\\", \\"payee_name\\": \\"\\", - \\"reconciled\\": 0," + \\"raw_synced_data\\": null," `; exports[`Transfer split transfers are retained on child transactions 3`] = ` @@ -76,7 +78,7 @@ exports[`Transfer split transfers are retained on child transactions 3`] = ` - First value + Second value -@@ -21,10 +21,56 @@ +@@ -22,10 +22,58 @@ \\"starting_balance_flag\\": 0, \\"tombstone\\": 0, \\"transfer_id\\": \\"id6\\", @@ -97,6 +99,7 @@ exports[`Transfer split transfers are retained on child transactions 3`] = ` + \\"parent_id\\": \\"id5\\", + \\"payee\\": \\"id2\\", + \\"payee_name\\": \\"\\", ++ \\"raw_synced_data\\": null, + \\"reconciled\\": 0, + \\"schedule\\": null, + \\"sort_order\\": 123456789, @@ -120,6 +123,7 @@ exports[`Transfer split transfers are retained on child transactions 3`] = ` + \\"parent_id\\": null, + \\"payee\\": \\"id2\\", + \\"payee_name\\": \\"\\", ++ \\"raw_synced_data\\": null, + \\"reconciled\\": 0, + \\"schedule\\": null, + \\"sort_order\\": 123456789, @@ -153,6 +157,7 @@ Array [ "parent_id": null, "payee": "id5", "payee_name": "Non-transfer", + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -168,7 +173,7 @@ exports[`Transfer transfers are properly de-categorized 2`] = ` - First value + Second value -@@ -9,17 +9,40 @@ +@@ -9,18 +9,42 @@ \\"id\\": \\"id6\\", \\"imported_id\\": null, \\"imported_payee\\": null, @@ -176,14 +181,18 @@ exports[`Transfer transfers are properly de-categorized 2`] = ` \\"is_parent\\": 0, - \\"notes\\": null, + \\"notes\\": \\"hi\\", -+ \\"parent_id\\": null, + \\"parent_id\\": null, +- \\"payee\\": \\"id5\\", +- \\"payee_name\\": \\"Non-transfer\\", + \\"payee\\": \\"id4\\", + \\"payee_name\\": \\"\\", -+ \\"reconciled\\": 0, -+ \\"schedule\\": null, -+ \\"sort_order\\": 123456789, -+ \\"starting_balance_flag\\": 0, -+ \\"tombstone\\": 0, + \\"raw_synced_data\\": null, + \\"reconciled\\": 0, + \\"schedule\\": null, + \\"sort_order\\": 123456789, + \\"starting_balance_flag\\": 0, + \\"tombstone\\": 0, +- \\"transfer_id\\": null, + \\"transfer_id\\": \\"id7\\", + }, + Object { @@ -199,17 +208,15 @@ exports[`Transfer transfers are properly de-categorized 2`] = ` + \\"is_child\\": 0, + \\"is_parent\\": 0, + \\"notes\\": \\"hi\\", - \\"parent_id\\": null, -- \\"payee\\": \\"id5\\", -- \\"payee_name\\": \\"Non-transfer\\", ++ \\"parent_id\\": null, + \\"payee\\": \\"id2\\", + \\"payee_name\\": \\"\\", - \\"reconciled\\": 0, - \\"schedule\\": null, - \\"sort_order\\": 123456789, - \\"starting_balance_flag\\": 0, - \\"tombstone\\": 0, -- \\"transfer_id\\": null, ++ \\"raw_synced_data\\": null, ++ \\"reconciled\\": 0, ++ \\"schedule\\": null, ++ \\"sort_order\\": 123456789, ++ \\"starting_balance_flag\\": 0, ++ \\"tombstone\\": 0, + \\"transfer_id\\": \\"id6\\", }, ]" @@ -220,7 +227,7 @@ exports[`Transfer transfers are properly de-categorized 3`] = ` - First value + Second value -@@ -1,31 +1,31 @@ +@@ -1,32 +1,32 @@ Array [ Object { \\"account\\": \\"one\\", @@ -240,6 +247,7 @@ exports[`Transfer transfers are properly de-categorized 3`] = ` - \\"payee\\": \\"id4\\", + \\"payee\\": \\"id3\\", \\"payee_name\\": \\"\\", + \\"raw_synced_data\\": null, \\"reconciled\\": 0, \\"schedule\\": null, \\"sort_order\\": 123456789, @@ -275,6 +283,7 @@ Array [ "parent_id": null, "payee": "id5", "payee_name": "Non-transfer", + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -290,7 +299,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 2`] = ` - First value + Second value -@@ -20,6 +20,52 @@ +@@ -21,6 +21,54 @@ \\"sort_order\\": 123456789, \\"starting_balance_flag\\": 0, \\"tombstone\\": 0, @@ -312,6 +321,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 2`] = ` + \\"parent_id\\": null, + \\"payee\\": \\"id3\\", + \\"payee_name\\": \\"\\", ++ \\"raw_synced_data\\": null, + \\"reconciled\\": 0, + \\"schedule\\": null, + \\"sort_order\\": 123456789, @@ -335,6 +345,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 2`] = ` + \\"parent_id\\": null, + \\"payee\\": \\"id2\\", + \\"payee_name\\": \\"\\", ++ \\"raw_synced_data\\": null, + \\"reconciled\\": 0, + \\"schedule\\": null, + \\"sort_order\\": 123456789, @@ -350,7 +361,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 3`] = ` - First value + Second value -@@ -2,70 +2,70 @@ +@@ -2,73 +2,73 @@ Object { \\"account\\": \\"one\\", \\"amount\\": 5000, @@ -372,6 +383,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 3`] = ` - \\"payee_name\\": \\"Non-transfer\\", + \\"payee\\": \\"id3\\", + \\"payee_name\\": \\"\\", + \\"raw_synced_data\\": null, \\"reconciled\\": 0, \\"schedule\\": null, \\"sort_order\\": 123456789, @@ -403,6 +415,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 3`] = ` - \\"payee\\": \\"id3\\", + \\"payee\\": \\"id2\\", \\"payee_name\\": \\"\\", + \\"raw_synced_data\\": null, \\"reconciled\\": 0, \\"schedule\\": null, \\"sort_order\\": 123456789, @@ -433,6 +446,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 3`] = ` - \\"payee_name\\": \\"\\", + \\"payee\\": \\"id5\\", + \\"payee_name\\": \\"Non-transfer\\", + \\"raw_synced_data\\": null, \\"reconciled\\": 0, \\"schedule\\": null, \\"sort_order\\": 123456789, @@ -449,7 +463,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 4`] = ` - First value + Second value -@@ -11,21 +11,21 @@ +@@ -11,22 +11,22 @@ \\"imported_payee\\": null, \\"is_child\\": 0, \\"is_parent\\": 0, @@ -458,6 +472,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 4`] = ` - \\"payee\\": \\"id3\\", + \\"payee\\": \\"id4\\", \\"payee_name\\": \\"\\", + \\"raw_synced_data\\": null, \\"reconciled\\": 0, \\"schedule\\": null, \\"sort_order\\": 123456789, @@ -480,7 +495,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 5`] = ` - First value + Second value -@@ -11,41 +11,18 @@ +@@ -11,43 +11,19 @@ \\"imported_payee\\": null, \\"is_child\\": 0, \\"is_parent\\": 0, @@ -488,11 +503,14 @@ exports[`Transfer transfers are properly inserted/updated/deleted 5`] = ` \\"parent_id\\": null, - \\"payee\\": \\"id4\\", - \\"payee_name\\": \\"\\", -- \\"reconciled\\": 0, -- \\"schedule\\": null, -- \\"sort_order\\": 123456789, -- \\"starting_balance_flag\\": 0, -- \\"tombstone\\": 0, ++ \\"payee\\": \\"id9\\", ++ \\"payee_name\\": \\"Not transferred anymore\\", + \\"raw_synced_data\\": null, + \\"reconciled\\": 0, + \\"schedule\\": null, + \\"sort_order\\": 123456789, + \\"starting_balance_flag\\": 0, + \\"tombstone\\": 0, - \\"transfer_id\\": \\"id8\\", - }, - Object { @@ -511,13 +529,12 @@ exports[`Transfer transfers are properly inserted/updated/deleted 5`] = ` - \\"parent_id\\": null, - \\"payee\\": \\"id2\\", - \\"payee_name\\": \\"\\", -+ \\"payee\\": \\"id9\\", -+ \\"payee_name\\": \\"Not transferred anymore\\", - \\"reconciled\\": 0, - \\"schedule\\": null, - \\"sort_order\\": 123456789, - \\"starting_balance_flag\\": 0, - \\"tombstone\\": 0, +- \\"raw_synced_data\\": null, +- \\"reconciled\\": 0, +- \\"schedule\\": null, +- \\"sort_order\\": 123456789, +- \\"starting_balance_flag\\": 0, +- \\"tombstone\\": 0, - \\"transfer_id\\": \\"id7\\", + \\"transfer_id\\": null, }, @@ -532,7 +549,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 6`] = ` - First value + Second value -@@ -11,18 +11,41 @@ +@@ -11,19 +11,43 @@ \\"imported_payee\\": null, \\"is_child\\": 0, \\"is_parent\\": 0, @@ -542,11 +559,13 @@ exports[`Transfer transfers are properly inserted/updated/deleted 6`] = ` - \\"payee_name\\": \\"Not transferred anymore\\", + \\"payee\\": \\"id3\\", + \\"payee_name\\": \\"\\", -+ \\"reconciled\\": 0, -+ \\"schedule\\": null, -+ \\"sort_order\\": 123456789, -+ \\"starting_balance_flag\\": 0, -+ \\"tombstone\\": 0, + \\"raw_synced_data\\": null, + \\"reconciled\\": 0, + \\"schedule\\": null, + \\"sort_order\\": 123456789, + \\"starting_balance_flag\\": 0, + \\"tombstone\\": 0, +- \\"transfer_id\\": null, + \\"transfer_id\\": \\"id10\\", + }, + Object { @@ -565,12 +584,12 @@ exports[`Transfer transfers are properly inserted/updated/deleted 6`] = ` + \\"parent_id\\": null, + \\"payee\\": \\"id2\\", + \\"payee_name\\": \\"\\", - \\"reconciled\\": 0, - \\"schedule\\": null, - \\"sort_order\\": 123456789, - \\"starting_balance_flag\\": 0, - \\"tombstone\\": 0, -- \\"transfer_id\\": null, ++ \\"raw_synced_data\\": null, ++ \\"reconciled\\": 0, ++ \\"schedule\\": null, ++ \\"sort_order\\": 123456789, ++ \\"starting_balance_flag\\": 0, ++ \\"tombstone\\": 0, + \\"transfer_id\\": \\"id7\\", }, Object { @@ -584,7 +603,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 7`] = ` - First value + Second value -@@ -2,56 +2,10 @@ +@@ -2,58 +2,10 @@ Object { \\"account\\": \\"one\\", \\"amount\\": 5000, @@ -601,6 +620,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 7`] = ` - \\"parent_id\\": null, - \\"payee\\": \\"id3\\", - \\"payee_name\\": \\"\\", +- \\"raw_synced_data\\": null, - \\"reconciled\\": 0, - \\"schedule\\": null, - \\"sort_order\\": 123456789, @@ -624,6 +644,7 @@ exports[`Transfer transfers are properly inserted/updated/deleted 7`] = ` - \\"parent_id\\": null, - \\"payee\\": \\"id2\\", - \\"payee_name\\": \\"\\", +- \\"raw_synced_data\\": null, - \\"reconciled\\": 0, - \\"schedule\\": null, - \\"sort_order\\": 123456789, diff --git a/packages/loot-core/src/server/transactions/app.ts b/packages/loot-core/src/server/transactions/app.ts new file mode 100644 index 00000000000..d762be8f1ce --- /dev/null +++ b/packages/loot-core/src/server/transactions/app.ts @@ -0,0 +1,116 @@ +import { q, Query, QueryState } from '../../shared/query'; +import { + AccountEntity, + CategoryGroupEntity, + PayeeEntity, + TransactionEntity, +} from '../../types/models'; +import { createApp } from '../app'; +import { runQuery } from '../aql'; +import { mutator } from '../mutators'; +import { undoable } from '../undo'; + +import { exportQueryToCSV, exportToCSV } from './export/export-to-csv'; +import { parseFile, ParseFileOptions } from './import/parse-file'; + +import { batchUpdateTransactions } from '.'; + +export type TransactionHandlers = { + 'transactions-batch-update': typeof handleBatchUpdateTransactions; + 'transaction-add': typeof addTransaction; + 'transaction-update': typeof updateTransaction; + 'transaction-delete': typeof deleteTransaction; + 'transactions-parse-file': typeof parseTransactionsFile; + 'transactions-export': typeof exportTransactions; + 'transactions-export-query': typeof exportTransactionsQuery; + 'get-earliest-transaction': typeof getEarliestTransaction; +}; + +async function handleBatchUpdateTransactions({ + added, + deleted, + updated, + learnCategories, +}: Parameters[0]) { + const result = await batchUpdateTransactions({ + added, + updated, + deleted, + learnCategories, + }); + + return result; +} + +async function addTransaction(transaction: TransactionEntity) { + await handleBatchUpdateTransactions({ added: [transaction] }); + return {}; +} + +async function updateTransaction(transaction: TransactionEntity) { + await handleBatchUpdateTransactions({ updated: [transaction] }); + return {}; +} + +async function deleteTransaction(transaction: Pick) { + await handleBatchUpdateTransactions({ deleted: [transaction] }); + return {}; +} + +async function parseTransactionsFile({ + filepath, + options, +}: { + filepath: string; + options: ParseFileOptions; +}) { + return parseFile(filepath, options); +} + +async function exportTransactions({ + transactions, + accounts, + categoryGroups, + payees, +}: { + transactions: TransactionEntity[]; + accounts: AccountEntity[]; + categoryGroups: CategoryGroupEntity[]; + payees: PayeeEntity[]; +}) { + return exportToCSV(transactions, accounts, categoryGroups, payees); +} + +async function exportTransactionsQuery({ + query: queryState, +}: { + query: QueryState; +}) { + return exportQueryToCSV(new Query(queryState)); +} + +async function getEarliestTransaction() { + const { data } = await runQuery( + q('transactions') + .options({ splits: 'none' }) + .orderBy({ date: 'asc' }) + .select('*') + .limit(1), + ); + return data[0] || null; +} + +export const app = createApp(); + +app.method( + 'transactions-batch-update', + mutator(undoable(handleBatchUpdateTransactions)), +); + +app.method('transaction-add', mutator(addTransaction)); +app.method('transaction-update', mutator(updateTransaction)); +app.method('transaction-delete', mutator(deleteTransaction)); +app.method('transactions-parse-file', mutator(parseTransactionsFile)); +app.method('transactions-export', mutator(exportTransactions)); +app.method('transactions-export-query', mutator(exportTransactionsQuery)); +app.method('get-earliest-transaction', getEarliestTransaction); diff --git a/packages/loot-core/src/server/accounts/export-to-csv.ts b/packages/loot-core/src/server/transactions/export/export-to-csv.ts similarity index 97% rename from packages/loot-core/src/server/accounts/export-to-csv.ts rename to packages/loot-core/src/server/transactions/export/export-to-csv.ts index 7b8dda5af4a..7035069fd18 100644 --- a/packages/loot-core/src/server/accounts/export-to-csv.ts +++ b/packages/loot-core/src/server/transactions/export/export-to-csv.ts @@ -1,8 +1,8 @@ // @ts-strict-ignore import csvStringify from 'csv-stringify/lib/sync'; -import { integerToAmount } from '../../shared/util'; -import { runQuery as aqlQuery } from '../aql'; +import { integerToAmount } from '../../../shared/util'; +import { runQuery as aqlQuery } from '../../aql'; export async function exportToCSV( transactions, diff --git a/packages/loot-core/src/server/accounts/__snapshots__/parse-file.test.ts.snap b/packages/loot-core/src/server/transactions/import/__snapshots__/parse-file.test.ts.snap similarity index 95% rename from packages/loot-core/src/server/accounts/__snapshots__/parse-file.test.ts.snap rename to packages/loot-core/src/server/transactions/import/__snapshots__/parse-file.test.ts.snap index 316320ba970..0b432f716c9 100644 --- a/packages/loot-core/src/server/accounts/__snapshots__/parse-file.test.ts.snap +++ b/packages/loot-core/src/server/transactions/import/__snapshots__/parse-file.test.ts.snap @@ -21,6 +21,7 @@ Array [ GENODEF1PFK ABWE: Testkonto 1", "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -48,6 +49,7 @@ Array [ GENODEF1PFK ABWE: Testkonto 1", "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456788, @@ -73,6 +75,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456787, @@ -101,6 +104,7 @@ Array [ GENODEF1PFK ABWE: Testkonto", "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456786, @@ -126,6 +130,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456785, @@ -151,6 +156,7 @@ Array [ "notes": "Lastschrift 2. Zahlung TAN:747216 ", "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456784, @@ -176,6 +182,7 @@ Array [ "notes": "Lastschrift 1. Zahlung TAN:747216 ", "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456783, @@ -206,6 +213,7 @@ Array [ "notes": "PREAUTHORIZED DEBIT;B.C. HYDRO & POWER AUTHORITY;Electronic Funds Transfer", "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -231,6 +239,7 @@ Array [ "notes": "PREAUTHORIZED DEBIT;LUXMORE REALTY PPTY MGMT;Electronic Funds Transfer", "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456788, @@ -261,6 +270,7 @@ Array [ "notes": "PWW", "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -291,6 +301,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -321,6 +332,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -346,6 +358,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456788, @@ -371,6 +384,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456787, @@ -396,6 +410,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456786, @@ -421,6 +436,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456785, @@ -446,6 +462,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456784, @@ -471,6 +488,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456783, @@ -496,6 +514,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456782, @@ -521,6 +540,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456781, @@ -546,6 +566,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456780, @@ -576,6 +597,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -601,6 +623,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456788, @@ -626,6 +649,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456787, @@ -651,6 +675,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456786, @@ -676,6 +701,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456785, @@ -701,6 +727,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456784, @@ -726,6 +753,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456783, @@ -751,6 +779,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456782, @@ -776,6 +805,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456781, @@ -801,6 +831,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456780, @@ -831,6 +862,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456789, @@ -856,6 +888,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456788, @@ -881,6 +914,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456787, @@ -906,6 +940,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456786, @@ -931,6 +966,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456785, @@ -956,6 +992,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456784, @@ -981,6 +1018,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456783, @@ -1006,6 +1044,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456782, @@ -1031,6 +1070,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456781, @@ -1056,6 +1096,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456780, @@ -1081,6 +1122,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456779, @@ -1106,6 +1148,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456778, @@ -1131,6 +1174,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456777, @@ -1156,6 +1200,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456776, @@ -1181,6 +1226,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456775, @@ -1206,6 +1252,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456774, @@ -1231,6 +1278,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456773, @@ -1256,6 +1304,7 @@ Array [ "notes": null, "parent_id": null, "pending": 0, + "raw_synced_data": null, "reconciled": 0, "schedule": null, "sort_order": 123456772, diff --git a/packages/loot-core/src/server/accounts/ofx2json.ts b/packages/loot-core/src/server/transactions/import/ofx2json.ts similarity index 98% rename from packages/loot-core/src/server/accounts/ofx2json.ts rename to packages/loot-core/src/server/transactions/import/ofx2json.ts index b5ad38faa88..91fc8abeb55 100644 --- a/packages/loot-core/src/server/accounts/ofx2json.ts +++ b/packages/loot-core/src/server/transactions/import/ofx2json.ts @@ -1,7 +1,7 @@ // @ts-strict-ignore import { parseStringPromise } from 'xml2js'; -import { dayFromDate } from '../../shared/months'; +import { dayFromDate } from '../../../shared/months'; type OFXTransaction = { amount: string; diff --git a/packages/loot-core/src/server/accounts/parse-file.test.ts b/packages/loot-core/src/server/transactions/import/parse-file.test.ts similarity index 85% rename from packages/loot-core/src/server/accounts/parse-file.test.ts rename to packages/loot-core/src/server/transactions/import/parse-file.test.ts index 04cfc9f0892..9fbfff4df89 100644 --- a/packages/loot-core/src/server/accounts/parse-file.test.ts +++ b/packages/loot-core/src/server/transactions/import/parse-file.test.ts @@ -1,12 +1,12 @@ // @ts-strict-ignore import * as d from 'date-fns'; -import { amountToInteger } from '../../shared/util'; -import * as db from '../db'; -import * as prefs from '../prefs'; +import { amountToInteger } from '../../../shared/util'; +import { reconcileTransactions } from '../../accounts/sync'; +import * as db from '../../db'; +import * as prefs from '../../prefs'; import { parseFile } from './parse-file'; -import { reconcileTransactions } from './sync'; beforeEach(global.emptyDatabase()); @@ -65,7 +65,7 @@ describe('File import', () => { await db.insertAccount({ id: 'one', name: 'one' }); const { errors } = await importFileWithRealTime( 'one', - __dirname + '/../../mocks/files/data.qif', + __dirname + '/../../../mocks/files/data.qif', 'MM/dd/yy', ); expect(errors.length).toBe(0); @@ -78,7 +78,7 @@ describe('File import', () => { const { errors } = await importFileWithRealTime( 'one', - __dirname + '/../../mocks/files/data.ofx', + __dirname + '/../../../mocks/files/data.ofx', ); expect(errors.length).toBe(0); expect(await getTransactions('one')).toMatchSnapshot(); @@ -90,7 +90,7 @@ describe('File import', () => { const { errors } = await importFileWithRealTime( 'one', - __dirname + '/../../mocks/files/credit-card.ofx', + __dirname + '/../../../mocks/files/credit-card.ofx', ); expect(errors.length).toBe(0); expect(await getTransactions('one')).toMatchSnapshot(); @@ -102,7 +102,7 @@ describe('File import', () => { const { errors } = await importFileWithRealTime( 'one', - __dirname + '/../../mocks/files/data.qfx', + __dirname + '/../../../mocks/files/data.qfx', ); expect(errors.length).toBe(0); expect(await getTransactions('one')).toMatchSnapshot(); @@ -114,13 +114,13 @@ describe('File import', () => { let res = await importFileWithRealTime( 'one', - __dirname + '/../../mocks/files/best.data-ever$.QFX', + __dirname + '/../../../mocks/files/best.data-ever$.QFX', ); expect(res.errors.length).toBe(0); res = await importFileWithRealTime( 'one', - __dirname + '/../../mocks/files/big.data.QiF', + __dirname + '/../../../mocks/files/big.data.QiF', 'MM/dd/yy', ); expect(res.errors.length).toBe(0); @@ -136,7 +136,7 @@ describe('File import', () => { const { errors } = await importFileWithRealTime( 'one', - __dirname + '/../../mocks/files/8859-1.qfx', + __dirname + '/../../../mocks/files/8859-1.qfx', 'yyyy-MM-dd', ); expect(errors.length).toBe(0); @@ -149,7 +149,7 @@ describe('File import', () => { const { errors } = await importFileWithRealTime( 'one', - __dirname + '/../../mocks/files/html-vals.qfx', + __dirname + '/../../../mocks/files/html-vals.qfx', 'yyyy-MM-dd', ); expect(errors.length).toBe(0); @@ -162,7 +162,7 @@ describe('File import', () => { const { errors } = await importFileWithRealTime( 'one', - __dirname + '/../../mocks/files/camt/camt.053.xml', + __dirname + '/../../../mocks/files/camt/camt.053.xml', ); expect(errors.length).toBe(0); expect(await getTransactions('one')).toMatchSnapshot(); diff --git a/packages/loot-core/src/server/accounts/parse-file.ts b/packages/loot-core/src/server/transactions/import/parse-file.ts similarity index 96% rename from packages/loot-core/src/server/accounts/parse-file.ts rename to packages/loot-core/src/server/transactions/import/parse-file.ts index fd6178411cd..f551306018c 100644 --- a/packages/loot-core/src/server/accounts/parse-file.ts +++ b/packages/loot-core/src/server/transactions/import/parse-file.ts @@ -1,8 +1,8 @@ // @ts-strict-ignore import csv2json from 'csv-parse/lib/sync'; -import * as fs from '../../platform/server/fs'; -import { looselyParseAmount } from '../../shared/util'; +import * as fs from '../../../platform/server/fs'; +import { looselyParseAmount } from '../../../shared/util'; import { ofx2json } from './ofx2json'; import { qif2json } from './qif2json'; @@ -14,7 +14,7 @@ export type ParseFileResult = { transactions?: unknown[]; }; -type ParseFileOptions = { +export type ParseFileOptions = { hasHeaderRow?: boolean; delimiter?: string; fallbackMissingPayeeToMemo?: boolean; diff --git a/packages/loot-core/src/server/accounts/qif2json.ts b/packages/loot-core/src/server/transactions/import/qif2json.ts similarity index 100% rename from packages/loot-core/src/server/accounts/qif2json.ts rename to packages/loot-core/src/server/transactions/import/qif2json.ts diff --git a/packages/loot-core/src/server/accounts/xmlcamt2json.ts b/packages/loot-core/src/server/transactions/import/xmlcamt2json.ts similarity index 100% rename from packages/loot-core/src/server/accounts/xmlcamt2json.ts rename to packages/loot-core/src/server/transactions/import/xmlcamt2json.ts diff --git a/packages/loot-core/src/server/accounts/transactions.ts b/packages/loot-core/src/server/transactions/index.ts similarity index 99% rename from packages/loot-core/src/server/accounts/transactions.ts rename to packages/loot-core/src/server/transactions/index.ts index cacc9dbd4b3..e306d749131 100644 --- a/packages/loot-core/src/server/accounts/transactions.ts +++ b/packages/loot-core/src/server/transactions/index.ts @@ -1,4 +1,5 @@ // @ts-strict-ignore + import * as connection from '../../platform/server/connection'; import { Diff } from '../../shared/util'; import { PayeeEntity, TransactionEntity } from '../../types/models'; diff --git a/packages/loot-core/src/server/accounts/transaction-rules.test.ts b/packages/loot-core/src/server/transactions/transaction-rules.test.ts similarity index 100% rename from packages/loot-core/src/server/accounts/transaction-rules.test.ts rename to packages/loot-core/src/server/transactions/transaction-rules.test.ts diff --git a/packages/loot-core/src/server/accounts/transaction-rules.ts b/packages/loot-core/src/server/transactions/transaction-rules.ts similarity index 99% rename from packages/loot-core/src/server/accounts/transaction-rules.ts rename to packages/loot-core/src/server/transactions/transaction-rules.ts index 4e644f59524..40448d3ac40 100644 --- a/packages/loot-core/src/server/accounts/transaction-rules.ts +++ b/packages/loot-core/src/server/transactions/transaction-rules.ts @@ -1,4 +1,5 @@ // @ts-strict-ignore + import { currentDay, addDays, @@ -21,8 +22,6 @@ import { getPayee, getPayeeByName, insertPayee, getAccount } from '../db'; import { getMappings } from '../db/mappings'; import { RuleError } from '../errors'; import { requiredFields, toDateRepr } from '../models'; -import { batchMessages, addSyncListener } from '../sync'; - import { Condition, Action, @@ -32,15 +31,17 @@ import { migrateIds, iterateIds, execActions, -} from './rules'; -import { batchUpdateTransactions } from './transactions'; +} from '../rules'; +import { batchMessages, addSyncListener } from '../sync'; + +import { batchUpdateTransactions } from '.'; // TODO: Detect if it looks like the user is creating a rename rule // and prompt to create it in the pre phase instead // * We could also make the "create rule" button a dropdown that // provides different "templates" like "create renaming rule" -export { iterateIds } from './rules'; +export { iterateIds } from '../rules'; let allRules; let unlistenSync; diff --git a/packages/loot-core/src/server/accounts/transfer.test.ts b/packages/loot-core/src/server/transactions/transfer.test.ts similarity index 100% rename from packages/loot-core/src/server/accounts/transfer.test.ts rename to packages/loot-core/src/server/transactions/transfer.test.ts diff --git a/packages/loot-core/src/server/accounts/transfer.ts b/packages/loot-core/src/server/transactions/transfer.ts similarity index 100% rename from packages/loot-core/src/server/accounts/transfer.ts rename to packages/loot-core/src/server/transactions/transfer.ts diff --git a/packages/loot-core/src/server/util/custom-sync-mapping.ts b/packages/loot-core/src/server/util/custom-sync-mapping.ts new file mode 100644 index 00000000000..ddf9311b71d --- /dev/null +++ b/packages/loot-core/src/server/util/custom-sync-mapping.ts @@ -0,0 +1,48 @@ +export type Mappings = Map>; + +export const mappingsToString = (mapping: Mappings): string => + JSON.stringify( + Object.fromEntries( + [...mapping.entries()].map(([key, value]) => [ + key, + Object.fromEntries(value), + ]), + ), + ); + +export const mappingsFromString = (str: string): Mappings => { + try { + const parsed = JSON.parse(str); + if (typeof parsed !== 'object' || parsed === null) { + throw new Error('Invalid mapping format'); + } + return new Map( + Object.entries(parsed).map(([key, value]) => [ + key, + new Map(Object.entries(value as object)), + ]), + ); + } catch (e) { + const message = e instanceof Error ? e.message : e; + throw new Error(`Failed to parse mapping: ${message}`); + } +}; + +export const defaultMappings: Mappings = new Map([ + [ + 'payment', + new Map([ + ['date', 'date'], + ['payee', 'payeeName'], + ['notes', 'notes'], + ]), + ], + [ + 'deposit', + new Map([ + ['date', 'date'], + ['payee', 'payeeName'], + ['notes', 'notes'], + ]), + ], +]); diff --git a/packages/loot-core/src/shared/schedules.ts b/packages/loot-core/src/shared/schedules.ts index 584f66fe4f5..d41370c2f28 100644 --- a/packages/loot-core/src/shared/schedules.ts +++ b/packages/loot-core/src/shared/schedules.ts @@ -2,7 +2,7 @@ import type { IRuleOptions } from '@rschedule/core'; import * as d from 'date-fns'; -import { Condition } from '../server/accounts/rules'; +import { Condition } from '../server/rules'; import * as monthUtils from './months'; import { q } from './query'; diff --git a/packages/loot-core/src/shared/util.ts b/packages/loot-core/src/shared/util.ts index 9c603384b57..9ce69619c28 100644 --- a/packages/loot-core/src/shared/util.ts +++ b/packages/loot-core/src/shared/util.ts @@ -210,7 +210,7 @@ export function appendDecimals( amountText: string, hideDecimals = false, ): string { - const { separator } = getNumberFormat(); + const { decimalSeparator: separator } = getNumberFormat(); let result = amountText; if (result.slice(-1) === separator) { result = result.slice(0, -1); @@ -283,47 +283,44 @@ export function getNumberFormat({ format?: NumberFormats; hideFraction: boolean; } = numberFormatConfig) { - let locale, regex, separator, separatorRegex; + let locale, thousandsSeparator, decimalSeparator; switch (format) { case 'space-comma': locale = 'en-SE'; - regex = /[^-0-9,.]/g; - separator = ','; - separatorRegex = /[,.]/g; + thousandsSeparator = '\xa0'; + decimalSeparator = ','; break; case 'dot-comma': locale = 'de-DE'; - regex = /[^-0-9,]/g; - separator = ','; + thousandsSeparator = '.'; + decimalSeparator = ','; break; case 'apostrophe-dot': locale = 'de-CH'; - regex = /[^-0-9,.]/g; - separator = '.'; - separatorRegex = /[,.]/g; + thousandsSeparator = '’'; + decimalSeparator = '.'; break; case 'comma-dot-in': locale = 'en-IN'; - regex = /[^-0-9.]/g; - separator = '.'; + thousandsSeparator = ','; + decimalSeparator = '.'; break; case 'comma-dot': default: locale = 'en-US'; - regex = /[^-0-9.]/g; - separator = '.'; + thousandsSeparator = ','; + decimalSeparator = '.'; } return { value: format, - separator, + thousandsSeparator, + decimalSeparator, formatter: new Intl.NumberFormat(locale, { minimumFractionDigits: hideFraction ? 0 : 2, maximumFractionDigits: hideFraction ? 0 : 2, }), - regex, - separatorRegex, }; } @@ -390,23 +387,25 @@ export function amountToCurrencyNoDecimal(amount: Amount): CurrencyAmount { }).formatter.format(amount); } -export function currencyToAmount( - currencyAmount: CurrencyAmount, -): Amount | null { - let amount; - if (getNumberFormat().separatorRegex) { - amount = parseFloat( - currencyAmount - .replace(getNumberFormat().regex, '') - .replace(getNumberFormat().separatorRegex, '.'), - ); +export function currencyToAmount(currencyAmount: string): Amount | null { + let integer, fraction; + + // match the last dot or comma in the string + const match = currencyAmount.match(/[,.](?=[^.,]*$)/); + + if ( + !match || + (match[0] === getNumberFormat().thousandsSeparator && + match.index + 4 === currencyAmount.length) + ) { + fraction = null; + integer = currencyAmount.replace(/\D/g, ''); } else { - amount = parseFloat( - currencyAmount - .replace(getNumberFormat().regex, '') - .replace(getNumberFormat().separator, '.'), - ); + integer = currencyAmount.slice(0, match.index).replace(/\D/g, ''); + fraction = currencyAmount.slice(match.index + 1); } + + const amount = parseFloat(integer + '.' + fraction); return isNaN(amount) ? null : amount; } diff --git a/packages/loot-core/src/types/api-handlers.d.ts b/packages/loot-core/src/types/api-handlers.d.ts index 37ca5336689..1d31dc097fb 100644 --- a/packages/loot-core/src/types/api-handlers.d.ts +++ b/packages/loot-core/src/types/api-handlers.d.ts @@ -1,6 +1,5 @@ import { ImportTransactionsOpts } from '@actual-app/api'; -import { type batchUpdateTransactions } from '../server/accounts/transactions'; import type { APIAccountEntity, APICategoryEntity, @@ -8,6 +7,7 @@ import type { APIFileEntity, APIPayeeEntity, } from '../server/api-models'; +import { type batchUpdateTransactions } from '../server/transactions'; import type { NewRuleEntity, RuleEntity, TransactionEntity } from './models'; import { type ServerHandlers } from './server-handlers'; @@ -76,6 +76,7 @@ export interface ApiHandlers { transactions; categoryGroups; payees; + accounts; }) => Promise; 'api/transactions-import': (arg: { diff --git a/packages/loot-core/src/types/handlers.d.ts b/packages/loot-core/src/types/handlers.d.ts index cf5b2c1bec8..5fc1d912a75 100644 --- a/packages/loot-core/src/types/handlers.d.ts +++ b/packages/loot-core/src/types/handlers.d.ts @@ -8,6 +8,7 @@ import type { ReportsHandlers } from '../server/reports/types/handlers'; import type { RulesHandlers } from '../server/rules/types/handlers'; import type { SchedulesHandlers } from '../server/schedules/types/handlers'; import type { ToolsHandlers } from '../server/tools/types/handlers'; +import type { TransactionHandlers } from '../server/transactions/app'; import type { ApiHandlers } from './api-handlers'; import type { ServerHandlers } from './server-handlers'; @@ -23,6 +24,7 @@ export interface Handlers ReportsHandlers, RulesHandlers, SchedulesHandlers, + TransactionHandlers, AdminHandlers, ToolsHandlers {} diff --git a/packages/loot-core/src/types/models/account.d.ts b/packages/loot-core/src/types/models/account.d.ts index cb668f6622f..74436bad581 100644 --- a/packages/loot-core/src/types/models/account.d.ts +++ b/packages/loot-core/src/types/models/account.d.ts @@ -10,12 +10,15 @@ export type AccountEntity = { type _SyncFields = { account_id: T extends true ? string : null; bank: T extends true ? string : null; + bankName: T extends true ? string : null; + bankId: T extends true ? number : null; mask: T extends true ? string : null; // end of bank account number official_name: T extends true ? string : null; balance_current: T extends true ? number : null; balance_available: T extends true ? number : null; balance_limit: T extends true ? number : null; account_sync_source: T extends true ? AccountSyncSource : null; + last_sync: T extends true ? string : null; }; export type AccountSyncSource = 'simpleFin' | 'goCardless'; diff --git a/packages/loot-core/src/types/models/bank-sync.d.ts b/packages/loot-core/src/types/models/bank-sync.d.ts index 279a23cfae7..f0ab8a3758c 100644 --- a/packages/loot-core/src/types/models/bank-sync.d.ts +++ b/packages/loot-core/src/types/models/bank-sync.d.ts @@ -19,3 +19,5 @@ export type BankSyncResponse = { error_type: string; error_code: string; }; + +export type BankSyncProviders = 'goCardless' | 'simpleFin'; diff --git a/packages/loot-core/src/types/models/transaction.d.ts b/packages/loot-core/src/types/models/transaction.d.ts index 7fc23821bef..60c99b673a2 100644 --- a/packages/loot-core/src/types/models/transaction.d.ts +++ b/packages/loot-core/src/types/models/transaction.d.ts @@ -32,4 +32,5 @@ export interface TransactionEntity { version: 1; difference: number; } | null; + raw_synced_data?: string | undefined; } diff --git a/packages/loot-core/src/types/prefs.d.ts b/packages/loot-core/src/types/prefs.d.ts index 6cf89ed4509..fad8fc946d3 100644 --- a/packages/loot-core/src/types/prefs.d.ts +++ b/packages/loot-core/src/types/prefs.d.ts @@ -1,5 +1,6 @@ export type FeatureFlag = | 'goalTemplatesEnabled' + | 'goalTemplatesUIEnabled' | 'actionTemplating' | 'contextMenus' | 'openidAuth'; @@ -28,6 +29,9 @@ export type SyncedPrefs = Partial< | `csv-in-out-mode-${string}` | `csv-out-value-${string}` | `csv-has-header-${string}` + | `custom-sync-mappings-${string}` + | `sync-import-pending-${string}` + | `sync-import-notes-${string}` | `ofx-fallback-missing-payee-${string}` | `flip-amount-${string}-${'csv' | 'qif'}` | `flags.${FeatureFlag}` @@ -73,6 +77,8 @@ export type LocalPrefs = Partial<{ export type Theme = 'light' | 'dark' | 'auto' | 'midnight' | 'development'; export type DarkTheme = 'dark' | 'midnight'; + +// GlobalPrefs are the parsed global-store.json values export type GlobalPrefs = Partial<{ floatingSidebar: boolean; maxMonths: number; @@ -84,4 +90,24 @@ export type GlobalPrefs = Partial<{ serverSelfSignedCert: string; // Electron only }>; +// GlobalPrefsJson represents what's saved in the global-store.json file +export type GlobalPrefsJson = Partial<{ + 'user-id'?: string; + 'user-key'?: string; + 'encrypt-keys'?: string; + lastBudget?: string; + readOnly?: string; + 'server-url'?: string; + 'did-bootstrap'?: boolean; + 'user-token'?: string; + 'floating-sidebar'?: string; // "true" or "false" + 'max-months'?: string; // e.g. "2" or "3" + 'document-dir'?: GlobalPrefs['documentDir']; + 'encrypt-key'?: string; + language?: GlobalPrefs['language']; + theme?: GlobalPrefs['theme']; + 'preferred-dark-theme'?: GlobalPrefs['preferredDarkTheme']; + 'server-self-signed-cert'?: GlobalPrefs['serverSelfSignedCert']; +}>; + export type AuthMethods = 'password' | 'openid'; diff --git a/packages/loot-core/src/types/server-handlers.d.ts b/packages/loot-core/src/types/server-handlers.d.ts index 69f30deceac..92dfdf5eda7 100644 --- a/packages/loot-core/src/types/server-handlers.d.ts +++ b/packages/loot-core/src/types/server-handlers.d.ts @@ -1,12 +1,9 @@ import { ImportTransactionsOpts } from '@actual-app/api'; -import { ParseFileResult } from '../server/accounts/parse-file'; -import { batchUpdateTransactions } from '../server/accounts/transactions'; import { Backup } from '../server/backups'; import { RemoteFile } from '../server/cloud-storage'; import { Node as SpreadsheetNode } from '../server/spreadsheet/spreadsheet'; import { Message } from '../server/sync'; -import { QueryState } from '../shared/query'; import { Budget } from './budget'; import { @@ -26,34 +23,9 @@ import { Query } from './query'; import { EmptyObject } from './util'; export interface ServerHandlers { - 'transaction-update': (transaction: { id: string }) => Promise; - undo: () => Promise; - redo: () => Promise; - 'transactions-batch-update': ( - ...arg: Parameters - ) => ReturnType; - - 'transaction-add': (transaction) => Promise; - - 'transaction-delete': (transaction) => Promise; - - 'transactions-parse-file': (arg: { - filepath: string; - options; - }) => Promise; - - 'transactions-export': (arg: { - transactions; - accounts?; - categoryGroups; - payees; - }) => Promise; - - 'transactions-export-query': (arg: { query: QueryState }) => Promise; - 'get-categories': () => Promise<{ grouped: Array; list: Array; diff --git a/packages/sync-server/docker/edge-alpine.Dockerfile b/packages/sync-server/docker/edge-alpine.Dockerfile index 070ae8f65ec..48a7fd69533 100644 --- a/packages/sync-server/docker/edge-alpine.Dockerfile +++ b/packages/sync-server/docker/edge-alpine.Dockerfile @@ -4,7 +4,7 @@ WORKDIR /app COPY .yarn ./.yarn COPY yarn.lock packages/sync-server/package.json .yarnrc.yml ./ RUN if [ "$(uname -m)" = "armv7l" ]; then yarn config set taskPoolConcurrency 2; yarn config set networkConcurrency 5; fi -RUN yarn workspaces focus actual-sync --production +RUN yarn workspaces focus @actual-app/sync-server --production RUN if [ "$(uname -m)" = "armv7l" ]; then npm install bcrypt better-sqlite3 --build-from-source; fi RUN mkdir /public diff --git a/packages/sync-server/docker/edge-ubuntu.Dockerfile b/packages/sync-server/docker/edge-ubuntu.Dockerfile index b0744a09548..18e009dad96 100644 --- a/packages/sync-server/docker/edge-ubuntu.Dockerfile +++ b/packages/sync-server/docker/edge-ubuntu.Dockerfile @@ -4,7 +4,7 @@ WORKDIR /app COPY .yarn ./.yarn COPY yarn.lock packages/sync-server/package.json .yarnrc.yml ./ RUN if [ "$(uname -m)" = "armv7l" ]; then yarn config set taskPoolConcurrency 2; yarn config set networkConcurrency 5; fi -RUN yarn workspaces focus actual-sync --production +RUN yarn workspaces focus @actual-app/sync-server --production RUN mkdir /public COPY artifacts.json /tmp/artifacts.json diff --git a/packages/sync-server/docker/stable-alpine.Dockerfile b/packages/sync-server/docker/stable-alpine.Dockerfile index 4fbf546ef5a..0194d5ba69e 100644 --- a/packages/sync-server/docker/stable-alpine.Dockerfile +++ b/packages/sync-server/docker/stable-alpine.Dockerfile @@ -4,7 +4,7 @@ WORKDIR /app COPY .yarn ./.yarn COPY yarn.lock packages/sync-server/package.json .yarnrc.yml ./ RUN if [ "$(uname -m)" = "armv7l" ]; then yarn config set taskPoolConcurrency 2; yarn config set networkConcurrency 5; fi -RUN yarn workspaces focus actual-sync --production +RUN yarn workspaces focus @actual-app/sync-server --production RUN if [ "$(uname -m)" = "armv7l" ]; then npm install bcrypt better-sqlite3 --build-from-source; fi FROM alpine:3.18 AS prod diff --git a/packages/sync-server/docker/stable-ubuntu.Dockerfile b/packages/sync-server/docker/stable-ubuntu.Dockerfile index a10d594476a..01834a5102b 100644 --- a/packages/sync-server/docker/stable-ubuntu.Dockerfile +++ b/packages/sync-server/docker/stable-ubuntu.Dockerfile @@ -4,7 +4,7 @@ WORKDIR /app COPY .yarn ./.yarn COPY yarn.lock packages/sync-server/package.json .yarnrc.yml ./ RUN if [ "$(uname -m)" = "armv7l" ]; then yarn config set taskPoolConcurrency 2; yarn config set networkConcurrency 5; fi -RUN yarn workspaces focus actual-sync --production +RUN yarn workspaces focus @actual-app/sync-server --production FROM node:18-bookworm-slim AS prod RUN apt-get update && apt-get install tini && apt-get clean -y && rm -rf /var/lib/apt/lists/* diff --git a/packages/sync-server/package.json b/packages/sync-server/package.json index e3532423b93..9b8f9b63e1e 100644 --- a/packages/sync-server/package.json +++ b/packages/sync-server/package.json @@ -1,11 +1,12 @@ { - "name": "actual-sync", + "name": "@actual-app/sync-server", "version": "25.2.1", "license": "MIT", "description": "actual syncing server", "type": "module", "scripts": { "start": "node app", + "start-monitor": "nodemon app", "lint": "eslint . --max-warnings 0", "lint:fix": "eslint . --fix", "build": "tsc", @@ -57,7 +58,9 @@ "@typescript-eslint/parser": "^5.51.0", "eslint": "^8.33.0", "eslint-plugin-prettier": "^4.2.1", + "http-proxy-middleware": "^3.0.3", "jest": "^29.3.1", + "nodemon": "^3.1.9", "prettier": "^2.8.3", "supertest": "^6.3.1", "typescript": "^4.9.5" diff --git a/packages/sync-server/src/app-gocardless/README.md b/packages/sync-server/src/app-gocardless/README.md index de406253dc3..b24bd3f519f 100644 --- a/packages/sync-server/src/app-gocardless/README.md +++ b/packages/sync-server/src/app-gocardless/README.md @@ -10,9 +10,9 @@ If the default bank integration does not work for you, you can integrate a new b This will trigger the process of fetching the data from the bank and will log the data in the backend. Use this data to fill the logic of the bank class. -4. Create new a bank class based on `app-gocardless/banks/sandboxfinance-sfin0000.js`. +4. Create new a bank class based on an existing example in `app-gocardless/banks`. - Name of the file and class should be created based on the ID of the integrated institution, found in step 1. + Name of the file and class should follow the existing patterns and be created based on the ID of the integrated institution, found in step 1. 5. Fill the logic of `normalizeAccount`, `normalizeTransaction`, `sortTransactions`, and `calculateStartingBalance` functions. You do not need to fill every function, only those which are necessary for the integration to work. @@ -162,3 +162,34 @@ If the default bank integration does not work for you, you can integrate a new b 6. Add new bank integration to `BankFactory` class in file `actual-server/app-gocardless/bank-factory.js` 7. Remember to add tests for new bank integration in + +## normalizeTransaction +This is the most commonly used override as it allows you to change the data that is returned to the client. + +Please follow the following patterns when implementing a custom normalizeTransaction method: +1. If you need to edit the values of transaction fields (excluding the transaction amount) do not mutate the original transaction object. Instead, create a shallow copy and make your changes there. +2. End the function by returning the result of calling the fallback normalizeTransaction method from integration-bank.js + +E.g. +```js +import Fallback from './integration-bank.js'; + +export default { + ... + + normalizeTransaction(transaction, booked) { + // create a shallow copy of the transaction object + const editedTrans = { ...transaction }; + + // make any changes required to the copy + editedTrans.remittanceInformationUnstructured = transaction.remittanceInformationStructured; + + // call the fallback method, passing in your edited transaction as the 3rd parameter + // this will calculate the date, payee name and notes fields based on your changes + // but leave the original fields available for mapping in the UI + return Fallback.normalizeTransaction(transaction, booked, editedTrans); + } + + ... +} +``` diff --git a/packages/sync-server/src/app-gocardless/bank-factory.js b/packages/sync-server/src/app-gocardless/bank-factory.js index d0cf32cfb5b..00ac1b233cb 100644 --- a/packages/sync-server/src/app-gocardless/bank-factory.js +++ b/packages/sync-server/src/app-gocardless/bank-factory.js @@ -34,8 +34,10 @@ export const BANKS_WITH_LIMITED_HISTORY = [ 'BANCA_AIDEXA_AIDXITMM', 'BANCA_PATRIMONI_SENVITT1', 'BANCA_SELLA_SELBIT2B', + 'BANK_MILLENNIUM_BIGBPLPW', 'BANKINTER_BKBKESMM', 'BBVA_BBVAESMM', + 'BNP_PL_PPABPLPK', 'BRED_BREDFRPPXXX', 'CAIXA_GERAL_DEPOSITOS_CGDIPTPL', 'CAIXABANK_CAIXESBB', @@ -60,6 +62,7 @@ export const BANKS_WITH_LIMITED_HISTORY = [ 'LUMINOR_NDEALV2X', 'LUMINOR_RIKOEE22', 'LUMINOR_RIKOLV2X', + 'MBANK_RETAIL_BREXPLPW', 'MEDICINOSBANK_MDBALT22XXX', 'NORDEA_NDEADKKK', 'N26_NTSBDEB1', diff --git a/packages/sync-server/src/app-gocardless/banks/abanca_caglesmm.js b/packages/sync-server/src/app-gocardless/banks/abanca_caglesmm.js index 4562869be66..a564be9bf9c 100644 --- a/packages/sync-server/src/app-gocardless/banks/abanca_caglesmm.js +++ b/packages/sync-server/src/app-gocardless/banks/abanca_caglesmm.js @@ -1,5 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; - import Fallback from './integration-bank.js'; /** @type {import('./bank.interface.js').IBank} */ @@ -13,14 +11,12 @@ export default { ], // Abanca transactions doesn't get the creditorName/debtorName properly - normalizeTransaction(transaction, _booked) { - transaction.creditorName = transaction.remittanceInformationStructured; - transaction.debtorName = transaction.remittanceInformationStructured; + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + + editedTrans.creditorName = transaction.remittanceInformationStructured; + editedTrans.debtorName = transaction.remittanceInformationStructured; - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate || transaction.valueDate, - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/abnamro_abnanl2a.js b/packages/sync-server/src/app-gocardless/banks/abnamro_abnanl2a.js index 9749c4002bc..8999b57346a 100644 --- a/packages/sync-server/src/app-gocardless/banks/abnamro_abnanl2a.js +++ b/packages/sync-server/src/app-gocardless/banks/abnamro_abnanl2a.js @@ -1,4 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -9,9 +8,11 @@ export default { institutionIds: ['ABNAMRO_ABNANL2A'], - normalizeTransaction(transaction, _booked) { + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + // There is no remittanceInformationUnstructured, so we'll make it - transaction.remittanceInformationUnstructured = + editedTrans.remittanceInformationUnstructured = transaction.remittanceInformationUnstructuredArray.join(', '); // Remove clutter to extract the payee from remittanceInformationUnstructured ... @@ -19,14 +20,13 @@ export default { const payeeName = transaction.remittanceInformationUnstructuredArray .map(el => el.match(/^(?:.*\*)?(.+),PAS\d+$/)) .find(match => match)?.[1]; - transaction.debtorName = transaction.debtorName || payeeName; - transaction.creditorName = transaction.creditorName || payeeName; - - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.valueDateTime.slice(0, 10), - }; + + editedTrans.debtorName = transaction.debtorName || payeeName; + editedTrans.creditorName = transaction.creditorName || payeeName; + + editedTrans.date = transaction.valueDateTime.slice(0, 10); + + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, sortTransactions(transactions = []) { diff --git a/packages/sync-server/src/app-gocardless/banks/american_express_aesudef1.js b/packages/sync-server/src/app-gocardless/banks/american_express_aesudef1.js index 418da7bbafb..e10e5e8c6a8 100644 --- a/packages/sync-server/src/app-gocardless/banks/american_express_aesudef1.js +++ b/packages/sync-server/src/app-gocardless/banks/american_express_aesudef1.js @@ -1,4 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -21,14 +20,6 @@ export default { }; }, - normalizeTransaction(transaction, _booked) { - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate, - }; - }, - /** * For AMERICAN_EXPRESS_AESUDEF1 we don't know what balance was * after each transaction so we have to calculate it by getting diff --git a/packages/sync-server/src/app-gocardless/banks/bancsabadell_bsabesbbb.js b/packages/sync-server/src/app-gocardless/banks/bancsabadell_bsabesbbb.js index 723de21c561..ecf6e2bba55 100644 --- a/packages/sync-server/src/app-gocardless/banks/bancsabadell_bsabesbbb.js +++ b/packages/sync-server/src/app-gocardless/banks/bancsabadell_bsabesbbb.js @@ -1,5 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; - import Fallback from './integration-bank.js'; /** @type {import('./bank.interface.js').IBank} */ @@ -9,7 +7,9 @@ export default { institutionIds: ['BANCSABADELL_BSABESBB'], // Sabadell transactions don't get the creditorName/debtorName properly - normalizeTransaction(transaction, _booked) { + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + const amount = transaction.transactionAmount.amount; // The amount is negative for outgoing transactions, positive for incoming transactions. @@ -23,13 +23,9 @@ export default { const creditorName = isCreditorPayee ? payeeName : null; const debtorName = isCreditorPayee ? null : payeeName; - transaction.creditorName = creditorName; - transaction.debtorName = debtorName; + editedTrans.creditorName = creditorName; + editedTrans.debtorName = debtorName; - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate || transaction.valueDate, - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/bank.interface.ts b/packages/sync-server/src/app-gocardless/banks/bank.interface.ts index 4fc484d05fb..e0dfc47e726 100644 --- a/packages/sync-server/src/app-gocardless/banks/bank.interface.ts +++ b/packages/sync-server/src/app-gocardless/banks/bank.interface.ts @@ -4,6 +4,14 @@ import { NormalizedAccountDetails, } from '../gocardless.types.js'; +type TransactionExtended = Transaction & { + date?: string; + payeeName?: string; + notes?: string; + remittanceInformationUnstructuredArrayString?: string; + remittanceInformationStructuredArrayString?: string; +}; + export interface IBank { institutionIds: string[]; @@ -23,9 +31,10 @@ export interface IBank { * transaction date. */ normalizeTransaction: ( - transaction: Transaction, + transaction: TransactionExtended, booked: boolean, - ) => (Transaction & { date?: string; payeeName: string }) | null; + editedTransaction?: TransactionExtended, + ) => TransactionExtended | null; /** * Function sorts an array of transactions from newest to oldest diff --git a/packages/sync-server/src/app-gocardless/banks/bank_of_ireland_b365_bofiie2d.js b/packages/sync-server/src/app-gocardless/banks/bank_of_ireland_b365_bofiie2d.js index 959a0fd9865..0225bbbb9b5 100644 --- a/packages/sync-server/src/app-gocardless/banks/bank_of_ireland_b365_bofiie2d.js +++ b/packages/sync-server/src/app-gocardless/banks/bank_of_ireland_b365_bofiie2d.js @@ -7,11 +7,13 @@ export default { institutionIds: ['BANK_OF_IRELAND_B365_BOFIIE2D'], normalizeTransaction(transaction, booked) { - transaction.remittanceInformationUnstructured = fixupPayee( + const editedTrans = { ...transaction }; + + editedTrans.remittanceInformationUnstructured = fixupPayee( transaction.remittanceInformationUnstructured, ); - return Fallback.normalizeTransaction(transaction, booked); + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/bankinter_bkbkesmm.js b/packages/sync-server/src/app-gocardless/banks/bankinter_bkbkesmm.js index 0ef9b2106d8..fd824e1dd32 100644 --- a/packages/sync-server/src/app-gocardless/banks/bankinter_bkbkesmm.js +++ b/packages/sync-server/src/app-gocardless/banks/bankinter_bkbkesmm.js @@ -1,5 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; - import Fallback from './integration-bank.js'; /** @type {import('./bank.interface.js').IBank} */ @@ -8,21 +6,19 @@ export default { institutionIds: ['BANKINTER_BKBKESMM'], - normalizeTransaction(transaction, _booked) { - transaction.remittanceInformationUnstructured = + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + + editedTrans.remittanceInformationUnstructured = transaction.remittanceInformationUnstructured .replaceAll(/\/Txt\/(\w\|)?/gi, '') .replaceAll(';', ' '); - transaction.debtorName = transaction.debtorName?.replaceAll(';', ' '); - transaction.creditorName = + editedTrans.debtorName = transaction.debtorName?.replaceAll(';', ' '); + editedTrans.creditorName = transaction.creditorName?.replaceAll(';', ' ') ?? - transaction.remittanceInformationUnstructured; + editedTrans.remittanceInformationUnstructured; - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate || transaction.valueDate, - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/belfius_gkccbebb.js b/packages/sync-server/src/app-gocardless/banks/belfius_gkccbebb.js index e5d93e2dc84..5b7ede23c16 100644 --- a/packages/sync-server/src/app-gocardless/banks/belfius_gkccbebb.js +++ b/packages/sync-server/src/app-gocardless/banks/belfius_gkccbebb.js @@ -1,5 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; - import Fallback from './integration-bank.js'; /** @type {import('./bank.interface.js').IBank} */ @@ -11,12 +9,9 @@ export default { // The problem is that we have transaction with duplicated transaction ids. // This is not expected and the nordigen api has a work-around for some backs // They will set an internalTransactionId which is unique - normalizeTransaction(transaction, _booked) { - return { - ...transaction, - transactionId: transaction.internalTransactionId, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate || transaction.valueDate, - }; + normalizeTransaction(transaction, booked) { + transaction.transactionId = transaction.internalTransactionId; + + return Fallback.normalizeTransaction(transaction, booked); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/berliner_sparkasse_beladebexxx.js b/packages/sync-server/src/app-gocardless/banks/berliner_sparkasse_beladebexxx.js index 842bc27de8f..619b76f3c6d 100644 --- a/packages/sync-server/src/app-gocardless/banks/berliner_sparkasse_beladebexxx.js +++ b/packages/sync-server/src/app-gocardless/banks/berliner_sparkasse_beladebexxx.js @@ -1,4 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -9,27 +8,8 @@ export default { institutionIds: ['BERLINER_SPARKASSE_BELADEBEXXX'], - /** - * Following the GoCardless documentation[0] we should prefer `bookingDate` - * here, though some of their bank integrations uses the date field - * differently from what's described in their documentation and so it's - * sometimes necessary to use `valueDate` instead. - * - * [0]: https://nordigen.zendesk.com/hc/en-gb/articles/7899367372829-valueDate-and-bookingDate-for-transactions - */ - normalizeTransaction(transaction, _booked) { - const date = - transaction.bookingDate || - transaction.bookingDateTime || - transaction.valueDate || - transaction.valueDateTime; - - // If we couldn't find a valid date field we filter out this transaction - // and hope that we will import it again once the bank has processed the - // transaction further. - if (!date) { - return null; - } + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; let remittanceInformationUnstructured; @@ -54,15 +34,11 @@ export default { transaction.creditorName || transaction.debtorName; - transaction.creditorName = usefulCreditorName; - transaction.remittanceInformationUnstructured = + editedTrans.creditorName = usefulCreditorName; + editedTrans.remittanceInformationUnstructured = remittanceInformationUnstructured; - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate || transaction.valueDate, - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, /** diff --git a/packages/sync-server/src/app-gocardless/banks/bnp_be_gebabebb.js b/packages/sync-server/src/app-gocardless/banks/bnp_be_gebabebb.js index e24a1818fc0..28f4be55abe 100644 --- a/packages/sync-server/src/app-gocardless/banks/bnp_be_gebabebb.js +++ b/packages/sync-server/src/app-gocardless/banks/bnp_be_gebabebb.js @@ -1,5 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; - import Fallback from './integration-bank.js'; /** @type {import('./bank.interface.js').IBank} */ @@ -22,7 +20,9 @@ export default { * The goal of the normalization is to place any relevant information of the additionalInformation * field in the remittanceInformationUnstructuredArray field. */ - normalizeTransaction(transaction, _booked) { + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + // Extract the creditor name to fill it in with information from the // additionalInformation field in case it's not yet defined. let creditorName = transaction.creditorName; @@ -49,7 +49,7 @@ export default { additionalInformationObject[key] = value; } // Keep existing unstructuredArray and add atmPosName and narrative - transaction.remittanceInformationUnstructuredArray = [ + editedTrans.remittanceInformationUnstructuredArray = [ transaction.remittanceInformationUnstructuredArray ?? '', additionalInformationObject?.atmPosName ?? '', additionalInformationObject?.narrative ?? '', @@ -66,12 +66,8 @@ export default { } } - transaction.creditorName = creditorName; + editedTrans.creditorName = creditorName; - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.valueDate || transaction.bookingDate, - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/cbc_cregbebb.js b/packages/sync-server/src/app-gocardless/banks/cbc_cregbebb.js index b8e3bec7e68..013fe7f2ddb 100644 --- a/packages/sync-server/src/app-gocardless/banks/cbc_cregbebb.js +++ b/packages/sync-server/src/app-gocardless/banks/cbc_cregbebb.js @@ -11,26 +11,24 @@ export default { * For negative amounts, the only payee information we have is returned in * remittanceInformationUnstructured. */ - normalizeTransaction(transaction, _booked) { - if (Number(transaction.transactionAmount.amount) > 0) { - return { - ...transaction, - payeeName: - transaction.debtorName || - transaction.remittanceInformationUnstructured, - date: transaction.bookingDate || transaction.valueDate, - }; - } + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; - return { - ...transaction, - payeeName: + if (Number(transaction.transactionAmount.amount) > 0) { + editedTrans.payeeName = + transaction.debtorName || + transaction.remittanceInformationUnstructured || + 'undefined'; + } else { + editedTrans.payeeName = transaction.creditorName || extractPayeeNameFromRemittanceInfo( transaction.remittanceInformationUnstructured, ['Paiement', 'Domiciliation', 'Transfert', 'Ordre permanent'], - ), - date: transaction.bookingDate || transaction.valueDate, - }; + ) || + 'undefined'; + } + + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/commerzbank_cobadeff.js b/packages/sync-server/src/app-gocardless/banks/commerzbank_cobadeff.js index d58b98a191f..e2fe8bc9a2a 100644 --- a/packages/sync-server/src/app-gocardless/banks/commerzbank_cobadeff.js +++ b/packages/sync-server/src/app-gocardless/banks/commerzbank_cobadeff.js @@ -1,5 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; - import Fallback from './integration-bank.js'; /** @type {import('./bank.interface.js').IBank} */ @@ -8,11 +6,13 @@ export default { institutionIds: ['COMMERZBANK_COBADEFF'], - normalizeTransaction(transaction, _booked) { + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + // remittanceInformationUnstructured is limited to 140 chars thus ... // ... missing information form remittanceInformationUnstructuredArray ... // ... so we recreate it. - transaction.remittanceInformationUnstructured = + editedTrans.remittanceInformationUnstructured = transaction.remittanceInformationUnstructuredArray.join(' '); // The limitations of remittanceInformationUnstructuredArray ... @@ -27,8 +27,8 @@ export default { 'Dauerauftrag', ]; keywords.forEach(keyword => { - transaction.remittanceInformationUnstructured = - transaction.remittanceInformationUnstructured.replace( + editedTrans.remittanceInformationUnstructured = + editedTrans.remittanceInformationUnstructured.replace( // There can be spaces in keywords RegExp(keyword.split('').join('\\s*'), 'gi'), ', ' + keyword + ' ', @@ -39,17 +39,13 @@ export default { // ... that are added to the remittanceInformation field), and ... // ... remove clutter like "End-to-End-Ref.: NOTPROVIDED" const payee = transaction.creditorName || transaction.debtorName || ''; - transaction.remittanceInformationUnstructured = - transaction.remittanceInformationUnstructured + editedTrans.remittanceInformationUnstructured = + editedTrans.remittanceInformationUnstructured .replace(/\s*(,)?\s+/g, '$1 ') .replace(RegExp(payee.split(' ').join('(/*| )'), 'gi'), ' ') .replace(', End-to-End-Ref.: NOTPROVIDED', '') .trim(); - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate, - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/danskebank_dabno22.js b/packages/sync-server/src/app-gocardless/banks/danskebank_dabno22.js index 215925dfa2c..94a34c648d5 100644 --- a/packages/sync-server/src/app-gocardless/banks/danskebank_dabno22.js +++ b/packages/sync-server/src/app-gocardless/banks/danskebank_dabno22.js @@ -1,4 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -9,7 +8,9 @@ export default { institutionIds: ['DANSKEBANK_DABANO22'], - normalizeTransaction(transaction, _booked) { + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + /** * Danske Bank appends the EndToEndID: NOTPROVIDED to * remittanceInformationUnstructured, cluttering the data. @@ -17,21 +18,13 @@ export default { * We clean thais up by removing any instances of this string from all transactions. * */ - transaction.remittanceInformationUnstructured = + editedTrans.remittanceInformationUnstructured = transaction.remittanceInformationUnstructured.replace( '\nEndToEndID: NOTPROVIDED', '', ); - /** - * The valueDate in transactions from Danske Bank is not the one expected, but rather the date - * the funds are expected to be paid back for credit accounts. - */ - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate, - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, calculateStartingBalance(sortedTransactions = [], balances = []) { diff --git a/packages/sync-server/src/app-gocardless/banks/direkt_heladef1822.js b/packages/sync-server/src/app-gocardless/banks/direkt_heladef1822.js index 74a2393547b..4d753e0326c 100644 --- a/packages/sync-server/src/app-gocardless/banks/direkt_heladef1822.js +++ b/packages/sync-server/src/app-gocardless/banks/direkt_heladef1822.js @@ -7,10 +7,12 @@ export default { institutionIds: ['DIREKT_HELADEF1822'], normalizeTransaction(transaction, booked) { - transaction.remittanceInformationUnstructured = + const editedTrans = { ...transaction }; + + editedTrans.remittanceInformationUnstructured = transaction.remittanceInformationUnstructured ?? transaction.remittanceInformationStructured; - return Fallback.normalizeTransaction(transaction, booked); + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/easybank_bawaatww.js b/packages/sync-server/src/app-gocardless/banks/easybank_bawaatww.js index 7becc29b2e6..c5a88596111 100644 --- a/packages/sync-server/src/app-gocardless/banks/easybank_bawaatww.js +++ b/packages/sync-server/src/app-gocardless/banks/easybank_bawaatww.js @@ -21,24 +21,14 @@ export default { return parseInt(b.transactionId) - parseInt(a.transactionId); }), - normalizeTransaction(transaction, _booked) { - const date = transaction.bookingDate || transaction.valueDate; - - // If we couldn't find a valid date field we filter out this transaction - // and hope that we will import it again once the bank has processed the - // transaction further. - if (!date) { - return null; - } + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; let payeeName = formatPayeeName(transaction); if (!payeeName) payeeName = extractPayeeName(transaction); + editedTrans.payeeName = payeeName; - return { - ...transaction, - payeeName, - date: d.format(d.parseISO(date), 'yyyy-MM-dd'), - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/entercard_swednokk.js b/packages/sync-server/src/app-gocardless/banks/entercard_swednokk.js index 0cfa09797ce..ce7ddf14223 100644 --- a/packages/sync-server/src/app-gocardless/banks/entercard_swednokk.js +++ b/packages/sync-server/src/app-gocardless/banks/entercard_swednokk.js @@ -1,6 +1,3 @@ -import * as d from 'date-fns'; - -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -11,7 +8,9 @@ export default { institutionIds: ['ENTERCARD_SWEDNOKK'], - normalizeTransaction(transaction, _booked) { + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + // GoCardless's Entercard integration returns forex transactions with the // foreign amount in `transactionAmount`, but at least the amount actually // billed to the account is now available in @@ -25,11 +24,9 @@ export default { }; } - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: d.format(d.parseISO(transaction.valueDate), 'yyyy-MM-dd'), - }; + editedTrans.date = transaction.valueDate; + + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, calculateStartingBalance(sortedTransactions = [], balances = []) { diff --git a/packages/sync-server/src/app-gocardless/banks/fortuneo_ftnofrp1xxx.js b/packages/sync-server/src/app-gocardless/banks/fortuneo_ftnofrp1xxx.js index 3dab221946a..b84452ec7c8 100644 --- a/packages/sync-server/src/app-gocardless/banks/fortuneo_ftnofrp1xxx.js +++ b/packages/sync-server/src/app-gocardless/banks/fortuneo_ftnofrp1xxx.js @@ -1,5 +1,3 @@ -import * as d from 'date-fns'; - import { formatPayeeName } from '../../util/payee-name.js'; import Fallback from './integration-bank.js'; @@ -10,18 +8,8 @@ export default { institutionIds: ['FORTUNEO_FTNOFRP1XXX'], - normalizeTransaction(transaction, _booked) { - const date = - transaction.bookingDate || - transaction.bookingDateTime || - transaction.valueDate || - transaction.valueDateTime; - // If we couldn't find a valid date field we filter out this transaction - // and hope that we will import it again once the bank has processed the - // transaction further. - if (!date) { - return null; - } + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; // Most of the information from the transaction is in the remittanceInformationUnstructuredArray field. // We extract the creditor and debtor names from this field. @@ -50,13 +38,9 @@ export default { const creditorName = isCreditorPayee ? payeeName : null; const debtorName = isCreditorPayee ? null : payeeName; - transaction.creditorName = creditorName; - transaction.debtorName = debtorName; + editedTrans.creditorName = creditorName; + editedTrans.debtorName = debtorName; - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: d.format(d.parseISO(date), 'yyyy-MM-dd'), - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/hype_hyeeit22.js b/packages/sync-server/src/app-gocardless/banks/hype_hyeeit22.js index e165a2f51b1..deef0019672 100644 --- a/packages/sync-server/src/app-gocardless/banks/hype_hyeeit22.js +++ b/packages/sync-server/src/app-gocardless/banks/hype_hyeeit22.js @@ -1,5 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; - import Fallback from './integration-bank.js'; /** @type {import('./bank.interface.js').IBank} */ @@ -8,13 +6,15 @@ export default { institutionIds: ['HYPE_HYEEIT22'], - normalizeTransaction(transaction, _booked) { + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + /** Online card payments - identified by "crd" transaction code * always start with PAGAMENTO PRESSO + */ if (transaction.proprietaryBankTransactionCode === 'crd') { // remove PAGAMENTO PRESSO and set payee name - transaction.debtorName = + editedTrans.debtorName = transaction.remittanceInformationUnstructured?.slice( 'PAGAMENTO PRESSO '.length, ); @@ -32,7 +32,7 @@ export default { // NOTE: if {payee_name} contains dashes (unlikely / impossible?), this probably gets bugged! const infoIdx = transaction.remittanceInformationUnstructured.indexOf(' - ') + 3; - transaction.remittanceInformationUnstructured = + editedTrans.remittanceInformationUnstructured = infoIdx === -1 ? transaction.remittanceInformationUnstructured : transaction.remittanceInformationUnstructured.slice(infoIdx).trim(); @@ -64,12 +64,11 @@ export default { idx = str.indexOf('\\U'); // slight inefficiency? start_idx = idx; } - transaction.remittanceInformationUnstructured = str; + editedTrans.remittanceInformationUnstructured = str; } - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.valueDate || transaction.bookingDate, - }; + + editedTrans.date = transaction.valueDate || transaction.bookingDate; + + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/ing_ingbrobu.js b/packages/sync-server/src/app-gocardless/banks/ing_ingbrobu.js index b514d78e8d4..4305502b6ea 100644 --- a/packages/sync-server/src/app-gocardless/banks/ing_ingbrobu.js +++ b/packages/sync-server/src/app-gocardless/banks/ing_ingbrobu.js @@ -7,6 +7,8 @@ export default { institutionIds: ['ING_INGBROBU'], normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + //Merchant transactions all have the same transactionId of 'NOTPROVIDED'. //For booked transactions, this can be set to the internalTransactionId //For pending transactions, this needs to be removed for them to show up in Actual @@ -19,7 +21,7 @@ export default { transaction.proprietaryBankTransactionCode && !transaction.remittanceInformationUnstructured ) { - transaction.remittanceInformationUnstructured = + editedTrans.remittanceInformationUnstructured = transaction.proprietaryBankTransactionCode; } @@ -31,11 +33,11 @@ export default { .toLowerCase() .includes('card no:') ) { - transaction.creditorName = + editedTrans.creditorName = transaction.remittanceInformationUnstructured.split(',')[0]; //Catch all case for other types of payees } else { - transaction.creditorName = + editedTrans.creditorName = transaction.remittanceInformationUnstructured; } } else { @@ -47,22 +49,22 @@ export default { .toLowerCase() .includes('card no:') ) { - transaction.creditorName = + editedTrans.creditorName = transaction.remittanceInformationUnstructured.replace( /x{4}/g, 'Xxxx ', ); //Catch all case for other types of payees } else { - transaction.creditorName = + editedTrans.creditorName = transaction.remittanceInformationUnstructured; } //Remove remittanceInformationUnstructured from pending transactions, so the `notes` field remains empty (there is no merchant information) //Once booked, the right `notes` (containing the merchant) will be populated - transaction.remittanceInformationUnstructured = null; + editedTrans.remittanceInformationUnstructured = null; } } - return Fallback.normalizeTransaction(transaction, booked); + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/ing_ingddeff.js b/packages/sync-server/src/app-gocardless/banks/ing_ingddeff.js index 9d4ea894b73..bb77aa0d854 100644 --- a/packages/sync-server/src/app-gocardless/banks/ing_ingddeff.js +++ b/packages/sync-server/src/app-gocardless/banks/ing_ingddeff.js @@ -1,4 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -9,20 +8,18 @@ export default { institutionIds: ['ING_INGDDEFF'], - normalizeTransaction(transaction, _booked) { + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + const remittanceInformationMatch = /remittanceinformation:(.*)$/.exec( transaction.remittanceInformationUnstructured, ); - transaction.remittanceInformationUnstructured = remittanceInformationMatch + editedTrans.remittanceInformationUnstructured = remittanceInformationMatch ? remittanceInformationMatch[1] : transaction.remittanceInformationUnstructured; - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate || transaction.valueDate, - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, sortTransactions(transactions = []) { diff --git a/packages/sync-server/src/app-gocardless/banks/ing_pl_ingbplpw.js b/packages/sync-server/src/app-gocardless/banks/ing_pl_ingbplpw.js index 5e7ead0f858..2f7e69aa107 100644 --- a/packages/sync-server/src/app-gocardless/banks/ing_pl_ingbplpw.js +++ b/packages/sync-server/src/app-gocardless/banks/ing_pl_ingbplpw.js @@ -1,4 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -9,12 +8,12 @@ export default { institutionIds: ['ING_PL_INGBPLPW'], - normalizeTransaction(transaction, _booked) { - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.valueDate ?? transaction.bookingDate, - }; + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + + editedTrans.date = transaction.valueDate; + + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, sortTransactions(transactions = []) { diff --git a/packages/sync-server/src/app-gocardless/banks/integration-bank.js b/packages/sync-server/src/app-gocardless/banks/integration-bank.js index a4cd882a097..adb5e94a225 100644 --- a/packages/sync-server/src/app-gocardless/banks/integration-bank.js +++ b/packages/sync-server/src/app-gocardless/banks/integration-bank.js @@ -44,22 +44,38 @@ export default { }; }, - normalizeTransaction(transaction, _booked) { + normalizeTransaction(transaction, _booked, editedTransaction = null) { + const trans = editedTransaction ?? transaction; + const date = + trans.date || transaction.bookingDate || transaction.bookingDateTime || transaction.valueDate || transaction.valueDateTime; + // If we couldn't find a valid date field we filter out this transaction // and hope that we will import it again once the bank has processed the // transaction further. if (!date) { return null; } + + const notes = + trans.notes ?? + trans.remittanceInformationUnstructured ?? + trans.remittanceInformationUnstructuredArray?.join(' '); + + transaction.remittanceInformationUnstructuredArrayString = + transaction.remittanceInformationUnstructuredArray?.join(','); + transaction.remittanceInformationStructuredArrayString = + transaction.remittanceInformationStructuredArray?.join(','); + return { ...transaction, - payeeName: formatPayeeName(transaction), + payeeName: trans.payeeName ?? formatPayeeName(trans), date: d.format(d.parseISO(date), 'yyyy-MM-dd'), + notes, }; }, diff --git a/packages/sync-server/src/app-gocardless/banks/isybank_itbbitmm.js b/packages/sync-server/src/app-gocardless/banks/isybank_itbbitmm.js index a6685ccae02..a927412facf 100644 --- a/packages/sync-server/src/app-gocardless/banks/isybank_itbbitmm.js +++ b/packages/sync-server/src/app-gocardless/banks/isybank_itbbitmm.js @@ -9,8 +9,10 @@ export default { // It has been reported that valueDate is more accurate than booking date // when it is provided normalizeTransaction(transaction, booked) { - transaction.bookingDate = transaction.valueDate ?? transaction.bookingDate; + const editedTrans = { ...transaction }; - return Fallback.normalizeTransaction(transaction, booked); + editedTrans.date = transaction.valueDate ?? transaction.bookingDate; + + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/kbc_kredbebb.js b/packages/sync-server/src/app-gocardless/banks/kbc_kredbebb.js index bd9df6f90a7..477686318ee 100644 --- a/packages/sync-server/src/app-gocardless/banks/kbc_kredbebb.js +++ b/packages/sync-server/src/app-gocardless/banks/kbc_kredbebb.js @@ -11,27 +11,23 @@ export default { * For negative amounts, the only payee information we have is returned in * remittanceInformationUnstructured. */ - normalizeTransaction(transaction, _booked) { - if (Number(transaction.transactionAmount.amount) > 0) { - return { - ...transaction, - payeeName: - transaction.debtorName || - transaction.remittanceInformationUnstructured || - 'undefined', - date: transaction.bookingDate || transaction.valueDate, - }; - } + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; - return { - ...transaction, - payeeName: + if (Number(transaction.transactionAmount.amount) > 0) { + editedTrans.payeeName = + transaction.debtorName || + transaction.remittanceInformationUnstructured || + 'undefined'; + } else { + editedTrans.payeeName = transaction.creditorName || extractPayeeNameFromRemittanceInfo( transaction.remittanceInformationUnstructured, ['Betaling met', 'Domiciliëring', 'Overschrijving'], - ), - date: transaction.bookingDate || transaction.valueDate, - }; + ); + } + + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/lhv-lhvbee22.js b/packages/sync-server/src/app-gocardless/banks/lhv-lhvbee22.js index 8f948c6508b..1f3f19664ac 100644 --- a/packages/sync-server/src/app-gocardless/banks/lhv-lhvbee22.js +++ b/packages/sync-server/src/app-gocardless/banks/lhv-lhvbee22.js @@ -9,6 +9,8 @@ export default { institutionIds: ['LHV_LHVBEE22'], normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + // extract bookingDate and creditorName for card transactions, e.g. // (..1234) 2025-01-02 09:32 CrustumOU\Poordi 3\Tallinn\10156 ESTEST // bookingDate: 2025-01-02 @@ -22,19 +24,13 @@ export default { if (cardTxMatch) { const extractedDate = d.parse(cardTxMatch[2], 'yyyy-MM-dd', new Date()); - transaction = { - ...transaction, - creditorName: cardTxMatch[4].split('\\')[0].trim(), - }; + editedTrans.payeeName = cardTxMatch[4].split('\\')[0].trim(); if (booked && d.isValid(extractedDate)) { - transaction = { - ...transaction, - bookingDate: d.format(extractedDate, 'yyyy-MM-dd'), - }; + editedTrans.date = d.format(extractedDate, 'yyyy-MM-dd'); } } - return Fallback.normalizeTransaction(transaction, booked); + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/mbank_retail_brexplpw.js b/packages/sync-server/src/app-gocardless/banks/mbank_retail_brexplpw.js index 7338b35ec8f..6e15cd8f9c9 100644 --- a/packages/sync-server/src/app-gocardless/banks/mbank_retail_brexplpw.js +++ b/packages/sync-server/src/app-gocardless/banks/mbank_retail_brexplpw.js @@ -1,4 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -9,14 +8,6 @@ export default { institutionIds: ['MBANK_RETAIL_BREXPLPW'], - normalizeTransaction(transaction, _booked) { - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate || transaction.valueDate, - }; - }, - sortTransactions(transactions = []) { return transactions.sort( (a, b) => Number(b.transactionId) - Number(a.transactionId), diff --git a/packages/sync-server/src/app-gocardless/banks/nationwide_naiagb21.js b/packages/sync-server/src/app-gocardless/banks/nationwide_naiagb21.js index fdcb933527d..22efe83a5ee 100644 --- a/packages/sync-server/src/app-gocardless/banks/nationwide_naiagb21.js +++ b/packages/sync-server/src/app-gocardless/banks/nationwide_naiagb21.js @@ -7,6 +7,8 @@ export default { institutionIds: ['NATIONWIDE_NAIAGB21'], normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + // Nationwide can sometimes return pending transactions with a date // representing the latest a transaction could be booked. This stops // actual's deduplication logic from working as it only checks 7 days @@ -19,7 +21,7 @@ export default { new Date().getTime(), ), ); - transaction.bookingDate = useDate.toISOString().slice(0, 10); + editedTrans.date = useDate.toISOString().slice(0, 10); } // Nationwide also occasionally returns erroneous transaction_ids @@ -39,6 +41,6 @@ export default { transaction.transactionId = null; } - return Fallback.normalizeTransaction(transaction, booked); + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/nbg_ethngraaxxx.js b/packages/sync-server/src/app-gocardless/banks/nbg_ethngraaxxx.js index 8bce27d75d0..60361ebb402 100644 --- a/packages/sync-server/src/app-gocardless/banks/nbg_ethngraaxxx.js +++ b/packages/sync-server/src/app-gocardless/banks/nbg_ethngraaxxx.js @@ -1,4 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -14,27 +13,22 @@ export default { * - Corrects amount to negative (nbg erroneously omits the minus sign in pending transactions) * - Removes prefix 'ΑΓΟΡΑ' from remittance information to align with the booked transaction (necessary for fuzzy matching to work) */ - normalizeTransaction(transaction, _booked) { + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + if ( !transaction.transactionId && transaction.remittanceInformationUnstructured.startsWith('ΑΓΟΡΑ ') ) { - transaction = { - ...transaction, - transactionAmount: { - amount: '-' + transaction.transactionAmount.amount, - currency: transaction.transactionAmount.currency, - }, - remittanceInformationUnstructured: - transaction.remittanceInformationUnstructured.substring(6), + transaction.transactionAmount = { + amount: '-' + transaction.transactionAmount.amount, + currency: transaction.transactionAmount.currency, }; + editedTrans.remittanceInformationUnstructured = + transaction.remittanceInformationUnstructured.substring(6); } - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate || transaction.valueDate, - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, /** diff --git a/packages/sync-server/src/app-gocardless/banks/norwegian_xx_norwnok1.js b/packages/sync-server/src/app-gocardless/banks/norwegian_xx_norwnok1.js index 05c3498da50..3846dd7cbd7 100644 --- a/packages/sync-server/src/app-gocardless/banks/norwegian_xx_norwnok1.js +++ b/packages/sync-server/src/app-gocardless/banks/norwegian_xx_norwnok1.js @@ -1,4 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -17,12 +16,11 @@ export default { ], normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + if (booked) { - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate, - }; + editedTrans.date = transaction.bookingDate; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); } /** @@ -38,11 +36,8 @@ export default { * once the bank has processed it further. */ if (transaction.valueDate !== undefined) { - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.valueDate, - }; + editedTrans.date = transaction.valueDate; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); } if (transaction.remittanceInformationStructured) { @@ -50,12 +45,8 @@ export default { const matches = transaction.remittanceInformationStructured.match(remittanceInfoRegex); if (matches) { - transaction.valueDate = matches[1]; - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: matches[1], - }; + editedTrans.date = matches[1]; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); } } diff --git a/packages/sync-server/src/app-gocardless/banks/revolut_revolt21.js b/packages/sync-server/src/app-gocardless/banks/revolut_revolt21.js index 1e25618c8b2..b5fb4321624 100644 --- a/packages/sync-server/src/app-gocardless/banks/revolut_revolt21.js +++ b/packages/sync-server/src/app-gocardless/banks/revolut_revolt21.js @@ -1,7 +1,3 @@ -import * as d from 'date-fns'; - -import { formatPayeeName } from '../../util/payee-name.js'; - import Fallback from './integration-bank.js'; /** @type {import('./bank.interface.js').IBank} */ @@ -10,29 +6,21 @@ export default { institutionIds: ['REVOLUT_REVOLT21'], - normalizeTransaction(transaction, _booked) { + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + if ( transaction.remittanceInformationUnstructuredArray[0].startsWith( 'Bizum payment from: ', ) ) { - const date = - transaction.bookingDate || - transaction.bookingDateTime || - transaction.valueDate || - transaction.valueDateTime; - - return { - ...transaction, - payeeName: - transaction.remittanceInformationUnstructuredArray[0].replace( - 'Bizum payment from: ', - '', - ), - remittanceInformationUnstructured: - transaction.remittanceInformationUnstructuredArray[1], - date: d.format(d.parseISO(date), 'yyyy-MM-dd'), - }; + editedTrans.payeeName = + transaction.remittanceInformationUnstructuredArray[0].replace( + 'Bizum payment from: ', + '', + ); + editedTrans.remittanceInformationUnstructured = + transaction.remittanceInformationUnstructuredArray[1]; } if ( @@ -40,21 +28,10 @@ export default { 'Bizum payment to: ', ) ) { - const date = - transaction.bookingDate || - transaction.bookingDateTime || - transaction.valueDate || - transaction.valueDateTime; - - return { - ...transaction, - payeeName: formatPayeeName(transaction), - remittanceInformationUnstructured: - transaction.remittanceInformationUnstructuredArray[1], - date: d.format(d.parseISO(date), 'yyyy-MM-dd'), - }; + editedTrans.remittanceInformationUnstructured = + transaction.remittanceInformationUnstructuredArray[1]; } - return Fallback.normalizeTransaction(transaction, _booked); + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/sandboxfinance_sfin0000.js b/packages/sync-server/src/app-gocardless/banks/sandboxfinance_sfin0000.js index 5e61ccbd830..f63a3c9c8d9 100644 --- a/packages/sync-server/src/app-gocardless/banks/sandboxfinance_sfin0000.js +++ b/packages/sync-server/src/app-gocardless/banks/sandboxfinance_sfin0000.js @@ -1,4 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -9,22 +8,6 @@ export default { institutionIds: ['SANDBOXFINANCE_SFIN0000'], - /** - * Following the GoCardless documentation[0] we should prefer `bookingDate` - * here, though some of their bank integrations uses the date field - * differently from what's described in their documentation and so it's - * sometimes necessary to use `valueDate` instead. - * - * [0]: https://nordigen.zendesk.com/hc/en-gb/articles/7899367372829-valueDate-and-bookingDate-for-transactions - */ - normalizeTransaction(transaction, _booked) { - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate || transaction.valueDate, - }; - }, - /** * For SANDBOXFINANCE_SFIN0000 we don't know what balance was * after each transaction so we have to calculate it by getting diff --git a/packages/sync-server/src/app-gocardless/banks/seb_kort_bank_ab.js b/packages/sync-server/src/app-gocardless/banks/seb_kort_bank_ab.js index 2db12b4eea0..241ceaefb22 100644 --- a/packages/sync-server/src/app-gocardless/banks/seb_kort_bank_ab.js +++ b/packages/sync-server/src/app-gocardless/banks/seb_kort_bank_ab.js @@ -1,4 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -16,20 +15,18 @@ export default { /** * Sign of transaction amount needs to be flipped for SEB credit cards */ - normalizeTransaction(transaction, _booked) { + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + // Creditor name is stored in additionInformation for SEB - transaction.creditorName = transaction.additionalInformation; + editedTrans.creditorName = transaction.additionalInformation; transaction.transactionAmount = { // Flip transaction amount sign amount: (-parseFloat(transaction.transactionAmount.amount)).toString(), currency: transaction.transactionAmount.currency, }; - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.valueDate, - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, /** diff --git a/packages/sync-server/src/app-gocardless/banks/seb_privat.js b/packages/sync-server/src/app-gocardless/banks/seb_privat.js index c4baf7f4226..c7725319cf6 100644 --- a/packages/sync-server/src/app-gocardless/banks/seb_privat.js +++ b/packages/sync-server/src/app-gocardless/banks/seb_privat.js @@ -1,6 +1,3 @@ -import * as d from 'date-fns'; - -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -11,27 +8,13 @@ export default { institutionIds: ['SEB_ESSESESS_PRIVATE'], - normalizeTransaction(transaction, _booked) { - const date = - transaction.bookingDate || - transaction.bookingDateTime || - transaction.valueDate || - transaction.valueDateTime; - // If we couldn't find a valid date field we filter out this transaction - // and hope that we will import it again once the bank has processed the - // transaction further. - if (!date) { - return null; - } + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; // Creditor name is stored in additionInformation for SEB - transaction.creditorName = transaction.additionalInformation; + editedTrans.creditorName = transaction.additionalInformation; - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: d.format(d.parseISO(date), 'yyyy-MM-dd'), - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, calculateStartingBalance(sortedTransactions = [], balances = []) { diff --git a/packages/sync-server/src/app-gocardless/banks/sparnord_spnodk22.js b/packages/sync-server/src/app-gocardless/banks/sparnord_spnodk22.js index 7364d8c5640..9f5dec860c3 100644 --- a/packages/sync-server/src/app-gocardless/banks/sparnord_spnodk22.js +++ b/packages/sync-server/src/app-gocardless/banks/sparnord_spnodk22.js @@ -1,5 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; - import Fallback from './integration-bank.js'; /** @type {import('./bank.interface.js').IBank} */ @@ -15,14 +13,12 @@ export default { /** * Banks on the BEC backend only give information regarding the transaction in additionalInformation */ - normalizeTransaction(transaction, _booked) { - transaction.remittanceInformationUnstructured = + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + + editedTrans.remittanceInformationUnstructured = transaction.additionalInformation; - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate, - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/spk_karlsruhe_karsde66.js b/packages/sync-server/src/app-gocardless/banks/spk_karlsruhe_karsde66.js index c3c2f1d378f..59dda43cbe5 100644 --- a/packages/sync-server/src/app-gocardless/banks/spk_karlsruhe_karsde66.js +++ b/packages/sync-server/src/app-gocardless/banks/spk_karlsruhe_karsde66.js @@ -1,4 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; import { amountToInteger } from '../utils.js'; import Fallback from './integration-bank.js'; @@ -9,27 +8,8 @@ export default { institutionIds: ['SPK_KARLSRUHE_KARSDE66XXX'], - /** - * Following the GoCardless documentation[0] we should prefer `bookingDate` - * here, though some of their bank integrations uses the date field - * differently from what's described in their documentation and so it's - * sometimes necessary to use `valueDate` instead. - * - * [0]: https://nordigen.zendesk.com/hc/en-gb/articles/7899367372829-valueDate-and-bookingDate-for-transactions - */ - normalizeTransaction(transaction, _booked) { - const date = - transaction.bookingDate || - transaction.bookingDateTime || - transaction.valueDate || - transaction.valueDateTime; - - // If we couldn't find a valid date field we filter out this transaction - // and hope that we will import it again once the bank has processed the - // transaction further. - if (!date) { - return null; - } + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; let remittanceInformationUnstructured; @@ -54,15 +34,11 @@ export default { transaction.creditorName || transaction.debtorName; - transaction.creditorName = usefulCreditorName; - transaction.remittanceInformationUnstructured = + editedTrans.creditorName = usefulCreditorName; + editedTrans.remittanceInformationUnstructured = remittanceInformationUnstructured; - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate || transaction.valueDate, - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, /** diff --git a/packages/sync-server/src/app-gocardless/banks/spk_marburg_biedenkopf_heladef1mar.js b/packages/sync-server/src/app-gocardless/banks/spk_marburg_biedenkopf_heladef1mar.js index 9dea0dafc17..e5c5079830e 100644 --- a/packages/sync-server/src/app-gocardless/banks/spk_marburg_biedenkopf_heladef1mar.js +++ b/packages/sync-server/src/app-gocardless/banks/spk_marburg_biedenkopf_heladef1mar.js @@ -1,7 +1,3 @@ -import d from 'date-fns'; - -import { formatPayeeName } from '../../util/payee-name.js'; - import Fallback from './integration-bank.js'; /** @type {import('./bank.interface.js').IBank} */ @@ -10,19 +6,8 @@ export default { institutionIds: ['SPK_MARBURG_BIEDENKOPF_HELADEF1MAR'], - normalizeTransaction(transaction, _booked) { - const date = - transaction.bookingDate || - transaction.bookingDateTime || - transaction.valueDate || - transaction.valueDateTime; - - // If we couldn't find a valid date field we filter out this transaction - // and hope that we will import it again once the bank has processed the - // transaction further. - if (!date) { - return null; - } + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; let remittanceInformationUnstructured; @@ -37,13 +22,9 @@ export default { transaction.remittanceInformationStructuredArray?.join(' '); } - transaction.remittanceInformationUnstructured = + editedTrans.remittanceInformationUnstructured = remittanceInformationUnstructured; - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: d.format(d.parseISO(date), 'yyyy-MM-dd'), - }; + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/spk_worms_alzey_ried_malade51wor.js b/packages/sync-server/src/app-gocardless/banks/spk_worms_alzey_ried_malade51wor.js index dbc1b1f90b6..06aaea5f004 100644 --- a/packages/sync-server/src/app-gocardless/banks/spk_worms_alzey_ried_malade51wor.js +++ b/packages/sync-server/src/app-gocardless/banks/spk_worms_alzey_ried_malade51wor.js @@ -1,5 +1,3 @@ -import { formatPayeeName } from '../../util/payee-name.js'; - import Fallback from './integration-bank.js'; /** @type {import('./bank.interface.js').IBank} */ @@ -8,20 +6,14 @@ export default { institutionIds: ['SPK_WORMS_ALZEY_RIED_MALADE51WOR'], - normalizeTransaction(transaction, _booked) { - const date = transaction.bookingDate || transaction.valueDate; - if (!date) { - return null; - } + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; - transaction.remittanceInformationUnstructured = + editedTrans.remittanceInformationUnstructured = transaction.remittanceInformationUnstructured ?? transaction.remittanceInformationStructured ?? transaction.remittanceInformationStructuredArray?.join(' '); - return { - ...transaction, - payeeName: formatPayeeName(transaction), - date: transaction.bookingDate || transaction.valueDate, - }; + + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/ssk_dusseldorf_dussdeddxxx.js b/packages/sync-server/src/app-gocardless/banks/ssk_dusseldorf_dussdeddxxx.js index b4c6dc2b17a..d1e8ecc5abc 100644 --- a/packages/sync-server/src/app-gocardless/banks/ssk_dusseldorf_dussdeddxxx.js +++ b/packages/sync-server/src/app-gocardless/banks/ssk_dusseldorf_dussdeddxxx.js @@ -6,12 +6,14 @@ export default { institutionIds: ['SSK_DUSSELDORF_DUSSDEDDXXX'], - normalizeTransaction(transaction, _booked) { + normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + // If the transaction is not booked yet by the bank, don't import it. // Reason being that the transaction doesn't have the information yet // to make the payee and notes field be of any use. It's filled with // a placeholder text and wouldn't be corrected on the next sync. - if (!_booked) { + if (!booked) { console.debug( 'Skipping unbooked transaction:', transaction.transactionId, @@ -39,10 +41,10 @@ export default { transaction.creditorName || transaction.debtorName; - transaction.creditorName = usefulCreditorName; - transaction.remittanceInformationUnstructured = + editedTrans.creditorName = usefulCreditorName; + editedTrans.remittanceInformationUnstructured = remittanceInformationUnstructured; - return Fallback.normalizeTransaction(transaction, _booked); + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/swedbank_habalv22.js b/packages/sync-server/src/app-gocardless/banks/swedbank_habalv22.js index 205bf045586..e9b2283df3d 100644 --- a/packages/sync-server/src/app-gocardless/banks/swedbank_habalv22.js +++ b/packages/sync-server/src/app-gocardless/banks/swedbank_habalv22.js @@ -12,6 +12,8 @@ export default { * The actual transaction date for card transactions is only available in the remittanceInformationUnstructured field when the transaction is booked. */ normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + const isCardTransaction = transaction.remittanceInformationUnstructured?.startsWith('PIRKUMS'); @@ -23,10 +25,7 @@ export default { ); if (creditorNameMatch) { - transaction = { - ...transaction, - creditorName: creditorNameMatch[1], - }; + editedTrans.creditorName = creditorNameMatch[1]; } } @@ -35,15 +34,14 @@ export default { ); if (dateMatch) { - const extractedDate = d.parse(dateMatch[1], 'dd.MM.yyyy', new Date()); + const extractedDate = d + .parse(dateMatch[1], 'dd.MM.yyyy', new Date()) + .toISOString(); - transaction = { - ...transaction, - bookingDate: d.format(extractedDate, 'yyyy-MM-dd'), - }; + editedTrans.date = extractedDate; } } - return Fallback.normalizeTransaction(transaction, booked); + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/banks/tests/abanca_caglesmm.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/abanca_caglesmm.spec.js index bdcaa3e0f3a..54af0cc421c 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/abanca_caglesmm.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/abanca_caglesmm.spec.js @@ -9,17 +9,13 @@ describe('Abanca', () => { internalTransactionId: 'D202301180000003', transactionAmount: mockTransactionAmount, remittanceInformationStructured: 'some-creditor-name', + date: new Date().toISOString(), }; const normalizedTransaction = Abanca.normalizeTransaction( transaction, true, ); - expect(normalizedTransaction.creditorName).toEqual( - transaction.remittanceInformationStructured, - ); - expect(normalizedTransaction.debtorName).toEqual( - transaction.remittanceInformationStructured, - ); + expect(normalizedTransaction.payeeName).toEqual('Some-Creditor-Name'); }); }); }); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/abnamro_abnanl2a.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/abnamro_abnanl2a.spec.js index db93bd2c7f3..fd9b35c5a50 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/abnamro_abnanl2a.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/abnamro_abnanl2a.spec.js @@ -24,7 +24,7 @@ describe('AbnamroAbnanl2a', () => { false, ); - expect(normalizedTransaction.remittanceInformationUnstructured).toEqual( + expect(normalizedTransaction.notes).toEqual( 'BEA, Betaalpas, My Payee Name,PAS123, NR:123A4B, 09.12.23/15:43, CITY', ); expect(normalizedTransaction.payeeName).toEqual('My Payee Name'); @@ -52,7 +52,7 @@ describe('AbnamroAbnanl2a', () => { false, ); - expect(normalizedTransaction.remittanceInformationUnstructured).toEqual( + expect(normalizedTransaction.notes).toEqual( 'BEA, Google Pay, CCV*Other payee name,PAS123, NR:123A4B, 09.12.23/15:43, CITY', ); expect(normalizedTransaction.payeeName).toEqual('Other Payee Name'); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/bancsabadell_bsabesbbb.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/bancsabadell_bsabesbbb.spec.js index 1e7cb914018..9cf1949d122 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/bancsabadell_bsabesbbb.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/bancsabadell_bsabesbbb.spec.js @@ -15,10 +15,7 @@ describe('BancSabadell', () => { transaction, true, ); - expect(normalizedTransaction.creditorName).toEqual( - 'some-creditor-name', - ); - expect(normalizedTransaction.debtorName).toEqual(null); + expect(normalizedTransaction.payeeName).toEqual('Some-Creditor-Name'); }); it('creditor role - amount > 0', () => { @@ -33,8 +30,7 @@ describe('BancSabadell', () => { transaction, true, ); - expect(normalizedTransaction.debtorName).toEqual('some-debtor-name'); - expect(normalizedTransaction.creditorName).toEqual(null); + expect(normalizedTransaction.payeeName).toEqual('Some-Debtor-Name'); }); }); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/belfius_gkccbebb.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/belfius_gkccbebb.spec.js index da9d5530d6a..9a61253fd75 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/belfius_gkccbebb.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/belfius_gkccbebb.spec.js @@ -8,6 +8,7 @@ describe('Belfius', () => { transactionId: 'non-unique-id', internalTransactionId: 'D202301180000003', transactionAmount: mockTransactionAmount, + date: new Date().toISOString(), }; const normalizedTransaction = Belfius.normalizeTransaction( transaction, diff --git a/packages/sync-server/src/app-gocardless/banks/tests/cbc_cregbebb.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/cbc_cregbebb.spec.js index 4114ac4944f..21d248a85d2 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/cbc_cregbebb.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/cbc_cregbebb.spec.js @@ -7,6 +7,7 @@ describe('cbc_cregbebb', () => { remittanceInformationUnstructured: 'ONKART FR Viry Paiement Maestro par Carte de débit CBC 05-09-2024 à 15.43 heures 6703 19XX XXXX X201 5 JOHN DOE', transactionAmount: { amount: '-45.00', currency: 'EUR' }, + date: new Date().toISOString(), }; const normalizedTransaction = CBCcregbebb.normalizeTransaction( transaction, @@ -21,6 +22,7 @@ describe('cbc_cregbebb', () => { remittanceInformationUnstructured: 'ONKART FR Viry Paiement Maestro par Carte de débit CBC 05-09-2024 à 15.43 heures 6703 19XX XXXX X201 5 JOHN DOE', transactionAmount: { amount: '10.99', currency: 'EUR' }, + date: new Date().toISOString(), }; const normalizedTransaction = CBCcregbebb.normalizeTransaction( transaction, diff --git a/packages/sync-server/src/app-gocardless/banks/tests/commerzbank_cobadeff.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/commerzbank_cobadeff.spec.js index 9667ce22609..fe3a161f682 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/commerzbank_cobadeff.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/commerzbank_cobadeff.spec.js @@ -28,7 +28,7 @@ describe('CommerzbankCobadeff', () => { transaction, false, ); - expect(normalizedTransaction.remittanceInformationUnstructured).toEqual( + expect(normalizedTransaction.notes).toEqual( '2024-12-19T15:34:31 KFN 1 AB 1234, Kartenzahlung', ); }); @@ -66,7 +66,7 @@ describe('CommerzbankCobadeff', () => { transaction, false, ); - expect(normalizedTransaction.remittanceInformationUnstructured).toEqual( + expect(normalizedTransaction.notes).toEqual( '901234567890/. Long description tha t gets cut and is very long, did I mention it is long, End-to-End-Ref.: 901234567890, Mandatsref: ABC123DEF456, Gläubiger-ID: AB12CDE0000000000000000012, SEPA-BASISLASTSCHRIFT wiederholend', ); }); @@ -102,7 +102,7 @@ describe('CommerzbankCobadeff', () => { transaction, false, ); - expect(normalizedTransaction.remittanceInformationUnstructured).toEqual( + expect(normalizedTransaction.notes).toEqual( 'CREDITOR00BIC CREDITOR000IBAN DESCRIPTION, Dauerauftrag', ); }); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/fortuneo_ftnofrp1xxx.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/fortuneo_ftnofrp1xxx.spec.js index d4a1b30ff74..a19dd3b5609 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/fortuneo_ftnofrp1xxx.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/fortuneo_ftnofrp1xxx.spec.js @@ -140,14 +140,12 @@ describe('Fortuneo', () => { true, ); - expect(normalizedCreditorTransaction.creditorName).toBeDefined(); - expect(normalizedCreditorTransaction.debtorName).toBeNull(); + expect(normalizedCreditorTransaction.payeeName).toBeDefined(); expect( parseFloat(normalizedCreditorTransaction.transactionAmount.amount), ).toBeLessThan(0); - expect(normalizedDebtorTransaction.debtorName).toBeDefined(); - expect(normalizedDebtorTransaction.creditorName).toBeNull(); + expect(normalizedDebtorTransaction.payeeName).toBeDefined(); expect( parseFloat(normalizedDebtorTransaction.transactionAmount.amount), ).toBeGreaterThan(0); @@ -160,7 +158,7 @@ describe('Fortuneo', () => { true, ); - expect(normalizedTransaction.creditorName).toBe('ONG'); + expect(normalizedTransaction.payeeName).toBe('Ong'); const transaction2 = transactionsRaw[2]; const normalizedTransaction2 = Fortuneo.normalizeTransaction( @@ -168,7 +166,7 @@ describe('Fortuneo', () => { true, ); - expect(normalizedTransaction2.creditorName).toBe('XXXYYYYZZZ'); + expect(normalizedTransaction2.payeeName).toBe('Xxxyyyyzzz'); const transaction3 = transactionsRaw[3]; const normalizedTransaction3 = Fortuneo.normalizeTransaction( @@ -176,9 +174,7 @@ describe('Fortuneo', () => { true, ); - expect(normalizedTransaction3.creditorName).toBe( - 'Google Payment I Dublin', - ); + expect(normalizedTransaction3.payeeName).toBe('Google Payment I Dublin'); const transaction4 = transactionsRaw[4]; const normalizedTransaction4 = Fortuneo.normalizeTransaction( @@ -186,7 +182,7 @@ describe('Fortuneo', () => { true, ); - expect(normalizedTransaction4.creditorName).toBe('SPORT MARKET'); + expect(normalizedTransaction4.payeeName).toBe('Sport Market'); const transaction5 = transactionsRaw[5]; const normalizedTransaction5 = Fortuneo.normalizeTransaction( @@ -194,7 +190,7 @@ describe('Fortuneo', () => { true, ); - expect(normalizedTransaction5.debtorName).toBe('WEEZEVENT SOMEPLACE'); + expect(normalizedTransaction5.payeeName).toBe('Weezevent Someplace'); const transaction7 = transactionsRaw[7]; const normalizedTransaction7 = Fortuneo.normalizeTransaction( @@ -202,8 +198,8 @@ describe('Fortuneo', () => { true, ); - expect(normalizedTransaction7.creditorName).toBe( - 'Leclerc XXXX Leclerc XXXX 44321IXCRT211141232', + expect(normalizedTransaction7.payeeName).toBe( + 'Leclerc Xxxx Leclerc Xxxx 44321ixcrt211141232', ); }); }); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/kbc_kredbebb.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/kbc_kredbebb.spec.js index 21e67dcd36a..e3326a12af7 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/kbc_kredbebb.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/kbc_kredbebb.spec.js @@ -7,6 +7,7 @@ describe('kbc_kredbebb', () => { remittanceInformationUnstructured: 'CARREFOUR ST GIL BE1060 BRUXELLES Betaling met Google Pay via Debit Mastercard 28-08-2024 om 19.15 uur 5127 04XX XXXX 1637 5853 98XX XXXX 2266 JOHN SMITH', transactionAmount: { amount: '-10.99', currency: 'EUR' }, + date: new Date().toISOString(), }; const normalizedTransaction = KBCkredbebb.normalizeTransaction( transaction, @@ -23,6 +24,7 @@ describe('kbc_kredbebb', () => { remittanceInformationUnstructured: 'CARREFOUR ST GIL BE1060 BRUXELLES Betaling met Google Pay via Debit Mastercard 28-08-2024 om 19.15 uur 5127 04XX XXXX 1637 5853 98XX XXXX 2266 JOHN SMITH', transactionAmount: { amount: '10.99', currency: 'EUR' }, + date: new Date().toISOString(), }; const normalizedTransaction = KBCkredbebb.normalizeTransaction( transaction, diff --git a/packages/sync-server/src/app-gocardless/banks/tests/lhv-lhvbee22.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/lhv-lhvbee22.spec.js index 72e61fb384a..0f40c49d741 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/lhv-lhvbee22.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/lhv-lhvbee22.spec.js @@ -18,16 +18,11 @@ describe('#normalizeTransaction', () => { it('extracts booked card transaction creditor name', () => { expect( - LhvLhvbee22.normalizeTransaction(bookedCardTransaction, true) - .creditorName, + LhvLhvbee22.normalizeTransaction(bookedCardTransaction, true).payeeName, ).toEqual('CrustumOU'); }); it('extracts booked card transaction date', () => { - expect( - LhvLhvbee22.normalizeTransaction(bookedCardTransaction, true).bookingDate, - ).toEqual('2025-01-02'); - expect( LhvLhvbee22.normalizeTransaction(bookedCardTransaction, true).date, ).toEqual('2025-01-02'); @@ -45,7 +40,6 @@ describe('#normalizeTransaction', () => { }; const normalized = LhvLhvbee22.normalizeTransaction(transaction, true); - expect(normalized.bookingDate).toEqual('2025-01-03'); expect(normalized.date).toEqual('2025-01-03'); }); @@ -62,17 +56,11 @@ describe('#normalizeTransaction', () => { it('extracts pending card transaction creditor name', () => { expect( - LhvLhvbee22.normalizeTransaction(pendingCardTransaction, false) - .creditorName, + LhvLhvbee22.normalizeTransaction(pendingCardTransaction, false).payeeName, ).toEqual('CrustumOU'); }); it('extracts pending card transaction date', () => { - expect( - LhvLhvbee22.normalizeTransaction(pendingCardTransaction, false) - .bookingDate, - ).toEqual(undefined); - expect( LhvLhvbee22.normalizeTransaction(pendingCardTransaction, false).date, ).toEqual('2025-01-03'); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/revolut_revolt21.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/revolut_revolt21.spec.js index 40e8bef752d..c48e3447b99 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/revolut_revolt21.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/revolut_revolt21.spec.js @@ -17,9 +17,7 @@ describe('RevolutRevolt21', () => { true, ); - expect(normalizedTransaction.remittanceInformationUnstructured).toEqual( - 'Bizum description', - ); + expect(normalizedTransaction.notes).toEqual('Bizum description'); }); }); @@ -39,8 +37,6 @@ describe('RevolutRevolt21', () => { ); expect(normalizedTransaction.payeeName).toEqual('DEBTOR NAME'); - expect(normalizedTransaction.remittanceInformationUnstructured).toEqual( - 'Bizum description', - ); + expect(normalizedTransaction.notes).toEqual('Bizum description'); }); }); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/spk_marburg_biedenkopf_heladef1mar.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/spk_marburg_biedenkopf_heladef1mar.spec.js index 87b51c9a03a..62b4ee6f655 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/spk_marburg_biedenkopf_heladef1mar.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/spk_marburg_biedenkopf_heladef1mar.spec.js @@ -148,7 +148,7 @@ describe('SpkMarburgBiedenkopfHeladef1mar', () => { expect( SpkMarburgBiedenkopfHeladef1mar.normalizeTransaction(transaction, true) - .remittanceInformationUnstructured, + .notes, ).toEqual('AUTORISATION 28.12. 18:30'); }); @@ -172,7 +172,7 @@ describe('SpkMarburgBiedenkopfHeladef1mar', () => { expect( SpkMarburgBiedenkopfHeladef1mar.normalizeTransaction(transaction, true) - .remittanceInformationUnstructured, + .notes, ).toEqual('Entgeltabrechnung siehe Anlage'); }); }); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/ssk_dusseldorf_dussdeddxxx.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/ssk_dusseldorf_dussdeddxxx.spec.js index 0781c899323..68bf9299762 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/ssk_dusseldorf_dussdeddxxx.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/ssk_dusseldorf_dussdeddxxx.spec.js @@ -47,14 +47,14 @@ describe('ssk_dusseldorf_dussdeddxxx', () => { SskDusseldorfDussdeddxxx.normalizeTransaction( bookedTransactionOne, true, - ).remittanceInformationUnstructured, + ).notes, ).toEqual('unstructured information some additional information'); expect( SskDusseldorfDussdeddxxx.normalizeTransaction( bookedTransactionTwo, true, - ).remittanceInformationUnstructured, + ).notes, ).toEqual('structured information some additional information'); }); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/swedbank_habalv22.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/swedbank_habalv22.spec.js index 31673a4eb33..7fa3c0ad79f 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/swedbank_habalv22.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/swedbank_habalv22.spec.js @@ -17,11 +17,6 @@ describe('#normalizeTransaction', () => { }; it('extracts card transaction date', () => { - expect( - SwedbankHabaLV22.normalizeTransaction(bookedCardTransaction, true) - .bookingDate, - ).toEqual('2024-10-28'); - expect( SwedbankHabaLV22.normalizeTransaction(bookedCardTransaction, true).date, ).toEqual('2024-10-28'); @@ -56,7 +51,7 @@ describe('#normalizeTransaction', () => { it('extracts pending card transaction creditor name', () => { expect( SwedbankHabaLV22.normalizeTransaction(pendingCardTransaction, false) - .creditorName, - ).toEqual('SOME CREDITOR NAME'); + .payeeName, + ).toEqual('Some Creditor Name'); }); }); diff --git a/packages/sync-server/src/app-gocardless/banks/tests/virgin_nrnbgb22.spec.js b/packages/sync-server/src/app-gocardless/banks/tests/virgin_nrnbgb22.spec.js index e4f6f52d655..902a221564a 100644 --- a/packages/sync-server/src/app-gocardless/banks/tests/virgin_nrnbgb22.spec.js +++ b/packages/sync-server/src/app-gocardless/banks/tests/virgin_nrnbgb22.spec.js @@ -15,13 +15,8 @@ describe('Virgin', () => { true, ); - expect(normalizedTransaction.creditorName).toEqual( - 'DIRECT DEBIT PAYMENT', - ); - expect(normalizedTransaction.debtorName).toEqual('DIRECT DEBIT PAYMENT'); - expect(normalizedTransaction.remittanceInformationUnstructured).toEqual( - 'DIRECT DEBIT PAYMENT', - ); + expect(normalizedTransaction.payeeName).toEqual('Direct Debit Payment'); + expect(normalizedTransaction.notes).toEqual('DIRECT DEBIT PAYMENT'); }); it('formats bank transfer payee and references', () => { @@ -36,11 +31,8 @@ describe('Virgin', () => { true, ); - expect(normalizedTransaction.creditorName).toEqual('Joe Bloggs'); - expect(normalizedTransaction.debtorName).toEqual('Joe Bloggs'); - expect(normalizedTransaction.remittanceInformationUnstructured).toEqual( - 'Food', - ); + expect(normalizedTransaction.payeeName).toEqual('Joe Bloggs'); + expect(normalizedTransaction.notes).toEqual('Food'); }); it('removes method information from payee name', () => { @@ -55,11 +47,8 @@ describe('Virgin', () => { true, ); - expect(normalizedTransaction.creditorName).toEqual('Tesco Express'); - expect(normalizedTransaction.debtorName).toEqual('Tesco Express'); - expect(normalizedTransaction.remittanceInformationUnstructured).toEqual( - 'Card 99, Tesco Express', - ); + expect(normalizedTransaction.payeeName).toEqual('Tesco Express'); + expect(normalizedTransaction.notes).toEqual('Card 99, Tesco Express'); }); }); }); diff --git a/packages/sync-server/src/app-gocardless/banks/virgin_nrnbgb22.js b/packages/sync-server/src/app-gocardless/banks/virgin_nrnbgb22.js index 78418bc2dfe..7529c6d4cf0 100644 --- a/packages/sync-server/src/app-gocardless/banks/virgin_nrnbgb22.js +++ b/packages/sync-server/src/app-gocardless/banks/virgin_nrnbgb22.js @@ -7,6 +7,8 @@ export default { institutionIds: ['VIRGIN_NRNBGB22'], normalizeTransaction(transaction, booked) { + const editedTrans = { ...transaction }; + const transferPrefixes = ['MOB', 'FPS']; const methodRegex = /^(Card|WLT)\s\d+/; @@ -17,21 +19,21 @@ export default { // the second field contains the payee and the third contains the // reference - transaction.creditorName = parts[1]; - transaction.debtorName = parts[1]; - transaction.remittanceInformationUnstructured = parts[2]; + editedTrans.creditorName = parts[1]; + editedTrans.debtorName = parts[1]; + editedTrans.remittanceInformationUnstructured = parts[2]; } else if (parts[0].match(methodRegex)) { // The payee is prefixed with the payment method, eg "Card 11, {payee}" - transaction.creditorName = parts[1]; - transaction.debtorName = parts[1]; + editedTrans.creditorName = parts[1]; + editedTrans.debtorName = parts[1]; } else { // Simple payee name - transaction.creditorName = transaction.remittanceInformationUnstructured; - transaction.debtorName = transaction.remittanceInformationUnstructured; + editedTrans.creditorName = transaction.remittanceInformationUnstructured; + editedTrans.debtorName = transaction.remittanceInformationUnstructured; } - return Fallback.normalizeTransaction(transaction, booked); + return Fallback.normalizeTransaction(transaction, booked, editedTrans); }, }; diff --git a/packages/sync-server/src/app-gocardless/services/tests/fixtures.js b/packages/sync-server/src/app-gocardless/services/tests/fixtures.js index 40c34501a32..aecf48b695a 100644 --- a/packages/sync-server/src/app-gocardless/services/tests/fixtures.js +++ b/packages/sync-server/src/app-gocardless/services/tests/fixtures.js @@ -35,8 +35,8 @@ export const mockTransactions = { amount: '328.18', }, bankTransactionCode: 'string', - bookingDate: 'date', - valueDate: 'date', + bookingDate: '2000-01-01', + valueDate: '2000-01-01', }, { transactionId: 'string', @@ -45,8 +45,8 @@ export const mockTransactions = { amount: '947.26', }, bankTransactionCode: 'string', - bookingDate: 'date', - valueDate: 'date', + bookingDate: '2000-01-01', + valueDate: '2000-01-01', }, ], pending: [ @@ -55,7 +55,7 @@ export const mockTransactions = { currency: 'EUR', amount: '947.26', }, - valueDate: 'date', + valueDate: '2000-01-01', }, ], }, diff --git a/packages/sync-server/src/app-gocardless/services/tests/gocardless-service.spec.js b/packages/sync-server/src/app-gocardless/services/tests/gocardless-service.spec.js index a96d765a1b5..3ba22679881 100644 --- a/packages/sync-server/src/app-gocardless/services/tests/gocardless-service.spec.js +++ b/packages/sync-server/src/app-gocardless/services/tests/gocardless-service.spec.js @@ -417,42 +417,51 @@ describe('goCardlessService', () => { "booked": [ { "bankTransactionCode": "string", - "bookingDate": "date", - "date": "date", + "bookingDate": "2000-01-01", + "date": "2000-01-01", "debtorAccount": { "iban": "string", }, "debtorName": "string", + "notes": undefined, "payeeName": "String (stri XXX ring)", + "remittanceInformationStructuredArrayString": undefined, + "remittanceInformationUnstructuredArrayString": undefined, "transactionAmount": { "amount": "328.18", "currency": "EUR", }, "transactionId": "string", - "valueDate": "date", + "valueDate": "2000-01-01", }, { "bankTransactionCode": "string", - "bookingDate": "date", - "date": "date", + "bookingDate": "2000-01-01", + "date": "2000-01-01", + "notes": undefined, "payeeName": "", + "remittanceInformationStructuredArrayString": undefined, + "remittanceInformationUnstructuredArrayString": undefined, "transactionAmount": { "amount": "947.26", "currency": "EUR", }, "transactionId": "string", - "valueDate": "date", + "valueDate": "2000-01-01", }, ], "pending": [ { - "date": "date", + "date": "2000-01-01", + "notes": undefined, "payeeName": "", + "remittanceInformationStructuredArrayString": undefined, + "remittanceInformationUnstructuredArrayString": undefined, "transactionAmount": { "amount": "947.26", "currency": "EUR", }, - "valueDate": "date", + "valueDate": "2000-01-01", }, ], }, diff --git a/packages/sync-server/src/app-simplefin/app-simplefin.js b/packages/sync-server/src/app-simplefin/app-simplefin.js index 9885a962317..c6f6fdb9cb9 100644 --- a/packages/sync-server/src/app-simplefin/app-simplefin.js +++ b/packages/sync-server/src/app-simplefin/app-simplefin.js @@ -222,11 +222,19 @@ function getAccountResponse(results, accountId, startDate) { newTrans.sortOrder = dateToUse; newTrans.date = getDate(transactionDate); newTrans.payeeName = trans.payee; - newTrans.remittanceInformationUnstructured = trans.description; + newTrans.notes = trans.description; newTrans.transactionAmount = { amount: trans.amount, currency: 'USD' }; newTrans.transactionId = trans.id; newTrans.valueDate = newTrans.bookingDate; + if (trans.transacted_at) { + newTrans.transactedDate = getDate(new Date(trans.transacted_at * 1000)); + } + + if (trans.posted) { + newTrans.postedDate = getDate(new Date(trans.posted * 1000)); + } + if (newTrans.booked) { booked.push(newTrans); } else { diff --git a/packages/sync-server/src/app.js b/packages/sync-server/src/app.js index 78bde184e5f..73c258bb705 100644 --- a/packages/sync-server/src/app.js +++ b/packages/sync-server/src/app.js @@ -24,14 +24,17 @@ process.on('unhandledRejection', reason => { app.disable('x-powered-by'); app.use(cors()); app.set('trust proxy', config.trustedProxies); -app.use( - rateLimit({ - windowMs: 60 * 1000, - max: 500, - legacyHeaders: false, - standardHeaders: true, - }), -); +if (process.env.NODE_ENV !== 'development') { + app.use( + rateLimit({ + windowMs: 60 * 1000, + max: 500, + legacyHeaders: false, + standardHeaders: true, + }), + ); +} + app.use(bodyParser.json({ limit: `${config.upload.fileSizeLimitMB}mb` })); app.use( bodyParser.raw({ @@ -67,9 +70,28 @@ app.use((req, res, next) => { res.set('Cross-Origin-Embedder-Policy', 'require-corp'); next(); }); -app.use(express.static(config.webRoot, { index: false })); +if (process.env.NODE_ENV === 'development') { + console.log( + 'Running in development mode - Proxying frontend routes to React Dev Server', + ); + + // Imported within Dev block to allow dev dependency in package.json (reduces package size in production) + const httpProxyMiddleware = await import('http-proxy-middleware'); -app.get('/*', (req, res) => res.sendFile(config.webRoot + '/index.html')); + app.use( + httpProxyMiddleware.createProxyMiddleware({ + target: 'http://localhost:3001', + changeOrigin: true, + ws: true, + logLevel: 'debug', + }), + ); +} else { + console.log('Running in production mode - Serving static React app'); + + app.use(express.static(config.webRoot, { index: false })); + app.get('/*', (req, res) => res.sendFile(config.webRoot + '/index.html')); +} function parseHTTPSConfig(value) { if (value.startsWith('-----BEGIN')) { diff --git a/sync-server.Dockerfile b/sync-server.Dockerfile index 9d83bf40526..eb17eeef7d0 100644 --- a/sync-server.Dockerfile +++ b/sync-server.Dockerfile @@ -3,7 +3,7 @@ RUN apt-get update && apt-get install -y openssl WORKDIR /app COPY .yarn ./.yarn COPY yarn.lock packages/sync-server/package.json .yarnrc.yml ./ -RUN yarn workspaces focus actual-sync --production +RUN yarn workspaces focus @actual-app/sync-server --production FROM node:18-bookworm-slim as prod RUN apt-get update && apt-get install tini && apt-get clean -y && rm -rf /var/lib/apt/lists/* diff --git a/upcoming-release-notes/4145.md b/upcoming-release-notes/4145.md new file mode 100644 index 00000000000..d8f8660e424 --- /dev/null +++ b/upcoming-release-notes/4145.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [jfdoming] +--- + +Fix types of `send` function diff --git a/upcoming-release-notes/4213.md b/upcoming-release-notes/4213.md new file mode 100644 index 00000000000..74d4924cbd8 --- /dev/null +++ b/upcoming-release-notes/4213.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [joel-jeremy] +--- + +useDisplayPayee hook to unify payee names in mobile and desktop. diff --git a/upcoming-release-notes/4218.md b/upcoming-release-notes/4218.md new file mode 100644 index 00000000000..e0128c7b0fe --- /dev/null +++ b/upcoming-release-notes/4218.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [joel-jeremy] +--- + +Convert playwright page models to TypeScript. diff --git a/upcoming-release-notes/4221.md b/upcoming-release-notes/4221.md new file mode 100644 index 00000000000..a1e71ca1831 --- /dev/null +++ b/upcoming-release-notes/4221.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [joel-jeremy] +--- + +Move transactions related server handlers from main.ts to server/transactions/app.ts diff --git a/upcoming-release-notes/4247.md b/upcoming-release-notes/4247.md new file mode 100644 index 00000000000..ca01ecc5b5b --- /dev/null +++ b/upcoming-release-notes/4247.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [joel-jeremy] +--- + +[TypeScript] Make `runQuery` generic to make it easy to type DB query results. \ No newline at end of file diff --git a/upcoming-release-notes/4253.md b/upcoming-release-notes/4253.md new file mode 100644 index 00000000000..2c2f82b61cc --- /dev/null +++ b/upcoming-release-notes/4253.md @@ -0,0 +1,6 @@ +--- +category: Features +authors: [matt-fidd] +--- + +Add a UI for bank sync settings diff --git a/upcoming-release-notes/4257.md b/upcoming-release-notes/4257.md new file mode 100644 index 00000000000..27615aac638 --- /dev/null +++ b/upcoming-release-notes/4257.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [MattFaz] +--- + +Add percentage adjustments to schedule templates diff --git a/upcoming-release-notes/4258.md b/upcoming-release-notes/4258.md new file mode 100644 index 00000000000..f5e11b5edc5 --- /dev/null +++ b/upcoming-release-notes/4258.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [joel-jeremy] +--- + +Fix react-hooks/exhaustive-deps error on useSelected.tsx diff --git a/upcoming-release-notes/4259.md b/upcoming-release-notes/4259.md new file mode 100644 index 00000000000..4eaa1c56838 --- /dev/null +++ b/upcoming-release-notes/4259.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [joel-jeremy] +--- + +Fix react-hooks/exhaustive-deps error on useProperFocus.tsx diff --git a/upcoming-release-notes/4260.md b/upcoming-release-notes/4260.md new file mode 100644 index 00000000000..7b0484bf242 --- /dev/null +++ b/upcoming-release-notes/4260.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [joel-jeremy] +--- + +Fix react-hooks/exhaustive-deps error on usePayees.ts diff --git a/upcoming-release-notes/4261.md b/upcoming-release-notes/4261.md new file mode 100644 index 00000000000..945197c8744 --- /dev/null +++ b/upcoming-release-notes/4261.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [joel-jeremy] +--- + +Fix react-hooks/exhaustive-deps error on useCategories.ts diff --git a/upcoming-release-notes/4262.md b/upcoming-release-notes/4262.md new file mode 100644 index 00000000000..27e071d7e31 --- /dev/null +++ b/upcoming-release-notes/4262.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [joel-jeremy] +--- + +Fix react-hooks/exhaustive-deps error on useAccounts.ts diff --git a/upcoming-release-notes/4268.md b/upcoming-release-notes/4268.md new file mode 100644 index 00000000000..cfd3c8b789b --- /dev/null +++ b/upcoming-release-notes/4268.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [joel-jeremy] +--- + +Fix react-hooks/exhaustive-deps error on TransactionsTable.jsx diff --git a/upcoming-release-notes/4273.md b/upcoming-release-notes/4273.md new file mode 100644 index 00000000000..179d7948c51 --- /dev/null +++ b/upcoming-release-notes/4273.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [joel-jeremy] +--- + +Fix react-hooks/exhaustive-deps error on Titlebar.tsx diff --git a/upcoming-release-notes/4274.md b/upcoming-release-notes/4274.md new file mode 100644 index 00000000000..6c0a4e0d997 --- /dev/null +++ b/upcoming-release-notes/4274.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [joel-jeremy] +--- + +Fix react-hooks/exhaustive-deps error on table.tsx diff --git a/upcoming-release-notes/4308.md b/upcoming-release-notes/4308.md new file mode 100644 index 00000000000..a94dab0b95a --- /dev/null +++ b/upcoming-release-notes/4308.md @@ -0,0 +1,6 @@ +--- +category: Features +authors: [jfdoming] +--- + +Foundations for the budget automations UI diff --git a/upcoming-release-notes/4343.md b/upcoming-release-notes/4343.md new file mode 100644 index 00000000000..6d1e85ae9fe --- /dev/null +++ b/upcoming-release-notes/4343.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [matt-fidd] +--- + +Rename migrations to realign with the convention diff --git a/upcoming-release-notes/4369.md b/upcoming-release-notes/4369.md new file mode 100644 index 00000000000..d4fbd673ffc --- /dev/null +++ b/upcoming-release-notes/4369.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [MikesGlitch] +--- + +Adding typescript type for the global-store.json setting file diff --git a/upcoming-release-notes/4370.md b/upcoming-release-notes/4370.md new file mode 100644 index 00000000000..f6b84764348 --- /dev/null +++ b/upcoming-release-notes/4370.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [MikesGlitch] +--- + +Update package name of sync server to be consistent with the rest of the workspace diff --git a/upcoming-release-notes/4372.md b/upcoming-release-notes/4372.md new file mode 100644 index 00000000000..fe0bd325863 --- /dev/null +++ b/upcoming-release-notes/4372.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [lelemm] +--- + +Sync server development mode with react fast refresh diff --git a/upcoming-release-notes/4375.md b/upcoming-release-notes/4375.md new file mode 100644 index 00000000000..6bc9b82912c --- /dev/null +++ b/upcoming-release-notes/4375.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [MikesGlitch] +--- + +Prevent the app getting stuck on the "Initializing the connection to the local database..." screen diff --git a/upcoming-release-notes/4380.md b/upcoming-release-notes/4380.md new file mode 100644 index 00000000000..e9af3ecd558 --- /dev/null +++ b/upcoming-release-notes/4380.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [adastx] +--- + +Add button to go to current month in budget view on mobile diff --git a/upcoming-release-notes/4382.md b/upcoming-release-notes/4382.md new file mode 100644 index 00000000000..d16fc10044e --- /dev/null +++ b/upcoming-release-notes/4382.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [langelgjm] +--- + +Ignore CSV inOutMode during OFX imports diff --git a/upcoming-release-notes/4383.md b/upcoming-release-notes/4383.md new file mode 100644 index 00000000000..8736e493857 --- /dev/null +++ b/upcoming-release-notes/4383.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [AntoineTA] +--- + +Ensure decimal separator is recognized independantly of the configured number format. \ No newline at end of file diff --git a/upcoming-release-notes/4384.md b/upcoming-release-notes/4384.md new file mode 100644 index 00000000000..f020cbf1784 --- /dev/null +++ b/upcoming-release-notes/4384.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [MMichotte] +--- + +Provides a default fallback payeename value ('undefined') for the CBC bank in case the payeename is missing. diff --git a/upcoming-release-notes/4385.md b/upcoming-release-notes/4385.md new file mode 100644 index 00000000000..753e89eeab0 --- /dev/null +++ b/upcoming-release-notes/4385.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [AlbertoCortina] +--- + +Remove deprecated components in favour of components from @actual-app library diff --git a/upcoming-release-notes/4388.md b/upcoming-release-notes/4388.md new file mode 100644 index 00000000000..4e5780b5752 --- /dev/null +++ b/upcoming-release-notes/4388.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [michalgolab] +--- + +Add BANK_MILLENNIUM_BIGBPLPW, BNP_PL_PPABPLPK, MBANK_RETAIL_BREXPLPW to BANKS_WITH_LIMITED_HISTORY constant. diff --git a/upcoming-release-notes/4397.md b/upcoming-release-notes/4397.md new file mode 100644 index 00000000000..15200dde8b4 --- /dev/null +++ b/upcoming-release-notes/4397.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [lelemm] +--- + +Fix for User directory page that was calling the api every frame diff --git a/upcoming-release-notes/4400.md b/upcoming-release-notes/4400.md new file mode 100644 index 00000000000..aca1e2b4435 --- /dev/null +++ b/upcoming-release-notes/4400.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [MikesGlitch] +--- + +Fix new proxy middleware importing in production when only required in developement diff --git a/upcoming-release-notes/4403.md b/upcoming-release-notes/4403.md new file mode 100644 index 00000000000..b57e3f0e466 --- /dev/null +++ b/upcoming-release-notes/4403.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [matt-fidd] +--- + +Update bank sync mapping data for existing transactions on sync diff --git a/upcoming-release-notes/4408.md b/upcoming-release-notes/4408.md new file mode 100644 index 00000000000..7089479f72e --- /dev/null +++ b/upcoming-release-notes/4408.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [lelemm] +--- + +Extending translations for components: Account, SidebarCategory, TotalsList, MobileNavTabs, AccountTransactions (Mobile), Accounts (Mobile), BudgetTable (Mobile), TransactionEdit (Mobile), TransactionList (Mobile), TransactionListItem (Mobile), CategoryMenuModal, CreateLocalAccountModal, ImportModal, ReportSideBar, CustomReport, Spending, Export, TransactionsTable diff --git a/upcoming-release-notes/4417.md b/upcoming-release-notes/4417.md new file mode 100644 index 00000000000..275a004e6e1 --- /dev/null +++ b/upcoming-release-notes/4417.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [lelemm] +--- + +Fix `On budget` / `Off budget` underline with translated languages diff --git a/yarn.lock b/yarn.lock index 925228ba6b6..0c6ecd67aee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -79,6 +79,52 @@ __metadata: languageName: unknown linkType: soft +"@actual-app/sync-server@workspace:packages/sync-server": + version: 0.0.0-use.local + resolution: "@actual-app/sync-server@workspace:packages/sync-server" + dependencies: + "@actual-app/crdt": "npm:2.1.0" + "@actual-app/web": "npm:25.2.1" + "@babel/preset-typescript": "npm:^7.20.2" + "@types/bcrypt": "npm:^5.0.2" + "@types/better-sqlite3": "npm:^7.6.12" + "@types/cors": "npm:^2.8.13" + "@types/express": "npm:^4.17.17" + "@types/express-actuator": "npm:^1.8.0" + "@types/jest": "npm:^29.2.3" + "@types/node": "npm:^17.0.45" + "@types/supertest": "npm:^2.0.12" + "@types/uuid": "npm:^9.0.0" + "@typescript-eslint/eslint-plugin": "npm:^5.51.0" + "@typescript-eslint/parser": "npm:^5.51.0" + bcrypt: "npm:^5.1.1" + better-sqlite3: "npm:^11.7.0" + body-parser: "npm:^1.20.3" + cors: "npm:^2.8.5" + date-fns: "npm:^2.30.0" + debug: "npm:^4.3.4" + eslint: "npm:^8.33.0" + eslint-plugin-prettier: "npm:^4.2.1" + express: "npm:4.20.0" + express-actuator: "npm:1.8.4" + express-rate-limit: "npm:^6.7.0" + express-response-size: "npm:^0.0.3" + express-winston: "npm:^4.2.0" + http-proxy-middleware: "npm:^3.0.3" + jest: "npm:^29.3.1" + jws: "npm:^4.0.0" + migrate: "npm:^2.0.1" + nodemon: "npm:^3.1.9" + nordigen-node: "npm:^1.4.0" + openid-client: "npm:^5.4.2" + prettier: "npm:^2.8.3" + supertest: "npm:^6.3.1" + typescript: "npm:^4.9.5" + uuid: "npm:^9.0.0" + winston: "npm:^3.14.2" + languageName: unknown + linkType: soft + "@actual-app/web@npm:25.2.1": version: 25.2.1 resolution: "@actual-app/web@npm:25.2.1" @@ -6319,6 +6365,15 @@ __metadata: languageName: node linkType: hard +"@types/http-proxy@npm:^1.17.15": + version: 1.17.16 + resolution: "@types/http-proxy@npm:1.17.16" + dependencies: + "@types/node": "npm:*" + checksum: 10/a054ac8f5301acfcfdcec3a775f52dc371180bbe60037906534312f10cceb3799b4a16e46c56c22f9925d078e11dcda1723c38f1ddd124be8169a4cccca69c8c + languageName: node + linkType: hard + "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": version: 2.0.4 resolution: "@types/istanbul-lib-coverage@npm:2.0.4" @@ -7485,50 +7540,6 @@ __metadata: languageName: node linkType: hard -"actual-sync@workspace:packages/sync-server": - version: 0.0.0-use.local - resolution: "actual-sync@workspace:packages/sync-server" - dependencies: - "@actual-app/crdt": "npm:2.1.0" - "@actual-app/web": "npm:25.2.1" - "@babel/preset-typescript": "npm:^7.20.2" - "@types/bcrypt": "npm:^5.0.2" - "@types/better-sqlite3": "npm:^7.6.12" - "@types/cors": "npm:^2.8.13" - "@types/express": "npm:^4.17.17" - "@types/express-actuator": "npm:^1.8.0" - "@types/jest": "npm:^29.2.3" - "@types/node": "npm:^17.0.45" - "@types/supertest": "npm:^2.0.12" - "@types/uuid": "npm:^9.0.0" - "@typescript-eslint/eslint-plugin": "npm:^5.51.0" - "@typescript-eslint/parser": "npm:^5.51.0" - bcrypt: "npm:^5.1.1" - better-sqlite3: "npm:^11.7.0" - body-parser: "npm:^1.20.3" - cors: "npm:^2.8.5" - date-fns: "npm:^2.30.0" - debug: "npm:^4.3.4" - eslint: "npm:^8.33.0" - eslint-plugin-prettier: "npm:^4.2.1" - express: "npm:4.20.0" - express-actuator: "npm:1.8.4" - express-rate-limit: "npm:^6.7.0" - express-response-size: "npm:^0.0.3" - express-winston: "npm:^4.2.0" - jest: "npm:^29.3.1" - jws: "npm:^4.0.0" - migrate: "npm:^2.0.1" - nordigen-node: "npm:^1.4.0" - openid-client: "npm:^5.4.2" - prettier: "npm:^2.8.3" - supertest: "npm:^6.3.1" - typescript: "npm:^4.9.5" - uuid: "npm:^9.0.0" - winston: "npm:^3.14.2" - languageName: unknown - linkType: soft - "actual@workspace:.": version: 0.0.0-use.local resolution: "actual@workspace:." @@ -8935,6 +8946,25 @@ __metadata: languageName: node linkType: hard +"chokidar@npm:^3.5.2": + version: 3.6.0 + resolution: "chokidar@npm:3.6.0" + dependencies: + anymatch: "npm:~3.1.2" + braces: "npm:~3.0.2" + fsevents: "npm:~2.3.2" + glob-parent: "npm:~5.1.2" + is-binary-path: "npm:~2.1.0" + is-glob: "npm:~4.0.1" + normalize-path: "npm:~3.0.0" + readdirp: "npm:~3.6.0" + dependenciesMeta: + fsevents: + optional: true + checksum: 10/c327fb07704443f8d15f7b4a7ce93b2f0bc0e6cea07ec28a7570aa22cd51fcf0379df589403976ea956c369f25aa82d84561947e227cd925902e1751371658df + languageName: node + linkType: hard + "chownr@npm:^1.1.1": version: 1.1.4 resolution: "chownr@npm:1.1.4" @@ -9889,7 +9919,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:^4.3.7": +"debug@npm:^4, debug@npm:^4.3.6, debug@npm:^4.3.7": version: 4.4.0 resolution: "debug@npm:4.4.0" dependencies: @@ -11729,7 +11759,7 @@ __metadata: languageName: node linkType: hard -"eventemitter3@npm:^4.0.1": +"eventemitter3@npm:^4.0.0, eventemitter3@npm:^4.0.1": version: 4.0.7 resolution: "eventemitter3@npm:4.0.7" checksum: 10/8030029382404942c01d0037079f1b1bc8fed524b5849c237b80549b01e2fc49709e1d0c557fa65ca4498fc9e24cff1475ef7b855121fcc15f9d61f93e282346 @@ -12315,7 +12345,7 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.15.6": +"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.15.6": version: 1.15.9 resolution: "follow-redirects@npm:1.15.9" peerDependenciesMeta: @@ -13277,6 +13307,31 @@ __metadata: languageName: node linkType: hard +"http-proxy-middleware@npm:^3.0.3": + version: 3.0.3 + resolution: "http-proxy-middleware@npm:3.0.3" + dependencies: + "@types/http-proxy": "npm:^1.17.15" + debug: "npm:^4.3.6" + http-proxy: "npm:^1.18.1" + is-glob: "npm:^4.0.3" + is-plain-object: "npm:^5.0.0" + micromatch: "npm:^4.0.8" + checksum: 10/32f58c29288ca63e109909fb998bd0f6f50eb15a98dec9487eac07dfc4f09d8507dbfa00b44442d868bafa904bd633c8bbd55686bb13b4d4af4f5c5b3bbca430 + languageName: node + linkType: hard + +"http-proxy@npm:^1.18.1": + version: 1.18.1 + resolution: "http-proxy@npm:1.18.1" + dependencies: + eventemitter3: "npm:^4.0.0" + follow-redirects: "npm:^1.0.0" + requires-port: "npm:^1.0.0" + checksum: 10/2489e98aba70adbfd8b9d41ed1ff43528be4598c88616c558b109a09eaffe4bb35e551b6c75ac42ed7d948bb7530a22a2be6ef4f0cecacb5927be139f4274594 + languageName: node + linkType: hard + "http2-wrapper@npm:^1.0.0-beta.5.2": version: 1.0.3 resolution: "http2-wrapper@npm:1.0.3" @@ -13423,6 +13478,13 @@ __metadata: languageName: node linkType: hard +"ignore-by-default@npm:^1.0.1": + version: 1.0.1 + resolution: "ignore-by-default@npm:1.0.1" + checksum: 10/441509147b3615e0365e407a3c18e189f78c07af08564176c680be1fabc94b6c789cad1342ad887175d4ecd5225de86f73d376cec8e06b42fd9b429505ffcf8a + languageName: node + linkType: hard + "ignore@npm:^5.1.1, ignore@npm:^5.2.0, ignore@npm:^5.3.1": version: 5.3.2 resolution: "ignore@npm:5.3.2" @@ -16864,7 +16926,7 @@ __metadata: languageName: node linkType: hard -"micromatch@npm:^4.0.4, micromatch@npm:~4.0.7": +"micromatch@npm:^4.0.4, micromatch@npm:^4.0.8, micromatch@npm:~4.0.7": version: 4.0.8 resolution: "micromatch@npm:4.0.8" dependencies: @@ -17420,6 +17482,26 @@ __metadata: languageName: node linkType: hard +"nodemon@npm:^3.1.9": + version: 3.1.9 + resolution: "nodemon@npm:3.1.9" + dependencies: + chokidar: "npm:^3.5.2" + debug: "npm:^4" + ignore-by-default: "npm:^1.0.1" + minimatch: "npm:^3.1.2" + pstree.remy: "npm:^1.1.8" + semver: "npm:^7.5.3" + simple-update-notifier: "npm:^2.0.0" + supports-color: "npm:^5.5.0" + touch: "npm:^3.1.0" + undefsafe: "npm:^2.0.5" + bin: + nodemon: bin/nodemon.js + checksum: 10/7c01ddfa30815f4147006f5b7c015a1f75017118cf398ee8c4ba3ac904667f4555b91cca6b7b191e0f6ccf5072727aa20224a1456d5446f3f6053e15132068a2 + languageName: node + linkType: hard + "noms@npm:0.0.0": version: 0.0.0 resolution: "noms@npm:0.0.0" @@ -18542,6 +18624,13 @@ __metadata: languageName: node linkType: hard +"pstree.remy@npm:^1.1.8": + version: 1.1.8 + resolution: "pstree.remy@npm:1.1.8" + checksum: 10/ef13b1b5896b35f67dbd4fb7ba54bb2a5da1a5c317276cbad4bcad4159bf8f7b5e1748dc244bf36865f3d560d2fc952521581280a91468c9c2df166cc760c8c1 + languageName: node + linkType: hard + "pump@npm:^3.0.0": version: 3.0.0 resolution: "pump@npm:3.0.0" @@ -20251,7 +20340,7 @@ __metadata: languageName: node linkType: hard -"simple-update-notifier@npm:2.0.0": +"simple-update-notifier@npm:2.0.0, simple-update-notifier@npm:^2.0.0": version: 2.0.0 resolution: "simple-update-notifier@npm:2.0.0" dependencies: @@ -20974,7 +21063,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^5.3.0": +"supports-color@npm:^5.3.0, supports-color@npm:^5.5.0": version: 5.5.0 resolution: "supports-color@npm:5.5.0" dependencies: @@ -21413,6 +21502,15 @@ __metadata: languageName: node linkType: hard +"touch@npm:^3.1.0": + version: 3.1.1 + resolution: "touch@npm:3.1.1" + bin: + nodetouch: bin/nodetouch.js + checksum: 10/853e763a1f4903302c5654ed353f84ad85baf757dac62c2d37ab67e0477cfd271e8c64771fcfad42310aff7c9d284ddb435ee5ca13ff36d0f3693fedd8e971d1 + languageName: node + linkType: hard + "tough-cookie@npm:^4.0.0": version: 4.1.4 resolution: "tough-cookie@npm:4.1.4" @@ -21907,6 +22005,13 @@ __metadata: languageName: node linkType: hard +"undefsafe@npm:^2.0.5": + version: 2.0.5 + resolution: "undefsafe@npm:2.0.5" + checksum: 10/f42ab3b5770fedd4ada175fc1b2eb775b78f609156f7c389106aafd231bfc210813ee49f54483d7191d7b76e483bc7f537b5d92d19ded27156baf57592eb02cc + languageName: node + linkType: hard + "underscore.string@npm:~3.3.4": version: 3.3.6 resolution: "underscore.string@npm:3.3.6"