Skip to content

Commit

Permalink
Merge pull request #395 from bigcapitalhq/validate-imported-sheet-empty
Browse files Browse the repository at this point in the history
feat: validate the given imported sheet whether is empty
  • Loading branch information
abouolia authored Apr 1, 2024
2 parents 291301c + 80c14ba commit 1a8ca83
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 22 deletions.
7 changes: 5 additions & 2 deletions packages/server/src/services/Import/ImportFileUpload.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
import { sanitizeResourceName } from './_utils';
import { sanitizeResourceName, validateSheetEmpty } from './_utils';
import ResourceService from '../Resource/ResourceService';
import { IModelMetaField } from '@/interfaces';
import { ImportFileCommon } from './ImportFileCommon';
import { ImportFileDataValidator } from './ImportFileDataValidator';
import { ImportFileUploadPOJO } from './interfaces';
import { ServiceError } from '@/exceptions';

@Service()
export class ImportFileUploadService {
Expand Down Expand Up @@ -51,6 +50,10 @@ export class ImportFileUploadService {

// Parse the buffer file to array data.
const sheetData = this.importFileCommon.parseXlsxSheet(buffer);

// Throws service error if the sheet data is empty.
validateSheetEmpty(sheetData);

const sheetColumns = this.importFileCommon.parseSheetColumns(sheetData);
const coumnsStringified = JSON.stringify(sheetColumns);

Expand Down
20 changes: 19 additions & 1 deletion packages/server/src/services/Import/_utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
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 { ResourceMetaFieldsMap } from './interfaces';
import { IModelMetaField } from '@/interfaces';
import moment from 'moment';
import { ServiceError } from '@/exceptions';

export const ERRORS = {
RESOURCE_NOT_IMPORTABLE: 'RESOURCE_NOT_IMPORTABLE',
Expand Down Expand Up @@ -124,6 +133,15 @@ export const getUniqueImportableValue = (
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);
}

const booleanValuesRepresentingTrue: string[] = ['true', 'yes', 'y', 't', '1'];
const booleanValuesRepresentingFalse: string[] = ['false', 'no', 'n', 'f', '0'];

Expand Down
53 changes: 53 additions & 0 deletions packages/webapp/src/containers/Import/AlertsManager.tsx
Original file line number Diff line number Diff line change
@@ -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);
4 changes: 4 additions & 0 deletions packages/webapp/src/containers/Import/ImportDropzone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@ import { Field } from 'formik';
import { Box, Group, Stack } from '@/components';
import styles from './ImportDropzone.module.css';
import { ImportDropzoneField } from './ImportDropzoneFile';
import { useAlertsManager } from './AlertsManager';

export function ImportDropzone() {
const { hideAlerts } = useAlertsManager();

return (
<Stack spacing={0}>
<Field id={'file'} name={'file'} type="file">
{({ form }) => (
<ImportDropzoneField
value={form.file}
onChange={(file) => {
hideAlerts();
form.setFieldValue('file', file);
}}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { Intent } from '@blueprintjs/core';
import { Formik, Form, FormikHelpers } from 'formik';
import * as Yup from 'yup';
import { useImportFileContext } from './ImportFileProvider';
import { ImportStepperStep } from './_types';
import { ImportAlert, ImportStepperStep } from './_types';
import { useAlertsManager } from './AlertsManager';

const initialValues = {
file: null,
Expand All @@ -28,6 +29,7 @@ export function ImportFileUploadForm({
formikProps,
formProps,
}: ImportFileUploadFormProps) {
const { showAlert, hideAlerts } = useAlertsManager();
const { mutateAsync: uploadImportFile } = useImportFileUpload();
const {
resource,
Expand All @@ -42,6 +44,7 @@ export function ImportFileUploadForm({
values: ImportFileUploadValues,
{ setSubmitting }: FormikHelpers<ImportFileUploadValues>,
) => {
hideAlerts();
if (!values.file) return;

setSubmitting(true);
Expand Down Expand Up @@ -69,6 +72,9 @@ export function ImportFileUploadForm({
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);
});
};
Expand Down
57 changes: 39 additions & 18 deletions packages/webapp/src/containers/Import/ImportFileUploadStep.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,56 @@
// @ts-nocheck
import { Classes } from '@blueprintjs/core';
import { Callout, Classes, Intent } from '@blueprintjs/core';
import { Stack } from '@/components';
import { ImportDropzone } from './ImportDropzone';
import { ImportSampleDownload } from './ImportSampleDownload';
import { ImportFileUploadForm } from './ImportFileUploadForm';
import { ImportFileUploadFooterActions } from './ImportFileFooterActions';
import { ImportFileContainer } from './ImportFileContainer';
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() {
const { exampleDownload } = useImportFileContext();

return (
<ImportFileUploadForm>
<ImportFileContainer>
<p
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
the same order, you can map them later.
</p>
<AlertsManager>
<ImportFileUploadForm>
<ImportFileContainer>
<p
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 the same order, you can map them later.
</p>

<Stack>
<ImportFileUploadCallouts />

<Stack spacing={40}>
<ImportDropzone />
{exampleDownload && <ImportSampleDownload />}
</Stack>
</ImportFileContainer>
<Stack spacing={40}>
<ImportDropzone />
{exampleDownload && <ImportSampleDownload />}
</Stack>
</Stack>
</ImportFileContainer>

<ImportFileUploadFooterActions />
</ImportFileUploadForm>
<ImportFileUploadFooterActions />
</ImportFileUploadForm>
</AlertsManager>
);
}
4 changes: 4 additions & 0 deletions packages/webapp/src/containers/Import/_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ export enum ImportStepperStep {
Mapping = 1,
Preview = 2,
}

export enum ImportAlert {
IMPORTED_SHEET_EMPTY = 'IMPORTED_SHEET_EMPTY'
}

0 comments on commit 1a8ca83

Please sign in to comment.