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({ function AccountsActionsBar({
openDialog, openDialog,
views, views,
bulkActions, selectedRows = [],
getResourceFields, getResourceFields,
onFilterChange, addAccountsTableQueries,
onFilterChanged,
}) { }) {
const {path} = useRouteMatch(); const {path} = useRouteMatch();
const onClickNewAccount = () => { openDialog('account-form', {}); }; const onClickNewAccount = () => { openDialog('account-form', {}); };
@@ -37,13 +38,16 @@ function AccountsActionsBar({
const viewsMenuItems = views.map((view) => { const viewsMenuItems = views.map((view) => {
return (<MenuItem href={`${path}/${view.id}/custom_view`} text={view.name} />); return (<MenuItem href={`${path}/${view.id}/custom_view`} text={view.name} />);
}); });
const hasBulkActionsSelected = useMemo(() => { const hasSelectedRows = useMemo(() => selectedRows.length > 0, [selectedRows]);
return Object.keys(bulkActions).length > 0;
}, [bulkActions]);
const filterDropdown = FilterDropdown({ const filterDropdown = FilterDropdown({
fields: accountsFields, fields: accountsFields,
onFilterChange, onFilterChange: (filterConditions) => {
addAccountsTableQueries({
filter_roles: filterConditions || '',
});
onFilterChanged && onFilterChanged(filterConditions);
},
}); });
return ( return (
<DashboardActionsBar> <DashboardActionsBar>
@@ -82,7 +86,7 @@ function AccountsActionsBar({
</Popover> </Popover>
{hasBulkActionsSelected && ( {hasSelectedRows && (
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon='archive' iconSize={15} />} icon={<Icon icon='archive' iconSize={15} />}
@@ -90,7 +94,7 @@ function AccountsActionsBar({
/> />
)} )}
{hasBulkActionsSelected && ( {hasSelectedRows && (
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon='trash' iconSize={15} />} icon={<Icon icon='trash' iconSize={15} />}
@@ -115,7 +119,7 @@ function AccountsActionsBar({
const mapStateToProps = state => { const mapStateToProps = state => {
return { return {
bulkActions: state.accounts.bulkActions // selectedRows: state.accounts.selectedRows
}; };
}; };

View File

@@ -1,11 +1,4 @@
import { import React, { useEffect, useCallback, useState, useMemo } from 'react';
GridComponent,
ColumnsDirective,
ColumnDirective,
Inject,
Sort
} from '@syncfusion/ej2-react-grids';
import React, { useEffect } from 'react';
import { import {
Button, Button,
Popover, Popover,
@@ -13,50 +6,34 @@ import {
MenuItem, MenuItem,
MenuDivider, MenuDivider,
Position, Position,
Checkbox
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import useAsync from 'hooks/async';
import Icon from 'components/Icon'; import Icon from 'components/Icon';
import { handleBooleanChange, compose } from 'utils'; import { compose } from 'utils';
import AccountsConnect from 'connectors/Accounts.connector'; import AccountsConnect from 'connectors/Accounts.connector';
import DialogConnect from 'connectors/Dialog.connector'; import DialogConnect from 'connectors/Dialog.connector';
import DashboardConnect from 'connectors/Dashboard.connector'; import DashboardConnect from 'connectors/Dashboard.connector';
import ViewConnect from 'connectors/View.connector'; import ViewConnect from 'connectors/View.connector';
import LoadingIndicator from 'components/LoadingIndicator'; import LoadingIndicator from 'components/LoadingIndicator';
import DataTable from 'components/DataTable';
function AccountsDataTable({ function AccountsDataTable({
filterConditions,
accounts, accounts,
onDeleteAccount, onDeleteAccount,
onInactiveAccount, onInactiveAccount,
openDialog, openDialog,
addBulkActionAccount,
removeBulkActionAccount,
fetchAccounts,
changeCurrentView, changeCurrentView,
changePageSubtitle, changePageSubtitle,
getViewItem, getViewItem,
setTopbarEditView setTopbarEditView,
accountsLoading,
onFetchData,
setSelectedRowsAccounts,
}) { }) {
const { custom_view_id: customViewId } = useParams(); 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(() => { useEffect(() => {
const viewMeta = getViewItem(customViewId); const viewMeta = getViewItem(customViewId);
fetchHook.execute();
if (customViewId) { if (customViewId) {
changeCurrentView(customViewId); changeCurrentView(customViewId);
@@ -65,10 +42,8 @@ function AccountsDataTable({
changePageSubtitle((customViewId && viewMeta) ? viewMeta.name : ''); changePageSubtitle((customViewId && viewMeta) ? viewMeta.name : '');
}, [customViewId]); }, [customViewId]);
useEffect(() => () => {
// Clear page subtitle when unmount the page. // Clear page subtitle when unmount the page.
changePageSubtitle(''); useEffect(() => () => { changePageSubtitle(''); }, []);
}, []);
const handleEditAccount = account => () => { const handleEditAccount = account => () => {
openDialog('account-form', { action: 'edit', id: account.id }); openDialog('account-form', { action: 'edit', id: account.id });
@@ -88,94 +63,75 @@ function AccountsDataTable({
onClick={() => onDeleteAccount(account)} /> onClick={() => onDeleteAccount(account)} />
</Menu> </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 => return (<Icon icon={`arrow-${arrowDirection}`} />);
handleBooleanChange(value => { },
if (value) { className: 'normal',
addBulkActionAccount(account.id); },
} else { {
removeBulkActionAccount(account.id); id: 'balance',
} Header: 'Balance',
}); Cell: ({ cell }) => {
const account = cell.row.original;
const {balance} = account;
const columns = [ return ('undefined' !== typeof balance) ?
{ (<span>{ balance.amount }</span>) :
field: '', (<span>--</span>);
headerText: '', },
template: account => (
<Checkbox onChange={handleClickCheckboxBulk(account)} /> // canResize: false,
),
customAttributes: { class: 'checkbox-row' }
}, },
{ {
field: 'name', id: 'actions',
headerText: 'Account Name', Header: '',
customAttributes: { class: 'account-name' } Cell: ({ cell }) => (
},
{
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'} />
);
},
customAttributes: { class: 'account-normal' }
},
{
field: 'balance',
headerText: 'Balance',
template: (column, data) => {
return <span>$10,000</span>;
}
},
{
headerText: '',
template: account => (
<Popover <Popover
content={actionMenuList(account)} content={actionMenuList(cell.row.original)}
position={Position.RIGHT_BOTTOM} position={Position.RIGHT_BOTTOM}>
>
<Button icon={<Icon icon='ellipsis-h' />} /> <Button icon={<Icon icon='ellipsis-h' />} />
</Popover> </Popover>
) ),
className: 'actions',
width: 50,
// canResize: false
} }
]; ], []);
const handleDatatableFetchData = useCallback(() => {
onFetchData && onFetchData();
}, []);
const dataStateChange = state => {};
return ( return (
<LoadingIndicator loading={fetchHook.pending} spinnerSize={30}> <LoadingIndicator loading={accountsLoading} spinnerSize={30}>
<GridComponent <DataTable
allowSorting={true} columns={columns}
allowGrouping={true} data={accounts}
dataSource={{ result: accounts, count: 12 }} onFetchData={handleDatatableFetchData}
dataStateChange={dataStateChange} manualSortBy={true} />
>
<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>
); );
} }

View File

@@ -1,4 +1,4 @@
import React from 'react'; import React, {useEffect} from 'react';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
@@ -15,10 +15,14 @@ import { Link } from 'react-router-dom';
import { compose } from 'utils'; import { compose } from 'utils';
import AccountsConnect from 'connectors/Accounts.connector'; import AccountsConnect from 'connectors/Accounts.connector';
import DashboardConnect from 'connectors/Dashboard.connector'; import DashboardConnect from 'connectors/Dashboard.connector';
import {useUpdateEffect} from 'hooks';
function AccountsViewsTabs({ function AccountsViewsTabs({
views, views,
setTopbarEditView, setTopbarEditView,
customViewChanged,
addAccountsTableQueries,
onViewChanged,
}) { }) {
const history = useHistory(); const history = useHistory();
const { custom_view_id: customViewId } = useParams(); const { custom_view_id: customViewId } = useParams();
@@ -30,6 +34,22 @@ function AccountsViewsTabs({
const handleViewLinkClick = () => { const handleViewLinkClick = () => {
setTopbarEditView(customViewId); 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 tabs = views.map(view => {
const baseUrl = '/dashboard/accounts'; const baseUrl = '/dashboard/accounts';
const link = ( const link = (
@@ -38,7 +58,9 @@ function AccountsViewsTabs({
onClick={handleViewLinkClick} onClick={handleViewLinkClick}
>{view.name}</Link> >{view.name}</Link>
); );
return <Tab id={`custom_view_${view.id}`} title={link} />; return <Tab
id={`custom_view_${view.id}`}
title={link} />;
}); });
return ( return (
<Navbar className='navbar--dashboard-views'> <Navbar className='navbar--dashboard-views'>
@@ -49,7 +71,9 @@ function AccountsViewsTabs({
selectedTabId={`custom_view_${customViewId}`} selectedTabId={`custom_view_${customViewId}`}
className='tabs--dashboard-views' 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} {tabs}
<Button <Button

View File

@@ -1,10 +1,41 @@
import React from 'react'; import React, {useEffect} from 'react';
import { useTable, useExpanded, usePagination } from 'react-table' 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({ export default function DataTable({
columns, columns,
data, data,
loading, loading,
onFetchData,
onSelectedRowsChange,
manualSortBy = 'false'
}) { }) {
const { const {
getTableProps, getTableProps,
@@ -20,8 +51,10 @@ export default function DataTable({
nextPage, nextPage,
previousPage, previousPage,
setPageSize, setPageSize,
selectedFlatRows,
// Get the state from the instance // Get the state from the instance
state: { pageIndex, pageSize }, state: { pageIndex, pageSize, sortBy, selectedRowIds },
} = useTable( } = useTable(
{ {
columns, columns,
@@ -33,59 +66,102 @@ export default function DataTable({
// pageCount. // pageCount.
// pageCount: controlledPageCount, // pageCount: controlledPageCount,
getSubRows: row => row.children, getSubRows: row => row.children,
manualSortBy
}, },
useSortBy,
useExpanded, useExpanded,
useRowSelect,
usePagination, 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 ( return (
<div className={'bigcapital-datatable'}> <div className={'bigcapital-datatable'}>
<table {...getTableProps()}> <div {...getTableProps()} className="table">
<thead> <div className="thead">
{headerGroups.map(headerGroup => ( {headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}> <div {...headerGroup.getHeaderGroupProps()} className="tr">
{headerGroup.headers.map(column => ( {headerGroup.headers.map(column => (
<th {...column.getHeaderProps({ <div {...column.getHeaderProps({
className: column.className || '', className: classnames(column.className || '', 'th'),
})}> })}>
<div {...column.getSortByToggleProps()}>
{column.render('Header')} {column.render('Header')}
<span> <span>
{column.isSorted {column.isSorted
? column.isSortedDesc ? column.isSortedDesc
? ' 🔽' ? (<Icon icon="sort-down" />)
: ' 🔼' : (<Icon icon="sort-up" />)
: ''} : ''}
</span> </span>
</th> </div>
{column.canResize && (
<div
{...column.getResizerProps()}
className={`resizer ${
column.isResizing ? 'isResizing' : ''
}`}>
<div class="inner-resizer" />
</div>
)}
</div>
))} ))}
</tr> </div>
))} ))}
</thead> </div>
<tbody {...getTableBodyProps()}> <div {...getTableBodyProps()} className="tbody">
{page.map((row, i) => { {page.map((row, i) => {
prepareRow(row) prepareRow(row)
return ( return (
<tr {...row.getRowProps()}> <div {...row.getRowProps()} className="tr">
{row.cells.map((cell) => { {row.cells.map((cell) => {
return <td {...cell.getCellProps({ return <div {...cell.getCellProps({
className: cell.column.className || '', className: classnames(cell.column.className || '', 'td'),
})}>{ cell.render('Cell') }</td> })}>{ cell.render('Cell') }</div>
})} })}
</tr> </div>
) )
})} })}
<tr> </div>
{loading ? ( </div>
// 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>
) )
} }

View File

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

View File

@@ -1,4 +1,4 @@
import React from 'react'; import React, {useState, useEffect, useMemo} from 'react';
import { Spinner } from '@blueprintjs/core'; import { Spinner } from '@blueprintjs/core';
export default function LoadingIndicator({ export default function LoadingIndicator({
@@ -6,15 +6,33 @@ export default function LoadingIndicator({
spinnerSize = 40, spinnerSize = 40,
children children
}) { }) {
return ( const [rendered, setRendered] = useState(false);
<>
{loading ? ( useEffect(() => {
if (!loading) { setRendered(true); }
}, [loading]);
const componentStyle = useMemo(() => {
return {display: !loading ? 'block' : 'none'};
}, [loading]);
const loadingComponent = useMemo(() => (
<div class='dashboard__loading-indicator'> <div class='dashboard__loading-indicator'>
<Spinner size={spinnerSize} value={null} /> <Spinner size={spinnerSize} value={null} />
</div> </div>
) : ( ), [spinnerSize]);
children
)} const renderComponent = useMemo(() => (
<div style={componentStyle}>{ children }</div>
), [children, componentStyle]);
const maybeRenderComponent = rendered && renderComponent;
const maybeRenderLoadingSpinner = loading && loadingComponent;
return (
<>
{ maybeRenderLoadingSpinner }
{ maybeRenderComponent }
</> </>
); );
} }

View File

@@ -1,11 +1,11 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import {useParams} from 'react-router-dom';
import t from 'store/types'; import t from 'store/types';
import { import {
fetchAccountTypes, fetchAccountTypes,
fetchAccountsList, fetchAccountsList,
deleteAccount, deleteAccount,
inactiveAccount, inactiveAccount,
fetchAccountsTable,
} from 'store/accounts/accounts.actions'; } from 'store/accounts/accounts.actions';
import { import {
getAccountsItems, getAccountsItems,
@@ -17,23 +17,32 @@ import {
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
views: getResourceViews(state, 'accounts'), views: getResourceViews(state, 'accounts'),
accounts: getAccountsItems(state, state.accounts.currentViewId), accounts: getAccountsItems(state, state.accounts.currentViewId),
tableQuery: state.accounts.tableQuery,
accountsLoading: state.accounts.loading,
}); });
const mapActionsToProps = (dispatch) => ({ const mapActionsToProps = (dispatch) => ({
fetchAccounts: (query) => dispatch(fetchAccountsList({ query })), fetchAccounts: (query) => dispatch(fetchAccountsList({ query })),
fetchAccountTypes: () => dispatch(fetchAccountTypes()), fetchAccountTypes: () => dispatch(fetchAccountTypes()),
deleteAccount: (id) => dispatch(deleteAccount({ id })), requestDeleteAccount: (id) => dispatch(deleteAccount({ id })),
inactiveAccount: (id) => dispatch(inactiveAccount({ id })), requestInactiveAccount: (id) => dispatch(inactiveAccount({ id })),
addBulkActionAccount: (id) => dispatch({
type: t.ACCOUNT_BULK_ACTION_ADD, account_id: id
}),
removeBulkActionAccount: (id) => dispatch({
type: t.ACCOUNT_BULK_ACTION_REMOVE, account_id: id,
}),
changeCurrentView: (id) => dispatch({ changeCurrentView: (id) => dispatch({
type: t.ACCOUNTS_SET_CURRENT_VIEW, type: t.ACCOUNTS_SET_CURRENT_VIEW,
currentViewId: parseInt(id, 10), currentViewId: parseInt(id, 10),
}), }),
setAccountsTableQuery: (key, value) => dispatch({
type: 'ACCOUNTS_TABLE_QUERY_SET', key, value,
}),
addAccountsTableQueries: (queries) => dispatch({
type: 'ACCOUNTS_TABLE_QUERIES_ADD', queries,
}),
fetchAccountsTable: (query = {}) => dispatch(fetchAccountsTable({ query: { ...query } })),
setSelectedRowsAccounts: (ids) => dispatch({
type: t.ACCOUNTS_SELECTED_ROWS_SET, ids,
}),
}); });
export default connect(mapStateToProps, mapActionsToProps); export default connect(mapStateToProps, mapActionsToProps);

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useCallback } from 'react';
import { import {
Route, Route,
Switch, Switch,
@@ -22,11 +22,12 @@ import { compose } from 'utils';
function AccountsChart({ function AccountsChart({
changePageTitle, changePageTitle,
fetchAccounts, fetchAccounts,
deleteAccount, requestDeleteAccount,
inactiveAccount, requestInactiveAccount,
fetchResourceViews, fetchResourceViews,
fetchResourceFields, fetchResourceFields,
getResourceFields, getResourceFields,
fetchAccountsTable,
}) { }) {
const [state, setState] = useState({ const [state, setState] = useState({
deleteAlertActive: false, deleteAlertActive: false,
@@ -34,8 +35,8 @@ function AccountsChart({
inactiveAlertActive: false, inactiveAlertActive: false,
targetAccount: {}, targetAccount: {},
}); });
const [deleteAccount, setDeleteAccount] = useState(false);
const [filterConditions, setFilterConditions] = useState([]); const [inactiveAccount, setInactiveAccount] = useState(false);
const fetchHook = useAsync(async () => { const fetchHook = useAsync(async () => {
await Promise.all([ await Promise.all([
@@ -44,57 +45,56 @@ function AccountsChart({
]); ]);
}); });
// Fetch accounts list according to the given custom view id.
const fetchAccountsHook = useAsync(async () => {
await Promise.all([
fetchAccountsTable(),
]);
}, false);
useEffect(() => { useEffect(() => {
changePageTitle('Chart of Accounts'); changePageTitle('Chart of Accounts');
}, []); }, []);
/** // Handle click and cancel/confirm account delete
* Handle click and cancel/confirm account delete const handleDeleteAccount = (account) => { setDeleteAccount(account); };
*/
const handleDeleteAccount = (account) => {
setState({
deleteAlertActive: true,
deleteAccount: account,
});
};
const handleCancelAccountDelete = () => { // handle cancel delete account alert.
setState({ deleteAlertActive: false }); const handleCancelAccountDelete = () => { setDeleteAccount(false); };
};
const handleConfirmAccountDelete = () => { // Handle confirm account delete
const { targetAccount: account } = state; const handleConfirmAccountDelete = useCallback(() => {
deleteAccount(account.id).then(() => { requestDeleteAccount(deleteAccount.id).then(() => {
setState({ deleteAlertActive: false }); setDeleteAccount(false);
fetchAccounts(); fetchAccountsHook.execute();
AppToaster.show({ message: 'the_account_has_been_deleted' }); AppToaster.show({ message: 'the_account_has_been_deleted' });
}); });
}; }, [deleteAccount]);
/** // Handle cancel/confirm account inactive.
* Handle cancel/confirm account inactive. const handleInactiveAccount = useCallback((account) => {
*/ setInactiveAccount(account);
const handleInactiveAccount = (account) => { }, []);
setState({ inactiveAlertActive: true, targetAccount: account });
};
const handleCancelInactiveAccount = () => { // Handle cancel inactive account alert.
setState({ inactiveAlertActive: false }); const handleCancelInactiveAccount = useCallback(() => {
}; setInactiveAccount(false);
}, []);
const handleConfirmAccountActive = () => { // Handle confirm account activation.
const { targetAccount: account } = state; const handleConfirmAccountActive = useCallback(() => {
inactiveAccount(account.id).then(() => { requestInactiveAccount(inactiveAccount.id).then(() => {
setState({ inactiveAlertActive: true }); setInactiveAccount(false);
fetchAccounts(); fetchAccountsTable();
AppToaster.show({ message: 'the_account_has_been_inactivated' }); AppToaster.show({ message: 'the_account_has_been_inactivated' });
}); });
}; }, [inactiveAccount]);
/** /**
* Handle cancel/confirm account restore. * Handle cancel/confirm account restore.
*/ */
const handleCancelAccountRestore = () => { const handleCancelAccountRestore = () => {
setState({ restoreAlertActive: false });
}; };
const handleEditAccount = (account) => { const handleEditAccount = (account) => {
@@ -108,16 +108,22 @@ function AccountsChart({
const handleConfirmAccountRestore = (account) => { const handleConfirmAccountRestore = (account) => {
}; };
const handleDeleteBulkAccounts = (accounts) => { const handleDeleteBulkAccounts = (accounts) => {
}; };
const handleFilterChange = (conditions) => { setFilterConditions(conditions); };
const handleSelectedRowsChange = (accounts) => {
console.log(accounts);
};
const handleFilterChanged = useCallback(() => { fetchAccountsHook.execute(); }, []);
const handleViewChanged = useCallback(() => { fetchAccountsHook.execute(); }, []);
const handleFetchData = useCallback(() => { fetchAccountsHook.execute(); }, []);
return ( return (
<DashboardInsider loading={fetchHook.pending} name={'accounts-chart'}> <DashboardInsider loading={fetchHook.pending} name={'accounts-chart'}>
<DashboardActionsBar <DashboardActionsBar
onFilterChange={handleFilterChange} /> onFilterChanged={handleFilterChanged} />
<DashboardPageContent> <DashboardPageContent>
<Switch> <Switch>
<Route <Route
@@ -126,14 +132,17 @@ function AccountsChart({
'/dashboard/accounts/:custom_view_id/custom_view', '/dashboard/accounts/:custom_view_id/custom_view',
'/dashboard/accounts' '/dashboard/accounts'
]}> ]}>
<AccountsViewsTabs onDeleteBulkAccounts={handleDeleteBulkAccounts} /> <AccountsViewsTabs
onViewChanged={handleViewChanged}
onDeleteBulkAccounts={handleDeleteBulkAccounts} />
<AccountsDataTable <AccountsDataTable
filterConditions={filterConditions} onSelectedRowsChange={handleSelectedRowsChange}
onDeleteAccount={handleDeleteAccount} onDeleteAccount={handleDeleteAccount}
onInactiveAccount={handleInactiveAccount} onInactiveAccount={handleInactiveAccount}
onRestoreAccount={handleRestoreAccount} onRestoreAccount={handleRestoreAccount}
onEditAccount={handleEditAccount} /> onEditAccount={handleEditAccount}
onFetchData={handleFetchData} />
</Route> </Route>
</Switch> </Switch>
@@ -142,7 +151,7 @@ function AccountsChart({
confirmButtonText="Move to Trash" confirmButtonText="Move to Trash"
icon="trash" icon="trash"
intent={Intent.DANGER} intent={Intent.DANGER}
isOpen={state.deleteAlertActive} isOpen={deleteAccount}
onCancel={handleCancelAccountDelete} onCancel={handleCancelAccountDelete}
onConfirm={handleConfirmAccountDelete}> onConfirm={handleConfirmAccountDelete}>
<p> <p>
@@ -156,7 +165,7 @@ function AccountsChart({
confirmButtonText="Inactivate" confirmButtonText="Inactivate"
icon="trash" icon="trash"
intent={Intent.WARNING} intent={Intent.WARNING}
isOpen={state.inactiveAlertActive} isOpen={inactiveAccount}
onCancel={handleCancelInactiveAccount} onCancel={handleCancelInactiveAccount}
onConfirm={handleConfirmAccountActive}> onConfirm={handleConfirmAccountActive}>
<p> <p>
@@ -164,20 +173,6 @@ function AccountsChart({
but it will become private to you. but it will become private to you.
</p> </p>
</Alert> </Alert>
<Alert
cancelButtonText="Cancel"
confirmButtonText="Move to Trash"
icon="trash"
intent={Intent.DANGER}
isOpen={state.restoreAlertActive}
onCancel={handleCancelAccountRestore}
onConfirm={handleConfirmAccountRestore}>
<p>
Are you sure you want to move <b>filename</b> to Trash? You will be able to restore it later,
but it will become private to you.
</p>
</Alert>
</DashboardPageContent> </DashboardPageContent>
</DashboardInsider> </DashboardInsider>
); );

View File

@@ -21,6 +21,7 @@ import AppToaster from 'components/AppToaster';
import DialogConnect from 'connectors/Dialog.connector'; import DialogConnect from 'connectors/Dialog.connector';
import DialogReduxConnect from 'components/DialogReduxConnect'; import DialogReduxConnect from 'components/DialogReduxConnect';
import AccountFormDialogConnect from 'connectors/AccountFormDialog.connector'; import AccountFormDialogConnect from 'connectors/AccountFormDialog.connector';
import AccountsConnect from 'connectors/Accounts.connector';
function AccountFormDialog({ function AccountFormDialog({
name, name,
@@ -33,7 +34,8 @@ function AccountFormDialog({
closeDialog, closeDialog,
submitAccount, submitAccount,
fetchAccount, fetchAccount,
editAccount editAccount,
fetchAccountsTable,
}) { }) {
const intl = useIntl(); const intl = useIntl();
const accountFormValidationSchema = Yup.object().shape({ const accountFormValidationSchema = Yup.object().shape({
@@ -64,6 +66,7 @@ function AccountFormDialog({
AppToaster.show({ AppToaster.show({
message: 'the_account_has_been_edited' message: 'the_account_has_been_edited'
}); });
refetchAccounts.execute();
}); });
} else { } else {
submitAccount({ form: { ...omit(values, exclude) } }).then(response => { submitAccount({ form: { ...omit(values, exclude) } }).then(response => {
@@ -71,6 +74,7 @@ function AccountFormDialog({
AppToaster.show({ AppToaster.show({
message: 'the_account_has_been_submit' message: 'the_account_has_been_submit'
}); });
refetchAccounts.execute();
}); });
} }
} }
@@ -131,15 +135,19 @@ function AccountFormDialog({
await Promise.all([ await Promise.all([
fetchAccounts(), fetchAccounts(),
fetchAccountTypes(), fetchAccountTypes(),
// Fetch the target in case edit mode. // Fetch the target in case edit mode.
...(payload.action === 'edit' ? [fetchAccount(payload.id)] : []) ...(payload.action === 'edit' ? [fetchAccount(payload.id)] : [])
]); ]);
}); }, false);
const onDialogOpening = async () => { const refetchAccounts = useAsync(async () => {
fetchHook.execute(); await Promise.all([
}; fetchAccountsTable(),
]);
}, false);
// Fetch requests on dialog opening.
const onDialogOpening = async () => { fetchHook.execute(); };
const onChangeAccountType = accountType => { const onChangeAccountType = accountType => {
setState({ ...state, selectedAccountType: accountType.name }); setState({ ...state, selectedAccountType: accountType.name });
@@ -294,6 +302,7 @@ function AccountFormDialog({
export default compose( export default compose(
AccountFormDialogConnect, AccountFormDialogConnect,
AccountsConnect,
DialogReduxConnect, DialogReduxConnect,
DialogConnect DialogConnect
)(AccountFormDialog); )(AccountFormDialog);

View File

@@ -1,6 +1,27 @@
import {useRef, useEffect} from 'react';
import useAsync from './async'; import useAsync from './async';
// import use from 'async'; // import use from 'async';
/**
* A custom useEffect hook that only triggers on updates, not on initial mount
* Idea stolen from: https://stackoverflow.com/a/55075818/1526448
* @param {Function} effect
* @param {Array<any>} dependencies
*/
export function useUpdateEffect(effect, dependencies = []) {
const isInitialMount = useRef(true);
useEffect(() => {
if (isInitialMount.current) {
isInitialMount.current = false;
} else {
effect();
}
}, dependencies);
}
export default { export default {
useAsync, useAsync,
useUpdateEffect,
} }

View File

@@ -78,5 +78,13 @@ export default {
"pen": { "pen": {
path: ['M493.26 56.26l-37.51-37.51C443.25 6.25 426.87 0 410.49 0s-32.76 6.25-45.25 18.74l-74.49 74.49L256 127.98 12.85 371.12.15 485.34C-1.45 499.72 9.88 512 23.95 512c.89 0 1.79-.05 2.69-.15l114.14-12.61L384.02 256l34.74-34.74 74.49-74.49c25-25 25-65.52.01-90.51zM118.75 453.39l-67.58 7.46 7.53-67.69 231.24-231.24 31.02-31.02 60.14 60.14-31.02 31.02-231.33 231.33zm340.56-340.57l-44.28 44.28-60.13-60.14 44.28-44.28c4.08-4.08 8.84-4.69 11.31-4.69s7.24.61 11.31 4.69l37.51 37.51c6.24 6.25 6.24 16.4 0 22.63z'], path: ['M493.26 56.26l-37.51-37.51C443.25 6.25 426.87 0 410.49 0s-32.76 6.25-45.25 18.74l-74.49 74.49L256 127.98 12.85 371.12.15 485.34C-1.45 499.72 9.88 512 23.95 512c.89 0 1.79-.05 2.69-.15l114.14-12.61L384.02 256l34.74-34.74 74.49-74.49c25-25 25-65.52.01-90.51zM118.75 453.39l-67.58 7.46 7.53-67.69 231.24-231.24 31.02-31.02 60.14 60.14-31.02 31.02-231.33 231.33zm340.56-340.57l-44.28 44.28-60.13-60.14 44.28-44.28c4.08-4.08 8.84-4.69 11.31-4.69s7.24.61 11.31 4.69l37.51 37.51c6.24 6.25 6.24 16.4 0 22.63z'],
viewBox: '0 0 512 512', viewBox: '0 0 512 512',
},
"sort-up": {
path: ['M279 224H41c-21.4 0-32.1-25.9-17-41L143 64c9.4-9.4 24.6-9.4 33.9 0l119 119c15.2 15.1 4.5 41-16.9 41z'],
viewBox: '0 0 320 512'
},
"sort-down": {
path: ['M41 288h238c21.4 0 32.1 25.9 17 41L177 448c-9.4 9.4-24.6 9.4-33.9 0L24 329c-15.1-15.1-4.4-41 17-41z'],
viewBox: '0 0 320 512'
} }
} }

View File

@@ -12,9 +12,7 @@ export const fetchAccountTypes = () => {
}); });
resolve(response); resolve(response);
}) })
.catch(error => { .catch(error => { reject(error); });
reject(error);
});
}); });
}; };
@@ -40,6 +38,38 @@ export const fetchAccountsList = ({ query } = {}) => {
}); });
}; };
export const fetchAccountsTable = ({ query } = {}) => {
return (dispatch, getState) =>
new Promise((resolve, reject) => {
const pageQuery = getState().accounts.tableQuery;
dispatch({
type: t.ACCOUNTS_TABLE_LOADING,
loading: true,
});
ApiService.get('accounts', { params: { ...pageQuery, ...query } })
.then(response => {
dispatch({
type: t.ACCOUNTS_PAGE_SET,
accounts: response.data.accounts,
customViewId: response.data.customViewId
});
dispatch({
type: t.ACCOUNTS_ITEMS_SET,
accounts: response.data.accounts
});
dispatch({
type: t.ACCOUNTS_TABLE_LOADING,
loading: false,
});
resolve(response);
})
.catch(error => {
reject(error);
});
});
};
export const fetchAccountsDataTable = ({ query }) => { export const fetchAccountsDataTable = ({ query }) => {
return dispatch => return dispatch =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
@@ -109,7 +139,12 @@ export const inactiveAccount = ({ id }) => {
}; };
export const deleteAccount = ({ id }) => { export const deleteAccount = ({ id }) => {
return dispatch => ApiService.delete(`accounts/${id}`); return dispatch => new Promise((resolve, reject) => {
ApiService.delete(`accounts/${id}`).then((response) => {
dispatch({ type: t.ACCOUNT_DELETE, id });
resolve(response);
}).catch(error => { reject(error); });
});
}; };
export const deleteBulkAccounts = ({ ids }) => {}; export const deleteBulkAccounts = ({ ids }) => {};

View File

@@ -1,5 +1,6 @@
import t from 'store/types'; import t from 'store/types';
import { createReducer } from '@reduxjs/toolkit'; import { createReducer, combineReducers } from '@reduxjs/toolkit';
import { createTableQueryReducers } from 'store/queryReducers';
const initialState = { const initialState = {
items: {}, items: {},
@@ -9,10 +10,11 @@ const initialState = {
accountFormErrors: [], accountFormErrors: [],
datatableQuery: {}, datatableQuery: {},
currentViewId: -1, currentViewId: -1,
bulkActions: {}, selectedRows: [],
loading: false,
}; };
export default createReducer(initialState, { const accountsReducer = createReducer(initialState, {
[t.ACCOUNTS_ITEMS_SET]: (state, action) => { [t.ACCOUNTS_ITEMS_SET]: (state, action) => {
const _items = {}; const _items = {};
@@ -44,19 +46,32 @@ export default createReducer(initialState, {
state.accountsById[action.account.id] = action.account; state.accountsById[action.account.id] = action.account;
}, },
[t.ACCOUNT_BULK_ACTION_ADD]: (state, action) => { [t.ACCOUNT_DELETE]: (state, action) => {
state.bulkActions[action.account_id] = true; if (typeof state.items[action.id] !== 'undefined'){
delete state.items[action.id];
}
}, },
[t.ACCOUNT_BULK_ACTION_REMOVE]: (state, action) => { [t.ACCOUNTS_SELECTED_ROWS_SET]: (state, action) => {
delete state.bulkActions[action.account_id]; state.selectedRows.push(...action.ids);
}, },
[t.ACCOUNTS_SET_CURRENT_VIEW]: (state, action) => { [t.ACCOUNTS_SET_CURRENT_VIEW]: (state, action) => {
state.currentViewId = action.currentViewId; state.currentViewId = action.currentViewId;
} },
[t.ACCOUNTS_TABLE_LOADING]: (state, action) => {
state.loading = action.loading;
},
}); });
export default createTableQueryReducers('accounts', accountsReducer);
export const getAccountById = (state, id) => { export const getAccountById = (state, id) => {
return state.accounts.accountsById[id]; return state.accounts.accountsById[id];
}; };
// export default {
// // ...accountsReducer,
// // testReducer,
// }

View File

@@ -5,11 +5,16 @@ export default {
ACCOUNTS_PAGE_SET: 'ACCOUNTS_PAGE_SET', ACCOUNTS_PAGE_SET: 'ACCOUNTS_PAGE_SET',
ACCOUNTS_ITEMS_SET: 'ACCOUNTS_ITEMS_SET', ACCOUNTS_ITEMS_SET: 'ACCOUNTS_ITEMS_SET',
ACCOUNT_SET: 'ACCOUNT_SET', ACCOUNT_SET: 'ACCOUNT_SET',
ACCOUNT_DELETE: 'ACCOUNT_DELETE',
ACCOUNT_FORM_ERRORS: 'ACCOUNT_FORM_ERRORS', ACCOUNT_FORM_ERRORS: 'ACCOUNT_FORM_ERRORS',
CLEAR_ACCOUNT_FORM_ERRORS: 'CLEAR_ACCOUNT_FORM_ERRORS', CLEAR_ACCOUNT_FORM_ERRORS: 'CLEAR_ACCOUNT_FORM_ERRORS',
ACCOUNT_BULK_ACTION_ADD: 'ACCOUNT_BULK_ACTION_ADD', ACCOUNTS_SELECTED_ROWS_SET: 'ACCOUNTS_SELECTED_ROWS_SET',
ACCOUNT_BULK_ACTION_REMOVE: 'ACCOUNT_BULK_ACTION_REMOVE',
ACCOUNTS_SET_CURRENT_VIEW: 'ACCOUNTS_SET_CURRENT_VIEW', ACCOUNTS_SET_CURRENT_VIEW: 'ACCOUNTS_SET_CURRENT_VIEW',
ACCOUNTS_TABLE_QUERY_SET: 'ACCOUNTS_TABLE_QUERY_SET',
ACCOUNTS_TABLE_QUERIES_SET: 'ACCOUNTS_TABLE_QUERIES_SET',
ACCOUNTS_TABLE_LOADING: 'ACCOUNTS_TABLE_LOADING',
}; };

View File

@@ -0,0 +1,28 @@
export const createTableQueryReducers =
(resourceName = '', reducer) =>
(state, action) => {
const RESOURCE_NAME = resourceName.toUpperCase();
switch (action.type) {
case `${RESOURCE_NAME}_TABLE_QUERY_SET`:
return {
...state,
tableQuery: {
...state.tableQuery,
[state.key]: state.value,
}
};
case `${RESOURCE_NAME}_TABLE_QUERIES_ADD`:
return {
...state,
tableQuery: {
...state.tableQuery,
...action.queries
},
};
default:
return reducer(state, action);
}
}

View File

@@ -1,49 +1,107 @@
.bigcapital-datatable{ .bigcapital-datatable{
table { display: block;
// max-width: 100%;
overflow-x: auto;
overflow-y: hidden;
.table {
text-align: left; text-align: left;
border-spacing: 0; border-spacing: 0;
width: 100%; min-width: 100%;
display: block;
// width: 100%;
thead{ .thead{
th{ overflow-y: auto;
height: 48px; overflow-x: hidden;
padding: 0.5rem 1.5rem;
.th{
padding: 1rem 1.5rem;
background: #F8FAFA; background: #F8FAFA;
font-size: 14px; font-size: 14px;
color: #666; color: #666;
font-weight: 500; font-weight: 500;
border-bottom: 1px solid rgb(224, 224, 224); border-bottom: 1px solid rgb(224, 224, 224);
} }
} }
tr { .tr {
:last-child { display: flex;
td { flex: 1 0 auto;
&:last-child {
.td {
border-bottom: 0; border-bottom: 0;
} }
} }
} }
th, .th,
td { .td {
box-sizing: border-box;
flex: 0 0 auto;
justify-content: flex-start;
align-items: flex-start;
display: flex;
margin: 0; margin: 0;
padding: 0.5rem; padding: 0.5rem;
:last-child { :last-child {
border-right: 0; border-right: 0;
} }
.bp3-control{
margin-bottom: 0;
} }
tbody{ .resizer {
display: inline-block;
background: transparent;
width: 10px;
height: 100%;
position: absolute;
right: 0;
top: 0;
transform: translateX(50%);
z-index: 1;
touch-action:none;
.#{$ns}-button--action{ &.isResizing {
// background: red;
}
.inner-resizer{
height: 100%;
width: 1px;
background: #ececec;
margin: 0 auto;
}
&.isResizing .inner-resizer{
background: #1183DA;
}
}
}
.tbody{
overflow-y: scroll;
overflow-x: hidden;
.tr .td{
border-bottom: 1px solid #E0E2E2;
}
.td.actions .#{$ns}-button{
background: #E6EFFB; background: #E6EFFB;
border: 0; border: 0;
box-shadow: 0 0 0; box-shadow: 0 0 0;
padding: 5px 15px; padding: 5px 15px;
border-radius: 2px;
} }
} }
} }

View File

@@ -1,25 +1,12 @@
.dashboard__insider--accounts-chart{ .dashboard__insider--accounts-chart{
.e-grid{ .bigcapital-datatable{
.e-gridheader{ .normal{
border-top: 0;
}
.account-normal{
.#{$ns}-icon{ .#{$ns}-icon{
color: #666; color: #666;
padding-left: 15px; padding-left: 15px;
} }
} }
.checkbox-row{
width: 0;
.#{$ns}-control{
margin: 0;
}
}
} }
} }

View File

@@ -196,8 +196,7 @@
width: calc(100% - 220px); width: calc(100% - 220px);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-left: 220px;
} }
&__insider{ &__insider{

View File

@@ -10,6 +10,8 @@ $sidebar-popover-submenu-bg: rgb(1, 20, 62);
background: $sidebar-background; background: $sidebar-background;
color: $sidebar-text-color; color: $sidebar-text-color;
width: $sidebar-width; width: $sidebar-width;
position: fixed;
height: 100%;
&__inner{ &__inner{
overflow-y: scroll; overflow-y: scroll;