mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 14:20:31 +00:00
feat: validate the given imported sheet whether is empty
This commit is contained in:
@@ -1,12 +1,11 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import HasTenancyService from '../Tenancy/TenancyService';
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
import { sanitizeResourceName } from './_utils';
|
import { sanitizeResourceName, validateSheetEmpty } from './_utils';
|
||||||
import ResourceService from '../Resource/ResourceService';
|
import ResourceService from '../Resource/ResourceService';
|
||||||
import { IModelMetaField } from '@/interfaces';
|
import { IModelMetaField } from '@/interfaces';
|
||||||
import { ImportFileCommon } from './ImportFileCommon';
|
import { ImportFileCommon } from './ImportFileCommon';
|
||||||
import { ImportFileDataValidator } from './ImportFileDataValidator';
|
import { ImportFileDataValidator } from './ImportFileDataValidator';
|
||||||
import { ImportFileUploadPOJO } from './interfaces';
|
import { ImportFileUploadPOJO } from './interfaces';
|
||||||
import { ServiceError } from '@/exceptions';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ImportFileUploadService {
|
export class ImportFileUploadService {
|
||||||
@@ -51,6 +50,10 @@ export class ImportFileUploadService {
|
|||||||
|
|
||||||
// Parse the buffer file to array data.
|
// Parse the buffer file to array data.
|
||||||
const sheetData = this.importFileCommon.parseXlsxSheet(buffer);
|
const sheetData = this.importFileCommon.parseXlsxSheet(buffer);
|
||||||
|
|
||||||
|
// Throws service error if the sheet data is empty.
|
||||||
|
validateSheetEmpty(sheetData);
|
||||||
|
|
||||||
const sheetColumns = this.importFileCommon.parseSheetColumns(sheetData);
|
const sheetColumns = this.importFileCommon.parseSheetColumns(sheetData);
|
||||||
const coumnsStringified = JSON.stringify(sheetColumns);
|
const coumnsStringified = JSON.stringify(sheetColumns);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { defaultTo, upperFirst, camelCase, first, isUndefined, pickBy } from 'lodash';
|
import {
|
||||||
|
defaultTo,
|
||||||
|
upperFirst,
|
||||||
|
camelCase,
|
||||||
|
first,
|
||||||
|
isUndefined,
|
||||||
|
pickBy,
|
||||||
|
isEmpty,
|
||||||
|
} 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';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import { ServiceError } from '@/exceptions';
|
||||||
|
|
||||||
export const ERRORS = {
|
export const ERRORS = {
|
||||||
RESOURCE_NOT_IMPORTABLE: 'RESOURCE_NOT_IMPORTABLE',
|
RESOURCE_NOT_IMPORTABLE: 'RESOURCE_NOT_IMPORTABLE',
|
||||||
@@ -13,6 +22,7 @@ export const ERRORS = {
|
|||||||
IMPORT_FILE_NOT_MAPPED: 'IMPORT_FILE_NOT_MAPPED',
|
IMPORT_FILE_NOT_MAPPED: 'IMPORT_FILE_NOT_MAPPED',
|
||||||
INVALID_MAP_DATE_FORMAT: 'INVALID_MAP_DATE_FORMAT',
|
INVALID_MAP_DATE_FORMAT: 'INVALID_MAP_DATE_FORMAT',
|
||||||
MAP_DATE_FORMAT_NOT_DEFINED: 'MAP_DATE_FORMAT_NOT_DEFINED',
|
MAP_DATE_FORMAT_NOT_DEFINED: 'MAP_DATE_FORMAT_NOT_DEFINED',
|
||||||
|
IMPORTED_SHEET_EMPTY: 'IMPORTED_SHEET_EMPTY',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function trimObject(obj) {
|
export function trimObject(obj) {
|
||||||
@@ -122,3 +132,13 @@ export const getUniqueImportableValue = (
|
|||||||
|
|
||||||
return defaultTo(objectDTO[uniqueImportableKey], '');
|
return defaultTo(objectDTO[uniqueImportableKey], '');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws service error the given sheet is empty.
|
||||||
|
* @param {Array<any>} sheetData
|
||||||
|
*/
|
||||||
|
export const validateSheetEmpty = (sheetData: Array<any>) => {
|
||||||
|
if (isEmpty(sheetData)) {
|
||||||
|
throw new ServiceError(ERRORS.IMPORTED_SHEET_EMPTY);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
53
packages/webapp/src/containers/Import/AlertsManager.tsx
Normal file
53
packages/webapp/src/containers/Import/AlertsManager.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { useState, createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
interface AlertsManagerContextValue {
|
||||||
|
alerts: (string | number)[];
|
||||||
|
showAlert: (alert: string | number) => void;
|
||||||
|
hideAlert: (alert: string | number) => void;
|
||||||
|
|
||||||
|
hideAlerts: () => void;
|
||||||
|
isAlertActive: (alert: string | number) => boolean;
|
||||||
|
findAlert: (alert: string | number) => string | number | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AlertsManagerContext = createContext<AlertsManagerContextValue>(
|
||||||
|
{} as AlertsManagerContextValue,
|
||||||
|
);
|
||||||
|
|
||||||
|
export function AlertsManager({ children }: { children: ReactNode }) {
|
||||||
|
const [alerts, setAlerts] = useState<(string | number)[]>([]);
|
||||||
|
|
||||||
|
const showAlert = (type: string | number): void => {
|
||||||
|
setAlerts([...alerts, type]);
|
||||||
|
};
|
||||||
|
const hideAlert = (type: string | number): void => {
|
||||||
|
alerts.filter((t) => t !== type);
|
||||||
|
};
|
||||||
|
const hideAlerts = (): void => {
|
||||||
|
setAlerts([]);
|
||||||
|
};
|
||||||
|
const isAlertActive = (type: string | number): boolean => {
|
||||||
|
return alerts.some((t) => t === type);
|
||||||
|
};
|
||||||
|
const findAlert = (type: string | number): number | string | undefined => {
|
||||||
|
return alerts.find((t) => t === type);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertsManagerContext.Provider
|
||||||
|
value={{
|
||||||
|
alerts,
|
||||||
|
showAlert,
|
||||||
|
hideAlert,
|
||||||
|
hideAlerts,
|
||||||
|
isAlertActive,
|
||||||
|
findAlert,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AlertsManagerContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAlertsManager = () => useContext(AlertsManagerContext);
|
||||||
@@ -3,8 +3,11 @@ import { Field } from 'formik';
|
|||||||
import { Box, Group, Stack } from '@/components';
|
import { Box, Group, Stack } from '@/components';
|
||||||
import styles from './ImportDropzone.module.css';
|
import styles from './ImportDropzone.module.css';
|
||||||
import { ImportDropzoneField } from './ImportDropzoneFile';
|
import { ImportDropzoneField } from './ImportDropzoneFile';
|
||||||
|
import { useAlertsManager } from './AlertsManager';
|
||||||
|
|
||||||
export function ImportDropzone() {
|
export function ImportDropzone() {
|
||||||
|
const { hideAlerts } = useAlertsManager();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={0}>
|
<Stack spacing={0}>
|
||||||
<Field id={'file'} name={'file'} type="file">
|
<Field id={'file'} name={'file'} type="file">
|
||||||
@@ -12,6 +15,7 @@ export function ImportDropzone() {
|
|||||||
<ImportDropzoneField
|
<ImportDropzoneField
|
||||||
value={form.file}
|
value={form.file}
|
||||||
onChange={(file) => {
|
onChange={(file) => {
|
||||||
|
hideAlerts();
|
||||||
form.setFieldValue('file', file);
|
form.setFieldValue('file', file);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { Intent } from '@blueprintjs/core';
|
|||||||
import { Formik, Form, FormikHelpers } from 'formik';
|
import { Formik, Form, FormikHelpers } from 'formik';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { useImportFileContext } from './ImportFileProvider';
|
import { useImportFileContext } from './ImportFileProvider';
|
||||||
import { ImportStepperStep } from './_types';
|
import { ImportAlert, ImportStepperStep } from './_types';
|
||||||
|
import { useAlertsManager } from './AlertsManager';
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
file: null,
|
file: null,
|
||||||
@@ -28,6 +29,7 @@ export function ImportFileUploadForm({
|
|||||||
formikProps,
|
formikProps,
|
||||||
formProps,
|
formProps,
|
||||||
}: ImportFileUploadFormProps) {
|
}: ImportFileUploadFormProps) {
|
||||||
|
const { showAlert, hideAlerts } = useAlertsManager();
|
||||||
const { mutateAsync: uploadImportFile } = useImportFileUpload();
|
const { mutateAsync: uploadImportFile } = useImportFileUpload();
|
||||||
const {
|
const {
|
||||||
resource,
|
resource,
|
||||||
@@ -42,6 +44,7 @@ export function ImportFileUploadForm({
|
|||||||
values: ImportFileUploadValues,
|
values: ImportFileUploadValues,
|
||||||
{ setSubmitting }: FormikHelpers<ImportFileUploadValues>,
|
{ setSubmitting }: FormikHelpers<ImportFileUploadValues>,
|
||||||
) => {
|
) => {
|
||||||
|
hideAlerts();
|
||||||
if (!values.file) return;
|
if (!values.file) return;
|
||||||
|
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
@@ -69,6 +72,9 @@ export function ImportFileUploadForm({
|
|||||||
message: 'The extenstion of uploaded file is not supported.',
|
message: 'The extenstion of uploaded file is not supported.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (data.errors.find((er) => er.type === 'IMPORTED_SHEET_EMPTY')) {
|
||||||
|
showAlert(ImportAlert.IMPORTED_SHEET_EMPTY);
|
||||||
|
}
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { Classes } from '@blueprintjs/core';
|
import { Callout, Classes, Intent } from '@blueprintjs/core';
|
||||||
import { Stack } from '@/components';
|
import { Stack } from '@/components';
|
||||||
import { ImportDropzone } from './ImportDropzone';
|
import { ImportDropzone } from './ImportDropzone';
|
||||||
import { ImportSampleDownload } from './ImportSampleDownload';
|
import { ImportSampleDownload } from './ImportSampleDownload';
|
||||||
@@ -7,29 +7,50 @@ import { ImportFileUploadForm } from './ImportFileUploadForm';
|
|||||||
import { ImportFileUploadFooterActions } from './ImportFileFooterActions';
|
import { ImportFileUploadFooterActions } from './ImportFileFooterActions';
|
||||||
import { ImportFileContainer } from './ImportFileContainer';
|
import { ImportFileContainer } from './ImportFileContainer';
|
||||||
import { useImportFileContext } from './ImportFileProvider';
|
import { useImportFileContext } from './ImportFileProvider';
|
||||||
|
import { AlertsManager, useAlertsManager } from './AlertsManager';
|
||||||
|
import { ImportAlert } from './_types';
|
||||||
|
|
||||||
|
function ImportFileUploadCallouts() {
|
||||||
|
const { isAlertActive } = useAlertsManager();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isAlertActive(ImportAlert.IMPORTED_SHEET_EMPTY) && (
|
||||||
|
<Callout intent={Intent.DANGER} icon={null}>
|
||||||
|
The imported sheet is empty.
|
||||||
|
</Callout>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function ImportFileUploadStep() {
|
export function ImportFileUploadStep() {
|
||||||
const { exampleDownload } = useImportFileContext();
|
const { exampleDownload } = useImportFileContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ImportFileUploadForm>
|
<AlertsManager>
|
||||||
<ImportFileContainer>
|
<ImportFileUploadForm>
|
||||||
<p
|
<ImportFileContainer>
|
||||||
className={Classes.TEXT_MUTED}
|
<p
|
||||||
style={{ marginBottom: 18, lineHeight: 1.6 }}
|
className={Classes.TEXT_MUTED}
|
||||||
>
|
style={{ marginBottom: 18, lineHeight: 1.6 }}
|
||||||
Download a sample file and compare it with your import file to ensure
|
>
|
||||||
it is properly formatted. It's not necessary for the columns to be in
|
Download a sample file and compare it with your import file to
|
||||||
the same order, you can map them later.
|
ensure it is properly formatted. It's not necessary for the columns
|
||||||
</p>
|
to be in the same order, you can map them later.
|
||||||
|
</p>
|
||||||
|
|
||||||
<Stack spacing={40}>
|
<Stack>
|
||||||
<ImportDropzone />
|
<ImportFileUploadCallouts />
|
||||||
{exampleDownload && <ImportSampleDownload />}
|
|
||||||
</Stack>
|
|
||||||
</ImportFileContainer>
|
|
||||||
|
|
||||||
<ImportFileUploadFooterActions />
|
<Stack spacing={40}>
|
||||||
</ImportFileUploadForm>
|
<ImportDropzone />
|
||||||
|
{exampleDownload && <ImportSampleDownload />}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</ImportFileContainer>
|
||||||
|
|
||||||
|
<ImportFileUploadFooterActions />
|
||||||
|
</ImportFileUploadForm>
|
||||||
|
</AlertsManager>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,3 +3,7 @@ export enum ImportStepperStep {
|
|||||||
Mapping = 1,
|
Mapping = 1,
|
||||||
Preview = 2,
|
Preview = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ImportAlert {
|
||||||
|
IMPORTED_SHEET_EMPTY = 'IMPORTED_SHEET_EMPTY'
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user