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 sample file with prefilled data #358

Merged
merged 2 commits into from
Sep 5, 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
13 changes: 11 additions & 2 deletions apps/api/src/app/shared/services/file/file.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export class ExcelFileService {

return columnName.reverse().join('');
}
getExcelFileForHeadings(headings: IExcelFileHeading[]): Promise<any> {
getExcelFileForHeadings(headings: IExcelFileHeading[], data?: Record<string, any>[]): Promise<Buffer> {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Data');
const headingNames = headings.map((heading) => heading.key);
Expand All @@ -131,7 +131,16 @@ export class ExcelFileService {
}
});

return workbook.xlsx.writeBuffer();
if (Array.isArray(data) && data.length > 0) {
const rows: string[][] = data.reduce<string[][]>((acc: string[][], rowItem: Record<string, any>) => {
acc.push(headingNames.map((headingKey) => rowItem[headingKey]));

return acc;
}, []);
worksheet.addRows(rows);
}

return workbook.xlsx.writeBuffer() as Promise<Buffer>;
}
}

Expand Down
11 changes: 11 additions & 0 deletions apps/api/src/app/template/dtos/download-sample-request.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { IsOptional, IsArray } from 'class-validator';

export class DownloadSampleDto {
@IsArray()
@IsOptional()
data: Record<string, unknown>[];

@IsArray()
@IsOptional()
schema: Record<string, unknown>[];
}
38 changes: 29 additions & 9 deletions apps/api/src/app/template/template.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Body, Controller, Delete, Get, Param, ParseArrayPipe, Post, Put, UseGuards } from '@nestjs/common';
import { Response } from 'express';
import { ApiOperation, ApiTags, ApiOkResponse, ApiSecurity, ApiBody } from '@nestjs/swagger';
import { Body, Controller, Delete, Get, Param, ParseArrayPipe, Post, Put, Res, UseGuards } from '@nestjs/common';

import { UploadEntity } from '@impler/dal';
import { ACCESS_KEY_NAME } from '@impler/shared';
Expand All @@ -8,10 +9,6 @@ import { AddColumnCommand } from 'app/column/commands/add-column.command';
import { ValidateMongoId } from '@shared/validations/valid-mongo-id.validation';
import { DocumentNotFoundException } from '@shared/exceptions/document-not-found.exception';

import { TemplateResponseDto } from './dtos/template-response.dto';
import { CreateTemplateRequestDto } from './dtos/create-template-request.dto';
import { UpdateTemplateRequestDto } from './dtos/update-template-request.dto';

