diff --git a/packages/server/src/interfaces/Model.ts b/packages/server/src/interfaces/Model.ts index 49b875c2c7..c54d05f382 100644 --- a/packages/server/src/interfaces/Model.ts +++ b/packages/server/src/interfaces/Model.ts @@ -37,6 +37,7 @@ export interface IModelMetaFieldCommon { required?: boolean; importHint?: string; order?: number; + unique?: number; } export interface IModelMetaFieldText { diff --git a/packages/server/src/models/Account.Settings.ts b/packages/server/src/models/Account.Settings.ts index 6b9fa341f6..8d82ac1de6 100644 --- a/packages/server/src/models/Account.Settings.ts +++ b/packages/server/src/models/Account.Settings.ts @@ -41,6 +41,7 @@ export default { importable: true, minLength: 3, maxLength: 6, + unique: true, importHint: 'Unique number to identify the account.', }, rootType: { diff --git a/packages/server/src/services/Accounts/CommandAccountValidators.ts b/packages/server/src/services/Accounts/CommandAccountValidators.ts index 2819c7fddb..ed4d7ffbd3 100644 --- a/packages/server/src/services/Accounts/CommandAccountValidators.ts +++ b/packages/server/src/services/Accounts/CommandAccountValidators.ts @@ -97,9 +97,11 @@ export class CommandAccountValidators { query.whereNot('id', notAccountId); } }); - if (account.length > 0) { - throw new ServiceError(ERRORS.ACCOUNT_CODE_NOT_UNIQUE); + throw new ServiceError( + ERRORS.ACCOUNT_CODE_NOT_UNIQUE, + 'Account code is not unique.' + ); } } @@ -124,7 +126,10 @@ export class CommandAccountValidators { } }); if (foundAccount) { - throw new ServiceError(ERRORS.ACCOUNT_NAME_NOT_UNIQUE); + throw new ServiceError( + ERRORS.ACCOUNT_NAME_NOT_UNIQUE, + 'Account name is not unique.' + ); } } diff --git a/packages/server/src/services/Cashflow/CashflowDeleteAccount.ts b/packages/server/src/services/Cashflow/CashflowDeleteAccount.ts index 1c15672c78..bdc90641af 100644 --- a/packages/server/src/services/Cashflow/CashflowDeleteAccount.ts +++ b/packages/server/src/services/Cashflow/CashflowDeleteAccount.ts @@ -6,7 +6,7 @@ import { ERRORS } from './constants'; @Service() export default class CashflowDeleteAccount { @Inject() - tenancy: HasTenancyService; + private tenancy: HasTenancyService; /** * Validate the account has no associated cashflow transactions. diff --git a/packages/server/src/services/Import/ImportFileCommon.ts b/packages/server/src/services/Import/ImportFileCommon.ts index 90fe573107..9562e083ab 100644 --- a/packages/server/src/services/Import/ImportFileCommon.ts +++ b/packages/server/src/services/Import/ImportFileCommon.ts @@ -7,12 +7,13 @@ import { first } from 'lodash'; import { ImportFileDataValidator } from './ImportFileDataValidator'; import { Knex } from 'knex'; import { + ImportInsertError, ImportOperError, ImportOperSuccess, ImportableContext, } from './interfaces'; import { ServiceError } from '@/exceptions'; -import { trimObject } from './_utils'; +import { getUniqueImportableValue, trimObject } from './_utils'; import { ImportableResources } from './ImportableResources'; import ResourceService from '../Resource/ResourceService'; import HasTenancyService from '../Tenancy/TenancyService'; @@ -88,7 +89,12 @@ export class ImportFileCommon { import: importFile, }; const transformedDTO = importable.transform(objectDTO, context); - + const rowNumber = index + 1; + const uniqueValue = getUniqueImportableValue(importableFields, objectDTO); + const errorContext = { + rowNumber, + uniqueValue, + }; try { // Validate the DTO object before passing it to the service layer. await this.importFileValidator.validateData( @@ -105,18 +111,27 @@ export class ImportFileCommon { success.push({ index, data }); } catch (err) { if (err instanceof ServiceError) { - const error = [ + const error: ImportInsertError[] = [ { - errorCode: 'ValidationError', + errorCode: 'ServiceError', errorMessage: err.message || err.errorType, - rowNumber: index + 1, + ...errorContext, + }, + ]; + failed.push({ index, error }); + } else { + const error: ImportInsertError[] = [ + { + errorCode: 'UnknownError', + errorMessage: 'Unknown error occurred', + ...errorContext, }, ]; failed.push({ index, error }); } } } catch (errors) { - const error = errors.map((er) => ({ ...er, rowNumber: index + 1 })); + const error = errors.map((er) => ({ ...er, ...errorContext })); failed.push({ index, error }); } }; diff --git a/packages/server/src/services/Import/ImportFileDataValidator.ts b/packages/server/src/services/Import/ImportFileDataValidator.ts index 0f41f2bb42..143533c668 100644 --- a/packages/server/src/services/Import/ImportFileDataValidator.ts +++ b/packages/server/src/services/Import/ImportFileDataValidator.ts @@ -32,10 +32,14 @@ export class ImportFileDataValidator { try { await YupSchema.validate(_data, { abortEarly: false }); } catch (validationError) { - const errors = validationError.inner.map((error) => ({ - errorCode: 'ValidationError', - errorMessage: error.errors, - })); + const errors = validationError.inner.reduce((errors, error) => { + const newErrors = error.errors.map((errMsg) => ({ + errorCode: 'ValidationError', + errorMessage: errMsg, + })); + return [...errors, ...newErrors]; + }, []); + throw errors; } } diff --git a/packages/server/src/services/Import/_utils.ts b/packages/server/src/services/Import/_utils.ts index b0de7885c3..6c94bfade4 100644 --- a/packages/server/src/services/Import/_utils.ts +++ b/packages/server/src/services/Import/_utils.ts @@ -1,5 +1,5 @@ import * as Yup from 'yup'; -import { upperFirst, camelCase, first, isUndefined } from 'lodash'; +import { defaultTo, upperFirst, camelCase, first, isUndefined, pickBy } from 'lodash'; import pluralize from 'pluralize'; import { ResourceMetaFieldsMap } from './interfaces'; import { IModelMetaField } from '@/interfaces'; @@ -101,3 +101,24 @@ export const sanitizeResourceName = (resourceName: string) => { export const getSheetColumns = (sheetData: unknown[]) => { return Object.keys(first(sheetData)); }; + +/** + * Retrieves the unique value from the given imported object DTO based on the + * configured unique resource field. + * @param {{ [key: string]: IModelMetaField }} importableFields - + * @param {} + * @returns {string} + */ +export const getUniqueImportableValue = ( + importableFields: { [key: string]: IModelMetaField }, + objectDTO: Record +) => { + const uniqueImportableValue = pickBy( + importableFields, + (field) => field.unique + ); + const uniqueImportableKeys = Object.keys(uniqueImportableValue); + const uniqueImportableKey = first(uniqueImportableKeys); + + return defaultTo(objectDTO[uniqueImportableKey], ''); +}; diff --git a/packages/server/src/services/Import/interfaces.ts b/packages/server/src/services/Import/interfaces.ts index 69897086c2..b3cb9d8e19 100644 --- a/packages/server/src/services/Import/interfaces.ts +++ b/packages/server/src/services/Import/interfaces.ts @@ -58,7 +58,7 @@ export interface ImportOperSuccess { } export interface ImportOperError { - error: ImportInsertError; + error: ImportInsertError[]; index: number; } diff --git a/packages/webapp/src/containers/Import/ImportFilePreview.module.scss b/packages/webapp/src/containers/Import/ImportFilePreview.module.scss index f24ce44722..c62e7e7383 100644 --- a/packages/webapp/src/containers/Import/ImportFilePreview.module.scss +++ b/packages/webapp/src/containers/Import/ImportFilePreview.module.scss @@ -17,19 +17,13 @@ padding-left: 2rem; } -.skippedTable { +table.skippedTable { width: 100%; - - thead{ - th{ - padding-top: 0; - padding-bottom: 8px; - color: #738091; - font-weight: 500; - } - } - + tbody{ + tr:first-child td { + box-shadow: 0 0 0 0; + } tr td { vertical-align: middle; padding: 7px; diff --git a/packages/webapp/src/containers/Import/ImportFilePreview.tsx b/packages/webapp/src/containers/Import/ImportFilePreview.tsx index 7bdf5c4637..45b3a70ca8 100644 --- a/packages/webapp/src/containers/Import/ImportFilePreview.tsx +++ b/packages/webapp/src/containers/Import/ImportFilePreview.tsx @@ -93,23 +93,12 @@ function ImportFilePreviewSkipped() { > - - - - - - - {importPreview?.errors.map((error, key) => ( - - + + ))}
#NameError
{error.rowNumber}{error.rowNumber} - {error.errorMessage.map((message) => ( -
{message}
- ))} -
{error.uniqueValue}{error.errorMessage}