Compare commits
1 Commits
darkmode-u
...
date-forma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e6f1efe30 |
@@ -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)),
|
||||
|
||||
@@ -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';
|
||||
@@ -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 -
|
||||
@@ -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.
|
||||
|
||||
@@ -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';
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
|
||||
@@ -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)) {
|
||||
@@ -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;
|
||||
@@ -270,6 +290,7 @@ export const getResourceColumns = (resourceColumns: {
|
||||
name,
|
||||
required,
|
||||
hint: importHint,
|
||||
type,
|
||||
order,
|
||||
...extra,
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
}, {});
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
th.label,
|
||||
td.label{
|
||||
width: 32% !important;
|
||||
width: 30% !important;
|
||||
}
|
||||
|
||||
thead{
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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 })),
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
@@ -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>
|
||||
|
||||
@@ -13,6 +13,7 @@ export type EntityColumnField = {
|
||||
required?: boolean;
|
||||
hint?: string;
|
||||
group?: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
export interface EntityColumn {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}[];
|
||||
|
||||
@@ -17,13 +17,15 @@ import {
|
||||
} from './ImportFileProvider';
|
||||
import { useImportFileMapBootContext } from './ImportFileMappingBoot';
|
||||
import { deepdash, transformToForm } from '@/utils';
|
||||
import { ImportFileMappingFormValues } from './_types';
|
||||
import { ImportFileMappingFormValues, ImportFileMappingRes } from './_types';
|
||||
|
||||
export const getFieldKey = (key: string, group = '') => {
|
||||
return group ? `${group}.${key}` : key;
|
||||
};
|
||||
|
||||
type ImportFileMappingRes = { from: string; to: string; group: string }[];
|
||||
export const getDateFieldKey = (key: string, group: string = '') => {
|
||||
return `${getFieldKey(key, group)}.dateFormat`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Transformes the mapping form values to request.
|
||||
@@ -34,10 +36,10 @@ export const transformValueToReq = (
|
||||
value: ImportFileMappingFormValues,
|
||||
): { mapping: ImportFileMappingRes[] } => {
|
||||
const mapping = chain(value)
|
||||
.thru(deepdash.index)
|
||||
.pickBy((_value, key) => !isEmpty(get(value, key)))
|
||||
.map((from, key) => ({
|
||||
from,
|
||||
.pickBy((_value, key) => !isEmpty(_value) && _value?.from)
|
||||
.map((_value, key) => ({
|
||||
from: _value.from,
|
||||
dateFormat: _value.dateFormat,
|
||||
to: key.includes('.') ? last(key.split('.')) : key,
|
||||
group: key.includes('.') ? head(key.split('.')) : '',
|
||||
}))
|
||||
@@ -52,19 +54,23 @@ export const transformValueToReq = (
|
||||
* @returns {Record<string, object | string>}
|
||||
*/
|
||||
export const transformResToFormValues = (
|
||||
value: { from: string; to: string , group: string }[],
|
||||
value: { from: string; to: string; group: string }[],
|
||||
): Record<string, object | string> => {
|
||||
return value?.reduce((acc, map) => {
|
||||
const path = map?.group ? `${map.group}.${map.to}` : map.to;
|
||||
set(acc, path, map.from);
|
||||
const path = map?.group ? `['${map.group}.${map.to}']` : map.to;
|
||||
const dateFormatObj = map?.dateFormat
|
||||
? { dateFormat: map?.dateFormat }
|
||||
: {};
|
||||
|
||||
set(acc, path, { from: map?.from, ...dateFormatObj });
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the initial values of mapping form.
|
||||
* @param {EntityColumn[]} entityColumns
|
||||
* @param {SheetColumn[]} sheetColumns
|
||||
* Retrieves the initial values of mapping form.
|
||||
* @param {EntityColumn[]} entityColumns
|
||||
* @param {SheetColumn[]} sheetColumns
|
||||
*/
|
||||
const getInitialDefaultValues = (
|
||||
entityColumns: EntityColumn[],
|
||||
@@ -76,10 +82,10 @@ const getInitialDefaultValues = (
|
||||
const _matched = sheetColumns.find(
|
||||
(column) => lowerCase(column) === _name,
|
||||
);
|
||||
const _key = groupKey ? `${groupKey}.${key}` : key;
|
||||
const _value = _matched ? _matched : '';
|
||||
const path = groupKey ? `['${groupKey}.${key}']` : key;
|
||||
const from = _matched ? _matched : '';
|
||||
|
||||
set(acc, _key, _value);
|
||||
set(acc, path, { from });
|
||||
});
|
||||
return acc;
|
||||
}, {});
|
||||
@@ -102,7 +108,6 @@ export const useImportFileMappingInitialValues = () => {
|
||||
() => getInitialDefaultValues(entityColumns, sheetColumns),
|
||||
[entityColumns, sheetColumns],
|
||||
);
|
||||
|
||||
return useMemo<Record<string, any>>(
|
||||
() =>
|
||||
assign(
|
||||
|
||||
Reference in New Issue
Block a user