@adaptabletools/adaptable
Version:
Powerful data-agnostic HTML5 AG Grid extension which provides advanced, cutting-edge functionality to meet all DataGrid requirements
285 lines (284 loc) • 11.9 kB
JavaScript
import * as React from 'react';
import HelpBlock from '../../../components/HelpBlock';
import { Tag } from '../../../components/Tag';
import { useAdaptable } from '../../AdaptableContext';
import { OnePageAdaptableWizard } from '../../Wizard/OnePageAdaptableWizard';
import { parseCSV, systemFileHandlers } from '../systemFileHandlers';
import { ColumnsSection } from './sections/ColumnsSection';
import { UploadSection } from './sections/UploadSection';
import { ValidationSection } from './sections/ValidationSection';
export const DataImportWizard = (props) => {
const adaptable = useAdaptable();
const adaptableApi = adaptable.api;
const dataImportOptions = adaptableApi.optionsApi.getDataImportOptions();
const module = adaptableApi.internalApi.getModuleService().getModuleById('DataImport');
const [rowData, setRowData] = React.useState(null);
const [file, setFile] = React.useState(null);
const [text, setText] = React.useState('');
// This needs to be called only when:
// - columnsMap changes
// - new data is loaded
const [importType, setImportType] = React.useState('file');
const primaryKey = adaptableApi.optionsApi.getPrimaryKey();
const hasDynamicallyAddedPrimaryKey = typeof dataImportOptions._getPrimaryKeyValue === 'function';
const getPrimaryKeyValue = (rowData) => {
if (hasDynamicallyAddedPrimaryKey) {
return dataImportOptions._getPrimaryKeyValue({
...adaptableApi.internalApi.buildBaseContext(),
rowData,
});
}
return rowData[primaryKey];
};
const preprocessRowData = (rowData) => {
if (typeof dataImportOptions?._preprocessRowData === 'function') {
return dataImportOptions._preprocessRowData({
...adaptableApi.internalApi.buildBaseContext(),
rowData,
});
}
// by default just trim the keys
let processedRowData = {};
if (rowData) {
processedRowData = Object.keys(rowData).reduce((acc, key) => {
acc[key.trim()] = rowData[key];
return acc;
}, processedRowData);
}
return processedRowData;
};
// ---- COLUMNS ----
const [columnsMap, setColumnsMap] = React.useState(null);
const mappedRowDataToColumns = React.useMemo(() => {
return (rowData ?? []).map((row) => {
// pick only included columns, and need to map to abColumn.field
return columnsMap?.reduce((acc, colMap) => {
if (colMap.include && colMap.abColumn && colMap.abColumn.field) {
acc[colMap.abColumn.field] = row[colMap.field];
}
return acc;
}, {});
});
}, [rowData, columnsMap]);
const columnsErrors = React.useMemo(() => {
return columnsMap
? columnsMap.reduce((acc, colMap) => {
if (colMap.include && !colMap.abColumn) {
acc[colMap.field] = `Field ${colMap.field} does not have a coresponding column.`;
}
return acc;
}, {})
: null;
}, [columnsMap]);
const handleNewRowData = React.useCallback((data) => {
if (Array.isArray(data)) {
const processedData = data.map((rowData) => preprocessRowData(rowData));
setRowData(processedData);
const fields = new Set();
processedData.forEach((row) => {
Object.keys(row).forEach((field) => {
fields.add(field);
});
});
const allAbColumns = adaptableApi.columnApi.getColumns();
const columnsMap = Array.from(fields).map((field) => {
const abColumn = allAbColumns.find((c) => {
// exact field match
if (c.field === field) {
return true;
}
// based on friendly name
const friendlyName = c.friendlyName;
if (typeof friendlyName !== 'string') {
return false;
}
// exact match
if (friendlyName === field) {
return true;
}
// match without case
if (friendlyName.toLowerCase() === field.toLowerCase()) {
return true;
}
// match without spaces and case, and special characters: ' ', '-', '_'
if (friendlyName.replace(/\s|-|_/g, '').toLowerCase() === field.toLowerCase()) {
return true;
}
return false;
});
return { field, abColumn, include: true };
});
setColumnsMap(columnsMap);
}
else {
setRowData(null);
setColumnsMap(null);
}
}, []);
const handleRowDataChange = React.useCallback((data) => {
if (Array.isArray(data)) {
setRowData(data);
}
else {
setRowData(null);
}
}, []);
const handleChangeImportType = React.useCallback((importType) => {
setImportType(importType);
// clear data
handleNewRowData(null);
setFile(null);
setText('');
}, []);
// ---- FILE HANDLERS ----
const fileHandlers = React.useMemo(() => {
const userDefinedHandlers = adaptableApi.optionsApi.getDataImportOptions()?.fileHandlers ?? [];
return [...userDefinedHandlers, ...systemFileHandlers];
}, []);
const supportedFileFormats = React.useMemo(() => {
return [...new Set(fileHandlers.map((h) => h.fileExtension)).values()].join(', ');
}, [fileHandlers]);
const readFile = React.useCallback(async (file) => {
const userDefinedFileHandlers = adaptableApi.optionsApi.getDataImportOptions()?.fileHandlers ?? [];
const fileExtension = `.${file.name.split('.').pop()}`;
const handler = [...userDefinedFileHandlers, ...systemFileHandlers].find((fh) => fh.fileExtension === fileExtension);
if (!handler) {
adaptableApi.logError(`No file handler found for file extension ${fileExtension}. Please provide a file handler in Data Import Options.`);
return;
}
try {
const data = await handler.handleFile(file);
setFile(file);
handleNewRowData(data);
return data;
}
catch {
adaptableApi.logError(`Error reading file ${file.name}. Please check the file is valid.`);
}
}, [adaptable]);
const fileMessage = file ? (React.createElement(HelpBlock, { mb: 2 },
"File ",
React.createElement(Tag, null, file.name),
" is ready to be imported.")) : (React.createElement(HelpBlock, { mb: 2 },
"Supported file types: ",
supportedFileFormats));
// ---- TEXT IMPORT ----
const [textError, setTextError] = React.useState(null);
const handleTextChange = React.useCallback(async (text) => {
//
let textHandler = adaptableApi.optionsApi.getDataImportOptions()?.textHandler;
if (!textHandler) {
// regex to test for json
const jsonRegex = /^\s*[\[{]/;
if (text && jsonRegex.test(text)) {
textHandler = JSON.parse;
}
else {
textHandler = parseCSV;
}
}
setText(text);
let textRawData = null;
if (!text || text === '') {
setTextError(null);
}
else {
try {
textRawData = await textHandler(text);
if (Array.isArray(textRawData)) {
handleNewRowData(textRawData);
setTextError('');
}
else {
throw 'Error parsing text';
}
}
catch {
handleNewRowData(null);
setTextError('Error parsing text');
}
}
}, []);
const textMessage = textError ?? (!rowData ? 'Paste your data here' : '');
// ---- VALIDATION ----
const errors = React.useMemo(() => {
if (!dataImportOptions?.validate) {
return null;
}
if (!rowData) {
return null;
}
return rowData?.reduce?.((acc, rowData) => {
const error = dataImportOptions.validate({
rowData,
...adaptableApi.internalApi.buildBaseContext(),
});
const primaryKeyValue = getPrimaryKeyValue(rowData);
if (error && primaryKeyValue && error.length) {
acc[primaryKeyValue] = error;
}
return acc;
}, {});
}, [rowData]) ?? {};
const [skipInvalidRows, setSkipInvalidRows] = React.useState(false);
const handleFinish = () => {
const validData = mappedRowDataToColumns.filter((row) => {
const rowErrors = errors[getPrimaryKeyValue(row)];
return !rowErrors || rowErrors.length === 0;
});
adaptableApi.dataImportApi.internalApi.importData(validData);
};
return (React.createElement(OnePageAdaptableWizard, { data: null, moduleInfo: module.moduleInfo, finishText: "Import", onFinish: () => {
handleFinish();
props.onClose();
}, onHide: () => {
props.onClose();
}, sections: [
{
title: 'Upload',
isValid: () => {
return rowData ? true : 'No valid data uploaded';
},
details: 'Upload the data',
render: () => {
return (React.createElement(UploadSection, { importType: importType, onImportTypeChange: handleChangeImportType,
// FILE
readFile: readFile, supportedFileFormats: supportedFileFormats, fileMessage: fileMessage,
// TEXT
text: text, onTextChange: handleTextChange, textMessage: textMessage }));
},
},
{
title: 'Columns',
isValid: () => {
if (columnsErrors && Object.keys(columnsErrors).length > 0) {
return 'Some fields do not have a corresponding column.';
}
if (!hasDynamicallyAddedPrimaryKey &&
!columnsMap?.some((map) => map?.abColumn?.field === primaryKey)) {
return 'You need to select a column for the primary key.';
}
return true;
},
render: () => {
return (React.createElement(ColumnsSection, { columnsMap: columnsMap, onColumnsChange: (columnsMap) => {
setColumnsMap(columnsMap);
} }));
},
},
{
title: 'Validation',
details: 'Check the Data is Valid',
isValid: () => {
if (errors && Object.keys(errors).length > 0) {
if (!skipInvalidRows)
return 'There are errors in the data.';
}
return true;
},
render: () => {
return (React.createElement(ValidationSection, { errors: errors, data: rowData, columnsMap: columnsMap, onDataChange: handleRowDataChange, skipInvalidRows: skipInvalidRows, onSkipInvalidRowsChange: setSkipInvalidRows }));
},
},
] }));
};