Merge pull request #392 from bigcapitalhq/import-show-unique-value-preview

fix: show the unique row value in the import preview
This commit is contained in:
Ahmed Bouhuolia
2024-03-28 05:39:40 +02:00
committed by GitHub
10 changed files with 70 additions and 40 deletions

View File

@@ -37,6 +37,7 @@ export interface IModelMetaFieldCommon {
required?: boolean; required?: boolean;
importHint?: string; importHint?: string;
order?: number; order?: number;
unique?: number;
} }
export interface IModelMetaFieldText { export interface IModelMetaFieldText {

View File

@@ -41,6 +41,7 @@ export default {
importable: true, importable: true,
minLength: 3, minLength: 3,
maxLength: 6, maxLength: 6,
unique: true,
importHint: 'Unique number to identify the account.', importHint: 'Unique number to identify the account.',
}, },
rootType: { rootType: {

View File

@@ -97,9 +97,11 @@ export class CommandAccountValidators {
query.whereNot('id', notAccountId); query.whereNot('id', notAccountId);
} }
}); });
if (account.length > 0) { 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) { if (foundAccount) {
throw new ServiceError(ERRORS.ACCOUNT_NAME_NOT_UNIQUE); throw new ServiceError(
ERRORS.ACCOUNT_NAME_NOT_UNIQUE,
'Account name is not unique.'
);
} }
} }

View File

@@ -6,7 +6,7 @@ import { ERRORS } from './constants';
@Service() @Service()
export default class CashflowDeleteAccount { export default class CashflowDeleteAccount {
@Inject() @Inject()
tenancy: HasTenancyService; private tenancy: HasTenancyService;
/** /**
* Validate the account has no associated cashflow transactions. * Validate the account has no associated cashflow transactions.

View File

@@ -7,12 +7,13 @@ import { first } from 'lodash';
import { ImportFileDataValidator } from './ImportFileDataValidator'; import { ImportFileDataValidator } from './ImportFileDataValidator';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { import {
ImportInsertError,
ImportOperError, ImportOperError,
ImportOperSuccess, ImportOperSuccess,
ImportableContext, ImportableContext,
} from './interfaces'; } from './interfaces';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { trimObject } from './_utils'; import { getUniqueImportableValue, trimObject } from './_utils';
import { ImportableResources } from './ImportableResources'; import { ImportableResources } from './ImportableResources';
import ResourceService from '../Resource/ResourceService'; import ResourceService from '../Resource/ResourceService';
import HasTenancyService from '../Tenancy/TenancyService'; import HasTenancyService from '../Tenancy/TenancyService';
@@ -88,7 +89,12 @@ export class ImportFileCommon {
import: importFile, import: importFile,
}; };
const transformedDTO = importable.transform(objectDTO, context); const transformedDTO = importable.transform(objectDTO, context);
const rowNumber = index + 1;
const uniqueValue = getUniqueImportableValue(importableFields, objectDTO);
const errorContext = {
rowNumber,
uniqueValue,
};
try { try {
// Validate the DTO object before passing it to the service layer. // Validate the DTO object before passing it to the service layer.
await this.importFileValidator.validateData( await this.importFileValidator.validateData(
@@ -105,18 +111,27 @@ export class ImportFileCommon {
success.push({ index, data }); success.push({ index, data });
} catch (err) { } catch (err) {
if (err instanceof ServiceError) { if (err instanceof ServiceError) {
const error = [ const error: ImportInsertError[] = [
{ {
errorCode: 'ValidationError', errorCode: 'ServiceError',
errorMessage: err.message || err.errorType, 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 }); failed.push({ index, error });
} }
} }
} catch (errors) { } catch (errors) {
const error = errors.map((er) => ({ ...er, rowNumber: index + 1 })); const error = errors.map((er) => ({ ...er, ...errorContext }));
failed.push({ index, error }); failed.push({ index, error });
} }
}; };

View File

@@ -32,10 +32,14 @@ export class ImportFileDataValidator {
try { try {
await YupSchema.validate(_data, { abortEarly: false }); await YupSchema.validate(_data, { abortEarly: false });
} catch (validationError) { } catch (validationError) {
const errors = validationError.inner.map((error) => ({ const errors = validationError.inner.reduce((errors, error) => {
const newErrors = error.errors.map((errMsg) => ({
errorCode: 'ValidationError', errorCode: 'ValidationError',
errorMessage: error.errors, errorMessage: errMsg,
})); }));
return [...errors, ...newErrors];
}, []);
throw errors; throw errors;
} }
} }

View File

@@ -1,5 +1,5 @@
import * as Yup from 'yup'; 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 pluralize from 'pluralize';
import { ResourceMetaFieldsMap } from './interfaces'; import { ResourceMetaFieldsMap } from './interfaces';
import { IModelMetaField } from '@/interfaces'; import { IModelMetaField } from '@/interfaces';
@@ -101,3 +101,24 @@ export const sanitizeResourceName = (resourceName: string) => {
export const getSheetColumns = (sheetData: unknown[]) => { export const getSheetColumns = (sheetData: unknown[]) => {
return Object.keys(first(sheetData)); 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 {<Record<string, any>}
* @returns {string}
*/
export const getUniqueImportableValue = (
importableFields: { [key: string]: IModelMetaField },
objectDTO: Record<string, any>
) => {
const uniqueImportableValue = pickBy(
importableFields,
(field) => field.unique
);
const uniqueImportableKeys = Object.keys(uniqueImportableValue);
const uniqueImportableKey = first(uniqueImportableKeys);
return defaultTo(objectDTO[uniqueImportableKey], '');
};

View File

@@ -58,7 +58,7 @@ export interface ImportOperSuccess {
} }
export interface ImportOperError { export interface ImportOperError {
error: ImportInsertError; error: ImportInsertError[];
index: number; index: number;
} }

View File

@@ -17,19 +17,13 @@
padding-left: 2rem; padding-left: 2rem;
} }
.skippedTable { table.skippedTable {
width: 100%; width: 100%;
thead{
th{
padding-top: 0;
padding-bottom: 8px;
color: #738091;
font-weight: 500;
}
}
tbody{ tbody{
tr:first-child td {
box-shadow: 0 0 0 0;
}
tr td { tr td {
vertical-align: middle; vertical-align: middle;
padding: 7px; padding: 7px;

View File

@@ -93,23 +93,12 @@ function ImportFilePreviewSkipped() {
> >
<SectionCard padded={true}> <SectionCard padded={true}>
<table className={clsx('bp4-html-table', styles.skippedTable)}> <table className={clsx('bp4-html-table', styles.skippedTable)}>
<thead>
<tr>
<th className={'number'}>#</th>
<th className={'name'}>Name</th>
<th>Error</th>
</tr>
</thead>
<tbody> <tbody>
{importPreview?.errors.map((error, key) => ( {importPreview?.errors.map((error, key) => (
<tr key={key}> <tr key={key}>
<td>{error.rowNumber}</td> <td>{error.rowNumber}</td>
<td>{error.rowNumber}</td> <td>{error.uniqueValue}</td>
<td> <td>{error.errorMessage}</td>
{error.errorMessage.map((message) => (
<div>{message}</div>
))}
</td>
</tr> </tr>
))} ))}
</tbody> </tbody>