From 85ad2d4dd0c1d9d96504048767616abd269df4ce Mon Sep 17 00:00:00 2001
From: Manuel Miranda
Date: Tue, 30 Apr 2024 14:34:12 +0700
Subject: [PATCH 1/3] Update README.md
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index 16bcdc7a..15a4c54f 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,7 @@
🍭 Our demo
📖 Help center
📰 Blog
+ 👾 Discord
Maffin is an **accounting software** focused on particulars or small businesses. It allows you to track income, expenses, investments and other types of assets while showing your financials in nice dashboards and reports.
From 9bd2580c6b844303f5244b36421c06636bb0498a Mon Sep 17 00:00:00 2001
From: Manuel Miranda
Date: Tue, 30 Apr 2024 14:34:45 +0700
Subject: [PATCH 2/3] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 15a4c54f..cd41f478 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
🌍 Landing page
🍭 Our demo
📖 Help center
- 📰 Blog
+ 📰 Blog
👾 Discord
From 21bd6345eb1d0dc6dffaec9fda5a9ee892cc9ab9 Mon Sep 17 00:00:00 2001
From: Manuel Miranda
Date: Tue, 30 Apr 2024 18:28:30 +0700
Subject: [PATCH 3/3] Transactions table description search (#853)
---
package.json | 3 +-
src/__tests__/components/SearchBox.test.tsx | 18 +++
.../tables/TransactionsTable.test.tsx | 22 +++-
src/__tests__/hooks/api/useSplits.test.tsx | 124 ++++++++++++++++--
src/components/SearchBox.tsx | 22 ++++
src/components/tables/TransactionsTable.tsx | 37 ++++--
src/css/globals.css | 2 +-
src/hooks/api/useSplits.ts | 15 ++-
yarn.lock | 17 ++-
9 files changed, 224 insertions(+), 36 deletions(-)
create mode 100644 src/__tests__/components/SearchBox.test.tsx
create mode 100644 src/components/SearchBox.tsx
diff --git a/package.json b/package.json
index b1586b9e..d9a40b12 100644
--- a/package.json
+++ b/package.json
@@ -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",
@@ -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",
diff --git a/src/__tests__/components/SearchBox.test.tsx b/src/__tests__/components/SearchBox.test.tsx
new file mode 100644
index 00000000..29320afb
--- /dev/null
+++ b/src/__tests__/components/SearchBox.test.tsx
@@ -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();
+
+ const input = screen.getByRole('textbox');
+ await userEvent.type(input, 'text');
+
+ await waitFor(() => expect(onChange).toBeCalledTimes(1));
+ expect(onChange).toBeCalledWith('text');
+ });
+});
diff --git a/src/__tests__/components/tables/TransactionsTable.test.tsx b/src/__tests__/components/tables/TransactionsTable.test.tsx
index 5cabc318..5a1b53ed 100644
--- a/src/__tests__/components/tables/TransactionsTable.test.tsx
+++ b/src/__tests__/components/tables/TransactionsTable.test.tsx
@@ -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';
@@ -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 () => {
@@ -173,6 +175,22 @@ describe('TransactionsTable', () => {
}), {});
});
+ it('filters with search text', async () => {
+ render(
+ ,
+ );
+
+ 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();
diff --git a/src/__tests__/hooks/api/useSplits.test.tsx b/src/__tests__/hooks/api/useSplits.test.tsx
index d8e5a023..0d14243c 100644
--- a/src/__tests__/hooks/api/useSplits.test.tsx
+++ b/src/__tests__/hooks/api/useSplits.test.tsx
@@ -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 {
@@ -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({
@@ -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({
@@ -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 },
@@ -155,7 +155,7 @@ 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
}),
]);
@@ -163,7 +163,52 @@ 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 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);
+
+ 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: '',
+ },
+ ],
);
});
@@ -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' },
+ ],
);
});
});
diff --git a/src/components/SearchBox.tsx b/src/components/SearchBox.tsx
new file mode 100644
index 00000000..f875f479
--- /dev/null
+++ b/src/components/SearchBox.tsx
@@ -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) => onChange(e.target?.value),
+ 500,
+ );
+
+ return (
+
+ );
+}
diff --git a/src/components/tables/TransactionsTable.tsx b/src/components/tables/TransactionsTable.tsx
index cfcc39c7..3923d08c 100644
--- a/src/components/tables/TransactionsTable.tsx
+++ b/src/components/tables/TransactionsTable.tsx
@@ -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 {
@@ -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({
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);
@@ -53,18 +59,21 @@ export default function TransactionsTable({
);
return (
-
- id="transactions-table"
- columns={columns}
- data={splits || []}
- showPagination
- onPaginationChange={setPagination}
- pageCount={Math.ceil((splitsCount || 0) / pageSize)}
- state={{
- pagination,
- }}
- manualPagination
- />
+ <>
+
+
+ id="transactions-table"
+ columns={columns}
+ data={splits || []}
+ showPagination
+ onPaginationChange={setPagination}
+ pageCount={Math.ceil((splitsCount || 0) / pageSize)}
+ state={{
+ pagination,
+ }}
+ manualPagination
+ />
+ >
);
}
diff --git a/src/css/globals.css b/src/css/globals.css
index dabec454..b44cff34 100644
--- a/src/css/globals.css
+++ b/src/css/globals.css
@@ -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;
}
diff --git a/src/hooks/api/useSplits.ts b/src/hooks/api/useSplits.ts
index c9110ed5..5e3d9c92 100644
--- a/src/hooks/api/useSplits.ts
+++ b/src/hooks/api/useSplits.ts
@@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query';
-import { Between, FindOptionsWhere } from 'typeorm';
+import { Between, FindOptionsWhere, Like } from 'typeorm';
import type { UseQueryResult } from '@tanstack/react-query';
import { Split } from '@/book/entities';
@@ -60,6 +60,7 @@ export function useSplits(
export function useSplitsPagination(
account: string,
pagination: { pageSize: number, pageIndex: number } = { pageSize: 10, pageIndex: 0 },
+ search = '',
): UseQueryResult {
const { data: interval } = useInterval();
@@ -67,7 +68,7 @@ export function useSplitsPagination(
...Split.CACHE_KEY,
account,
'page',
- { ...pagination, interval: interval.toISODate() },
+ { ...pagination, search, interval: interval.toISODate() },
];
const result = useQuery({
queryKey,
@@ -91,6 +92,7 @@ export function useSplitsPagination(
.where('splits.account_guid = :account_guid', { account_guid: account })
.andWhere('tx.post_date >= :start', { start: interval.start?.toSQLDate() })
.andWhere('tx.post_date <= :end', { end: interval.end?.toSQLDate() })
+ .andWhere('tx.description LIKE :search', { search: `%${search}%` })
.orderBy('tx.post_date', 'DESC')
.addOrderBy('tx.enter_date', 'DESC')
.addOrderBy('splits.quantity_num', 'ASC')
@@ -111,10 +113,16 @@ export function useSplitsPagination(
*/
export function useSplitsCount(
account: string,
+ search = '',
): UseQueryResult {
const { data: interval } = useInterval();
- const queryKey = [...Split.CACHE_KEY, account, 'count', { interval: interval.toISODate() }];
+ const queryKey = [
+ ...Split.CACHE_KEY,
+ account,
+ 'count',
+ { search, interval: interval.toISODate() },
+ ];
const result = useQuery({
queryKey,
queryFn: fetcher(
@@ -123,6 +131,7 @@ export function useSplitsCount(
fk_account: { guid: account },
fk_transaction: {
date: Between(interval.start, interval.end),
+ description: Like(`%${search}%`),
},
},
}),
diff --git a/yarn.lock b/yarn.lock
index f66659cb..ef366c7d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4530,6 +4530,18 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
+"@types/lodash.debounce@^4.0.9":
+ version "4.0.9"
+ resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz#0f5f21c507bce7521b5e30e7a24440975ac860a5"
+ integrity sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==
+ dependencies:
+ "@types/lodash" "*"
+
+"@types/lodash@*":
+ version "4.17.0"
+ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.0.tgz#d774355e41f372d5350a4d0714abb48194a489c3"
+ integrity sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==
+
"@types/luxon@^3.4.2":
version "3.4.2"
resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.4.2.tgz#e4fc7214a420173cea47739c33cdf10874694db7"
@@ -6076,11 +6088,6 @@ dayjs@^1.11.10, dayjs@^1.11.9:
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0"
integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==
-debounce-promise@^3.1.2:
- version "3.1.2"
- resolved "https://registry.yarnpkg.com/debounce-promise/-/debounce-promise-3.1.2.tgz#320fb8c7d15a344455cd33cee5ab63530b6dc7c5"
- integrity sha512-rZHcgBkbYavBeD9ej6sP56XfG53d51CD4dnaw989YX/nZ/ZJfgRx/9ePKmTNiUiyQvh4mtrMoS3OAWW+yoYtpg==
-
debug@2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"