feat: cashflow account transactions infinity scroll loading.

This commit is contained in:
a.bouhuolia
2021-10-23 23:10:48 +02:00
parent c7013caf12
commit 65e8d3f26a
11 changed files with 268 additions and 68 deletions

View File

@@ -2,6 +2,8 @@ import React from 'react';
import styled from 'styled-components';
import { Classes } from '@blueprintjs/core';
import clsx from 'classnames';
import useContextMenu from 'react-use-context-menu';
import ContextMenu from '../ContextMenu';
import Icon from '../Icon';
const BankAccountWrap = styled.div`
@@ -20,21 +22,6 @@ const BankAccountWrap = styled.div`
}
`;
const BankAccountAnchor = styled.a`
text-decoration: none;
display: flex;
flex-direction: column;
flex: 1;
color: inherit;
&:hover,
&:focus,
&:active {
color: inherit;
text-decoration: none;
}
`;
const BankAccountHeader = styled.div`
padding: 10px 12px;
padding-top: 16px;
@@ -179,43 +166,65 @@ function BankAccountTypeIcon({ type }) {
);
}
export function BankAccount({
title,
code,
type,
balance,
loading = false,
to
updatedBeforeText,
to,
contextMenuContent,
}) {
const [
bindMenu,
bindMenuItem,
useContextTrigger,
{ coords, setVisible, isVisible },
] = useContextMenu();
const [bindTrigger] = useContextTrigger({
collect: () => 'Title',
});
const handleClose = React.useCallback(() => {
setVisible(false);
}, [setVisible]);
return (
<BankAccountWrap>
<BankAccountAnchor href="#">
<BankAccountHeader>
<BankAccountTitle className={clsx({ [Classes.SKELETON]: loading })}>
{title}
</BankAccountTitle>
<BnakAccountCode className={clsx({ [Classes.SKELETON]: loading })}>
{code}
</BnakAccountCode>
{!loading && <BankAccountTypeIcon type={type} />}
</BankAccountHeader>
<BankAccountWrap {...bindTrigger}>
<BankAccountHeader>
<BankAccountTitle className={clsx({ [Classes.SKELETON]: loading })}>
{title}
</BankAccountTitle>
<BnakAccountCode className={clsx({ [Classes.SKELETON]: loading })}>
{code}
</BnakAccountCode>
{!loading && <BankAccountTypeIcon type={type} />}
</BankAccountHeader>
<BankAccountMeta>
<BankAccountMetaLine
title={'Account transactions'}
value={2}
className={clsx({ [Classes.SKELETON]: loading })}
/>
<BankAccountMetaLine
title={'Updated 2 days ago'}
className={clsx({ [Classes.SKELETON]: loading })}
/>
</BankAccountMeta>
<BankAccountMeta>
<BankAccountMetaLine
title={'Account transactions'}
value={2}
className={clsx({ [Classes.SKELETON]: loading })}
/>
<BankAccountMetaLine
title={updatedBeforeText}
className={clsx({ [Classes.SKELETON]: loading })}
/>
</BankAccountMeta>
<BankAccountBalance amount={balance} loading={loading} />
</BankAccountAnchor>
<BankAccountBalance amount={balance} loading={loading} />
<ContextMenu
bindMenu={bindMenu}
isOpen={isVisible}
coords={coords}
onClosed={handleClose}
>
<contextMenuContent />
</ContextMenu>
</BankAccountWrap>
);
}

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { useIntersectionObserver } from 'hooks/utils';
/**
* Intersection observer.
*/
export function IntersectionObserver({ onIntersect }) {
const loadMoreButtonRef = React.useRef();
useIntersectionObserver({
// enabled: !isItemsLoading && !isResourceLoading,
target: loadMoreButtonRef,
onIntersect: () => {
onIntersect && onIntersect();
},
});
return (
<div ref={loadMoreButtonRef} style={{ opacity: 0 }}>
Load Newer
</div>
);
}

View File

@@ -82,6 +82,7 @@ export * from './MultiSelectTaggable';
export * from './Utils/FormatNumber';
export * from './Utils/FormatDate';
export * from './BankAccounts';
export * from './IntersectionObserver'
const Hint = FieldHint;

View File

