From 6c0c9ff93da0a1963026585e7022105fced85608 Mon Sep 17 00:00:00 2001 From: Joel Jeremy Marquez Date: Thu, 19 Dec 2024 13:56:50 -0800 Subject: [PATCH] Queries slice --- .../src/components/ManageRules.tsx | 2 +- .../src/components/accounts/Account.tsx | 24 +- .../autocomplete/PayeeAutocomplete.tsx | 9 +- .../src/components/budget/index.tsx | 52 +- .../mobile/accounts/AccountTransactions.tsx | 16 +- .../mobile/budget/CategoryTransactions.tsx | 2 +- .../src/components/mobile/budget/index.tsx | 35 +- .../mobile/transactions/TransactionEdit.jsx | 3 +- .../components/modals/CloseAccountModal.tsx | 17 +- .../modals/CreateLocalAccountModal.tsx | 11 +- .../src/components/modals/EditRuleModal.jsx | 10 +- .../ImportTransactionsModal.jsx | 32 +- .../payees/ManagePayeesWithData.tsx | 4 +- .../src/components/reports/Overview.tsx | 5 +- .../src/components/rules/ScheduleValue.tsx | 2 +- .../components/schedules/ScheduleDetails.jsx | 2 +- .../transactions/TransactionsTable.jsx | 2 +- packages/desktop-client/src/global-events.ts | 8 +- .../desktop-client/src/hooks/useAccounts.ts | 2 +- .../desktop-client/src/hooks/useCategories.ts | 2 +- .../desktop-client/src/hooks/usePayees.ts | 5 +- packages/desktop-client/src/hooks/useUndo.ts | 23 +- packages/desktop-client/src/index.tsx | 7 +- packages/loot-core/package.json | 1 - .../src/client/accounts/accountSlice.ts | 21 +- .../loot-core/src/client/actions/index.ts | 1 - .../loot-core/src/client/actions/modals.ts | 28 + .../loot-core/src/client/actions/queries.ts | 487 ---------- packages/loot-core/src/client/constants.ts | 9 - .../src/client/queries/queriesSlice.ts | 845 ++++++++++++++++++ .../loot-core/src/client/reducers/index.ts | 2 - .../loot-core/src/client/reducers/queries.ts | 133 --- .../loot-core/src/client/shared-listeners.ts | 4 +- .../src/client/state-types/index.d.ts | 2 - .../src/client/state-types/queries.d.ts | 76 -- packages/loot-core/src/client/store/index.ts | 9 +- packages/loot-core/src/client/store/mock.ts | 5 + packages/loot-core/src/client/undo.ts | 39 + yarn.lock | 8 - 39 files changed, 1094 insertions(+), 851 deletions(-) delete mode 100644 packages/loot-core/src/client/actions/queries.ts create mode 100644 packages/loot-core/src/client/queries/queriesSlice.ts delete mode 100644 packages/loot-core/src/client/reducers/queries.ts delete mode 100644 packages/loot-core/src/client/state-types/queries.d.ts create mode 100644 packages/loot-core/src/client/undo.ts diff --git a/packages/desktop-client/src/components/ManageRules.tsx b/packages/desktop-client/src/components/ManageRules.tsx index 9aacf2fbbf8..f8f348d7482 100644 --- a/packages/desktop-client/src/components/ManageRules.tsx +++ b/packages/desktop-client/src/components/ManageRules.tsx @@ -10,9 +10,9 @@ import React, { import { useTranslation } from 'react-i18next'; import { useSchedules } from 'loot-core/client/data-hooks/schedules'; +import { initiallyLoadPayees } from 'loot-core/client/queries/queriesSlice'; import { q } from 'loot-core/shared/query'; import { pushModal } from 'loot-core/src/client/actions/modals'; -import { initiallyLoadPayees } from 'loot-core/src/client/actions/queries'; import { send } from 'loot-core/src/platform/client/fetch'; import * as undo from 'loot-core/src/platform/client/undo'; import { getNormalisedString } from 'loot-core/src/shared/normalisation'; diff --git a/packages/desktop-client/src/components/accounts/Account.tsx b/packages/desktop-client/src/components/accounts/Account.tsx index f24eeeff790..c104dc00b03 100644 --- a/packages/desktop-client/src/components/accounts/Account.tsx +++ b/packages/desktop-client/src/components/accounts/Account.tsx @@ -15,19 +15,21 @@ import { t } from 'i18next'; import { v4 as uuidv4 } from 'uuid'; import { unlinkAccount } from 'loot-core/client/accounts/accountSlice'; +import { + openAccountCloseModal, + pushModal, + replaceModal, + syncAndDownload, +} from 'loot-core/client/actions'; import { createPayee, getPayees, initiallyLoadPayees, markAccountRead, - openAccountCloseModal, - pushModal, reopenAccount, - replaceModal, - syncAndDownload, updateAccount, updateNewTransactions, -} from 'loot-core/client/actions'; +} from 'loot-core/client/queries/queriesSlice'; import { type AppDispatch } from 'loot-core/client/store'; import { validForTransfer } from 'loot-core/client/transfer'; import { type UndoState } from 'loot-core/server/undo'; @@ -500,7 +502,7 @@ class AccountInternal extends PureComponent< else this.updateQuery(query); if (this.props.accountId) { - this.props.dispatch(markAccountRead(this.props.accountId)); + this.props.dispatch(markAccountRead({ accountId: this.props.accountId })); } }; @@ -684,7 +686,7 @@ class AccountInternal extends PureComponent< } }); - this.props.dispatch(updateNewTransactions(updatedTransaction.id)); + this.props.dispatch(updateNewTransactions({ id: updatedTransaction.id })); }; canCalculateBalance = () => { @@ -736,7 +738,7 @@ class AccountInternal extends PureComponent< const account = this.props.accounts.find( account => account.id === this.props.accountId, ); - this.props.dispatch(updateAccount({ ...account, name })); + this.props.dispatch(updateAccount({ account: { ...account, name } })); this.setState({ editingName: false, nameError: '' }); } }; @@ -784,7 +786,7 @@ class AccountInternal extends PureComponent< this.props.dispatch(openAccountCloseModal(accountId)); break; case 'reopen': - this.props.dispatch(reopenAccount(accountId)); + this.props.dispatch(reopenAccount({ accountId })); break; case 'export': const accountName = this.getAccountTitle(account, accountId); @@ -899,7 +901,7 @@ class AccountInternal extends PureComponent< onCreatePayee = (name: string) => { const trimmed = name.trim(); if (trimmed !== '') { - return this.props.dispatch(createPayee(name)); + return this.props.dispatch(createPayee({ name })).unwrap(); } return null; }; @@ -1277,7 +1279,7 @@ class AccountInternal extends PureComponent< const onConfirmTransfer = async (ids: string[]) => { this.setState({ workingHard: true }); - const payees = await this.props.dispatch(getPayees()); + const payees = await this.props.dispatch(getPayees()).unwrap(); const { data: transactions } = await runQuery( q('transactions') .filter({ id: { $oneof: ids } }) diff --git a/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.tsx b/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.tsx index 5334e6e86d3..fb50745a486 100644 --- a/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.tsx +++ b/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.tsx @@ -15,8 +15,10 @@ import { Trans, useTranslation } from 'react-i18next'; import { css, cx } from '@emotion/css'; -import { createPayee } from 'loot-core/src/client/actions/queries'; -import { getActivePayees } from 'loot-core/src/client/reducers/queries'; +import { + createPayee, + getActivePayees, +} from 'loot-core/client/queries/queriesSlice'; import { getNormalisedString } from 'loot-core/src/shared/normalisation'; import { type AccountEntity, @@ -326,7 +328,8 @@ export function PayeeAutocomplete({ if (!clearOnBlur) { onSelect?.(makeNew(idOrIds, rawInputValue), rawInputValue); } else { - const create = payeeName => dispatch(createPayee(payeeName)); + const create = payeeName => + dispatch(createPayee({ name: payeeName })).unwrap(); if (Array.isArray(idOrIds)) { idOrIds = await Promise.all( diff --git a/packages/desktop-client/src/components/budget/index.tsx b/packages/desktop-client/src/components/budget/index.tsx index f57ed655702..9ba6b5781cf 100644 --- a/packages/desktop-client/src/components/budget/index.tsx +++ b/packages/desktop-client/src/components/budget/index.tsx @@ -3,7 +3,6 @@ import React, { memo, useMemo, useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { - addNotification, applyBudgetAction, createCategory, createGroup, @@ -12,10 +11,10 @@ import { getCategories, moveCategory, moveCategoryGroup, - pushModal, updateCategory, updateGroup, -} from 'loot-core/src/client/actions'; +} from 'loot-core/client/queries/queriesSlice'; +import { addNotification, pushModal } from 'loot-core/src/client/actions'; import { useSpreadsheet } from 'loot-core/src/client/SpreadsheetProvider'; import { send, listen } from 'loot-core/src/platform/client/fetch'; import * as monthUtils from 'loot-core/src/shared/months'; @@ -201,15 +200,15 @@ function BudgetInner(props: BudgetInnerProps) { if (category.id === 'new') { dispatch( - createCategory( - category.name, - category.cat_group, - category.is_income, - category.hidden, - ), + createCategory({ + name: category.name, + groupId: category.cat_group, + isIncome: category.is_income, + isHidden: category.hidden, + }), ); } else { - dispatch(updateCategory(category)); + dispatch(updateCategory({ category })); } }; @@ -222,21 +221,21 @@ function BudgetInner(props: BudgetInnerProps) { category: id, onDelete: transferCategory => { if (id !== transferCategory) { - dispatch(deleteCategory(id, transferCategory)); + dispatch(deleteCategory({ id, transferId: transferCategory })); } }, }), ); } else { - dispatch(deleteCategory(id)); + dispatch(deleteCategory({ id })); } }; const onSaveGroup = group => { if (group.id === 'new') { - dispatch(createGroup(group.name)); + dispatch(createGroup({ name: group.name })); } else { - dispatch(updateGroup(group)); + dispatch(updateGroup({ group })); } }; @@ -256,26 +255,29 @@ function BudgetInner(props: BudgetInnerProps) { pushModal('confirm-category-delete', { group: id, onDelete: transferCategory => { - dispatch(deleteGroup(id, transferCategory)); + dispatch(deleteGroup({ id, transferId: transferCategory })); }, }), ); } else { - dispatch(deleteGroup(id)); + dispatch(deleteGroup({ id })); } }; const onApplyBudgetTemplatesInGroup = async categories => { dispatch( - applyBudgetAction(startMonth, 'apply-multiple-templates', { + applyBudgetAction({ month: startMonth, - categories, + type: 'apply-multiple-templates', + args: { + categories, + }, }), ); }; const onBudgetAction = (month, type, args) => { - dispatch(applyBudgetAction(month, type, args)); + dispatch(applyBudgetAction({ month, type, args })); }; const onShowActivity = (categoryId, month) => { @@ -314,11 +316,19 @@ function BudgetInner(props: BudgetInnerProps) { return; } - dispatch(moveCategory(sortInfo.id, sortInfo.groupId, sortInfo.targetId)); + dispatch( + moveCategory({ + id: sortInfo.id, + groupId: sortInfo.groupId, + targetId: sortInfo.targetId, + }), + ); }; const onReorderGroup = async sortInfo => { - dispatch(moveCategoryGroup(sortInfo.id, sortInfo.targetId)); + dispatch( + moveCategoryGroup({ id: sortInfo.id, targetId: sortInfo.targetId }), + ); }; const onToggleCollapse = () => { diff --git a/packages/desktop-client/src/components/mobile/accounts/AccountTransactions.tsx b/packages/desktop-client/src/components/mobile/accounts/AccountTransactions.tsx index 8144a6f22c6..5182ce9f3ff 100644 --- a/packages/desktop-client/src/components/mobile/accounts/AccountTransactions.tsx +++ b/packages/desktop-client/src/components/mobile/accounts/AccountTransactions.tsx @@ -8,13 +8,9 @@ import React, { import { collapseModals, - getPayees, - markAccountRead, openAccountCloseModal, pushModal, - reopenAccount, syncAndDownload, - updateAccount, } from 'loot-core/client/actions'; import { accountSchedulesQuery, @@ -25,6 +21,12 @@ import { useTransactionsSearch, } from 'loot-core/client/data-hooks/transactions'; import * as queries from 'loot-core/client/queries'; +import { + getPayees, + markAccountRead, + reopenAccount, + updateAccount, +} from 'loot-core/client/queries/queriesSlice'; import { listen, send } from 'loot-core/platform/client/fetch'; import { type Query } from 'loot-core/shared/query'; import { isPreviewId } from 'loot-core/shared/transactions'; @@ -113,7 +115,7 @@ function AccountHeader({ account }: { readonly account: AccountEntity }) { const onSave = useCallback( (account: AccountEntity) => { - dispatch(updateAccount(account)); + dispatch(updateAccount({ account })); }, [dispatch], ); @@ -140,7 +142,7 @@ function AccountHeader({ account }: { readonly account: AccountEntity }) { }, [account.id, dispatch]); const onReopenAccount = useCallback(() => { - dispatch(reopenAccount(account.id)); + dispatch(reopenAccount({ accountId: account.id })); }, [account.id, dispatch]); const onClick = useCallback(() => { @@ -264,7 +266,7 @@ function TransactionListWithPreviews({ useEffect(() => { if (accountId) { - dispatch(markAccountRead(accountId)); + dispatch(markAccountRead({ accountId })); } }, [accountId, dispatch]); diff --git a/packages/desktop-client/src/components/mobile/budget/CategoryTransactions.tsx b/packages/desktop-client/src/components/mobile/budget/CategoryTransactions.tsx index 9626c6e9cfa..6b2fbfe28f2 100644 --- a/packages/desktop-client/src/components/mobile/budget/CategoryTransactions.tsx +++ b/packages/desktop-client/src/components/mobile/budget/CategoryTransactions.tsx @@ -1,11 +1,11 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { getPayees } from 'loot-core/client/actions'; import { useTransactions, useTransactionsSearch, } from 'loot-core/client/data-hooks/transactions'; import * as queries from 'loot-core/client/queries'; +import { getPayees } from 'loot-core/client/queries/queriesSlice'; import { listen } from 'loot-core/platform/client/fetch'; import * as monthUtils from 'loot-core/shared/months'; import { q } from 'loot-core/shared/query'; diff --git a/packages/desktop-client/src/components/mobile/budget/index.tsx b/packages/desktop-client/src/components/mobile/budget/index.tsx index bcec3e25233..8e6d3ababcc 100644 --- a/packages/desktop-client/src/components/mobile/budget/index.tsx +++ b/packages/desktop-client/src/components/mobile/budget/index.tsx @@ -1,9 +1,9 @@ // @ts-strict-ignore import React, { useCallback, useEffect, useState } from 'react'; +import { collapseModals, pushModal, sync } from 'loot-core/client/actions'; import { applyBudgetAction, - collapseModals, createCategory, createGroup, deleteCategory, @@ -11,11 +11,9 @@ import { getCategories, moveCategory, moveCategoryGroup, - pushModal, updateCategory, updateGroup, - sync, -} from 'loot-core/client/actions'; +} from 'loot-core/client/queries/queriesSlice'; import { useSpreadsheet } from 'loot-core/src/client/SpreadsheetProvider'; import { send, listen } from 'loot-core/src/platform/client/fetch'; import * as monthUtils from 'loot-core/src/shared/months'; @@ -86,7 +84,7 @@ export function Budget() { const onBudgetAction = useCallback( async (month, type, args) => { - dispatch(applyBudgetAction(month, type, args)); + dispatch(applyBudgetAction({ month, type, args })); }, [dispatch], ); @@ -114,7 +112,7 @@ export function Budget() { onValidate: name => (!name ? 'Name is required.' : null), onSubmit: async name => { dispatch(collapseModals('budget-page-menu')); - dispatch(createGroup(name)); + dispatch(createGroup({ name })); }, }), ); @@ -127,7 +125,9 @@ export function Budget() { onValidate: name => (!name ? 'Name is required.' : null), onSubmit: async name => { dispatch(collapseModals('category-group-menu')); - dispatch(createCategory(name, groupId, isIncome, false)); + dispatch( + createCategory({ name, groupId, isIncome, isHidden: false }), + ); }, }), ); @@ -137,7 +137,7 @@ export function Budget() { const onSaveGroup = useCallback( group => { - dispatch(updateGroup(group)); + dispatch(updateGroup({ group })); }, [dispatch], ); @@ -164,7 +164,9 @@ export function Budget() { group: groupId, onDelete: transferCategory => { dispatch(collapseModals('category-group-menu')); - dispatch(deleteGroup(groupId, transferCategory)); + dispatch( + deleteGroup({ id: groupId, transferId: transferCategory }), + ); }, }), ); @@ -190,7 +192,7 @@ export function Budget() { const onSaveCategory = useCallback( category => { - dispatch(updateCategory(category)); + dispatch(updateCategory({ category })); }, [dispatch], ); @@ -208,14 +210,19 @@ export function Budget() { onDelete: transferCategory => { if (categoryId !== transferCategory) { dispatch(collapseModals('category-menu')); - dispatch(deleteCategory(categoryId, transferCategory)); + dispatch( + deleteCategory({ + id: categoryId, + transferId: transferCategory, + }), + ); } }, }), ); } else { dispatch(collapseModals('category-menu')); - dispatch(deleteCategory(categoryId)); + dispatch(deleteCategory({ id: categoryId })); } }, [dispatch], @@ -261,7 +268,7 @@ export function Budget() { targetId = catId; } - dispatch(moveCategory(id, groupId, targetId)); + dispatch(moveCategory({ id, groupId, targetId })); }, [categoryGroups, dispatch], ); @@ -274,7 +281,7 @@ export function Budget() { idx < categoryGroups.length - 1 ? categoryGroups[idx + 1].id : null; } - dispatch(moveCategoryGroup(id, targetId)); + dispatch(moveCategoryGroup({ id, targetId })); }, [categoryGroups, dispatch], ); diff --git a/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx b/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx index c4852d5d852..608a277a6f1 100644 --- a/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx +++ b/packages/desktop-client/src/components/mobile/transactions/TransactionEdit.jsx @@ -17,7 +17,8 @@ import { isValid as isValidDate, } from 'date-fns'; -import { pushModal, setLastTransaction } from 'loot-core/client/actions'; +import { pushModal } from 'loot-core/client/actions'; +import { setLastTransaction } from 'loot-core/client/queries/queriesSlice'; import { runQuery } from 'loot-core/src/client/query-helpers'; import { send } from 'loot-core/src/platform/client/fetch'; import * as monthUtils from 'loot-core/src/shared/months'; diff --git a/packages/desktop-client/src/components/modals/CloseAccountModal.tsx b/packages/desktop-client/src/components/modals/CloseAccountModal.tsx index 80c860424ce..430af36ff16 100644 --- a/packages/desktop-client/src/components/modals/CloseAccountModal.tsx +++ b/packages/desktop-client/src/components/modals/CloseAccountModal.tsx @@ -3,11 +3,8 @@ import React, { type FormEvent, useState, type CSSProperties } from 'react'; import { Form } from 'react-aria-components'; import { useTranslation } from 'react-i18next'; // Import useTranslation -import { - closeAccount, - forceCloseAccount, - pushModal, -} from 'loot-core/client/actions'; +import { pushModal } from 'loot-core/client/actions'; +import { closeAccount } from 'loot-core/client/queries/queriesSlice'; import { integerToCurrency } from 'loot-core/src/shared/util'; import { type AccountEntity } from 'loot-core/src/types/models'; @@ -100,7 +97,11 @@ export function CloseAccountModal({ setLoading(true); dispatch( - closeAccount(account.id, transferAccountId || null, categoryId || null), + closeAccount({ + accountId: account.id, + transferAccountId: transferAccountId || null, + categoryId: categoryId || null, + }), ); } }; @@ -229,7 +230,9 @@ export function CloseAccountModal({ onClick={() => { setLoading(true); - dispatch(forceCloseAccount(account.id)); + dispatch( + closeAccount({ accountId: account.id, forced: true }), + ); close(); }} style={{ color: theme.errorText }} diff --git a/packages/desktop-client/src/components/modals/CreateLocalAccountModal.tsx b/packages/desktop-client/src/components/modals/CreateLocalAccountModal.tsx index 273593ba553..d8be444d371 100644 --- a/packages/desktop-client/src/components/modals/CreateLocalAccountModal.tsx +++ b/packages/desktop-client/src/components/modals/CreateLocalAccountModal.tsx @@ -3,7 +3,8 @@ import { type FormEvent, useState } from 'react'; import { Form } from 'react-aria-components'; import { useTranslation } from 'react-i18next'; -import { closeModal, createAccount } from 'loot-core/client/actions'; +import { closeModal } from 'loot-core/client/actions'; +import { createAccount } from 'loot-core/client/queries/queriesSlice'; import { toRelaxedNumber } from 'loot-core/src/shared/util'; import * as useAccounts from '../../hooks/useAccounts'; @@ -63,8 +64,12 @@ export function CreateLocalAccountModal() { if (!nameError && !balanceError) { dispatch(closeModal()); const id = await dispatch( - createAccount(name, toRelaxedNumber(balance), offbudget), - ); + createAccount({ + name, + balance: toRelaxedNumber(balance), + offBudget: offbudget, + }), + ).unwrap(); navigate('/accounts/' + id); } }; diff --git a/packages/desktop-client/src/components/modals/EditRuleModal.jsx b/packages/desktop-client/src/components/modals/EditRuleModal.jsx index d935c01d770..857b61f240d 100644 --- a/packages/desktop-client/src/components/modals/EditRuleModal.jsx +++ b/packages/desktop-client/src/components/modals/EditRuleModal.jsx @@ -4,10 +4,8 @@ import { useTranslation } from 'react-i18next'; import { css } from '@emotion/css'; import { v4 as uuid } from 'uuid'; -import { - initiallyLoadPayees, - setUndoEnabled, -} from 'loot-core/src/client/actions/queries'; +import { initiallyLoadPayees } from 'loot-core/client/queries/queriesSlice'; +import { enableUndo, disableUndo } from 'loot-core/client/undo'; import { useSchedules } from 'loot-core/src/client/data-hooks/schedules'; import { runQuery } from 'loot-core/src/client/query-helpers'; import { send } from 'loot-core/src/platform/client/fetch'; @@ -795,8 +793,8 @@ export function EditRuleModal({ defaultRule, onSave: originalOnSave }) { dispatch(initiallyLoadPayees()); // Disable undo while this modal is open - setUndoEnabled(false); - return () => setUndoEnabled(true); + disableUndo(); + return () => enableUndo(); }, [dispatch]); useEffect(() => { diff --git a/packages/desktop-client/src/components/modals/ImportTransactionsModal/ImportTransactionsModal.jsx b/packages/desktop-client/src/components/modals/ImportTransactionsModal/ImportTransactionsModal.jsx index 1eb5b2c0859..9b750c7b81a 100644 --- a/packages/desktop-client/src/components/modals/ImportTransactionsModal/ImportTransactionsModal.jsx +++ b/packages/desktop-client/src/components/modals/ImportTransactionsModal/ImportTransactionsModal.jsx @@ -7,8 +7,8 @@ import { getPayees, importPreviewTransactions, importTransactions, - parseTransactions, -} from 'loot-core/client/actions'; +} from 'loot-core/client/queries/queriesSlice'; +import { send } from 'loot-core/platform/client/fetch'; import { amountToInteger } from 'loot-core/src/shared/util'; import { useDateFormat } from '../../../hooks/useDateFormat'; @@ -267,7 +267,7 @@ export function ImportTransactionsModal({ options }) { // Retreive the transactions that would be updated (along with the existing trx) const previewTrx = await dispatch( importPreviewTransactions(accountId, previewTransactions), - ); + ).unwrap(); const matchedUpdateMap = previewTrx.reduce((map, entry) => { map[entry.transaction.trx_id] = entry; return map; @@ -321,8 +321,12 @@ export function ImportTransactionsModal({ options }) { setFilename(filename); setFileType(filetype); - const { errors, transactions: parsedTransactions = [] } = await dispatch( - parseTransactions(filename, options), + const { errors, transactions: parsedTransactions = [] } = await send( + 'transactions-parse-file', + { + filepath: filename, + options, + }, ); let index = 0; @@ -399,15 +403,7 @@ export function ImportTransactionsModal({ options }) { setTransactions(transactionPreview); } }, - [ - accountId, - dispatch, - getImportPreview, - inOutMode, - multiplierAmount, - outValue, - prefs, - ], + [accountId, getImportPreview, inOutMode, multiplierAmount, outValue, prefs], ); function onMultiplierChange(e) { @@ -655,8 +651,12 @@ export function ImportTransactionsModal({ options }) { } const didChange = await dispatch( - importTransactions(accountId, finalTransactions, reconcile), - ); + importTransactions({ + id: accountId, + transactions: finalTransactions, + reconcile, + }), + ).unwrap(); if (didChange) { await dispatch(getPayees()); } diff --git a/packages/desktop-client/src/components/payees/ManagePayeesWithData.tsx b/packages/desktop-client/src/components/payees/ManagePayeesWithData.tsx index fc075bb7f8d..f888bde517c 100644 --- a/packages/desktop-client/src/components/payees/ManagePayeesWithData.tsx +++ b/packages/desktop-client/src/components/payees/ManagePayeesWithData.tsx @@ -1,10 +1,10 @@ import React, { useState, useEffect, useCallback } from 'react'; +import { pushModal } from 'loot-core/client/actions'; import { getPayees, initiallyLoadPayees, - pushModal, -} from 'loot-core/client/actions'; +} from 'loot-core/client/queries/queriesSlice'; import { type UndoState } from 'loot-core/server/undo'; import { send, listen } from 'loot-core/src/platform/client/fetch'; import * as undo from 'loot-core/src/platform/client/undo'; diff --git a/packages/desktop-client/src/components/reports/Overview.tsx b/packages/desktop-client/src/components/reports/Overview.tsx index 7820a03d359..785dc07cb6f 100644 --- a/packages/desktop-client/src/components/reports/Overview.tsx +++ b/packages/desktop-client/src/components/reports/Overview.tsx @@ -21,6 +21,7 @@ import { import { useAccounts } from '../../hooks/useAccounts'; import { useNavigate } from '../../hooks/useNavigate'; import { useSyncedPref } from '../../hooks/useSyncedPref'; +import { useUndo } from '../../hooks/useUndo'; import { useAppDispatch } from '../../redux'; import { breakpoints } from '../../tokens'; import { Button } from '../common/Button2'; @@ -109,6 +110,8 @@ export function Overview() { [closeNotifications], ); + const { undo } = useUndo(); + const onDispatchSucessNotification = (message: string) => { dispatch( addNotification({ @@ -120,7 +123,7 @@ export function Overview() { messageActions: { undo: () => { closeNotifications(); - window.__actionsForMenu.undo(); + undo(); }, }, }), diff --git a/packages/desktop-client/src/components/rules/ScheduleValue.tsx b/packages/desktop-client/src/components/rules/ScheduleValue.tsx index 736b0378cf6..9d4e70aff8a 100644 --- a/packages/desktop-client/src/components/rules/ScheduleValue.tsx +++ b/packages/desktop-client/src/components/rules/ScheduleValue.tsx @@ -2,7 +2,7 @@ import React, { useMemo } from 'react'; import { useSchedules } from 'loot-core/client/data-hooks/schedules'; import { q } from 'loot-core/shared/query'; -import { getPayeesById } from 'loot-core/src/client/reducers/queries'; +import { getPayeesById } from 'loot-core/src/client/queries/queriesSlice'; import { describeSchedule } from 'loot-core/src/shared/schedules'; import { type ScheduleEntity } from 'loot-core/src/types/models'; diff --git a/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx b/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx index 989b1593b50..a91472dfb92 100644 --- a/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx +++ b/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx @@ -3,7 +3,7 @@ import { Trans, useTranslation } from 'react-i18next'; import { t } from 'i18next'; -import { getPayeesById } from 'loot-core/client/reducers/queries'; +import { getPayeesById } from 'loot-core/client/queries/queriesSlice'; import { pushModal } from 'loot-core/src/client/actions/modals'; import { runQuery, liveQuery } from 'loot-core/src/client/query-helpers'; import { send, sendCatch } from 'loot-core/src/platform/client/fetch'; diff --git a/packages/desktop-client/src/components/transactions/TransactionsTable.jsx b/packages/desktop-client/src/components/transactions/TransactionsTable.jsx index 541223095fe..5088b5db758 100644 --- a/packages/desktop-client/src/components/transactions/TransactionsTable.jsx +++ b/packages/desktop-client/src/components/transactions/TransactionsTable.jsx @@ -26,7 +26,7 @@ import { getAccountsById, getPayeesById, getCategoriesById, -} from 'loot-core/src/client/reducers/queries'; +} from 'loot-core/src/client/queries/queriesSlice'; import { evalArithmetic } from 'loot-core/src/shared/arithmetic'; import { currentDay } from 'loot-core/src/shared/months'; import * as monthUtils from 'loot-core/src/shared/months'; diff --git a/packages/desktop-client/src/global-events.ts b/packages/desktop-client/src/global-events.ts index 257ede994cd..b6ca55dff5e 100644 --- a/packages/desktop-client/src/global-events.ts +++ b/packages/desktop-client/src/global-events.ts @@ -4,15 +4,17 @@ import { addNotification, closeBudgetUI, closeModal, - getAccounts, - getCategories, - getPayees, loadPrefs, pushModal, reloadApp, replaceModal, setAppState, } from 'loot-core/client/actions'; +import { + getAccounts, + getCategories, + getPayees, +} from 'loot-core/client/queries/queriesSlice'; import { type AppStore } from 'loot-core/client/store'; import * as sharedListeners from 'loot-core/src/client/shared-listeners'; import { listen } from 'loot-core/src/platform/client/fetch'; diff --git a/packages/desktop-client/src/hooks/useAccounts.ts b/packages/desktop-client/src/hooks/useAccounts.ts index 7bc9b13117f..3bcb2a4de13 100644 --- a/packages/desktop-client/src/hooks/useAccounts.ts +++ b/packages/desktop-client/src/hooks/useAccounts.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react'; -import { getAccounts } from 'loot-core/src/client/actions'; +import { getAccounts } from 'loot-core/client/queries/queriesSlice'; import { useAppSelector, useAppDispatch } from '../redux'; diff --git a/packages/desktop-client/src/hooks/useCategories.ts b/packages/desktop-client/src/hooks/useCategories.ts index 4af19885a55..cbc479f0d05 100644 --- a/packages/desktop-client/src/hooks/useCategories.ts +++ b/packages/desktop-client/src/hooks/useCategories.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react'; -import { getCategories } from 'loot-core/src/client/actions'; +import { getCategories } from 'loot-core/client/queries/queriesSlice'; import { useAppSelector, useAppDispatch } from '../redux'; diff --git a/packages/desktop-client/src/hooks/usePayees.ts b/packages/desktop-client/src/hooks/usePayees.ts index 6c2fb62ff6e..b1424a582e7 100644 --- a/packages/desktop-client/src/hooks/usePayees.ts +++ b/packages/desktop-client/src/hooks/usePayees.ts @@ -1,6 +1,9 @@ import { useEffect } from 'react'; -import { getCommonPayees, getPayees } from 'loot-core/src/client/actions'; +import { + getCommonPayees, + getPayees, +} from 'loot-core/client/queries/queriesSlice'; import { useAppSelector, useAppDispatch } from '../redux'; diff --git a/packages/desktop-client/src/hooks/useUndo.ts b/packages/desktop-client/src/hooks/useUndo.ts index f13eed64df4..bb47097ec80 100644 --- a/packages/desktop-client/src/hooks/useUndo.ts +++ b/packages/desktop-client/src/hooks/useUndo.ts @@ -1,7 +1,8 @@ import { useCallback } from 'react'; -import { undo, redo, addNotification } from 'loot-core/client/actions'; +import { addNotification } from 'loot-core/client/actions'; import { type Notification } from 'loot-core/client/state-types/notifications'; +import { redo, undo } from 'loot-core/client/undo'; import { useAppDispatch } from '../redux'; @@ -17,14 +18,6 @@ const timeout = 10000; export function useUndo(): UndoActions { const dispatch = useAppDispatch(); - const dispatchUndo = useCallback(() => { - dispatch(undo()); - }, [dispatch]); - - const dispatchRedo = useCallback(() => { - dispatch(redo()); - }, [dispatch]); - const showUndoNotification = useCallback( (notification: Notification) => { dispatch( @@ -33,13 +26,13 @@ export function useUndo(): UndoActions { timeout, button: { title: 'Undo', - action: dispatchUndo, + action: undo, }, ...notification, }), ); }, - [dispatch, dispatchUndo], + [dispatch], ); const showRedoNotification = useCallback( @@ -50,18 +43,18 @@ export function useUndo(): UndoActions { timeout, button: { title: 'Redo', - action: dispatchRedo, + action: redo, }, ...notificaton, }), ); }, - [dispatch, dispatchRedo], + [dispatch], ); return { - undo: dispatchUndo, - redo: dispatchRedo, + undo, + redo, showUndoNotification, showRedoNotification, }; diff --git a/packages/desktop-client/src/index.tsx b/packages/desktop-client/src/index.tsx index ab15e1c5f23..c002b53a394 100644 --- a/packages/desktop-client/src/index.tsx +++ b/packages/desktop-client/src/index.tsx @@ -15,8 +15,10 @@ import { createRoot } from 'react-dom/client'; import * as accountSlice from 'loot-core/src/client/accounts/accountSlice'; import * as actions from 'loot-core/src/client/actions'; +import * as queriesSlice from 'loot-core/src/client/queries/queriesSlice'; import { runQuery } from 'loot-core/src/client/query-helpers'; import { store } from 'loot-core/src/client/store'; +import { redo, undo } from 'loot-core/src/client/undo'; import { send } from 'loot-core/src/platform/client/fetch'; import { q } from 'loot-core/src/shared/query'; @@ -32,6 +34,7 @@ const boundActions = bindActionCreators( { ...actions, ...accountSlice.actions, + ...queriesSlice.actions, }, store.dispatch, ); @@ -43,6 +46,8 @@ declare global { // eslint-disable-next-line @typescript-eslint/consistent-type-definitions interface Window { __actionsForMenu: typeof boundActions & { + undo: typeof undo; + redo: typeof redo; inputFocused: typeof inputFocused; }; @@ -61,7 +66,7 @@ function inputFocused() { } // Expose this to the main process to menu items can access it -window.__actionsForMenu = { ...boundActions, inputFocused }; +window.__actionsForMenu = { ...boundActions, undo, redo, inputFocused }; // Expose send for fun! window.$send = send; diff --git a/packages/loot-core/package.json b/packages/loot-core/package.json index 73213cbd959..b544e42de55 100644 --- a/packages/loot-core/package.json +++ b/packages/loot-core/package.json @@ -36,7 +36,6 @@ "mitt": "^3.0.1", "reselect": "^4.1.8", "slash": "3.0.0", - "throttleit": "^1.0.1", "uuid": "^9.0.1" }, "devDependencies": { diff --git a/packages/loot-core/src/client/accounts/accountSlice.ts b/packages/loot-core/src/client/accounts/accountSlice.ts index 6c477e54e36..ef2b934c0d2 100644 --- a/packages/loot-core/src/client/accounts/accountSlice.ts +++ b/packages/loot-core/src/client/accounts/accountSlice.ts @@ -6,8 +6,12 @@ import { import { send } from '../../platform/client/fetch'; import { type AccountEntity, type TransactionEntity } from '../../types/models'; -import { addNotification, getAccounts, getPayees } from '../actions'; -import * as constants from '../constants'; +import { addNotification } from '../actions'; +import { + getAccounts, + getPayees, + setNewTransactions, +} from '../queries/queriesSlice'; import { type AppDispatch, type RootState } from '../store'; const createAppAsyncThunk = createAsyncThunk.withTypes<{ @@ -284,12 +288,13 @@ export const syncAccounts = createAppAsyncThunk( } // Set new transactions - thunkApi.dispatch({ - type: constants.SET_NEW_TRANSACTIONS, - newTransactions, - matchedTransactions, - updatedAccounts, - }); + thunkApi.dispatch( + setNewTransactions({ + newTransactions, + matchedTransactions, + updatedAccounts, + }), + ); // Reset the sync state back to empty (fallback in case something breaks // in the logic above) diff --git a/packages/loot-core/src/client/actions/index.ts b/packages/loot-core/src/client/actions/index.ts index ef2b435b02c..51ebdfd4ea1 100644 --- a/packages/loot-core/src/client/actions/index.ts +++ b/packages/loot-core/src/client/actions/index.ts @@ -1,4 +1,3 @@ -export * from './queries'; export * from './modals'; export * from './notifications'; export * from './prefs'; diff --git a/packages/loot-core/src/client/actions/modals.ts b/packages/loot-core/src/client/actions/modals.ts index 3d0a23ccee1..e810a4bcb24 100644 --- a/packages/loot-core/src/client/actions/modals.ts +++ b/packages/loot-core/src/client/actions/modals.ts @@ -1,3 +1,5 @@ +import { send } from '../../platform/client/fetch'; +import { type AccountEntity } from '../../types/models'; import * as constants from '../constants'; import type { OptionlessModal, @@ -10,6 +12,7 @@ import type { FinanceModals, Modal, } from '../state-types/modals'; +import { type AppDispatch, type GetRootState } from '../store'; export function pushModal( name: M, @@ -52,3 +55,28 @@ export function closeModal(): CloseModalAction { export function collapseModals(rootModalName: string) { return { type: constants.COLLAPSE_MODALS, rootModalName }; } + +export function openAccountCloseModal(accountId: AccountEntity['id']) { + return async (dispatch: AppDispatch, getState: GetRootState) => { + const { + balance, + numTransactions, + }: { balance: number; numTransactions: number } = await send( + 'account-properties', + { + id: accountId, + }, + ); + const account = getState().queries.accounts.find( + acct => acct.id === accountId, + ); + + dispatch( + pushModal('close-account' as ModalType, { + account, + balance, + canDelete: numTransactions === 0, + }), + ); + }; +} diff --git a/packages/loot-core/src/client/actions/queries.ts b/packages/loot-core/src/client/actions/queries.ts deleted file mode 100644 index 7220285d2eb..00000000000 --- a/packages/loot-core/src/client/actions/queries.ts +++ /dev/null @@ -1,487 +0,0 @@ -// @ts-strict-ignore -import { t } from 'i18next'; -import throttle from 'throttleit'; - -import { send } from '../../platform/client/fetch'; -import { type AccountEntity } from '../../types/models'; -import * as constants from '../constants'; -import { - type MarkAccountReadAction, - type SetLastTransactionAction, - type UpdateNewTransactionsAction, -} from '../state-types/queries'; -import { type AppDispatch, type GetRootState } from '../store'; - -import { pushModal } from './modals'; -import { addNotification, addGenericErrorNotification } from './notifications'; - -export function applyBudgetAction(month, type, args) { - return async (dispatch: AppDispatch) => { - switch (type) { - case 'budget-amount': - await send('budget/budget-amount', { - month, - category: args.category, - amount: args.amount, - }); - break; - case 'copy-last': - await send('budget/copy-previous-month', { month }); - break; - case 'set-zero': - await send('budget/set-zero', { month }); - break; - case 'set-3-avg': - await send('budget/set-3month-avg', { month }); - break; - case 'check-templates': - dispatch(addNotification(await send('budget/check-templates'))); - break; - case 'apply-goal-template': - dispatch( - addNotification(await send('budget/apply-goal-template', { month })), - ); - break; - case 'overwrite-goal-template': - dispatch( - addNotification( - await send('budget/overwrite-goal-template', { month }), - ), - ); - break; - case 'cleanup-goal-template': - dispatch( - addNotification( - await send('budget/cleanup-goal-template', { month }), - ), - ); - break; - case 'hold': - await send('budget/hold-for-next-month', { - month, - amount: args.amount, - }); - break; - case 'reset-hold': - await send('budget/reset-hold', { month }); - break; - case 'cover-overspending': - await send('budget/cover-overspending', { - month, - to: args.to, - from: args.from, - }); - break; - case 'transfer-available': - await send('budget/transfer-available', { - month, - amount: args.amount, - category: args.category, - }); - break; - case 'cover-overbudgeted': - await send('budget/cover-overbudgeted', { - month, - category: args.category, - }); - break; - case 'transfer-category': - await send('budget/transfer-category', { - month, - amount: args.amount, - from: args.from, - to: args.to, - }); - break; - case 'carryover': { - await send('budget/set-carryover', { - startMonth: month, - category: args.category, - flag: args.flag, - }); - break; - } - case 'apply-single-category-template': - await send('budget/apply-single-template', { - month, - category: args.category, - }); - break; - case 'apply-multiple-templates': - dispatch( - addNotification( - await send('budget/apply-multiple-templates', { - month, - categoryIds: args.categories, - }), - ), - ); - break; - case 'set-single-3-avg': - await send('budget/set-n-month-avg', { - month, - N: 3, - category: args.category, - }); - break; - case 'set-single-6-avg': - await send('budget/set-n-month-avg', { - month, - N: 6, - category: args.category, - }); - break; - case 'set-single-12-avg': - await send('budget/set-n-month-avg', { - month, - N: 12, - category: args.category, - }); - break; - case 'copy-single-last': - await send('budget/copy-single-month', { - month, - category: args.category, - }); - break; - default: - } - }; -} - -export function getCategories() { - return async (dispatch: AppDispatch) => { - const categories = await send('get-categories'); - dispatch({ - type: constants.LOAD_CATEGORIES, - categories, - }); - return categories; - }; -} - -export function createCategory( - name: string, - groupId: string, - isIncome: boolean, - hidden: boolean, -) { - return async (dispatch: AppDispatch) => { - const id = await send('category-create', { - name, - groupId, - isIncome, - hidden, - }); - dispatch(getCategories()); - return id; - }; -} - -export function deleteCategory(id: string, transferId?: string) { - return async (dispatch: AppDispatch) => { - const { error } = await send('category-delete', { id, transferId }); - - if (error) { - switch (error) { - case 'category-type': - dispatch( - addNotification({ - type: 'error', - message: t( - 'A category must be transferred to another of the same type (expense or income)', - ), - }), - ); - break; - default: - dispatch(addGenericErrorNotification()); - } - - throw new Error(error); - } else { - dispatch(getCategories()); - // Also need to refresh payees because they might use one of the - // deleted categories as the default category - dispatch(getPayees()); - } - }; -} - -export function updateCategory(category) { - return async (dispatch: AppDispatch) => { - await send('category-update', category); - dispatch(getCategories()); - }; -} - -export function moveCategory(id, groupId, targetId) { - return async (dispatch: AppDispatch) => { - await send('category-move', { id, groupId, targetId }); - await dispatch(getCategories()); - }; -} - -export function moveCategoryGroup(id, targetId) { - return async (dispatch: AppDispatch) => { - await send('category-group-move', { id, targetId }); - await dispatch(getCategories()); - }; -} - -export function createGroup(name) { - return async (dispatch: AppDispatch) => { - const id = await send('category-group-create', { name }); - dispatch(getCategories()); - return id; - }; -} - -export function updateGroup(group) { - // Strip off the categories field if it exist. It's not a real db - // field but groups have this extra field in the client most of the - // time - const { categories, ...rawGroup } = group; - - return async dispatch => { - await send('category-group-update', rawGroup); - await dispatch(getCategories()); - }; -} - -export function deleteGroup(id, transferId?) { - return async function (dispatch) { - await send('category-group-delete', { id, transferId }); - await dispatch(getCategories()); - // See `deleteCategory` for why we need this - await dispatch(getPayees()); - }; -} - -export function getPayees() { - return async (dispatch: AppDispatch) => { - const payees = await send('payees-get'); - dispatch({ - type: constants.LOAD_PAYEES, - payees, - }); - return payees; - }; -} - -export function getCommonPayees() { - return async (dispatch: AppDispatch) => { - const payees = await send('common-payees-get'); - dispatch({ - type: constants.LOAD_COMMON_PAYEES, - payees, - }); - return payees; - }; -} - -export function initiallyLoadPayees() { - return async (dispatch: AppDispatch, getState: GetRootState) => { - if (getState().queries.payees.length === 0) { - return dispatch(getPayees()); - } - }; -} - -export function createPayee(name: string) { - return async (dispatch: AppDispatch) => { - const id = await send('payee-create', { name: name.trim() }); - dispatch(getPayees()); - return id; - }; -} - -export function getAccounts() { - return async (dispatch: AppDispatch) => { - const accounts = await send('accounts-get'); - dispatch({ type: constants.LOAD_ACCOUNTS, accounts }); - return accounts; - }; -} - -export function updateAccount(account: AccountEntity) { - return async (dispatch: AppDispatch) => { - dispatch({ type: constants.UPDATE_ACCOUNT, account }); - await send('account-update', account); - }; -} - -export function createAccount(name, balance, offBudget) { - return async (dispatch: AppDispatch) => { - const id = await send('account-create', { name, balance, offBudget }); - await dispatch(getAccounts()); - await dispatch(getPayees()); - return id; - }; -} - -export function openAccountCloseModal(accountId) { - return async (dispatch: AppDispatch, getState: GetRootState) => { - const { balance, numTransactions } = await send('account-properties', { - id: accountId, - }); - const account = getState().queries.accounts.find( - acct => acct.id === accountId, - ); - - dispatch( - pushModal('close-account', { - account, - balance, - canDelete: numTransactions === 0, - }), - ); - }; -} - -export function closeAccount( - accountId: string, - transferAccountId: string, - categoryId: string, - forced?: boolean, -) { - return async (dispatch: AppDispatch) => { - await send('account-close', { - id: accountId, - transferAccountId, - categoryId, - forced, - }); - dispatch(getAccounts()); - }; -} - -export function reopenAccount(accountId) { - return async (dispatch: AppDispatch) => { - await send('account-reopen', { id: accountId }); - dispatch(getAccounts()); - }; -} - -export function forceCloseAccount(accountId) { - return closeAccount(accountId, null, null, true); -} - -// Remember the last transaction manually added to the system -export function setLastTransaction( - transaction: SetLastTransactionAction['transaction'], -): SetLastTransactionAction { - return { - type: constants.SET_LAST_TRANSACTION, - transaction, - }; -} - -export function parseTransactions(filepath, options) { - return async () => { - return await send('transactions-parse-file', { - filepath, - options, - }); - }; -} - -export function importPreviewTransactions(id: string, transactions) { - return async (dispatch: AppDispatch): Promise => { - const { errors = [], updatedPreview } = await send('transactions-import', { - accountId: id, - transactions, - isPreview: true, - }); - - errors.forEach(error => { - dispatch( - addNotification({ - type: 'error', - message: error.message, - }), - ); - }); - - return updatedPreview; - }; -} - -export function importTransactions(id: string, transactions, reconcile = true) { - return async (dispatch: AppDispatch): Promise => { - if (!reconcile) { - await send('api/transactions-add', { - accountId: id, - transactions, - }); - - return true; - } - - const { - errors = [], - added, - updated, - } = await send('transactions-import', { - accountId: id, - transactions, - isPreview: false, - }); - - errors.forEach(error => { - dispatch( - addNotification({ - type: 'error', - message: error.message, - }), - ); - }); - - dispatch({ - type: constants.SET_NEW_TRANSACTIONS, - newTransactions: added, - matchedTransactions: updated, - updatedAccounts: added.length > 0 ? [id] : [], - }); - - return added.length > 0 || updated.length > 0; - }; -} - -export function updateNewTransactions(changedId): UpdateNewTransactionsAction { - return { - type: constants.UPDATE_NEW_TRANSACTIONS, - changedId, - }; -} - -export function markAccountRead(accountId): MarkAccountReadAction { - return { - type: constants.MARK_ACCOUNT_READ, - accountId, - }; -} - -const _undo = throttle(() => send('undo'), 100); -const _redo = throttle(() => send('redo'), 100); - -let _undoEnabled = true; -export function setUndoEnabled(flag: boolean) { - _undoEnabled = flag; -} - -export function undo() { - return async () => { - if (_undoEnabled) { - _undo(); - } - }; -} - -export function redo() { - return async () => { - if (_undoEnabled) { - _redo(); - } - }; -} diff --git a/packages/loot-core/src/client/constants.ts b/packages/loot-core/src/client/constants.ts index 40406878536..c2d2de63323 100644 --- a/packages/loot-core/src/client/constants.ts +++ b/packages/loot-core/src/client/constants.ts @@ -1,12 +1,3 @@ -export const SET_NEW_TRANSACTIONS = 'SET_NEW_TRANSACTIONS'; -export const UPDATE_NEW_TRANSACTIONS = 'UPDATE_NEW_TRANSACTIONS'; -export const SET_LAST_TRANSACTION = 'SET_LAST_TRANSACTION'; -export const MARK_ACCOUNT_READ = 'MARK_ACCOUNT_READ'; -export const LOAD_ACCOUNTS = 'LOAD_ACCOUNTS'; -export const UPDATE_ACCOUNT = 'UPDATE_ACCOUNT'; -export const LOAD_CATEGORIES = 'LOAD_CATEGORIES'; -export const LOAD_COMMON_PAYEES = 'LOAD_COMMON_PAYEES'; -export const LOAD_PAYEES = 'LOAD_PAYEES'; export const SET_PREFS = 'SET_PREFS'; export const MERGE_LOCAL_PREFS = 'MERGE_LOCAL_PREFS'; export const MERGE_GLOBAL_PREFS = 'MERGE_GLOBAL_PREFS'; diff --git a/packages/loot-core/src/client/queries/queriesSlice.ts b/packages/loot-core/src/client/queries/queriesSlice.ts new file mode 100644 index 00000000000..45aab6ced1a --- /dev/null +++ b/packages/loot-core/src/client/queries/queriesSlice.ts @@ -0,0 +1,845 @@ +// @ts-strict-ignore +import { + createAsyncThunk, + createSlice, + type PayloadAction, +} from '@reduxjs/toolkit'; +import { t } from 'i18next'; +import memoizeOne from 'memoize-one'; + +import { send } from '../../platform/client/fetch'; +import { groupById } from '../../shared/util'; +import { + type CategoryEntity, + type CategoryGroupEntity, + type TransactionEntity, + type AccountEntity, + type PayeeEntity, +} from '../../types/models'; +import { addGenericErrorNotification, addNotification } from '../actions'; +import { type AppDispatch, type RootState } from '../store'; + +const createAppAsyncThunk = createAsyncThunk.withTypes<{ + state: RootState; + dispatch: AppDispatch; +}>(); + +type Categories = { + grouped: CategoryGroupEntity[]; + list: CategoryEntity[]; +}; + +type QueriesState = { + newTransactions: Array; + matchedTransactions: Array; + lastTransaction: TransactionEntity | null; + updatedAccounts: Array; + accounts: AccountEntity[]; + accountsLoaded: boolean; + categories: Categories; + categoriesLoaded: boolean; + commonPayeesLoaded: boolean; + commonPayees: PayeeEntity[]; + payees: PayeeEntity[]; + payeesLoaded: boolean; +}; + +const initialState: QueriesState = { + newTransactions: [], + matchedTransactions: [], + lastTransaction: null, + updatedAccounts: [], + accounts: [], + accountsLoaded: false, + categories: { + grouped: [], + list: [], + }, + categoriesLoaded: false, + commonPayees: [], + commonPayeesLoaded: false, + payees: [], + payeesLoaded: false, +}; + +type MarkAccountReadAction = PayloadAction<{ + accountId: AccountEntity['id']; +}>; + +// Account actions + +type CreateAccountArgs = { + name: AccountEntity['name']; + balance: AccountEntity['balance_current']; + offBudget: boolean; +}; + +export const createAccount = createAppAsyncThunk( + 'queries/createAccount', + async ({ name, balance, offBudget }: CreateAccountArgs, thunkApi) => { + const id: AccountEntity['id'] = await send('account-create', { + name, + balance, + offBudget, + }); + await thunkApi.dispatch(getAccounts()); + await thunkApi.dispatch(getPayees()); + return id; + }, +); + +type CloseAccountArgs = { + accountId: AccountEntity['id']; + transferAccountId?: AccountEntity['id']; + categoryId?: CategoryEntity['id']; + forced?: boolean; +}; + +export const closeAccount = createAppAsyncThunk( + 'queries/closeAccount', + async ( + { accountId, transferAccountId, categoryId, forced }: CloseAccountArgs, + thunkApi, + ) => { + await send('account-close', { + id: accountId, + transferAccountId: transferAccountId || null, + categoryId: categoryId || null, + forced, + }); + thunkApi.dispatch(getAccounts()); + }, +); + +type ReopenAccountArgs = { + accountId: AccountEntity['id']; +}; + +export const reopenAccount = createAppAsyncThunk( + 'queries/reopenAccount', + async ({ accountId }: ReopenAccountArgs, thunkApi) => { + await send('account-reopen', { id: accountId }); + thunkApi.dispatch(getAccounts()); + }, +); + +type UpdateAccountArgs = { + account: AccountEntity; +}; + +export const updateAccount = createAppAsyncThunk( + 'queries/updateAccount', + async ({ account }: UpdateAccountArgs) => { + await send('account-update', account); + return account; + }, +); + +export const getAccounts = createAppAsyncThunk( + 'queries/getAccounts', + async () => { + const accounts: AccountEntity[] = await send('accounts-get'); + return accounts; + }, +); + +// Category actions + +type CreateGroupArgs = { + name: CategoryGroupEntity['name']; +}; + +export const createGroup = createAppAsyncThunk( + 'queries/createGroup', + async ({ name }: CreateGroupArgs, thunkApi) => { + const id = await send('category-group-create', { name }); + thunkApi.dispatch(getCategories()); + return id; + }, +); + +type UpdateGroupArgs = { + group: CategoryGroupEntity; +}; + +export const updateGroup = createAppAsyncThunk( + 'queries/updateGroup', + async ({ group }: UpdateGroupArgs, thunkApi) => { + // Strip off the categories field if it exist. It's not a real db + // field but groups have this extra field in the client most of the time + const { categories: _, ...groupNoCategories } = group; + await send('category-group-update', groupNoCategories); + await thunkApi.dispatch(getCategories()); + }, +); + +type DeleteGroupArgs = { + id: CategoryGroupEntity['id']; + transferId?: CategoryGroupEntity['id']; +}; + +export const deleteGroup = createAppAsyncThunk( + 'queries/deleteGroup', + async ({ id, transferId }: DeleteGroupArgs, thunkApi) => { + await send('category-group-delete', { id, transferId }); + await thunkApi.dispatch(getCategories()); + // See `deleteCategory` for why we need this + await thunkApi.dispatch(getPayees()); + }, +); + +type CreateCategoryArgs = { + name: CategoryEntity['name']; + groupId: CategoryGroupEntity['id']; + isIncome: boolean; + isHidden: boolean; +}; +export const createCategory = createAppAsyncThunk( + 'queries/createCategory', + async ( + { name, groupId, isIncome, isHidden }: CreateCategoryArgs, + thunkApi, + ) => { + const id = await send('category-create', { + name, + groupId, + isIncome, + hidden: isHidden, + }); + thunkApi.dispatch(getCategories()); + return id; + }, +); + +type UpdateCategoryArgs = { + category: CategoryEntity; +}; + +export const updateCategory = createAppAsyncThunk( + 'queries/updateCategory', + async ({ category }: UpdateCategoryArgs, thunkApi) => { + await send('category-update', category); + thunkApi.dispatch(getCategories()); + }, +); + +type DeleteCategoryArgs = { + id: CategoryEntity['id']; + transferId?: CategoryEntity['id']; +}; + +export const deleteCategory = createAppAsyncThunk( + 'queries/deleteCategory', + async ({ id, transferId }: DeleteCategoryArgs, thunkApi) => { + const { error } = await send('category-delete', { id, transferId }); + + if (error) { + switch (error) { + case 'category-type': + thunkApi.dispatch( + addNotification({ + type: 'error', + message: t( + 'A category must be transferred to another of the same type (expense or income)', + ), + }), + ); + break; + default: + thunkApi.dispatch(addGenericErrorNotification()); + } + + throw new Error(error); + } else { + thunkApi.dispatch(getCategories()); + // Also need to refresh payees because they might use one of the + // deleted categories as the default category + thunkApi.dispatch(getPayees()); + } + }, +); + +type MoveCategoryArgs = { + id: CategoryEntity['id']; + groupId: CategoryGroupEntity['id']; + targetId: CategoryEntity['id']; +}; + +export const moveCategory = createAppAsyncThunk( + 'queries/moveCategory', + async ({ id, groupId, targetId }: MoveCategoryArgs, thunkApi) => { + await send('category-move', { id, groupId, targetId }); + await thunkApi.dispatch(getCategories()); + }, +); + +type MoveCategoryGroupArgs = { + id: CategoryGroupEntity['id']; + targetId: CategoryGroupEntity['id']; +}; + +export const moveCategoryGroup = createAppAsyncThunk( + 'queries/moveCategoryGroup', + async ({ id, targetId }: MoveCategoryGroupArgs, thunkApi) => { + await send('category-group-move', { id, targetId }); + await thunkApi.dispatch(getCategories()); + }, +); + +export const getCategories = createAppAsyncThunk( + 'queries/getCategories', + async () => { + const categories: Categories = await send('get-categories'); + return categories; + }, +); + +// Payee actions + +type CreatePayeeArgs = { + name: PayeeEntity['name']; +}; + +export const createPayee = createAppAsyncThunk( + 'queries/createPayee', + async ({ name }: CreatePayeeArgs, thunkApi) => { + const id: PayeeEntity['id'] = await send('payee-create', { + name: name.trim(), + }); + thunkApi.dispatch(getPayees()); + return id; + }, +); + +export const initiallyLoadPayees = createAppAsyncThunk( + 'queries/initiallyLoadPayees', + async (_, thunkApi) => { + const queriesState = thunkApi.getState().queries; + if (queriesState.payees.length === 0) { + return thunkApi.dispatch(getPayees()); + } + }, +); + +export const getCommonPayees = createAppAsyncThunk( + 'queries/getCommonPayees', + async () => { + const payees: PayeeEntity[] = await send('common-payees-get'); + return payees; + }, +); + +export const getPayees = createAppAsyncThunk('queries/getPayees', async () => { + const payees: PayeeEntity[] = await send('payees-get'); + return payees; +}); + +// Budget actions + +type ApplyBudgetActionArgs = + | { + type: 'budget-amount'; + month: string; + args: { + category: CategoryEntity['id']; + amount: number; + }; + } + | { + type: 'copy-last'; + month: string; + args: never; + } + | { + type: 'set-zero'; + month: string; + args: never; + } + | { + type: 'set-3-avg'; + month: string; + args: never; + } + | { + type: 'check-templates'; + month: never; + args: never; + } + | { + type: 'apply-goal-template'; + month: string; + args: never; + } + | { + type: 'overwrite-goal-template'; + month: string; + args: never; + } + | { + type: 'cleanup-goal-template'; + month: string; + args: never; + } + | { + type: 'hold'; + month: string; + args: { + amount: number; + }; + } + | { + type: 'reset-hold'; + month: string; + args: never; + } + | { + type: 'cover-overspending'; + month: string; + args: { + to: CategoryEntity['id']; + from: CategoryEntity['id']; + }; + } + | { + type: 'transfer-available'; + month: string; + args: { + amount: number; + category: CategoryEntity['id']; + }; + } + | { + type: 'cover-overbudgeted'; + month: string; + args: { + category: CategoryEntity['id']; + }; + } + | { + type: 'transfer-category'; + month: string; + args: { + amount: number; + from: CategoryEntity['id']; + to: CategoryEntity['id']; + }; + } + | { + type: 'carryover'; + month: string; + args: { + category: CategoryEntity['id']; + flag: boolean; + }; + } + | { + type: 'apply-single-category-template'; + month: string; + args: { + category: CategoryEntity['id']; + }; + } + | { + type: 'apply-multiple-templates'; + month: string; + args: { + categories: Array; + }; + } + | { + type: 'set-single-3-avg'; + month: string; + args: { + category: CategoryEntity['id']; + }; + } + | { + type: 'set-single-6-avg'; + month: string; + args: { + category: CategoryEntity['id']; + }; + } + | { + type: 'set-single-12-avg'; + month: string; + args: { + category: CategoryEntity['id']; + }; + } + | { + type: 'copy-single-last'; + month: string; + args: { + category: CategoryEntity['id']; + }; + }; + +export const applyBudgetAction = createAppAsyncThunk( + 'queries/applyBudgetAction', + async ({ month, type, args }: ApplyBudgetActionArgs, thunkApi) => { + switch (type) { + case 'budget-amount': + await send('budget/budget-amount', { + month, + category: args.category, + amount: args.amount, + }); + break; + case 'copy-last': + await send('budget/copy-previous-month', { month }); + break; + case 'set-zero': + await send('budget/set-zero', { month }); + break; + case 'set-3-avg': + await send('budget/set-3month-avg', { month }); + break; + case 'check-templates': + thunkApi.dispatch( + addNotification(await send('budget/check-templates')), + ); + break; + case 'apply-goal-template': + thunkApi.dispatch( + addNotification(await send('budget/apply-goal-template', { month })), + ); + break; + case 'overwrite-goal-template': + thunkApi.dispatch( + addNotification( + await send('budget/overwrite-goal-template', { month }), + ), + ); + break; + case 'cleanup-goal-template': + thunkApi.dispatch( + addNotification( + await send('budget/cleanup-goal-template', { month }), + ), + ); + break; + case 'hold': + await send('budget/hold-for-next-month', { + month, + amount: args.amount, + }); + break; + case 'reset-hold': + await send('budget/reset-hold', { month }); + break; + case 'cover-overspending': + await send('budget/cover-overspending', { + month, + to: args.to, + from: args.from, + }); + break; + case 'transfer-available': + await send('budget/transfer-available', { + month, + amount: args.amount, + category: args.category, + }); + break; + case 'cover-overbudgeted': + await send('budget/cover-overbudgeted', { + month, + category: args.category, + }); + break; + case 'transfer-category': + await send('budget/transfer-category', { + month, + amount: args.amount, + from: args.from, + to: args.to, + }); + break; + case 'carryover': { + await send('budget/set-carryover', { + startMonth: month, + category: args.category, + flag: args.flag, + }); + break; + } + case 'apply-single-category-template': + await send('budget/apply-single-template', { + month, + category: args.category, + }); + break; + case 'apply-multiple-templates': + thunkApi.dispatch( + addNotification( + await send('budget/apply-multiple-templates', { + month, + categoryIds: args.categories, + }), + ), + ); + break; + case 'set-single-3-avg': + await send('budget/set-n-month-avg', { + month, + N: 3, + category: args.category, + }); + break; + case 'set-single-6-avg': + await send('budget/set-n-month-avg', { + month, + N: 6, + category: args.category, + }); + break; + case 'set-single-12-avg': + await send('budget/set-n-month-avg', { + month, + N: 12, + category: args.category, + }); + break; + case 'copy-single-last': + await send('budget/copy-single-month', { + month, + category: args.category, + }); + break; + default: + console.log(`Invalid action type: ${type}`); + } + }, +); + +// Transaction actions + +type ImportPreviewTransactionsArgs = { + id: string; + transactions: TransactionEntity[]; +}; + +export const importPreviewTransactions = createAppAsyncThunk( + 'queries/importPreviewTransactions', + async ({ id, transactions }: ImportPreviewTransactionsArgs, thunkApi) => { + const { errors = [], updatedPreview } = await send('transactions-import', { + accountId: id, + transactions, + isPreview: true, + }); + + errors.forEach(error => { + thunkApi.dispatch( + addNotification({ + type: 'error', + message: error.message, + }), + ); + }); + + return updatedPreview; + }, +); + +type ImportTransactionsArgs = { + id: string; + transactions: TransactionEntity[]; + reconcile: boolean; +}; + +export const importTransactions = createAppAsyncThunk( + 'queries/importTransactions', + async ({ id, transactions, reconcile }: ImportTransactionsArgs, thunkApi) => { + if (!reconcile) { + await send('api/transactions-add', { + accountId: id, + transactions, + }); + + return true; + } + + const { + errors = [], + added, + updated, + } = await send('transactions-import', { + accountId: id, + transactions, + isPreview: false, + }); + + errors.forEach(error => { + thunkApi.dispatch( + addNotification({ + type: 'error', + message: error.message, + }), + ); + }); + + const { setNewTransactions } = queriesSlice.actions; + + thunkApi.dispatch( + setNewTransactions({ + newTransactions: added, + matchedTransactions: updated, + updatedAccounts: added.length > 0 ? [id] : [], + }), + ); + + return added.length > 0 || updated.length > 0; + }, +); + +type SetNewTransactionsAction = PayloadAction<{ + newTransactions: QueriesState['newTransactions']; + matchedTransactions: QueriesState['matchedTransactions']; + updatedAccounts: QueriesState['updatedAccounts']; +}>; +type UpdateNewTransactionsAction = PayloadAction<{ + id: TransactionEntity['id']; +}>; +type SetLastTransactionAction = PayloadAction<{ + transaction: TransactionEntity; +}>; + +const queriesSlice = createSlice({ + name: 'queries', + initialState, + reducers: { + setNewTransactions(state, action: SetNewTransactionsAction) { + state.newTransactions = action.payload.newTransactions + ? [...state.newTransactions, ...action.payload.newTransactions] + : state.newTransactions; + + state.matchedTransactions = action.payload.matchedTransactions + ? [...state.matchedTransactions, ...action.payload.matchedTransactions] + : state.matchedTransactions; + + state.updatedAccounts = action.payload.updatedAccounts + ? [...state.updatedAccounts, ...action.payload.updatedAccounts] + : state.updatedAccounts; + }, + updateNewTransactions(state, action: UpdateNewTransactionsAction) { + state.newTransactions = state.newTransactions.filter( + id => id !== action.payload.id, + ); + state.matchedTransactions = state.matchedTransactions.filter( + id => id !== action.payload.id, + ); + }, + setLastTransaction(state, action: SetLastTransactionAction) { + state.lastTransaction = action.payload.transaction; + }, + markAccountRead(state, action: MarkAccountReadAction) { + state.updatedAccounts = state.updatedAccounts.filter( + id => id !== action.payload.accountId, + ); + }, + }, + extraReducers: builder => { + // Accounts + builder.addCase(updateAccount.fulfilled, (state, action) => { + const payloadAccount = action.payload; + state.accounts = state.accounts.map(account => { + if (account.id === payloadAccount.id) { + return { ...account, ...payloadAccount }; + } + return account; + }); + }); + + builder.addCase(getAccounts.fulfilled, (state, action) => { + state.accounts = action.payload; + state.accountsLoaded = true; + }); + + // Categories + + builder.addCase(getCategories.fulfilled, (state, action) => { + state.categories = action.payload; + state.categoriesLoaded = true; + }); + + // Payees + + builder.addCase(getCommonPayees.fulfilled, (state, action) => { + state.commonPayees = action.payload; + state.commonPayeesLoaded = true; + }); + + builder.addCase(getPayees.fulfilled, (state, action) => { + state.payees = action.payload; + state.payeesLoaded = true; + }); + }, +}); + +// Helper functions + +export const getActivePayees = memoizeOne( + (payees: PayeeEntity[], accounts: AccountEntity[]) => { + const accountsById = getAccountsById(accounts); + + return payees.filter(payee => { + if (payee.transfer_acct) { + const account = accountsById[payee.transfer_acct]; + return account != null && !account.closed; + } + return true; + }); + }, +); + +export const getAccountsById = memoizeOne((accounts: AccountEntity[]) => + groupById(accounts), +); +export const getPayeesById = memoizeOne((payees: PayeeEntity[]) => + groupById(payees), +); +export const getCategoriesById = memoizeOne(categoryGroups => { + const res = {}; + categoryGroups.forEach(group => { + group.categories.forEach(cat => { + res[cat.id] = cat; + }); + }); + return res; +}); + +// Slice exports + +export const { name, reducer, getInitialState } = queriesSlice; +export const actions = { + ...queriesSlice.actions, + updateAccount, + getAccounts, + closeAccount, + reopenAccount, + getCategories, + createPayee, + getCommonPayees, + getPayees, + importPreviewTransactions, + importTransactions, + applyBudgetAction, + createAccount, + createGroup, + updateGroup, + deleteGroup, + createCategory, + updateCategory, + deleteCategory, + moveCategory, + moveCategoryGroup, + initiallyLoadPayees, +}; + +export const { + markAccountRead, + setLastTransaction, + updateNewTransactions, + setNewTransactions, +} = actions; diff --git a/packages/loot-core/src/client/reducers/index.ts b/packages/loot-core/src/client/reducers/index.ts index 1622ff46452..d87b61a7534 100644 --- a/packages/loot-core/src/client/reducers/index.ts +++ b/packages/loot-core/src/client/reducers/index.ts @@ -3,12 +3,10 @@ import { update as budgets } from './budgets'; import { update as modals } from './modals'; import { update as notifications } from './notifications'; import { update as prefs } from './prefs'; -import { update as queries } from './queries'; import { update as user } from './user'; export const reducers = { app, - queries, prefs, modals, notifications, diff --git a/packages/loot-core/src/client/reducers/queries.ts b/packages/loot-core/src/client/reducers/queries.ts deleted file mode 100644 index 0ef1d87107c..00000000000 --- a/packages/loot-core/src/client/reducers/queries.ts +++ /dev/null @@ -1,133 +0,0 @@ -// @ts-strict-ignore -import memoizeOne from 'memoize-one'; - -import { groupById } from '../../shared/util'; -import { type AccountEntity, type PayeeEntity } from '../../types/models'; -import * as constants from '../constants'; -import type { Action } from '../state-types'; -import type { QueriesState } from '../state-types/queries'; - -export const initialState: QueriesState = { - newTransactions: [], - matchedTransactions: [], - lastTransaction: null, - updatedAccounts: [], - accounts: [], - accountsLoaded: false, - categories: { - grouped: [], - list: [], - }, - categoriesLoaded: false, - commonPayees: [], - commonPayeesLoaded: false, - payees: [], - payeesLoaded: false, -}; - -export function update(state = initialState, action: Action): QueriesState { - switch (action.type) { - case constants.SET_NEW_TRANSACTIONS: - return { - ...state, - newTransactions: action.newTransactions - ? [...state.newTransactions, ...action.newTransactions] - : state.newTransactions, - matchedTransactions: action.matchedTransactions - ? [...state.matchedTransactions, ...action.matchedTransactions] - : state.matchedTransactions, - updatedAccounts: action.updatedAccounts - ? [...state.updatedAccounts, ...action.updatedAccounts] - : state.updatedAccounts, - }; - case constants.UPDATE_NEW_TRANSACTIONS: - return { - ...state, - newTransactions: state.newTransactions.filter( - id => id !== action.changedId, - ), - matchedTransactions: state.matchedTransactions.filter( - id => id !== action.changedId, - ), - }; - case constants.SET_LAST_TRANSACTION: - return { - ...state, - lastTransaction: action.transaction, - }; - case constants.MARK_ACCOUNT_READ: - return { - ...state, - updatedAccounts: state.updatedAccounts.filter( - id => id !== action.accountId, - ), - }; - case constants.LOAD_ACCOUNTS: - return { - ...state, - accounts: action.accounts, - accountsLoaded: true, - }; - case constants.UPDATE_ACCOUNT: { - return { - ...state, - accounts: state.accounts.map(account => { - if (account.id === action.account.id) { - return { ...account, ...action.account }; - } - return account; - }), - }; - } - case constants.LOAD_CATEGORIES: - return { - ...state, - categories: action.categories, - categoriesLoaded: true, - }; - case constants.LOAD_COMMON_PAYEES: - return { - ...state, - commonPayees: action.payees, - commonPayeesLoaded: true, - }; - case constants.LOAD_PAYEES: - return { - ...state, - payees: action.payees, - payeesLoaded: true, - }; - default: - } - return state; -} - -export const getAccountsById = memoizeOne((accounts: AccountEntity[]) => - groupById(accounts), -); -export const getPayeesById = memoizeOne((payees: PayeeEntity[]) => - groupById(payees), -); -export const getCategoriesById = memoizeOne(categoryGroups => { - const res = {}; - categoryGroups.forEach(group => { - group.categories.forEach(cat => { - res[cat.id] = cat; - }); - }); - return res; -}); - -export const getActivePayees = memoizeOne( - (payees: PayeeEntity[], accounts: AccountEntity[]) => { - const accountsById = getAccountsById(accounts); - - return payees.filter(payee => { - if (payee.transfer_acct) { - const account = accountsById[payee.transfer_acct]; - return account != null && !account.closed; - } - return true; - }); - }, -); diff --git a/packages/loot-core/src/client/shared-listeners.ts b/packages/loot-core/src/client/shared-listeners.ts index b2b0b124af9..d0eaae58f7c 100644 --- a/packages/loot-core/src/client/shared-listeners.ts +++ b/packages/loot-core/src/client/shared-listeners.ts @@ -6,15 +6,13 @@ import { listen, send } from '../platform/client/fetch'; import { addNotification, closeAndDownloadBudget, - getAccounts, - getCategories, - getPayees, loadPrefs, pushModal, resetSync, sync, uploadBudget, } from './actions'; +import { getAccounts, getCategories, getPayees } from './queries/queriesSlice'; import type { Notification } from './state-types/notifications'; import { type AppStore } from './store'; diff --git a/packages/loot-core/src/client/state-types/index.d.ts b/packages/loot-core/src/client/state-types/index.d.ts index 5765d6da599..db9ce13bcce 100644 --- a/packages/loot-core/src/client/state-types/index.d.ts +++ b/packages/loot-core/src/client/state-types/index.d.ts @@ -5,7 +5,6 @@ import type { BudgetsActions, BudgetsState } from './budgets'; import type { ModalsActions, ModalsState } from './modals'; import type { NotificationsActions, NotificationsState } from './notifications'; import type { PrefsActions, PrefsState } from './prefs'; -import type { QueriesActions, QueriesState } from './queries'; import type { UserActions, UserState } from './user'; export type CloseBudgetAction = { @@ -19,7 +18,6 @@ export type Action = | ModalsActions | NotificationsActions | PrefsActions - | QueriesActions | UserActions | CloseBudgetAction; diff --git a/packages/loot-core/src/client/state-types/queries.d.ts b/packages/loot-core/src/client/state-types/queries.d.ts deleted file mode 100644 index 5a423edaf94..00000000000 --- a/packages/loot-core/src/client/state-types/queries.d.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { Handlers } from '../../types/handlers'; -import { type TransactionEntity, type AccountEntity } from '../../types/models'; -import type * as constants from '../constants'; - -export type QueriesState = { - newTransactions: Array; - matchedTransactions: Array; - lastTransaction: TransactionEntity | null; - updatedAccounts: Array; - accounts: AccountEntity[]; - accountsLoaded: boolean; - categories: Awaited>; - categoriesLoaded: boolean; - commonPayeesLoaded: boolean; - commonPayees: Awaited>; - payees: Awaited>; - payeesLoaded: boolean; -}; - -type SetNewTransactionsAction = { - type: typeof constants.SET_NEW_TRANSACTIONS; - newTransactions?: Array; - matchedTransactions?: Array; - updatedAccounts?: Array; -}; - -type UpdateNewTransactionsAction = { - type: typeof constants.UPDATE_NEW_TRANSACTIONS; - changedId: string; -}; - -type SetLastTransactionAction = { - type: typeof constants.SET_LAST_TRANSACTION; - transaction: TransactionEntity; -}; - -type MarkAccountReadAction = { - type: typeof constants.MARK_ACCOUNT_READ; - accountId: string; -}; - -type LoadAccountsAction = { - type: typeof constants.LOAD_ACCOUNTS; - accounts: AccountEntity[]; -}; - -type UpdateAccountAction = { - type: typeof constants.UPDATE_ACCOUNT; - account: AccountEntity; -}; - -type LoadCategoriesAction = { - type: typeof constants.LOAD_CATEGORIES; - categories: State['categories']; -}; - -type LoadPayeesAction = { - type: typeof constants.LOAD_PAYEES; - payees: State['payees']; -}; - -type LoadCommonPayeesAction = { - type: typeof constants.LOAD_COMMON_PAYEES; - payees: State['common_payees']; -}; - -export type QueriesActions = - | SetNewTransactionsAction - | UpdateNewTransactionsAction - | SetLastTransactionAction - | MarkAccountReadAction - | LoadAccountsAction - | UpdateAccountAction - | LoadCategoriesAction - | LoadCommonPayeesAction - | LoadPayeesAction; diff --git a/packages/loot-core/src/client/store/index.ts b/packages/loot-core/src/client/store/index.ts index 9404e3758cf..fc3e70f49e9 100644 --- a/packages/loot-core/src/client/store/index.ts +++ b/packages/loot-core/src/client/store/index.ts @@ -6,18 +6,23 @@ import { getInitialState as getInitialAccountState, } from '../accounts/accountSlice'; import * as constants from '../constants'; +import { + name as queriesSliceName, + reducer as queriesSliceReducer, + getInitialState as getInitialQueriesState, +} from '../queries/queriesSlice'; import { reducers } from '../reducers'; import { initialState as initialAppState } from '../reducers/app'; import { initialState as initialBudgetsState } from '../reducers/budgets'; import { initialState as initialModalsState } from '../reducers/modals'; import { initialState as initialNotificationsState } from '../reducers/notifications'; import { initialState as initialPrefsState } from '../reducers/prefs'; -import { initialState as initialQueriesState } from '../reducers/queries'; import { initialState as initialUserState } from '../reducers/user'; const appReducer = combineReducers({ ...reducers, [accountSliceName]: accountSliceReducer, + [queriesSliceName]: queriesSliceReducer, }); const rootReducer: typeof appReducer = (state, action) => { if (action.type === constants.CLOSE_BUDGET) { @@ -27,7 +32,7 @@ const rootReducer: typeof appReducer = (state, action) => { account: getInitialAccountState(), modals: initialModalsState, notifications: initialNotificationsState, - queries: initialQueriesState, + queries: getInitialQueriesState(), budgets: state?.budgets || initialBudgetsState, user: state?.user || initialUserState, prefs: { diff --git a/packages/loot-core/src/client/store/mock.ts b/packages/loot-core/src/client/store/mock.ts index 9698652289b..531f310fc93 100644 --- a/packages/loot-core/src/client/store/mock.ts +++ b/packages/loot-core/src/client/store/mock.ts @@ -4,6 +4,10 @@ import { name as accountSliceName, reducer as accountSliceReducer, } from '../accounts/accountSlice'; +import { + name as queriesSliceName, + reducer as qeriesSliceReducer, +} from '../queries/queriesSlice'; import { reducers } from '../reducers'; import { type store as realStore } from './index'; @@ -11,6 +15,7 @@ import { type store as realStore } from './index'; const appReducer = combineReducers({ ...reducers, [accountSliceName]: accountSliceReducer, + [queriesSliceName]: qeriesSliceReducer, }); export let mockStore: typeof realStore = configureStore({ diff --git a/packages/loot-core/src/client/undo.ts b/packages/loot-core/src/client/undo.ts new file mode 100644 index 00000000000..d34608f16c9 --- /dev/null +++ b/packages/loot-core/src/client/undo.ts @@ -0,0 +1,39 @@ +import { send } from '../platform/client/fetch'; + +function throttle(callback: () => void, wait: number) { + let waiting = false; + return () => { + if (!waiting) { + callback(); + waiting = true; + setTimeout(function () { + waiting = false; + }, wait); + } + }; +} + +const _undo = throttle(() => send('undo'), 100); +const _redo = throttle(() => send('redo'), 100); + +let _undoEnabled = true; + +export function enableUndo() { + _undoEnabled = true; +} + +export function disableUndo() { + _undoEnabled = false; +} + +export function undo() { + if (_undoEnabled) { + _undo(); + } +} + +export function redo() { + if (_undoEnabled) { + _redo(); + } +} diff --git a/yarn.lock b/yarn.lock index 3017f66c692..ee034111ec5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13418,7 +13418,6 @@ __metadata: stream-browserify: "npm:^3.0.0" swc-loader: "npm:^0.2.6" terser-webpack-plugin: "npm:^5.3.10" - throttleit: "npm:^1.0.1" ts-node: "npm:^10.9.2" typescript: "npm:^5.5.4" uuid: "npm:^9.0.1" @@ -18154,13 +18153,6 @@ __metadata: languageName: node linkType: hard -"throttleit@npm:^1.0.1": - version: 1.0.1 - resolution: "throttleit@npm:1.0.1" - checksum: 10/17f1aba82192d8b4f5be5f7e7955acd2db0b60557a2e041900bcb685c03fc0a42e44fae955741c2994ec314918c6c1c2c179bfe17b1fbb4a011c506e9ea7cc33 - languageName: node - linkType: hard - "through2@npm:^2.0.1": version: 2.0.5 resolution: "through2@npm:2.0.5"