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

View File

@@ -8,6 +8,7 @@ import {
fetchAccountsTable,
submitAccount,
fetchAccount,
deleteBulkAccounts,
} from 'store/accounts/accounts.actions';
import {
getAccountsItems,
@@ -26,6 +27,7 @@ const mapStateToProps = (state, props) => ({
tableQuery: state.accounts.tableQuery,
accountsLoading: state.accounts.loading,
accountErrors: state.accounts.errors,
getAccountById: (id) => getItemById(state.accounts.items, id),
});
@@ -38,6 +40,7 @@ const mapActionsToProps = (dispatch) => ({
requestInactiveAccount: (id) => dispatch(inactiveAccount({ id })),
requestFetchAccount: (id) => dispatch(fetchAccount({ id })),
requestFetchAccountsTable: (query = {}) => dispatch(fetchAccountsTable({ query: { ...query } })),
requestDeleteBulkAccounts: (ids) => dispatch(deleteBulkAccounts({ ids })),
changeCurrentView: (id) => dispatch({
type: t.ACCOUNTS_SET_CURRENT_VIEW,
@@ -50,7 +53,7 @@ const mapActionsToProps = (dispatch) => ({
type: 'ACCOUNTS_TABLE_QUERIES_ADD', queries,
}),
setSelectedRowsAccounts: (ids) => dispatch({
type: t.ACCOUNTS_SELECTED_ROWS_SET, ids,
type: t.ACCOUNTS_SELECTED_ROWS_SET, payload: { ids },
}),
});

View File

@@ -18,6 +18,14 @@ const mapActionsToProps = (dispatch) => ({
setTopbarEditView: (id) => dispatch({
type: t.SET_TOPBAR_EDIT_VIEW, id,
}),
setDashboardRequestLoading: () => dispatch({
type: t.SET_DASHBOARD_REQUEST_LOADING,
}),
setDashboardRequestCompleted: () => dispatch({
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
}),
});
export default connect(mapStateToProps, mapActionsToProps);

View File

@@ -12,7 +12,7 @@ import {
import RegisterFromConnect from 'connectors/RegisterForm.connect';
import ErrorMessage from 'components/ErrorMessage';
import AppToaster from 'components/AppToaster';
import { compose, regExpCollection } from 'utils';
import { compose } from 'utils';
function Register({
requestSubmitRegister,
@@ -29,7 +29,6 @@ function Register({
last_name: Yup.string().required(),
email: Yup.string().email().required(),
phone_number: Yup.string()
.matches(regExpCollection.phoneNumber)
.required(intl.formatMessage({ id: 'required' })),
password: Yup.string()
.min(4, 'Password has to be longer than 8 characters!')

View File

@@ -2,8 +2,6 @@ import React, { useEffect, useState, useCallback } from 'react';
import {
Route,
Switch,
useParams,
useRouteMatch
} from 'react-router-dom';
import useAsync from 'hooks/async';
import { Alert, Intent } from '@blueprintjs/core';
@@ -21,56 +19,71 @@ import { compose } from 'utils';
function AccountsChart({
changePageTitle,
requestFetchAccounts,
requestDeleteAccount,
requestInactiveAccount,
fetchResourceViews,
fetchResourceFields,
getResourceFields,
requestFetchAccountsTable,
addAccountsTableQueries
addAccountsTableQueries,
requestDeleteBulkAccounts,
setDashboardRequestLoading,
setDashboardRequestCompleted,
}) {
const [state, setState] = useState({
deleteAlertActive: false,
restoreAlertActive: false,
inactiveAlertActive: false,
targetAccount: {},
});
const [deleteAccount, setDeleteAccount] = useState(false);
const [inactiveAccount, setInactiveAccount] = useState(false);
const [bulkDelete, setBulkDelete] = useState(false);
const [selectedRows, setSelectedRows] = useState([]);
// Fetch accounts resource views and fields.
const fetchHook = useAsync(async () => {
setDashboardRequestLoading();
await Promise.all([
fetchResourceViews('accounts'),
fetchResourceFields('accounts'),
]);
setDashboardRequestCompleted();
});
// Fetch accounts list according to the given custom view id.
const fetchAccountsHook = useAsync(async () => {
setDashboardRequestLoading();
await Promise.all([
requestFetchAccountsTable(),
]);
setDashboardRequestCompleted();
}, false);
useEffect(() => {
changePageTitle('Chart of Accounts');
}, []);
}, [changePageTitle]);
// Handle click and cancel/confirm account delete
const handleDeleteAccount = (account) => { setDeleteAccount(account); };
// handle cancel delete account alert.
const handleCancelAccountDelete = () => { setDeleteAccount(false); };
const handleCancelAccountDelete = useCallback(() => { setDeleteAccount(false); }, []);
// Handle confirm account delete
const handleConfirmAccountDelete = useCallback(() => {
requestDeleteAccount(deleteAccount.id).then(() => {
setDeleteAccount(false);
fetchAccountsHook.execute();
AppToaster.show({ message: 'the_account_has_been_deleted' });
}).catch(errors => {
setDeleteAccount(false);
if (errors.find((e) => e.type === 'ACCOUNT.PREDEFINED')) {
AppToaster.show({
message: 'cannot_delete_predefined_account'
});
}
if (errors.find((e) => e.type === 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS')) {
AppToaster.show({
message: 'cannot_delete_account_has_associated_transactions'
});
}
});
}, [deleteAccount]);
}, [deleteAccount, requestDeleteAccount]);
// Handle cancel/confirm account inactive.
const handleInactiveAccount = useCallback((account) => {
@@ -91,13 +104,7 @@ function AccountsChart({
});
}, [inactiveAccount]);
/**
* Handle cancel/confirm account restore.
*/
const handleCancelAccountRestore = () => {
};
const handleEditAccount = (account) => {
};
@@ -106,22 +113,43 @@ function AccountsChart({
};
const handleConfirmAccountRestore = (account) => {
const handleBulkDelete = useCallback((accountsIds) => {
setBulkDelete(accountsIds);
}, [setBulkDelete]);
};
const handleDeleteBulkAccounts = (accounts) => {
const handleConfirmBulkDelete = useCallback(() => {
requestDeleteBulkAccounts(bulkDelete).then(() => {
setBulkDelete(false);
AppToaster.show({ message: 'the_accounts_have_been_deleted' });
}).catch((error) => {
setBulkDelete(false);
});
}, [requestDeleteBulkAccounts, bulkDelete]);
};
const handleCancelBulkDelete = useCallback(() => {
setBulkDelete(false);
}, []);
const handleSelectedRowsChange = (accounts) => {
console.log(accounts);
};
const handleBulkArchive = useCallback((accounts) => {
}, []);
// Handle selected rows change.
const handleSelectedRowsChange = useCallback((accounts) => {
setSelectedRows(accounts);
}, [setSelectedRows]);
// Refetches accounts data table when current custom view changed.
const handleFilterChanged = useCallback(() => {
fetchAccountsHook.execute();
}, [fetchAccountsHook]);
const handleViewChanged = useCallback(() => { fetchAccountsHook.execute(); }, []);
// Refetch accounts data table when current custom view changed.
const handleViewChanged = useCallback(() => {
fetchAccountsHook.execute();
}, [fetchAccountsHook]);
// Handle fetch data of accounts datatable.
const handleFetchData = useCallback(({ pageIndex, pageSize, sortBy }) => {
addAccountsTableQueries({
...(sortBy.length > 0) ? {
@@ -135,26 +163,29 @@ function AccountsChart({
return (
<DashboardInsider loading={fetchHook.pending} name={'accounts-chart'}>
<DashboardActionsBar
onFilterChanged={handleFilterChanged} />
selectedRows={selectedRows}
onFilterChanged={handleFilterChanged}
onBulkDelete={handleBulkDelete}
onBulkArchive={handleBulkArchive} />
<DashboardPageContent>
<Switch>
<Route
exact={true}
path={[
'/dashboard/accounts/:custom_view_id/custom_view',
'/dashboard/accounts'
'/dashboard/accounts',
]}>
<AccountsViewsTabs
onViewChanged={handleViewChanged}
onDeleteBulkAccounts={handleDeleteBulkAccounts} />
onViewChanged={handleViewChanged} />
<AccountsDataTable
onSelectedRowsChange={handleSelectedRowsChange}
onDeleteAccount={handleDeleteAccount}
onInactiveAccount={handleInactiveAccount}
onRestoreAccount={handleRestoreAccount}
onEditAccount={handleEditAccount}
onFetchData={handleFetchData} />
onFetchData={handleFetchData}
onSelectedRowsChange={handleSelectedRowsChange} />
</Route>
</Switch>
@@ -185,6 +216,20 @@ function AccountsChart({
but it will become private to you.
</p>
</Alert>
<Alert
cancelButtonText="Cancel"
confirmButtonText="Delete"
icon="trash"
intent={Intent.DANGER}
isOpen={bulkDelete}
onCancel={handleCancelBulkDelete}
onConfirm={handleConfirmBulkDelete}>
<p>
Are you sure you want to move <b>filename</b> to Trash? You will be able to restore it later,
but it will become private to you.
</p>
</Alert>
</DashboardPageContent>
</DashboardInsider>
);

View File

@@ -38,7 +38,7 @@ function AccountFormDialog({
closeDialog,
requestSubmitAccount,
requestEditAccount,
getAccountById
getAccountById,
}) {
const intl = useIntl();
const accountFormValidationSchema = Yup.object().shape({
@@ -57,12 +57,23 @@ function AccountFormDialog({
}), []);
const [selectedAccountType, setSelectedAccountType] = useState(null);
const [selectedSubaccount, setSelectedSubaccount] = useState(null);
const [selectedSubaccount, setSelectedSubaccount] = useState(
payload.action === 'new_child' ?
accounts.find(a => a.id === payload.id) : null,
);
const editAccount = useMemo(() =>
payload.action === 'edit' ? getAccountById(payload.id) : null,
[payload, getAccountById]);
const transformApiErrors = (errors) => {
const fields = {};
if (errors.find(e => e.type === 'NOT_UNIQUE_CODE')) {
fields.code = 'Account code is not unqiue.'
}
return fields;
};
// Formik
const formik = useFormik({
enableReinitialize: true,
@@ -71,21 +82,22 @@ function AccountFormDialog({
? editAccount : initialValues,
},
validationSchema: accountFormValidationSchema,
onSubmit: (values, { setSubmitting }) => {
onSubmit: (values, { setSubmitting, setErrors }) => {
const exclude = ['subaccount'];
if (payload.action === 'edit') {
requestEditAccount({
payload: payload.id,
form: { ...omit(values, exclude) }
form: { ...omit(values, [...exclude, 'account_type_id']) }
}).then(response => {
closeDialog(name);
AppToaster.show({
message: 'the_account_has_been_edited'
});
setSubmitting(false);
}).catch(() => {
}).catch((errors) => {
setSubmitting(false);
setErrors(transformApiErrors(errors));
});
} else {
requestSubmitAccount({ form: { ...omit(values, exclude) } }).then(response => {
@@ -94,8 +106,9 @@ function AccountFormDialog({
message: 'the_account_has_been_submit'
});
setSubmitting(false);
}).catch(() => {
}).catch((errors) => {
setSubmitting(false);
setErrors(transformApiErrors(errors));
});
}
}
@@ -187,9 +200,7 @@ function AccountFormDialog({
return (<span>{'Sub account?'} <Icon icon="info-circle" iconSize={12} /></span>);
}, []);
const requiredSpan = useMemo(() => (
<span class="required">*</span>
), []);
const requiredSpan = useMemo(() => (<span class="required">*</span>), []);
return (
<Dialog
@@ -232,6 +243,7 @@ function AccountFormDialog({
rightIcon='caret-down'
text={selectedAccountType ?
selectedAccountType.name : 'Select account type'}
disabled={payload.action === 'edit'}
/>
</Select>
</FormGroup>

View File

@@ -1,3 +1,4 @@
import { omit } from 'lodash';
import ApiService from 'services/ApiService';
import t from 'store/types';
@@ -41,14 +42,20 @@ export const fetchAccountsList = ({ query } = {}) => {
export const fetchAccountsTable = ({ query } = {}) => {
return (dispatch, getState) =>
new Promise((resolve, reject) => {
const pageQuery = getState().accounts.tableQuery;
let pageQuery = getState().accounts.tableQuery;
if (pageQuery.filter_roles) {
pageQuery = {
...omit(pageQuery, ['filter_roles']),
stringified_filter_roles: JSON.stringify(pageQuery.filter_roles) || '',
};
}
dispatch({
type: t.ACCOUNTS_TABLE_LOADING,
loading: true,
});
ApiService.get('accounts', { params: { ...pageQuery, ...query } })
.then(response => {
.then((response) => {
dispatch({
type: t.ACCOUNTS_PAGE_SET,
accounts: response.data.accounts,
@@ -64,7 +71,7 @@ export const fetchAccountsTable = ({ query } = {}) => {
});
resolve(response);
})
.catch(error => {
.catch((error) => {
reject(error);
});
});
@@ -89,9 +96,12 @@ export const fetchAccountsDataTable = ({ query }) => {
export const submitAccount = ({ form }) => {
return dispatch =>
new Promise((resolve, reject) => {
ApiService.post('accounts', form)
.then(response => {
dispatch({ type: t.CLEAR_ACCOUNT_FORM_ERRORS });
dispatch({
type: t.ACCOUNT_ERRORS_CLEAR,
});
resolve(response);
})
.catch(error => {
@@ -99,11 +109,16 @@ export const submitAccount = ({ form }) => {
const { data } = response;
const { errors } = data;
dispatch({ type: t.CLEAR_ACCOUNT_FORM_ERRORS });
dispatch({
type: t.ACCOUNT_ERRORS_CLEAR,
});
if (errors) {
dispatch({ type: t.ACCOUNT_FORM_ERRORS, errors });
dispatch({
type: t.ACCOUNT_ERRORS_SET,
payload: { errors },
});
}
reject(error);
reject(errors);
});
});
};
@@ -125,7 +140,7 @@ export const editAccount = ({ id, form }) => {
if (errors) {
dispatch({ type: t.ACCOUNT_FORM_ERRORS, errors });
}
reject(error);
reject(errors);
});
});
};
@@ -143,11 +158,25 @@ export const deleteAccount = ({ id }) => {
ApiService.delete(`accounts/${id}`).then((response) => {
dispatch({ type: t.ACCOUNT_DELETE, id });
resolve(response);
}).catch(error => { reject(error); });
}).catch((error) => {
reject(error.response.data.errors || []);
});
});
};
export const deleteBulkAccounts = ({ ids }) => {};
export const deleteBulkAccounts = ({ ids }) => {
return dispatch => new Promise((resolve, reject) => {
ApiService.delete(`accounts`, { params: { ids }}).then((response) => {
dispatch({
type: t.ACCOUNTS_BULK_DELETE,
payload: { ids }
});
resolve(response);
}).catch((error) => {
reject(error.response.data.errors || []);
});
});
};
export const fetchAccount = ({ id }) => {
return dispatch =>

View File

@@ -7,11 +7,11 @@ const initialState = {
views: {},
accountsTypes: [],
accountsById: {},
accountFormErrors: [],
datatableQuery: {},
tableQuery: {},
currentViewId: -1,
selectedRows: [],
loading: false,
errors: [],
};
const accountsReducer = createReducer(initialState, {
@@ -34,8 +34,7 @@ const accountsReducer = createReducer(initialState, {
state.views[viewId] = {
...view,
ids: action.accounts.map(i => i.id),
};
state.accounts = action.accounts;
};
},
[t.ACCOUNT_TYPES_LIST_SET]: (state, action) => {
@@ -53,7 +52,8 @@ const accountsReducer = createReducer(initialState, {
},
[t.ACCOUNTS_SELECTED_ROWS_SET]: (state, action) => {
state.selectedRows.push(...action.ids);
const { ids } = action.payload;
state.selectedRows = [];
},
[t.ACCOUNTS_SET_CURRENT_VIEW]: (state, action) => {
@@ -63,6 +63,27 @@ const accountsReducer = createReducer(initialState, {
[t.ACCOUNTS_TABLE_LOADING]: (state, action) => {
state.loading = action.loading;
},
[t.ACCOUNT_ERRORS_SET]: (state, action) => {
const { errors } = action.payload;
state.errors = errors;
},
[t.ACCOUNT_ERRORS_CLEAR]: (state, action) => {
state.errors = [];
},
[t.ACCOUNTS_BULK_DELETE]: (state, action) => {
const { ids } = action.payload;
const items = { ...state.items };
ids.forEach((id) => {
if (typeof items[id] !== 'undefined') {
delete items[id];
}
});
state.items = items;
},
});
export default createTableQueryReducers('accounts', accountsReducer);

View File

@@ -1,7 +1,6 @@
import { pickItemsFromIds } from 'store/selectors';
export const getAccountsItems = (state, viewId) => {
const accountsView = state.accounts.views[viewId || -1];
const accountsItems = state.accounts.items;

View File

@@ -17,4 +17,8 @@ export default {
ACCOUNTS_TABLE_QUERIES_SET: 'ACCOUNTS_TABLE_QUERIES_SET',
ACCOUNTS_TABLE_LOADING: 'ACCOUNTS_TABLE_LOADING',
ACCOUNT_ERRORS_SET: 'ACCOUNT_ERRORS_SET',
ACCOUNT_ERRORS_CLEAR: 'ACCOUNT_ERRORS_CLEAR',
ACCOUNTS_BULK_DELETE: 'ACCOUNTS_BULK_DELETE'
};

View File

@@ -7,6 +7,7 @@ const initialState = {
preferencesPageTitle: '',
dialogs: {},
topbarEditViewId: 1,
requestsLoading: 0,
};
export default createReducer(initialState, {
@@ -42,6 +43,15 @@ export default createReducer(initialState, {
[t.SET_TOPBAR_EDIT_VIEW]: (state, action) => {
state.topbarEditViewId = action.id;
},
[t.SET_DASHBOARD_REQUEST_LOADING]: (state, action) => {
state.requestsLoading = state.requestsLoading + 1;
},
[t.SET_DASHBOARD_REQUEST_COMPLETED]: (state, action) => {
const requestsLoading = state.requestsLoading - 1;
state.requestsLoading = Math.max(requestsLoading, 0);
}
});

View File

@@ -9,4 +9,6 @@ export default {
CHANGE_PREFERENCES_PAGE_TITLE: 'CHANGE_PREFERENCES_PAGE_TITLE',
ALTER_DASHBOARD_PAGE_SUBTITLE: 'ALTER_DASHBOARD_PAGE_SUBTITLE',
SET_TOPBAR_EDIT_VIEW: 'SET_TOPBAR_EDIT_VIEW',
SET_DASHBOARD_REQUEST_LOADING: 'SET_DASHBOARD_REQUEST_LOADING',
SET_DASHBOARD_REQUEST_COMPLETED: 'SET_DASHBOARD_REQUEST_COMPLETED',
};

View File

@@ -1,11 +1,11 @@
import {pick} from 'lodash';
import {pick, at} from 'lodash';
export const getItemById = (items, id) => {
return items[id] || null;
};
export const pickItemsFromIds = (items, ids) => {
return Object.values(pick(items, ids));
return at(items, ids).filter(i => i);
}
export const getCurrentPageResults = (items, pages, pageNumber) => {

View File

@@ -60,4 +60,16 @@ $pt-font-family: Noto Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
.path-13{
fill: #2d95fd;
}
}
.bigcapital--alt{
svg{
path,
.path-13,
.path-1{
fill: #fff;
}
}
}

View File

@@ -111,9 +111,6 @@
}
.tbody{
overflow-y: scroll;
overflow-x: hidden;
.tr .td{
border-bottom: 1px solid #E8E8E8;
align-items: center;
@@ -145,6 +142,10 @@
align-items: center;
}
}
> .loading{
padding-top: 50px;
}
}
.tr .th,
@@ -175,8 +176,6 @@
display: block;
}
}
}
}
@@ -199,6 +198,13 @@
}
}
&.has-virtualized-rows{
.tbody{
overflow-y: scroll;
overflow-x: hidden;
}
}
&--financial-report{
.table {

View File

@@ -210,13 +210,13 @@
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
}
&__page-content{
// padding: 22px;
.bigcapital-datatable{
.table{
@@ -229,6 +229,10 @@
}
}
}
.dashboard__loading-indicator{
padding-top: 150px;
}
}
&__preferences-topbar{

View File

@@ -18,22 +18,14 @@ $sidebar-popover-submenu-bg: rgb(1, 20, 62);
overflow-x: hidden;
height: 100%;
}
&__head{
padding: 16px 10px;
padding: 16px 12px;
&-company-meta{
&-logo{
margin-top: 4px;
.company-name{
font-size: 16px;
font-weight: 200;
margin-bottom: 5px;
color: rgba(255, 255, 255, 0.75);
}
.company-meta{
color: rgba(255, 255, 255, 0.4);
font-size: 12px;
svg{
opacity: 0.35;
}
}
}

View File

@@ -21,7 +21,14 @@
margin-bottom: 0;
&:not(:last-of-type) {
padding-right: 15px;
padding-right: 12px;
}
.bp3-html-select select,
.bp3-select select{
padding: 0 20px 0 6px;
}
.bp3-input{
padding: 0 6px;
}
}

View File

@@ -144,4 +144,11 @@ export function formattedAmount(cents, currency) {
}
export const ConditionalWrapper = ({ condition, wrapper, children }) =>
condition ? wrapper(children) : children;
condition ? wrapper(children) : children;
export const checkRequiredProperties = (obj, properties) => {
return properties.some((prop) => {
const value = obj[prop];
return (value === '' || value === null || value === undefined);
})
}