feat: fix a bunch of bugs.

This commit is contained in:
Ahmed Bouhuolia
2020-04-28 04:01:10 +02:00
parent 6d0ad42582
commit 0cfa1126c5
41 changed files with 591 additions and 187 deletions

View File

@@ -1,4 +1,4 @@
import React, { useMemo, useState } from 'react';
import React, { useMemo, useState, useCallback } from 'react';
import Icon from 'components/Icon';
import {
Button,
@@ -29,6 +29,8 @@ function AccountsActionsBar({
getResourceFields,
addAccountsTableQueries,
onFilterChanged,
onBulkDelete,
onBulkArchive,
}) {
const {path} = useRouteMatch();
const onClickNewAccount = () => { openDialog('account-form', {}); };
@@ -51,6 +53,15 @@ function AccountsActionsBar({
onFilterChanged && onFilterChanged(filterConditions);
},
});
const handleBulkArchive = useCallback(() => {
onBulkArchive && onBulkArchive(selectedRows.map(r => r.id));
}, [onBulkArchive, selectedRows]);
const handleBulkDelete = useCallback(() => {
onBulkDelete && onBulkDelete(selectedRows.map(r => r.id));
}, [onBulkDelete, selectedRows]);
return (
<DashboardActionsBar>
<NavbarGroup>
@@ -77,6 +88,7 @@ function AccountsActionsBar({
onClick={onClickNewAccount}
/>
<Popover
minimal={true}
content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}>
@@ -92,6 +104,7 @@ function AccountsActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon='archive' iconSize={15} />}
text='Archive'
onClick={handleBulkArchive}
/>
)}
{hasSelectedRows && (
@@ -100,6 +113,7 @@ function AccountsActionsBar({
icon={<Icon icon='trash' iconSize={15} />}
text='Delete'
intent={Intent.DANGER}
onClick={handleBulkDelete}
/>
)}
<Button

View File

@@ -19,6 +19,7 @@ import ViewConnect from 'connectors/View.connector';
import LoadingIndicator from 'components/LoadingIndicator';
import DataTable from 'components/DataTable';
import Money from 'components/Money';
import { useUpdateEffect } from 'hooks';
function AccountsDataTable({
accounts,
@@ -31,9 +32,16 @@ function AccountsDataTable({
setTopbarEditView,
accountsLoading,
onFetchData,
setSelectedRowsAccounts,
onSelectedRowsChange
}) {
const { custom_view_id: customViewId } = useParams();
const {custom_view_id: customViewId} = useParams();
const [initialMount, setInitialMount] = useState(false);
useUpdateEffect(() => {
if (!accountsLoading) {
setInitialMount(true);
}
}, [accountsLoading, setInitialMount]);
useEffect(() => {
const viewMeta = getViewItem(customViewId);
@@ -52,12 +60,20 @@ function AccountsDataTable({
openDialog('account-form', { action: 'edit', id: account.id });
}, [openDialog]);
const actionMenuList = useCallback(account => (
const handleNewParentAccount = useCallback((account) => {
openDialog('account-form', { action: 'new_child', id: account.id });
}, [openDialog]);
const actionMenuList = useCallback((account) => (
<Menu>
<MenuItem text='View Details' />
<MenuDivider />
<MenuItem text='Edit Account' onClick={handleEditAccount(account)} />
<MenuItem text='New Account' />
<MenuItem
text='Edit Account'
onClick={handleEditAccount(account)} />
<MenuItem
text='New Account'
onClick={() => handleNewParentAccount(account)} />
<MenuDivider />
<MenuItem
text='Inactivate Account'
@@ -152,17 +168,22 @@ function AccountsDataTable({
onFetchData && onFetchData(...params);
}, []);
const handleSelectedRowsChange = useCallback((selectedRows) => {
onSelectedRowsChange && onSelectedRowsChange(selectedRows.map(s => s.original));
}, [onSelectedRowsChange]);
return (
<LoadingIndicator loading={accountsLoading} spinnerSize={30}>
<DataTable
columns={columns}
data={accounts}
onFetchData={handleDatatableFetchData}
manualSortBy={true}
selectionColumn={selectionColumn}
expandable={true}
treeGraph={true} />
</LoadingIndicator>
<DataTable
columns={columns}
data={accounts}
onFetchData={handleDatatableFetchData}
manualSortBy={true}
selectionColumn={selectionColumn}
expandable={true}
treeGraph={true}
onSelectedRowsChange={handleSelectedRowsChange}
loading={accountsLoading && !initialMount}
spinnerProps={{size: 30}} />
);
}

View File

@@ -8,6 +8,7 @@ import PrivateRoute from 'components/PrivateRoute';
import Authentication from 'components/Authentication';
import Dashboard from 'components/Dashboard/Dashboard';
import { isAuthenticated } from 'store/authentication/authentication.reducer'
import Progress from 'components/NProgress/Progress';
import messages from 'lang/en';
import 'style/App.scss';
@@ -22,6 +23,8 @@ function App(props) {
<PrivateRoute isAuthenticated={props.isAuthorized} component={Dashboard} />
</Router>
</div>
<Progress isAnimating={props.isLoading} />
</IntlProvider>
);
}
@@ -37,5 +40,6 @@ App.propTypes = {
const mapStateToProps = (state) => ({
isAuthorized: isAuthenticated(state),
isLoading: !!state.dashboard.requestsLoading,
});
export default connect(mapStateToProps)(App);

View File

@@ -6,7 +6,7 @@ export default function DashboardInsider({
loading,
children,
name,
mount = true,
mount = false,
}) {
return (
<div className={classnames({

View File

@@ -1,18 +1,18 @@
import React, {useEffect, useMemo, useCallback} from 'react';
import React, {useEffect, useRef, useCallback} from 'react';
import {
useTable,
useExpanded,
useRowSelect,
usePagination,
useResizeColumns,
useAsyncDebounce,
useSortBy,
useFlexLayout
} from 'react-table'
import {Checkbox} from '@blueprintjs/core';
} from 'react-table';
import { Checkbox, Spinner } from '@blueprintjs/core';
import classnames from 'classnames';
import { FixedSizeList } from 'react-window'
import { ConditionalWrapper } from 'utils';
import { useUpdateEffect } from 'hooks';
const IndeterminateCheckbox = React.forwardRef(
({ indeterminate, ...rest }, ref) => {
@@ -40,6 +40,8 @@ export default function DataTable({
payload,
expandable = false,
expandToggleColumn = 2,
noInitialFetch = false,
spinnerProps = { size: 40 },
}) {
const {
getTableProps,
@@ -62,7 +64,7 @@ export default function DataTable({
isAllRowsExpanded,
// Get the state from the instance
state: { pageIndex, pageSize, sortBy, selectedRowIds },
state: { pageIndex, pageSize, sortBy, selectedRowIds, selectedRows },
} = useTable(
{
columns,
@@ -115,12 +117,22 @@ export default function DataTable({
])
}
);
const isInitialMount = useRef(noInitialFetch);
// When these table states change, fetch new data!
useEffect(() => {
onFetchData && onFetchData({ pageIndex, pageSize, sortBy })
if (isInitialMount.current) {
isInitialMount.current = false;
} else {
onFetchData && onFetchData({ pageIndex, pageSize, sortBy })
}
}, [pageIndex, pageSize, sortBy]);
useUpdateEffect(() => {
onSelectedRowsChange && onSelectedRowsChange(selectedFlatRows);
}, [selectedRowIds, onSelectedRowsChange]);
// Renders table cell.
const RenderCell = useCallback(({ row, cell, index }) => (
<ConditionalWrapper
@@ -188,13 +200,18 @@ export default function DataTable({
{RenderVirtualizedRows}
</FixedSizeList>
) : RenderPage();
}, [fixedSizeHeight, rows, fixedItemSize, virtualizedRows, RenderVirtualizedRows, RenderPage]);
}, [fixedSizeHeight, rows, fixedItemSize, virtualizedRows,
RenderVirtualizedRows, RenderPage]);
return (
<div className={classnames(
'bigcapital-datatable',
className,
{'has-sticky-header': stickyHeader, 'is-expandable': expandable})}>
{
'has-sticky-header': stickyHeader,
'is-expandable': expandable,
'has-virtualized-rows': virtualizedRows,
})}>
<div {...getTableProps()} className="table">
<div className="thead">
{headerGroups.map(headerGroup => (
@@ -240,13 +257,16 @@ export default function DataTable({
))}
</div>
<div {...getTableBodyProps()} className="tbody">
{ RenderTBody() }
{ (page.length === 0) && (
{ !loading && RenderTBody() }
{ !loading && (page.length === 0) && (
<div className={'tr no-results'}>
<div class="td">{ noResults }</div>
</div>
)}
{ loading && (
<div class="loading"><Spinner size={spinnerProps.size} /></div>
) }
</div>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import React, {useEffect, useMemo} from 'react';
import React, {useEffect, useMemo, useCallback, useRef} from 'react';
import {
FormGroup,
InputGroup,
@@ -10,67 +10,94 @@ import {
import { useFormik } from 'formik';
import { isEqual } from 'lodash';
import { usePrevious } from 'react-use';
import { debounce } from 'lodash';
import Icon from 'components/Icon';
import { checkRequiredProperties } from 'utils';
export default function FilterDropdown({
fields,
onFilterChange,
}) {
const conditionalsItems = [
const conditionalsItems = useMemo(() => [
{ value: 'and', label: 'AND' },
{ value: 'or', label: 'OR' },
];
const resourceFields = [
], []);
const resourceFields = useMemo(() => [
...fields.map((field) => ({ value: field.key, label: field.label_name, })),
];
const compatatorsItems = [
], [fields]);
const compatatorsItems = useMemo(() => [
{value: '', label: 'Select a compatator'},
{value: 'equals', label: 'Equals'},
{value: 'not_equal', label: 'Not Equal'},
{value: 'contain', label: 'Contain'},
{value: 'not_contain', label: 'Not Contain'},
];
const defaultFilterCondition = {
], []);
const defaultFilterCondition = useMemo(() => ({
condition: 'and',
field_key: fields.length > 0 ? fields[0].key : '',
compatator: 'equals',
value: '',
};
const formik = useFormik({
}), [fields]);
const {
setFieldValue,
getFieldProps,
values,
errors,
touched,
} = useFormik({
enableReinitialize: true,
initialValues: {
conditions: [ defaultFilterCondition ],
},
});
const onClickNewFilter = () => {
formik.setFieldValue('conditions', [
...formik.values.conditions, defaultFilterCondition,
const onClickNewFilter = useCallback(() => {
setFieldValue('conditions', [
...values.conditions, defaultFilterCondition,
]);
};
}, [values, defaultFilterCondition, setFieldValue]);
const filteredFilterConditions = useMemo(() => {
return formik.values.conditions.filter(condition => !!condition.value);
}, [formik.values.conditions]);
const requiredProps = ['field_key', 'condition', 'compatator', 'value'];
return values.conditions
.filter((condition) =>
!checkRequiredProperties(condition, requiredProps));
}, [values.conditions]);
const prevConditions = usePrevious(filteredFilterConditions);
const onClickRemoveCondition = (index) => () => {
if (formik.values.conditions.length === 1) { return; }
const conditions = [ ...formik.values.conditions ];
conditions.splice(index, 1);
formik.setFieldValue('conditions', [ ...conditions ]);
}
const onFilterChangeThrottled = useRef(debounce((conditions) => {
onFilterChange && onFilterChange(conditions);
}, 1000));
useEffect(() => {
if (!isEqual(filteredFilterConditions, prevConditions)) {
onFilterChange(filteredFilterConditions);
if (!isEqual(prevConditions, filteredFilterConditions) && prevConditions) {
onFilterChangeThrottled.current(filteredFilterConditions);
}
}, [filteredFilterConditions])
}, [filteredFilterConditions]);
// Handle click remove condition.
const onClickRemoveCondition = (index) => () => {
if (values.conditions.length === 1) {
setFieldValue('conditions', [
defaultFilterCondition,
]);
return;
}
const conditions = [ ...values.conditions ];
conditions.splice(index, 1);
setFieldValue('conditions', [ ...conditions ]);
};
return (
<div class="filter-dropdown">
<div class="filter-dropdown__body">
{formik.values.conditions.map((condition, index) => (
{values.conditions.map((condition, index) => (
<div class="filter-dropdown__condition">
<FormGroup
className={'form-group--condition'}>
@@ -78,7 +105,7 @@ export default function FilterDropdown({
options={conditionalsItems}
className={Classes.FILL}
disabled={index > 1}
{...formik.getFieldProps(`conditions[${index}].condition`)} />
{...getFieldProps(`conditions[${index}].condition`)} />
</FormGroup>
<FormGroup
@@ -87,7 +114,7 @@ export default function FilterDropdown({
options={resourceFields}
value={1}
className={Classes.FILL}
{...formik.getFieldProps(`conditions[${index}].field_key`)} />
{...getFieldProps(`conditions[${index}].field_key`)} />
</FormGroup>
<FormGroup
@@ -95,14 +122,14 @@ export default function FilterDropdown({
<HTMLSelect
options={compatatorsItems}
className={Classes.FILL}
{...formik.getFieldProps(`conditions[${index}].compatator`)} />
{...getFieldProps(`conditions[${index}].compatator`)} />
</FormGroup>
<FormGroup
className={'form-group--value'}>
<InputGroup
placeholder="Value"
{...formik.getFieldProps(`conditions[${index}].value`)} />
{...getFieldProps(`conditions[${index}].value`)} />
</FormGroup>
<Button

View File

@@ -39,6 +39,8 @@ export default class Icon extends React.Component{
color,
htmlTitle,
iconSize = Icon.SIZE_STANDARD,
height,
width,
intent,
title = icon,
tagName = "span",
@@ -57,6 +59,9 @@ export default class Icon extends React.Component{
const classes = classNames(Classes.ICON, Classes.iconClass(icon), Classes.intentClass(intent), className);
const viewBox = iconPath.viewBox;
const computedHeight = height || iconSize;
const computedWidth = width || iconSize;
return React.createElement(
tagName,
{
@@ -64,7 +69,7 @@ export default class Icon extends React.Component{
className: classes,
title: htmlTitle,
},
<svg fill={color} data-icon={icon} width={iconSize} height={iconSize} viewBox={viewBox}>
<svg fill={color} data-icon={icon} width={computedWidth} height={computedHeight} viewBox={viewBox}>
{title && <desc>{title}</desc>}
{paths}
</svg>,

View File

@@ -0,0 +1,38 @@
import PropTypes from 'prop-types'
import * as React from 'react'
const Bar = ({ progress, animationDuration }) => (
<div
style={{
background: '#79b8ff',
height: 3,
left: 0,
marginLeft: `${(-1 + progress) * 100}%`,
position: 'fixed',
top: 0,
transition: `margin-left ${animationDuration}ms linear`,
width: '100%',
zIndex: 1031,
}}
>
<div
style={{
boxShadow: '0 0 10px #79b8ff, 0 0 5px #79b8ff',
display: 'block',
height: '100%',
opacity: 1,
position: 'absolute',
right: 0,
transform: 'rotate(3deg) translate(0px, -4px)',
width: 100,
}}
/>
</div>
)
Bar.propTypes = {
animationDuration: PropTypes.number.isRequired,
progress: PropTypes.number.isRequired,
}
export default Bar;

View File

@@ -0,0 +1,22 @@
import PropTypes from 'prop-types'
import * as React from 'react'
const Container = ({ children, isFinished, animationDuration }) => (
<div
style={{
opacity: isFinished ? 0 : 1,
pointerEvents: 'none',
transition: `opacity ${animationDuration}ms linear`,
}}
>
{children}
</div>
)
Container.propTypes = {
animationDuration: PropTypes.number.isRequired,
children: PropTypes.node.isRequired,
isFinished: PropTypes.bool.isRequired,
}
export default Container;

View File

@@ -0,0 +1,27 @@
import { useNProgress } from '@tanem/react-nprogress'
import PropTypes from 'prop-types'
import React from 'react'
import Bar from './Bar'
import Container from './Container'
import Spinner from './Spinner'
const Progress = ({
isAnimating,
minimum = 0.2
}) => {
const { animationDuration, isFinished, progress } = useNProgress({
isAnimating, minimum,
});
return (
<Container isFinished={isFinished} animationDuration={animationDuration}>
<Bar progress={progress} animationDuration={animationDuration} />
</Container>
)
}
Progress.propTypes = {
isAnimating: PropTypes.bool.isRequired,
};
export default Progress;

View File

@@ -0,0 +1,29 @@
import * as React from 'react'
const Spinner = () => (
<div
style={{
display: 'block',
position: 'fixed',
right: 15,
top: 15,
zIndex: 1031,
}}
>
<div
style={{
animation: '400ms linear infinite spinner',
borderBottom: '2px solid transparent',
borderLeft: '2px solid #29d',
borderRadius: '50%',
borderRight: '2px solid transparent',
borderTop: '2px solid #29d',
boxSizing: 'border-box',
height: 18,
width: 18,
}}
/>
</div>
)
export default Spinner;

View File

@@ -1,23 +1,12 @@
import React from 'react';
import appMeta from 'config/app';
import Icon from 'components/Icon';
export default function() {
return (
<div className="sidebar__head">
<div className="sidebar__head-logo">
</div>
<div className="sidebar__head-company-meta">
<div className="company-name">
{ appMeta.app_name }
</div>
<div className="company-meta">
<span class="version">
{ appMeta.app_version }
</span>
</div>
<Icon icon={'bigcapital'} width={140} height={28} className="bigcapital--alt" />
</div>
</div>
);