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