mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 05:40:31 +00:00
feat: optimize accounts performance.
feat: optimize alerts architecture. feat: optimize datatable architecture. feat: optimize datatable style.
This commit is contained in:
@@ -80,7 +80,7 @@
|
||||
"react-query": "^2.4.6",
|
||||
"react-redux": "^7.1.3",
|
||||
"react-router-breadcrumbs-hoc": "^3.2.10",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scroll-sync": "^0.7.1",
|
||||
"react-scrollbars-custom": "^4.0.21",
|
||||
"react-sortablejs": "^2.0.11",
|
||||
@@ -89,7 +89,7 @@
|
||||
"react-table-sticky": "^1.1.2",
|
||||
"react-transition-group": "^4.4.1",
|
||||
"react-use": "^13.26.1",
|
||||
"react-window": "^1.8.5",
|
||||
"react-virtualized": "^9.22.3",
|
||||
"redux": "^4.0.5",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
|
||||
@@ -12,7 +12,8 @@ const CLASSES = {
|
||||
|
||||
DASHBOARD_CONTENT: 'dashboard-content',
|
||||
DASHBOARD_CONTENT_PREFERENCES: 'dashboard-content--preferences',
|
||||
|
||||
DASHBOARD_CONTENT_PANE: 'Pane2',
|
||||
|
||||
PAGE_FORM: 'page-form',
|
||||
PAGE_FORM_HEADER: 'page-form__header',
|
||||
PAGE_FORM_HEADER_PRIMARY: 'page-form__primary-section',
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function AccountsSelectList({
|
||||
onAccountSelected,
|
||||
disabled = false,
|
||||
popoverFill = false,
|
||||
filterByRootTypes = [],
|
||||
filterByParentTypes = [],
|
||||
filterByTypes = [],
|
||||
filterByNormal,
|
||||
buttonProps = {}
|
||||
@@ -23,23 +23,23 @@ export default function AccountsSelectList({
|
||||
const filteredAccounts = useMemo(() => {
|
||||
let filteredAccounts = [...accounts];
|
||||
|
||||
if (!isEmpty(filterByRootTypes)) {
|
||||
if (!isEmpty(filterByParentTypes)) {
|
||||
filteredAccounts = filteredAccounts.filter(
|
||||
(account) => filterByRootTypes.indexOf(account.type.root_type) !== -1,
|
||||
(account) => filterByParentTypes.indexOf(account.account_parent_type) !== -1,
|
||||
);
|
||||
}
|
||||
if (!isEmpty(filterByTypes)) {
|
||||
filteredAccounts = filteredAccounts.filter(
|
||||
(account) => filterByTypes.indexOf(account.type.key) !== -1,
|
||||
(account) => filterByTypes.indexOf(account.account_type) !== -1,
|
||||
);
|
||||
}
|
||||
if (!isEmpty(filterByNormal)) {
|
||||
filteredAccounts = filteredAccounts.filter(
|
||||
(account) => filterByTypes.indexOf(account.type.normal) === filterByNormal,
|
||||
(account) => filterByTypes.indexOf(account.account_normal) === filterByNormal,
|
||||
);
|
||||
}
|
||||
return filteredAccounts;
|
||||
}, [accounts, filterByRootTypes, filterByTypes, filterByNormal]);
|
||||
}, [accounts, filterByParentTypes, filterByTypes, filterByNormal]);
|
||||
|
||||
// Find initial account object to set it as default account in initial render.
|
||||
const initialAccount = useMemo(
|
||||
|
||||
@@ -119,7 +119,7 @@ export default function AccountsSuggestField({
|
||||
inputProps={{ placeholder: defaultSelectText }}
|
||||
resetOnClose={true}
|
||||
fill={true}
|
||||
popoverProps={{ minimal: true }}
|
||||
popoverProps={{ minimal: true, boundary: 'window' }}
|
||||
inputValueRenderer={handleInputValueRenderer}
|
||||
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
|
||||
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Position, Toaster, Intent } from "@blueprintjs/core";
|
||||
|
||||
const AppToaster = Toaster.create({
|
||||
position: Position.TOP,
|
||||
position: Position.RIGHT_BOTTOM,
|
||||
intent: Intent.WARNING,
|
||||
});
|
||||
|
||||
|
||||
@@ -94,8 +94,7 @@ export default function ContactsSuggestField({
|
||||
selectedItem={selecetedContact}
|
||||
inputProps={{ placeholder: defaultTextSelect }}
|
||||
resetOnClose={true}
|
||||
// fill={true}
|
||||
popoverProps={{ minimal: true }}
|
||||
popoverProps={{ minimal: true, boundary: 'window' }}
|
||||
inputValueRenderer={handleInputValueRenderer}
|
||||
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
|
||||
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
|
||||
|
||||
@@ -9,7 +9,7 @@ function DashboardSplitPane({
|
||||
sidebarExpended,
|
||||
children
|
||||
}) {
|
||||
const initialSize = 190;
|
||||
const initialSize = 200;
|
||||
|
||||
const [defaultSize, setDefaultSize] = useState(
|
||||
parseInt(localStorage.getItem('dashboard-size'), 10) || initialSize,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
MenuDivider,
|
||||
Button,
|
||||
Popover,
|
||||
Position,
|
||||
} from '@blueprintjs/core';
|
||||
import { FormattedMessage as T } from 'react-intl';
|
||||
|
||||
@@ -50,7 +51,7 @@ function DashboardTopbarUser({ requestLogout, user }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover content={userAvatarDropMenu}>
|
||||
<Popover content={userAvatarDropMenu} position={Position.BOTTOM}>
|
||||
<Button>
|
||||
<div className="user-text">
|
||||
{firstLettersArgs(user.first_name, user.last_name)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import {
|
||||
useTable,
|
||||
useExpanded,
|
||||
@@ -9,104 +9,101 @@ import {
|
||||
useFlexLayout,
|
||||
useAsyncDebounce,
|
||||
} from 'react-table';
|
||||
import { Checkbox, Spinner, ContextMenu } from '@blueprintjs/core';
|
||||
import classnames from 'classnames';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import { useSticky } from 'react-table-sticky';
|
||||
import { ScrollSync, ScrollSyncPane } from 'react-scroll-sync';
|
||||
|
||||
import { useUpdateEffect } from 'hooks';
|
||||
import { If, Pagination, Choose } from 'components';
|
||||
|
||||
import { ConditionalWrapper, saveInvoke } from 'utils';
|
||||
import { saveInvoke } from 'utils';
|
||||
|
||||
import 'style/components/DataTable/DataTable.scss';
|
||||
|
||||
const IndeterminateCheckbox = React.forwardRef(
|
||||
({ indeterminate, ...rest }, ref) => {
|
||||
return <Checkbox indeterminate={indeterminate} {...rest} />;
|
||||
},
|
||||
);
|
||||
import TableNoResultsRow from './Datatable/TableNoResultsRow';
|
||||
import TableLoadingRow from './Datatable/TableLoading';
|
||||
import TableHeader from './Datatable/TableHeader';
|
||||
import TablePage from './Datatable/TablePage';
|
||||
import TableRow from './Datatable/TableRow';
|
||||
import TableRows from './Datatable/TableRows';
|
||||
import TableCell from './Datatable/TableCell';
|
||||
import TableTBody from './Datatable/TableTBody';
|
||||
import TableContext from './Datatable/TableContext';
|
||||
import TablePagination from './Datatable/TablePagination';
|
||||
import TableWrapper from './Datatable/TableWrapper';
|
||||
|
||||
export default function DataTable({
|
||||
columns,
|
||||
data,
|
||||
import TableIndeterminateCheckboxRow from './Datatable/TableIndeterminateCheckboxRow';
|
||||
import TableIndeterminateCheckboxHeader from './Datatable/TableIndeterminateCheckboxHeader';
|
||||
|
||||
loading,
|
||||
onFetchData,
|
||||
|
||||
onSelectedRowsChange,
|
||||
manualSortBy = false,
|
||||
manualPagination = true,
|
||||
selectionColumn = false,
|
||||
expandSubRows = true,
|
||||
className,
|
||||
noResults = 'This report does not contain any data.',
|
||||
expanded = {},
|
||||
rowClassNames,
|
||||
sticky = false,
|
||||
virtualizedRows = false,
|
||||
fixedSizeHeight = 100,
|
||||
fixedItemSize = 30,
|
||||
payload,
|
||||
expandable = false,
|
||||
expandToggleColumn = 2,
|
||||
noInitialFetch = false,
|
||||
spinnerProps = { size: 30 },
|
||||
|
||||
pagination = false,
|
||||
pagesCount: controlledPageCount,
|
||||
|
||||
// Pagination props.
|
||||
initialPageIndex = 0,
|
||||
initialPageSize = 10,
|
||||
rowContextMenu,
|
||||
|
||||
expandColumnSpace = 1.5,
|
||||
|
||||
updateDebounceTime = 200,
|
||||
selectionColumnWidth = 42,
|
||||
|
||||
// Read this document to know why! https://bit.ly/2Uw9SEc
|
||||
autoResetPage = true,
|
||||
autoResetExpanded = true,
|
||||
autoResetGroupBy = true,
|
||||
autoResetSelectedRows = true,
|
||||
autoResetSortBy = true,
|
||||
autoResetFilters = true,
|
||||
autoResetRowState = true,
|
||||
}) {
|
||||
/**
|
||||
* Datatable component.
|
||||
*/
|
||||
export default function DataTable(props) {
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
prepareRow,
|
||||
page,
|
||||
rows,
|
||||
selectedFlatRows,
|
||||
getToggleAllRowsExpandedProps,
|
||||
isAllRowsExpanded,
|
||||
totalColumnsWidth,
|
||||
columns,
|
||||
data,
|
||||
|
||||
// page,
|
||||
pageCount,
|
||||
canPreviousPage,
|
||||
canNextPage,
|
||||
gotoPage,
|
||||
previousPage,
|
||||
nextPage,
|
||||
setPageSize,
|
||||
onFetchData,
|
||||
|
||||
// Get the state from the instance
|
||||
state: { pageIndex, pageSize, sortBy, selectedRowIds },
|
||||
} = useTable(
|
||||
onSelectedRowsChange,
|
||||
manualSortBy = false,
|
||||
manualPagination = true,
|
||||
selectionColumn = false,
|
||||
expandSubRows = true,
|
||||
expanded = {},
|
||||
rowClassNames,
|
||||
payload,
|
||||
expandable = false,
|
||||
expandToggleColumn = 2,
|
||||
noInitialFetch = false,
|
||||
|
||||
pagesCount: controlledPageCount,
|
||||
|
||||
// Pagination props.
|
||||
initialPageIndex = 0,
|
||||
initialPageSize = 10,
|
||||
rowContextMenu,
|
||||
expandColumnSpace = 1.5,
|
||||
|
||||
updateDebounceTime = 200,
|
||||
selectionColumnWidth = 42,
|
||||
|
||||
autoResetPage,
|
||||
autoResetExpanded,
|
||||
autoResetGroupBy,
|
||||
autoResetSelectedRows,
|
||||
autoResetSortBy,
|
||||
autoResetFilters,
|
||||
autoResetRowState,
|
||||
|
||||
// Components
|
||||
TableHeaderRenderer,
|
||||
TablePageRenderer,
|
||||
TableWrapperRenderer,
|
||||
TableTBodyRenderer,
|
||||
TablePaginationRenderer,
|
||||
} = props;
|
||||
|
||||
const selectionColumnObj = {
|
||||
id: 'selection',
|
||||
disableResizing: true,
|
||||
minWidth: selectionColumnWidth,
|
||||
width: selectionColumnWidth,
|
||||
maxWidth: selectionColumnWidth,
|
||||
// The header can use the table's getToggleAllRowsSelectedProps method
|
||||
// to render a checkbox
|
||||
Header: TableIndeterminateCheckboxHeader,
|
||||
// The cell can use the individual row's getToggleRowSelectedProps method
|
||||
// to the render a checkbox
|
||||
Cell: TableIndeterminateCheckboxRow,
|
||||
className: 'selection',
|
||||
...(typeof selectionColumn === 'object' ? selectionColumn : {}),
|
||||
};
|
||||
|
||||
const table = useTable(
|
||||
{
|
||||
columns,
|
||||
data,
|
||||
initialState: {
|
||||
pageIndex: initialPageIndex,
|
||||
pageSize: initialPageSize,
|
||||
expanded
|
||||
expanded,
|
||||
},
|
||||
manualPagination,
|
||||
pageCount: controlledPageCount,
|
||||
@@ -133,49 +130,23 @@ export default function DataTable({
|
||||
(hooks) => {
|
||||
hooks.visibleColumns.push((columns) => [
|
||||
// Let's make a column for selection
|
||||
...(selectionColumn
|
||||
? [
|
||||
{
|
||||
id: 'selection',
|
||||
disableResizing: true,
|
||||
minWidth: selectionColumnWidth,
|
||||
width: selectionColumnWidth,
|
||||
maxWidth: selectionColumnWidth,
|
||||
// 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>
|
||||
),
|
||||
className: 'selection',
|
||||
...(typeof selectionColumn === 'object' ? selectionColumn : {}),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(selectionColumn ? [selectionColumnObj] : []),
|
||||
...columns,
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
selectedFlatRows,
|
||||
state: { pageIndex, pageSize, sortBy, selectedRowIds },
|
||||
} = table;
|
||||
|
||||
const isInitialMount = useRef(noInitialFetch);
|
||||
const onFetchDataDebounced = useAsyncDebounce(
|
||||
(...args) => {
|
||||
saveInvoke(onFetchData, ...args);
|
||||
},
|
||||
updateDebounceTime,
|
||||
);
|
||||
|
||||
const onFetchDataDebounced = useAsyncDebounce((...args) => {
|
||||
saveInvoke(onFetchData, ...args);
|
||||
}, updateDebounceTime);
|
||||
|
||||
// When these table states change, fetch new data!
|
||||
useEffect(() => {
|
||||
if (isInitialMount.current) {
|
||||
@@ -189,285 +160,42 @@ export default function DataTable({
|
||||
saveInvoke(onSelectedRowsChange, selectedFlatRows);
|
||||
}, [selectedRowIds, onSelectedRowsChange]);
|
||||
|
||||
// Renders table cell.
|
||||
const RenderCell = useCallback(
|
||||
({ row, cell, column, index }) => (
|
||||
<ConditionalWrapper
|
||||
condition={expandToggleColumn === index && expandable}
|
||||
wrapper={(children) => (
|
||||
<div
|
||||
style={{
|
||||
'padding-left': `${row.depth * expandColumnSpace}rem`,
|
||||
}}
|
||||
className={'expend-padding'}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{
|
||||
// Use the row.canExpand and row.getToggleRowExpandedProps prop getter
|
||||
// to build the toggle for expanding a row
|
||||
}
|
||||
<If
|
||||
condition={
|
||||
row.canExpand && expandable && index === expandToggleColumn
|
||||
}
|
||||
>
|
||||
<span
|
||||
{...row.getToggleRowExpandedProps({ className: 'expand-toggle' })}
|
||||
>
|
||||
<span
|
||||
className={classnames({
|
||||
'arrow-down': row.isExpanded,
|
||||
'arrow-right': !row.isExpanded,
|
||||
})}
|
||||
/>
|
||||
</span>
|
||||
</If>
|
||||
|
||||
<ConditionalWrapper
|
||||
condition={cell.column.textOverview}
|
||||
wrapper={(children) => (
|
||||
<span class="text-overview">{ children }</span>
|
||||
)}>
|
||||
{cell.render('Cell')}
|
||||
</ConditionalWrapper>
|
||||
</ConditionalWrapper>
|
||||
),
|
||||
[expandable, expandToggleColumn, expandColumnSpace],
|
||||
);
|
||||
|
||||
// Handle rendering row context menu.
|
||||
const handleRowContextMenu = useMemo(
|
||||
() => (cell, row) => (e) => {
|
||||
if (typeof rowContextMenu === 'function') {
|
||||
e.preventDefault();
|
||||
const tr = e.currentTarget.closest('.tr');
|
||||
tr.classList.add('is-context-menu-active');
|
||||
|
||||
const DropdownEl = rowContextMenu(cell, row);
|
||||
|
||||
ContextMenu.show(
|
||||
DropdownEl,
|
||||
{ left: e.clientX, top: e.clientY },
|
||||
() => {
|
||||
tr.classList.remove('is-context-menu-active');
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
[rowContextMenu],
|
||||
);
|
||||
|
||||
// Renders table row.
|
||||
const RenderRow = useCallback(
|
||||
({ style = {}, row }) => {
|
||||
prepareRow(row);
|
||||
const rowClasses = rowClassNames && rowClassNames(row);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...row.getRowProps({
|
||||
className: classnames(
|
||||
'tr',
|
||||
{
|
||||
'is-expanded': row.isExpanded && row.canExpand,
|
||||
},
|
||||
rowClasses,
|
||||
),
|
||||
style,
|
||||
})}
|
||||
>
|
||||
{row.cells.map((cell, i) => {
|
||||
const index = i + 1;
|
||||
return (
|
||||
<div
|
||||
{...cell.getCellProps({
|
||||
className: classnames(
|
||||
cell.column.className,
|
||||
'td',
|
||||
{
|
||||
'is-text-overview': cell.column.textOverview,
|
||||
}
|
||||
),
|
||||
})}
|
||||
onContextMenu={handleRowContextMenu(cell, row)}
|
||||
>
|
||||
{RenderCell({ cell, row, index })}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[prepareRow, rowClassNames, RenderCell, handleRowContextMenu],
|
||||
);
|
||||
|
||||
// Renders virtualize circle table rows.
|
||||
const RenderVirtualizedRows = useCallback(
|
||||
({ index, style }) => {
|
||||
const row = rows[index];
|
||||
return RenderRow({ row, style });
|
||||
},
|
||||
[RenderRow, rows],
|
||||
);
|
||||
// Renders page with multi-rows.
|
||||
const RenderPage = useCallback(
|
||||
({ style, index } = {}) => {
|
||||
return page.map((row, index) => RenderRow({ row }));
|
||||
},
|
||||
[RenderRow, page],
|
||||
);
|
||||
|
||||
// Renders fixed size tbody.
|
||||
const RenderTBody = useCallback(() => {
|
||||
return virtualizedRows ? (
|
||||
<FixedSizeList
|
||||
height={fixedSizeHeight}
|
||||
itemCount={rows.length}
|
||||
itemSize={fixedItemSize}
|
||||
>
|
||||
{RenderVirtualizedRows}
|
||||
</FixedSizeList>
|
||||
) : (
|
||||
RenderPage()
|
||||
);
|
||||
}, [
|
||||
fixedSizeHeight,
|
||||
rows,
|
||||
fixedItemSize,
|
||||
virtualizedRows,
|
||||
RenderVirtualizedRows,
|
||||
RenderPage,
|
||||
]);
|
||||
|
||||
const handlePageChange = useCallback(
|
||||
(currentPage) => {
|
||||
gotoPage(currentPage - 1);
|
||||
},
|
||||
[gotoPage],
|
||||
);
|
||||
|
||||
const handlePageSizeChange = useCallback(
|
||||
(pageSize, currentPage) => {
|
||||
gotoPage(0);
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
[gotoPage, setPageSize],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('bigcapital-datatable', className, {
|
||||
'has-sticky': sticky,
|
||||
'has-pagination': pagination,
|
||||
'is-expandable': expandable,
|
||||
'is-loading': loading,
|
||||
'has-virtualized-rows': virtualizedRows,
|
||||
})}
|
||||
>
|
||||
<ScrollSync>
|
||||
<div
|
||||
{...getTableProps({ style: { minWidth: 'none' } })}
|
||||
className="table"
|
||||
>
|
||||
<ScrollSyncPane>
|
||||
<div className="thead">
|
||||
{headerGroups.map((headerGroup) => (
|
||||
<div {...headerGroup.getHeaderGroupProps()} className="tr">
|
||||
{headerGroup.headers.map((column, index) => (
|
||||
<div
|
||||
{...column.getHeaderProps({
|
||||
className: classnames(column.className || '', 'th'),
|
||||
})}
|
||||
>
|
||||
<If
|
||||
condition={
|
||||
expandable && index + 1 === expandToggleColumn
|
||||
}
|
||||
>
|
||||
<span
|
||||
{...getToggleAllRowsExpandedProps()}
|
||||
className="expand-toggle"
|
||||
>
|
||||
<span
|
||||
className={classnames({
|
||||
'arrow-down': isAllRowsExpanded,
|
||||
'arrow-right': !isAllRowsExpanded,
|
||||
})}
|
||||
/>
|
||||
</span>
|
||||
</If>
|
||||
<TableContext.Provider value={{ table, props }}>
|
||||
<TableWrapperRenderer>
|
||||
<TableHeaderRenderer />
|
||||
|
||||
<div {...column.getSortByToggleProps()}>
|
||||
{column.render('Header')}
|
||||
<TableTBodyRenderer>
|
||||
<TablePageRenderer />
|
||||
</TableTBodyRenderer>
|
||||
</TableWrapperRenderer>
|
||||
|
||||
<If condition={column.isSorted}>
|
||||
<span
|
||||
className={classnames(
|
||||
{
|
||||
'sort-icon--desc': column.isSortedDesc,
|
||||
'sort-icon--asc': !column.isSortedDesc,
|
||||
},
|
||||
'sort-icon',
|
||||
)}
|
||||
></span>
|
||||
</If>
|
||||
</div>
|
||||
|
||||
{column.canResize && (
|
||||
<div
|
||||
{...column.getResizerProps()}
|
||||
className={`resizer ${
|
||||
column.isResizing ? 'isResizing' : ''
|
||||
}`}
|
||||
>
|
||||
<div class="inner-resizer" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollSyncPane>
|
||||
|
||||
<ScrollSyncPane>
|
||||
<div {...getTableBodyProps()} className="tbody">
|
||||
<div class="tbody-inner" style={{ minWidth: totalColumnsWidth }}>
|
||||
<Choose>
|
||||
<Choose.When condition={loading}>
|
||||
<div class="loading">
|
||||
<Spinner {...spinnerProps} />
|
||||
</div>
|
||||
</Choose.When>
|
||||
|
||||
<Choose.Otherwise>
|
||||
{RenderTBody()}
|
||||
|
||||
<If condition={page.length === 0}>
|
||||
<div className={'tr no-results'}>
|
||||
<div class="td">{noResults}</div>
|
||||
</div>
|
||||
</If>
|
||||
</Choose.Otherwise>
|
||||
</Choose>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollSyncPane>
|
||||
</div>
|
||||
</ScrollSync>
|
||||
|
||||
<If condition={pagination && !loading}>
|
||||
<Pagination
|
||||
initialPage={pageIndex + 1}
|
||||
total={pageSize * pageCount}
|
||||
size={pageSize}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
/>
|
||||
</If>
|
||||
</div>
|
||||
<TablePaginationRenderer />
|
||||
</TableContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
DataTable.defaultProps = {
|
||||
pagination: false,
|
||||
spinnerProps: { size: 30 },
|
||||
|
||||
autoResetPage: true,
|
||||
autoResetExpanded: true,
|
||||
autoResetGroupBy: true,
|
||||
autoResetSelectedRows: true,
|
||||
autoResetSortBy: true,
|
||||
autoResetFilters: true,
|
||||
autoResetRowState: true,
|
||||
|
||||
TableHeaderRenderer: TableHeader,
|
||||
TableLoadingRenderer: TableLoadingRow,
|
||||
TablePageRenderer: TablePage,
|
||||
TableRowsRenderer: TableRows,
|
||||
TableRowRenderer: TableRow,
|
||||
TableCellRenderer: TableCell,
|
||||
TableWrapperRenderer: TableWrapper,
|
||||
TableTBodyRenderer: TableTBody,
|
||||
TablePaginationRenderer: TablePagination,
|
||||
TableNoResultsRowRenderer: TableNoResultsRow,
|
||||
};
|
||||
11
client/src/components/Datatable/TableBody.js
Normal file
11
client/src/components/Datatable/TableBody.js
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
|
||||
export default function TableBody({}) {
|
||||
return (
|
||||
<ScrollSyncPane>
|
||||
<div {...getTableBodyProps()} className="tbody">
|
||||
<div class="tbody-inner" style={{ minWidth: totalColumnsWidth }}></div>
|
||||
</div>
|
||||
</ScrollSyncPane>
|
||||
);
|
||||
}
|
||||
52
client/src/components/Datatable/TableCell.js
Normal file
52
client/src/components/Datatable/TableCell.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import React, { useContext } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { If } from 'components';
|
||||
import { ConditionalWrapper } from 'utils';
|
||||
import TableContext from './TableContext';
|
||||
|
||||
/**
|
||||
* Tabl cell.
|
||||
*/
|
||||
export default function TableCell({ cell, row, index }) {
|
||||
const {
|
||||
props: { expandToggleColumn, expandable }
|
||||
} = useContext(TableContext);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...cell.getCellProps({
|
||||
className: classNames(cell.column.className, 'td', {
|
||||
'is-text-overview': cell.column.textOverview,
|
||||
}),
|
||||
})}
|
||||
>
|
||||
{
|
||||
// Use the row.canExpand and row.getToggleRowExpandedProps prop getter
|
||||
// to build the toggle for expanding a row
|
||||
}
|
||||
<If
|
||||
condition={
|
||||
cell.row.canExpand && expandable && index === expandToggleColumn
|
||||
}
|
||||
>
|
||||
<span
|
||||
{...row.getToggleRowExpandedProps({ className: 'expand-toggle' })}
|
||||
>
|
||||
<span
|
||||
className={classNames({
|
||||
'arrow-down': row.isExpanded,
|
||||
'arrow-right': !row.isExpanded,
|
||||
})}
|
||||
/>
|
||||
</span>
|
||||
</If>
|
||||
|
||||
<ConditionalWrapper
|
||||
condition={cell.column.textOverview}
|
||||
wrapper={(children) => <span class="text-overview">{children}</span>}
|
||||
>
|
||||
{cell.render('Cell')}
|
||||
</ConditionalWrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
client/src/components/Datatable/TableContext.js
Normal file
3
client/src/components/Datatable/TableContext.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export default createContext();
|
||||
18
client/src/components/Datatable/TableFastCell.js
Normal file
18
client/src/components/Datatable/TableFastCell.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import React, { memo } from 'react';
|
||||
import TableCell from './TableCell';
|
||||
|
||||
function TableFastCell({ cell, row, index }) {
|
||||
return <TableCell cell={cell} row={row} index={index} />;
|
||||
}
|
||||
|
||||
export default memo(TableFastCell, (prevProps, nextProps) => {
|
||||
if (
|
||||
prevProps.cell.value === nextProps.cell.value &&
|
||||
prevProps.cell.maxWidth === nextProps.cell.maxWidth &&
|
||||
prevProps.cell.width === nextProps.cell.width
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
82
client/src/components/Datatable/TableHeader.js
Normal file
82
client/src/components/Datatable/TableHeader.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import React, { useContext } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { ScrollSyncPane } from 'react-scroll-sync';
|
||||
import { If } from 'components';
|
||||
import TableContext from './TableContext';
|
||||
|
||||
function TableHeaderGroup({ headerGroup }) {
|
||||
const {
|
||||
table: { getToggleAllRowsExpandedProps, isAllRowsExpanded },
|
||||
props: { expandable, expandToggleColumn },
|
||||
} = useContext(TableContext);
|
||||
|
||||
return (
|
||||
<div {...headerGroup.getHeaderGroupProps()} className="tr">
|
||||
{headerGroup.headers.map((column, index) => (
|
||||
<div
|
||||
{...column.getHeaderProps({
|
||||
className: classNames(column.className || '', 'th'),
|
||||
})}
|
||||
>
|
||||
<If condition={expandable && index + 1 === expandToggleColumn}>
|
||||
<span
|
||||
{...getToggleAllRowsExpandedProps()}
|
||||
className="expand-toggle"
|
||||
>
|
||||
<span
|
||||
className={classNames({
|
||||
'arrow-down': isAllRowsExpanded,
|
||||
'arrow-right': !isAllRowsExpanded,
|
||||
})}
|
||||
/>
|
||||
</span>
|
||||
</If>
|
||||
|
||||
<div {...column.getSortByToggleProps()}>
|
||||
{column.render('Header')}
|
||||
|
||||
<If condition={column.isSorted}>
|
||||
<span
|
||||
className={classNames(
|
||||
{
|
||||
'sort-icon--desc': column.isSortedDesc,
|
||||
'sort-icon--asc': !column.isSortedDesc,
|
||||
},
|
||||
'sort-icon',
|
||||
)}
|
||||
></span>
|
||||
</If>
|
||||
</div>
|
||||
|
||||
{column.canResize && (
|
||||
<div
|
||||
{...column.getResizerProps()}
|
||||
className={`resizer ${column.isResizing ? 'isResizing' : ''}`}
|
||||
>
|
||||
<div class="inner-resizer" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Table header.
|
||||
*/
|
||||
export default function TableHeader() {
|
||||
const {
|
||||
table: { headerGroups },
|
||||
} = useContext(TableContext);
|
||||
|
||||
return (
|
||||
<ScrollSyncPane>
|
||||
<div className="thead">
|
||||
{headerGroups.map((headerGroup) => (
|
||||
<TableHeaderGroup headerGroup={headerGroup} />
|
||||
))}
|
||||
</div>
|
||||
</ScrollSyncPane>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Checkbox } from '@blueprintjs/core';
|
||||
|
||||
export default function TableIndeterminateCheckboxHeader({
|
||||
getToggleAllRowsSelectedProps,
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<Checkbox {...getToggleAllRowsSelectedProps()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Checkbox } from '@blueprintjs/core';
|
||||
|
||||
export default function TableIndeterminateCheckboxRow({ row }) {
|
||||
return (
|
||||
<div>
|
||||
<Checkbox {...row.getToggleRowSelectedProps()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
client/src/components/Datatable/TableLoading.js
Normal file
15
client/src/components/Datatable/TableLoading.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { Spinner } from '@blueprintjs/core';
|
||||
|
||||
/**
|
||||
* Table loading component.
|
||||
*/
|
||||
export default function TableLoading({
|
||||
spinnerProps
|
||||
}) {
|
||||
return (
|
||||
<div class="loading">
|
||||
<Spinner {...spinnerProps} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
17
client/src/components/Datatable/TableNoResultsRow.js
Normal file
17
client/src/components/Datatable/TableNoResultsRow.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import React, { useContext } from 'react';
|
||||
import TableContext from './TableContext';
|
||||
|
||||
/**
|
||||
* Table no-results row text.
|
||||
*/
|
||||
export default function TableNoResultsRow() {
|
||||
const {
|
||||
props: { noResults }
|
||||
} = useContext(TableContext);
|
||||
|
||||
return (
|
||||
<div className={'tr no-results'}>
|
||||
<div class="td">{ noResults }</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
26
client/src/components/Datatable/TablePage.js
Normal file
26
client/src/components/Datatable/TablePage.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import React, { useContext } from 'react';
|
||||
import TableContext from './TableContext';
|
||||
|
||||
/**
|
||||
* Table page.
|
||||
*/
|
||||
export default function TablePage() {
|
||||
const {
|
||||
table: { page },
|
||||
props: {
|
||||
spinnerProps,
|
||||
loading,
|
||||
TableRowsRenderer,
|
||||
TableLoadingRenderer,
|
||||
TableNoResultsRow,
|
||||
},
|
||||
} = useContext(TableContext);
|
||||
|
||||
if (loading) {
|
||||
return <TableLoadingRenderer spinnerProps={spinnerProps} />;
|
||||
}
|
||||
if (page.length === 0) {
|
||||
return <TableNoResultsRow />;
|
||||
}
|
||||
return (<TableRowsRenderer />);
|
||||
}
|
||||
45
client/src/components/Datatable/TablePagination.js
Normal file
45
client/src/components/Datatable/TablePagination.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import { If, Pagination } from 'components';
|
||||
import TableContext from './TableContext';
|
||||
|
||||
/**
|
||||
* Table pagination.
|
||||
*/
|
||||
export default function TablePagination({}) {
|
||||
const {
|
||||
table: {
|
||||
gotoPage,
|
||||
setPageSize,
|
||||
pageCount,
|
||||
state: { pageIndex, pageSize },
|
||||
},
|
||||
props: { pagination, loading },
|
||||
} = useContext(TableContext);
|
||||
|
||||
const handlePageChange = useCallback(
|
||||
(currentPage) => {
|
||||
gotoPage(currentPage - 1);
|
||||
},
|
||||
[gotoPage],
|
||||
);
|
||||
|
||||
const handlePageSizeChange = useCallback(
|
||||
(pageSize, currentPage) => {
|
||||
gotoPage(0);
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
[gotoPage, setPageSize],
|
||||
);
|
||||
|
||||
return (
|
||||
<If condition={pagination && !loading}>
|
||||
<Pagination
|
||||
initialPage={pageIndex + 1}
|
||||
total={pageSize * pageCount}
|
||||
size={pageSize}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
/>
|
||||
</If>
|
||||
);
|
||||
}
|
||||
51
client/src/components/Datatable/TableRow.js
Normal file
51
client/src/components/Datatable/TableRow.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import React, { useContext } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { ContextMenu } from '@blueprintjs/core';
|
||||
|
||||
import TableContext from './TableContext';
|
||||
import { saveInvoke } from 'utils';
|
||||
|
||||
/**
|
||||
* Table row.
|
||||
*/
|
||||
export default function TableRow({ row, className, style }) {
|
||||
const {
|
||||
props: { TableCellRenderer, rowContextMenu, rowClassNames },
|
||||
} = useContext(TableContext);
|
||||
|
||||
// Handle rendering row context menu.
|
||||
const handleRowContextMenu = (row) => (e) => {
|
||||
if (typeof rowContextMenu === 'function') {
|
||||
e.preventDefault();
|
||||
const tr = e.currentTarget.closest('.tr');
|
||||
tr.classList.add('is-context-menu-active');
|
||||
|
||||
const DropdownEl = rowContextMenu({ row });
|
||||
|
||||
ContextMenu.show(DropdownEl, { left: e.clientX, top: e.clientY }, () => {
|
||||
tr.classList.remove('is-context-menu-active');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
{...row.getRowProps({
|
||||
className: classNames(
|
||||
'tr',
|
||||
{
|
||||
'is-expanded': row.isExpanded && row.canExpand,
|
||||
},
|
||||
saveInvoke(rowClassNames, row),
|
||||
className,
|
||||
),
|
||||
style,
|
||||
onContextMenu: handleRowContextMenu(row)
|
||||
})}
|
||||
>
|
||||
{row.cells.map((cell, index) => (
|
||||
<TableCellRenderer cell={cell} row={row} index={index + 1} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
17
client/src/components/Datatable/TableRows.js
Normal file
17
client/src/components/Datatable/TableRows.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import React, { useContext } from "react";
|
||||
import TableContext from "./TableContext";
|
||||
|
||||
/**
|
||||
* Table rows.
|
||||
*/
|
||||
export default function TableRows() {
|
||||
const {
|
||||
table: { prepareRow, page },
|
||||
props: { TableRowRenderer, TableCellRenderer },
|
||||
} = useContext(TableContext);
|
||||
|
||||
return page.map((row) => {
|
||||
prepareRow(row);
|
||||
return <TableRowRenderer row={row} TableCellRenderer={TableCellRenderer} />;
|
||||
});
|
||||
}
|
||||
21
client/src/components/Datatable/TableTBody.js
Normal file
21
client/src/components/Datatable/TableTBody.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { ScrollSyncPane } from 'react-scroll-sync';
|
||||
import TableContext from './TableContext';
|
||||
|
||||
export default function TableTBody({
|
||||
children
|
||||
}) {
|
||||
const {
|
||||
table: { getTableBodyProps }
|
||||
} = useContext(TableContext);
|
||||
|
||||
return (
|
||||
<ScrollSyncPane>
|
||||
<div {...getTableBodyProps()} className="tbody">
|
||||
<div class="tbody-inner">
|
||||
{ children }
|
||||
</div>
|
||||
</div>
|
||||
</ScrollSyncPane>
|
||||
);
|
||||
}
|
||||
66
client/src/components/Datatable/TableVirtualizedRows.js
Normal file
66
client/src/components/Datatable/TableVirtualizedRows.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { WindowScroller, AutoSizer, List } from 'react-virtualized';
|
||||
import { CLASSES } from 'common/classes';
|
||||
import TableContext from './TableContext';
|
||||
|
||||
function TableVirtualizedListRow({
|
||||
index,
|
||||
isScrolling,
|
||||
isVisible,
|
||||
key,
|
||||
style,
|
||||
}) {
|
||||
const {
|
||||
table: { page, prepareRow },
|
||||
props: { TableRowRenderer },
|
||||
} = useContext(TableContext);
|
||||
|
||||
const row = page[index];
|
||||
prepareRow(row);
|
||||
|
||||
return <TableRowRenderer row={row} style={style} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Table virtualized list rows.
|
||||
*/
|
||||
export default function TableVirtualizedListRows() {
|
||||
const {
|
||||
table: { page },
|
||||
props: { vListrowHeight, vListOverscanRowCount }
|
||||
} = useContext(TableContext);
|
||||
|
||||
// Dashboard content pane.
|
||||
const dashboardContentPane = document.querySelector(
|
||||
`.${CLASSES.DASHBOARD_CONTENT_PANE}`,
|
||||
);
|
||||
return (
|
||||
<WindowScroller scrollElement={dashboardContentPane}>
|
||||
{({ height, isScrolling, registerChild, onChildScroll, scrollTop }) => (
|
||||
<div className={'WindowScrollerWrapper'}>
|
||||
<AutoSizer disableHeight>
|
||||
{({ width }) => (
|
||||
<div ref={registerChild}>
|
||||
<List
|
||||
autoHeight={true}
|
||||
className={'List'}
|
||||
height={height}
|
||||
isScrolling={isScrolling}
|
||||
onScroll={onChildScroll}
|
||||
overscanRowCount={vListOverscanRowCount}
|
||||
rowCount={page.length}
|
||||
rowHeight={vListrowHeight}
|
||||
rowRenderer={({ ...args }) => {
|
||||
return <TableVirtualizedListRow {...args} />;
|
||||
}}
|
||||
scrollTop={scrollTop}
|
||||
width={width}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
)}
|
||||
</WindowScroller>
|
||||
);
|
||||
}
|
||||
35
client/src/components/Datatable/TableWrapper.js
Normal file
35
client/src/components/Datatable/TableWrapper.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import React, { useContext } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { ScrollSync } from 'react-scroll-sync';
|
||||
import TableContext from './TableContext';
|
||||
|
||||
/**
|
||||
* Table wrapper.
|
||||
*/
|
||||
export default function TableWrapper({ children }) {
|
||||
const {
|
||||
table: { getTableProps },
|
||||
props: { sticky, pagination, loading, expandable, virtualizedRows, className },
|
||||
} = useContext(TableContext);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('bigcapital-datatable', className, {
|
||||
'has-sticky': sticky,
|
||||
'has-pagination': pagination,
|
||||
'is-expandable': expandable,
|
||||
'is-loading': loading,
|
||||
'has-virtualized-rows': virtualizedRows,
|
||||
})}
|
||||
>
|
||||
<ScrollSync>
|
||||
<div
|
||||
{...getTableProps({ style: { minWidth: 'none' } })}
|
||||
className="table"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</ScrollSync>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -100,7 +100,7 @@ export default function ItemsSuggestField({
|
||||
inputProps={{ placeholder: defautlSelectText }}
|
||||
resetOnClose={true}
|
||||
fill={true}
|
||||
popoverProps={{ minimal: true }}
|
||||
popoverProps={{ minimal: true, boundary: 'window' }}
|
||||
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
|
||||
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
|
||||
})}
|
||||
|
||||
@@ -45,6 +45,9 @@ import PageFormBigNumber from './PageFormBigNumber';
|
||||
import AccountsMultiSelect from './AccountsMultiSelect';
|
||||
import CustomersMultiSelect from './CustomersMultiSelect';
|
||||
|
||||
|
||||
import TableFastCell from './Datatable/TableFastCell';
|
||||
|
||||
const Hint = FieldHint;
|
||||
|
||||
export {
|
||||
@@ -93,5 +96,7 @@ export {
|
||||
PageFormBigNumber,
|
||||
AccountsMultiSelect,
|
||||
DataTableEditable,
|
||||
CustomersMultiSelect
|
||||
CustomersMultiSelect,
|
||||
|
||||
TableFastCell,
|
||||
};
|
||||
|
||||
@@ -7,9 +7,6 @@ export default [
|
||||
disabled: false,
|
||||
href: '/homepage',
|
||||
},
|
||||
{
|
||||
spacer: 1,
|
||||
},
|
||||
{
|
||||
text: 'Sales & inventory',
|
||||
label: true,
|
||||
@@ -91,10 +88,7 @@ export default [
|
||||
],
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
text: <T id={'financial'} />,
|
||||
text: <T id={'accounting'} />,
|
||||
label: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -137,7 +137,8 @@ function ManualJournalsDataTable({
|
||||
accessor: (r) => (
|
||||
<Tooltip
|
||||
content={<AmountPopoverContent journalEntries={r.entries} />}
|
||||
position={Position.RIGHT_BOTTOM}
|
||||
position={Position.RIGHT_TOP}
|
||||
boundary={'viewport'}
|
||||
>
|
||||
<Money amount={r.amount} currency={'USD'} />
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo, useState, useCallback } from 'react';
|
||||
import React, { memo, useState } from 'react';
|
||||
import Icon from 'components/Icon';
|
||||
import {
|
||||
Button,
|
||||
@@ -22,9 +22,13 @@ import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||
import withResourceDetail from 'containers/Resources/withResourceDetails';
|
||||
import withAccountsTableActions from 'containers/Accounts/withAccountsTableActions';
|
||||
import withAccounts from 'containers/Accounts/withAccounts';
|
||||
import withAlertActions from 'containers/Alert/withAlertActions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
/**
|
||||
* Accounts actions bar.
|
||||
*/
|
||||
function AccountsActionsBar({
|
||||
openDialog,
|
||||
accountsViews,
|
||||
@@ -32,18 +36,18 @@ function AccountsActionsBar({
|
||||
// #withResourceDetail
|
||||
resourceFields,
|
||||
|
||||
// #withAccountsActions
|
||||
// #withAccountsTableActions
|
||||
addAccountsTableQueries,
|
||||
setAccountsBulkAction,
|
||||
|
||||
// #withAccounts
|
||||
accountsTableQuery,
|
||||
accountsSelectedRows,
|
||||
|
||||
// #withAlertActions
|
||||
openAlert,
|
||||
|
||||
selectedRows = [],
|
||||
onFilterChanged,
|
||||
onBulkDelete,
|
||||
onBulkArchive,
|
||||
onBulkActivate,
|
||||
onBulkInactive,
|
||||
}) {
|
||||
const [filterCount, setFilterCount] = useState(
|
||||
accountsTableQuery?.filter_roles?.length || 0,
|
||||
@@ -53,10 +57,7 @@ function AccountsActionsBar({
|
||||
openDialog('account-form', {});
|
||||
};
|
||||
|
||||
const hasSelectedRows = useMemo(() => selectedRows.length > 0, [
|
||||
selectedRows,
|
||||
]);
|
||||
|
||||
// Filter dropdown.
|
||||
const filterDropdown = FilterDropdown({
|
||||
fields: resourceFields,
|
||||
initialConditions: accountsTableQuery.filter_roles,
|
||||
@@ -74,17 +75,17 @@ function AccountsActionsBar({
|
||||
},
|
||||
});
|
||||
|
||||
const handleBulkDelete = useCallback(() => {
|
||||
onBulkDelete && onBulkDelete(selectedRows.map((r) => r.id));
|
||||
}, [onBulkDelete, selectedRows]);
|
||||
const handleBulkDelete = () => {
|
||||
openAlert('accounts-bulk-delete', { accountsIds: accountsSelectedRows });
|
||||
};
|
||||
|
||||
const handelBulkActivate = useCallback(() => {
|
||||
onBulkActivate && onBulkActivate(selectedRows.map((r) => r.id));
|
||||
}, [onBulkActivate, selectedRows]);
|
||||
const handelBulkActivate = () => {
|
||||
openAlert('accounts-bulk-activate', { accountsIds: accountsSelectedRows });
|
||||
};
|
||||
|
||||
const handelBulkInactive = useCallback(() => {
|
||||
onBulkInactive && onBulkInactive(selectedRows.map((r) => r.id));
|
||||
}, [onBulkInactive, selectedRows]);
|
||||
const handelBulkInactive = () => {
|
||||
openAlert('accounts-bulk-inactivate', { accountsIds: accountsSelectedRows });
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
@@ -113,7 +114,7 @@ function AccountsActionsBar({
|
||||
'has-active-filters': filterCount > 0,
|
||||
})}
|
||||
text={
|
||||
filterCount <= 0 ? (
|
||||
(filterCount <= 0) ? (
|
||||
<T id={'filter'} />
|
||||
) : (
|
||||
<T
|
||||
@@ -126,7 +127,7 @@ function AccountsActionsBar({
|
||||
/>
|
||||
</Popover>
|
||||
|
||||
<If condition={hasSelectedRows}>
|
||||
<If condition={accountsSelectedRows.length}>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="play-16" iconSize={16} />}
|
||||
@@ -168,21 +169,30 @@ function AccountsActionsBar({
|
||||
);
|
||||
}
|
||||
|
||||
// Momerize the component.
|
||||
const AccountsActionsBarMemo = memo(AccountsActionsBar);
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
resourceName: 'accounts',
|
||||
});
|
||||
|
||||
const withAccountsActionsBar = connect(mapStateToProps);
|
||||
|
||||
export default compose(
|
||||
const comp = compose(
|
||||
withAccountsActionsBar,
|
||||
withDialogActions,
|
||||
withAccounts(({ accountsViews, accountsTableQuery }) => ({
|
||||
accountsViews,
|
||||
accountsTableQuery,
|
||||
})),
|
||||
withAccounts(
|
||||
({ accountsSelectedRows, accountsViews, accountsTableQuery }) => ({
|
||||
accountsViews,
|
||||
accountsTableQuery,
|
||||
accountsSelectedRows,
|
||||
}),
|
||||
),
|
||||
withResourceDetail(({ resourceFields }) => ({
|
||||
resourceFields,
|
||||
})),
|
||||
withAccountsTableActions,
|
||||
)(AccountsActionsBar);
|
||||
withAlertActions
|
||||
)(AccountsActionsBarMemo);
|
||||
|
||||
export default comp;
|
||||
|
||||
26
client/src/containers/Accounts/AccountsAlerts.js
Normal file
26
client/src/containers/Accounts/AccountsAlerts.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import AccountDeleteAlert from 'containers/Alerts/AccountDeleteAlert';
|
||||
import AccountInactivateAlert from 'containers/Alerts/AccountInactivateAlert';
|
||||
import AccountActivateAlert from 'containers/Alerts/AccountActivateAlert';
|
||||
import AccountBulkDeleteAlert from 'containers/Alerts/AccountBulkDeleteAlert';
|
||||
import AccountBulkInactivateAlert from 'containers/Alerts/AccountBulkInactivateAlert';
|
||||
import AccountBulkActivateAlert from 'containers/Alerts/AccountBulkActivateAlert';
|
||||
|
||||
/**
|
||||
* Accounts alert.
|
||||
*/
|
||||
export default function AccountsAlerts({
|
||||
|
||||
}) {
|
||||
return (
|
||||
<div class="accounts-alerts">
|
||||
<AccountDeleteAlert name={'account-delete'} />
|
||||
<AccountInactivateAlert name={'account-inactivate'} />
|
||||
<AccountActivateAlert name={'account-activate'} />
|
||||
|
||||
<AccountBulkDeleteAlert name={'accounts-bulk-delete'} />
|
||||
<AccountBulkInactivateAlert name={'accounts-bulk-inactivate'} />
|
||||
<AccountBulkActivateAlert name={'accounts-bulk-activate'} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,20 +1,17 @@
|
||||
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import { Alert, Intent } from '@blueprintjs/core';
|
||||
import { useQuery, queryCache } from 'react-query';
|
||||
import { useQuery } from 'react-query';
|
||||
import {
|
||||
FormattedMessage as T,
|
||||
FormattedHTMLMessage,
|
||||
useIntl,
|
||||
} from 'react-intl';
|
||||
|
||||
import AppToaster from 'components/AppToaster';
|
||||
import 'style/pages/Accounts/List.scss';
|
||||
|
||||
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
|
||||
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||
import AccountsViewsTabs from 'containers/Accounts/AccountsViewsTabs';
|
||||
import AccountsDataTable from 'containers/Accounts/AccountsDataTable';
|
||||
import DashboardActionsBar from 'containers/Accounts/AccountsActionsBar';
|
||||
import AccountsViewPage from 'containers/Accounts/AccountsViewPage';
|
||||
import AccountsActionsBar from 'containers/Accounts/AccountsActionsBar';
|
||||
import AccountsAlerts from './AccountsAlerts';
|
||||
|
||||
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
|
||||
import withResourceActions from 'containers/Resources/withResourcesActions';
|
||||
@@ -25,8 +22,6 @@ import withAccounts from 'containers/Accounts/withAccounts';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
import 'style/pages/Accounts/List.scss';
|
||||
|
||||
/**
|
||||
* Accounts chart list.
|
||||
*/
|
||||
@@ -34,11 +29,6 @@ function AccountsChart({
|
||||
// #withDashboardActions
|
||||
changePageTitle,
|
||||
|
||||
// #withAccountsActions
|
||||
requestDeleteAccount,
|
||||
requestInactiveAccount,
|
||||
requestActivateAccount,
|
||||
|
||||
// #withViewsActions
|
||||
requestFetchResourceViews,
|
||||
|
||||
@@ -47,35 +37,23 @@ function AccountsChart({
|
||||
|
||||
// #withAccountsTableActions
|
||||
requestFetchAccountsTable,
|
||||
requestDeleteBulkAccounts,
|
||||
addAccountsTableQueries,
|
||||
requestBulkActivateAccounts,
|
||||
requestBulkInactiveAccounts,
|
||||
|
||||
// #withAccounts
|
||||
accountsTableQuery,
|
||||
}) {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const [deleteAccount, setDeleteAccount] = useState(false);
|
||||
const [inactiveAccount, setInactiveAccount] = useState(false);
|
||||
const [activateAccount, setActivateAccount] = useState(false);
|
||||
const [bulkDelete, setBulkDelete] = useState(false);
|
||||
const [selectedRows, setSelectedRows] = useState([]);
|
||||
const [bulkActivate, setBulkActivate] = useState(false);
|
||||
const [bulkInactiveAccounts, setBulkInactiveAccounts] = useState(false);
|
||||
|
||||
// Fetch accounts resource views and fields.
|
||||
const fetchResourceViews = useQuery(
|
||||
['resource-views', 'accounts'],
|
||||
(key, resourceName) => requestFetchResourceViews(resourceName),
|
||||
);
|
||||
|
||||
// Fetch the accounts resource fields.
|
||||
const fetchResourceFields = useQuery(
|
||||
['resource-fields', 'accounts'],
|
||||
(key, resourceName) => requestFetchResourceFields(resourceName),
|
||||
);
|
||||
|
||||
// Fetch accounts list according to the given custom view id.
|
||||
const fetchAccountsHook = useQuery(
|
||||
['accounts-table', accountsTableQuery],
|
||||
@@ -86,162 +64,10 @@ function AccountsChart({
|
||||
changePageTitle(formatMessage({ id: 'chart_of_accounts' }));
|
||||
}, [changePageTitle, formatMessage]);
|
||||
|
||||
// Handle click and cancel/confirm account delete
|
||||
const handleDeleteAccount = (account) => {
|
||||
setDeleteAccount(account);
|
||||
};
|
||||
|
||||
// handle cancel delete account alert.
|
||||
const handleCancelAccountDelete = useCallback(() => {
|
||||
setDeleteAccount(false);
|
||||
}, []);
|
||||
|
||||
// Handle delete errors in bulk and singular.
|
||||
const handleDeleteErrors = (errors) => {
|
||||
if (errors.find((e) => e.type === 'ACCOUNT.PREDEFINED')) {
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'you_could_not_delete_predefined_accounts',
|
||||
}),
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
}
|
||||
if (errors.find((e) => e.type === 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS')) {
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'cannot_delete_account_has_associated_transactions',
|
||||
}),
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Handle confirm account delete
|
||||
const handleConfirmAccountDelete = useCallback(() => {
|
||||
requestDeleteAccount(deleteAccount.id)
|
||||
.then(() => {
|
||||
setDeleteAccount(false);
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'the_account_has_been_successfully_deleted',
|
||||
}),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
queryCache.invalidateQueries('accounts-table');
|
||||
})
|
||||
.catch((errors) => {
|
||||
setDeleteAccount(false);
|
||||
handleDeleteErrors(errors);
|
||||
});
|
||||
}, [deleteAccount, requestDeleteAccount, formatMessage]);
|
||||
|
||||
// Handle cancel/confirm account inactive.
|
||||
const handleInactiveAccount = useCallback((account) => {
|
||||
setInactiveAccount(account);
|
||||
}, []);
|
||||
|
||||
// Handle cancel inactive account alert.
|
||||
const handleCancelInactiveAccount = useCallback(() => {
|
||||
setInactiveAccount(false);
|
||||
}, []);
|
||||
|
||||
// Handle confirm account activation.
|
||||
|
||||
const handleConfirmAccountActive = useCallback(() => {
|
||||
requestInactiveAccount(inactiveAccount.id)
|
||||
.then(() => {
|
||||
setInactiveAccount(false);
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'the_account_has_been_successfully_inactivated',
|
||||
}),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
queryCache.invalidateQueries('accounts-table');
|
||||
})
|
||||
.catch((error) => {
|
||||
setInactiveAccount(false);
|
||||
});
|
||||
}, [inactiveAccount, requestInactiveAccount, formatMessage]);
|
||||
|
||||
// Handle activate account click.
|
||||
const handleActivateAccount = useCallback((account) => {
|
||||
setActivateAccount(account);
|
||||
});
|
||||
|
||||
// Handle activate account alert cancel.
|
||||
const handleCancelActivateAccount = useCallback(() => {
|
||||
setActivateAccount(false);
|
||||
});
|
||||
|
||||
// Handle activate account confirm.
|
||||
const handleConfirmAccountActivate = useCallback(() => {
|
||||
requestActivateAccount(activateAccount.id)
|
||||
.then(() => {
|
||||
setActivateAccount(false);
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'the_account_has_been_successfully_activated',
|
||||
}),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
queryCache.invalidateQueries('accounts-table');
|
||||
})
|
||||
.catch((error) => {
|
||||
setActivateAccount(false);
|
||||
});
|
||||
}, [activateAccount, requestActivateAccount, formatMessage]);
|
||||
|
||||
const handleRestoreAccount = (account) => {};
|
||||
|
||||
// Handle accounts bulk delete button click.,
|
||||
const handleBulkDelete = useCallback(
|
||||
(accountsIds) => {
|
||||
setBulkDelete(accountsIds);
|
||||
},
|
||||
[setBulkDelete],
|
||||
);
|
||||
|
||||
// Handle confirm accounts bulk delete.
|
||||
const handleConfirmBulkDelete = useCallback(() => {
|
||||
requestDeleteBulkAccounts(bulkDelete)
|
||||
.then(() => {
|
||||
setBulkDelete(false);
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'the_accounts_has_been_successfully_deleted',
|
||||
}),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
queryCache.invalidateQueries('accounts-table');
|
||||
})
|
||||
.catch((errors) => {
|
||||
setBulkDelete(false);
|
||||
handleDeleteErrors(errors);
|
||||
});
|
||||
}, [requestDeleteBulkAccounts, bulkDelete, formatMessage]);
|
||||
|
||||
// Handle cancel accounts bulk delete.
|
||||
const handleCancelBulkDelete = useCallback(() => {
|
||||
setBulkDelete(false);
|
||||
}, []);
|
||||
|
||||
const handleBulkArchive = useCallback((accounts) => {}, []);
|
||||
|
||||
const handleEditAccount = useCallback(() => {}, []);
|
||||
|
||||
// Handle selected rows change.
|
||||
const handleSelectedRowsChange = useCallback(
|
||||
(accounts) => {
|
||||
setSelectedRows(accounts);
|
||||
},
|
||||
[setSelectedRows],
|
||||
);
|
||||
|
||||
// Refetches accounts data table when current custom view changed.
|
||||
const handleFilterChanged = useCallback(() => {
|
||||
fetchAccountsHook.refetch();
|
||||
}, [fetchAccountsHook]);
|
||||
|
||||
}, []);
|
||||
|
||||
// Handle fetch data of accounts datatable.
|
||||
const handleFetchData = useCallback(
|
||||
@@ -255,198 +81,22 @@ function AccountsChart({
|
||||
: {}),
|
||||
});
|
||||
},
|
||||
[fetchAccountsHook, addAccountsTableQueries],
|
||||
[addAccountsTableQueries],
|
||||
);
|
||||
|
||||
// Calculates the data table selected rows count.
|
||||
const selectedRowsCount = useMemo(() => Object.values(selectedRows).length, [
|
||||
selectedRows,
|
||||
]);
|
||||
|
||||
// Handle bulk Activate accounts button click.,
|
||||
const handleBulkActivate = useCallback(
|
||||
(bulkActivateIds) => {
|
||||
setBulkActivate(bulkActivateIds);
|
||||
},
|
||||
[setBulkActivate],
|
||||
);
|
||||
|
||||
// Handle cancel Bulk Activate accounts bulk delete.
|
||||
const handleCancelBulkActivate = useCallback(() => {
|
||||
setBulkActivate(false);
|
||||
}, []);
|
||||
|
||||
// Handle Bulk activate account confirm.
|
||||
const handleConfirmBulkActivate = useCallback(() => {
|
||||
requestBulkActivateAccounts(bulkActivate)
|
||||
.then(() => {
|
||||
setBulkActivate(false);
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'the_accounts_has_been_successfully_activated',
|
||||
}),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
queryCache.invalidateQueries('accounts-table');
|
||||
})
|
||||
.catch((errors) => {
|
||||
setBulkActivate(false);
|
||||
});
|
||||
}, [requestBulkActivateAccounts, bulkActivate, formatMessage]);
|
||||
|
||||
// Handle bulk Inactive accounts button click.,
|
||||
const handleBulkInactive = useCallback(
|
||||
(bulkInactiveIds) => {
|
||||
setBulkInactiveAccounts(bulkInactiveIds);
|
||||
},
|
||||
[setBulkInactiveAccounts],
|
||||
);
|
||||
|
||||
// Handle cancel Bulk Inactive accounts bulk delete.
|
||||
const handleCancelBulkInactive = useCallback(() => {
|
||||
setBulkInactiveAccounts(false);
|
||||
}, []);
|
||||
|
||||
// Handle Bulk Inactive accounts confirm.
|
||||
const handleConfirmBulkInactive = useCallback(() => {
|
||||
requestBulkInactiveAccounts(bulkInactiveAccounts)
|
||||
.then(() => {
|
||||
setBulkInactiveAccounts(false);
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'the_accounts_have_been_successfully_inactivated',
|
||||
}),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
queryCache.invalidateQueries('accounts-table');
|
||||
})
|
||||
.catch((errors) => {
|
||||
setBulkInactiveAccounts(false);
|
||||
});
|
||||
}, [requestBulkInactiveAccounts, bulkInactiveAccounts]);
|
||||
|
||||
return (
|
||||
<DashboardInsider
|
||||
loading={fetchResourceFields.isFetching || fetchResourceViews.isFetching}
|
||||
name={'accounts-chart'}
|
||||
>
|
||||
<DashboardActionsBar
|
||||
selectedRows={selectedRows}
|
||||
<AccountsActionsBar
|
||||
onFilterChanged={handleFilterChanged}
|
||||
onBulkDelete={handleBulkDelete}
|
||||
onBulkArchive={handleBulkArchive}
|
||||
onBulkActivate={handleBulkActivate}
|
||||
onBulkInactive={handleBulkInactive}
|
||||
/>
|
||||
<DashboardPageContent>
|
||||
<Switch>
|
||||
<Route
|
||||
exact={true}
|
||||
path={['/accounts/:custom_view_id/custom_view', '/accounts']}
|
||||
>
|
||||
<AccountsViewsTabs />
|
||||
|
||||
<AccountsDataTable
|
||||
onDeleteAccount={handleDeleteAccount}
|
||||
onInactiveAccount={handleInactiveAccount}
|
||||
onActivateAccount={handleActivateAccount}
|
||||
onRestoreAccount={handleRestoreAccount}
|
||||
onEditAccount={handleEditAccount}
|
||||
onFetchData={handleFetchData}
|
||||
onSelectedRowsChange={handleSelectedRowsChange}
|
||||
/>
|
||||
</Route>
|
||||
</Switch>
|
||||
|
||||
<Alert
|
||||
cancelButtonText={<T id={'cancel'} />}
|
||||
confirmButtonText={<T id={'delete'} />}
|
||||
icon="trash"
|
||||
intent={Intent.DANGER}
|
||||
isOpen={deleteAccount}
|
||||
onCancel={handleCancelAccountDelete}
|
||||
onConfirm={handleConfirmAccountDelete}
|
||||
>
|
||||
<p>
|
||||
<FormattedHTMLMessage
|
||||
id={'once_delete_this_account_you_will_able_to_restore_it'}
|
||||
/>
|
||||
</p>
|
||||
</Alert>
|
||||
|
||||
<Alert
|
||||
cancelButtonText={<T id={'cancel'} />}
|
||||
confirmButtonText={<T id={'inactivate'} />}
|
||||
intent={Intent.WARNING}
|
||||
isOpen={inactiveAccount}
|
||||
onCancel={handleCancelInactiveAccount}
|
||||
onConfirm={handleConfirmAccountActive}
|
||||
>
|
||||
<p>
|
||||
<T id={'are_sure_to_inactive_this_account'} />
|
||||
</p>
|
||||
</Alert>
|
||||
|
||||
<Alert
|
||||
cancelButtonText={<T id={'cancel'} />}
|
||||
confirmButtonText={<T id={'activate'} />}
|
||||
intent={Intent.WARNING}
|
||||
isOpen={activateAccount}
|
||||
onCancel={handleCancelActivateAccount}
|
||||
onConfirm={handleConfirmAccountActivate}
|
||||
>
|
||||
<p>
|
||||
<T id={'are_sure_to_activate_this_account'} />
|
||||
</p>
|
||||
</Alert>
|
||||
|
||||
<Alert
|
||||
cancelButtonText={<T id={'cancel'} />}
|
||||
confirmButtonText={`${formatMessage({
|
||||
id: 'delete',
|
||||
})} (${selectedRowsCount})`}
|
||||
icon="trash"
|
||||
intent={Intent.DANGER}
|
||||
isOpen={bulkDelete}
|
||||
onCancel={handleCancelBulkDelete}
|
||||
onConfirm={handleConfirmBulkDelete}
|
||||
>
|
||||
<p>
|
||||
<T
|
||||
id={'once_delete_these_accounts_you_will_not_able_restore_them'}
|
||||
/>
|
||||
</p>
|
||||
</Alert>
|
||||
|
||||
<Alert
|
||||
cancelButtonText={<T id={'cancel'} />}
|
||||
confirmButtonText={`${formatMessage({
|
||||
id: 'activate',
|
||||
})} (${selectedRowsCount})`}
|
||||
intent={Intent.WARNING}
|
||||
isOpen={bulkActivate}
|
||||
onCancel={handleCancelBulkActivate}
|
||||
onConfirm={handleConfirmBulkActivate}
|
||||
>
|
||||
<p>
|
||||
<T id={'are_sure_to_activate_this_accounts'} />
|
||||
</p>
|
||||
</Alert>
|
||||
<Alert
|
||||
cancelButtonText={<T id={'cancel'} />}
|
||||
confirmButtonText={`${formatMessage({
|
||||
id: 'inactivate',
|
||||
})} (${selectedRowsCount})`}
|
||||
intent={Intent.WARNING}
|
||||
isOpen={bulkInactiveAccounts}
|
||||
onCancel={handleCancelBulkInactive}
|
||||
onConfirm={handleConfirmBulkInactive}
|
||||
>
|
||||
<p>
|
||||
<T id={'are_sure_to_inactive_this_accounts'} />
|
||||
</p>
|
||||
</Alert>
|
||||
<AccountsViewPage />
|
||||
</DashboardPageContent>
|
||||
|
||||
<AccountsAlerts />
|
||||
</DashboardInsider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,47 +2,45 @@ import React, { useCallback, useState, useMemo, useEffect } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Popover,
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuDivider,
|
||||
Position,
|
||||
Intent,
|
||||
} from '@blueprintjs/core';
|
||||
import { withRouter } from 'react-router';
|
||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import { Icon, DataTable, If } from 'components';
|
||||
import { compose } from 'utils';
|
||||
import { saveInvoke, compose } from 'utils';
|
||||
import { useUpdateEffect } from 'hooks';
|
||||
|
||||
import { CLASSES } from 'common/classes';
|
||||
import {
|
||||
NormalCell,
|
||||
BalanceCell,
|
||||
} from './components';
|
||||
|
||||
import { NormalCell, BalanceCell, AccountActionsMenuList } from './components';
|
||||
import { TableFastCell } from 'components';
|
||||
import TableVirtualizedListRows from 'components/Datatable/TableVirtualizedRows';
|
||||
|
||||
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
|
||||
import withAccountsActions from 'containers/Accounts/withAccountsActions';
|
||||
import withAccounts from 'containers/Accounts/withAccounts';
|
||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||
import withCurrentView from 'containers/Views/withCurrentView';
|
||||
|
||||
/**
|
||||
* Accounts data-table.
|
||||
*/
|
||||
function AccountsDataTable({
|
||||
// #withDashboardActions
|
||||
accountsTable,
|
||||
accountsLoading,
|
||||
|
||||
// #withDialog.
|
||||
openDialog,
|
||||
|
||||
// #
|
||||
currentViewId,
|
||||
|
||||
// own properties
|
||||
// #ownProps
|
||||
onFetchData,
|
||||
onSelectedRowsChange,
|
||||
onDeleteAccount,
|
||||
onInactiveAccount,
|
||||
onInactivateAccount,
|
||||
onActivateAccount,
|
||||
onEditAccount,
|
||||
onNewChildAccount
|
||||
}) {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const { formatMessage } = useIntl();
|
||||
@@ -57,77 +55,44 @@ function AccountsDataTable({
|
||||
}
|
||||
}, [accountsLoading, setIsMounted]);
|
||||
|
||||
const handleEditAccount = useCallback(
|
||||
(account) => () => {
|
||||
openDialog('account-form', { action: 'edit', id: account.id });
|
||||
},
|
||||
[openDialog],
|
||||
);
|
||||
|
||||
const handleNewParentAccount = useCallback(
|
||||
(account) => {
|
||||
openDialog('account-form', {
|
||||
action: 'new_child',
|
||||
parentAccountId: account.id,
|
||||
accountTypeId: account.account_type_id,
|
||||
});
|
||||
},
|
||||
[openDialog],
|
||||
);
|
||||
const ActionsCell = useMemo(() =>
|
||||
({ row }) => (
|
||||
<Popover
|
||||
content={<AccountActionsMenuList
|
||||
account={row.original}
|
||||
onDeleteAccount={onDeleteAccount}
|
||||
onInactivateAccount={onInactivateAccount}
|
||||
onActivateAccount={onActivateAccount}
|
||||
onEditAccount={onEditAccount}
|
||||
/>}
|
||||
position={Position.RIGHT_TOP}
|
||||
>
|
||||
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
|
||||
</Popover>
|
||||
), [
|
||||
onDeleteAccount,
|
||||
onInactivateAccount,
|
||||
onActivateAccount,
|
||||
onEditAccount
|
||||
]);
|
||||
|
||||
const actionMenuList = useCallback(
|
||||
(account) => (
|
||||
<Menu>
|
||||
<MenuItem
|
||||
icon={<Icon icon="reader-18" />}
|
||||
text={formatMessage({ id: 'view_details' })}
|
||||
/>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
icon={<Icon icon="pen-18" />}
|
||||
text={formatMessage({ id: 'edit_account' })}
|
||||
onClick={handleEditAccount(account)}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={<Icon icon="plus" />}
|
||||
text={formatMessage({ id: 'new_child_account' })}
|
||||
onClick={() => handleNewParentAccount(account)}
|
||||
/>
|
||||
<MenuDivider />
|
||||
<If condition={account.active}>
|
||||
<MenuItem
|
||||
text={formatMessage({ id: 'inactivate_account' })}
|
||||
icon={<Icon icon="pause-16" iconSize={16} />}
|
||||
onClick={() => onInactiveAccount(account)}
|
||||
/>
|
||||
</If>
|
||||
<If condition={!account.active}>
|
||||
<MenuItem
|
||||
text={formatMessage({ id: 'activate_account' })}
|
||||
icon={<Icon icon="play-16" iconSize={16} />}
|
||||
onClick={() => onActivateAccount(account)}
|
||||
/>
|
||||
</If>
|
||||
<MenuItem
|
||||
text={formatMessage({ id: 'delete_account' })}
|
||||
icon={<Icon icon="trash-16" iconSize={16} />}
|
||||
intent={Intent.DANGER}
|
||||
onClick={() => onDeleteAccount(account)}
|
||||
/>
|
||||
</Menu>
|
||||
),
|
||||
[
|
||||
handleEditAccount,
|
||||
onDeleteAccount,
|
||||
onInactiveAccount,
|
||||
handleNewParentAccount,
|
||||
formatMessage,
|
||||
],
|
||||
);
|
||||
|
||||
const rowContextMenu = (cell) => {
|
||||
return actionMenuList(cell.row.original);
|
||||
};
|
||||
const RowContextMenu = useMemo(() => ({ row }) => (
|
||||
<AccountActionsMenuList
|
||||
account={row.original}
|
||||
onDeleteAccount={onDeleteAccount}
|
||||
onInactivateAccount={onInactivateAccount}
|
||||
onActivateAccount={onActivateAccount}
|
||||
onEditAccount={onEditAccount}
|
||||
onNewChildAccount={onNewChildAccount}
|
||||
/>
|
||||
), [
|
||||
onDeleteAccount,
|
||||
onInactivateAccount,
|
||||
onActivateAccount,
|
||||
onEditAccount,
|
||||
onNewChildAccount
|
||||
]);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
@@ -143,7 +108,7 @@ function AccountsDataTable({
|
||||
Header: formatMessage({ id: 'code' }),
|
||||
accessor: 'code',
|
||||
className: 'code',
|
||||
width: 70,
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
id: 'type',
|
||||
@@ -158,7 +123,7 @@ function AccountsDataTable({
|
||||
Cell: NormalCell,
|
||||
accessor: 'account_normal',
|
||||
className: 'normal',
|
||||
width: 65,
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
id: 'currency',
|
||||
@@ -176,40 +141,31 @@ function AccountsDataTable({
|
||||
{
|
||||
id: 'actions',
|
||||
Header: '',
|
||||
// Cell: ({ cell }) => (
|
||||
// <Popover
|
||||
// content={actionMenuList(cell.row.original)}
|
||||
// position={Position.RIGHT_TOP}
|
||||
// >
|
||||
// <Button icon={<Icon icon="more-h-16" iconSize={16} />} />
|
||||
// </Popover>
|
||||
// ),
|
||||
Cell: ActionsCell,
|
||||
className: 'actions',
|
||||
width: 50,
|
||||
},
|
||||
],
|
||||
[actionMenuList, formatMessage],
|
||||
[ActionsCell, formatMessage],
|
||||
);
|
||||
|
||||
|
||||
|
||||
const handleDatatableFetchData = useCallback((...params) => {
|
||||
onFetchData && onFetchData(...params);
|
||||
}, []);
|
||||
const handleDatatableFetchData = useCallback(
|
||||
(...params) => {
|
||||
saveInvoke(onFetchData, params);
|
||||
},
|
||||
[onFetchData],
|
||||
);
|
||||
|
||||
const handleSelectedRowsChange = useCallback(
|
||||
(selectedRows) => {
|
||||
onSelectedRowsChange &&
|
||||
onSelectedRowsChange(selectedRows.map((s) => s.original));
|
||||
saveInvoke(onSelectedRowsChange, selectedRows);
|
||||
},
|
||||
[onSelectedRowsChange],
|
||||
);
|
||||
|
||||
const rowClassNames = (row) => {
|
||||
return {
|
||||
'inactive': !row.original.active,
|
||||
};
|
||||
};
|
||||
const rowClassNames = (row) => ({
|
||||
inactive: !row.original.active,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
|
||||
@@ -223,14 +179,19 @@ function AccountsDataTable({
|
||||
sticky={true}
|
||||
onSelectedRowsChange={handleSelectedRowsChange}
|
||||
loading={accountsLoading && !isMounted}
|
||||
rowContextMenu={rowContextMenu}
|
||||
rowContextMenu={RowContextMenu}
|
||||
rowClassNames={rowClassNames}
|
||||
expandColumnSpace={1}
|
||||
autoResetExpanded={false}
|
||||
autoResetSortBy={false}
|
||||
autoResetSelectedRows={false}
|
||||
expandColumnSpace={1}
|
||||
expandToggleColumn={2}
|
||||
selectionColumnWidth={50}
|
||||
virtualizedRows={true}
|
||||
fixedSizeHeight={1000}
|
||||
TableCellRenderer={TableFastCell}
|
||||
TableRowsRenderer={TableVirtualizedListRows}
|
||||
// #TableVirtualizedListRows props.
|
||||
vListrowHeight={42}
|
||||
vListOverscanRowCount={10}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -239,7 +200,6 @@ function AccountsDataTable({
|
||||
export default compose(
|
||||
withRouter,
|
||||
withCurrentView,
|
||||
withDialogActions,
|
||||
withDashboardActions,
|
||||
withAccountsActions,
|
||||
withAccounts(({ accountsLoading, accountsTable }) => ({
|
||||
|
||||
84
client/src/containers/Accounts/AccountsViewPage.js
Normal file
84
client/src/containers/Accounts/AccountsViewPage.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import React, { memo } from 'react';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
import AccountsViewsTabs from 'containers/Accounts/AccountsViewsTabs';
|
||||
import AccountsDataTable from 'containers/Accounts/AccountsDataTable';
|
||||
|
||||
import withAccountsTableActions from 'containers/Accounts/withAccountsTableActions';
|
||||
import withAlertsActions from 'containers/Alert/withAlertActions';
|
||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
/**
|
||||
* Accounts view page.
|
||||
*/
|
||||
function AccountsViewPage({
|
||||
openAlert,
|
||||
|
||||
// #withDialog.
|
||||
openDialog,
|
||||
|
||||
// #withAccountsTableActions
|
||||
setSelectedRowsAccounts
|
||||
}) {
|
||||
// Handle delete action account.
|
||||
const handleDeleteAccount = (account) => {
|
||||
openAlert('account-delete', { accountId: account.id })
|
||||
};
|
||||
|
||||
// Handle activate action account.
|
||||
const handleActivateAccount = (account) => {
|
||||
openAlert('account-activate', { accountId: account.id });
|
||||
};
|
||||
|
||||
// Handle inactivate action account.
|
||||
const handleInactivateAccount = (account) => {
|
||||
openAlert('account-inactivate', { accountId: account.id });
|
||||
};
|
||||
|
||||
// Handle select accounts datatable rows.
|
||||
const handleSelectedRowsChange = (selectedRows) => {
|
||||
const selectedRowsIds = selectedRows.map(r => r.id);
|
||||
setSelectedRowsAccounts(selectedRowsIds);
|
||||
};
|
||||
|
||||
const handleEditAccount = (account) => {
|
||||
openDialog('account-form', { action: 'edit', id: account.id });
|
||||
}
|
||||
|
||||
const handleNewChildAccount = (account) => {
|
||||
openDialog('account-form', {
|
||||
action: 'new_child',
|
||||
parentAccountId: account.id,
|
||||
accountType: account.account_type,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route
|
||||
exact={true}
|
||||
path={['/accounts/:custom_view_id/custom_view', '/accounts']}
|
||||
>
|
||||
<AccountsViewsTabs />
|
||||
|
||||
<AccountsDataTable
|
||||
onDeleteAccount={handleDeleteAccount}
|
||||
onInactivateAccount={handleInactivateAccount}
|
||||
onActivateAccount={handleActivateAccount}
|
||||
onSelectedRowsChange={handleSelectedRowsChange}
|
||||
onEditAccount={handleEditAccount}
|
||||
onNewChildAccount={handleNewChildAccount}
|
||||
/>
|
||||
</Route>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
const AccountsViewPageMemo = memo(AccountsViewPage);
|
||||
|
||||
export default compose(
|
||||
withAlertsActions,
|
||||
withAccountsTableActions,
|
||||
withDialogActions
|
||||
)(AccountsViewPageMemo);
|
||||
@@ -1,11 +1,10 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useMemo, memo, useCallback } from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
import { connect } from 'react-redux';
|
||||
import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
|
||||
import { useParams, withRouter } from 'react-router-dom';
|
||||
import { pick } from 'lodash';
|
||||
|
||||
import { useUpdateEffect } from 'hooks';
|
||||
import { DashboardViewsTabs } from 'components';
|
||||
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
|
||||
import withAccounts from 'containers/Accounts/withAccounts';
|
||||
@@ -14,6 +13,9 @@ import withViewDetail from 'containers/Views/withViewDetails';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
/**
|
||||
* Accounts views tabs.
|
||||
*/
|
||||
function AccountsViewsTabs({
|
||||
// #withViewDetail
|
||||
viewId,
|
||||
@@ -23,16 +25,11 @@ function AccountsViewsTabs({
|
||||
accountsViews,
|
||||
|
||||
// #withAccountsTableActions
|
||||
addAccountsTableQueries,
|
||||
changeAccountsCurrentView,
|
||||
|
||||
// #withDashboardActions
|
||||
setTopbarEditView,
|
||||
changePageSubtitle,
|
||||
|
||||
// props
|
||||
customViewChanged,
|
||||
onViewChanged,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const { custom_view_id: customViewId = null } = useParams();
|
||||
@@ -40,28 +37,27 @@ function AccountsViewsTabs({
|
||||
useEffect(() => {
|
||||
setTopbarEditView(customViewId);
|
||||
changePageSubtitle(customViewId && viewItem ? viewItem.name : '');
|
||||
}, [customViewId]);
|
||||
}, [customViewId, viewItem, changePageSubtitle, setTopbarEditView]);
|
||||
|
||||
// Handle click a new view tab.
|
||||
const handleClickNewView = () => {
|
||||
const handleClickNewView = useCallback(() => {
|
||||
setTopbarEditView(null);
|
||||
history.push('/custom_views/accounts/new');
|
||||
};
|
||||
}, [setTopbarEditView]);
|
||||
|
||||
const handleTabChange = (viewId) => {
|
||||
const handleTabChange = useCallback((viewId) => {
|
||||
changeAccountsCurrentView(viewId || -1);
|
||||
// addAccountsTableQueries({
|
||||
// custom_view_id: viewId || null,
|
||||
// });
|
||||
};
|
||||
}, [changeAccountsCurrentView]);
|
||||
|
||||
const tabs = accountsViews.map((view) => ({
|
||||
const tabs = useMemo(() => accountsViews.map((view) => ({
|
||||
...pick(view, ['name', 'id']),
|
||||
}));
|
||||
})), [accountsViews]);;
|
||||
|
||||
return (
|
||||
<Navbar className="navbar--dashboard-views">
|
||||
<NavbarGroup align={Alignment.LEFT}>
|
||||
<DashboardViewsTabs
|
||||
defaultTabText={'All Accounts'}
|
||||
initialViewId={customViewId}
|
||||
resourceName={'accounts'}
|
||||
onChange={handleTabChange}
|
||||
@@ -72,8 +68,10 @@ function AccountsViewsTabs({
|
||||
);
|
||||
}
|
||||
|
||||
const AccountsViewsTabsMemo = memo(AccountsViewsTabs);
|
||||
|
||||
const mapStateToProps = (state, ownProps) => ({
|
||||
viewId: ownProps.match.params.custom_view_id,
|
||||
viewId: -1,
|
||||
});
|
||||
|
||||
const withAccountsViewsTabs = connect(mapStateToProps);
|
||||
@@ -87,4 +85,4 @@ export default compose(
|
||||
})),
|
||||
withAccountsTableActions,
|
||||
withViewDetail(),
|
||||
)(AccountsViewsTabs);
|
||||
)(AccountsViewsTabsMemo);
|
||||
|
||||
@@ -3,15 +3,76 @@ import {
|
||||
Position,
|
||||
Classes,
|
||||
Tooltip,
|
||||
MenuItem,
|
||||
Menu,
|
||||
MenuDivider,
|
||||
Intent
|
||||
} from '@blueprintjs/core';
|
||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import { Icon, Money, If, Choose } from 'components';
|
||||
import { Icon, Money, If } from 'components';
|
||||
import { saveInvoke } from 'utils';
|
||||
import { formatMessage } from 'services/intl';
|
||||
import { POPOVER_CONTENT_SIZING } from '@blueprintjs/core/lib/esm/common/classes';
|
||||
|
||||
export function AccountActionsMenuList({
|
||||
account,
|
||||
|
||||
onNewChildAccount,
|
||||
onEditAccount,
|
||||
onActivateAccount,
|
||||
onInactivateAccount,
|
||||
onDeleteAccount,
|
||||
}) {
|
||||
return (
|
||||
<Menu>
|
||||
<MenuItem
|
||||
icon={<Icon icon="reader-18" />}
|
||||
text={formatMessage({ id: 'view_details' })}
|
||||
/>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
icon={<Icon icon="pen-18" />}
|
||||
text={formatMessage({ id: 'edit_account' })}
|
||||
onClick={() => saveInvoke(onEditAccount, account)}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={<Icon icon="plus" />}
|
||||
text={formatMessage({ id: 'new_child_account' })}
|
||||
onClick={() => saveInvoke(onNewChildAccount, account)}
|
||||
/>
|
||||
<MenuDivider />
|
||||
<If condition={account.active}>
|
||||
<MenuItem
|
||||
text={formatMessage({ id: 'inactivate_account' })}
|
||||
icon={<Icon icon="pause-16" iconSize={16} />}
|
||||
onClick={() => saveInvoke(onInactivateAccount, account)}
|
||||
/>
|
||||
</If>
|
||||
<If condition={!account.active}>
|
||||
<MenuItem
|
||||
text={formatMessage({ id: 'activate_account' })}
|
||||
icon={<Icon icon="play-16" iconSize={16} />}
|
||||
onClick={() => saveInvoke(onActivateAccount, account)}
|
||||
/>
|
||||
</If>
|
||||
<MenuItem
|
||||
text={formatMessage({ id: 'delete_account' })}
|
||||
icon={<Icon icon="trash-16" iconSize={16} />}
|
||||
intent={Intent.DANGER}
|
||||
onClick={() => saveInvoke(onDeleteAccount, account)}
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
export function NormalCell({ cell: { value } }) {
|
||||
const { formatMessage } = useIntl();
|
||||
const arrowDirection = value === 'credit' ? 'down' : 'up';
|
||||
|
||||
// if (value !== 'credit' || value !== 'debit') {
|
||||
// return '';
|
||||
// }
|
||||
return (
|
||||
<Tooltip
|
||||
className={Classes.TOOLTIP_INDICATOR}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
import { If } from 'components';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { If, AppToaster } from 'components';
|
||||
import { formatMessage } from 'services/intl';
|
||||
|
||||
export const accountNameAccessor = (account) => {
|
||||
return (
|
||||
@@ -11,3 +13,23 @@ export const accountNameAccessor = (account) => {
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
// Handle delete errors in bulk and singular.
|
||||
export const handleDeleteErrors = (errors) => {
|
||||
if (errors.find((e) => e.type === 'ACCOUNT.PREDEFINED')) {
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'you_could_not_delete_predefined_accounts',
|
||||
}),
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
}
|
||||
if (errors.find((e) => e.type === 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS')) {
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'cannot_delete_account_has_associated_transactions',
|
||||
}),
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -18,6 +18,7 @@ export default (mapState) => {
|
||||
accountsTableQuery: state.accounts.tableQuery,
|
||||
accountsLoading: state.accounts.loading,
|
||||
accountErrors: state.accounts.errors,
|
||||
accountsSelectedRows: state.accounts.selectedRows,
|
||||
};
|
||||
return mapState ? mapState(mapped, state, props) : mapped;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { connect } from 'react-redux';
|
||||
import t from 'store/types';
|
||||
import { fetchAccountsTable } from 'store/accounts/accounts.actions';
|
||||
import { fetchAccountsTable, setBulkAction } from 'store/accounts/accounts.actions';
|
||||
|
||||
const mapActionsToProps = (dispatch) => ({
|
||||
requestFetchAccountsTable: (query = {}) =>
|
||||
@@ -21,11 +21,12 @@ const mapActionsToProps = (dispatch) => ({
|
||||
type: t.ACCOUNTS_TABLE_QUERIES_ADD,
|
||||
queries,
|
||||
}),
|
||||
setSelectedRowsAccounts: (ids) =>
|
||||
setSelectedRowsAccounts: (selectedRows) =>
|
||||
dispatch({
|
||||
type: t.ACCOUNTS_SELECTED_ROWS_SET,
|
||||
payload: { ids },
|
||||
payload: { selectedRows },
|
||||
}),
|
||||
setAccountsBulkAction: (actionName) => setBulkAction(actionName),
|
||||
});
|
||||
|
||||
export default connect(null, mapActionsToProps);
|
||||
|
||||
13
client/src/containers/Alert/withAlertActions.js
Normal file
13
client/src/containers/Alert/withAlertActions.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { connect } from 'react-redux';
|
||||
import t from 'store/types';
|
||||
|
||||
export const mapStateToProps = (state, props) => {
|
||||
return {};
|
||||
};
|
||||
|
||||
export const mapDispatchToProps = (dispatch) => ({
|
||||
openAlert: (name, payload) => dispatch({ type: t.OPEN_ALERT, name, payload }),
|
||||
closeAlert: (name, payload) => dispatch({ type: t.CLOSE_ALERT, name, payload }),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps);
|
||||
19
client/src/containers/Alert/withAlertStoreConnect.js
Normal file
19
client/src/containers/Alert/withAlertStoreConnect.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
isAlertOpenFactory,
|
||||
getAlertPayloadFactory,
|
||||
} from 'store/dashboard/dashboard.selectors';
|
||||
|
||||
export default (mapState) => {
|
||||
const isAlertOpen = isAlertOpenFactory();
|
||||
const getAlertPayload = getAlertPayloadFactory();
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
const mapped = {
|
||||
isOpen: isAlertOpen(state, props),
|
||||
payload: getAlertPayload(state, props),
|
||||
};
|
||||
return mapState ? mapState(mapped) : mapped;
|
||||
};
|
||||
return connect(mapStateToProps);
|
||||
}
|
||||
74
client/src/containers/Alerts/AccountActivateAlert.js
Normal file
74
client/src/containers/Alerts/AccountActivateAlert.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
FormattedMessage as T,
|
||||
useIntl,
|
||||
} from 'react-intl';
|
||||
import { Intent, Alert } from '@blueprintjs/core';
|
||||
import { queryCache } from 'react-query';
|
||||
import { AppToaster } from 'components';
|
||||
|
||||
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
|
||||
import withAlertActions from 'containers/Alert/withAlertActions';
|
||||
import withAccountsActions from 'containers/Accounts/withAccountsActions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
/**
|
||||
* Account activate alert.
|
||||
*/
|
||||
function AccountActivateAlert({
|
||||
name,
|
||||
isOpen,
|
||||
payload: { accountId },
|
||||
|
||||
// #withAlertActions
|
||||
closeAlert,
|
||||
|
||||
requestActivateAccount
|
||||
}) {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
// Handle alert cancel.
|
||||
const handleCancel = () => {
|
||||
closeAlert('account-activate');
|
||||
};
|
||||
|
||||
// Handle activate account confirm.
|
||||
const handleConfirmAccountActivate = () => {
|
||||
requestActivateAccount(accountId)
|
||||
.then(() => {
|
||||
closeAlert('account-activate');
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'the_account_has_been_successfully_activated',
|
||||
}),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
queryCache.invalidateQueries('accounts-table');
|
||||
})
|
||||
.catch((error) => {
|
||||
closeAlert('account-activate');
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Alert
|
||||
cancelButtonText={<T id={'cancel'} />}
|
||||
confirmButtonText={<T id={'activate'} />}
|
||||
intent={Intent.WARNING}
|
||||
isOpen={isOpen}
|
||||
onCancel={handleCancel}
|
||||
onConfirm={handleConfirmAccountActivate}
|
||||
>
|
||||
<p>
|
||||
<T id={'are_sure_to_activate_this_account'} />
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withAlertStoreConnect(),
|
||||
withAlertActions,
|
||||
withAccountsActions
|
||||
)(AccountActivateAlert);
|
||||
76
client/src/containers/Alerts/AccountBulkActivateAlert.js
Normal file
76
client/src/containers/Alerts/AccountBulkActivateAlert.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
FormattedMessage as T,
|
||||
FormattedHTMLMessage,
|
||||
useIntl
|
||||
} from 'react-intl';
|
||||
import { Intent, Alert } from '@blueprintjs/core';
|
||||
import { queryCache } from 'react-query';
|
||||
import { AppToaster } from 'components';
|
||||
|
||||
import withAccountsActions from 'containers/Accounts/withAccountsActions';
|
||||
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
|
||||
import withAlertActions from 'containers/Alert/withAlertActions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
function AccountBulkActivateAlert({
|
||||
name,
|
||||
isOpen,
|
||||
payload: { accountsIds },
|
||||
|
||||
// #withAlertActions
|
||||
closeAlert,
|
||||
|
||||
requestBulkActivateAccounts
|
||||
}) {
|
||||
const { formatMessage } = useIntl();
|
||||
const selectedRowsCount = 0;
|
||||
|
||||
// Handle alert cancel.
|
||||
const handleClose = () => {
|
||||
closeAlert(name);
|
||||
}
|
||||
|
||||
// Handle Bulk activate account confirm.
|
||||
const handleConfirmBulkActivate = () => {
|
||||
requestBulkActivateAccounts(accountsIds)
|
||||
.then(() => {
|
||||
closeAlert(name);
|
||||
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'the_accounts_has_been_successfully_activated',
|
||||
}),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
queryCache.invalidateQueries('accounts-table');
|
||||
})
|
||||
.catch((errors) => {
|
||||
closeAlert(name);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Alert
|
||||
cancelButtonText={<T id={'cancel'} />}
|
||||
confirmButtonText={`${formatMessage({
|
||||
id: 'activate',
|
||||
})} (${selectedRowsCount})`}
|
||||
intent={Intent.WARNING}
|
||||
isOpen={isOpen}
|
||||
onCancel={handleClose}
|
||||
onConfirm={handleConfirmBulkActivate}
|
||||
>
|
||||
<p>
|
||||
<T id={'are_sure_to_activate_this_accounts'} />
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withAlertStoreConnect(),
|
||||
withAlertActions,
|
||||
withAccountsActions
|
||||
)(AccountBulkActivateAlert);
|
||||
80
client/src/containers/Alerts/AccountBulkDeleteAlert.js
Normal file
80
client/src/containers/Alerts/AccountBulkDeleteAlert.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
FormattedMessage as T,
|
||||
useIntl
|
||||
} from 'react-intl';
|
||||
import { Intent, Alert } from '@blueprintjs/core';
|
||||
import { queryCache } from 'react-query';
|
||||
import { AppToaster } from 'components';
|
||||
|
||||
import { handleDeleteErrors } from 'containers/Accounts/utils';
|
||||
|
||||
import withAccountsActions from 'containers/Accounts/withAccountsActions';
|
||||
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
|
||||
import withAlertActions from 'containers/Alert/withAlertActions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
function AccountBulkDeleteAlert({
|
||||
// #ownProps
|
||||
name,
|
||||
|
||||
// #withAlertStoreConnect
|
||||
isOpen,
|
||||
payload: { accountsIds },
|
||||
|
||||
// #withAlertActions
|
||||
closeAlert,
|
||||
|
||||
// #withAccountsActions
|
||||
requestDeleteBulkAccounts
|
||||
}) {
|
||||
const { formatMessage } = useIntl();
|
||||
const selectedRowsCount = 0;
|
||||
|
||||
const handleCancel = () => {
|
||||
closeAlert(name);
|
||||
};
|
||||
// Handle confirm accounts bulk delete.
|
||||
const handleConfirmBulkDelete = () => {
|
||||
requestDeleteBulkAccounts(accountsIds)
|
||||
.then(() => {
|
||||
closeAlert(name);
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'the_accounts_has_been_successfully_deleted',
|
||||
}),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
queryCache.invalidateQueries('accounts-table');
|
||||
})
|
||||
.catch((errors) => {
|
||||
closeAlert(name);
|
||||
handleDeleteErrors(errors);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Alert
|
||||
cancelButtonText={<T id={'cancel'} />}
|
||||
confirmButtonText={`${formatMessage({
|
||||
id: 'delete',
|
||||
})} (${selectedRowsCount})`}
|
||||
icon="trash"
|
||||
intent={Intent.DANGER}
|
||||
isOpen={isOpen}
|
||||
onCancel={handleCancel}
|
||||
onConfirm={handleConfirmBulkDelete}
|
||||
>
|
||||
<p>
|
||||
<T id={'once_delete_these_accounts_you_will_not_able_restore_them'} />
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withAlertStoreConnect(),
|
||||
withAlertActions,
|
||||
withAccountsActions
|
||||
)(AccountBulkDeleteAlert);
|
||||
71
client/src/containers/Alerts/AccountBulkInactivateAlert.js
Normal file
71
client/src/containers/Alerts/AccountBulkInactivateAlert.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||
import { Intent, Alert } from '@blueprintjs/core';
|
||||
import { queryCache } from 'react-query';
|
||||
import { AppToaster } from 'components';
|
||||
|
||||
import withAccountsActions from 'containers/Accounts/withAccountsActions';
|
||||
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
|
||||
import withAlertActions from 'containers/Alert/withAlertActions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
function AccountBulkInactivateAlert({
|
||||
name,
|
||||
isOpen,
|
||||
payload: { accountsIds },
|
||||
|
||||
// #withAccountsActions
|
||||
requestBulkInactiveAccounts,
|
||||
|
||||
closeAlert,
|
||||
}) {
|
||||
const { formatMessage } = useIntl();
|
||||
const selectedRowsCount = 0;
|
||||
|
||||
// Handle alert cancel.
|
||||
const handleCancel = () => {
|
||||
closeAlert(name);
|
||||
};
|
||||
// Handle Bulk Inactive accounts confirm.
|
||||
const handleConfirmBulkInactive = () => {
|
||||
requestBulkInactiveAccounts(accountsIds)
|
||||
.then(() => {
|
||||
closeAlert(name);
|
||||
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'the_accounts_have_been_successfully_inactivated',
|
||||
}),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
queryCache.invalidateQueries('accounts-table');
|
||||
})
|
||||
.catch((errors) => {
|
||||
closeAlert(name);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Alert
|
||||
cancelButtonText={<T id={'cancel'} />}
|
||||
confirmButtonText={`${formatMessage({
|
||||
id: 'inactivate',
|
||||
})} (${selectedRowsCount})`}
|
||||
intent={Intent.WARNING}
|
||||
isOpen={isOpen}
|
||||
onCancel={handleCancel}
|
||||
onConfirm={handleConfirmBulkInactive}
|
||||
>
|
||||
<p>
|
||||
<T id={'are_sure_to_inactive_this_accounts'} />
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withAlertStoreConnect(),
|
||||
withAlertActions,
|
||||
withAccountsActions,
|
||||
)(AccountBulkInactivateAlert);
|
||||
84
client/src/containers/Alerts/AccountDeleteAlert.js
Normal file
84
client/src/containers/Alerts/AccountDeleteAlert.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
FormattedMessage as T,
|
||||
FormattedHTMLMessage,
|
||||
useIntl
|
||||
} from 'react-intl';
|
||||
import { Intent, Alert } from '@blueprintjs/core';
|
||||
import { queryCache } from 'react-query';
|
||||
import { AppToaster } from 'components';
|
||||
|
||||
import { handleDeleteErrors } from 'containers/Accounts/utils';
|
||||
|
||||
import withAccountsActions from 'containers/Accounts/withAccountsActions';
|
||||
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
|
||||
import withAlertActions from 'containers/Alert/withAlertActions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
/**
|
||||
* Account delete alerts.
|
||||
*/
|
||||
function AccountDeleteAlert({
|
||||
name,
|
||||
|
||||
// #withAlertStoreConnect
|
||||
isOpen,
|
||||
payload: { accountId },
|
||||
|
||||
// #withAccountsActions
|
||||
requestDeleteAccount,
|
||||
|
||||
// #withAlertActions
|
||||
closeAlert
|
||||
}) {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
// handle cancel delete account alert.
|
||||
const handleCancelAccountDelete = () => {
|
||||
closeAlert(name);
|
||||
};
|
||||
|
||||
// Handle confirm account delete.
|
||||
const handleConfirmAccountDelete = () => {
|
||||
requestDeleteAccount(accountId)
|
||||
.then(() => {
|
||||
closeAlert(name);
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'the_account_has_been_successfully_deleted',
|
||||
}),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
queryCache.invalidateQueries('accounts-table');
|
||||
})
|
||||
.catch((errors) => {
|
||||
handleDeleteErrors(errors);
|
||||
closeAlert(name);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Alert
|
||||
cancelButtonText={<T id={'cancel'} />}
|
||||
confirmButtonText={<T id={'delete'} />}
|
||||
icon="trash"
|
||||
intent={Intent.DANGER}
|
||||
isOpen={isOpen}
|
||||
onCancel={handleCancelAccountDelete}
|
||||
onConfirm={handleConfirmAccountDelete}
|
||||
>
|
||||
<p>
|
||||
<FormattedHTMLMessage
|
||||
id={'once_delete_this_account_you_will_able_to_restore_it'}
|
||||
/>
|
||||
</p>
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withAlertStoreConnect(),
|
||||
withAlertActions,
|
||||
withAccountsActions
|
||||
)(AccountDeleteAlert);
|
||||
71
client/src/containers/Alerts/AccountInactivateAlert.js
Normal file
71
client/src/containers/Alerts/AccountInactivateAlert.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
FormattedMessage as T,
|
||||
useIntl,
|
||||
} from 'react-intl';
|
||||
import { Intent, Alert } from '@blueprintjs/core';
|
||||
import { queryCache } from 'react-query';
|
||||
import { AppToaster } from 'components';
|
||||
|
||||
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
|
||||
import withAlertActions from 'containers/Alert/withAlertActions';
|
||||
import withAccountsActions from 'containers/Accounts/withAccountsActions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
function AccountInactivateAlert({
|
||||
name,
|
||||
isOpen,
|
||||
payload: { accountId },
|
||||
|
||||
// #withAlertActions
|
||||
closeAlert,
|
||||
|
||||
// #withAccountsActions
|
||||
requestInactiveAccount,
|
||||
|
||||
}) {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const handleCancelInactiveAccount = () => {
|
||||
closeAlert('account-inactivate');
|
||||
};
|
||||
|
||||
const handleConfirmAccountActive = () => {
|
||||
requestInactiveAccount(accountId)
|
||||
.then(() => {
|
||||
closeAlert('account-inactivate');
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'the_account_has_been_successfully_inactivated',
|
||||
}),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
queryCache.invalidateQueries('accounts-table');
|
||||
})
|
||||
.catch((error) => {
|
||||
closeAlert('account-inactivate');
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Alert
|
||||
cancelButtonText={<T id={'cancel'} />}
|
||||
confirmButtonText={<T id={'inactivate'} />}
|
||||
intent={Intent.WARNING}
|
||||
isOpen={isOpen}
|
||||
onCancel={handleCancelInactiveAccount}
|
||||
onConfirm={handleConfirmAccountActive}
|
||||
>
|
||||
<p>
|
||||
<T id={'are_sure_to_inactive_this_account'} />
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withAlertStoreConnect(),
|
||||
withAlertActions,
|
||||
withAccountsActions
|
||||
)(AccountInactivateAlert);
|
||||
5
client/src/containers/Alerts/index.js
Normal file
5
client/src/containers/Alerts/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import AccountDeleteAlert from './AccountDeleteAlert';
|
||||
|
||||
export default {
|
||||
AccountDeleteAlert,
|
||||
};
|
||||
@@ -22,7 +22,7 @@ import { transformApiErrors, transformAccountToForm } from './utils';
|
||||
import 'style/pages/Accounts/AccountFormDialog.scss';
|
||||
|
||||
const defaultInitialValues = {
|
||||
account_type_id: '',
|
||||
account_type: '',
|
||||
parent_account_id: '',
|
||||
name: '',
|
||||
code: '',
|
||||
@@ -51,7 +51,7 @@ function AccountFormDialogContent({
|
||||
accountId,
|
||||
action,
|
||||
parentAccountId,
|
||||
accountTypeId,
|
||||
accountType,
|
||||
}) {
|
||||
const { formatMessage } = useIntl();
|
||||
const isNewMode = !accountId;
|
||||
@@ -72,7 +72,10 @@ function AccountFormDialogContent({
|
||||
const handleSuccess = () => {
|
||||
closeDialog(dialogName);
|
||||
queryCache.invalidateQueries('accounts-table');
|
||||
queryCache.invalidateQueries('accounts-list');
|
||||
|
||||
setTimeout(() => {
|
||||
queryCache.invalidateQueries('accounts-list');
|
||||
}, 1000);
|
||||
|
||||
AppToaster.show({
|
||||
message: formatMessage(
|
||||
@@ -116,7 +119,7 @@ function AccountFormDialogContent({
|
||||
transformAccountToForm(account, {
|
||||
action,
|
||||
parentAccountId,
|
||||
accountTypeId,
|
||||
accountType,
|
||||
}),
|
||||
defaultInitialValues,
|
||||
),
|
||||
@@ -158,7 +161,7 @@ function AccountFormDialogContent({
|
||||
>
|
||||
<AccountFormDialogFields
|
||||
dialogName={dialogName}
|
||||
isNewMode={isNewMode}
|
||||
action={action}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
</Formik>
|
||||
|
||||
@@ -30,7 +30,7 @@ import { useAutofocus } from 'hooks';
|
||||
function AccountFormDialogFields({
|
||||
// #ownPropscl
|
||||
onClose,
|
||||
isNewMode,
|
||||
action,
|
||||
|
||||
// #withAccounts
|
||||
accounts,
|
||||
@@ -42,7 +42,7 @@ function AccountFormDialogFields({
|
||||
return (
|
||||
<Form>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<FastField name={'account_type'}>
|
||||
<Field name={'account_type'}>
|
||||
{({ form, field: { value }, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'account_type'} />}
|
||||
@@ -59,13 +59,13 @@ function AccountFormDialogFields({
|
||||
onTypeSelected={(accountType) => {
|
||||
form.setFieldValue('account_type', accountType.key);
|
||||
}}
|
||||
disabled={!isNewMode}
|
||||
disabled={action === 'edit' || action === 'new_child'}
|
||||
popoverProps={{ minimal: true }}
|
||||
popoverFill={true}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
</Field>
|
||||
|
||||
<FastField name={'name'}>
|
||||
{({ field, meta: { error, touched } }) => (
|
||||
@@ -126,7 +126,11 @@ function AccountFormDialogFields({
|
||||
|
||||
<If condition={values.subaccount}>
|
||||
<FastField name={'parent_account_id'}>
|
||||
{({ form, field: { value }, meta: { error, touched } }) => (
|
||||
{({
|
||||
form: { values, setFieldValue },
|
||||
field: { value },
|
||||
meta: { error, touched },
|
||||
}) => (
|
||||
<FormGroup
|
||||
label={<T id={'parent_account'} />}
|
||||
className={classNames(
|
||||
@@ -139,11 +143,12 @@ function AccountFormDialogFields({
|
||||
<AccountsSelectList
|
||||
accounts={accounts}
|
||||
onAccountSelected={(account) => {
|
||||
form.setFieldValue('parent_account_id', account.id);
|
||||
setFieldValue('parent_account_id', account.id);
|
||||
}}
|
||||
defaultSelectText={<T id={'select_parent_account'} />}
|
||||
selectedAccountId={value}
|
||||
popoverFill={true}
|
||||
filterByTypes={values.account_type}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
@@ -177,7 +182,7 @@ function AccountFormDialogFields({
|
||||
style={{ minWidth: '75px' }}
|
||||
type="submit"
|
||||
>
|
||||
{!isNewMode ? <T id={'edit'} /> : <T id={'submit'} />}
|
||||
{action === 'edit' ? <T id={'edit'} /> : <T id={'submit'} />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@ function AccountFormDialog({
|
||||
accountId={payload.id}
|
||||
action={payload.action}
|
||||
parentAccountId={payload.parentAccountId}
|
||||
accountTypeId={payload.accountTypeId}
|
||||
accountType={payload.accountType}
|
||||
/>
|
||||
</DialogSuspense>
|
||||
</Dialog>
|
||||
|
||||
@@ -14,11 +14,11 @@ export const transformApiErrors = (errors) => {
|
||||
export const transformAccountToForm = (account, {
|
||||
action,
|
||||
parentAccountId,
|
||||
accountTypeId
|
||||
accountType
|
||||
}) => {
|
||||
return {
|
||||
parent_account_id: action === 'new_child' ? parentAccountId : '',
|
||||
account_type_id: action === 'new_child'? accountTypeId : '',
|
||||
account_type: action === 'new_child'? accountType : '',
|
||||
subaccount: action === 'new_child' ? true : false,
|
||||
...account,
|
||||
}
|
||||
|
||||
6
client/src/containers/Router/withRoute.js
Normal file
6
client/src/containers/Router/withRoute.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { connect } from "react-redux";
|
||||
import { withRouter } from "react-router-dom"
|
||||
|
||||
export default (mapState) => {
|
||||
return () => withRouter ;
|
||||
};
|
||||
@@ -28,4 +28,4 @@ const mapDispatchToProps = (dispatch, props) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)
|
||||
export default connect(null, mapDispatchToProps)
|
||||
@@ -1,3 +1,4 @@
|
||||
import { batch } from 'react-redux'
|
||||
import { omit } from 'lodash';
|
||||
import ApiService from 'services/ApiService';
|
||||
import t from 'store/types';
|
||||
@@ -26,15 +27,17 @@ export const fetchAccountsList = () => {
|
||||
|
||||
ApiService.get('accounts', { params: query })
|
||||
.then((response) => {
|
||||
dispatch({
|
||||
type: t.ACCOUNTS_ITEMS_SET,
|
||||
accounts: response.data.accounts,
|
||||
});
|
||||
dispatch({
|
||||
type: t.ACCOUNTS_LIST_SET,
|
||||
payload: {
|
||||
batch(() => {
|
||||
dispatch({
|
||||
type: t.ACCOUNTS_ITEMS_SET,
|
||||
accounts: response.data.accounts,
|
||||
}
|
||||
});
|
||||
dispatch({
|
||||
type: t.ACCOUNTS_LIST_SET,
|
||||
payload: {
|
||||
accounts: response.data.accounts,
|
||||
}
|
||||
});
|
||||
});
|
||||
resolve(response);
|
||||
})
|
||||
@@ -62,18 +65,20 @@ export const fetchAccountsTable = ({ query } = {}) => {
|
||||
});
|
||||
ApiService.get('accounts', { params: { ...pageQuery, ...query } })
|
||||
.then((response) => {
|
||||
dispatch({
|
||||
type: t.ACCOUNTS_PAGE_SET,
|
||||
accounts: response.data.accounts,
|
||||
customViewId: response.data?.filter_meta?.view?.custom_view_id,
|
||||
});
|
||||
dispatch({
|
||||
type: t.ACCOUNTS_ITEMS_SET,
|
||||
accounts: response.data.accounts,
|
||||
});
|
||||
dispatch({
|
||||
type: t.ACCOUNTS_TABLE_LOADING,
|
||||
loading: false,
|
||||
batch(() => {
|
||||
dispatch({
|
||||
type: t.ACCOUNTS_PAGE_SET,
|
||||
accounts: response.data.accounts,
|
||||
customViewId: response.data?.filter_meta?.view?.custom_view_id,
|
||||
});
|
||||
dispatch({
|
||||
type: t.ACCOUNTS_ITEMS_SET,
|
||||
accounts: response.data.accounts,
|
||||
});
|
||||
dispatch({
|
||||
type: t.ACCOUNTS_TABLE_LOADING,
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
resolve(response);
|
||||
})
|
||||
@@ -243,3 +248,11 @@ export const fetchAccount = ({ id }) => {
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const setBulkAction = ({ action }) => {
|
||||
return (dispatch) => dispatch({
|
||||
type: t.ACCOUNTS_BULK_ACTION,
|
||||
payload: { action }
|
||||
});
|
||||
}
|
||||
@@ -75,11 +75,6 @@ const accountsReducer = createReducer(initialState, {
|
||||
}
|
||||
},
|
||||
|
||||
[t.ACCOUNTS_SELECTED_ROWS_SET]: (state, action) => {
|
||||
const { ids } = action.payload;
|
||||
state.selectedRows = [];
|
||||
},
|
||||
|
||||
[t.ACCOUNTS_SET_CURRENT_VIEW]: (state, action) => {
|
||||
state.currentViewId = action.currentViewId;
|
||||
},
|
||||
@@ -108,6 +103,11 @@ const accountsReducer = createReducer(initialState, {
|
||||
});
|
||||
state.items = items;
|
||||
},
|
||||
|
||||
[t.ACCOUNTS_SELECTED_ROWS_SET]: (state, action) => {
|
||||
const { selectedRows } = action.payload;
|
||||
state.selectedRows = selectedRows;
|
||||
}
|
||||
});
|
||||
|
||||
export default createTableQueryReducers('accounts', accountsReducer);
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import { repeat } from 'lodash';
|
||||
import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect';
|
||||
import { repeat, isEqual } from 'lodash';
|
||||
import {
|
||||
pickItemsFromIds,
|
||||
getItemById,
|
||||
paginationLocationQuery,
|
||||
} from 'store/selectors';
|
||||
import { flatToNestedArray, treeToList } from 'utils';
|
||||
|
||||
const createDeepEqualSelector = createSelectorCreator(
|
||||
defaultMemoize,
|
||||
isEqual
|
||||
);
|
||||
const accountsViewsSelector = (state) => state.accounts.views;
|
||||
const accountsDataSelector = (state) => state.accounts.items;
|
||||
const accountsCurrentViewSelector = (state) => state.accounts.currentViewId;
|
||||
@@ -23,7 +26,7 @@ export const getAccountsTableQuery = createSelector(
|
||||
},
|
||||
);
|
||||
|
||||
export const getAccountsItems = createSelector(
|
||||
export const getAccountsItems = createDeepEqualSelector(
|
||||
accountsViewsSelector,
|
||||
accountsDataSelector,
|
||||
accountsCurrentViewSelector,
|
||||
@@ -42,7 +45,7 @@ export const getAccountsItems = createSelector(
|
||||
);
|
||||
|
||||
export const getAccountsListFactory = () =>
|
||||
createSelector(
|
||||
createDeepEqualSelector(
|
||||
accountsListSelector,
|
||||
accountsDataSelector,
|
||||
(accountsTree, accountsItems) => {
|
||||
|
||||
@@ -8,6 +8,8 @@ export default {
|
||||
ACCOUNT_SET: 'ACCOUNT_SET',
|
||||
ACCOUNT_DELETE: 'ACCOUNT_DELETE',
|
||||
ACCOUNT_FORM_ERRORS: 'ACCOUNT_FORM_ERRORS',
|
||||
ACCOUNTS_BULK_ACTION: 'ACCOUNTS_BULK_ACTION',
|
||||
|
||||
CLEAR_ACCOUNT_FORM_ERRORS: 'CLEAR_ACCOUNT_FORM_ERRORS',
|
||||
|
||||
ACCOUNTS_SELECTED_ROWS_SET: 'ACCOUNTS_SELECTED_ROWS_SET',
|
||||
|
||||
@@ -3,10 +3,10 @@ import { pickItemsFromIds } from 'store/selectors';
|
||||
import { getResourceColumn } from 'store/resources/resources.reducer';
|
||||
|
||||
const resourceViewsIdsSelector = (state, props, resourceName) =>
|
||||
state.views.resourceViews[resourceName] || [];
|
||||
state.views.resourceViews[resourceName];
|
||||
|
||||
const viewsSelector = (state) => state.views.views;
|
||||
const viewByIdSelector = (state, props) => state.views.views[props.viewId] || {};
|
||||
const viewByIdSelector = (state, props) => state.views.views[props.viewId];
|
||||
|
||||
const viewColumnsSelector = (state, props) => {
|
||||
};
|
||||
|
||||
@@ -13,5 +13,21 @@ export function closeDialog(name, payload) {
|
||||
type: t.CLOSE_DIALOG,
|
||||
name: name,
|
||||
payload: payload,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function openAlert(name, payload) {
|
||||
return {
|
||||
type: t.OPEN_ALERT,
|
||||
name,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
export function closeAlert(name, payload) {
|
||||
return {
|
||||
type: t.CLOSE_ALERT,
|
||||
name,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
@@ -11,6 +11,7 @@ const initialState = {
|
||||
sidebarExpended: true,
|
||||
previousSidebarExpended: null,
|
||||
dialogs: {},
|
||||
alerts: {},
|
||||
topbarEditViewId: null,
|
||||
requestsLoading: 0,
|
||||
backLink: false,
|
||||
@@ -47,6 +48,20 @@ const reducerInstance = createReducer(initialState, {
|
||||
};
|
||||
},
|
||||
|
||||
[t.OPEN_ALERT]: (state, action) => {
|
||||
state.alerts[action.name] = {
|
||||
isOpen: true,
|
||||
payload: action.payload || {},
|
||||
};
|
||||
},
|
||||
|
||||
[t.CLOSE_ALERT]: (state, action) => {
|
||||
state.alerts[action.name] = {
|
||||
...state.alerts[action.name],
|
||||
isOpen: false,
|
||||
};
|
||||
},
|
||||
|
||||
[t.CLOSE_ALL_DIALOGS]: (state, action) => {
|
||||
|
||||
},
|
||||
|
||||
@@ -14,4 +14,20 @@ export const getDialogPayloadFactory = () => createSelector(
|
||||
(dialog) => {
|
||||
return { ...dialog?.payload };
|
||||
},
|
||||
);
|
||||
);
|
||||
|
||||
const alertByNameSelector = (state, props) => state.dashboard.alerts?.[props.name];
|
||||
|
||||
export const isAlertOpenFactory = () => createSelector(
|
||||
alertByNameSelector,
|
||||
(alert) => {
|
||||
return alert && alert.isOpen;
|
||||
},
|
||||
);
|
||||
|
||||
export const getAlertPayloadFactory = () => createSelector(
|
||||
alertByNameSelector,
|
||||
(alert) => {
|
||||
return { ...alert?.payload };
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
|
||||
|
||||
|
||||
export default {
|
||||
OPEN_DIALOG: 'OPEN_DIALOG',
|
||||
CLOSE_DIALOG: 'CLOSE_DIALOG',
|
||||
OPEN_ALERT: 'OPEN_ALERT',
|
||||
CLOSE_ALERT: 'CLOSE_ALERT',
|
||||
CLOSE_ALL_DIALOGS: 'CLOSE_ALL_DIALOGS',
|
||||
CLOSE_ALL_ALERTS: 'CLOSE_ALL_ALERTS',
|
||||
CHANGE_DASHBOARD_PAGE_TITLE: 'CHANGE_DASHBOARD_PAGE_TITLE',
|
||||
CHANGE_DASHBOARD_PAGE_HINT: 'CHANGE_DASHBOARD_PAGE_HINT',
|
||||
CHANGE_PREFERENCES_PAGE_TITLE: 'CHANGE_PREFERENCES_PAGE_TITLE',
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
.tr .td {
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
align-items: center;
|
||||
color: #141720;
|
||||
color: #101219;
|
||||
|
||||
.placeholder {
|
||||
color: #a0a0a0;
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
display: block;
|
||||
color: $sidebar-menu-label-color;
|
||||
font-size: 11px;
|
||||
padding: 8px 18px;
|
||||
padding: 10px 18px;
|
||||
margin-top: 4px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
|
||||
@@ -60,7 +60,6 @@
|
||||
.bp3-navbar-divider {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.form-group-quick-new-downDrop {
|
||||
.bp3-popover-target .bp3-button {
|
||||
color: #1552c8;
|
||||
@@ -146,7 +145,7 @@
|
||||
}
|
||||
.#{$ns}-button {
|
||||
color: #32304a;
|
||||
padding: 8px 10px;
|
||||
padding: 8px 12px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(167, 182, 194, 0.12);
|
||||
@@ -323,7 +322,7 @@
|
||||
flex: 1 0 0;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
margin: 20px;
|
||||
margin: 22px 32px;
|
||||
border: 1px solid #d2dce2;
|
||||
|
||||
.bigcapital-datatable {
|
||||
@@ -425,15 +424,15 @@
|
||||
|
||||
&[aria-selected='true'] {
|
||||
color: #0052cc;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.#{$ns}-tab-indicator-wrapper {
|
||||
.#{$ns}-tab-indicator {
|
||||
height: 3px;
|
||||
height: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.button--new-view {
|
||||
margin: 0;
|
||||
height: 50px;
|
||||
|
||||
@@ -38,7 +38,7 @@ $sidebar-popover-submenu-bg: rgb(1, 20, 62);
|
||||
$sidebar-menu-label-color: rgba(255, 255, 255, 0.45);
|
||||
$sidebar-submenu-item-color: rgba(255, 255, 255, 0.6);
|
||||
$sidebar-submenu-item-hover-color: rgba(255, 255, 255, 0.85);
|
||||
$sidebar-logo-opacity: 0.55;
|
||||
$sidebar-logo-opacity: 0.5;
|
||||
$sidebar-submenu-item-bg-color: #01287d;
|
||||
|
||||
$form-check-input-checked-color: #fff;
|
||||
|
||||
@@ -11,6 +11,7 @@ export default (req: Request, res: Response, next: Function) => {
|
||||
}
|
||||
if (!req.tenant.initializedAt) {
|
||||
Logger.info('[ensure_tenant_initialized_middleware] tenant database not initalized.');
|
||||
|
||||
return res.boom.badRequest(
|
||||
'Tenant database is not migrated with application schema yut.',
|
||||
{ errors: [{ type: 'TENANT.DATABASE.NOT.INITALIZED' }] },
|
||||
|
||||
@@ -31,7 +31,7 @@ export default class Account extends TenantModel {
|
||||
static get virtualAttributes() {
|
||||
return [
|
||||
'accountTypeLabel',
|
||||
'accountParentTypeLabel',
|
||||
'accountParentType',
|
||||
'accountNormal',
|
||||
'isBalanceSheetAccount',
|
||||
'isPLSheet'
|
||||
|
||||
@@ -160,8 +160,7 @@ export default class ManualJournalsService implements IManualJournalsService {
|
||||
const manualAccountsIds = manualJournalDTO.entries.map((e) => e.accountId);
|
||||
|
||||
const accounts = await Account.query()
|
||||
.whereIn('id', manualAccountsIds)
|
||||
.withGraphFetched('type');
|
||||
.whereIn('id', manualAccountsIds);
|
||||
|
||||
const storedAccountsIds = accounts.map((account) => account.id);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user