Skip to content

Commit

Permalink
Transactions table description search (#853)
Browse files Browse the repository at this point in the history
  • Loading branch information
argaen authored Apr 30, 2024
1 parent 9bd2580 commit 21bd634
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 36 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@
"class-validator": "^0.14.1",
"classnames": "^2.5.1",
"dayjs": "^1.11.10",
"debounce-promise": "^3.1.2",
"dinero.js": "^2.0.0-alpha.14",
"lodash.debounce": "^4.0.8",
"luxon": "^3.4.4",
"next": "^14.1.4",
"pako": "^2.1.0",
Expand Down Expand Up @@ -118,6 +118,7 @@
"@types/gapi.client.people": "^1.0.5",
"@types/google.accounts": "^0.0.14",
"@types/jest": "^29.5.5",
"@types/lodash.debounce": "^4.0.9",
"@types/luxon": "^3.4.2",
"@types/node": "^20.12.2",
"@types/pako": "^2.0.3",
Expand Down
18 changes: 18 additions & 0 deletions src/__tests__/components/SearchBox.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';

import SearchBox from '@/components/SearchBox';

describe('SearchBox', () => {
it('calls debounced on change', async () => {
const onChange = jest.fn();
render(<SearchBox onChange={onChange} />);

const input = screen.getByRole('textbox');
await userEvent.type(input, 'text');

await waitFor(() => expect(onChange).toBeCalledTimes(1));
expect(onChange).toBeCalledWith('text');
});
});
22 changes: 20 additions & 2 deletions src/__tests__/components/tables/TransactionsTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { DateTime } from 'luxon';
import {
render,
screen,
waitFor,
} from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import type { LinkProps } from 'next/link';
import type { UseQueryResult } from '@tanstack/react-query';

Expand Down Expand Up @@ -153,8 +155,8 @@ describe('TransactionsTable', () => {
},
{},
);
expect(apiHook.useSplitsCount).toBeCalledWith('account_guid_1');
expect(apiHook.useSplitsPagination).toBeCalledWith('account_guid_1', { pageIndex: 0, pageSize: 10 });
expect(apiHook.useSplitsCount).toBeCalledWith('account_guid_1', '');
expect(apiHook.useSplitsPagination).toBeCalledWith('account_guid_1', { pageIndex: 0, pageSize: 10 }, '');
});

it('creates Table with expected params', async () => {
Expand All @@ -173,6 +175,22 @@ describe('TransactionsTable', () => {
}), {});
});

it('filters with search text', async () => {
render(
<TransactionsTable
account={account}
/>,
);

const searchBox = screen.getByRole('textbox');
await userEvent.type(searchBox, 'search-text');

await waitFor(() => expect(apiHook.useSplitsCount).toBeCalledWith('account_guid_1', 'search-text'));
expect(
apiHook.useSplitsPagination,
).toBeCalledWith('account_guid_1', { pageIndex: 0, pageSize: 10 }, 'search-text');
});

