mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-30 03:23:51 -05:00
Retrofit useTransactions to use react-query under the hood (#6757)
* Retrofit useTransactions to use react-query under the hood * Add release notes for PR #6757 * Update packages/desktop-client/src/transactions/queries.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Disable when there is no query parameter * Fix typecheck errors * Remove space * Update tests * Coderabbit: Add pageSize to query key * Use isPending instead of isFetching * Unexport mockStore * Revert variables * Change category from Enhancements to Maintenance Refactor the useTransactions hook to improve data fetching with react-query. * Fix lint errors * Fix lint errors * Migrate setupTests.js to TypeScript with proper types (#6871) * Initial plan * Rename setupTests.js to setupTests.ts and add proper types Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com> * Extract Size type to avoid duplication Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com> * Add release note for setupTests TypeScript migration Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com> * Rename release note file to match PR number 6871 Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com> * [autofix.ci] apply automated fixes * Update transactionQueries * Delete setupTests PR release note --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
e3e4b13d2b
commit
37a7d0eccd
@@ -33,7 +33,7 @@
|
||||
"@rollup/plugin-inject": "^5.0.5",
|
||||
"@swc/core": "^1.15.8",
|
||||
"@swc/helpers": "^0.5.18",
|
||||
"@tanstack/react-query": "^5.90.5",
|
||||
"@tanstack/react-query": "^5.90.19",
|
||||
"@testing-library/dom": "10.4.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "16.3.0",
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { AccountEntity } from 'loot-core/types/models';
|
||||
import { ReconcileMenu, ReconcilingMessage } from './Reconcile';
|
||||
|
||||
import { useSheetValue } from '@desktop-client/hooks/useSheetValue';
|
||||
import { TestProvider } from '@desktop-client/redux/mock';
|
||||
import { TestProviders } from '@desktop-client/mocks';
|
||||
|
||||
vi.mock('@desktop-client/hooks/useSheetValue', () => ({
|
||||
useSheetValue: vi.fn(),
|
||||
@@ -40,14 +40,14 @@ describe('ReconcilingMessage math & UI', () => {
|
||||
const onCreateTransaction = vi.fn();
|
||||
|
||||
render(
|
||||
<TestProvider>
|
||||
<TestProviders>
|
||||
<ReconcilingMessage
|
||||
balanceQuery={makeBalanceQuery()}
|
||||
targetBalance={5000}
|
||||
onDone={onDone}
|
||||
onCreateTransaction={onCreateTransaction}
|
||||
/>
|
||||
</TestProvider>,
|
||||
</TestProviders>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('All reconciled!')).toBeInTheDocument();
|
||||
@@ -67,14 +67,14 @@ describe('ReconcilingMessage math & UI', () => {
|
||||
const onCreateTransaction = vi.fn();
|
||||
|
||||
render(
|
||||
<TestProvider>
|
||||
<TestProviders>
|
||||
<ReconcilingMessage
|
||||
balanceQuery={makeBalanceQuery()}
|
||||
targetBalance={10000}
|
||||
onDone={vi.fn()}
|
||||
onCreateTransaction={onCreateTransaction}
|
||||
/>
|
||||
</TestProvider>,
|
||||
</TestProviders>,
|
||||
);
|
||||
|
||||
// Formatted amounts present
|
||||
@@ -95,14 +95,14 @@ describe('ReconcilingMessage math & UI', () => {
|
||||
const onCreateTransaction = vi.fn();
|
||||
|
||||
render(
|
||||
<TestProvider>
|
||||
<TestProviders>
|
||||
<ReconcilingMessage
|
||||
balanceQuery={makeBalanceQuery()}
|
||||
targetBalance={10000}
|
||||
onDone={vi.fn()}
|
||||
onCreateTransaction={onCreateTransaction}
|
||||
/>
|
||||
</TestProvider>,
|
||||
</TestProviders>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('120.00')).toBeInTheDocument();
|
||||
@@ -133,13 +133,13 @@ describe('ReconcileMenu arithmetic evaluation', () => {
|
||||
const onClose = vi.fn();
|
||||
|
||||
render(
|
||||
<TestProvider>
|
||||
<TestProviders>
|
||||
<ReconcileMenu
|
||||
account={baseAccount as AccountEntity}
|
||||
onReconcile={onReconcile}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</TestProvider>,
|
||||
</TestProviders>,
|
||||
);
|
||||
|
||||
const input = screen.getByRole('textbox');
|
||||
@@ -162,13 +162,13 @@ describe('ReconcileMenu arithmetic evaluation', () => {
|
||||
const onClose = vi.fn();
|
||||
|
||||
render(
|
||||
<TestProvider>
|
||||
<TestProviders>
|
||||
<ReconcileMenu
|
||||
account={baseAccount as AccountEntity}
|
||||
onReconcile={onReconcile}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</TestProvider>,
|
||||
</TestProviders>,
|
||||
);
|
||||
|
||||
const input = screen.getByRole('textbox');
|
||||
@@ -200,13 +200,13 @@ describe('ReconcileMenu arithmetic evaluation', () => {
|
||||
connectedAccount.balance_current = 4321;
|
||||
|
||||
render(
|
||||
<TestProvider>
|
||||
<TestProviders>
|
||||
<ReconcileMenu
|
||||
account={connectedAccount}
|
||||
onReconcile={onReconcile}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</TestProvider>,
|
||||
</TestProviders>,
|
||||
);
|
||||
|
||||
// Fill from last synced value (43.21)
|
||||
@@ -223,13 +223,13 @@ describe('ReconcileMenu arithmetic evaluation', () => {
|
||||
const onReconcile = vi.fn();
|
||||
const onClose = vi.fn();
|
||||
render(
|
||||
<TestProvider>
|
||||
<TestProviders>
|
||||
<ReconcileMenu
|
||||
account={baseAccount as AccountEntity}
|
||||
onReconcile={onReconcile}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</TestProvider>,
|
||||
</TestProviders>,
|
||||
);
|
||||
|
||||
const input = screen.getByRole('textbox');
|
||||
@@ -247,13 +247,13 @@ describe('ReconcileMenu arithmetic evaluation', () => {
|
||||
const onReconcile = vi.fn();
|
||||
const onClose = vi.fn();
|
||||
render(
|
||||
<TestProvider>
|
||||
<TestProviders>
|
||||
<ReconcileMenu
|
||||
account={baseAccount as AccountEntity}
|
||||
onReconcile={onReconcile}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</TestProvider>,
|
||||
</TestProviders>,
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Reconcile' }));
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { PayeeAutocompleteProps } from './PayeeAutocomplete';
|
||||
|
||||
import { AuthProvider } from '@desktop-client/auth/AuthProvider';
|
||||
import { useCommonPayees } from '@desktop-client/hooks/usePayees';
|
||||
import { TestProvider } from '@desktop-client/redux/mock';
|
||||
import { TestProviders } from '@desktop-client/mocks';
|
||||
|
||||
const PAYEE_SELECTOR = '[data-testid][role=option]';
|
||||
const PAYEE_SECTION_SELECTOR = '[data-testid$="-item-group"]';
|
||||
@@ -74,7 +74,7 @@ function renderPayeeAutocomplete(
|
||||
};
|
||||
|
||||
render(
|
||||
<TestProvider>
|
||||
<TestProviders>
|
||||
<AuthProvider>
|
||||
<div data-testid="autocomplete-test">
|
||||
<PayeeAutocomplete
|
||||
@@ -86,7 +86,7 @@ function renderPayeeAutocomplete(
|
||||
/>
|
||||
</div>
|
||||
</AuthProvider>
|
||||
</TestProvider>,
|
||||
</TestProviders>,
|
||||
);
|
||||
return screen.getByTestId('autocomplete-test');
|
||||
}
|
||||
|
||||
@@ -86,10 +86,10 @@ function TransactionListWithPreviews({
|
||||
const {
|
||||
transactions,
|
||||
runningBalances,
|
||||
isLoading: isTransactionsLoading,
|
||||
reload: reloadTransactions,
|
||||
isLoadingMore,
|
||||
loadMore: loadMoreTransactions,
|
||||
isPending: isTransactionsLoading,
|
||||
refetch: reloadTransactions,
|
||||
isFetchingNextPage: isLoadingMoreTransactions,
|
||||
fetchNextPage: fetchMoreTransactions,
|
||||
} = useTransactions({
|
||||
query: transactionsQuery,
|
||||
options: {
|
||||
@@ -223,8 +223,8 @@ function TransactionListWithPreviews({
|
||||
balanceUncleared={balanceBindings.uncleared}
|
||||
runningBalances={allBalances}
|
||||
showRunningBalances={shouldCalculateRunningBalances}
|
||||
isLoadingMore={isLoadingMore}
|
||||
onLoadMore={loadMoreTransactions}
|
||||
isLoadingMore={isLoadingMoreTransactions}
|
||||
onLoadMore={fetchMoreTransactions}
|
||||
searchPlaceholder={t('Search {{accountName}}', {
|
||||
accountName: account.name,
|
||||
})}
|
||||
|
||||
@@ -41,10 +41,10 @@ function TransactionListWithPreviews() {
|
||||
);
|
||||
const {
|
||||
transactions,
|
||||
isLoading: isTransactionsLoading,
|
||||
reload: reloadTransactions,
|
||||
isLoadingMore,
|
||||
loadMore: loadMoreTransactions,
|
||||
isPending: isTransactionsLoading,
|
||||
refetch: reloadTransactions,
|
||||
isFetchingNextPage: isLoadingMoreTransactions,
|
||||
fetchNextPage: fetchMoreTransactions,
|
||||
} = useTransactions({
|
||||
query: transactionsQuery,
|
||||
});
|
||||
@@ -144,8 +144,8 @@ function TransactionListWithPreviews() {
|
||||
}
|
||||
transactions={transactionsToDisplay}
|
||||
balance={balanceBindings.balance}
|
||||
isLoadingMore={isLoadingMore}
|
||||
onLoadMore={loadMoreTransactions}
|
||||
isLoadingMore={isLoadingMoreTransactions}
|
||||
onLoadMore={fetchMoreTransactions}
|
||||
searchPlaceholder={t('Search All Accounts')}
|
||||
onSearch={onSearch}
|
||||
onOpenTransaction={onOpenTransaction}
|
||||
|
||||
@@ -43,10 +43,10 @@ function TransactionListWithPreviews() {
|
||||
);
|
||||
const {
|
||||
transactions,
|
||||
isLoading: isTransactionsLoading,
|
||||
reload: reloadTransactions,
|
||||
isLoadingMore,
|
||||
loadMore: loadMoreTransactions,
|
||||
isPending: isTransactionsLoading,
|
||||
refetch: reloadTransactions,
|
||||
isFetchingNextPage: isLoadingMoreTransactions,
|
||||
fetchNextPage: fetchMoreTransactions,
|
||||
} = useTransactions({
|
||||
query: transactionsQuery,
|
||||
});
|
||||
@@ -155,8 +155,8 @@ function TransactionListWithPreviews() {
|
||||
}
|
||||
transactions={transactionsToDisplay}
|
||||
balance={balanceBindings.balance}
|
||||
isLoadingMore={isLoadingMore}
|
||||
onLoadMore={loadMoreTransactions}
|
||||
isLoadingMore={isLoadingMoreTransactions}
|
||||
onLoadMore={fetchMoreTransactions}
|
||||
searchPlaceholder={t('Search Off Budget Accounts')}
|
||||
onSearch={onSearch}
|
||||
onOpenTransaction={onOpenTransaction}
|
||||
|
||||
@@ -43,10 +43,10 @@ function TransactionListWithPreviews() {
|
||||
);
|
||||
const {
|
||||
transactions,
|
||||
isLoading: isTransactionsLoading,
|
||||
reload: reloadTransactions,
|
||||
isLoadingMore,
|
||||
loadMore: loadMoreTransactions,
|
||||
isPending: isTransactionsLoading,
|
||||
refetch: reloadTransactions,
|
||||
isFetchingNextPage: isLoadingMoreTransactions,
|
||||
fetchNextPage: fetchMoreTransactions,
|
||||
} = useTransactions({
|
||||
query: transactionsQuery,
|
||||
});
|
||||
@@ -155,8 +155,8 @@ function TransactionListWithPreviews() {
|
||||
}
|
||||
transactions={transactionsToDisplay}
|
||||
balance={balanceBindings.balance}
|
||||
isLoadingMore={isLoadingMore}
|
||||
onLoadMore={loadMoreTransactions}
|
||||
isLoadingMore={isLoadingMoreTransactions}
|
||||
onLoadMore={fetchMoreTransactions}
|
||||
searchPlaceholder={t('Search On Budget Accounts')}
|
||||
onSearch={onSearch}
|
||||
onOpenTransaction={onOpenTransaction}
|
||||
|
||||
@@ -666,14 +666,14 @@ function UncategorizedTransactionsBanner(props) {
|
||||
[],
|
||||
);
|
||||
|
||||
const { transactions, isLoading } = useTransactions({
|
||||
const { transactions, isPending: isTransactionsLoading } = useTransactions({
|
||||
query: transactionsQuery,
|
||||
options: {
|
||||
pageCount: 1000,
|
||||
pageSize: 1000,
|
||||
},
|
||||
});
|
||||
|
||||
if (isLoading || transactions.length === 0) {
|
||||
if (isTransactionsLoading || transactions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -59,10 +59,10 @@ function TransactionListWithPreviews({
|
||||
);
|
||||
const {
|
||||
transactions,
|
||||
isLoading: isTransactionsLoading,
|
||||
isLoadingMore,
|
||||
loadMore: loadMoreTransactions,
|
||||
reload: reloadTransactions,
|
||||
isPending: isTransactionsLoading,
|
||||
isFetchingNextPage: isLoadingMoreTransactions,
|
||||
fetchNextPage: fetchMoreTransactions,
|
||||
refetch: reloadTransactions,
|
||||
} = useTransactions({
|
||||
query: transactionsQuery,
|
||||
});
|
||||
@@ -130,8 +130,8 @@ function TransactionListWithPreviews({
|
||||
balanceUncleared={balanceUncleared}
|
||||
searchPlaceholder={`Search ${category.name}`}
|
||||
onSearch={onSearch}
|
||||
isLoadingMore={isLoadingMore}
|
||||
onLoadMore={loadMoreTransactions}
|
||||
isLoadingMore={isLoadingMoreTransactions}
|
||||
onLoadMore={fetchMoreTransactions}
|
||||
onOpenTransaction={onOpenTransaction}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -27,10 +27,10 @@ export function UncategorizedTransactions() {
|
||||
);
|
||||
const {
|
||||
transactions,
|
||||
isLoading,
|
||||
isLoadingMore,
|
||||
loadMore: loadMoreTransactions,
|
||||
reload: reloadTransactions,
|
||||
isPending: isTransactionsLoading,
|
||||
isFetchingNextPage: isLoadingMoreTransactions,
|
||||
fetchNextPage: fetchMoreTransactions,
|
||||
refetch: reloadTransactions,
|
||||
} = useTransactions({
|
||||
query: transactionsQuery,
|
||||
});
|
||||
@@ -73,13 +73,13 @@ export function UncategorizedTransactions() {
|
||||
return (
|
||||
<SchedulesProvider>
|
||||
<TransactionListWithBalances
|
||||
isLoading={isLoading}
|
||||
isLoading={isTransactionsLoading}
|
||||
transactions={transactions}
|
||||
balance={balance}
|
||||
searchPlaceholder="Search uncategorized transactions"
|
||||
onSearch={onSearch}
|
||||
isLoadingMore={isLoadingMore}
|
||||
onLoadMore={loadMoreTransactions}
|
||||
isLoadingMore={isLoadingMoreTransactions}
|
||||
onLoadMore={fetchMoreTransactions}
|
||||
onOpenTransaction={onOpenTransaction}
|
||||
/>
|
||||
</SchedulesProvider>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { MobilePayeesPage } from './MobilePayeesPage';
|
||||
import { useNavigate } from '@desktop-client/hooks/useNavigate';
|
||||
import { usePayeeRuleCounts } from '@desktop-client/hooks/usePayeeRuleCounts';
|
||||
import { usePayees } from '@desktop-client/hooks/usePayees';
|
||||
import { TestProvider } from '@desktop-client/redux/mock';
|
||||
import { TestProviders } from '@desktop-client/mocks';
|
||||
|
||||
vi.mock('@use-gesture/react', () => ({
|
||||
useDrag: vi.fn().mockReturnValue(() => ({})),
|
||||
@@ -57,9 +57,9 @@ describe('MobilePayeesPage', () => {
|
||||
|
||||
const renderPayeesPage = () => {
|
||||
return render(
|
||||
<TestProvider>
|
||||
<TestProviders>
|
||||
<MobilePayeesPage />
|
||||
</TestProvider>,
|
||||
</TestProviders>,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { vi } from 'vitest';
|
||||
|
||||
import { GoCardlessExternalMsgModal } from './GoCardlessExternalMsgModal';
|
||||
|
||||
import { TestProvider } from '@desktop-client/redux/mock';
|
||||
import { TestProviders } from '@desktop-client/mocks';
|
||||
|
||||
vi.mock('@desktop-client/hooks/useGlobalPref', () => ({
|
||||
useGlobalPref: () => [null],
|
||||
@@ -50,9 +50,9 @@ describe('GoCardlessExternalMsgModal - Country Auto-selection', () => {
|
||||
});
|
||||
|
||||
render(
|
||||
<TestProvider>
|
||||
<TestProviders>
|
||||
<GoCardlessExternalMsgModal {...mockProps} />
|
||||
</TestProvider>,
|
||||
</TestProviders>,
|
||||
);
|
||||
|
||||
const countryInput = screen.getByPlaceholderText('(please select)');
|
||||
@@ -76,9 +76,9 @@ describe('GoCardlessExternalMsgModal - Country Auto-selection', () => {
|
||||
});
|
||||
|
||||
render(
|
||||
<TestProvider>
|
||||
<TestProviders>
|
||||
<GoCardlessExternalMsgModal {...mockProps} />
|
||||
</TestProvider>,
|
||||
</TestProviders>,
|
||||
);
|
||||
|
||||
const countryInput = screen.getByPlaceholderText('(please select)');
|
||||
@@ -101,9 +101,9 @@ describe('GoCardlessExternalMsgModal - Country Auto-selection', () => {
|
||||
});
|
||||
|
||||
render(
|
||||
<TestProvider>
|
||||
<TestProviders>
|
||||
<GoCardlessExternalMsgModal {...mockProps} />
|
||||
</TestProvider>,
|
||||
</TestProviders>,
|
||||
);
|
||||
|
||||
const countryInput = screen.getByPlaceholderText('(please select)');
|
||||
@@ -126,9 +126,9 @@ describe('GoCardlessExternalMsgModal - Country Auto-selection', () => {
|
||||
});
|
||||
|
||||
render(
|
||||
<TestProvider>
|
||||
<TestProviders>
|
||||
<GoCardlessExternalMsgModal {...mockProps} />
|
||||
</TestProvider>,
|
||||
</TestProviders>,
|
||||
);
|
||||
|
||||
const countryInput = screen.getByPlaceholderText('(please select)');
|
||||
|
||||
@@ -6,7 +6,9 @@ import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { Change } from './Change';
|
||||
|
||||
import { store } from '@desktop-client/redux/store';
|
||||
import { configureAppStore } from '@desktop-client/redux/store';
|
||||
|
||||
const store = configureAppStore();
|
||||
|
||||
describe('Change', () => {
|
||||
it('renders a positive amount with a plus sign and positive color', () => {
|
||||
|
||||
@@ -110,8 +110,10 @@ function CalendarInner({ widget, parameters }: CalendarInnerProps) {
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const [latestTransaction, setLatestTransaction] = useState('');
|
||||
|
||||
const { transactions: transactionsGrouped, loadMore: loadMoreTransactions } =
|
||||
useTransactions({ query });
|
||||
const {
|
||||
transactions: transactionsGrouped,
|
||||
fetchNextPage: loadMoreTransactions,
|
||||
} = useTransactions({ query });
|
||||
|
||||
const allTransactions = useMemo(
|
||||
() => ungroupTransactions(transactionsGrouped as TransactionEntity[]),
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
useMultiuserEnabled,
|
||||
} from '@desktop-client/components/ServerContext';
|
||||
import { useSyncServerStatus } from '@desktop-client/hooks/useSyncServerStatus';
|
||||
import { TestProvider } from '@desktop-client/redux/mock';
|
||||
import { TestProviders } from '@desktop-client/mocks';
|
||||
|
||||
vi.mock('@desktop-client/hooks/useSyncServerStatus', () => ({
|
||||
useSyncServerStatus: vi.fn(),
|
||||
@@ -28,7 +28,7 @@ describe('AuthSettings', () => {
|
||||
vi.mocked(useMultiuserEnabled).mockReturnValue(false);
|
||||
vi.mocked(useLoginMethod).mockReturnValue('password');
|
||||
|
||||
const { container } = render(<AuthSettings />, { wrapper: TestProvider });
|
||||
const { container } = render(<AuthSettings />, { wrapper: TestProviders });
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
@@ -42,7 +42,7 @@ describe('AuthSettings', () => {
|
||||
vi.mocked(useMultiuserEnabled).mockReturnValue(false);
|
||||
vi.mocked(useLoginMethod).mockReturnValue('password');
|
||||
|
||||
render(<AuthSettings />, { wrapper: TestProvider });
|
||||
render(<AuthSettings />, { wrapper: TestProviders });
|
||||
|
||||
const startUsingButton = screen.getByRole('button', {
|
||||
name: /start using openid/i,
|
||||
@@ -59,7 +59,7 @@ describe('AuthSettings', () => {
|
||||
vi.mocked(useMultiuserEnabled).mockReturnValue(false);
|
||||
vi.mocked(useLoginMethod).mockReturnValue('openid');
|
||||
|
||||
render(<AuthSettings />, { wrapper: TestProvider });
|
||||
render(<AuthSettings />, { wrapper: TestProviders });
|
||||
|
||||
const disableButton = screen.getByRole('button', {
|
||||
name: /disable openid/i,
|
||||
@@ -82,7 +82,7 @@ describe('AuthSettings', () => {
|
||||
vi.mocked(useMultiuserEnabled).mockReturnValue(false);
|
||||
vi.mocked(useLoginMethod).mockReturnValue('password');
|
||||
|
||||
render(<AuthSettings />, { wrapper: TestProvider });
|
||||
render(<AuthSettings />, { wrapper: TestProviders });
|
||||
|
||||
const startUsingButton = screen.getByRole('button', {
|
||||
name: /start using openid/i,
|
||||
@@ -99,7 +99,7 @@ describe('AuthSettings', () => {
|
||||
vi.mocked(useMultiuserEnabled).mockReturnValue(false);
|
||||
vi.mocked(useLoginMethod).mockReturnValue('openid');
|
||||
|
||||
render(<AuthSettings />, { wrapper: TestProvider });
|
||||
render(<AuthSettings />, { wrapper: TestProviders });
|
||||
|
||||
const disableButton = screen.getByRole('button', {
|
||||
name: /disable openid/i,
|
||||
@@ -116,7 +116,7 @@ describe('AuthSettings', () => {
|
||||
vi.mocked(useMultiuserEnabled).mockReturnValue(true);
|
||||
vi.mocked(useLoginMethod).mockReturnValue('openid');
|
||||
|
||||
render(<AuthSettings />, { wrapper: TestProvider });
|
||||
render(<AuthSettings />, { wrapper: TestProviders });
|
||||
|
||||
const warningText = screen.getByText(
|
||||
/disabling openid will deactivate multi-user mode\./i,
|
||||
|
||||
@@ -33,7 +33,7 @@ import { SchedulesProvider } from '@desktop-client/hooks/useCachedSchedules';
|
||||
import { SelectedProviderWithItems } from '@desktop-client/hooks/useSelected';
|
||||
import { SplitsExpandedProvider } from '@desktop-client/hooks/useSplitsExpanded';
|
||||
import { SpreadsheetProvider } from '@desktop-client/hooks/useSpreadsheet';
|
||||
import { TestProvider } from '@desktop-client/redux/mock';
|
||||
import { TestProviders } from '@desktop-client/mocks';
|
||||
|
||||
vi.mock('loot-core/platform/client/fetch');
|
||||
vi.mock('../../hooks/useFeatureFlag', () => ({
|
||||
@@ -195,7 +195,7 @@ function LiveTransactionTable(props: LiveTransactionTableProps) {
|
||||
// implementation properly uses the right latest state even if the
|
||||
// hook dependencies haven't changed
|
||||
return (
|
||||
<TestProvider>
|
||||
<TestProviders>
|
||||
<AuthProvider>
|
||||
<SpreadsheetProvider>
|
||||
<SchedulesProvider>
|
||||
@@ -226,7 +226,7 @@ function LiveTransactionTable(props: LiveTransactionTableProps) {
|
||||
</SchedulesProvider>
|
||||
</SpreadsheetProvider>
|
||||
</AuthProvider>
|
||||
</TestProvider>
|
||||
</TestProviders>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ export function DisplayPayeeProvider({
|
||||
);
|
||||
const { transactions: allSubtransactions = [] } = useTransactions({
|
||||
query: subtransactionsQuery,
|
||||
options: { pageCount: transactions.length * 5 },
|
||||
options: { pageSize: transactions.length * 5 },
|
||||
});
|
||||
|
||||
const accounts = useAccounts();
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import type {
|
||||
InfiniteData,
|
||||
UseInfiniteQueryResult,
|
||||
} from '@tanstack/react-query';
|
||||
|
||||
import type { Query } from 'loot-core/shared/query';
|
||||
import type { IntegerAmount } from 'loot-core/shared/util';
|
||||
import type { TransactionEntity } from 'loot-core/types/models';
|
||||
|
||||
import { pagedQuery } from '@desktop-client/queries/pagedQuery';
|
||||
import type { PagedQuery } from '@desktop-client/queries/pagedQuery';
|
||||
import { transactionQueries } from '@desktop-client/transactions';
|
||||
|
||||
// Mirrors the `splits` AQL option from the server
|
||||
type TransactionSplitsOption = 'all' | 'inline' | 'grouped' | 'none';
|
||||
@@ -35,7 +40,7 @@ type UseTransactionsProps = {
|
||||
* The default is 50.
|
||||
* @default 50
|
||||
*/
|
||||
pageCount?: number;
|
||||
pageSize?: number;
|
||||
/**
|
||||
* Whether to calculate running balances for the transactions returned by the query.
|
||||
* This can be set to `true` to calculate running balances for all transactions
|
||||
@@ -60,7 +65,10 @@ type UseTransactionsProps = {
|
||||
};
|
||||
};
|
||||
|
||||
type UseTransactionsResult = {
|
||||
type UseTransactionsResult = UseInfiniteQueryResult<
|
||||
InfiniteData<TransactionEntity[]>,
|
||||
Error
|
||||
> & {
|
||||
/**
|
||||
* The transactions returned by the query.
|
||||
*/
|
||||
@@ -71,105 +79,26 @@ type UseTransactionsResult = {
|
||||
* or a function that implements the calculation in the options.
|
||||
*/
|
||||
runningBalances: Map<TransactionEntity['id'], IntegerAmount>;
|
||||
/**
|
||||
* Whether the transactions are currently being loaded.
|
||||
*/
|
||||
isLoading: boolean;
|
||||
/**
|
||||
* An error that occurred while loading the transactions.
|
||||
*/
|
||||
error?: Error;
|
||||
/**
|
||||
* Reload the transactions.
|
||||
*/
|
||||
reload: () => void;
|
||||
/**
|
||||
* Load more transactions.
|
||||
*/
|
||||
loadMore: () => void;
|
||||
/**
|
||||
* Whether more transactions are currently being loaded.
|
||||
*/
|
||||
isLoadingMore: boolean;
|
||||
};
|
||||
|
||||
export function useTransactions({
|
||||
query,
|
||||
options = { pageCount: 50, calculateRunningBalances: false },
|
||||
options = { pageSize: 50, calculateRunningBalances: false },
|
||||
}: UseTransactionsProps): UseTransactionsResult {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||
const [error, setError] = useState<Error | undefined>(undefined);
|
||||
const [transactions, setTransactions] = useState<
|
||||
ReadonlyArray<TransactionEntity>
|
||||
>([]);
|
||||
const [runningBalances, setRunningBalances] = useState<
|
||||
Map<TransactionEntity['id'], IntegerAmount>
|
||||
>(new Map());
|
||||
|
||||
const pagedQueryRef = useRef<PagedQuery<TransactionEntity> | null>(null);
|
||||
const queryResult = useInfiniteQuery(
|
||||
transactionQueries.aql({ query, pageSize: options.pageSize }),
|
||||
);
|
||||
|
||||
// We don't want to re-render if options changes.
|
||||
// Putting options in a ref will prevent that and
|
||||
// allow us to use the latest options on next render.
|
||||
const optionsRef = useRef(options);
|
||||
optionsRef.current = options;
|
||||
|
||||
useEffect(() => {
|
||||
let isUnmounted = false;
|
||||
|
||||
setError(undefined);
|
||||
|
||||
if (!query) {
|
||||
return;
|
||||
}
|
||||
|
||||
function onError(error: Error) {
|
||||
if (!isUnmounted) {
|
||||
setError(error);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (query.state.table !== 'transactions') {
|
||||
onError(new Error('Query must be a transactions query.'));
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
pagedQueryRef.current = pagedQuery<TransactionEntity>(query, {
|
||||
onData: data => {
|
||||
if (!isUnmounted) {
|
||||
setTransactions(data);
|
||||
|
||||
const calculateFn = getCalculateRunningBalancesFn(
|
||||
optionsRef.current?.calculateRunningBalances,
|
||||
);
|
||||
if (calculateFn) {
|
||||
setRunningBalances(
|
||||
calculateFn(
|
||||
data,
|
||||
query.state.tableOptions?.splits as TransactionSplitsOption,
|
||||
optionsRef.current?.startingBalance,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
onError,
|
||||
options: optionsRef.current.pageCount
|
||||
? { pageCount: optionsRef.current.pageCount }
|
||||
: {},
|
||||
});
|
||||
|
||||
return () => {
|
||||
isUnmounted = true;
|
||||
pagedQueryRef.current?.unsubscribe();
|
||||
};
|
||||
}, [query]);
|
||||
const calculateRunningBalancesOptionFn = getCalculateRunningBalancesFn(
|
||||
options?.calculateRunningBalances,
|
||||
);
|
||||
const startingBalanceOption = options?.startingBalance;
|
||||
const splitsOption = query?.state.tableOptions
|
||||
?.splits as TransactionSplitsOption;
|
||||
|
||||
// Recalculate running balances whenever the transactions change or the
|
||||
// calculation options change (for example, when toggling show/hide
|
||||
@@ -177,55 +106,32 @@ export function useTransactions({
|
||||
// dependent only on `query` above, but this effect ensures running
|
||||
// balances are updated in-place when the caller changes options.
|
||||
useEffect(() => {
|
||||
const calculateFn = getCalculateRunningBalancesFn(
|
||||
options?.calculateRunningBalances,
|
||||
);
|
||||
|
||||
if (calculateFn) {
|
||||
setRunningBalances(
|
||||
calculateFn(
|
||||
transactions,
|
||||
query?.state.tableOptions?.splits as TransactionSplitsOption,
|
||||
options?.startingBalance,
|
||||
),
|
||||
);
|
||||
if (calculateRunningBalancesOptionFn) {
|
||||
if (queryResult.isSuccess) {
|
||||
const transactions = queryResult.data.pages.flat();
|
||||
setRunningBalances(
|
||||
calculateRunningBalancesOptionFn(
|
||||
transactions,
|
||||
splitsOption,
|
||||
startingBalanceOption,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
setRunningBalances(new Map());
|
||||
}
|
||||
}, [
|
||||
transactions,
|
||||
query,
|
||||
options?.calculateRunningBalances,
|
||||
options?.startingBalance,
|
||||
queryResult.data,
|
||||
queryResult.isSuccess,
|
||||
calculateRunningBalancesOptionFn,
|
||||
startingBalanceOption,
|
||||
splitsOption,
|
||||
]);
|
||||
|
||||
const loadMore = useCallback(async () => {
|
||||
if (!pagedQueryRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoadingMore(true);
|
||||
|
||||
await pagedQueryRef.current
|
||||
.fetchNext()
|
||||
.catch(setError)
|
||||
.finally(() => {
|
||||
setIsLoadingMore(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const reload = useCallback(() => {
|
||||
pagedQueryRef.current?.run();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
transactions,
|
||||
...queryResult,
|
||||
transactions: queryResult.data ? queryResult.data.pages.flat() : [],
|
||||
runningBalances,
|
||||
isLoading,
|
||||
...(error && { error }),
|
||||
reload,
|
||||
loadMore,
|
||||
isLoadingMore,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -26,12 +26,17 @@ import * as notificationsSlice from './notifications/notificationsSlice';
|
||||
import * as payeesSlice from './payees/payeesSlice';
|
||||
import * as prefsSlice from './prefs/prefsSlice';
|
||||
import { aqlQuery } from './queries/aqlQuery';
|
||||
import { store } from './redux/store';
|
||||
import { configureAppStore } from './redux/store';
|
||||
import * as tagsSlice from './tags/tagsSlice';
|
||||
import * as transactionsSlice from './transactions/transactionsSlice';
|
||||
import { redo, undo } from './undo';
|
||||
import * as usersSlice from './users/usersSlice';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
window.__TANSTACK_QUERY_CLIENT__ = queryClient;
|
||||
|
||||
const store = configureAppStore();
|
||||
|
||||
const boundActions = bindActionCreators(
|
||||
{
|
||||
...accountsSlice.actions,
|
||||
@@ -82,9 +87,6 @@ window.$send = send;
|
||||
window.$query = aqlQuery;
|
||||
window.$q = q;
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
window.__TANSTACK_QUERY_CLIENT__ = queryClient;
|
||||
|
||||
const container = document.getElementById('root');
|
||||
const root = createRoot(container);
|
||||
root.render(
|
||||
|
||||
25
packages/desktop-client/src/mocks.tsx
Normal file
25
packages/desktop-client/src/mocks.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
import { configureAppStore } from './redux/store';
|
||||
import type { AppStore } from './redux/store';
|
||||
|
||||
let mockQueryClient = new QueryClient();
|
||||
let mockStore: AppStore = configureAppStore();
|
||||
|
||||
export function resetTestProviders() {
|
||||
mockQueryClient = new QueryClient();
|
||||
mockStore = configureAppStore();
|
||||
}
|
||||
|
||||
export function TestProviders({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<Provider store={mockStore}>
|
||||
<QueryClientProvider client={mockQueryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import React from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { combineReducers, configureStore } from '@reduxjs/toolkit';
|
||||
|
||||
import type { store as realStore } from './store';
|
||||
|
||||
import {
|
||||
name as accountsSliceName,
|
||||
reducer as accountsSliceReducer,
|
||||
} from '@desktop-client/accounts/accountsSlice';
|
||||
import {
|
||||
name as appSliceName,
|
||||
reducer as appSliceReducer,
|
||||
} from '@desktop-client/app/appSlice';
|
||||
import {
|
||||
name as budgetfilesSliceName,
|
||||
reducer as budgetfilesSliceReducer,
|
||||
} from '@desktop-client/budgetfiles/budgetfilesSlice';
|
||||
import {
|
||||
name as modalsSliceName,
|
||||
reducer as modalsSliceReducer,
|
||||
} from '@desktop-client/modals/modalsSlice';
|
||||
import {
|
||||
name as notificationsSliceName,
|
||||
reducer as notificationsSliceReducer,
|
||||
} from '@desktop-client/notifications/notificationsSlice';
|
||||
import {
|
||||
name as payeesSliceName,
|
||||
reducer as payeesSliceReducer,
|
||||
} from '@desktop-client/payees/payeesSlice';
|
||||
import {
|
||||
name as prefsSliceName,
|
||||
reducer as prefsSliceReducer,
|
||||
} from '@desktop-client/prefs/prefsSlice';
|
||||
import {
|
||||
name as tagsSliceName,
|
||||
reducer as tagsSliceReducer,
|
||||
} from '@desktop-client/tags/tagsSlice';
|
||||
import {
|
||||
name as transactionsSliceName,
|
||||
reducer as transactionsSliceReducer,
|
||||
} from '@desktop-client/transactions/transactionsSlice';
|
||||
import {
|
||||
name as usersSliceName,
|
||||
reducer as usersSliceReducer,
|
||||
} from '@desktop-client/users/usersSlice';
|
||||
|
||||
const appReducer = combineReducers({
|
||||
[accountsSliceName]: accountsSliceReducer,
|
||||
[appSliceName]: appSliceReducer,
|
||||
[budgetfilesSliceName]: budgetfilesSliceReducer,
|
||||
[modalsSliceName]: modalsSliceReducer,
|
||||
[notificationsSliceName]: notificationsSliceReducer,
|
||||
[payeesSliceName]: payeesSliceReducer,
|
||||
[prefsSliceName]: prefsSliceReducer,
|
||||
[transactionsSliceName]: transactionsSliceReducer,
|
||||
[tagsSliceName]: tagsSliceReducer,
|
||||
[usersSliceName]: usersSliceReducer,
|
||||
});
|
||||
|
||||
export let mockStore: typeof realStore = configureStore({
|
||||
reducer: appReducer,
|
||||
});
|
||||
|
||||
export function resetMockStore() {
|
||||
mockStore = configureStore({
|
||||
reducer: appReducer,
|
||||
});
|
||||
}
|
||||
|
||||
export function TestProvider({ children }: { children: ReactNode }) {
|
||||
return <Provider store={mockStore}>{children}</Provider>;
|
||||
}
|
||||
@@ -86,6 +86,17 @@ export const store = configureStore({
|
||||
}).prepend(notifyOnRejectedActionsMiddleware.middleware),
|
||||
});
|
||||
|
||||
export function configureAppStore() {
|
||||
return configureStore({
|
||||
reducer: rootReducer,
|
||||
middleware: getDefaultMiddleware =>
|
||||
getDefaultMiddleware({
|
||||
// TODO: Fix this in a separate PR. Remove non-serializable states in the store.
|
||||
serializableCheck: false,
|
||||
}).prepend(notifyOnRejectedActionsMiddleware.middleware),
|
||||
});
|
||||
}
|
||||
|
||||
export type AppStore = typeof store;
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { resetTestProviders } from './mocks';
|
||||
import { installPolyfills } from './polyfills';
|
||||
import { resetMockStore } from './redux/mock';
|
||||
|
||||
installPolyfills();
|
||||
|
||||
global.IS_TESTING = true;
|
||||
global.Actual = {};
|
||||
global.Actual = {} as typeof global.Actual;
|
||||
|
||||
type Size = { height: number; width: number };
|
||||
|
||||
type AutoSizerProps = {
|
||||
renderProp?: (size: Size) => ReactNode;
|
||||
children?: (size: Size) => ReactNode;
|
||||
};
|
||||
|
||||
vi.mock('react-virtualized-auto-sizer', () => {
|
||||
const AutoSizer = props => {
|
||||
const AutoSizer = (props: AutoSizerProps) => {
|
||||
const render = props.renderProp ?? props.children;
|
||||
return render ? render({ height: 1000, width: 600 }) : null;
|
||||
};
|
||||
@@ -22,13 +31,13 @@ vi.mock('react-virtualized-auto-sizer', () => {
|
||||
global.Date.now = () => 123456789;
|
||||
|
||||
global.__resetWorld = () => {
|
||||
resetMockStore();
|
||||
resetTestProviders();
|
||||
};
|
||||
|
||||
process.on('unhandledRejection', reason => {
|
||||
process.on('unhandledRejection', (reason: unknown) => {
|
||||
console.error('REJECTION', reason);
|
||||
});
|
||||
|
||||
global.afterEach(() => {
|
||||
afterEach(() => {
|
||||
global.__resetWorld();
|
||||
});
|
||||
1
packages/desktop-client/src/transactions/index.ts
Normal file
1
packages/desktop-client/src/transactions/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './queries';
|
||||
33
packages/desktop-client/src/transactions/queries.ts
Normal file
33
packages/desktop-client/src/transactions/queries.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { infiniteQueryOptions, keepPreviousData } from '@tanstack/react-query';
|
||||
|
||||
import type { Query } from 'loot-core/shared/query';
|
||||
import type { TransactionEntity } from 'loot-core/types/models';
|
||||
|
||||
import { aqlQuery } from '@desktop-client/queries/aqlQuery';
|
||||
|
||||
export const transactionQueries = {
|
||||
all: () => ['transactions'],
|
||||
aql: ({ query, pageSize = 50 }: { query?: Query; pageSize?: number }) =>
|
||||
infiniteQueryOptions<TransactionEntity[]>({
|
||||
queryKey: [...transactionQueries.all(), 'aql', query, pageSize],
|
||||
queryFn: async ({ pageParam }) => {
|
||||
if (!query) {
|
||||
// Shouldn't happen because of the enabled flag, but needed to satisfy TS
|
||||
throw new Error('No query provided.');
|
||||
}
|
||||
const queryWithOffset = query
|
||||
.offset((pageParam as number) * pageSize)
|
||||
.limit(pageSize);
|
||||
|
||||
const { data }: { data: TransactionEntity[] } =
|
||||
await aqlQuery(queryWithOffset);
|
||||
return data;
|
||||
},
|
||||
placeholderData: keepPreviousData,
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (lastPage, pages) => {
|
||||
return lastPage.length < pageSize ? undefined : pages.length;
|
||||
},
|
||||
enabled: !!query,
|
||||
}),
|
||||
};
|
||||
@@ -31,7 +31,7 @@ const addWatchers = (): Plugin => ({
|
||||
const injectShims = (): Plugin[] => {
|
||||
const buildShims = path.resolve('./src/build-shims.js');
|
||||
const commonInject = {
|
||||
exclude: ['src/setupTests.js'],
|
||||
exclude: ['src/setupTests.ts'],
|
||||
global: [buildShims, 'global'],
|
||||
};
|
||||
|
||||
@@ -216,7 +216,7 @@ export default defineConfig(async ({ mode }) => {
|
||||
include: ['src/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
setupFiles: './src/setupTests.js',
|
||||
setupFiles: './src/setupTests.ts',
|
||||
testTimeout: 10000,
|
||||
onConsoleLog(log: string, type: 'stdout' | 'stderr'): boolean | void {
|
||||
// print only console.error
|
||||
|
||||
Reference in New Issue
Block a user