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: Add date format controll to import #637

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export class ImportController extends BaseController {
body('mapping.*.group').optional(),
body('mapping.*.from').exists(),
body('mapping.*.to').exists(),
body('mapping.*.dateFormat').optional({ nullable: true }),
],
this.validationResult,
this.asyncMiddleware(this.mapping.bind(this)),
Expand Down
12 changes: 9 additions & 3 deletions packages/server/src/services/Import/ImportFileCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import {
ImportableContext,
} from './interfaces';
import { ServiceError } from '@/exceptions';
import { getUniqueImportableValue, trimObject } from './_utils';
import {
convertMappingsToObject,
getUniqueImportableValue,
trimObject,
} from './_utils';
import { ImportableResources } from './ImportableResources';
import ResourceService from '../Resource/ResourceService';
import { Import } from '@/system/models';
Expand Down Expand Up @@ -43,7 +47,6 @@ export class ImportFileCommon {
return XLSX.utils.sheet_to_json(worksheet, {});
}


/**
* Imports the given parsed data to the resource storage through registered importable service.
* @param {number} tenantId -
Expand Down Expand Up @@ -82,11 +85,14 @@ export class ImportFileCommon {
rowNumber,
uniqueValue,
};
const mappingSettings = convertMappingsToObject(importFile.columnsParsed);

try {
// Validate the DTO object before passing it to the service layer.
await this.importFileValidator.validateData(
resourceFields,
transformedDTO
transformedDTO,
mappingSettings
);
try {
// Run the importable function and listen to the errors.
Expand Down
14 changes: 11 additions & 3 deletions packages/server/src/services/Import/ImportFileDataValidator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Service } from 'typedi';
import { ImportInsertError, ResourceMetaFieldsMap } from './interfaces';
import {
ImportInsertError,
ImportMappingAttr,
ResourceMetaFieldsMap,
} from './interfaces';
import { ERRORS, convertFieldsToYupValidation } from './_utils';
import { IModelMeta } from '@/interfaces';
import { ServiceError } from '@/exceptions';
Expand All @@ -24,9 +28,13 @@ export class ImportFileDataValidator {
*/
public async validateData(
importableFields: ResourceMetaFieldsMap,
data: Record<string, any>
data: Record<string, any>,
mappingSettings: Record<string, ImportMappingAttr> = {}
): Promise<void | ImportInsertError[]> {
const YupSchema = convertFieldsToYupValidation(importableFields);
const YupSchema = convertFieldsToYupValidation(
importableFields,
mappingSettings
);
const _data = { ...data };

try {
Expand Down
51 changes: 45 additions & 6 deletions packages/server/src/services/Import/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ import {
head,
split,
last,
set,
} from 'lodash';
import pluralize from 'pluralize';
import { ResourceMetaFieldsMap } from './interfaces';
import { ImportMappingAttr, ResourceMetaFieldsMap } from './interfaces';
import { IModelMetaField, IModelMetaField2 } from '@/interfaces';
import { ServiceError } from '@/exceptions';
import { multiNumberParse } from '@/utils/multi-number-parse';
Expand Down Expand Up @@ -58,11 +59,19 @@ export function trimObject(obj: Record<string, string | number>) {
* @param {ResourceMetaFieldsMap} fields
* @returns {Yup}
*/
export const convertFieldsToYupValidation = (fields: ResourceMetaFieldsMap) => {
export const convertFieldsToYupValidation = (
fields: ResourceMetaFieldsMap,
parentFieldName: string = '',
mappingSettings: Record<string, ImportMappingAttr> = {}
) => {
const yupSchema = {};

Object.keys(fields).forEach((fieldName: string) => {
const field = fields[fieldName] as IModelMetaField;
const fieldPath = parentFieldName
? `${parentFieldName}.${fieldName}`
: fieldName;

let fieldSchema;
fieldSchema = Yup.string().label(field.name);

Expand Down Expand Up @@ -105,13 +114,23 @@ export const convertFieldsToYupValidation = (fields: ResourceMetaFieldsMap) => {
if (!val) {
return true;
}
return moment(val, 'YYYY-MM-DD', true).isValid();
const fieldDateFormat =
(get(
mappingSettings,
`${fieldPath}.dateFormat`
) as unknown as string) || 'YYYY-MM-DD';

return moment(val, fieldDateFormat, true).isValid();
}
);
} else if (field.fieldType === 'url') {
fieldSchema = fieldSchema.url();
} else if (field.fieldType === 'collection') {
const nestedFieldShema = convertFieldsToYupValidation(field.fields);
const nestedFieldShema = convertFieldsToYupValidation(
field.fields,
field.name,
mappingSettings
);
fieldSchema = Yup.array().label(field.name);

if (!isUndefined(field.collectionMaxLength)) {
Expand Down Expand Up @@ -258,6 +277,7 @@ export const getResourceColumns = (resourceColumns: {
]) => {
const extra: Record<string, any> = {};
const key = fieldKey;
const type = field.fieldType;

if (group) {
extra.group = group;
Expand All @@ -270,6 +290,7 @@ export const getResourceColumns = (resourceColumns: {
name,
required,
hint: importHint,
type,
order,
...extra,
};
Expand Down Expand Up @@ -322,6 +343,8 @@ export const valueParser =
});
const result = await relationQuery.first();
_value = get(result, 'id');
} else if (field.fieldType === 'date') {

} else if (field.fieldType === 'collection') {
const ObjectFieldKey = key.includes('.') ? key.split('.')[1] : key;
const _valueParser = valueParser(fields, tenantModels);
Expand Down Expand Up @@ -433,8 +456,8 @@ export const getMapToPath = (to: string, group = '') =>
group ? `${group}.${to}` : to;

export const getImportsStoragePath = () => {
return path.join(global.__storage_dir, `/imports`);
}
return path.join(global.__storage_dir, `/imports`);
};

/**
* Deletes the imported file from the storage and database.
Expand All @@ -457,3 +480,19 @@ export const readImportFile = (filename: string) => {

return fs.readFile(`${filePath}/${filename}`);
};

/**
* Converts an array of mapping objects to a structured object.
* @param {Array<Object>} mappings - Array of mapping objects.
* @returns {Object} - Structured object based on the mappings.
*/
export const convertMappingsToObject = (mappings) => {
return mappings.reduce((acc, mapping) => {
const { to, group } = mapping;
const key = group ? `['${group}.${to}']` : to;

set(acc, key, mapping);

return acc;
}, {});
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

th.label,
td.label{
width: 32% !important;
width: 30% !important;
}

thead{
Expand All @@ -31,16 +31,14 @@
tr td {
vertical-align: middle;
}

tr td{
:global(.bp4-popover-target .bp4-button),
:global(.bp4-popover-wrapper){
max-width: 250px;
}
}
}
}

.requiredSign{
color: rgb(250, 82, 82);
}

.columnSelectButton{
max-width: 250px;
min-width: 250px;
}
27 changes: 22 additions & 5 deletions packages/webapp/src/containers/Import/ImportFileMapping.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import { EntityColumnField, useImportFileContext } from './ImportFileProvider';
import { CLASSES } from '@/constants';
import { ImportFileContainer } from './ImportFileContainer';
import { ImportStepperStep } from './_types';
import { ImportFileMapBootProvider } from './ImportFileMappingBoot';
import {
ImportFileMapBootProvider,
useImportFileMapBootContext,
} from './ImportFileMappingBoot';
import styles from './ImportFileMapping.module.scss';
import { getFieldKey } from './_utils';
import { getDateFieldKey, getFieldKey } from './_utils';

export function ImportFileMapping() {
const { importId, entityColumns } = useImportFileContext();
Expand Down Expand Up @@ -82,6 +85,7 @@ interface ImportFileMappingFieldsProps {
*/
function ImportFileMappingFields({ fields }: ImportFileMappingFieldsProps) {
const { sheetColumns } = useImportFileContext();
const { dateFormats } = useImportFileMapBootContext();

const items = useMemo(
() => sheetColumns.map((column) => ({ value: column, text: column })),
Expand All @@ -95,22 +99,35 @@ function ImportFileMappingFields({ fields }: ImportFileMappingFieldsProps) {
{column.required && <span className={styles.requiredSign}>*</span>}
</td>
<td className={styles.field}>
<Group spacing={4}>
<Group spacing={12} noWrap>
<FSelect
name={getFieldKey(column.key, column.group)}
name={`['${getFieldKey(column.key, column.group)}'].from`}
items={items}
popoverProps={{ minimal: true }}
minimal={true}
fill={true}
className={styles.columnSelectButton}
/>
{column.hint && (
<Hint content={column.hint} position={Position.BOTTOM} />
)}
{column.type === 'date' && (
<FSelect
name={getDateFieldKey(column.key, column.group)}
items={dateFormats}
placeholder={'Select date format'}
minimal={true}
fill={true}
valueAccessor={'key'}
textAccessor={'label'}
labelAccessor={''}
/>
)}
</Group>
</td>
</tr>
),
[items],
[items, dateFormats],
);
const columns = useMemo(
() => fields.map(columnMapper),
Expand Down
15 changes: 13 additions & 2 deletions packages/webapp/src/containers/Import/ImportFileMappingBoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import { Spinner } from '@blueprintjs/core';
import React, { createContext, useContext } from 'react';
import { Box } from '@/components';
import { useImportFileMeta } from '@/hooks/query/import';
import { useDateFormats } from '@/hooks/query';

interface ImportFileMapBootContextValue {}
interface ImportFileMapBootContextValue {
dateFormats: Array<any>;
}

const ImportFileMapBootContext = createContext<ImportFileMapBootContextValue>(
{} as ImportFileMapBootContextValue,
Expand Down Expand Up @@ -39,14 +42,22 @@ export const ImportFileMapBootProvider = ({
enabled: Boolean(importId),
});

// Fetch date format options.
const { data: dateFormats, isLoading: isDateFormatsLoading } =
useDateFormats();

const value = {
importFile,
isImportFileLoading,
isImportFileFetching,
dateFormats,
isDateFormatsLoading,
};
const isLoading = isDateFormatsLoading || isImportFileLoading;

return (
<ImportFileMapBootContext.Provider value={value}>
{isImportFileLoading ? (
{isLoading ? (
<Box style={{ padding: '2rem', textAlign: 'center' }}>
<Spinner size={26} />
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type EntityColumnField = {
required?: boolean;
hint?: string;
group?: string;
type: string;
};

export interface EntityColumn {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
flex: 1;
padding: 32px 20px;
padding-bottom: 80px;
min-width: 660px;
max-width: 760px;
min-width: 800px;
max-width: 800px;
width: 75%;
margin-left: auto;
margin-right: auto;
Expand Down
11 changes: 10 additions & 1 deletion packages/webapp/src/containers/Import/_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,13 @@ export interface ImportFileMappingFormProps {
children: React.ReactNode;
}

export type ImportFileMappingFormValues = Record<string, string | null>;
export type ImportFileMappingFormValues = Record<
string,
{ from: string | null; dateFormat?: string }
>;

export type ImportFileMappingRes = {
from: string;
to: string;
group: string;
}[];
Loading
Loading