import {
GetUploads,
GetTemplateColumns,
Expand All @@ -27,16 +24,22 @@ import {
UpdateCustomization,
UpdateCustomizationCommand,
GetValidations,
DownloadSample,
UpdateValidations,
UpdateValidationsCommand,
} from './usecases';
import { ColumnResponseDto } from 'app/column/dtos/column-response.dto';

import { TemplateResponseDto } from './dtos/template-response.dto';
import { ColumnRequestDto } from 'app/column/dtos/column-request.dto';
import { CustomizationResponseDto } from './dtos/customization-response.dto';
import { UpdateCustomizationRequestDto } from './dtos/update-customization-request.dto';
import { DownloadSampleDto } from './dtos/download-sample-request.dto';
import { ColumnResponseDto } from 'app/column/dtos/column-response.dto';
import { ValidationsResponseDto } from './dtos/validations-response.dto';
import { UpdateValidationsRequestDto } from './dtos/update-validations-request.dto';
import { CustomizationResponseDto } from './dtos/customization-response.dto';
import { CreateTemplateRequestDto } from './dtos/create-template-request.dto';
import { UpdateTemplateRequestDto } from './dtos/update-template-request.dto';
import { UpdateValidationResponseDto } from './dtos/update-validation-response.dto';
import { UpdateValidationsRequestDto } from './dtos/update-validations-request.dto';
import { UpdateCustomizationRequestDto } from './dtos/update-customization-request.dto';

@Controller('/template')
@ApiTags('Template')
Expand All @@ -47,6 +50,7 @@ export class TemplateController {
private getTemplateColumns: GetTemplateColumns,
private getUploads: GetUploads,
private getValidations: GetValidations,
private downloadSample: DownloadSample,
private getCustomization: GetCustomization,
private updateValidations: UpdateValidations,
private updateCustomization: UpdateCustomization,
Expand All @@ -70,6 +74,22 @@ export class TemplateController {
return this.getTemplateDetails.execute(templateId);
}

@Post(':templateId/sample')
@ApiOperation({
summary: 'Get Template Sample',
})
async downloadSampleRoute(
@Param('templateId', ValidateMongoId) templateId: string,
@Body() data: DownloadSampleDto,
@Res() res: Response
) {
const buffer = await this.downloadSample.execute(templateId, data);
res.header('Content-disposition', 'attachment; filename=anlikodullendirme.xlsx');
res.type('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');

return res.send(buffer);
}

@Get(':templateId/columns')
@ApiOperation({
summary: 'Get template columns',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IsOptional, IsArray } from 'class-validator';
import { BaseCommand } from '@shared/commands/base.command';

export class DownloadSampleCommand extends BaseCommand {
@IsArray()
@IsOptional()
data: Record<string, unknown>[];

@IsArray()
@IsOptional()
schema: Record<string, unknown>[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Injectable } from '@nestjs/common';
import { ColumnRepository } from '@impler/dal';
import { ColumnTypesEnum } from '@impler/shared';
import { ExcelFileService } from '@shared/services/file';
import { IExcelFileHeading } from '@shared/types/file.types';
import { DownloadSampleCommand } from './download-sample.command';

@Injectable()
export class DownloadSample {
constructor(private columnsRepository: ColumnRepository, private excelFileService: ExcelFileService) {}

async execute(_templateId: string, data: DownloadSampleCommand): Promise<Buffer> {
const columns = await this.columnsRepository.find(
{
_templateId,
},
'key type selectValues isRequired'
);
const columnKeys: IExcelFileHeading[] = columns.map((columnItem) => ({
key: columnItem.key,
type: columnItem.type as ColumnTypesEnum,
selectValues: columnItem.selectValues,
isRequired: columnItem.isRequired,
}));

return await this.excelFileService.getExcelFileForHeadings(columnKeys, data.data);
}
}
3 changes: 3 additions & 0 deletions apps/api/src/app/template/usecases/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { GetTemplateColumns } from './get-columns/get-columns.usecase';
import { CreateTemplate } from './create-template/create-template.usecase';
import { UpdateTemplate } from './update-template/update-template.usecase';
import { DeleteTemplate } from './delete-template/delete-template.usecase';
import { DownloadSample } from './download-sample/download-sample.usecase';
import { GetTemplateDetails } from './get-template-details/get-template-details.usecase';
import { UpdateTemplateColumns } from './update-template-columns/update-template-columns.usecase';
import { UpdateCustomization } from './update-customization/update-customization.usecase';
Expand Down Expand Up @@ -30,6 +31,7 @@ export const USE_CASES = [
GetValidations,
UpdateValidations,
SaveSampleFile,
DownloadSample,
//
];

Expand All @@ -44,6 +46,7 @@ export {
UpdateCustomization,
GetCustomization,
GetValidations,
DownloadSample,
UpdateValidations,
};
export {
Expand Down
1 change: 1 addition & 0 deletions apps/widget/src/components/Common/Container/Container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export function Container({ children }: PropsWithChildren<{}>) {
api={api}
// impler-context
projectId={projectId}
data={secondaryPayload.data}
templateId={secondaryPayload.templateId}
accessToken={primaryPayload.accessToken}
authHeaderValue={secondaryPayload?.authHeaderValue}
Expand Down
9 changes: 6 additions & 3 deletions apps/widget/src/components/Common/Provider/Provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import APIContextProvider from '@store/api.context';
import AppContextProvider from '@store/app.context';

interface IProviderProps {
// app-context
title?: string;
primaryColor: string;
data?: Record<string, string | number>[];
// api-context
api: ApiService;
// impler-context
Expand All @@ -14,11 +17,11 @@ interface IProviderProps {
accessToken?: string;
extra?: string;
authHeaderValue?: string;
primaryColor: string;
}

export function Provider(props: PropsWithChildren<IProviderProps>) {
const { api, title, projectId, templateId, accessToken, extra, authHeaderValue, children, primaryColor } = props;
const { api, data, title, projectId, templateId, accessToken, extra, authHeaderValue, children, primaryColor } =
props;

return (
<ImplerContextProvider
Expand All @@ -29,7 +32,7 @@ export function Provider(props: PropsWithChildren<IProviderProps>) {
authHeaderValue={authHeaderValue}
>
<APIContextProvider api={api}>
<AppContextProvider title={title} primaryColor={primaryColor}>
<AppContextProvider title={title} primaryColor={primaryColor} data={data}>
{children}
</AppContextProvider>
</APIContextProvider>
Expand Down
35 changes: 28 additions & 7 deletions apps/widget/src/hooks/Phase1/usePhase1.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useImplerState } from '@store/impler.context';
import { logAmplitudeEvent } from '@amplitude';
import { useMutation, useQuery } from '@tanstack/react-query';

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

interface IUsePhase1Props {
goNext: () => void;
}

export function usePhase1({ goNext }: IUsePhase1Props) {
const { api } = useAPIState();
const { setUploadInfo, setTemplateInfo } = useAppState();
const { setUploadInfo, setTemplateInfo, data } = useAppState();
const [templates, setTemplates] = useState<IOption[]>([]);
const [isDownloadInProgress, setIsDownloadInProgress] = useState<boolean>(false);
const { projectId, templateId, authHeaderValue, extra } = useImplerState();
Expand Down Expand Up @@ -69,6 +70,24 @@ export function usePhase1({ goNext }: IUsePhase1Props) {
},
}
);
const { mutate: downloadSample } = useMutation<
ArrayBuffer,
IErrorObject,
[string, Record<string, string | number>[] | undefined, string]
>(
['downloadSample'],
([providedTemplateId, prefilledData]) => api.downloadSample(providedTemplateId, prefilledData),
{
onSuccess(excelFileData, queryVariables) {
downloadFile(
new Blob([excelFileData], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
}),
queryVariables[variables.secondIndex] as string
);
},
}
);
const {
control,
register,
Expand Down Expand Up @@ -114,7 +133,9 @@ export function usePhase1({ goNext }: IUsePhase1Props) {
}

const foundTemplate = findTemplate();
if (foundTemplate && foundTemplate.sampleFileUrl) {
if (foundTemplate && Array.isArray(data) && data.length > variables.baseIndex) {
downloadSample([foundTemplate._id, data, foundTemplate.name + '.xlsx']);
} else if (foundTemplate && foundTemplate.sampleFileUrl) {
getSignedUrl([foundTemplate.sampleFileUrl, foundTemplate.name + ' (sample).xlsx']);
}
setIsDownloadInProgress(false);
Expand Down
4 changes: 2 additions & 2 deletions apps/widget/src/store/app.context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interface AppContextProviderProps

const AppContext = createContext<IAppStore | null>(null);

const AppContextProvider = ({ children, primaryColor, title }: AppContextProviderProps) => {
const AppContextProvider = ({ children, primaryColor, title, data }: AppContextProviderProps) => {
const [templateInfo, setTemplateInfo] = useState<ITemplate>({} as ITemplate);
const [uploadInfo, setUploadInfo] = useState<IUpload>({} as IUpload);

Expand All @@ -18,7 +18,7 @@ const AppContextProvider = ({ children, primaryColor, title }: AppContextProvide

return (
<AppContext.Provider
value={{ title, templateInfo, setTemplateInfo, uploadInfo, setUploadInfo, reset, primaryColor }}
value={{ title, templateInfo, setTemplateInfo, uploadInfo, setUploadInfo, reset, primaryColor, data }}
>
{children}
</AppContext.Provider>
Expand Down
1 change: 1 addition & 0 deletions apps/widget/src/types/store.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface IApiStore {

export interface IAppStore {
title?: string;
data?: Record<string, string | number>[];
templateInfo: ITemplate;
uploadInfo: IUpload;
reset: () => void;
Expand Down
22 changes: 13 additions & 9 deletions apps/widget/src/util/helpers/common.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,26 @@ 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,
method: 'GET',
// headers: headers,
responseType: 'blob', // important
}).then((response) => {
const url = window.URL.createObjectURL(new Blob([response.data]));
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);
downloadFile(new Blob([response.data]), name);

return response;
});
Expand Down
4 changes: 2 additions & 2 deletions libs/shared/src/services/http-client/api.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ export class HttpClient {
return this.callWrapper(this.axiosClient.get.bind(this, url, { params }));
}

async post(url: string, body = {}, headers: AxiosRequestHeaders = {}) {
return this.callWrapper(this.axiosClient.post.bind(this, url, body, { headers }));
async post(url: string, body = {}, headers: AxiosRequestHeaders = {}, responseType?: XMLHttpRequestResponseType) {
return this.callWrapper(this.axiosClient.post.bind(this, url, body, { headers, responseType }));
}

async patch(url: string, body = {}) {
Expand Down
1 change: 1 addition & 0 deletions libs/shared/src/types/widget/widget.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface IShowPayload {
primaryColor?: string;
colorScheme?: string;
title?: string;
data?: Record<string, string | any>[];
}
export interface IOption {
value: string;
Expand Down
6 changes: 1 addition & 5 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,7 @@
"fix:prettier": "prettier \"src/**/*.ts\" --write",
"fix:lint": "eslint src --ext .ts --fix",
"test:prettier": "prettier \"src/**/*.ts\"",
"watch:build": "tsc -p tsconfig.json -w",
"doc": "run-s doc:html && open-cli build/docs/index.html",
"doc:html": "typedoc src/ --exclude **/*.spec.ts --target ES6 --mode file --out build/docs",
"doc:json": "typedoc src/ --exclude **/*.spec.ts --target ES6 --mode file --json build/docs/typedoc.json",
"doc:publish": "gh-pages -m \"[ci skip] Updates\" -d build/docs"
"watch:build": "tsc -p tsconfig.json -w"
},
"devDependencies": {
"@types/node": "^18.11.9",
Expand Down
Loading