diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 738f86f6943..869a733e325 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -6,3 +6,6 @@ contact_links: - name: Support url: https://discord.gg/pRYNYr4W5A about: Need help with something? Having troubles setting up? Or perhaps issues using the API? Reach out to the community on Discord. + - name: Translations + url: https://hosted.weblate.org/projects/actualbudget/actual/ + about: Found a string that needs a better translation? Add your suggestion or upvote an existing one in Weblate. diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index f0ac4b33ce3..eb0cd07a651 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -38,3 +38,7 @@ runs: repository: actualbudget/translations path: ${{ inputs.working-directory }}/packages/desktop-client/locale if: ${{ inputs.download-translations == 'true' }} + - name: Remove untranslated languages + run: packages/desktop-client/bin/remove-untranslated-languages + shell: bash + if: ${{ inputs.download-translations == 'true' }} diff --git a/.github/workflows/i18n-string-extract-master.yml b/.github/workflows/i18n-string-extract-master.yml index b12a98931b2..484f042d213 100644 --- a/.github/workflows/i18n-string-extract-master.yml +++ b/.github/workflows/i18n-string-extract-master.yml @@ -41,7 +41,7 @@ jobs: wlc \ --url https://hosted.weblate.org/api/ \ --key "${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}" \ - pull \ + push \ actualbudget/actual - name: Check out updated translations uses: actions/checkout@v4 @@ -69,6 +69,13 @@ jobs: else echo "No changes to commit" fi + - name: Update Weblate with latest translations + run: | + wlc \ + --url https://hosted.weblate.org/api/ \ + --key "${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}" \ + pull \ + actualbudget/actual - name: Unlock translations if: always() # Clean up even on failure diff --git a/.github/workflows/update-vrt.yml b/.github/workflows/update-vrt.yml index 7fc6e8260a8..6fa61d9a183 100644 --- a/.github/workflows/update-vrt.yml +++ b/.github/workflows/update-vrt.yml @@ -79,6 +79,8 @@ jobs: with: name: patch - name: Apply patch and push + env: + BRANCH_NAME: ${{ steps.comment-branch.outputs.head_ref }} run: | git config --global user.name "github-actions[bot]" git config --global user.email "github-actions[bot]@users.noreply.github.com" @@ -89,7 +91,7 @@ jobs: exit 0 fi git commit -m "Update VRT" - git push origin HEAD:${{ steps.comment-branch.outputs.head_ref }} + git push origin HEAD:${BRANCH_NAME} - name: Add finished reaction uses: dkershner6/reaction-action@v2 with: diff --git a/bin/package-browser b/bin/package-browser index 955d98f732e..6565d429956 100755 --- a/bin/package-browser +++ b/bin/package-browser @@ -11,6 +11,7 @@ fi pushd packages/desktop-client/locale > /dev/null git pull popd > /dev/null +packages/desktop-client/bin/remove-untranslated-languages yarn workspace loot-core build:browser yarn workspace @actual-app/web build:browser diff --git a/packages/api/methods.ts b/packages/api/methods.ts index 5ed53e9c7ec..426afc2d50e 100644 --- a/packages/api/methods.ts +++ b/packages/api/methods.ts @@ -85,10 +85,21 @@ export function addTransactions( }); } -export function importTransactions(accountId, transactions) { +export interface ImportTransactionsOpts { + defaultCleared?: boolean; +} + +export function importTransactions( + accountId, + transactions, + opts: ImportTransactionsOpts = { + defaultCleared: true, + }, +) { return send('api/transactions-import', { accountId, transactions, + opts, }); } diff --git a/packages/desktop-client/bin/remove-untranslated-languages b/packages/desktop-client/bin/remove-untranslated-languages new file mode 100755 index 00000000000..c3dd6506101 --- /dev/null +++ b/packages/desktop-client/bin/remove-untranslated-languages @@ -0,0 +1,48 @@ +#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); + +// Local path to the cloned translations repository +const localRepoPath = './packages/desktop-client/locale'; + +// Compare JSON files and delete incomplete ones +const processTranslations = () => { + try { + const files = fs.readdirSync(localRepoPath); + const enJsonPath = path.join(localRepoPath, 'en.json'); + + if (!fs.existsSync(enJsonPath)) { + throw new Error('en.json not found in the repository.'); + } + + const enJson = JSON.parse(fs.readFileSync(enJsonPath, 'utf8')); + const enKeysCount = Object.keys(enJson).length; + + console.log(`en.json has ${enKeysCount} keys.`); + + files.forEach((file) => { + if (file === 'en.json' || path.extname(file) !== '.json') return; + + const filePath = path.join(localRepoPath, file); + const jsonData = JSON.parse(fs.readFileSync(filePath, 'utf8')); + const fileKeysCount = Object.keys(jsonData).length; + + // Calculate the percentage of keys present compared to en.json + const percentage = (fileKeysCount / enKeysCount) * 100; + console.log(`${file} has ${fileKeysCount} keys (${percentage.toFixed(2)}%).`); + + if (percentage < 50) { + fs.unlinkSync(filePath); + console.log(`Deleted ${file} due to insufficient keys.`); + } else { + console.log(`Keeping ${file}.`); + } + }); + + console.log('Processing completed.'); + } catch (error) { + console.error(`Error: ${error.message}`); + } +}; + +processTranslations(); diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Bar-Graph-and-checks-the-visuals-1-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Bar-Graph-and-checks-the-visuals-1-chromium-linux.png index 9210a0b8dfd..1d7fb76c6b1 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Bar-Graph-and-checks-the-visuals-1-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Bar-Graph-and-checks-the-visuals-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Bar-Graph-and-checks-the-visuals-2-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Bar-Graph-and-checks-the-visuals-2-chromium-linux.png index 2b9312d930f..eee31376838 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Bar-Graph-and-checks-the-visuals-2-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Bar-Graph-and-checks-the-visuals-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Bar-Graph-and-checks-the-visuals-3-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Bar-Graph-and-checks-the-visuals-3-chromium-linux.png index bae67361093..9d78fe7cb7c 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Bar-Graph-and-checks-the-visuals-3-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Bar-Graph-and-checks-the-visuals-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Line-Graph-and-checks-the-visuals-1-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Line-Graph-and-checks-the-visuals-1-chromium-linux.png index 952ce325b76..af6971b2dd6 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Line-Graph-and-checks-the-visuals-1-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Line-Graph-and-checks-the-visuals-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Line-Graph-and-checks-the-visuals-2-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Line-Graph-and-checks-the-visuals-2-chromium-linux.png index 43ee73625d3..803b27d3890 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Line-Graph-and-checks-the-visuals-2-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Line-Graph-and-checks-the-visuals-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Line-Graph-and-checks-the-visuals-3-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Line-Graph-and-checks-the-visuals-3-chromium-linux.png index 47841b69da4..88bce2b0974 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Line-Graph-and-checks-the-visuals-3-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Switches-to-Line-Graph-and-checks-the-visuals-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-legend-button-shows-the-legend-side-bar-1-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-legend-button-shows-the-legend-side-bar-1-chromium-linux.png index e35ae86c415..7b7d3213884 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-legend-button-shows-the-legend-side-bar-1-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-legend-button-shows-the-legend-side-bar-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-legend-button-shows-the-legend-side-bar-2-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-legend-button-shows-the-legend-side-bar-2-chromium-linux.png index 49a0c2af9d7..7b7c5bd5555 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-legend-button-shows-the-legend-side-bar-2-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-legend-button-shows-the-legend-side-bar-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-legend-button-shows-the-legend-side-bar-3-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-legend-button-shows-the-legend-side-bar-3-chromium-linux.png index f7654637a52..cbb2116942c 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-legend-button-shows-the-legend-side-bar-3-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-custom-reports-Validates-that-show-legend-button-shows-the-legend-side-bar-3-chromium-linux.png differ diff --git a/packages/desktop-client/globals.d.ts b/packages/desktop-client/globals.d.ts index 9f9ea2437e3..07f968a4085 100644 --- a/packages/desktop-client/globals.d.ts +++ b/packages/desktop-client/globals.d.ts @@ -8,3 +8,7 @@ declare module 'react' { // eslint-disable-next-line @typescript-eslint/consistent-type-definitions, @typescript-eslint/no-empty-object-type interface CSSProperties extends CSSObject {} } + +declare global { + function __resetWorld(): void; +} diff --git a/packages/desktop-client/package.json b/packages/desktop-client/package.json index 19b8078aceb..23d298dc3d8 100644 --- a/packages/desktop-client/package.json +++ b/packages/desktop-client/package.json @@ -40,8 +40,6 @@ "i18next-parser": "^9.0.0", "i18next-resources-to-backend": "^1.2.1", "inter-ui": "^3.19.3", - "jest": "^27.5.1", - "jest-watch-typeahead": "^2.2.2", "lodash": "^4.17.21", "mdast-util-newline-to-break": "^2.0.0", "memoize-one": "^6.0.0", diff --git a/packages/desktop-client/src/components/ServerContext.tsx b/packages/desktop-client/src/components/ServerContext.tsx index c32ed2ac518..8e8d4681f1b 100644 --- a/packages/desktop-client/src/components/ServerContext.tsx +++ b/packages/desktop-client/src/components/ServerContext.tsx @@ -93,7 +93,11 @@ export function ServerProvider({ children }: { children: ReactNode }) { useEffect(() => { async function run() { - setServerURL(await send('get-server-url')); + const serverURL = await send('get-server-url'); + if (!serverURL) { + return; + } + setServerURL(serverURL); setVersion(await getServerVersion()); } run(); @@ -135,7 +139,8 @@ export function ServerProvider({ children }: { children: ReactNode }) { async (url: string, opts: { validate?: boolean } = {}) => { const { error } = await send('set-server-url', { ...opts, url }); if (!error) { - setServerURL(await send('get-server-url')); + const serverURL = await send('get-server-url'); + setServerURL(serverURL!); setVersion(await getServerVersion()); } return { error }; diff --git a/packages/desktop-client/src/components/admin/UserAccess/UserAccess.tsx b/packages/desktop-client/src/components/admin/UserAccess/UserAccess.tsx index 736daad12e1..4d4aba87896 100644 --- a/packages/desktop-client/src/components/admin/UserAccess/UserAccess.tsx +++ b/packages/desktop-client/src/components/admin/UserAccess/UserAccess.tsx @@ -106,8 +106,9 @@ function UserAccessContent({ }, [cloudFileId, setLoading, t]); const loadOwner = useCallback(async () => { - const file: Awaited<ReturnType<Handlers['get-user-file-info']>> = - (await send('get-user-file-info', cloudFileId as string)) ?? {}; + const file = (await send('get-user-file-info', cloudFileId as string)) ?? { + usersWithAccess: [], + }; const owner = file?.usersWithAccess.filter(user => user.owner); if (owner.length > 0) { diff --git a/packages/desktop-client/src/components/admin/UserDirectory/UserDirectory.tsx b/packages/desktop-client/src/components/admin/UserDirectory/UserDirectory.tsx index 2d57fc13deb..328f4ebc407 100644 --- a/packages/desktop-client/src/components/admin/UserDirectory/UserDirectory.tsx +++ b/packages/desktop-client/src/components/admin/UserDirectory/UserDirectory.tsx @@ -116,11 +116,24 @@ function UserDirectoryContent({ setLoading(true); const loadedUsers = (await send('users-get')) ?? []; + if ('error' in loadedUsers) { + dispatch( + addNotification({ + type: 'error', + id: 'error', + title: t('Error getting users'), + sticky: true, + message: getUserDirectoryErrors(loadedUsers.error), + }), + ); + setLoading(false); + return; + } setAllUsers(loadedUsers); setLoading(false); return loadedUsers; - }, [setLoading]); + }, [dispatch, getUserDirectoryErrors, setLoading, t]); useEffect(() => { async function loadData() { @@ -141,9 +154,11 @@ function UserDirectoryContent({ const onDeleteSelected = useCallback(async () => { setLoading(true); - const { error } = await send('user-delete-all', [...selectedInst.items]); + const res = await send('user-delete-all', [...selectedInst.items]); - if (error) { + const error = res['error']; + const someDeletionsFailed = res['someDeletionsFailed']; + if (error || someDeletionsFailed) { if (error === 'token-expired') { dispatch( addNotification({ diff --git a/packages/desktop-client/src/components/filters/SavedFilterMenuButton.tsx b/packages/desktop-client/src/components/filters/SavedFilterMenuButton.tsx index d6fd2e10021..56b7d0a40f3 100644 --- a/packages/desktop-client/src/components/filters/SavedFilterMenuButton.tsx +++ b/packages/desktop-client/src/components/filters/SavedFilterMenuButton.tsx @@ -42,7 +42,7 @@ export function SavedFilterMenuButton({ const [adding, setAdding] = useState(false); const [menuOpen, setMenuOpen] = useState(false); const triggerRef = useRef(null); - const [err, setErr] = useState(null); + const [err, setErr] = useState<string | null>(null); const [menuItem, setMenuItem] = useState(''); const [name, setName] = useState(filterId?.name ?? ''); const id = filterId?.id; diff --git a/packages/desktop-client/src/components/modals/CreateAccountModal.tsx b/packages/desktop-client/src/components/modals/CreateAccountModal.tsx index 64597e2a009..afd8bb790e8 100644 --- a/packages/desktop-client/src/components/modals/CreateAccountModal.tsx +++ b/packages/desktop-client/src/components/modals/CreateAccountModal.tsx @@ -88,7 +88,7 @@ export function CreateAccountModal({ upgradingAccountId }: CreateAccountProps) { balance: number; }; - for (const oldAccount of results.accounts) { + for (const oldAccount of results.accounts ?? []) { const newAccount: NormalizedAccount = { account_id: oldAccount.id, name: oldAccount.name, diff --git a/packages/desktop-client/src/components/modals/EditUser.tsx b/packages/desktop-client/src/components/modals/EditUser.tsx index 0e59ada14a7..5fdb1d71a30 100644 --- a/packages/desktop-client/src/components/modals/EditUser.tsx +++ b/packages/desktop-client/src/components/modals/EditUser.tsx @@ -83,12 +83,14 @@ function useSaveUser() { user: User, setError: (error: string) => void, ): Promise<boolean> { - const { error, id: newId } = (await send(method, user)) || {}; - if (!error) { + const res = (await send(method, user)) || {}; + if (!res['error']) { + const newId = res['id']; if (newId) { user.id = newId; } } else { + const error = res['error']; setError(getUserDirectoryErrors(error)); if (error === 'token-expired') { dispatch( diff --git a/packages/desktop-client/src/components/payees/ManagePayeesWithData.tsx b/packages/desktop-client/src/components/payees/ManagePayeesWithData.tsx index 66ce987f31c..2b9703637a0 100644 --- a/packages/desktop-client/src/components/payees/ManagePayeesWithData.tsx +++ b/packages/desktop-client/src/components/payees/ManagePayeesWithData.tsx @@ -35,9 +35,9 @@ export function ManagePayeesWithData({ }, []); const refetchRuleCounts = useCallback(async () => { - let counts = await send('payees-get-rule-counts'); - counts = new Map(Object.entries(counts)); - setRuleCounts({ value: counts }); + const counts = await send('payees-get-rule-counts'); + const countsMap = new Map(Object.entries(counts)); + setRuleCounts({ value: countsMap }); }, []); useEffect(() => { diff --git a/packages/desktop-client/src/components/reports/spreadsheets/calendar-spreadsheet.ts b/packages/desktop-client/src/components/reports/spreadsheets/calendar-spreadsheet.ts index eb50c66fdf3..71dee8760dd 100644 --- a/packages/desktop-client/src/components/reports/spreadsheets/calendar-spreadsheet.ts +++ b/packages/desktop-client/src/components/reports/spreadsheets/calendar-spreadsheet.ts @@ -34,7 +34,7 @@ export function calendarSpreadsheet( }[]; }) => void, ) => { - let filters; + let filters: unknown[]; try { const { filters: filtersLocal } = await send( diff --git a/packages/desktop-client/src/components/reports/spreadsheets/custom-spreadsheet.ts b/packages/desktop-client/src/components/reports/spreadsheets/custom-spreadsheet.ts index dfd11e58fa5..fca3740a622 100644 --- a/packages/desktop-client/src/components/reports/spreadsheets/custom-spreadsheet.ts +++ b/packages/desktop-client/src/components/reports/spreadsheets/custom-spreadsheet.ts @@ -277,18 +277,18 @@ export function createCustomSpreadsheet({ filterEmptyRows({ showEmpty, data: i, balanceTypeOp }), ); + const sortedCalcDataFiltered = [...calcDataFiltered].sort( + sortData({ balanceTypeOp, sortByOp }), + ); + const legend = calculateLegend( intervalData, - calcDataFiltered, + sortedCalcDataFiltered, groupBy, graphType, balanceTypeOp, ); - const sortedCalcDataFiltered = [...calcDataFiltered].sort( - sortData({ balanceTypeOp, sortByOp }), - ); - setData({ data: sortedCalcDataFiltered, intervalData, diff --git a/packages/desktop-client/src/components/reports/spreadsheets/spending-spreadsheet.ts b/packages/desktop-client/src/components/reports/spreadsheets/spending-spreadsheet.ts index 997bcd3a5a0..6159a2bbf4a 100644 --- a/packages/desktop-client/src/components/reports/spreadsheets/spending-spreadsheet.ts +++ b/packages/desktop-client/src/components/reports/spreadsheets/spending-spreadsheet.ts @@ -109,7 +109,7 @@ export function createSpendingSpreadsheet({ $and: [{ month: { $eq: budgetMonth } }], }) .filter({ - [conditionsOpKey]: filters.filter(filter => filter.category), + [conditionsOpKey]: filters.filter(filter => filter['category']), }) .groupBy([{ $id: '$category' }]) .select([ diff --git a/packages/desktop-client/src/components/reports/spreadsheets/summary-spreadsheet.ts b/packages/desktop-client/src/components/reports/spreadsheets/summary-spreadsheet.ts index a4200d1edee..059ff0191d7 100644 --- a/packages/desktop-client/src/components/reports/spreadsheets/summary-spreadsheet.ts +++ b/packages/desktop-client/src/components/reports/spreadsheets/summary-spreadsheet.ts @@ -27,7 +27,7 @@ export function summarySpreadsheet( toRange: string; }) => void, ) => { - let filters = []; + let filters: unknown[] = []; try { const response = await send('make-filters-from-conditions', { conditions: conditions.filter(cond => !cond.customName), diff --git a/packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx b/packages/desktop-client/src/components/transactions/TransactionsTable.test.tsx similarity index 88% rename from packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx rename to packages/desktop-client/src/components/transactions/TransactionsTable.test.tsx index 3acca829459..cbf94acfc19 100644 --- a/packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx +++ b/packages/desktop-client/src/components/transactions/TransactionsTable.test.tsx @@ -20,6 +20,13 @@ import { updateTransaction, } from 'loot-core/src/shared/transactions'; import { integerToCurrency } from 'loot-core/src/shared/util'; +import { + type AccountEntity, + type CategoryEntity, + type CategoryGroupEntity, + type PayeeEntity, + type TransactionEntity, +} from 'loot-core/types/models'; import { AuthProvider } from '../../auth/AuthProvider'; import { SelectedProviderWithItems } from '../../hooks/useSelected'; @@ -41,26 +48,20 @@ vi.mock('../../hooks/useFeatureFlag', () => ({ })); const accounts = [generateAccount('Bank of America')]; -const payees = [ +const payees: PayeeEntity[] = [ { id: 'bob-id', name: 'Bob', favorite: 1, - transfer_acct: null, - category: null, }, { id: 'alice-id', name: 'Alice', favorite: 1, - transfer_acct: null, - category: null, }, { id: 'guy', favorite: 0, - transfer_acct: null, - category: null, name: 'This guy on the side of the road', }, ]; @@ -80,8 +81,12 @@ const categoryGroups = generateCategoryGroups([ ]); const usualGroup = categoryGroups[1]; -function generateTransactions(count, splitAtIndexes = [], showError = false) { - const transactions = []; +function generateTransactions( + count: number, + splitAtIndexes: number[] = [], + showError: boolean = false, +) { + const transactions: TransactionEntity[] = []; for (let i = 0; i < count; i++) { const isSplit = splitAtIndexes.includes(i); @@ -94,10 +99,10 @@ function generateTransactions(count, splitAtIndexes = [], showError = false) { payee: 'alice-id', category: i === 0 - ? null + ? undefined : i === 1 - ? usualGroup.categories[1].id - : usualGroup.categories[0].id, + ? usualGroup.categories?.[1].id + : usualGroup.categories?.[0].id, amount: isSplit ? 50 : undefined, sort_order: i, }, @@ -110,31 +115,46 @@ function generateTransactions(count, splitAtIndexes = [], showError = false) { return transactions; } -function LiveTransactionTable(props) { +type LiveTransactionTableProps = { + transactions: TransactionEntity[]; + payees: PayeeEntity[]; + accounts: AccountEntity[]; + categoryGroups: CategoryGroupEntity[]; + currentAccountId: string | null; + showAccount: boolean; + showCategory: boolean; + showCleared: boolean; + isAdding: boolean; + onTransactionsChange?: (newTrans: TransactionEntity[]) => void; + onCloseAddTransaction?: () => void; +}; + +function LiveTransactionTable(props: LiveTransactionTableProps) { const [transactions, setTransactions] = useState(props.transactions); useEffect(() => { if (transactions === props.transactions) return; props.onTransactionsChange?.(transactions); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [transactions]); - const onSplit = id => { + const onSplit = (id: string) => { const { data, diff } = splitTransaction(transactions, id); setTransactions(data); return diff.added[0].id; }; - const onSave = transaction => { + const onSave = (transaction: TransactionEntity) => { const { data } = updateTransaction(transactions, transaction); setTransactions(data); }; - const onAdd = newTransactions => { + const onAdd = (newTransactions: TransactionEntity[]) => { newTransactions = realizeTempTransactions(newTransactions); setTransactions(trans => [...newTransactions, ...trans]); }; - const onAddSplit = id => { + const onAddSplit = (id: string) => { const { data, diff } = addSplitTransaction(transactions, id); setTransactions(data); return diff.added[0].id; @@ -155,16 +175,17 @@ function LiveTransactionTable(props) { <SelectedProviderWithItems name="transactions" items={transactions} - fetchAllIds={() => transactions.map(t => t.id)} + fetchAllIds={() => Promise.resolve(transactions.map(t => t.id))} > <SplitsExpandedProvider> <TransactionTable {...props} + // @ts-expect-error this will be auto-patched once TransactionTable is moved to TS transactions={transactions} loadMoreTransactions={() => {}} commonPayees={[]} payees={payees} - addNotification={n => console.log(n)} + addNotification={console.log} onSave={onSave} onSplit={onSplit} onAdd={onAdd} @@ -215,22 +236,22 @@ function waitForAutocomplete() { return new Promise(resolve => setTimeout(resolve, 0)); } -const categories = categoryGroups.reduce( - (all, group) => all.concat(group.categories), +const categories = categoryGroups.reduce<CategoryEntity[]>( + (all, group) => (group.categories ? [...all, ...group.categories] : all), [], ); -function prettyDate(date) { +function prettyDate(date: string) { return formatDate(parseDate(date, 'yyyy-MM-dd', new Date()), 'MM/dd/yyyy'); } -function renderTransactions(extraProps) { +function renderTransactions(extraProps?: Partial<LiveTransactionTableProps>) { let transactions = generateTransactions(5, [6]); // Hardcoding the first value makes it easier for tests to do // various this transactions[0].amount = -2777; - const defaultProps = { + const defaultProps: LiveTransactionTableProps = { transactions, payees, accounts, @@ -251,7 +272,7 @@ function renderTransactions(extraProps) { return { ...result, getTransactions: () => transactions, - updateProps: props => + updateProps: (props: Partial<LiveTransactionTableProps>) => render( <LiveTransactionTable {...defaultProps} {...extraProps} {...props} />, { container: result.container }, @@ -259,27 +280,37 @@ function renderTransactions(extraProps) { }; } -function queryNewField(container, name, subSelector = '', idx = 0) { +function queryNewField( + container: HTMLElement, + name: string, + subSelector: string = '', + idx: number = 0, +): HTMLInputElement { const field = container.querySelectorAll( `[data-testid="new-transaction"] [data-testid="${name}"]`, )[idx]; if (subSelector !== '') { - return field.querySelector(subSelector); + return field.querySelector(subSelector)!; } - return field; + return field as HTMLInputElement; } -function queryField(container, name, subSelector = '', idx) { +function queryField( + container: HTMLElement, + name: string, + subSelector: string = '', + idx: number, +) { const field = container.querySelectorAll( `[data-testid="transaction-table"] [data-testid="${name}"]`, )[idx]; if (subSelector !== '') { - return field.querySelector(subSelector); + return field.querySelector(subSelector)!; } return field; } -async function _editField(field, container) { +async function _editField(field: Element, container: HTMLElement) { // We only short-circuit this for inputs const input = field.querySelector(`input`); if (input) { @@ -287,17 +318,17 @@ async function _editField(field, container) { return input; } - let element; + let element: HTMLInputElement; const buttonQuery = 'button,div[data-testid=cell-button]'; if (field.querySelector(buttonQuery)) { - const btn = field.querySelector(buttonQuery); + const btn = field.querySelector(buttonQuery)!; await userEvent.click(btn); - element = field.querySelector(':focus'); + element = field.querySelector(':focus')!; expect(element).toBeTruthy(); } else { - await userEvent.click(field.querySelector('div')); - element = field.querySelector('input'); + await userEvent.click(field.querySelector('div')!); + element = field.querySelector('input')!; expect(element).toBeTruthy(); expect(container.ownerDocument.activeElement).toBe(element); } @@ -305,20 +336,23 @@ async function _editField(field, container) { return element; } -function editNewField(container, name, rowIndex) { +function editNewField(container: HTMLElement, name: string, rowIndex?: number) { const field = queryNewField(container, name, '', rowIndex); return _editField(field, container); } -function editField(container, name, rowIndex) { +function editField(container: HTMLElement, name: string, rowIndex: number) { const field = queryField(container, name, '', rowIndex); return _editField(field, container); } expect.extend({ - payeesToHaveFavoriteStars(container, validPayeeListWithFavorite) { - const incorrectStarList = []; - const foundStarList = []; + payeesToHaveFavoriteStars( + container: Element[], + validPayeeListWithFavorite: string[], + ) { + const incorrectStarList: string[] = []; + const foundStarList: string[] = []; validPayeeListWithFavorite.forEach(payeeItem => { const shouldHaveFavorite = payeeItem != null; let found = false; @@ -350,14 +384,19 @@ expect.extend({ }, }); -function expectToBeEditingField(container, name, rowIndex, isNew) { - let field; +function expectToBeEditingField( + container: HTMLElement, + name: string, + rowIndex: number, + isNew?: boolean, +) { + let field: Element; if (isNew) { field = queryNewField(container, name, '', rowIndex); } else { field = queryField(container, name, '', rowIndex); } - const input = field.querySelector(':focus'); + const input: HTMLInputElement = field.querySelector(':focus')!; expect(input).toBeTruthy(); expect(container.ownerDocument.activeElement).toBe(input); return input; @@ -372,10 +411,10 @@ describe('Transactions', () => { prettyDate(transaction.date), ); expect(queryField(container, 'account', 'div', idx).textContent).toBe( - accounts.find(acct => acct.id === transaction.account).name, + accounts.find(acct => acct.id === transaction.account)?.name, ); expect(queryField(container, 'payee', 'div', idx).textContent).toBe( - payees.find(p => p.id === transaction.payee).name, + payees.find(p => p.id === transaction.payee)?.name, ); expect(queryField(container, 'notes', 'div', idx).textContent).toBe( transaction.notes, @@ -383,7 +422,7 @@ describe('Transactions', () => { expect(queryField(container, 'category', 'div', idx).textContent).toBe( transaction.category ? categories.find(category => category.id === transaction.category) - .name + ?.name : 'Categorize', ); if (transaction.amount <= 0) { @@ -531,6 +570,7 @@ describe('Transactions', () => { expect(items.length).toBe(2); expect(items[0].textContent).toBe('Usual Expenses'); expect(items[1].textContent).toBe('General 129.87'); + // @ts-expect-error fix me expect(items[1].dataset['highlighted']).toBeDefined(); // It should not allow filtering on group names @@ -561,10 +601,10 @@ describe('Transactions', () => { .getByTestId('autocomplete') .querySelector('[data-highlighted]'); expect(highlighted).not.toBeNull(); - expect(highlighted.textContent).toBe('General 129.87'); + expect(highlighted!.textContent).toBe('General 129.87'); expect(getTransactions()[2].category).toBe( - categories.find(category => category.name === 'Food').id, + categories.find(category => category.name === 'Food')?.id, ); await userEvent.type(input, '[Enter]'); @@ -572,7 +612,7 @@ describe('Transactions', () => { // The transactions data should be updated with the right category expect(getTransactions()[2].category).toBe( - categories.find(category => category.name === 'General').id, + categories.find(category => category.name === 'General')?.id, ); // The category field should still be editing @@ -607,16 +647,16 @@ describe('Transactions', () => { .getByTestId('autocomplete') .querySelector('[data-highlighted]'); expect(highlighted).not.toBeNull(); - expect(highlighted.textContent).toBe('General 129.87'); + expect(highlighted!.textContent).toBe('General 129.87'); // Click the item and check the before/after values expect(getTransactions()[2].category).toBe( - categories.find(c => c.name === 'Food').id, + categories.find(c => c.name === 'Food')?.id, ); await userEvent.click(items[2]); await waitForAutocomplete(); expect(getTransactions()[2].category).toBe( - categories.find(c => c.name === 'General').id, + categories.find(c => c.name === 'General')?.id, ); // It should still be editing the category @@ -651,8 +691,9 @@ describe('Transactions', () => { // field was different than the transactions' category const currentCategory = getTransactions()[2].category; expect(currentCategory).toBe(oldCategory); + // @ts-expect-error fix me expect(highlighted.textContent).not.toBe( - categories.find(c => c.id === currentCategory).name, + categories.find(c => c.id === currentCategory)?.name, ); }); @@ -678,6 +719,7 @@ describe('Transactions', () => { 'Bob-payee-item', 'This guy on the side of the road-payee-item', ]); + // @ts-expect-error fix me expect(renderedPayees).payeesToHaveFavoriteStars([ 'Alice-payee-item', 'Bob-payee-item', @@ -807,7 +849,7 @@ describe('Transactions', () => { await waitForAutocomplete(); await userEvent.click( - container.querySelector('[data-testid="add-split-button"]'), + container.querySelector('[data-testid="add-split-button"]')!, ); input = await editNewField(container, 'debit', 1); @@ -825,7 +867,7 @@ describe('Transactions', () => { null, ); - const addButton = container.querySelector('[data-testid="add-button"]'); + const addButton = container.querySelector('[data-testid="add-button"]')!; expect(getTransactions().length).toBe(5); await userEvent.click(addButton); @@ -903,13 +945,13 @@ describe('Transactions', () => { transactions[0] = { ...transactions[0], id: uuidv4() }; updateProps({ transactions }); - function expectErrorToNotExist(transactions) { + function expectErrorToNotExist(transactions: TransactionEntity[]) { transactions.forEach(transaction => { expect(transaction.error).toBeFalsy(); }); } - function expectErrorToExist(transactions) { + function expectErrorToExist(transactions: TransactionEntity[]) { transactions.forEach((transaction, idx) => { if (idx === 0) { expect(transaction.error).toBeTruthy(); @@ -951,7 +993,7 @@ describe('Transactions', () => { // Add another split transaction and make sure everything is // updated properly await userEvent.click( - toolbar.querySelector('[data-testid="add-split-button"]'), + toolbar.querySelector('[data-testid="add-split-button"]')!, ); expect(getTransactions().length).toBe(7); expect(getTransactions()[2].amount).toBe(0); @@ -973,10 +1015,10 @@ describe('Transactions', () => { { account: accounts[0].id, amount: -2777, - category: null, + category: undefined, cleared: false, date: '2017-01-01', - error: null, + error: undefined, id: expect.any(String), is_parent: true, notes: 'Notes', @@ -986,7 +1028,7 @@ describe('Transactions', () => { { account: accounts[0].id, amount: -1000, - category: null, + category: undefined, cleared: false, date: '2017-01-01', error: null, @@ -994,13 +1036,14 @@ describe('Transactions', () => { is_child: true, parent_id: parentId, payee: 'alice-id', + reconciled: undefined, sort_order: -1, starting_balance_flag: null, }, { account: accounts[0].id, amount: -1777, - category: null, + category: undefined, cleared: false, date: '2017-01-01', error: null, @@ -1008,6 +1051,7 @@ describe('Transactions', () => { is_child: true, parent_id: parentId, payee: 'alice-id', + reconciled: undefined, sort_order: -2, starting_balance_flag: null, }, diff --git a/packages/desktop-client/src/hooks/useSplitsExpanded.tsx b/packages/desktop-client/src/hooks/useSplitsExpanded.tsx index 2d178e152d1..69c3675decb 100644 --- a/packages/desktop-client/src/hooks/useSplitsExpanded.tsx +++ b/packages/desktop-client/src/hooks/useSplitsExpanded.tsx @@ -87,7 +87,7 @@ export function useSplitsExpanded() { type SplitsExpandedProviderProps = { children?: ReactNode; - initialMode: SplitMode; + initialMode?: SplitMode; }; export function SplitsExpandedProvider({ diff --git a/packages/loot-core/src/client/query-helpers.test.ts b/packages/loot-core/src/client/query-helpers.test.ts index 20179f39e72..c37c454ee5f 100644 --- a/packages/loot-core/src/client/query-helpers.test.ts +++ b/packages/loot-core/src/client/query-helpers.test.ts @@ -122,7 +122,9 @@ function initPagingServer( describe('query helpers', () => { it('runQuery runs a query', async () => { - initServer({ query: query => ({ data: query, dependencies: [] }) }); + initServer({ + query: query => Promise.resolve({ data: query, dependencies: [] }), + }); const query = q('transactions').select('*'); const { data } = await runQuery(query); diff --git a/packages/loot-core/src/mocks/index.ts b/packages/loot-core/src/mocks/index.ts index e04cbb7db51..92a8a4194bd 100644 --- a/packages/loot-core/src/mocks/index.ts +++ b/packages/loot-core/src/mocks/index.ts @@ -1,33 +1,54 @@ -// @ts-strict-ignore import { v4 as uuidv4 } from 'uuid'; import * as monthUtils from '../shared/months'; import type { _SyncFields, AccountEntity, + CategoryEntity, + CategoryGroupEntity, + NewCategoryGroupEntity, TransactionEntity, } from '../types/models'; import { random } from './random'; export function generateAccount( - name, - isConnected, - offbudget, -): AccountEntity & { bankId: number; bankName: string } { - return { + name: AccountEntity['name'], + isConnected?: boolean, + offbudget?: boolean, +): AccountEntity & { bankId: number | null; bankName: string | null } { + const offlineAccount: AccountEntity & { + bankId: number | null; + bankName: string | null; + } = { id: uuidv4(), name, - balance_current: isConnected ? Math.floor(random() * 100000) : null, - bankId: isConnected ? Math.floor(random() * 10000) : null, - bankName: isConnected ? 'boa' : null, - bank: isConnected ? Math.floor(random() * 10000).toString() : null, + bankId: null, + bankName: null, offbudget: offbudget ? 1 : 0, sort_order: 0, tombstone: 0, closed: 0, ...emptySyncFields(), }; + + if (isConnected) { + return { + ...offlineAccount, + balance_current: Math.floor(random() * 100000), + bankId: Math.floor(random() * 10000), + bankName: 'boa', + bank: Math.floor(random() * 10000).toString(), + account_id: 'idx', + mask: 'xxx', + official_name: 'boa', + balance_available: 0, + balance_limit: 0, + account_sync_source: 'goCardless', + }; + } + + return offlineAccount; } function emptySyncFields(): _SyncFields<false> { @@ -44,40 +65,51 @@ function emptySyncFields(): _SyncFields<false> { } let sortOrder = 1; -export function generateCategory(name, group, isIncome = false) { +export function generateCategory( + name: string, + group: string, + isIncome: boolean = false, +): CategoryEntity { return { id: uuidv4(), name, cat_group: group, - is_income: isIncome ? 1 : 0, + is_income: isIncome, sort_order: sortOrder++, }; } let groupSortOrder = 1; -export function generateCategoryGroup(name, isIncome = false) { +export function generateCategoryGroup( + name: string, + isIncome: boolean = false, +): CategoryGroupEntity { return { id: uuidv4(), name, - is_income: isIncome ? 1 : 0, + is_income: isIncome, sort_order: groupSortOrder++, }; } -export function generateCategoryGroups(definition) { +export function generateCategoryGroups( + definition: Partial<NewCategoryGroupEntity>[], +): CategoryGroupEntity[] { return definition.map(group => { - const g = generateCategoryGroup(group.name, group.is_income); + const g = generateCategoryGroup(group.name ?? '', group.is_income); return { ...g, - categories: group.categories.map(cat => + categories: group.categories?.map(cat => generateCategory(cat.name, g.id, cat.is_income), ), }; }); } -function _generateTransaction(data): TransactionEntity { +function _generateTransaction( + data: Partial<TransactionEntity> & Pick<TransactionEntity, 'account'>, +): TransactionEntity { return { id: data.id || uuidv4(), amount: data.amount || Math.floor(random() * 10000 - 7000), @@ -91,8 +123,12 @@ function _generateTransaction(data): TransactionEntity { }; } -export function generateTransaction(data, splitAmount?, showError = false) { - const result = []; +export function generateTransaction( + data: Partial<TransactionEntity> & Pick<TransactionEntity, 'account'>, + splitAmount?: number, + showError: boolean = false, +) { + const result: TransactionEntity[] = []; const trans = _generateTransaction(data); result.push(trans); @@ -107,18 +143,14 @@ export function generateTransaction(data, splitAmount?, showError = false) { amount: trans.amount - splitAmount, account: parent.account, date: parent.date, - notes: null, - category: null, - isChild: true, + is_child: true, }, { id: parent.id + '/' + uuidv4(), amount: splitAmount, account: parent.account, date: parent.date, - notes: null, - category: null, - isChild: true, + is_child: true, }, ); @@ -137,13 +169,13 @@ export function generateTransaction(data, splitAmount?, showError = false) { } export function generateTransactions( - count, - accountId, - groupId, - splitAtIndexes = [], - showError = false, -) { - const transactions = []; + count: number, + accountId: string, + groupId: string, + splitAtIndexes: number[] = [], + showError: boolean = false, +): TransactionEntity[] { + const transactions: TransactionEntity[] = []; for (let i = 0; i < count; i++) { const isSplit = splitAtIndexes.includes(i); 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 7ca6e52bc8b..417f4f917d9 100644 --- a/packages/loot-core/src/platform/client/fetch/index.d.ts +++ b/packages/loot-core/src/platform/client/fetch/index.d.ts @@ -1,4 +1,5 @@ import type { Handlers } from '../../../types/handlers'; +import type { CategoryGroupEntity } from '../../../types/models'; import type { ServerEvents } from '../../../types/server-events'; export function init(socketName: string): Promise<unknown>; @@ -38,7 +39,14 @@ export function unlisten(name: string): void; export type Unlisten = typeof unlisten; /** Mock functions */ -export function initServer(handlers: unknown): void; +export function initServer(handlers: { + query: (query: { table: string; selectExpressions: unknown }) => Promise<{ + data: unknown; + dependencies: string[]; + }>; + getCell?: () => { value: number }; + 'get-categories'?: () => { grouped: CategoryGroupEntity[] }; +}): void; export type InitServer = typeof initServer; export function serverPush(name: string, args: unknown): void; diff --git a/packages/loot-core/src/server/accounts/rules.test.ts b/packages/loot-core/src/server/accounts/rules.test.ts index 88bf161ac00..996cadc6b2a 100644 --- a/packages/loot-core/src/server/accounts/rules.test.ts +++ b/packages/loot-core/src/server/accounts/rules.test.ts @@ -547,7 +547,6 @@ describe('Rule', () => { expect( fixedAmountRule.exec({ imported_payee: 'James', amount: 200 }), ).toMatchObject({ - error: null, subtransactions: [{ amount: 100 }, { amount: 100 }], }); }); @@ -574,7 +573,6 @@ describe('Rule', () => { expect(rule.exec({ imported_payee: 'James', amount: 200 })).toMatchObject( { - error: null, subtransactions: [{ amount: 100 }, { amount: 100 }], }, ); @@ -600,7 +598,6 @@ describe('Rule', () => { expect(rule.exec({ imported_payee: 'James', amount: 200 })).toMatchObject( { - error: null, subtransactions: [{ amount: 100 }, { amount: 100 }], }, ); @@ -635,7 +632,6 @@ describe('Rule', () => { expect( prioritizationRule.exec({ imported_payee: 'James', amount: 200 }), ).toMatchObject({ - error: null, subtransactions: [{ amount: 100 }, { amount: 50 }, { amount: 50 }], }); }); @@ -645,7 +641,6 @@ describe('Rule', () => { expect( prioritizationRule.exec({ imported_payee: 'James', amount: 50 }), ).toMatchObject({ - error: null, subtransactions: [{ amount: 100 }, { amount: -25 }, { amount: -25 }], }); }); @@ -677,7 +672,6 @@ describe('Rule', () => { expect(rule.exec({ imported_payee: 'James', amount: 150 })).toMatchObject( { - error: null, subtransactions: [{ amount: 100 }, { amount: 50 }, { amount: 0 }], }, ); diff --git a/packages/loot-core/src/server/accounts/sync.ts b/packages/loot-core/src/server/accounts/sync.ts index 62954778ca2..5add734d796 100644 --- a/packages/loot-core/src/server/accounts/sync.ts +++ b/packages/loot-core/src/server/accounts/sync.ts @@ -393,6 +393,7 @@ export async function reconcileTransactions( isBankSyncAccount = false, strictIdChecking = true, isPreview = false, + defaultCleared = true, ) { console.log('Performing transaction reconciliation'); @@ -436,7 +437,7 @@ export async function reconcileTransactions( category: existing.category || trans.category || null, imported_payee: trans.imported_payee || null, notes: existing.notes || trans.notes || null, - cleared: trans.cleared != null ? trans.cleared : true, + cleared: trans.cleared ?? existing.cleared, }; if (hasFieldsChanged(existing, updates, Object.keys(updates))) { @@ -468,7 +469,7 @@ export async function reconcileTransactions( ...newTrans, id: uuidv4(), category: trans.category || null, - cleared: trans.cleared != null ? trans.cleared : true, + cleared: trans.cleared ?? defaultCleared, }; if (subtransactions && subtransactions.length > 0) { diff --git a/packages/loot-core/src/server/main.ts b/packages/loot-core/src/server/main.ts index 4deee12800f..c4ac7e7ef00 100644 --- a/packages/loot-core/src/server/main.ts +++ b/packages/loot-core/src/server/main.ts @@ -1242,6 +1242,7 @@ handlers['transactions-import'] = mutator(function ({ accountId, transactions, isPreview, + opts, }) { return withUndo(async () => { if (typeof accountId !== 'string') { @@ -1255,6 +1256,7 @@ handlers['transactions-import'] = mutator(function ({ false, true, isPreview, + opts?.defaultCleared, ); } catch (err) { if (err instanceof TransactionError) { diff --git a/packages/loot-core/src/server/spreadsheet/spreadsheet.test.ts b/packages/loot-core/src/server/spreadsheet/spreadsheet.test.ts index 2d96b2d77ad..095252fde14 100644 --- a/packages/loot-core/src/server/spreadsheet/spreadsheet.test.ts +++ b/packages/loot-core/src/server/spreadsheet/spreadsheet.test.ts @@ -10,7 +10,7 @@ function wait(n) { return new Promise(resolve => setTimeout(resolve, n)); } -async function insertTransactions(payeeId = null) { +async function insertTransactions() { await db.insertAccount({ id: '1', name: 'checking', offbudget: 0 }); await db.insertAccount({ id: '2', name: 'checking', offbudget: 1 }); await db.insertCategoryGroup({ id: 'group1', name: 'group1' }); @@ -23,7 +23,6 @@ async function insertTransactions(payeeId = null) { account: '1', category: 'cat1', date: '2017-01-08', - description: payeeId, })[0], ); await db.insertTransaction( @@ -32,7 +31,6 @@ async function insertTransactions(payeeId = null) { account: '1', category: 'cat2', date: '2017-01-10', - description: payeeId, })[0], ); await db.insertTransaction( @@ -41,7 +39,6 @@ async function insertTransactions(payeeId = null) { account: '1', category: 'cat2', date: '2017-01-15', - description: payeeId, })[0], ); } @@ -128,8 +125,8 @@ describe('Spreadsheet', () => { test('querying deep join works', async () => { const spreadsheet = new Spreadsheet(db); await db.insertPayee({ name: '', transfer_acct: '1' }); - const payeeId2 = await db.insertPayee({ name: '', transfer_acct: '2' }); - await insertTransactions(payeeId2); + await db.insertPayee({ name: '', transfer_acct: '2' }); + await insertTransactions(); spreadsheet.set( 'g!foo', diff --git a/packages/loot-core/src/shared/transactions.ts b/packages/loot-core/src/shared/transactions.ts index 5572446d9ed..d09b9459139 100644 --- a/packages/loot-core/src/shared/transactions.ts +++ b/packages/loot-core/src/shared/transactions.ts @@ -4,11 +4,6 @@ import { type TransactionEntity } from '../types/models'; import { last, diffItems, applyChanges } from './util'; -interface TransactionEntityWithError extends TransactionEntity { - error: ReturnType<typeof SplitTransactionError> | null; - _deleted?: boolean; -} - export function isTemporaryId(id: string) { return id.indexOf('temp') !== -1; } @@ -26,13 +21,13 @@ function SplitTransactionError(total: number, parent: TransactionEntity) { const difference = num(parent.amount) - total; return { - type: 'SplitTransactionError', - version: 1, + type: 'SplitTransactionError' as const, + version: 1 as const, difference, }; } -type GenericTransactionEntity = TransactionEntity | TransactionEntityWithError; +type GenericTransactionEntity = TransactionEntity; export function makeChild<T extends GenericTransactionEntity>( parent: T, @@ -86,8 +81,10 @@ export function recalculateSplit(trans: TransactionEntity) { return { ...trans, error: - total === num(trans.amount) ? null : SplitTransactionError(total, trans), - } as TransactionEntityWithError; + total === num(trans.amount) + ? undefined + : SplitTransactionError(total, trans), + } satisfies TransactionEntity; } function findParentIndex(transactions: TransactionEntity[], idx: number) { @@ -129,7 +126,10 @@ export function ungroupTransactions(transactions: TransactionEntity[]) { } export function groupTransaction(split: TransactionEntity[]) { - return { ...split[0], subtransactions: split.slice(1) } as TransactionEntity; + return { + ...split[0], + subtransactions: split.slice(1), + } satisfies TransactionEntity; } export function ungroupTransaction(split: TransactionEntity | null) { @@ -154,12 +154,10 @@ export function applyTransactionDiff( function replaceTransactions( transactions: TransactionEntity[], id: string, - func: ( - transaction: TransactionEntity, - ) => TransactionEntity | TransactionEntityWithError | null, + func: (transaction: TransactionEntity) => TransactionEntity | null, ): { data: TransactionEntity[]; - newTransaction: TransactionEntity | TransactionEntityWithError | null; + newTransaction: TransactionEntity | null; diff: ReturnType<typeof diffItems<TransactionEntity>>; } { const idx = transactions.findIndex(t => t.id === id); @@ -282,8 +280,8 @@ export function deleteTransaction( ...trans, subtransactions: undefined, is_parent: false, - error: null, - } as TransactionEntityWithError; + error: undefined, + } satisfies TransactionEntity; } else { const sub = trans.subtransactions?.filter(t => t.id !== id); return recalculateSplit({ ...trans, subtransactions: sub }); @@ -313,12 +311,13 @@ export function splitTransaction( return { ...trans, is_parent: true, - error: num(trans.amount) === 0 ? null : SplitTransactionError(0, trans), + error: + num(trans.amount) === 0 ? undefined : SplitTransactionError(0, trans), subtransactions: subtransactions.map(t => ({ ...t, sort_order: t.sort_order || -1, })), - } as TransactionEntityWithError; + } satisfies TransactionEntity; }); } @@ -338,7 +337,7 @@ export function realizeTempTransactions( ...child, id: uuidv4(), parent_id: parent.id, - }) as TransactionEntity, + }) satisfies TransactionEntity, ), ]; } diff --git a/packages/loot-core/src/types/api-handlers.d.ts b/packages/loot-core/src/types/api-handlers.d.ts index eb294292d57..37ca5336689 100644 --- a/packages/loot-core/src/types/api-handlers.d.ts +++ b/packages/loot-core/src/types/api-handlers.d.ts @@ -1,3 +1,5 @@ +import { ImportTransactionsOpts } from '@actual-app/api'; + import { type batchUpdateTransactions } from '../server/accounts/transactions'; import type { APIAccountEntity, @@ -80,6 +82,7 @@ export interface ApiHandlers { accountId; transactions; isPreview?; + opts?: ImportTransactionsOpts; }) => Promise<{ errors?: { message: string }[]; added; diff --git a/packages/loot-core/src/types/models/transaction.d.ts b/packages/loot-core/src/types/models/transaction.d.ts index 648eec97495..4c071291c8b 100644 --- a/packages/loot-core/src/types/models/transaction.d.ts +++ b/packages/loot-core/src/types/models/transaction.d.ts @@ -26,4 +26,9 @@ export interface TransactionEntity { subtransactions?: TransactionEntity[]; _unmatched?: boolean; _deleted?: boolean; + error?: { + type: 'SplitTransactionError'; + version: 1; + difference: number; + }; } diff --git a/packages/loot-core/src/types/server-handlers.d.ts b/packages/loot-core/src/types/server-handlers.d.ts index f86c86a124f..904dd94aa71 100644 --- a/packages/loot-core/src/types/server-handlers.d.ts +++ b/packages/loot-core/src/types/server-handlers.d.ts @@ -1,3 +1,5 @@ +import { ImportTransactionsOpts } from '@actual-app/api'; + import { ParseFileResult } from '../server/accounts/parse-file'; import { batchUpdateTransactions } from '../server/accounts/transactions'; import { Backup } from '../server/backups'; @@ -246,6 +248,7 @@ export interface ServerHandlers { accountId; transactions; isPreview; + opts?: ImportTransactionsOpts; }) => Promise<{ errors?: { message: string }[]; added; diff --git a/tsconfig.json b/tsconfig.json index 5c5510f8c7f..d46dd74c7e6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,7 @@ "noFallthroughCasesInSwitch": true, "skipLibCheck": true, "jsx": "preserve", - "types": ["vite/client", "jest"], + "types": ["vite/client", "jest", "vitest/globals"], // Check JS files too "allowJs": true, "checkJs": false, diff --git a/upcoming-release-notes/4108.md b/upcoming-release-notes/4108.md new file mode 100644 index 00000000000..e75000244bf --- /dev/null +++ b/upcoming-release-notes/4108.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [MatissJanis] +--- + +TypeScript: ported transactions-table tests to TS. diff --git a/upcoming-release-notes/4129.md b/upcoming-release-notes/4129.md new file mode 100644 index 00000000000..4a903f60762 --- /dev/null +++ b/upcoming-release-notes/4129.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [NikxDa] +--- + +Add ability to provide default cleared status in the API and skip updating the cleared status on subsequent imports. diff --git a/upcoming-release-notes/4144.md b/upcoming-release-notes/4144.md new file mode 100644 index 00000000000..e516e334bc2 --- /dev/null +++ b/upcoming-release-notes/4144.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [jfdoming] +--- + +Update issue template with translation issue type diff --git a/upcoming-release-notes/4146.md b/upcoming-release-notes/4146.md new file mode 100644 index 00000000000..78c785e4cc0 --- /dev/null +++ b/upcoming-release-notes/4146.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [jfdoming] +--- + +Fix `send` types in a number of places (1/2) diff --git a/upcoming-release-notes/4148.md b/upcoming-release-notes/4148.md new file mode 100644 index 00000000000..30292735b18 --- /dev/null +++ b/upcoming-release-notes/4148.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [jfdoming] +--- + +Exclude untranslated languages from builds diff --git a/upcoming-release-notes/4149.md b/upcoming-release-notes/4149.md new file mode 100644 index 00000000000..678e93bdb46 --- /dev/null +++ b/upcoming-release-notes/4149.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [jfdoming] +--- + +Fix string upload if new changes are present diff --git a/upcoming-release-notes/4151.md b/upcoming-release-notes/4151.md new file mode 100644 index 00000000000..e45d84e8113 --- /dev/null +++ b/upcoming-release-notes/4151.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [UnderKoen] +--- + +Remove code injection in /update-vrt workflow diff --git a/upcoming-release-notes/4162.md b/upcoming-release-notes/4162.md new file mode 100644 index 00000000000..b464003bfd4 --- /dev/null +++ b/upcoming-release-notes/4162.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [matt-fidd] +--- + +Fix inconsistent legend coloring in custom reports diff --git a/yarn.lock b/yarn.lock index 5f60a120657..0722add2e6c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -93,8 +93,6 @@ __metadata: i18next-parser: "npm:^9.0.0" i18next-resources-to-backend: "npm:^1.2.1" inter-ui: "npm:^3.19.3" - jest: "npm:^27.5.1" - jest-watch-typeahead: "npm:^2.2.2" lodash: "npm:^4.17.21" mdast-util-newline-to-break: "npm:^2.0.0" memoize-one: "npm:^6.0.0" @@ -2366,20 +2364,6 @@ __metadata: languageName: node linkType: hard -"@jest/console@npm:^29.5.0": - version: 29.5.0 - resolution: "@jest/console@npm:29.5.0" - dependencies: - "@jest/types": "npm:^29.5.0" - "@types/node": "npm:*" - chalk: "npm:^4.0.0" - jest-message-util: "npm:^29.5.0" - jest-util: "npm:^29.5.0" - slash: "npm:^3.0.0" - checksum: 10/0971c3d6abbb6adfa0b4e88c41121bbd45d7df821f7a9f7b3f4fce86d25b237925db526b315f9791a24b29efd0028bb235f68d5b6cc343e83246a6e76b5724dc - languageName: node - linkType: hard - "@jest/core@npm:^27.5.1": version: 27.5.1 resolution: "@jest/core@npm:27.5.1" @@ -2546,18 +2530,6 @@ __metadata: languageName: node linkType: hard -"@jest/test-result@npm:^29.5.0": - version: 29.5.0 - resolution: "@jest/test-result@npm:29.5.0" - dependencies: - "@jest/console": "npm:^29.5.0" - "@jest/types": "npm:^29.5.0" - "@types/istanbul-lib-coverage": "npm:^2.0.0" - collect-v8-coverage: "npm:^1.0.0" - checksum: 10/e41ab6137b26dba4d08441f3c921c8c9f4543bddd23072e1dbb54770584ac118f957fc6da4bf94bc5127161bee8e1ea6983b4e92249e47604163b10347d373ce - languageName: node - linkType: hard - "@jest/test-sequencer@npm:^27.5.1": version: 27.5.1 resolution: "@jest/test-sequencer@npm:27.5.1" @@ -6622,15 +6594,6 @@ __metadata: languageName: node linkType: hard -"ansi-escapes@npm:^6.0.0": - version: 6.2.0 - resolution: "ansi-escapes@npm:6.2.0" - dependencies: - type-fest: "npm:^3.0.0" - checksum: 10/442f91b04650b35bc4815f47c20412d69ddbba5d4bf22f72ec03be352fca2de6819c7e3f4dfd17816ee4e0c6c965fe85e6f1b3f09683996a8d12fd366afd924e - languageName: node - linkType: hard - "ansi-escapes@npm:^7.0.0": version: 7.0.0 resolution: "ansi-escapes@npm:7.0.0" @@ -7704,7 +7667,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^5.2.0, chalk@npm:~5.3.0": +"chalk@npm:~5.3.0": version: 5.3.0 resolution: "chalk@npm:5.3.0" checksum: 10/6373caaab21bd64c405bfc4bd9672b145647fc9482657b5ea1d549b3b2765054e9d3d928870cdf764fb4aad67555f5061538ff247b8310f110c5c888d92397ea @@ -7718,13 +7681,6 @@ __metadata: languageName: node linkType: hard -"char-regex@npm:^2.0.0": - version: 2.0.1 - resolution: "char-regex@npm:2.0.1" - checksum: 10/fadd100b963c160a70192e47e122c654cadf447c2c8f23b0bda4dc9ef1a02c993abbb0f21f50e2e58f90a8453ca019b3c86f001688cb42fb7b54af4e661b1ada - languageName: node - linkType: hard - "character-entities@npm:^2.0.0": version: 2.0.2 resolution: "character-entities@npm:2.0.2" @@ -9269,13 +9225,6 @@ __metadata: languageName: node linkType: hard -"emittery@npm:^0.13.1": - version: 0.13.1 - resolution: "emittery@npm:0.13.1" - checksum: 10/fbe214171d878b924eedf1757badf58a5dce071cd1fa7f620fa841a0901a80d6da47ff05929d53163105e621ce11a71b9d8acb1148ffe1745e045145f6e69521 - languageName: node - linkType: hard - "emittery@npm:^0.8.1": version: 0.8.1 resolution: "emittery@npm:0.8.1" @@ -12997,7 +12946,7 @@ __metadata: languageName: node linkType: hard -"jest-regex-util@npm:^29.0.0, jest-regex-util@npm:^29.4.3": +"jest-regex-util@npm:^29.4.3": version: 29.4.3 resolution: "jest-regex-util@npm:29.4.3" checksum: 10/96fc7fc28cd4dd73a63c13a526202c4bd8b351d4e5b68b1a2a2c88da3308c2a16e26feaa593083eb0bac38cca1aa9dd05025412e7de013ba963fb8e66af22b8a @@ -13205,23 +13154,6 @@ __metadata: languageName: node linkType: hard -"jest-watch-typeahead@npm:^2.2.2": - version: 2.2.2 - resolution: "jest-watch-typeahead@npm:2.2.2" - dependencies: - ansi-escapes: "npm:^6.0.0" - chalk: "npm:^5.2.0" - jest-regex-util: "npm:^29.0.0" - jest-watcher: "npm:^29.0.0" - slash: "npm:^5.0.0" - string-length: "npm:^5.0.1" - strip-ansi: "npm:^7.0.1" - peerDependencies: - jest: ^27.0.0 || ^28.0.0 || ^29.0.0 - checksum: 10/8685277ce1b96ec775882111ec55ce90a862cc57acb21ce94f8ac44a25f6fb34c7a7ce119e07b2d8ff5353a8d9e4f981cf96fa35532f71ddba6ca8fedc05bd8e - languageName: node - linkType: hard - "jest-watcher@npm:^27.5.1": version: 27.5.1 resolution: "jest-watcher@npm:27.5.1" @@ -13237,22 +13169,6 @@ __metadata: languageName: node linkType: hard -"jest-watcher@npm:^29.0.0": - version: 29.5.0 - resolution: "jest-watcher@npm:29.5.0" - dependencies: - "@jest/test-result": "npm:^29.5.0" - "@jest/types": "npm:^29.5.0" - "@types/node": "npm:*" - ansi-escapes: "npm:^4.2.1" - chalk: "npm:^4.0.0" - emittery: "npm:^0.13.1" - jest-util: "npm:^29.5.0" - string-length: "npm:^4.0.1" - checksum: 10/accd79e95dbe27106500fcc6814c4690438dda54f3bae2e5373b341e398a7ee3be64c07ff0e1e26c675e699025a4d0dd7822466f0273a17a0613d5157f3941ad - languageName: node - linkType: hard - "jest-worker@npm:^27.4.5, jest-worker@npm:^27.5.1": version: 27.5.1 resolution: "jest-worker@npm:27.5.1" @@ -17692,13 +17608,6 @@ __metadata: languageName: node linkType: hard -"slash@npm:^5.0.0": - version: 5.0.1 - resolution: "slash@npm:5.0.1" - checksum: 10/9f08524c3cd187b8addd5982b314c56ae49d781454a202622e9ca6e15cc1f6d65d3388be40eb38f203700afcc023ebd2186845cf62266f10ff92a91d09b95d33 - languageName: node - linkType: hard - "slice-ansi@npm:^3.0.0": version: 3.0.0 resolution: "slice-ansi@npm:3.0.0" @@ -18046,16 +17955,6 @@ __metadata: languageName: node linkType: hard -"string-length@npm:^5.0.1": - version: 5.0.1 - resolution: "string-length@npm:5.0.1" - dependencies: - char-regex: "npm:^2.0.0" - strip-ansi: "npm:^7.0.1" - checksum: 10/71f73b8c8a743e01dcd001bcf1b197db78d5e5e53b12bd898cddaf0961be09f947dfd8c429783db3694b55b05cb5a51de6406c5085ff1aaa10c4771440c8396d - languageName: node - linkType: hard - "string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" @@ -18993,15 +18892,6 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^3.0.0": - version: 3.10.0 - resolution: "type-fest@npm:3.10.0" - peerDependencies: - typescript: ">=4.7.0" - checksum: 10/1cc20d5d258ee44a772dd7412c80e97763d3d5a94452663ae6ebb0a3c4e7be7c0c1134bbe8df79abaf972f31252b5e9e91ba59a08481887fb260a31f90dda59d - languageName: node - linkType: hard - "typed-array-buffer@npm:^1.0.2": version: 1.0.2 resolution: "typed-array-buffer@npm:1.0.2"