Skip to content
Merged
4 changes: 2 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 @@ -10,10 +10,10 @@ import { InvalidFileException } from '@shared/exceptions/invalid-file.exception'
import { IExcelFileHeading } from '@shared/types/file.types';

export class ExcelFileService {
async convertToCsv(file: Express.Multer.File, sheetName?: string): Promise<string> {
async convertToCsv(file: Express.Multer.File, sheetName?: string, dateNF?: string): Promise<string> {
return new Promise(async (resolve, reject) => {
try {
const wb = XLSX.read(file.buffer as any);
const wb = XLSX.read(file.buffer as any, { dateNF });
const ws = sheetName && wb.SheetNames.includes(sheetName) ? wb.Sheets[sheetName] : wb.Sheets[wb.SheetNames[0]];
resolve(
XLSX.utils.sheet_to_csv(ws, {
Expand Down
7 changes: 7 additions & 0 deletions apps/api/src/app/template/dtos/template-response.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,11 @@ export class TemplateResponseDto {
@IsString()
@IsDefined()
mode: string;

@ApiPropertyOptional({
description: 'Expected Date Format',
})
@IsString()
@IsOptional()
expectedDateFormat?: string;
}
7 changes: 7 additions & 0 deletions apps/api/src/app/template/dtos/update-template-request.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,11 @@ export class UpdateTemplateRequestDto {
@IsOptional()
@IsEnum(IntegrationEnum)
integration?: IntegrationEnum;

@ApiProperty({
description: 'Expected Date Format',
nullable: true,
})
@IsOptional()
expectedDateFormat?: string;
}
1 change: 1 addition & 0 deletions apps/api/src/app/template/template.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ export class TemplateController {
UpdateTemplateCommand.create({
name: body.name,
mode: body.mode,
expectedDateFormat: body.expectedDateFormat,
}),
templateId
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class GetTemplateDetails {
}): Promise<TemplateResponseDto> {
const template = await this.templateRepository.findOne(
{ _id: _templateId, _projectId },
'_projectId name sampleFileUrl _id totalUploads totalInvalidRecords totalRecords mode integration'
'_projectId name sampleFileUrl _id totalUploads totalInvalidRecords totalRecords mode integration expectedDateFormat'
);
if (!template) {
throw new DocumentNotFoundException('Template', _templateId);
Expand All @@ -33,6 +33,7 @@ export class GetTemplateDetails {
totalRecords: template.totalRecords,
mode: template.mode,
integration: template.integration as IntegrationEnum,
expectedDateFormat: template.expectedDateFormat,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@ export class UpdateTemplateCommand extends BaseCommand {
@IsEnum(IntegrationEnum)
@IsOptional()
integration?: IntegrationEnum;

@IsString()
@IsOptional()
expectedDateFormat?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export class MakeUploadEntry {
file.mimetype === FileMimeTypesEnum.EXCELM
) {
try {
const template = await this.templateRepository.findOne({ _id: templateId }, 'expectedDateFormat');
const fileService = new ExcelFileService();
const opts = await fileService.getExcelRowsColumnsCount(file, selectedSheetName);

Expand All @@ -77,7 +78,7 @@ export class MakeUploadEntry {
}

this.analyzeLargeFile(opts, true, maxRecords);
csvFile = await fileService.convertToCsv(file, selectedSheetName);
csvFile = await fileService.convertToCsv(file, selectedSheetName, template?.expectedDateFormat);
} catch (error) {
if (error instanceof FileSizeException || error instanceof MaxRecordsExceededException) {
throw error;
Expand Down
100 changes: 93 additions & 7 deletions apps/web/components/imports/forms/UpdateImportForm.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import { useEffect } from 'react';
import { Stack, TextInput as Input } from '@mantine/core';
import { useForm } from 'react-hook-form';
import { Stack, TextInput as Input, Text, SegmentedControl, Box } from '@mantine/core';
import { useForm, Controller } from 'react-hook-form';
import { useFocusTrap } from '@mantine/hooks';

import { Button } from '@ui/button';
import { ITemplate } from '@impler/shared';
import { ITemplate, TemplateModeEnum } from '@impler/shared';
import { SAMPLE_DATE_FORMATS, VARIABLES } from '@config';
import { MultiSelect } from '@ui/multi-select';
import { validateDateFormatString } from '@shared/utils';

interface UpdateImportFormProps {
data: ITemplate;
onSubmit: (data: IUpdateTemplateData) => void;
isAutoImportAvailable: boolean;
}

export function UpdateImportForm({ onSubmit, data }: UpdateImportFormProps) {
export function UpdateImportForm({ onSubmit, data, isAutoImportAvailable }: UpdateImportFormProps) {
const focusTrapRef = useFocusTrap();
const {
reset,
control,
register,
handleSubmit,
formState: { errors },
Expand All @@ -23,20 +28,101 @@ export function UpdateImportForm({ onSubmit, data }: UpdateImportFormProps) {
useEffect(() => {
reset({
name: data.name,
mode: data.mode || TemplateModeEnum.MANUAL,
expectedDateFormat: data.expectedDateFormat,
});
}, [data, reset]);

const handleFormSubmit = (formData: IUpdateTemplateData) => {
onSubmit(formData);
};

return (
<form onSubmit={handleSubmit(onSubmit)} ref={focusTrapRef}>
<Stack spacing="sm">
<form onSubmit={handleSubmit(handleFormSubmit)} ref={focusTrapRef}>
<Stack spacing="lg">
<Input
autoFocus
required
label="Import Name"
{...register('name')}
error={errors.name?.message}
placeholder="I want to import..."
description="A descriptive name for this import"
/>
<Button type="submit" fullWidth>

<Box>
<Text size="sm" weight={500} mb={4}>
Import Mode
</Text>
<Text size="xs" color="dimmed" mb="xs">
Choose whether this import is triggered manually or automatically
</Text>
<Controller
name="mode"
control={control}
render={({ field }) => (
<SegmentedControl
fullWidth
value={field.value || 'manual'}
onChange={field.onChange}
data={[
{ label: 'Manual', value: 'manual' },
{ label: 'Automatic', value: 'automatic', disabled: !isAutoImportAvailable },
]}
/>
)}
/>
</Box>

<Box>
<Controller
name="expectedDateFormat"
control={control}
rules={{
validate: (value) => {
if (!value) return true;

const result = validateDateFormatString(value as string);
if (typeof result === 'object' && 'isValid' in result) {
return result.isValid ? true : result.error || 'Invalid date format';
}

return result === true ? true : (result as string);
},
}}
render={({ field, fieldState }) => (
<MultiSelect
creatable
maxSelectedValues={VARIABLES.ONE}
clearable
searchable
label="Date Formats"
placeholder="Date Formats"
description="Define the date format you expect in your import data."
data={[
...SAMPLE_DATE_FORMATS,
...(field.value && !SAMPLE_DATE_FORMATS.includes(field.value) ? [field.value] : []),
]}
getCreateLabel={(query) => `Add "${query}"`}
onCreate={(newItem) => {
field.onChange(newItem);

return newItem;
}}
onChange={(value) => {
field.onChange(value[0]);
}}
error={fieldState.error?.message}
value={field.value ? [field.value] : []}
/>
)}
/>
<Text size="xs" color="dimmed" mt="xs">
Example formats: DD/MM/YYYY, MM/DD/YYYY, YYYY-MM-DD, DD-MMM-YYYY
</Text>
</Box>

<Button type="submit" fullWidth mt="md">
Update
</Button>
</Stack>
Expand Down
3 changes: 3 additions & 0 deletions apps/web/config/constants.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ export const NOTIFICATION_KEYS = {
ERROR_DELETING_INVITATION: 'INVITATION_DELETED',
PERMISSION_DENIED_WHILE_DELETING_PROJECT: 'PERMISSION_DENIED_WHILE_DELETING_PROJECT',
SUBSCRIPTION_FEATURE_NOT_INCLUDED_IN_CURRENT_PLAN: 'SUBSCRIPTION_FEATURE_NOT_INCLUDED_IN_CURRENT_PLAN',
ERROR_UPDATING_IMPORT_TEMPLATE: 'ERROR_UPDATING_IMPORT_TEMPLATE',
} as const;

export const ROUTES = {
Expand Down Expand Up @@ -881,3 +882,5 @@ export const defaultWidgetAppereanceThemeDark = {
buttonShadow: '0px 4px 16px rgba(0, 0, 0, 0.6)',
},
};

export const SAMPLE_DATE_FORMATS = ['DD/MM/YYYY', 'MM/DD/YYYY', 'YYYY-MM-DD', 'DD-MM-YYYY', 'MM-DD-YYYY'];
12 changes: 10 additions & 2 deletions apps/web/hooks/useImportDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function useImportDetails({ templateId }: useImportDetailProps) {
[API_KEYS.TEMPLATES_LIST, profileInfo!._projectId],
(oldData) => oldData?.map((item) => (item._id === data._id ? data : item))
);
queryClient.setQueryData<ITemplate>([API_KEYS.TEMPLATE_DETAILS, templateId], data);
queryClient.invalidateQueries([API_KEYS.TEMPLATE_DETAILS, templateId]);
notify(NOTIFICATION_KEYS.IMPORT_UPDATED);
},
}
Expand Down Expand Up @@ -111,7 +111,15 @@ export function useImportDetails({ templateId }: useImportDetailProps) {
modalId: MODAL_KEYS.IMPORT_UPDATE,
title: MODAL_TITLES.IMPORT_UPDATE,

children: <UpdateImportForm onSubmit={updateImport} data={templateData} />,
children: (
<UpdateImportForm
onSubmit={updateImport}
data={templateData}
isAutoImportAvailable={meta?.AUTOMATIC_IMPORTS ? true : false}
/>
),
size: 'xl',
centered: true,
});
};

Expand Down
25 changes: 6 additions & 19 deletions apps/web/pages/imports/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import Link from 'next/link';
import React, { useCallback, useEffect, useState } from 'react';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/router';
import { ActionIcon, Flex, Group, LoadingOverlay, Title, Select } from '@mantine/core';
import { Flex, Group, LoadingOverlay, Title } from '@mantine/core';
import { track } from '@libs/amplitude';
import { defaultWidgetAppereance, TemplateModeEnum } from '@impler/shared';
import { CONSTANTS, IMPORT_MODES, MODAL_KEYS, ROUTES, SubjectsEnum, colors } from '@config';
import { defaultWidgetAppereance } from '@impler/shared';
import { CONSTANTS, MODAL_KEYS, ROUTES, SubjectsEnum, colors } from '@config';
import { useImportDetails } from '@hooks/useImportDetails';

import { Tabs } from '@ui/Tabs';
Expand Down Expand Up @@ -44,10 +44,8 @@ function ImportDetails() {
const [activeTab, setActiveTab] = useState<'schema' | 'destination' | 'snippet' | 'validator' | 'output'>();

const {
meta,
columns,
profileInfo,
updateImport,
templateData,
onUpdateClick,
onDeleteClick,
Expand Down Expand Up @@ -149,23 +147,12 @@ function ImportDetails() {
</Button>
<Group spacing={0}>
<Title order={2}>{templateData?.name}</Title>
<ActionIcon radius={0} onClick={onUpdateClick} p={0}>
<EditIcon color={colors.blue} size="sm" />
</ActionIcon>
</Group>
</Group>
<Group spacing="xs">
<Select
size="sm"
maw={125}
placeholder="Mode"
data={IMPORT_MODES.map((mode) => ({
...mode,
disabled: mode.value === TemplateModeEnum.AUTOMATIC && !meta?.AUTOMATIC_IMPORTS ? true : false,
}))}
value={templateData?.mode || TemplateModeEnum.MANUAL}
onChange={(mode) => updateImport({ mode: mode || undefined })}
/>
<Button leftIcon={<EditIcon />} onClick={onUpdateClick}>
Edit
</Button>
<Button
color="green"
id="import"
Expand Down
Loading
Loading