Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sync: master to staging #852

Merged
merged 3 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
🌍 <a href="https://maffin.io" target="_blank">Landing page</a>&nbsp;&nbsp;
🍭 <a href="https://demo.maffin.io" target="_blank">Our demo</a>&nbsp;&nbsp;
📖 <a href="http://docs.maffin.io/docs" target="_blank">Help center</a>&nbsp;&nbsp;
📰 <a href="https://blog.maffin.io" target="_blank">Blog</a>
📰 <a href="https://blog.maffin.io" target="_blank">Blog</a>&nbsp;&nbsp;
👾 <a href="https://discord.com/channels/1222940742335463566/1222940742335463569" target="_blank">Discord</a>
</p>

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.
Expand Down
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
Loading