mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 04:40:32 +00:00
feat: Accounts datatable.
This commit is contained in:
@@ -25,9 +25,10 @@ import ResourceConnect from 'connectors/Resource.connector';
|
||||
function AccountsActionsBar({
|
||||
openDialog,
|
||||
views,
|
||||
bulkActions,
|
||||
selectedRows = [],
|
||||
getResourceFields,
|
||||
onFilterChange,
|
||||
addAccountsTableQueries,
|
||||
onFilterChanged,
|
||||
}) {
|
||||
const {path} = useRouteMatch();
|
||||
const onClickNewAccount = () => { openDialog('account-form', {}); };
|
||||
@@ -37,13 +38,16 @@ function AccountsActionsBar({
|
||||
const viewsMenuItems = views.map((view) => {
|
||||
return (<MenuItem href={`${path}/${view.id}/custom_view`} text={view.name} />);
|
||||
});
|
||||
const hasBulkActionsSelected = useMemo(() => {
|
||||
return Object.keys(bulkActions).length > 0;
|
||||
}, [bulkActions]);
|
||||
const hasSelectedRows = useMemo(() => selectedRows.length > 0, [selectedRows]);
|
||||
|
||||
const filterDropdown = FilterDropdown({
|
||||
fields: accountsFields,
|
||||
onFilterChange,
|
||||
onFilterChange: (filterConditions) => {
|
||||
addAccountsTableQueries({
|
||||
filter_roles: filterConditions || '',
|
||||
});
|
||||
onFilterChanged && onFilterChanged(filterConditions);
|
||||
},
|
||||
});
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
@@ -82,7 +86,7 @@ function AccountsActionsBar({
|
||||
|
||||
</Popover>
|
||||
|
||||
{hasBulkActionsSelected && (
|
||||
{hasSelectedRows && (
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon='archive' iconSize={15} />}
|
||||
@@ -90,7 +94,7 @@ function AccountsActionsBar({
|
||||
/>
|
||||
)}
|
||||
|
||||
{hasBulkActionsSelected && (
|
||||
{hasSelectedRows && (
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon='trash' iconSize={15} />}
|
||||
@@ -115,7 +119,7 @@ function AccountsActionsBar({
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
bulkActions: state.accounts.bulkActions
|
||||
// selectedRows: state.accounts.selectedRows
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import {
|
||||
GridComponent,
|
||||
ColumnsDirective,
|
||||
ColumnDirective,
|
||||
Inject,
|
||||
Sort
|
||||
} from '@syncfusion/ej2-react-grids';
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useCallback, useState, useMemo } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Popover,
|
||||
@@ -13,50 +6,34 @@ import {
|
||||
MenuItem,
|
||||
MenuDivider,
|
||||
Position,
|
||||
Checkbox
|
||||
} from '@blueprintjs/core';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import useAsync from 'hooks/async';
|
||||
import Icon from 'components/Icon';
|
||||
import { handleBooleanChange, compose } from 'utils';
|
||||
import { compose } from 'utils';
|
||||
import AccountsConnect from 'connectors/Accounts.connector';
|
||||
import DialogConnect from 'connectors/Dialog.connector';
|
||||
import DashboardConnect from 'connectors/Dashboard.connector';
|
||||
import ViewConnect from 'connectors/View.connector';
|
||||
import LoadingIndicator from 'components/LoadingIndicator';
|
||||
import DataTable from 'components/DataTable';
|
||||
|
||||
function AccountsDataTable({
|
||||
filterConditions,
|
||||
accounts,
|
||||
onDeleteAccount,
|
||||
onInactiveAccount,
|
||||
openDialog,
|
||||
addBulkActionAccount,
|
||||
removeBulkActionAccount,
|
||||
fetchAccounts,
|
||||
changeCurrentView,
|
||||
changePageSubtitle,
|
||||
getViewItem,
|
||||
setTopbarEditView
|
||||
setTopbarEditView,
|
||||
accountsLoading,
|
||||
onFetchData,
|
||||
setSelectedRowsAccounts,
|
||||
}) {
|
||||
const { custom_view_id: customViewId } = useParams();
|
||||
|
||||
// Fetch accounts list according to the given custom view id.
|
||||
const fetchHook = useAsync(async () => {
|
||||
await Promise.all([
|
||||
fetchAccounts({
|
||||
custom_view_id: customViewId,
|
||||
stringified_filter_roles: JSON.stringify(filterConditions) || '',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
useEffect(() => { fetchHook.execute(); }, [filterConditions]);
|
||||
|
||||
// Refetch accounts list after custom view id change.
|
||||
useEffect(() => {
|
||||
const viewMeta = getViewItem(customViewId);
|
||||
fetchHook.execute();
|
||||
|
||||
if (customViewId) {
|
||||
changeCurrentView(customViewId);
|
||||
@@ -65,10 +42,8 @@ function AccountsDataTable({
|
||||
changePageSubtitle((customViewId && viewMeta) ? viewMeta.name : '');
|
||||
}, [customViewId]);
|
||||
|
||||
useEffect(() => () => {
|
||||
// Clear page subtitle when unmount the page.
|
||||
changePageSubtitle('');
|
||||
}, []);
|
||||
// Clear page subtitle when unmount the page.
|
||||
useEffect(() => () => { changePageSubtitle(''); }, []);
|
||||
|
||||
const handleEditAccount = account => () => {
|
||||
openDialog('account-form', { action: 'edit', id: account.id });
|
||||
@@ -88,95 +63,76 @@ function AccountsDataTable({
|
||||
onClick={() => onDeleteAccount(account)} />
|
||||
</Menu>
|
||||
);
|
||||
const columns = useMemo(() => [
|
||||
{
|
||||
id: 'name',
|
||||
Header: 'Account Name',
|
||||
accessor: 'name',
|
||||
},
|
||||
{
|
||||
id: 'code',
|
||||
Header: 'Code',
|
||||
accessor: 'code'
|
||||
},
|
||||
{
|
||||
id: 'type',
|
||||
Header: 'Type',
|
||||
accessor: 'type.name'
|
||||
},
|
||||
{
|
||||
id: 'normal',
|
||||
Header: 'Normal',
|
||||
Cell: ({ cell }) => {
|
||||
const account = cell.row.original;
|
||||
const type = account.type ? account.type.normal : '';
|
||||
const arrowDirection = type === 'credit' ? 'down' : 'up';
|
||||
|
||||
const handleClickCheckboxBulk = account =>
|
||||
handleBooleanChange(value => {
|
||||
if (value) {
|
||||
addBulkActionAccount(account.id);
|
||||
} else {
|
||||
removeBulkActionAccount(account.id);
|
||||
}
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{
|
||||
field: '',
|
||||
headerText: '',
|
||||
template: account => (
|
||||
<Checkbox onChange={handleClickCheckboxBulk(account)} />
|
||||
),
|
||||
customAttributes: { class: 'checkbox-row' }
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
headerText: 'Account Name',
|
||||
customAttributes: { class: 'account-name' }
|
||||
},
|
||||
{
|
||||
field: 'code',
|
||||
headerText: 'Code'
|
||||
},
|
||||
{
|
||||
field: 'type.name',
|
||||
headerText: 'Type'
|
||||
},
|
||||
{
|
||||
headerText: 'Normal',
|
||||
template: column => {
|
||||
const type = column.type ? column.type.normal : '';
|
||||
return type === 'credit' ? (
|
||||
<Icon icon={'arrow-down'} />
|
||||
) : (
|
||||
<Icon icon={'arrow-up'} />
|
||||
);
|
||||
return (<Icon icon={`arrow-${arrowDirection}`} />);
|
||||
},
|
||||
customAttributes: { class: 'account-normal' }
|
||||
className: 'normal',
|
||||
},
|
||||
{
|
||||
field: 'balance',
|
||||
headerText: 'Balance',
|
||||
template: (column, data) => {
|
||||
return <span>$10,000</span>;
|
||||
}
|
||||
id: 'balance',
|
||||
Header: 'Balance',
|
||||
Cell: ({ cell }) => {
|
||||
const account = cell.row.original;
|
||||
const {balance} = account;
|
||||
|
||||
return ('undefined' !== typeof balance) ?
|
||||
(<span>{ balance.amount }</span>) :
|
||||
(<span>--</span>);
|
||||
},
|
||||
|
||||
// canResize: false,
|
||||
},
|
||||
{
|
||||
headerText: '',
|
||||
template: account => (
|
||||
id: 'actions',
|
||||
Header: '',
|
||||
Cell: ({ cell }) => (
|
||||
<Popover
|
||||
content={actionMenuList(account)}
|
||||
position={Position.RIGHT_BOTTOM}
|
||||
>
|
||||
content={actionMenuList(cell.row.original)}
|
||||
position={Position.RIGHT_BOTTOM}>
|
||||
<Button icon={<Icon icon='ellipsis-h' />} />
|
||||
</Popover>
|
||||
)
|
||||
),
|
||||
className: 'actions',
|
||||
width: 50,
|
||||
// canResize: false
|
||||
}
|
||||
];
|
||||
], []);
|
||||
|
||||
const handleDatatableFetchData = useCallback(() => {
|
||||
onFetchData && onFetchData();
|
||||
}, []);
|
||||
|
||||
const dataStateChange = state => {};
|
||||
return (
|
||||
<LoadingIndicator loading={fetchHook.pending} spinnerSize={30}>
|
||||
<GridComponent
|
||||
allowSorting={true}
|
||||
allowGrouping={true}
|
||||
dataSource={{ result: accounts, count: 12 }}
|
||||
dataStateChange={dataStateChange}
|
||||
>
|
||||
<ColumnsDirective>
|
||||
{columns.map(column => {
|
||||
return (
|
||||
<ColumnDirective
|
||||
field={column.field}
|
||||
headerText={column.headerText}
|
||||
template={column.template}
|
||||
allowSorting={true}
|
||||
customAttributes={column.customAttributes}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ColumnsDirective>
|
||||
<Inject services={[Sort]} />
|
||||
</GridComponent>
|
||||
</LoadingIndicator>
|
||||
<LoadingIndicator loading={accountsLoading} spinnerSize={30}>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={accounts}
|
||||
onFetchData={handleDatatableFetchData}
|
||||
manualSortBy={true} />
|
||||
</LoadingIndicator>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, {useEffect} from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
@@ -15,10 +15,14 @@ import { Link } from 'react-router-dom';
|
||||
import { compose } from 'utils';
|
||||
import AccountsConnect from 'connectors/Accounts.connector';
|
||||
import DashboardConnect from 'connectors/Dashboard.connector';
|
||||
import {useUpdateEffect} from 'hooks';
|
||||
|
||||
function AccountsViewsTabs({
|
||||
views,
|
||||
setTopbarEditView,
|
||||
setTopbarEditView,
|
||||
customViewChanged,
|
||||
addAccountsTableQueries,
|
||||
onViewChanged,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const { custom_view_id: customViewId } = useParams();
|
||||
@@ -30,6 +34,22 @@ function AccountsViewsTabs({
|
||||
const handleViewLinkClick = () => {
|
||||
setTopbarEditView(customViewId);
|
||||
}
|
||||
|
||||
useUpdateEffect(() => {
|
||||
customViewChanged && customViewChanged(customViewId);
|
||||
|
||||
addAccountsTableQueries({
|
||||
custom_view_id: customViewId || null,
|
||||
});
|
||||
onViewChanged && onViewChanged(customViewId);
|
||||
}, [customViewId]);
|
||||
|
||||
useEffect(() => {
|
||||
addAccountsTableQueries({
|
||||
custom_view_id: customViewId,
|
||||
})
|
||||
}, [customViewId]);
|
||||
|
||||
const tabs = views.map(view => {
|
||||
const baseUrl = '/dashboard/accounts';
|
||||
const link = (
|
||||
@@ -38,7 +58,9 @@ function AccountsViewsTabs({
|
||||
onClick={handleViewLinkClick}
|
||||
>{view.name}</Link>
|
||||
);
|
||||
return <Tab id={`custom_view_${view.id}`} title={link} />;
|
||||
return <Tab
|
||||
id={`custom_view_${view.id}`}
|
||||
title={link} />;
|
||||
});
|
||||
return (
|
||||
<Navbar className='navbar--dashboard-views'>
|
||||
@@ -49,7 +71,9 @@ function AccountsViewsTabs({
|
||||
selectedTabId={`custom_view_${customViewId}`}
|
||||
className='tabs--dashboard-views'
|
||||
>
|
||||
<Tab id='all' title={<Link to={`/dashboard/accounts`}>All</Link>} />
|
||||
<Tab
|
||||
id='all'
|
||||
title={<Link to={`/dashboard/accounts`}>All</Link>} />
|
||||
|
||||
{tabs}
|
||||
<Button
|
||||
|
||||
@@ -1,10 +1,41 @@
|
||||
import React from 'react';
|
||||
import { useTable, useExpanded, usePagination } from 'react-table'
|
||||
import React, {useEffect} from 'react';
|
||||
import {
|
||||
useTable,
|
||||
useExpanded,
|
||||
useRowSelect,
|
||||
usePagination,
|
||||
useResizeColumns,
|
||||
useAsyncDebounce,
|
||||
useSortBy,
|
||||
useFlexLayout
|
||||
} from 'react-table'
|
||||
import {Checkbox} from '@blueprintjs/core';
|
||||
import classnames from 'classnames';
|
||||
import Icon from 'components/Icon';
|
||||
// import { FixedSizeList } from 'react-window'
|
||||
|
||||
const IndeterminateCheckbox = React.forwardRef(
|
||||
({ indeterminate, ...rest }, ref) => {
|
||||
const defaultRef = React.useRef()
|
||||
const resolvedRef = ref || defaultRef
|
||||
|
||||
useEffect(() => {
|
||||
resolvedRef.current.indeterminate = indeterminate
|
||||
}, [resolvedRef, indeterminate])
|
||||
|
||||
return (
|
||||
<Checkbox ref={resolvedRef} {...rest} />
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default function DataTable({
|
||||
columns,
|
||||
data,
|
||||
loading,
|
||||
onFetchData,
|
||||
onSelectedRowsChange,
|
||||
manualSortBy = 'false'
|
||||
}) {
|
||||
const {
|
||||
getTableProps,
|
||||
@@ -20,8 +51,10 @@ export default function DataTable({
|
||||
nextPage,
|
||||
previousPage,
|
||||
setPageSize,
|
||||
|
||||
selectedFlatRows,
|
||||
// Get the state from the instance
|
||||
state: { pageIndex, pageSize },
|
||||
state: { pageIndex, pageSize, sortBy, selectedRowIds },
|
||||
} = useTable(
|
||||
{
|
||||
columns,
|
||||
@@ -33,59 +66,102 @@ export default function DataTable({
|
||||
// pageCount.
|
||||
// pageCount: controlledPageCount,
|
||||
getSubRows: row => row.children,
|
||||
manualSortBy
|
||||
},
|
||||
useSortBy,
|
||||
useExpanded,
|
||||
useRowSelect,
|
||||
usePagination,
|
||||
useResizeColumns,
|
||||
useFlexLayout,
|
||||
hooks => {
|
||||
hooks.visibleColumns.push(columns => [
|
||||
// Let's make a column for selection
|
||||
{
|
||||
id: 'selection',
|
||||
disableResizing: true,
|
||||
minWidth: 35,
|
||||
width: 35,
|
||||
maxWidth: 35,
|
||||
// The header can use the table's getToggleAllRowsSelectedProps method
|
||||
// to render a checkbox
|
||||
Header: ({ getToggleAllRowsSelectedProps }) => (
|
||||
<div>
|
||||
<IndeterminateCheckbox {...getToggleAllRowsSelectedProps()} />
|
||||
</div>
|
||||
),
|
||||
// The cell can use the individual row's getToggleRowSelectedProps method
|
||||
// to the render a checkbox
|
||||
Cell: ({ row }) => (
|
||||
<div>
|
||||
<IndeterminateCheckbox {...row.getToggleRowSelectedProps()} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
...columns,
|
||||
])
|
||||
}
|
||||
);
|
||||
|
||||
// Debounce our onFetchData call for 100ms
|
||||
const onFetchDataDebounced = useAsyncDebounce(onFetchData, 100);
|
||||
const onSelectRowsDebounced = useAsyncDebounce(onSelectedRowsChange, 250);
|
||||
|
||||
// When these table states change, fetch new data!
|
||||
useEffect(() => {
|
||||
onFetchDataDebounced({ pageIndex, pageSize, sortBy })
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={'bigcapital-datatable'}>
|
||||
<table {...getTableProps()}>
|
||||
<thead>
|
||||
<div {...getTableProps()} className="table">
|
||||
<div className="thead">
|
||||
{headerGroups.map(headerGroup => (
|
||||
<tr {...headerGroup.getHeaderGroupProps()}>
|
||||
<div {...headerGroup.getHeaderGroupProps()} className="tr">
|
||||
{headerGroup.headers.map(column => (
|
||||
<th {...column.getHeaderProps({
|
||||
className: column.className || '',
|
||||
<div {...column.getHeaderProps({
|
||||
className: classnames(column.className || '', 'th'),
|
||||
})}>
|
||||
{column.render('Header')}
|
||||
<span>
|
||||
{column.isSorted
|
||||
? column.isSortedDesc
|
||||
? ' 🔽'
|
||||
: ' 🔼'
|
||||
: ''}
|
||||
</span>
|
||||
</th>
|
||||
<div {...column.getSortByToggleProps()}>
|
||||
{column.render('Header')}
|
||||
<span>
|
||||
{column.isSorted
|
||||
? column.isSortedDesc
|
||||
? (<Icon icon="sort-down" />)
|
||||
: (<Icon icon="sort-up" />)
|
||||
: ''}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{column.canResize && (
|
||||
<div
|
||||
{...column.getResizerProps()}
|
||||
className={`resizer ${
|
||||
column.isResizing ? 'isResizing' : ''
|
||||
}`}>
|
||||
<div class="inner-resizer" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</tr>
|
||||
</div>
|
||||
))}
|
||||
</thead>
|
||||
<tbody {...getTableBodyProps()}>
|
||||
</div>
|
||||
<div {...getTableBodyProps()} className="tbody">
|
||||
{page.map((row, i) => {
|
||||
prepareRow(row)
|
||||
return (
|
||||
<tr {...row.getRowProps()}>
|
||||
<div {...row.getRowProps()} className="tr">
|
||||
{row.cells.map((cell) => {
|
||||
return <td {...cell.getCellProps({
|
||||
className: cell.column.className || '',
|
||||
})}>{ cell.render('Cell') }</td>
|
||||
return <div {...cell.getCellProps({
|
||||
className: classnames(cell.column.className || '', 'td'),
|
||||
})}>{ cell.render('Cell') }</div>
|
||||
})}
|
||||
</tr>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<tr>
|
||||
{loading ? (
|
||||
// Use our custom loading state to show a loading indicator
|
||||
<td colSpan="10000">Loading...</td>
|
||||
) : (
|
||||
<td colSpan="10000">
|
||||
{/* Showing {page.length} of ~{controlledPageCount * pageSize}{' '} results */}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -6,7 +6,7 @@ export default function DialogsContainer() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<AccountFormDialog />
|
||||
<UserFormDialog />
|
||||
{/* <UserFormDialog /> */}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, {useState, useEffect, useMemo} from 'react';
|
||||
import { Spinner } from '@blueprintjs/core';
|
||||
|
||||
export default function LoadingIndicator({
|
||||
@@ -6,15 +6,33 @@ export default function LoadingIndicator({
|
||||
spinnerSize = 40,
|
||||
children
|
||||
}) {
|
||||
const [rendered, setRendered] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) { setRendered(true); }
|
||||
}, [loading]);
|
||||
|
||||
const componentStyle = useMemo(() => {
|
||||
return {display: !loading ? 'block' : 'none'};
|
||||
}, [loading]);
|
||||
|
||||
const loadingComponent = useMemo(() => (
|
||||
<div class='dashboard__loading-indicator'>
|
||||
<Spinner size={spinnerSize} value={null} />
|
||||
</div>
|
||||
), [spinnerSize]);
|
||||
|
||||
const renderComponent = useMemo(() => (
|
||||
<div style={componentStyle}>{ children }</div>
|
||||
), [children, componentStyle]);
|
||||
|
||||
const maybeRenderComponent = rendered && renderComponent;
|
||||
const maybeRenderLoadingSpinner = loading && loadingComponent;
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading ? (
|
||||
<div class='dashboard__loading-indicator'>
|
||||
<Spinner size={spinnerSize} value={null} />
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
{ maybeRenderLoadingSpinner }
|
||||
{ maybeRenderComponent }
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user