it('renders Date column as expected', async () => {
render(<TransactionsTable account={account} />);

Expand Down
124 changes: 114 additions & 10 deletions src/__tests__/hooks/api/useSplits.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { Interval } from 'luxon';
import { DateTime, Interval } from 'luxon';
import { DataSource } from 'typeorm';
import { renderHook, waitFor } from '@testing-library/react';
import {
Expand Down Expand Up @@ -95,7 +95,7 @@ describe('useSplits', () => {
tx1 = await Transaction.create({
// inside the interval
date: TEST_INTERVAL.start?.plus({ month: 1 }),
description: 'description',
description: 'description1',
fk_currency: eur,
splits: [
await Split.create({
Expand All @@ -117,8 +117,8 @@ describe('useSplits', () => {

tx2 = await Transaction.create({
// outside the interval
date: TEST_INTERVAL.start?.minus({ month: 1 }),
description: 'description',
date: TEST_INTERVAL.start?.plus({ month: 2 }),
description: 'description2',
fk_currency: eur,
splits: [
await Split.create({
Expand Down Expand Up @@ -146,7 +146,7 @@ describe('useSplits', () => {
});

describe('useSplitsPagination', () => {
it('calls query as expected', async () => {
it('returns pageSize instances', async () => {
const { result } = renderHook(
() => useSplitsPagination('guid', { pageIndex: 0, pageSize: 1 }),
{ wrapper },
Expand All @@ -155,15 +155,60 @@ describe('useSplits', () => {
await waitFor(() => expect(result.current.status).toEqual('success'));
expect(result.current.data).toMatchObject([
expect.objectContaining({
guid: tx1.splits[0].guid,
guid: tx2.splits[0].guid,
balance: 100, // balance contains splits inserted previously so 50 + 50
}),
]);

const queryCache = QUERY_CLIENT.getQueryCache().getAll();
expect(queryCache).toHaveLength(1);
expect(queryCache[0].queryKey).toEqual(
['api', 'splits', 'guid', 'page', { interval: TEST_INTERVAL.toISODate(), pageIndex: 0, pageSize: 1 }],
[
'api', 'splits', 'guid', 'page',
{
interval: TEST_INTERVAL.toISODate(),
pageIndex: 0,
pageSize: 1,
search: '',
},
],
);
});

it('filters by interval', async () => {
const interval = Interval.fromDateTimes(
(TEST_INTERVAL.start as DateTime),
(TEST_INTERVAL.start as DateTime).plus({ month: 1 }),
);
jest.spyOn(stateHooks, 'useInterval').mockReturnValue({
data: interval,
} as DefinedUseQueryResult<Interval>);

const { result } = renderHook(
() => useSplitsPagination('guid', { pageIndex: 0, pageSize: 1 }),
{ wrapper },
);

await waitFor(() => expect(result.current.status).toEqual('success'));
expect(result.current.data).toMatchObject([
expect.objectContaining({
guid: tx1.splits[0].guid,
balance: 50, // this is the first split so balance is 50
}),
]);

const queryCache = QUERY_CLIENT.getQueryCache().getAll();
expect(queryCache).toHaveLength(1);
expect(queryCache[0].queryKey).toEqual(
[
'api', 'splits', 'guid', 'page',
{
interval: interval.toISODate(),
pageIndex: 0,
pageSize: 1,
search: '',
},
],
);
});

Expand All @@ -189,25 +234,84 @@ describe('useSplits', () => {
const queryCache = QUERY_CLIENT.getQueryCache().getAll();
expect(queryCache).toHaveLength(1);
expect(queryCache[0].queryKey).toEqual(
['api', 'splits', 'guid', 'page', { interval: TEST_INTERVAL.toISODate(), pageIndex: 0, pageSize: 1 }],
[
'api', 'splits', 'guid', 'page',
{
interval: TEST_INTERVAL.toISODate(),
pageIndex: 0,
pageSize: 1,
search: '',
},
],
);
});

it('filters with search term', async () => {
const { result } = renderHook(
() => useSplitsPagination('guid', { pageIndex: 0, pageSize: 2 }, 'description1'),
{ wrapper },
);

await waitFor(() => expect(result.current.status).toEqual('success'));
expect(result.current.data).toMatchObject([
expect.objectContaining({
guid: tx1.splits[0].guid,
balance: 50,
}),
]);

const queryCache = QUERY_CLIENT.getQueryCache().getAll();
expect(queryCache).toHaveLength(1);
expect(queryCache[0].queryKey).toEqual(
[
'api', 'splits', 'guid', 'page',
{
interval: TEST_INTERVAL.toISODate(),
pageIndex: 0,
pageSize: 2,
search: 'description1',
},
],
);
});
});

describe('useSplitsCount', () => {
it('calls query as expected', async () => {
it('filters by interval', async () => {
const { result } = renderHook(
() => useSplitsCount('guid'),
{ wrapper },
);

await waitFor(() => expect(result.current.status).toEqual('success'));
expect(result.current.data).toEqual(2);

const queryCache = QUERY_CLIENT.getQueryCache().getAll();
expect(queryCache).toHaveLength(1);
expect(queryCache[0].queryKey).toEqual(
[
'api', 'splits', 'guid', 'count',
{ interval: TEST_INTERVAL.toISODate(), search: '' },
],
);
});

it('filters with search term', async () => {
const { result } = renderHook(
() => useSplitsCount('guid', 'description2'),
{ wrapper },
);

await waitFor(() => expect(result.current.status).toEqual('success'));
expect(result.current.data).toEqual(1);

const queryCache = QUERY_CLIENT.getQueryCache().getAll();
expect(queryCache).toHaveLength(1);
expect(queryCache[0].queryKey).toEqual(
['api', 'splits', 'guid', 'count', { interval: TEST_INTERVAL.toISODate() }],
[
'api', 'splits', 'guid', 'count',
{ interval: TEST_INTERVAL.toISODate(), search: 'description2' },
],
);
});
});
Expand Down
22 changes: 22 additions & 0 deletions src/components/SearchBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import debounce from 'lodash.debounce';

export type SearchBoxProps = {
onChange: Function,
};

export default function SearchBox({
onChange,
}: SearchBoxProps): JSX.Element {
const debounced = debounce(
(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target?.value),
500,
);

return (
<input
onChange={debounced}
placeholder="Search..."
/>
);
}
37 changes: 23 additions & 14 deletions src/components/tables/TransactionsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { DateTime } from 'luxon';
import FormButton from '@/components/buttons/FormButton';
import { Tooltip } from '@/components/tooltips';
import TransactionForm from '@/components/forms/transaction/TransactionForm';
import SearchBox from '@/components/SearchBox';
import Table from '@/components/tables/Table';
import Money from '@/book/Money';
import {
Expand All @@ -34,12 +35,17 @@ export type TransactionsTableProps = {
export default function TransactionsTable({
account,
}: TransactionsTableProps): JSX.Element {
const [search, setSearch] = React.useState('');
const [{ pageIndex, pageSize }, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const { data: splitsCount } = useSplitsCount(account.guid);
const { data: splits } = useSplitsPagination(account.guid, { pageSize, pageIndex });
const { data: splitsCount } = useSplitsCount(account.guid, search);
const { data: splits } = useSplitsPagination(
account.guid,
{ pageSize, pageIndex },
search,
);

columns[3].cell = AmountPartial(account);
columns[4].cell = BalancePartial(account);
Expand All @@ -53,18 +59,21 @@ export default function TransactionsTable({
);

return (
<Table<Split>
id="transactions-table"
columns={columns}
data={splits || []}
showPagination
onPaginationChange={setPagination}
pageCount={Math.ceil((splitsCount || 0) / pageSize)}
state={{
pagination,
}}
manualPagination
/>
<>
<SearchBox onChange={setSearch} />
<Table<Split>
id="transactions-table"
columns={columns}
data={splits || []}
showPagination
onPaginationChange={setPagination}
pageCount={Math.ceil((splitsCount || 0) / pageSize)}
state={{
pagination,
}}
manualPagination
/>
</>
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/css/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
@apply text-cyan-700 hover:text-cyan-600;
}

form input, form select, .input {
form input, form select, .input, input {
@apply bg-light-100 dark:bg-dark-800 rounded-md border-none my-3 p-2 focus:outline-none;
}

Expand Down
Loading

0 comments on commit 21bd634

Please sign in to comment.