feat: Accounts datatable.

This commit is contained in:
Ahmed Bouhuolia
2020-03-30 00:13:45 +02:00
parent d695188d3a
commit d9e10fd6b4
19 changed files with 543 additions and 294 deletions

View File

@@ -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
};
};

View File

@@ -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>
);
}

View File

@@ -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

View File

@@ -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>
)
}

View File

@@ -6,7 +6,7 @@ export default function DialogsContainer() {
return (
<React.Fragment>
<AccountFormDialog />
<UserFormDialog />
{/* <UserFormDialog /> */}
</React.Fragment>
);
}

View File

@@ -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 }
</>
);
}