UNPKG

@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
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 })); }, }, ] })); };