feat: validate the given imported sheet whether is empty

This commit is contained in:
Ahmed Bouhuolia
2024-04-01 02:57:30 +02:00
parent 22a016b56e
commit 785045dbad
7 changed files with 133 additions and 22 deletions

View File

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

View File

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

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

View File

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

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

View File

@@ -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 (
<AlertsManager>
<ImportFileUploadForm> <ImportFileUploadForm>
<ImportFileContainer> <ImportFileContainer>
<p <p
className={Classes.TEXT_MUTED} className={Classes.TEXT_MUTED}
style={{ marginBottom: 18, lineHeight: 1.6 }} style={{ marginBottom: 18, lineHeight: 1.6 }}
> >
Download a sample file and compare it with your import file to ensure Download a sample file and compare it with your import file to
it is properly formatted. It's not necessary for the columns to be in ensure it is properly formatted. It's not necessary for the columns
the same order, you can map them later. to be in the same order, you can map them later.
</p> </p>
<Stack>
<ImportFileUploadCallouts />
<Stack spacing={40}> <Stack spacing={40}>
<ImportDropzone /> <ImportDropzone />
{exampleDownload && <ImportSampleDownload />} {exampleDownload && <ImportSampleDownload />}
</Stack> </Stack>
</Stack>
</ImportFileContainer> </ImportFileContainer>
<ImportFileUploadFooterActions /> <ImportFileUploadFooterActions />
</ImportFileUploadForm> </ImportFileUploadForm>
</AlertsManager>
); );
} }

View File

@@ -3,3 +3,7 @@ export enum ImportStepperStep {
Mapping = 1, Mapping = 1,
Preview = 2, Preview = 2,
} }
export enum ImportAlert {
IMPORTED_SHEET_EMPTY = 'IMPORTED_SHEET_EMPTY'
}