Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat download original file #381

Merged
merged 3 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/api/src/app/shared/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const APIMessages = {
FILE_TYPE_NOT_VALID: 'File type is not supported.',
FILE_IS_EMPTY: 'File is empty',
FILE_CONTENT_INVALID: 'File content is invalid. Please check the file or upload new file.',
FILE_HAS_ISSUE: 'Uploaded file has some issues. Please check the file or upload new file.',
EMPTY_HEADING_PREFIX: 'Empty Heading',
INVALID_TEMPLATE_ID_CODE_SUFFIX: 'is not valid TemplateId or CODE.',
FILE_MAPPING_REMAINING: 'File mapping is not yet done, please finalize mapping before.',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { BadRequestException } from '@nestjs/common';
import { APIMessages } from '../constants';

export class FileParseError extends BadRequestException {
constructor() {
super(APIMessages.FILE_HAS_ISSUE);
}
}
12 changes: 8 additions & 4 deletions apps/api/src/app/shared/services/file/file.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ import { IExcelFileHeading } from '@shared/types/file.types';

export class ExcelFileService {
async convertToCsv(file: Express.Multer.File): Promise<string> {
return new Promise(async (resolve) => {
const wb = XLSX.read(file.buffer);
const ws = wb.Sheets[wb.SheetNames[0]];
resolve(XLSX.utils.sheet_to_csv(ws));
return new Promise(async (resolve, reject) => {
try {
const wb = XLSX.read(file.buffer);
const ws = wb.Sheets[wb.SheetNames[0]];
resolve(XLSX.utils.sheet_to_csv(ws));
} catch (error) {
reject(error);
}
});
}
formatName(name: string): string {
Expand Down
3 changes: 1 addition & 2 deletions apps/api/src/app/upload/upload.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,9 @@ export class UploadController {
})
async getOriginalFile(@Param('uploadId', ValidateMongoId) uploadId: string, @Res() res: Response) {
const { name, content, type } = await this.getOriginalFileContent.execute(uploadId);

res.setHeader('Content-Type', type);
res.setHeader('Content-Disposition', 'attachment; filename=' + name);
res.send(content);
content.pipe(res);
}

@Get(':uploadId/rows/valid')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Readable } from 'stream';
import { Injectable } from '@nestjs/common';
import { UploadRepository } from '@impler/dal';
import { FileNameService } from '@shared/services';
Expand All @@ -12,12 +13,12 @@ export class GetOriginalFileContent {
private fileNameService: FileNameService
) {}

async execute(_uploadId: string): Promise<{ name: string; content: string; type: string }> {
async execute(_uploadId: string): Promise<{ name: string; content: Readable; type: string }> {
const upload = await this.uploadRepository.findById(_uploadId, 'originalFileName originalFileType');
if (!upload) {
throw new DocumentNotFoundException('Upload', _uploadId);
}
const content = await this.storageService.getFileContent(
const content = await this.storageService.getFileStream(
this.fileNameService.getOriginalFilePath(_uploadId, upload.originalFileName)
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { AddUploadEntryCommand } from './add-upload-entry.command';
import { MakeUploadEntryCommand } from './make-upload-entry.command';
import { StorageService } from '@impler/shared/dist/services/storage';
import { FileParseError } from '@shared/exceptions/file-parse-issue.exception';
import { CSVFileService2, ExcelFileService, FileNameService } from '@shared/services/file';

@Injectable()
Expand All @@ -37,8 +38,12 @@ export class MakeUploadEntry {
const fileOriginalName = file.originalname;
let csvFile: string | Express.Multer.File = file;
if (file.mimetype === FileMimeTypesEnum.EXCEL || file.mimetype === FileMimeTypesEnum.EXCELX) {
const fileService = new ExcelFileService();
csvFile = await fileService.convertToCsv(file);
try {
const fileService = new ExcelFileService();
csvFile = await fileService.convertToCsv(file);
} catch (error) {
throw new FileParseError();
}
} else if (file.mimetype === FileMimeTypesEnum.CSV) {
csvFile = file;
} else {
Expand Down
19 changes: 19 additions & 0 deletions apps/web/assets/icons/Download.icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { IconType } from '@types';
import { IconSizes } from 'config';

export const DownloadIcon = ({ size = 'sm', color }: IconType) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width={IconSizes[size]}
height={IconSizes[size]}
color={color}
>
<path
d="M1 14.5C1 12.1716 2.22429 10.1291 4.06426 8.9812C4.56469 5.044 7.92686 2 12 2C16.0731 2 19.4353 5.044 19.9357 8.9812C21.7757 10.1291 23 12.1716 23 14.5C23 17.9216 20.3562 20.7257 17 20.9811L7 21C3.64378 20.7257 1 17.9216 1 14.5ZM16.8483 18.9868C19.1817 18.8093 21 16.8561 21 14.5C21 12.927 20.1884 11.4962 18.8771 10.6781L18.0714 10.1754L17.9517 9.23338C17.5735 6.25803 15.0288 4 12 4C8.97116 4 6.42647 6.25803 6.0483 9.23338L5.92856 10.1754L5.12288 10.6781C3.81156 11.4962 3 12.927 3 14.5C3 16.8561 4.81833 18.8093 7.1517 18.9868L7.325 19H16.675L16.8483 18.9868ZM13 12H16L12 17L8 12H11V8H13V12Z"
fill="currentColor"
/>
</svg>
);
};
35 changes: 31 additions & 4 deletions apps/web/components/import-feed/History.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import { ChangeEvent } from 'react';
import { Badge, Group, LoadingOverlay, Stack, Title } from '@mantine/core';
import { ActionIcon, Badge, Group, LoadingOverlay, Stack, Title, Tooltip } from '@mantine/core';

import { Input } from '@ui/input';
import { Table } from '@ui/table';
import { VARIABLES } from '@config';
import { VARIABLES, colors } from '@config';
import { Pagination } from '@ui/pagination';
import { DateInput } from '@ui/date-input';
import { useHistory } from '@hooks/useHistory';
import { IHistoryRecord } from '@impler/shared';
import { SearchIcon } from '@assets/icons/Search.icon';
import { DownloadIcon } from '@assets/icons/Download.icon';

export function History() {
const { historyData, isHistoryDataLoading, onLimitChange, onPageChange, onNameChange, onDateChange, name, date } =
useHistory();
const {
historyData,
downloadOriginalFile,
isHistoryDataLoading,
onLimitChange,
onPageChange,
onNameChange,
onDateChange,
name,
date,
} = useHistory();

return (
<Stack spacing="xs">
Expand Down Expand Up @@ -82,6 +92,23 @@ export function History() {
title: 'Records',
key: 'totalRecords',
},
{
title: 'Download Original File',
key: 'download',
width: 100,
Cell(item) {
return item.originalFileName ? (
<Tooltip label="Download original file" withArrow>
<ActionIcon
variant="transparent"
onClick={() => downloadOriginalFile([item._id, item.originalFileName])}
>
<DownloadIcon color={colors.yellow} />
</ActionIcon>
</Tooltip>
) : null;
},
},
]}
data={historyData?.data || []}
/>
Expand Down
3 changes: 2 additions & 1 deletion apps/web/config/constants.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ export const API_KEYS = {
LOGOUT: 'LOGOUT',
SIGNIN: 'SIGNIN',
SIGNUP: 'SIGNUP',
REQUEST_FORGOT_PASSWORD: 'REQUEST_FORGOT_PASSWORD',
RESET_PASSWORD: 'RESET_PASSWORD',
REQUEST_FORGOT_PASSWORD: 'REQUEST_FORGOT_PASSWORD',

TEMPLATES_LIST: 'TEMPLATES_LIST',
TEMPLATES_CREATE: 'TEMPLATES_CREATE',
Expand All @@ -74,6 +74,7 @@ export const API_KEYS = {

ME: 'ME',
REGENERATE: 'REGENERATE',
DONWLOAD_ORIGINAL_FILE: 'DOWNLOAD_ORIGINAL_FILE',
};

export const NOTIFICATION_KEYS = {
Expand Down
21 changes: 18 additions & 3 deletions apps/web/hooks/useHistory.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useMutation, useQuery } from '@tanstack/react-query';
import { useDebouncedState } from '@mantine/hooks';

import { commonApi } from '@libs/api';
import { track } from '@libs/amplitude';
import { API_KEYS, VARIABLES } from '@config';
import { IErrorObject, IHistoryData } from '@impler/shared';
import { useAppState } from 'store/app.context';
import { IErrorObject, IHistoryData, downloadFile } from '@impler/shared';

export function useHistory() {
const { profileInfo } = useAppState();
const [date, setDate] = useState<Date>();
const [limit, setLimit] = useState<number>(VARIABLES.TEN);
const [page, setPage] = useState<number>();
const [limit, setLimit] = useState<number>(VARIABLES.TEN);
const [name, setName] = useDebouncedState('', VARIABLES.TWO_HUNDREDS);
const { isLoading: isDownloadOriginalFileLoading, mutate: downloadOriginalFile } = useMutation<
ArrayBuffer,
IErrorObject,
[string, string]
>(
['downloadOriginal'],
([uploadId]) => commonApi(API_KEYS.DONWLOAD_ORIGINAL_FILE as any, { parameters: [uploadId] }),
{
onSuccess(excelFileData, variables) {
downloadFile(new Blob([excelFileData]), variables[1]);
},
}
);
const { isLoading: isHistoryDataLoading, data: historyData } = useQuery<
unknown,
IErrorObject,
Expand Down Expand Up @@ -74,7 +87,9 @@ export function useHistory() {
onNameChange,
onPageChange: setPage,
onLimitChange,
downloadOriginalFile,
isHistoryDataLoading,
isDownloadOriginalFileLoading,
historyData,
name,
date,
Expand Down
9 changes: 8 additions & 1 deletion apps/web/libs/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ const routes: Record<string, Route> = {
url: (templateId) => `/v1/template/${templateId}/validations`,
method: 'PUT',
},
[API_KEYS.DONWLOAD_ORIGINAL_FILE]: {
url: (uploadId) => `/v1/upload/${uploadId}/files/original`,
method: 'GET',
},
[API_KEYS.ME]: {
url: () => `/v1/auth/me`,
method: 'GET',
Expand All @@ -120,7 +124,10 @@ function handleResponseStatusAndContentType(response: Response) {
if (contentType === null) return Promise.resolve(null);
else if (contentType.startsWith('application/json;')) return response.json();
else if (contentType.startsWith('text/plain;')) return response.text();
else throw new Error(`Unsupported response content-type: ${contentType}`);
else if (contentType.startsWith('text/csv')) return response.text();
else if (contentType.startsWith('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')) {
return response.arrayBuffer();
} else throw new Error(`Unsupported response content-type: ${contentType}`);
}

function queryObjToString(obj?: Record<string, string | number | undefined>): string {
Expand Down
3 changes: 2 additions & 1 deletion apps/widget/src/hooks/Phase1/usePhase1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { logAmplitudeEvent } from '@amplitude';
import { useMutation, useQuery } from '@tanstack/react-query';

import { variables } from '@config';
import { downloadFile } from '@impler/shared';
import { useAPIState } from '@store/api.context';
import { useAppState } from '@store/app.context';
import { IFormvalues, IUploadValues } from '@types';
import { useImplerState } from '@store/impler.context';
import { IErrorObject, IOption, ITemplate, IUpload } from '@impler/shared';
import { downloadFile, downloadFileFromURL, getFileNameFromUrl, notifier, ParentWindow } from '@util';
import { downloadFileFromURL, getFileNameFromUrl, notifier, ParentWindow } from '@util';

interface IUsePhase1Props {
goNext: () => void;
Expand Down
13 changes: 1 addition & 12 deletions apps/widget/src/util/helpers/common.helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import axios from 'axios';
import { variables } from '@config';
import { downloadFile } from '@impler/shared';

// eslint-disable-next-line no-magic-numbers
export function formatBytes(bytes, decimals = 2) {
Expand All @@ -26,18 +27,6 @@ function isValidHttpUrl(string: string) {
return url.protocol === 'http:' || url.protocol === 'https:';
}

export function downloadFile(blob: Blob, name: string) {
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', name);
document.body.appendChild(link);
link.click();

// Clean up and remove the link
link.parentNode?.removeChild(link);
}

function fetchFile(urlToFetch: string, name: string) {
axios({
url: urlToFetch,
Expand Down
1 change: 1 addition & 0 deletions libs/dal/src/repositories/upload/upload.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export class UploadRepository extends BaseRepository<UploadEntity> {
uploadedDate: 1,
totalRecords: 1,
validRecords: 1,
originalFileName: 1,
status: 1,
_template: {
name: 1,
Expand Down
1 change: 1 addition & 0 deletions libs/shared/src/entities/Activity/Activity.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface IHistoryRecord {
status: string;
uploadedDate: string;
validRecords: number;
originalFileName: string;
name: string;
}

Expand Down
12 changes: 12 additions & 0 deletions libs/shared/src/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,15 @@ export function replaceVariablesInString(str: string, obj: Record<string, string
return typeof value === 'string' || typeof value === 'number' ? value : a;
});
}

export function downloadFile(blob: Blob, name: string) {
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', name);
document.body.appendChild(link);
link.click();

// Clean up and remove the link
link.parentNode?.removeChild(link);
}
Loading
Loading