From 793ede341cf26c8f0fa7245280583ce6b7fe43b7 Mon Sep 17 00:00:00 2001 From: chavda-bhavik Date: Mon, 19 Feb 2024 17:53:03 +0530 Subject: [PATCH 1/4] feat: Created get-sheet-names API and updated upload API to accept user selected sheetname --- apps/api/src/app/common/common.controller.ts | 30 +++++++++++++++++-- apps/api/src/app/common/dtos/index.ts | 1 + .../src/app/common/dtos/sheet-names.dto.ts | 9 ++++++ .../get-sheet-names.command.ts | 7 +++++ .../get-sheet-names.usecase.ts | 23 ++++++++++++++ apps/api/src/app/common/usecases/index.ts | 10 +++++-- .../app/shared/services/file/file.service.ts | 14 +++++++-- .../src/app/upload/dtos/upload-request.dto.ts | 7 +++++ apps/api/src/app/upload/upload.controller.ts | 9 +++--- apps/api/src/app/upload/usecases/index.ts | 2 +- .../make-upload-entry.command.ts | 4 +++ .../make-upload-entry.usecase.ts | 12 ++++++-- 12 files changed, 113 insertions(+), 15 deletions(-) create mode 100644 apps/api/src/app/common/dtos/sheet-names.dto.ts create mode 100644 apps/api/src/app/common/usecases/get-sheet-names/get-sheet-names.command.ts create mode 100644 apps/api/src/app/common/usecases/get-sheet-names/get-sheet-names.usecase.ts diff --git a/apps/api/src/app/common/common.controller.ts b/apps/api/src/app/common/common.controller.ts index 05b1892ca..00accc3d2 100644 --- a/apps/api/src/app/common/common.controller.ts +++ b/apps/api/src/app/common/common.controller.ts @@ -1,9 +1,22 @@ -import { Body, Controller, Post, Get, UseGuards, Query, BadRequestException } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiSecurity, ApiExcludeEndpoint } from '@nestjs/swagger'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { ApiTags, ApiOperation, ApiSecurity, ApiExcludeEndpoint, ApiConsumes } from '@nestjs/swagger'; +import { + Body, + Controller, + Post, + Get, + UseGuards, + Query, + BadRequestException, + UseInterceptors, + UploadedFile, +} from '@nestjs/common'; + import { ACCESS_KEY_NAME } from '@impler/shared'; import { JwtAuthGuard } from '@shared/framework/auth.gaurd'; import { ValidRequestDto, SignedUrlDto, ImportConfigResponseDto } from './dtos'; -import { ValidRequestCommand, GetSignedUrl, ValidRequest, GetImportConfig } from './usecases'; +import { ValidImportFile } from '@shared/validations/valid-import-file.validation'; +import { ValidRequestCommand, GetSignedUrl, ValidRequest, GetImportConfig, GetSheetNames } from './usecases'; @ApiTags('Common') @Controller('/common') @@ -13,6 +26,7 @@ export class CommonController { constructor( private validRequest: ValidRequest, private getSignedUrl: GetSignedUrl, + private getSheetNames: GetSheetNames, private getImportConfig: GetImportConfig ) {} @@ -50,4 +64,14 @@ export class CommonController { return this.getImportConfig.execute(projectId); } + + @Post('/sheet-names') + @ApiOperation({ + summary: 'Get sheet names for user selected file', + }) + @ApiConsumes('multipart/form-data') + @UseInterceptors(FileInterceptor('file')) + async getSheetNamesRoute(@UploadedFile('file', ValidImportFile) file: Express.Multer.File): Promise { + return this.getSheetNames.execute({ file }); + } } diff --git a/apps/api/src/app/common/dtos/index.ts b/apps/api/src/app/common/dtos/index.ts index ace75056a..1ca28b7c1 100644 --- a/apps/api/src/app/common/dtos/index.ts +++ b/apps/api/src/app/common/dtos/index.ts @@ -1,3 +1,4 @@ export { SignedUrlDto } from './signed-url.dto'; export { ValidRequestDto } from './valid.dto'; +export { SheetNamesDto } from './sheet-names.dto'; export { ImportConfigResponseDto } from './import-config-response.dto'; diff --git a/apps/api/src/app/common/dtos/sheet-names.dto.ts b/apps/api/src/app/common/dtos/sheet-names.dto.ts new file mode 100644 index 000000000..9e89b0cef --- /dev/null +++ b/apps/api/src/app/common/dtos/sheet-names.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class SheetNamesDto { + @ApiProperty({ + type: 'file', + required: true, + }) + file: Express.Multer.File; +} diff --git a/apps/api/src/app/common/usecases/get-sheet-names/get-sheet-names.command.ts b/apps/api/src/app/common/usecases/get-sheet-names/get-sheet-names.command.ts new file mode 100644 index 000000000..29e160e5e --- /dev/null +++ b/apps/api/src/app/common/usecases/get-sheet-names/get-sheet-names.command.ts @@ -0,0 +1,7 @@ +import { IsDefined } from 'class-validator'; +import { BaseCommand } from '@shared/commands/base.command'; + +export class GetSheetNamesCommand extends BaseCommand { + @IsDefined() + file: Express.Multer.File; +} diff --git a/apps/api/src/app/common/usecases/get-sheet-names/get-sheet-names.usecase.ts b/apps/api/src/app/common/usecases/get-sheet-names/get-sheet-names.usecase.ts new file mode 100644 index 000000000..2bf2d9eb3 --- /dev/null +++ b/apps/api/src/app/common/usecases/get-sheet-names/get-sheet-names.usecase.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; +import { FileMimeTypesEnum } from '@impler/shared'; + +import { ExcelFileService } from '@shared/services/file'; +import { GetSheetNamesCommand } from './get-sheet-names.command'; +import { FileParseException } from '@shared/exceptions/file-parse-issue.exception'; + +@Injectable() +export class GetSheetNames { + async execute({ file }: GetSheetNamesCommand): Promise { + if (file.mimetype === FileMimeTypesEnum.EXCEL || file.mimetype === FileMimeTypesEnum.EXCELX) { + try { + const fileService = new ExcelFileService(); + + return await fileService.getExcelSheets(file); + } catch (error) { + throw new FileParseException(); + } + } else { + throw new Error('Invalid file type'); + } + } +} diff --git a/apps/api/src/app/common/usecases/index.ts b/apps/api/src/app/common/usecases/index.ts index 848ff3628..5d3d47c46 100644 --- a/apps/api/src/app/common/usecases/index.ts +++ b/apps/api/src/app/common/usecases/index.ts @@ -1,14 +1,18 @@ -import { GetImportConfig } from './get-import-config/get-import-config.usecase'; import { ValidRequest } from './valid-request/valid-request.usecase'; import { GetSignedUrl } from './get-signed-url/get-signed-url.usecase'; +import { GetSheetNames } from './get-sheet-names/get-sheet-names.usecase'; +import { GetImportConfig } from './get-import-config/get-import-config.usecase'; + import { ValidRequestCommand } from './valid-request/valid-request.command'; +import { GetSheetNamesCommand } from './get-sheet-names/get-sheet-names.command'; export const USE_CASES = [ ValidRequest, GetSignedUrl, + GetSheetNames, GetImportConfig, // ]; -export { GetSignedUrl, ValidRequest, GetImportConfig }; -export { ValidRequestCommand }; +export { GetSignedUrl, ValidRequest, GetImportConfig, GetSheetNames }; +export { ValidRequestCommand, GetSheetNamesCommand }; diff --git a/apps/api/src/app/shared/services/file/file.service.ts b/apps/api/src/app/shared/services/file/file.service.ts index c878b4681..ab95c1e2d 100644 --- a/apps/api/src/app/shared/services/file/file.service.ts +++ b/apps/api/src/app/shared/services/file/file.service.ts @@ -7,11 +7,11 @@ import { InvalidFileException } from '@shared/exceptions/invalid-file.exception' import { IExcelFileHeading } from '@shared/types/file.types'; export class ExcelFileService { - async convertToCsv(file: Express.Multer.File): Promise { + async convertToCsv(file: Express.Multer.File, sheetName?: string): Promise { return new Promise(async (resolve, reject) => { try { const wb = XLSX.read(file.buffer); - const ws = wb.Sheets[wb.SheetNames[0]]; + const ws = sheetName && wb.SheetNames.includes(sheetName) ? wb.Sheets[sheetName] : wb.Sheets[wb.SheetNames[0]]; resolve( XLSX.utils.sheet_to_csv(ws, { blankrows: false, @@ -119,6 +119,16 @@ export class ExcelFileService { return workbook.xlsx.writeBuffer() as Promise; } + getExcelSheets(file: Express.Multer.File): Promise { + return new Promise(async (resolve, reject) => { + try { + const wb = XLSX.read(file.buffer); + resolve(wb.SheetNames); + } catch (error) { + reject(error); + } + }); + } } export class CSVFileService2 { diff --git a/apps/api/src/app/upload/dtos/upload-request.dto.ts b/apps/api/src/app/upload/dtos/upload-request.dto.ts index bc6f85320..24c4eba1d 100644 --- a/apps/api/src/app/upload/dtos/upload-request.dto.ts +++ b/apps/api/src/app/upload/dtos/upload-request.dto.ts @@ -39,4 +39,11 @@ export class UploadRequestDto { @IsOptional() @IsJSON() extra: string; + + @ApiProperty({ + description: 'Name of the excel sheet to Import', + }) + @IsOptional() + @IsString() + selectedSheetName: string; } diff --git a/apps/api/src/app/upload/upload.controller.ts b/apps/api/src/app/upload/upload.controller.ts index e93869c80..52e62c809 100644 --- a/apps/api/src/app/upload/upload.controller.ts +++ b/apps/api/src/app/upload/upload.controller.ts @@ -40,8 +40,8 @@ import { GetOriginalFileContent, } from './usecases'; -@Controller('/upload') @ApiTags('Uploads') +@Controller('/upload') @ApiSecurity(ACCESS_KEY_NAME) @UseGuards(JwtAuthGuard) export class UploadController { @@ -68,18 +68,19 @@ export class UploadController { @ApiConsumes('multipart/form-data') @UseInterceptors(FileInterceptor('file')) async uploadFile( - @UploadedFile('file', ValidImportFile) file: Express.Multer.File, @Body() body: UploadRequestDto, - @Param('templateId', ValidateTemplate) templateId: string + @Param('templateId', ValidateTemplate) templateId: string, + @UploadedFile('file', ValidImportFile) file: Express.Multer.File ) { return this.makeUploadEntry.execute( MakeUploadEntryCommand.create({ file: file, templateId, extra: body.extra, - authHeaderValue: body.authHeaderValue, schema: body.schema, output: body.output, + authHeaderValue: body.authHeaderValue, + selectedSheetName: body.selectedSheetName, }) ); } diff --git a/apps/api/src/app/upload/usecases/index.ts b/apps/api/src/app/upload/usecases/index.ts index 7746c93f8..e1787fdc0 100644 --- a/apps/api/src/app/upload/usecases/index.ts +++ b/apps/api/src/app/upload/usecases/index.ts @@ -18,8 +18,8 @@ export const USE_CASES = [ ]; export { - MakeUploadEntry, GetUpload, + MakeUploadEntry, TerminateUpload, GetUploadColumns, GetOriginalFileContent, diff --git a/apps/api/src/app/upload/usecases/make-upload-entry/make-upload-entry.command.ts b/apps/api/src/app/upload/usecases/make-upload-entry/make-upload-entry.command.ts index 8bf89a6c2..1b8a33851 100644 --- a/apps/api/src/app/upload/usecases/make-upload-entry/make-upload-entry.command.ts +++ b/apps/api/src/app/upload/usecases/make-upload-entry/make-upload-entry.command.ts @@ -24,4 +24,8 @@ export class MakeUploadEntryCommand extends BaseCommand { @IsOptional() @IsString() authHeaderValue?: string; + + @IsString() + @IsOptional() + selectedSheetName?: string; } diff --git a/apps/api/src/app/upload/usecases/make-upload-entry/make-upload-entry.usecase.ts b/apps/api/src/app/upload/usecases/make-upload-entry/make-upload-entry.usecase.ts index d71c6a7df..a4f5f7614 100644 --- a/apps/api/src/app/upload/usecases/make-upload-entry/make-upload-entry.usecase.ts +++ b/apps/api/src/app/upload/usecases/make-upload-entry/make-upload-entry.usecase.ts @@ -38,13 +38,21 @@ export class MakeUploadEntry { private customizationRepository: CustomizationRepository ) {} - async execute({ file, templateId, extra, authHeaderValue, schema, output }: MakeUploadEntryCommand) { + async execute({ + file, + templateId, + extra, + authHeaderValue, + schema, + output, + selectedSheetName, + }: MakeUploadEntryCommand) { const fileOriginalName = file.originalname; let csvFile: string | Express.Multer.File = file; if (file.mimetype === FileMimeTypesEnum.EXCEL || file.mimetype === FileMimeTypesEnum.EXCELX) { try { const fileService = new ExcelFileService(); - csvFile = await fileService.convertToCsv(file); + csvFile = await fileService.convertToCsv(file, selectedSheetName); } catch (error) { throw new FileParseException(); } From 410a6979a7117e8586c425e33f5d98bd58d6e2d3 Mon Sep 17 00:00:00 2001 From: chavda-bhavik Date: Mon, 19 Feb 2024 17:57:09 +0530 Subject: [PATCH 2/4] feat: Added select sheet modal to widget --- .../widget/Phases/ConfirmModal/Styles.tsx | 30 ----------- .../widget/Phases/Phase1/Phase1.tsx | 28 +++++++--- .../SheetSelectModal/SheetSelectModal.tsx | 52 +++++++++++++++++++ .../Phases/Phase1/SheetSelectModal/index.ts | 1 + .../ConfirmModal/ConfirmModal.tsx | 0 .../Phases/{ => Phase3}/ConfirmModal/index.ts | 0 .../widget/Phases/Phase3/Phase3.tsx | 4 +- apps/widget/src/components/widget/Widget.tsx | 2 +- .../PromptModal/PromptModal.tsx | 6 +-- .../{Phases => modals}/PromptModal/index.ts | 0 apps/widget/src/config/texts.config.ts | 7 +++ .../src/design-system/Button/Button.tsx | 15 +++++- .../src/design-system/Select/Select.tsx | 17 +++++- apps/widget/src/hooks/Phase1/usePhase1.ts | 42 ++++++++++++--- apps/widget/src/types/component.types.ts | 3 +- packages/client/src/api/api.service.ts | 12 +++++ 16 files changed, 164 insertions(+), 55 deletions(-) delete mode 100644 apps/widget/src/components/widget/Phases/ConfirmModal/Styles.tsx create mode 100644 apps/widget/src/components/widget/Phases/Phase1/SheetSelectModal/SheetSelectModal.tsx create mode 100644 apps/widget/src/components/widget/Phases/Phase1/SheetSelectModal/index.ts rename apps/widget/src/components/widget/Phases/{ => Phase3}/ConfirmModal/ConfirmModal.tsx (100%) rename apps/widget/src/components/widget/Phases/{ => Phase3}/ConfirmModal/index.ts (100%) rename apps/widget/src/components/widget/{Phases => modals}/PromptModal/PromptModal.tsx (99%) rename apps/widget/src/components/widget/{Phases => modals}/PromptModal/index.ts (100%) diff --git a/apps/widget/src/components/widget/Phases/ConfirmModal/Styles.tsx b/apps/widget/src/components/widget/Phases/ConfirmModal/Styles.tsx deleted file mode 100644 index daa7e3403..000000000 --- a/apps/widget/src/components/widget/Phases/ConfirmModal/Styles.tsx +++ /dev/null @@ -1,30 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars, no-unused-vars */ -import { createStyles, MantineTheme } from '@mantine/core'; - -export const getActionsStyles = (theme: MantineTheme): React.CSSProperties => ({ - width: '100%', - flexDirection: 'column', - alignItems: 'stretch', - [`@media (min-width: ${theme.breakpoints.sm}px)`]: { - flexDirection: 'row', - flexWrap: 'nowrap', - }, -}); - -export const getWrapperStyles = (theme: MantineTheme): React.CSSProperties => ({ - flexDirection: 'column', - textAlign: 'center', -}); - -export const getWarningIconStyles = (theme: MantineTheme): React.CSSProperties => ({ - width: 40, - height: 40, -}); - -export default createStyles((theme: MantineTheme, params, getRef): Record => { - return { - wrapper: getWrapperStyles(theme), - actions: getActionsStyles(theme), - warning: getWarningIconStyles(theme), - }; -}); diff --git a/apps/widget/src/components/widget/Phases/Phase1/Phase1.tsx b/apps/widget/src/components/widget/Phases/Phase1/Phase1.tsx index b860f75aa..9aa39745e 100644 --- a/apps/widget/src/components/widget/Phases/Phase1/Phase1.tsx +++ b/apps/widget/src/components/widget/Phases/Phase1/Phase1.tsx @@ -1,15 +1,18 @@ -import { TEXTS, variables } from '@config'; +import { Group } from '@mantine/core'; +import { Controller } from 'react-hook-form'; + +import { Download } from '@icons'; +import { PhasesEnum } from '@types'; import { Select } from '@ui/Select'; import { Button } from '@ui/Button'; import { Dropzone } from '@ui/Dropzone'; +import { TEXTS, variables } from '@config'; import { LoadingOverlay } from '@ui/LoadingOverlay'; -import { Group } from '@mantine/core'; -import { Download } from '@icons'; +import { usePhase1 } from '@hooks/Phase1/usePhase1'; + import useStyles from './Styles'; import { Footer } from 'components/Common/Footer'; -import { usePhase1 } from '@hooks/Phase1/usePhase1'; -import { Controller } from 'react-hook-form'; -import { PhasesEnum } from '@types'; +import { SheetSelectModal } from './SheetSelectModal'; interface IPhase1Props { onNextClick: () => void; @@ -21,14 +24,17 @@ export function Phase1(props: IPhase1Props) { const { onSubmit, control, + setError, templates, onDownload, - setError, + excelSheetNames, isUploadLoading, onTemplateChange, + onSelectExcelSheet, showSelectTemplate, isInitialDataLoaded, isDownloadInProgress, + onSelectSheetModalReset, } = usePhase1({ goNext, }); @@ -93,6 +99,14 @@ export function Phase1(props: IPhase1Props) { )} /> + +