feat: aggregate rows on import feature

This commit is contained in:
Ahmed Bouhuolia
2024-04-04 05:01:09 +02:00
parent b9651f30d5
commit 3851d34ba4
32 changed files with 1115 additions and 298 deletions

View File

@@ -1,18 +1,19 @@
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import clsx from 'classnames';
import { Button, Intent, Position } from '@blueprintjs/core';
import { useFormikContext } from 'formik';
import { FSelect, Group, Hint } from '@/components';
import { Box, FSelect, Group, Hint } from '@/components';
import { ImportFileMappingForm } from './ImportFileMappingForm';
import { EntityColumn, useImportFileContext } from './ImportFileProvider';
import { EntityColumnField, useImportFileContext } from './ImportFileProvider';
import { CLASSES } from '@/constants';
import { ImportFileContainer } from './ImportFileContainer';
import { ImportStepperStep } from './_types';
import { ImportFileMapBootProvider } from './ImportFileMappingBoot';
import styles from './ImportFileMapping.module.scss';
import { getFieldKey } from './_utils';
export function ImportFileMapping() {
const { importId } = useImportFileContext();
const { importId, entityColumns } = useImportFileContext();
return (
<ImportFileMapBootProvider importId={importId}>
@@ -23,56 +24,98 @@ export function ImportFileMapping() {
Bigcapital fields.
</p>
<table className={clsx('bp4-html-table', styles.table)}>
<thead>
<tr>
<th className={styles.label}>Bigcapital Fields</th>
<th className={styles.field}>Sheet Column Headers</th>
</tr>
</thead>
<tbody>
<ImportFileMappingFields />
</tbody>
</table>
{entityColumns.map((entityColumn, index) => (
<ImportFileMappingGroup
groupKey={entityColumn.groupKey}
groupName={entityColumn.groupName}
fields={entityColumn.fields}
/>
))}
</ImportFileContainer>
<ImportFileMappingFloatingActions />
</ImportFileMappingForm>
</ImportFileMapBootProvider>
);
}
function ImportFileMappingFields() {
const { entityColumns, sheetColumns } = useImportFileContext();
interface ImportFileMappingGroupProps {
groupKey: string;
groupName: string;
fields: any;
}
/**
* Mapping fields group
* @returns {React.ReactNode}
*/
function ImportFileMappingGroup({
groupKey,
groupName,
fields,
}: ImportFileMappingGroupProps) {
return (
<Box>
{groupName && <h3>{groupName}</h3>}
<table className={clsx('bp4-html-table', styles.table)}>
<thead>
<tr>
<th className={styles.label}>Bigcapital Fields</th>
<th className={styles.field}>Sheet Column Headers</th>
</tr>
</thead>
<tbody>
<ImportFileMappingFields fields={fields} />
</tbody>
</table>
</Box>
);
}
interface ImportFileMappingFieldsProps {
fields: EntityColumnField[];
}
/**
* Import mapping fields.
* @returns {React.ReactNode}
*/
function ImportFileMappingFields({ fields }: ImportFileMappingFieldsProps) {
const { sheetColumns } = useImportFileContext();
const items = useMemo(
() => sheetColumns.map((column) => ({ value: column, text: column })),
[sheetColumns],
);
const columnMapper = (column: EntityColumn, index: number) => (
<tr key={index}>
<td className={styles.label}>
{column.name}{' '}
{column.required && <span className={styles.requiredSign}>*</span>}
</td>
<td className={styles.field}>
<Group spacing={4}>
<FSelect
name={column.key}
items={items}
popoverProps={{ minimal: true }}
minimal={true}
fill={true}
/>
{column.hint && (
<Hint content={column.hint} position={Position.BOTTOM} />
)}
</Group>
</td>
</tr>
const columnMapper = useCallback(
(column: EntityColumnField, index: number) => (
<tr key={index}>
<td className={styles.label}>
{column.name}{' '}
{column.required && <span className={styles.requiredSign}>*</span>}
</td>
<td className={styles.field}>
<Group spacing={4}>
<FSelect
name={getFieldKey(column.key, column.group)}
items={items}
popoverProps={{ minimal: true }}
minimal={true}
fill={true}
/>
{column.hint && (
<Hint content={column.hint} position={Position.BOTTOM} />
)}
</Group>
</td>
</tr>
),
[items],
);
const columns = useMemo(
() => fields.map(columnMapper),
[columnMapper, fields],
);
const columns = entityColumns.map(columnMapper);
return <>{columns}</>;
}

View File

@@ -3,17 +3,12 @@ import { Intent } from '@blueprintjs/core';
import { useImportFileMapping } from '@/hooks/query/import';
import { Form, Formik, FormikHelpers } from 'formik';
import { useImportFileContext } from './ImportFileProvider';
import { useMemo } from 'react';
import { isEmpty, lowerCase } from 'lodash';
import { AppToaster } from '@/components';
import { useImportFileMapBootContext } from './ImportFileMappingBoot';
import { transformToForm } from '@/utils';
interface ImportFileMappingFormProps {
children: React.ReactNode;
}
type ImportFileMappingFormValues = Record<string, string | null>;
import { ImportFileMappingFormProps } from './_types';
import {
transformValueToReq,
useImportFileMappingInitialValues,
} from './_utils';
export function ImportFileMappingForm({
children,
@@ -52,50 +47,3 @@ export function ImportFileMappingForm({
</Formik>
);
}
const transformValueToReq = (value: ImportFileMappingFormValues) => {
const mapping = Object.keys(value)
.filter((key) => !isEmpty(value[key]))
.map((key) => ({ from: value[key], to: key }));
return { mapping };
};
const transformResToFormValues = (value: { from: string; to: string }[]) => {
return value?.reduce((acc, map) => {
acc[map.to] = map.from;
return acc;
}, {});
};
const useImportFileMappingInitialValues = () => {
const { importFile } = useImportFileMapBootContext();
const { entityColumns, sheetColumns } = useImportFileContext();
const initialResValues = useMemo(
() => transformResToFormValues(importFile?.map || []),
[importFile?.map],
);
const initialValues = useMemo(
() =>
entityColumns.reduce((acc, { key, name }) => {
const _name = lowerCase(name);
const _matched = sheetColumns.find(
(column) => lowerCase(column) === _name,
);
// Match the default column name the same field name
// if matched one of sheet columns has the same field name.
acc[key] = _matched ? _matched : '';
return acc;
}, {}),
[entityColumns, sheetColumns],
);
return useMemo(
() => ({
...transformToForm(initialResValues, initialValues),
...initialValues,
}),
[initialValues, initialResValues],
);
};

View File

@@ -7,12 +7,19 @@ import React, {
useState,
} from 'react';
export type EntityColumn = {
export type EntityColumnField = {
key: string;
name: string;
required?: boolean;
hint?: string;
group?: string;
};
export interface EntityColumn {
groupKey: string;
groupName: string;
fields: EntityColumnField[];
}
export type SheetColumn = string;
export type SheetMap = { from: string; to: string };

View File

@@ -7,6 +7,7 @@ import * as Yup from 'yup';
import { useImportFileContext } from './ImportFileProvider';
import { ImportAlert, ImportStepperStep } from './_types';
import { useAlertsManager } from './AlertsManager';
import { transformToCamelCase } from '@/utils';
const initialValues = {
file: null,
@@ -55,9 +56,11 @@ export function ImportFileUploadForm({
uploadImportFile(formData)
.then(({ data }) => {
setImportId(data.import.import_id);
setSheetColumns(data.sheet_columns);
setEntityColumns(data.resource_columns);
const _data = transformToCamelCase(data);
setImportId(_data.import.importId);
setSheetColumns(_data.sheetColumns);
setEntityColumns(_data.resourceColumns);
setStep(ImportStepperStep.Mapping);
setSubmitting(false);
})

View File

@@ -5,5 +5,11 @@ export enum ImportStepperStep {
}
export enum ImportAlert {
IMPORTED_SHEET_EMPTY = 'IMPORTED_SHEET_EMPTY'
}
IMPORTED_SHEET_EMPTY = 'IMPORTED_SHEET_EMPTY',
}
export interface ImportFileMappingFormProps {
children: React.ReactNode;
}
export type ImportFileMappingFormValues = Record<string, string | null>;

View File

@@ -0,0 +1,87 @@
import { useMemo } from 'react';
import { chain, isEmpty, lowerCase, head, last, set } from 'lodash';
import { useImportFileContext } from './ImportFileProvider';
import { useImportFileMapBootContext } from './ImportFileMappingBoot';
import { deepdash, transformToForm } from '@/utils';
import { ImportFileMappingFormValues } from './_types';
export const getFieldKey = (key: string, group = '') => {
return group ? `${group}.${key}` : key;
};
type ImportFileMappingRes = { from: string; to: string; group: string }[];
/**
* Transformes the mapping form values to request.
* @param {ImportFileMappingFormValues} value
* @returns {ImportFileMappingRes[]}
*/
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,
to: key.includes('.') ? last(key.split('.')) : key,
group: key.includes('.') ? head(key.split('.')) : '',
}))
.value();
return { mapping };
};
/**
*
* @param value
* @returns
*/
export const transformResToFormValues = (
value: { from: string; to: string }[],
) => {
return value?.reduce((acc, map) => {
acc[map.to] = map.from;
return acc;
}, {});
};
/**
* Retrieves the initial values of mapping form.
* @returns {Record<string, any>}
*/
export const useImportFileMappingInitialValues = () => {
const { importFile } = useImportFileMapBootContext();
const { entityColumns, sheetColumns } = useImportFileContext();
const initialResValues = useMemo(
() => transformResToFormValues(importFile?.map || []),
[importFile?.map],
);
const initialValues = useMemo(
() =>
entityColumns.reduce((acc, { fields, groupKey }) => {
fields.forEach(({ key, name }) => {
const _name = lowerCase(name);
const _matched = sheetColumns.find(
(column) => lowerCase(column) === _name,
);
const _key = groupKey ? `${groupKey}.${key}` : key;
const _value = _matched ? _matched : '';
set(acc, _key, _value);
});
return acc;
}, {}),
[entityColumns, sheetColumns],
);
return useMemo<Record<string, any>>(
() => ({
...transformToForm(initialResValues, initialValues),
...initialValues,
}),
[initialValues, initialResValues],
);
};