diff --git a/client/src/components/ErrorBoundary/index.js b/client/src/components/ErrorBoundary/index.js new file mode 100644 index 000000000..5a0789e50 --- /dev/null +++ b/client/src/components/ErrorBoundary/index.js @@ -0,0 +1,34 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +function ErrorBoundary({ + error, + errorInfo, + children +}) { + + if (errorInfo) { + return ( +
+

Something went wrong.

+ +
+ {error && error.toString()} +
+ {errorInfo.componentStack} +
+
+ ); + } + return children; +} + +ErrorBoundary.defaultProps = { + children: null, +}; + +ErrorBoundary.propTypes = { + children: PropTypes.node, +}; + +export default ErrorBoundary; \ No newline at end of file diff --git a/server/package.json b/server/package.json index 953825d3c..ea4e47bce 100644 --- a/server/package.json +++ b/server/package.json @@ -59,6 +59,7 @@ "nodemon": "^1.19.1", "objection": "^2.0.10", "objection-soft-delete": "^1.0.7", + "pluralize": "^8.0.0", "reflect-metadata": "^0.1.13", "ts-transformer-keys": "^0.4.2", "tsyringe": "^4.3.0", diff --git a/server/src/api/controllers/Resources.ts b/server/src/api/controllers/Resources.ts index 91f0d8955..32795dda5 100644 --- a/server/src/api/controllers/Resources.ts +++ b/server/src/api/controllers/Resources.ts @@ -1,3 +1,4 @@ +import { Service, Inject } from 'typedi'; import { Router, Request, Response, NextFunction } from 'express'; import { param, @@ -5,20 +6,26 @@ import { } from 'express-validator'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; import BaseController from './BaseController'; -import { Service } from 'typedi'; -import ResourceFieldsKeys from 'data/ResourceFieldsKeys'; +import { ServiceError } from 'exceptions'; +import ResourceService from 'services/Resource/ResourceService'; @Service() export default class ResourceController extends BaseController{ + @Inject() + resourcesService: ResourceService; + /** * Router constructor. */ router() { const router = Router(); - router.get('/:resource_model/fields', - this.resourceModelParamSchema, - asyncMiddleware(this.resourceFields.bind(this)) + router.get( + '/:resource_model/fields', [ + ...this.resourceModelParamSchema, + ], + asyncMiddleware(this.resourceFields.bind(this)), + this.handleServiceErrors ); return router; } @@ -31,14 +38,39 @@ export default class ResourceController extends BaseController{ /** * Retrieve resource fields of the given resource. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ resourceFields(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; const { resource_model: resourceModel } = req.params; try { + const resourceFields = this.resourcesService.getResourceFields(tenantId, resourceModel); + return res.status(200).send({ + resource_fields: this.transfromToResponse(resourceFields), + }); } catch (error) { next(error); } } + + /** + * Handles service errors. + * @param {Error} error + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + handleServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) { + if (error instanceof ServiceError) { + if (error.errorType === 'RESOURCE_MODEL_NOT_FOUND') { + return res.status(400).send({ + errors: [{ type: 'RESOURCE.MODEL.NOT.FOUND', code: 100 }], + }); + } + } + } }; diff --git a/server/src/database/seeds/core/20190423085242_seed_accounts.js b/server/src/database/seeds/core/20190423085242_seed_accounts.js index 58c29f3cc..270ad5504 100644 --- a/server/src/database/seeds/core/20190423085242_seed_accounts.js +++ b/server/src/database/seeds/core/20190423085242_seed_accounts.js @@ -5,8 +5,6 @@ exports.up = function (knex) { const tenancyService = Container.get(TenancyService); const i18n = tenancyService.i18n(knex.userParams.tenantId); - console.log(i18n); - return knex('accounts').then(() => { // Inserts seed entries return knex('accounts').insert([ diff --git a/server/src/interfaces/Account.ts b/server/src/interfaces/Account.ts index 398b48275..76dcc40a4 100644 --- a/server/src/interfaces/Account.ts +++ b/server/src/interfaces/Account.ts @@ -29,6 +29,7 @@ export interface IAccountsFilter extends IDynamicListFilterDTO { export interface IAccountType { id: number, key: string, + label: string, normal: string, rootType: string, childType: string, diff --git a/server/src/lib/ViewRolesBuilder/index.ts b/server/src/lib/ViewRolesBuilder/index.ts index f8eeff373..89606fd4a 100644 --- a/server/src/lib/ViewRolesBuilder/index.ts +++ b/server/src/lib/ViewRolesBuilder/index.ts @@ -268,4 +268,31 @@ export function validateFilterRolesFieldsExistance(model, filterRoles: IFilterRo return filterRoles.filter((filterRole: IFilterRole) => { return !validateFieldKeyExistance(model, filterRole.fieldKey); }); -} \ No newline at end of file +} + +/** + * Retrieve model fields keys. + * @param {IModel} Model + * @return {string[]} + */ +export function getModelFieldsKeys(Model: IModel) { + const fields = Object.keys(Model.fields); + + return fields.sort((a, b) => { + if (a < b) { return -1; } + if (a > b) { return 1; } + return 0; + }); +} + +export function getModelFields(Model: IModel) { + const fieldsKey = this.getModelFieldsKeys(Model); + + return fieldsKey.map((fieldKey) => { + const field = Model.fields[fieldKey]; + return { + ...field, + key: fieldKey, + }; + }) +} diff --git a/server/src/locales/en.json b/server/src/locales/en.json index 13a8df5e2..69522edf2 100644 --- a/server/src/locales/en.json +++ b/server/src/locales/en.json @@ -1,7 +1,5 @@ { - "Empty": "", - "Hello": "Hello", - "Petty Cash": "Petty Cash 2", + "Petty Cash": "Petty Cash", "Bank": "Bank", "Other Income": "Other Income", "Interest Income": "Interest Income", @@ -30,4 +28,14 @@ "Assets": "Assets", "Liabilities": "Liabilities", "Expenses": "Expenses", + "Account name": "Account name", + "Account type": "Account type", + "Account normal": "Account normal", + "Description": "Description", + "Account code": "Account code", + "Currency": "Currency", + "Balance": "Balance", + "Active": "Active", + "Created at": "Created at", + "fixed_asset": "Fixed asset" } \ No newline at end of file diff --git a/server/src/models/Account.js b/server/src/models/Account.js index 6fa7625ac..26e38183e 100644 --- a/server/src/models/Account.js +++ b/server/src/models/Account.js @@ -121,7 +121,7 @@ export default class Account extends TenantModel { static get fields() { return { name: { - label: 'Name', + label: 'Account name', column: 'name', }, type: { @@ -145,20 +145,25 @@ export default class Account extends TenantModel { relationColumn: 'account_types.root_type', }, created_at: { + label: 'Created at', column: 'created_at', columnType: 'date', }, active: { + label: 'Active', column: 'active', }, balance: { + label: 'Balance', column: 'amount', columnType: 'number' }, currency: { + label: 'Currency', column: 'currency_code', }, normal: { + label: 'Account normal', column: 'account_type_id', relation: 'account_types.id', relationColumn: 'account_types.normal' diff --git a/server/src/models/AccountType.js b/server/src/models/AccountType.js index b0babeed6..a93b3d688 100644 --- a/server/src/models/AccountType.js +++ b/server/src/models/AccountType.js @@ -4,7 +4,7 @@ import TenantModel from 'models/TenantModel'; export default class AccountType extends TenantModel { /** - * Table name + * Table name. */ static get tableName() { return 'account_types'; @@ -30,4 +30,26 @@ export default class AccountType extends TenantModel { }, }; } + + /** + * Accounts types labels. + */ + static get labels() { + return { + fixed_asset: 'Fixed asset', + current_asset: "Current asset", + long_term_liability: "Long term liability", + current_liability: "Current liability", + equity: "Equity", + expense: "Expense", + income: "Income", + accounts_receivable: "Accounts receivable", + accounts_payable: "Accounts payable", + other_expense: "Other expense", + other_income: "Other income", + cost_of_goods_sold: "Cost of goods sold (COGS)", + other_liability: "Other liability", + other_asset: 'Other asset', + }; + } } diff --git a/server/src/repositories/ViewRepository.ts b/server/src/repositories/ViewRepository.ts index 0d124d24b..b7488da99 100644 --- a/server/src/repositories/ViewRepository.ts +++ b/server/src/repositories/ViewRepository.ts @@ -1,5 +1,4 @@ import { IView } from 'interfaces'; -import { View } from 'models'; import TenantRepository from 'repositories/TenantRepository'; export default class ViewRepository extends TenantRepository { @@ -50,13 +49,23 @@ export default class ViewRepository extends TenantRepository { * @param {IView} view */ async insert(view: IView): Promise { + const { View } = this.models; const insertedView = await View.query().insertGraph({ ...view }); this.flushCache(); return insertedView; } + async update(viewId: number, view: IView): Promise { + const { View } = this.models; + const updatedView = await View.query().upsertGraph({ + id: viewId, + ...view + }); + this.flushCache(); + return updatedView; + } /** * Flushes repository cache. diff --git a/server/src/services/Accounts/AccountsTypesServices.ts b/server/src/services/Accounts/AccountsTypesServices.ts index b2b6d02d6..c21126989 100644 --- a/server/src/services/Accounts/AccountsTypesServices.ts +++ b/server/src/services/Accounts/AccountsTypesServices.ts @@ -1,4 +1,5 @@ import { Inject, Service } from 'typedi'; +import { omit } from 'lodash'; import TenancyService from 'services/Tenancy/TenancyService'; import { IAccountsTypesService, IAccountType } from 'interfaces'; @@ -12,8 +13,17 @@ export default class AccountsTypesService implements IAccountsTypesService{ * @param {number} tenantId - * @return {Promise} */ - getAccountsTypes(tenantId: number): Promise { + async getAccountsTypes(tenantId: number): Promise { const { accountTypeRepository } = this.tenancy.repositories(tenantId); - return accountTypeRepository.all(); + const { AccountType } = this.tenancy.models(tenantId); + const { __ } = this.tenancy.i18n(tenantId); + + const allAccountsTypes = await accountTypeRepository.all(); + + return allAccountsTypes.map((_accountType: IAccountType) => ({ + id: _accountType.id, + label: __(AccountType.labels[_accountType.key]), + ...omit(_accountType, ['id']), + })); } } \ No newline at end of file diff --git a/server/src/services/Resource/ResourceService.ts b/server/src/services/Resource/ResourceService.ts index cd2364b46..99d25c6c0 100644 --- a/server/src/services/Resource/ResourceService.ts +++ b/server/src/services/Resource/ResourceService.ts @@ -1,78 +1,71 @@ import { Service, Inject } from 'typedi'; -import { camelCase, upperFirst } from 'lodash' +import { camelCase, upperFirst } from 'lodash'; +import pluralize from 'pluralize'; import { IModel } from 'interfaces'; -import resourceFieldsKeys from 'data/ResourceFieldsKeys'; +import { + getModelFields, +} from 'lib/ViewRolesBuilder' import TenancyService from 'services/Tenancy/TenancyService'; +import { ServiceError } from 'exceptions'; + +const ERRORS = { + RESOURCE_MODEL_NOT_FOUND: 'RESOURCE_MODEL_NOT_FOUND', +}; @Service() export default class ResourceService { @Inject() tenancy: TenancyService; - /** - * - * @param {string} resourceName - */ - getResourceFieldsRelations(modelName: string) { - const fieldsRelations = resourceFieldsKeys[modelName]; - - if (!fieldsRelations) { - throw new Error('Fields relation not found in thte given resource model.'); - } - return fieldsRelations; - } - /** * Transform resource to model name. * @param {string} resourceName */ private resourceToModelName(resourceName: string): string { - return upperFirst(camelCase(resourceName)); + return upperFirst(camelCase(pluralize.singular(resourceName))); } /** - * Retrieve model from resource name in specific tenant. + * Retrieve model fields. * @param {number} tenantId - * @param {string} resourceName + * @param {IModel} Model */ - public getModel(tenantId: number, resourceName: string) { - const models = this.tenancy.models(tenantId); - const modelName = this.resourceToModelName(resourceName); + private getModelFields(tenantId: number, Model: IModel) { + const { __ } = this.tenancy.i18n(tenantId); + const fields = getModelFields(Model); - return models[modelName]; - } - - getModelFields(Model: IModel) { - const fields = Object.keys(Model.fields); - - return fields.sort((a, b) => { - if (a < b) { return -1; } - if (a > b) { return 1; } - return 0; - }); + return fields.map((field) => ({ + label: __(field.label, field.label), + key: field.key, + dataType: field.columnType, + })); } /** - * + * Retrieve resource fields from resource model name. * @param {string} resourceName */ - getResourceFields(Model: IModel) { - console.log(Model); + public getResourceFields(tenantId: number, modelName: string) { + const resourceModel = this.getResourceModel(tenantId, modelName); - if (Model.resourceable) { - return this.getModelFields(Model); - } - return []; + return this.getModelFields(tenantId, resourceModel); } /** - * - * @param {string} resourceName + * Retrieve resource model object. + * @param {number} tenantId - + * @param {string} inputModelName - */ - getResourceColumns(Model: IModel) { - if (Model.resourceable) { - return this.getModelFields(Model); + public getResourceModel(tenantId: number, inputModelName: string) { + const modelName = this.resourceToModelName(inputModelName); + const Models = this.tenancy.models(tenantId); + + if (!Models[modelName]) { + throw new ServiceError(ERRORS.RESOURCE_MODEL_NOT_FOUND); } - return []; + if (!Models[modelName].resourceable) { + throw new ServiceError(ERRORS.RESOURCE_MODEL_NOT_FOUND); + } + return Models[modelName]; } } \ No newline at end of file diff --git a/server/src/services/Views/ViewsService.ts b/server/src/services/Views/ViewsService.ts index cec769496..a55a17265 100644 --- a/server/src/services/Views/ViewsService.ts +++ b/server/src/services/Views/ViewsService.ts @@ -6,7 +6,11 @@ import { IViewDTO, IView, IViewEditDTO, + IModel, + IViewColumnDTO, + IViewRoleDTO, } from 'interfaces'; +import { getModelFieldsKeys } from 'lib/ViewRolesBuilder'; import TenancyService from 'services/Tenancy/TenancyService'; import ResourceService from "services/Resource/ResourceService"; import { validateRolesLogicExpression } from 'lib/ViewRolesBuilder'; @@ -37,14 +41,14 @@ export default class ViewsService implements IViewsService { * @param {number} tenantId - * @param {string} resourceModel - */ - public async listResourceViews(tenantId: number, resourceModel: string): Promise { - this.logger.info('[views] trying to retrieve resource views.', { tenantId, resourceModel }); + public async listResourceViews(tenantId: number, resourceModelName: string): Promise { + this.logger.info('[views] trying to retrieve resource views.', { tenantId, resourceModelName }); // Validate the resource model name is valid. - this.getResourceModelOrThrowError(tenantId, resourceModel); + const resourceModel = this.getResourceModelOrThrowError(tenantId, resourceModelName); const { viewRepository } = this.tenancy.repositories(tenantId); - return viewRepository.allByResource(resourceModel); + return viewRepository.allByResource(resourceModel.name); } /** @@ -53,7 +57,7 @@ export default class ViewsService implements IViewsService { * @param {IViewRoleDTO[]} viewRoles */ private validateResourceRolesFieldsExistance(ResourceModel: IModel, viewRoles: IViewRoleDTO[]) { - const resourceFieldsKeys = this.resourceService.getResourceFields(ResourceModel); + const resourceFieldsKeys = getModelFieldsKeys(ResourceModel); const fieldsKeys = viewRoles.map(viewRole => viewRole.fieldKey); const notFoundFieldsKeys = difference(fieldsKeys, resourceFieldsKeys); @@ -70,7 +74,7 @@ export default class ViewsService implements IViewsService { * @param {IViewColumnDTO[]} viewColumns */ private validateResourceColumnsExistance(ResourceModel: IModel, viewColumns: IViewColumnDTO[]) { - const resourceFieldsKeys = this.resourceService.getResourceColumns(ResourceModel); + const resourceFieldsKeys = getModelFieldsKeys(ResourceModel); const fieldsKeys = viewColumns.map((viewColumn: IViewColumnDTO) => viewColumn.fieldKey); const notFoundFieldsKeys = difference(fieldsKeys, resourceFieldsKeys); @@ -115,12 +119,7 @@ export default class ViewsService implements IViewsService { * @param {number} resourceModel */ private getResourceModelOrThrowError(tenantId: number, resourceModel: string): IModel { - const ResourceModel = this.resourceService.getModel(tenantId, resourceModel); - - if (!ResourceModel || !ResourceModel.resourceable) { - throw new ServiceError(ERRORS.RESOURCE_MODEL_NOT_FOUND); - } - return ResourceModel; + return this.resourceService.getResourceModel(tenantId, resourceModel); } /** @@ -137,6 +136,8 @@ export default class ViewsService implements IViewsService { notViewId?: number ): void { const { View } = this.tenancy.models(tenantId); + + this.logger.info('[views] trying to validate view name uniqiness.', { tenantId, resourceModel, viewName }); const foundViews = await View.query() .where('resource_model', resourceModel) .where('name', viewName) @@ -165,6 +166,8 @@ export default class ViewsService implements IViewsService { * --------- * @param {number} tenantId - Tenant id. * @param {IViewDTO} viewDTO - View DTO. + * + * @return {Promise} */ public async newView(tenantId: number, viewDTO: IViewDTO): Promise { const { viewRepository } = this.tenancy.repositories(tenantId); @@ -187,6 +190,7 @@ export default class ViewsService implements IViewsService { throw new ServiceError(ERRORS.LOGIC_EXPRESSION_INVALID); } // Save view details. + this.logger.info('[views] trying to insert to storage.', { tenantId, viewDTO }) const view = await viewRepository.insert({ predefined: false, name: viewDTO.name, @@ -216,7 +220,7 @@ export default class ViewsService implements IViewsService { * @param {IViewEditDTO} */ public async editView(tenantId: number, viewId: number, viewEditDTO: IViewEditDTO): Promise { - const { View } = this.tenancy.models(tenantId); + const { viewRepository } = this.tenancy.repositories(tenantId); this.logger.info('[view] trying to edit custom view.', { tenantId, viewId }); // Retrieve view details or throw not found error. @@ -229,22 +233,23 @@ export default class ViewsService implements IViewsService { await this.validateViewNameUniquiness(tenantId, view.resourceModel, viewEditDTO.name, viewId); // Validate the given fields keys exist on the storage. - this.validateResourceRolesFieldsExistance(ResourceModel, view.roles); + this.validateResourceRolesFieldsExistance(ResourceModel, viewEditDTO.roles); // Validate the given columnable fields keys exists on the storage. - this.validateResourceColumnsExistance(ResourceModel, view.columns); + this.validateResourceColumnsExistance(ResourceModel, viewEditDTO.columns); // Validates the view conditional logic expression. if (!validateRolesLogicExpression(viewEditDTO.logicExpression, viewEditDTO.roles)) { throw new ServiceError(ERRORS.LOGIC_EXPRESSION_INVALID); } - // Save view details. - await View.query() - .where('id', view.id) - .patch({ - name: viewEditDTO.name, - roles_logic_expression: viewEditDTO.logicExpression, - }); + // Update view details. + await viewRepository.update(tenantId, viewId, { + predefined: false, + name: viewEditDTO.name, + rolesLogicExpression: viewEditDTO.logicExpression, + roles: viewEditDTO.roles, + columns: viewEditDTO.columns, + }) this.logger.info('[view] edited successfully.', { tenantId, viewId }); }