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

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'
}