@@ -52,14 +52,13 @@ function AccountTransactionsDataTable({
TableRowsRenderer={TableVirtualizedListRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
// #TableVirtualizedListRows props.
vListrowHeight={cashflowTansactionsTableSize == 'small' ? 40 : 42}
vListrowHeight={cashflowTansactionsTableSize == 'small' ? 32 : 40}
vListOverscanRowCount={0}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
initialColumnsWidths={initialColumnsWidths}
onColumnResizing={handleColumnResizing}
size={cashflowTansactionsTableSize}
noResults={'There is deposit/withdrawal transactions on the current account.'}
className="table-constrant"
/>
);

View File

@@ -29,7 +29,7 @@ function AccountSwitchButton() {
function AccountSwitchItem() {
const { push } = useHistory();
const { cashflowAccounts } = useAccountTransactionsContext();
const { cashflowAccounts, accountId } = useAccountTransactionsContext();
// Handle item click.
const handleItemClick = curry((account, event) => {
@@ -39,7 +39,9 @@ function AccountSwitchItem() {
const items = cashflowAccounts.map((account) => (
<AccountSwitchMenuItem
name={account.name}
balance={account.formatted_amount}
onClick={handleItemClick(account)}
active={account.id === accountId}
/>
));
@@ -111,7 +113,7 @@ function AccountSwitchMenuItem({
}) {
return (
<MenuItem
label={'LYD100,000'}
label={balance}
text={
<React.Fragment>
<AccountSwitchItemName>{name}</AccountSwitchItemName>

View File

@@ -1,14 +1,20 @@
import React from 'react';
import { useParams } from 'react-router-dom';
import { flatten, map } from 'lodash';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { IntersectionObserver } from 'components';
import {
useCashflowTransactions,
useAccountTransactionsInfinity,
useCashflowAccounts,
useAccount,
} from 'hooks/query';
const AccountTransactionsContext = React.createContext();
function flattenInfinityPages(data) {
return flatten(map(data.pages, (page) => page.cashflow_transactions));
}
/**
* Account transctions provider.
*/
@@ -18,26 +24,44 @@ function AccountTransactionsProvider({ query, ...props }) {
// Fetch cashflow account transactions list
const {
data: cashflowTransactions,
data: cashflowTransactionsPages,
isFetching: isCashFlowTransactionsFetching,
isLoading: isCashFlowTransactionsLoading,
} = useCashflowTransactions(accountId, {
enabled: !!accountId,
isSuccess: isCashflowTransactionsSuccess,
fetchNextPage: fetchNextTransactionsPage,
isFetchingNextPage,
} = useAccountTransactionsInfinity(accountId, {
page_size: 50,
});
// Fetch cashflow accounts .
const cashflowTransactions = React.useMemo(
() =>
isCashflowTransactionsSuccess
? flattenInfinityPages(cashflowTransactionsPages)
: [],
[cashflowTransactionsPages, isCashflowTransactionsSuccess],
);
// Fetch cashflow accounts.
const {
data: cashflowAccounts,
isFetching: isCashFlowAccountsFetching,
isLoading: isCashFlowAccountsLoading,
} = useCashflowAccounts(query, { keepPreviousData: true });
// Retrieve specific account details.
const {
data: currentAccount,
isFetching: isCurrentAccountFetching,
isLoading: isCurrentAccountLoading,
} = useAccount(accountId, { keepPreviousData: true });
const handleObserverInteract = React.useCallback(() => {
if (!isFetchingNextPage) {
fetchNextTransactionsPage();
}
}, [isFetchingNextPage, fetchNextTransactionsPage]);
// Provider payload.
const provider = {
accountId,
@@ -55,6 +79,10 @@ function AccountTransactionsProvider({ query, ...props }) {
return (
<DashboardInsider name={'account-transactions'}>
<AccountTransactionsContext.Provider value={provider} {...props} />
<IntersectionObserver
onIntersect={handleObserverInteract}
// enabled={!isFetchingNextPage}
/>
</DashboardInsider>
);
}

View File

@@ -21,32 +21,41 @@ export function useAccountTransactionsColumns() {
{
id: 'type',
Header: intl.get('type'),
accessor: 'type',
accessor: 'reference_type_formatted',
className: 'type',
width: 140,
textOverview: true,
},
{
id: 'status',
Header: intl.get('status'),
// accessor:
id: 'transaction_number',
Header: intl.get('transaction_number'),
accessor: 'transaction_number',
width: 160,
className: 'status',
className: 'transaction_number',
},
{
id: 'reference_number',
Header: intl.get('reference_no'),
accessor: 'reference_number',
width: 160,
className: 'reference_number',
},
{
id: 'deposit',
Header: intl.get('cash_flow.label.deposit'),
accessor: 'formattedDeposit',
accessor: 'formatted_deposit',
width: 110,
className: 'deposit',
align: 'right',
textOverview: true,
align: 'right'
},
{
id: 'withdrawal',
Header: intl.get('cash_flow.label.withdrawal'),
accessor: 'formattedWithdrawal',
accessor: 'formatted_withdrawal',
className: 'withdrawal',
width: 150,
textOverview: true,
align: 'right',
},
{
@@ -55,7 +64,8 @@ export function useAccountTransactionsColumns() {
accessor: 'running_balance',
className: 'running_balance',
width: 150,
align: 'right',
textOverview: true,
align: 'right'
},
],
[],

View File

@@ -2,7 +2,9 @@ import React from 'react';
import { isNull } from 'lodash';
import styled from 'styled-components';
import { Link } from 'react-router-dom';
import { BankAccountsList, BankAccount } from '../../../components';
import { Menu, MenuItem, MenuDivider, Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import { BankAccountsList, BankAccount, If, Icon } from '../../../components';
import { useCashFlowAccountsContext } from './CashFlowAccountsProvider';
const CashflowAccountsGridWrap = styled.div`
@@ -22,16 +24,22 @@ function CashflowAccountsSkeleton() {
));
}
function getUpdatedBeforeText(createdAt) {
return 'Updated before 2 years.';
}
function CashflowAccountsGridItems({ accounts }) {
return accounts.map((account) => (
<Link to={`/cashflow-accounts/${account.id}/transactions`}>
<CashflowAccountAnchor to={`/cashflow-accounts/${account.id}/transactions`}>
<BankAccount
title={account.name}
code={account.code}
balance={!isNull(account.amount) ? account.formattedAmount : '-'}
balance={!isNull(account.amount) ? account.formatted_amount : '-'}
type={'cash'}
contextMenuContent={CashflowAccountContextMenu}
updatedBeforeText={getUpdatedBeforeText(account.createdAt)}
/>
</Link>
</CashflowAccountAnchor>
));
}
@@ -52,3 +60,44 @@ export default function CashflowAccountsGrid() {
</CashflowAccountsGridWrap>
);
}
function CashflowAccountContextMenu() {
return (
<Menu>
<MenuItem
icon={<Icon icon="reader-18" />}
text={intl.get('view_details')}
/>
<MenuDivider />
<MenuItem icon={<Icon icon="pen-18" />} text={intl.get('edit_account')} />
<MenuDivider />
<If condition={false}>
<MenuItem
text={intl.get('inactivate_account')}
icon={<Icon icon="pause-16" iconSize={16} />}
/>
</If>
<If condition={!false}>
<MenuItem
text={intl.get('activate_account')}
icon={<Icon icon="play-16" iconSize={16} />}
/>
</If>
<MenuItem
text={intl.get('delete_account')}
icon={<Icon icon="trash-16" iconSize={16} />}
intent={Intent.DANGER}
/>
</Menu>
);
}
const CashflowAccountAnchor = styled(Link)`
&,
&:hover,
&:focus,
&:active {
color: inherit;
text-decoration: none;
}
`;

View File

@@ -1,10 +1,12 @@
import { useMutation, useQueryClient } from 'react-query';
import { useMutation, useQueryClient, useInfiniteQuery } from 'react-query';
import { useRequestQuery } from '../useQueryRequest';
import useApiRequest from '../useRequest';
import t from './types';
const commonInvalidateQueries = (queryClient) => {
// queryClient.invalidateQueries(t.CASH_FLOW_TRANSACTIONS);
// Invalidate accounts.
queryClient.invalidateQueries(t.ACCOUNTS);
queryClient.invalidateQueries(t.ACCOUNT);
queryClient.invalidateQueries(t.CASH_FLOW_TRANSACTION);
};
@@ -22,13 +24,14 @@ export function useCashflowAccounts(query, props) {
},
);
}
/**
* Retrieve account transactions list.
*/
export function useCashflowTransactions(id, props) {
return useRequestQuery(
[t.CASH_FLOW_TRANSACTIONS, id],
{ method: 'get', url: `cashflow/account/${1000}/transactions` },
{ method: 'get', url: `cashflow/account/${id}/transactions` },
{
select: (res) => res.data.cashflow_transactions,
defaultData: [],
@@ -73,3 +76,42 @@ export function useDeleteCashflowTransaction(props) {
...props,
});
}
/**
* Retrieve account transactions infinity scrolling.
* @param {number} accountId
* @param {*} axios
* @returns
*/
export function useAccountTransactionsInfinity(
accountId,
query,
axios,
infinityProps,
) {
const apiRequest = useApiRequest();
return useInfiniteQuery(
['CASHFLOW_ACCOUNT_TRANSACTIONS_INFINITY', accountId],
async ({ pageParam = 1 }) => {
const response = await apiRequest.http({
...axios,
method: 'get',
url: `/api/cashflow/account/${accountId}/transactions`,
params: { page: pageParam, ...query },
});
return response.data;
},
{
getPreviousPageParam: (firstPage) => firstPage.pagination.page - 1,
getNextPageParam: (lastPage) => {
const { pagination } = lastPage;
return pagination.total > pagination.page_size * pagination.page
? lastPage.pagination.page + 1
: undefined;
},
...infinityProps,
},
);
}

View File

@@ -6,4 +6,5 @@ export * from './useUpdateEffect';
export * from './useWatch';
export * from './useWhen';
export * from './useRequestPdf';
export * from './useAsync';
export * from './useAsync';
export * from './useIntersectionObserver';

View File

@@ -0,0 +1,36 @@
import React from 'react';
export function useIntersectionObserver({
root,
target,
onIntersect,
threshold = 1.0,
rootMargin = '0px',
enabled = true,
}) {
React.useEffect(() => {
if (!enabled) {
return;
}
const observer = new IntersectionObserver(
(entries) =>
entries.forEach((entry) => entry.isIntersecting && onIntersect()),
{
root: root && root.current,
rootMargin,
// threshold,
threshold: 0.25,
},
);
const el = target && target.current;
if (!el) {
return;
}
observer.observe(el);
return () => {
observer.unobserve(el);
};
}, [target.current, enabled, onIntersect, root]);
}