feat: optimize accounts performance.

feat: optimize alerts architecture.
feat: optimize datatable architecture.
feat: optimize datatable style.
This commit is contained in:
a.bouhuolia
2021-01-26 08:44:11 +02:00
parent 0655963607
commit b26f6c937c
70 changed files with 1607 additions and 1012 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@@ -0,0 +1,3 @@
import { createContext } from 'react';
export default createContext();

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

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

View File

@@ -0,0 +1,12 @@
import React from 'react';
import { Checkbox } from '@blueprintjs/core';
export default function TableIndeterminateCheckboxHeader({
getToggleAllRowsSelectedProps,
}) {
return (
<div>
<Checkbox {...getToggleAllRowsSelectedProps()} />
</div>
);
}

View File

@@ -0,0 +1,10 @@
import React from 'react';
import { Checkbox } from '@blueprintjs/core';
export default function TableIndeterminateCheckboxRow({ row }) {
return (
<div>
<Checkbox {...row.getToggleRowSelectedProps()} />
</div>
);
}

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

@@ -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,
},
{

View File

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

View File

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

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

View File

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

View File

@@ -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 }) => ({

View 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);

View File

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

View File

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

View File

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

View File

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

View File

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

View 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);

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

View 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);

View 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);

View 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);

View 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);

View 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);

View 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);

View File

@@ -0,0 +1,5 @@
import AccountDeleteAlert from './AccountDeleteAlert';
export default {
AccountDeleteAlert,
};

View File

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

View File

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

View File

@@ -33,7 +33,7 @@ function AccountFormDialog({
accountId={payload.id}
action={payload.action}
parentAccountId={payload.parentAccountId}
accountTypeId={payload.accountTypeId}
accountType={payload.accountType}
/>
</DialogSuspense>
</Dialog>

View File

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

View File

@@ -0,0 +1,6 @@
import { connect } from "react-redux";
import { withRouter } from "react-router-dom"
export default (mapState) => {
return () => withRouter ;
};

View File

@@ -28,4 +28,4 @@ const mapDispatchToProps = (dispatch, props) => {
}
}
export default connect(null, mapDispatchToProps)
export default connect(null, mapDispatchToProps)

View File

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

View File

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

View File

@@ -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) => {

View File

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

View File

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

View File

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

View File

@@ -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) => {
},

View File

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

View File

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

View File

@@ -142,7 +142,7 @@
.tr .td {
border-bottom: 1px solid #e8e8e8;
align-items: center;
color: #141720;
color: #101219;
.placeholder {
color: #a0a0a0;

View File

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

View File

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

View File

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

View File

@@ -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' }] },

View File

@@ -31,7 +31,7 @@ export default class Account extends TenantModel {
static get virtualAttributes() {
return [
'accountTypeLabel',
'accountParentTypeLabel',
'accountParentType',
'accountNormal',
'isBalanceSheetAccount',
'isPLSheet'

View File

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