diff --git a/client/src/components/DataTable.js b/client/src/components/DataTable.js index d1998c244..cb88633ee 100644 --- a/client/src/components/DataTable.js +++ b/client/src/components/DataTable.js @@ -63,6 +63,7 @@ export default function DataTable({ expandColumnSpace = 1.5, updateDebounceTime = 200, + selectionColumnWidth = 42, // Read this document to know why! https://bit.ly/2Uw9SEc autoResetPage = true, @@ -134,9 +135,9 @@ export default function DataTable({ { id: 'selection', disableResizing: true, - minWidth: 42, - width: 42, - maxWidth: 42, + minWidth: selectionColumnWidth, + width: selectionColumnWidth, + maxWidth: selectionColumnWidth, // The header can use the table's getToggleAllRowsSelectedProps method // to render a checkbox Header: ({ getToggleAllRowsSelectedProps }) => ( diff --git a/client/src/containers/Accounts/AccountsChart.js b/client/src/containers/Accounts/AccountsChart.js index de9ee1a6a..8f5b85894 100644 --- a/client/src/containers/Accounts/AccountsChart.js +++ b/client/src/containers/Accounts/AccountsChart.js @@ -109,14 +109,6 @@ function AccountsChart({ intent: Intent.DANGER, }); } - if (errors.find(e => e.type === 'ACCOUNT.HAS.CHILD.ACCOUNTS')) { - AppToaster.show({ - message: formatMessage({ - id: 'you_could_not_delete_account_has_child_accounts', - }), - intent: Intent.DANGER, - }) - } }; // Handle confirm account delete @@ -130,6 +122,7 @@ function AccountsChart({ }), intent: Intent.SUCCESS, }); + queryCache.invalidateQueries('accounts-table'); }) .catch((errors) => { setDeleteAccount(false); @@ -215,6 +208,7 @@ function AccountsChart({ }), intent: Intent.SUCCESS, }); + queryCache.invalidateQueries('accounts-table'); }) .catch((errors) => { setBulkDelete(false); @@ -288,6 +282,7 @@ function AccountsChart({ }), intent: Intent.SUCCESS, }); + queryCache.invalidateQueries('accounts-table'); }) .catch((errors) => { setBulkActivate(false); @@ -318,6 +313,7 @@ function AccountsChart({ }), intent: Intent.SUCCESS, }); + queryCache.invalidateQueries('accounts-table'); }) .catch((errors) => { setBulkInactiveAccounts(false); diff --git a/client/src/containers/Accounts/AccountsDataTable.js b/client/src/containers/Accounts/AccountsDataTable.js index 47de2509a..417ff82c9 100644 --- a/client/src/containers/Accounts/AccountsDataTable.js +++ b/client/src/containers/Accounts/AccountsDataTable.js @@ -192,10 +192,7 @@ function AccountsDataTable({ [actionMenuList, formatMessage], ); - const selectionColumn = useMemo( - () => ({ minWidth: 40, width: 40, maxWidth: 40 }), - [], - ); + const handleDatatableFetchData = useCallback((...params) => { onFetchData && onFetchData(...params); @@ -216,14 +213,15 @@ function AccountsDataTable({ columns={columns} data={accountsTable} onFetchData={handleDatatableFetchData} - manualSortBy={true} - selectionColumn={selectionColumn} + selectionColumn={true} expandable={true} sticky={true} onSelectedRowsChange={handleSelectedRowsChange} loading={accountsLoading && !isMounted} rowContextMenu={rowContextMenu} expandColumnSpace={1} + autoResetExpanded={false} + selectionColumnWidth={50} /> ); diff --git a/client/src/style/pages/accounts-chart.scss b/client/src/style/pages/accounts-chart.scss index d8e7b4c10..5247c5bdb 100644 --- a/client/src/style/pages/accounts-chart.scss +++ b/client/src/style/pages/accounts-chart.scss @@ -16,11 +16,6 @@ padding-bottom: 0.3rem; } .account_name{ - > div{ - width: 100%; - font-weight: 500; - } - .bp3-popover-wrapper--inactive-semafro{ margin-left: 8px; margin-right: 6px; diff --git a/client/src/utils.js b/client/src/utils.js index aa1bfe0d2..452e97d41 100644 --- a/client/src/utils.js +++ b/client/src/utils.js @@ -256,7 +256,7 @@ export const flatToNestedArray = ( if (!item[config.parentId]) { nestedArray.push(item); } - if (parentItemId) { + if (parentItemId && map[parentItemId]) { map[parentItemId].children.push(item); } }); diff --git a/server/src/api/controllers/Accounts.ts b/server/src/api/controllers/Accounts.ts index f4ce2c961..b2e9b69b3 100644 --- a/server/src/api/controllers/Accounts.ts +++ b/server/src/api/controllers/Accounts.ts @@ -155,7 +155,7 @@ export default class AccountsController extends BaseController{ get bulkSelectIdsQuerySchema() { return [ - query('ids').isArray({ min: 2 }), + query('ids').isArray({ min: 1 }), query('ids.*').isNumeric().toInt(), ]; } @@ -240,7 +240,11 @@ export default class AccountsController extends BaseController{ try { await this.accountsService.deleteAccount(tenantId, accountId); - return res.status(200).send({ id: accountId }); + + return res.status(200).send({ + id: accountId, + message: 'The deleted account has been deleted successfully.', + }); } catch (error) { next(error); } @@ -298,7 +302,12 @@ export default class AccountsController extends BaseController{ const isActive = (type === 'activate' ? true : false); await this.accountsService.activateAccounts(tenantId, accountsIds, isActive); - return res.status(200).send({ ids: accountsIds }); + const activatedText = isActive ? 'activated' : 'inactivated'; + + return res.status(200).send({ + ids: accountsIds, + message: `The given accounts have been ${activatedText} successfully`, + }); } catch (error) { next(error); } @@ -316,8 +325,11 @@ export default class AccountsController extends BaseController{ try { await this.accountsService.deleteAccounts(tenantId, accountsIds); - return res.status(200).send({ ids: accountsIds }); + return res.status(200).send({ + ids: accountsIds, + message: 'The given accounts have been deleted successfully.', + }); } catch (error) { next(error); } @@ -426,12 +438,6 @@ export default class AccountsController extends BaseController{ { errors: [{ type: 'NOT_UNIQUE_CODE', code: 600 }] } ); } - if (error.errorType === 'account_has_children') { - return res.boom.badRequest( - 'You could not delete account has children.', - { errors: [{ type: 'ACCOUNT.HAS.CHILD.ACCOUNTS', code: 700 }] } - ); - } if (error.errorType === 'account_has_associated_transactions') { return res.boom.badRequest( 'You could not delete account has associated transactions.', diff --git a/server/src/services/Accounts/AccountsService.ts b/server/src/services/Accounts/AccountsService.ts index f83ff3eaa..be1064566 100644 --- a/server/src/services/Accounts/AccountsService.ts +++ b/server/src/services/Accounts/AccountsService.ts @@ -1,5 +1,5 @@ import { Inject, Service } from 'typedi'; -import { difference } from 'lodash'; +import { difference, chain, uniq } from 'lodash'; import { kebabCase } from 'lodash' import TenancyService from 'services/Tenancy/TenancyService'; import { ServiceError } from 'exceptions'; @@ -273,23 +273,20 @@ export default class AccountsService { } /** - * Throws error if account has children accounts. - * @param {number} tenantId - * @param {number} accountId + * Unlink the given parent account with children accounts. + * @param {number} tenantId - + * @param {number|number[]} parentAccountId - */ - private async throwErrorIfAccountHasChildren(tenantId: number, accountId: number) { + private async unassociateChildrenAccountsFromParent( + tenantId: number, + parentAccountId: number | number[], + ) { const { Account } = this.tenancy.models(tenantId); + const accountsIds = Array.isArray(parentAccountId) ? parentAccountId : [parentAccountId]; - this.logger.info('[account] validating if the account has children.', { - tenantId, accountId, - }); - const childAccounts = await Account.query().where( - 'parent_account_id', - accountId, - ); - if (childAccounts.length > 0) { - throw new ServiceError('account_has_children'); - } + await Account.query() + .whereIn('parent_account_id', accountsIds) + .patch({ parent_account_id: null }); } /** @@ -316,11 +313,15 @@ export default class AccountsService { const { accountRepository } = this.tenancy.repositories(tenantId); const account = await this.getAccountOrThrowError(tenantId, accountId); + // Throw error if the account was predefined. this.throwErrorIfAccountPredefined(account); - await this.throwErrorIfAccountHasChildren(tenantId, accountId); + // Throw error if the account has transactions. await this.throwErrorIfAccountHasTransactions(tenantId, accountId); + // Unlink the parent account from children accounts. + await this.unassociateChildrenAccountsFromParent(tenantId, accountId); + await accountRepository.deleteById(account.id); this.logger.info('[account] account has been deleted successfully.', { tenantId, accountId, @@ -336,7 +337,10 @@ export default class AccountsService { * @param {number[]} accountsIds * @return {IAccount[]} */ - private async getAccountsOrThrowError(tenantId: number, accountsIds: number[]): IAccount[] { + private async getAccountsOrThrowError( + tenantId: number, + accountsIds: number[] + ): Promise { const { Account } = this.tenancy.models(tenantId); this.logger.info('[account] trying to validate accounts not exist.', { tenantId, accountsIds }); @@ -400,14 +404,21 @@ export default class AccountsService { const { Account } = this.tenancy.models(tenantId); const accounts = await this.getAccountsOrThrowError(tenantId, accountsIds); + // Validate the accounts are not predefined. this.validatePrefinedAccounts(accounts); + + // Valdiate the accounts have transactions. await this.validateAccountsHaveTransactions(tenantId, accountsIds); + + // Unlink the parent account from children accounts. + await this.unassociateChildrenAccountsFromParent(tenantId, accountsIds); + + // Delete the accounts in one query. await Account.query().whereIn('id', accountsIds).delete(); this.logger.info('[account] given accounts deleted in bulk successfully.', { tenantId, accountsIds }); - // Triggers `onBulkDeleted` event. this.eventDispatcher.dispatch(events.accounts.onBulkDeleted); } @@ -420,10 +431,23 @@ export default class AccountsService { */ public async activateAccounts(tenantId: number, accountsIds: number[], activate: boolean = true) { const { Account } = this.tenancy.models(tenantId); + const { accountRepository } = this.tenancy.repositories(tenantId); + + // Retrieve the given account or throw not found. await this.getAccountsOrThrowError(tenantId, accountsIds); + // Get all children accounts. + const accountsGraph = await accountRepository.getDependencyGraph(); + const dependenciesAccounts = chain(accountsIds) + .map(accountId => accountsGraph.dependenciesOf(accountId)) + .flatten() + .value(); + + // The children and parent accounts. + const patchAccountsIds = uniq([...dependenciesAccounts, accountsIds]); + this.logger.info('[account] trying activate/inactive the given accounts ids.', { accountsIds }); - await Account.query().whereIn('id', accountsIds) + await Account.query().whereIn('id', patchAccountsIds) .patch({ active: activate ? 1 : 0, }); @@ -441,14 +465,25 @@ export default class AccountsService { */ public async activateAccount(tenantId: number, accountId: number, activate?: boolean) { const { Account } = this.tenancy.models(tenantId); + const { accountRepository } = this.tenancy.repositories(tenantId); + + // Retrieve the given account or throw not found error. const account = await this.getAccountOrThrowError(tenantId, accountId); + // Get all children accounts. + const accountsGraph = await accountRepository.getDependencyGraph(); + const dependenciesAccounts = accountsGraph.dependenciesOf(accountId); + this.logger.info('[account] trying to activate/inactivate the given account id.'); - await Account.query().where('id', accountId) + await Account.query() + .whereIn('id', [...dependenciesAccounts, accountId]) .patch({ active: activate ? 1 : 0, }) - this.logger.info('[account] account have been activated successfully.', { tenantId, accountId }); + this.logger.info('[account] account have been activated successfully.', { + tenantId, + accountId + }); // Triggers `onAccountActivated` event. this.eventDispatcher.dispatch(events.accounts.onActivated);