mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 14:50:32 +00:00
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:
@@ -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 {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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], '');
|
||||||
|
};
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export interface ImportOperSuccess {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ImportOperError {
|
export interface ImportOperError {
|
||||||
error: ImportInsertError;
|
error: ImportInsertError[];
|
||||||
index: number;
|
index: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user