UNPKG

react-spreadsheet-mapper

Version:
380 lines (379 loc) 19.3 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); var react_1 = require("react"); var SpreadsheetService_1 = __importDefault(require("./SpreadsheetService")); /** * A headless React hook for mapping spreadsheet data with enhanced security, performance, and accessibility features. * Provides state and functions for file processing, field mapping, and error handling. * @function useSpreadsheetMapper * @param {UseSpreadsheetMapperProps} { options, onFinish, config, clientId, onAnnounce } - Props for the hook. * @returns Enhanced return object with performance metrics and accessibility features */ var useSpreadsheetMapper = function (_a) { var _b, _c; var options = _a.options, onFinish = _a.onFinish, config = _a.config, _d = _a.clientId, clientId = _d === void 0 ? 'default' : _d, onAnnounce = _a.onAnnounce; var _e = (0, react_1.useState)([]), map = _e[0], setMap = _e[1]; var _f = (0, react_1.useState)([]), errors = _f[0], setErrors = _f[1]; var _g = (0, react_1.useState)([]), processedFiles = _g[0], setProcessedFiles = _g[1]; var _h = (0, react_1.useState)([]), fileProcessingStates = _h[0], setFileProcessingStates = _h[1]; var _j = (0, react_1.useState)(false), isProcessing = _j[0], setIsProcessing = _j[1]; var _k = (0, react_1.useState)([]), performanceMetrics = _k[0], setPerformanceMetrics = _k[1]; // Use ref to track active processing count for rate limiting var activeProcessingCount = (0, react_1.useRef)(0); var maxConcurrentFiles = (_c = (_b = config === null || config === void 0 ? void 0 : config.performance) === null || _b === void 0 ? void 0 : _b.maxConcurrentFiles) !== null && _c !== void 0 ? _c : 3; /** * Announces messages for screen readers */ var announce = (0, react_1.useCallback)(function (message, type) { if (type === void 0) { type = 'info'; } if (onAnnounce) { onAnnounce(message, type); } }, [onAnnounce]); /** * Updates an existing mapped field or creates a new one. * @param {MappedField} record - The mapped field to update or create. */ var updateOrCreate = (0, react_1.useCallback)(function (record) { var value = record.value, fileName = record.fileName; // For backward compatibility: if fileName is not provided in either record or existing mappings, match by value only var index = map.findIndex(function (item) { return item.value === value && (!fileName || !item.fileName || item.fileName === fileName); }); var exists = index !== -1; if (!exists) { setMap(function (prevState) { return __spreadArray(__spreadArray([], prevState, true), [record], false); }); announce("Field mapping created: ".concat(record.field, " mapped to ").concat(record.value), 'success'); } else { setMap(function (prevState) { var newState = __spreadArray([], prevState, true); newState[index] = record; return newState; }); announce("Field mapping updated: ".concat(record.field, " mapped to ").concat(record.value), 'info'); } }, [map, announce]); /** * Marks a mapped field as saved or unsaved. * @param {string} value - The field value to save. * @param {string} fileName - Optional file name for per-file mapping. */ var save = (0, react_1.useCallback)(function (value, fileName) { // For backward compatibility: if fileName is not provided, match by value only var index = map.findIndex(function (item) { return item.value === value && (!fileName || !item.fileName || item.fileName === fileName); }); if (index !== -1) { setMap(function (prevState) { var newState = __spreadArray([], prevState, true); var currentItem = newState[index]; if (currentItem) { var saved = !(currentItem.saved && currentItem.saved === true); newState[index] = __assign(__assign({}, currentItem), { saved: saved }); announce("Field ".concat(currentItem.field, " ").concat(saved ? 'saved' : 'unsaved'), saved ? 'success' : 'info'); } return newState; }); } }, [map, announce]); /** * Finalizes the mapping process and checks for any required fields that are not mapped. */ var finish = (0, react_1.useCallback)(function () { setErrors([]); var validationErrors = []; options.forEach(function (option) { if (option.required && !map.find(function (item) { return item.value === option.value; })) { validationErrors.push({ option: option, message: "".concat(option.label, " is required"), type: 'validation' }); } }); setErrors(validationErrors); if (validationErrors.length > 0) { announce("Validation failed: ".concat(validationErrors.length, " required fields are missing"), 'error'); } else { announce('All required fields are mapped successfully', 'success'); } }, [map, options, announce]); /** * Processes a single file with concurrency control */ var processSingleFile = (0, react_1.useCallback)(function (file, fileIndex) { return __awaiter(void 0, void 0, void 0, function () { var data_1, error_1, errorMessage_1; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 2, 3, 4]); // Update state to processing setFileProcessingStates(function (prev) { return prev.map(function (state, index) { return index === fileIndex ? __assign(__assign({}, state), { status: 'processing' }) : state; }); }); return [4 /*yield*/, (0, SpreadsheetService_1.default)(file, config, clientId)]; case 1: data_1 = _a.sent(); // Update state to completed setFileProcessingStates(function (prev) { return prev.map(function (state, index) { return index === fileIndex ? __assign(__assign({}, state), { status: 'completed', data: data_1 }) : state; }); }); setProcessedFiles(function (prev) { return __spreadArray(__spreadArray([], prev, true), [data_1], false); }); // Store performance metrics if available if (data_1.metrics) { setPerformanceMetrics(function (prev) { return __spreadArray(__spreadArray([], prev, true), [data_1.metrics], false); }); } announce("File processed successfully: ".concat(file.name), 'success'); return [3 /*break*/, 4]; case 2: error_1 = _a.sent(); errorMessage_1 = error_1 instanceof Error ? error_1.message : 'Unknown error occurred'; // Update state to error setFileProcessingStates(function (prev) { return prev.map(function (state, index) { return index === fileIndex ? __assign(__assign({}, state), { status: 'error', error: errorMessage_1 }) : state; }); }); announce("File processing failed: ".concat(file.name, " - ").concat(errorMessage_1), 'error'); setErrors(function (prev) { return __spreadArray(__spreadArray([], prev, true), [{ option: { label: file.name, value: file.name }, message: "File processing failed: ".concat(errorMessage_1), type: 'security' }], false); }); return [3 /*break*/, 4]; case 3: activeProcessingCount.current--; return [7 /*endfinally*/]; case 4: return [2 /*return*/]; } }); }); }, [config, clientId, announce]); /** * Processes the selected files using the SpreadsheetService with concurrency control. * @param {File[]} files - An array of File objects to process. */ var handleFiles = (0, react_1.useCallback)(function (files) { if (files.length === 0) return; setProcessedFiles([]); setErrors([]); setPerformanceMetrics([]); setIsProcessing(true); // Initialize file processing states var initialStates = files.map(function (file) { return ({ file: file, status: 'pending' }); }); setFileProcessingStates(initialStates); announce("Starting to process ".concat(files.length, " file").concat(files.length > 1 ? 's' : ''), 'info'); // Process files with concurrency control var processQueue = function () { return __awaiter(void 0, void 0, void 0, function () { var promises, i, file, successCount, errorCount; return __generator(this, function (_a) { switch (_a.label) { case 0: promises = []; i = 0; _a.label = 1; case 1: if (!(i < files.length)) return [3 /*break*/, 6]; file = files[i]; if (!file) return [3 /*break*/, 5]; // Skip if file is undefined _a.label = 2; case 2: if (!(activeProcessingCount.current >= maxConcurrentFiles)) return [3 /*break*/, 4]; return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 100); })]; case 3: _a.sent(); return [3 /*break*/, 2]; case 4: activeProcessingCount.current++; promises.push(processSingleFile(file, i)); _a.label = 5; case 5: i++; return [3 /*break*/, 1]; case 6: // Wait for all files to complete return [4 /*yield*/, Promise.allSettled(promises)]; case 7: // Wait for all files to complete _a.sent(); setIsProcessing(false); successCount = initialStates.filter(function (state) { return state.status === 'completed'; }).length; errorCount = initialStates.filter(function (state) { return state.status === 'error'; }).length; if (errorCount === 0) { announce("All ".concat(files.length, " files processed successfully"), 'success'); } else { announce("Processing completed: ".concat(successCount, " successful, ").concat(errorCount, " failed"), 'info'); } return [2 /*return*/]; } }); }); }; processQueue(); }, [maxConcurrentFiles, processSingleFile, announce]); /** * Handles the completion of a single file's mapping process. * @param {SpreadsheetData} data - The processed data for the file. */ var handleFileFinish = (0, react_1.useCallback)(function (data) { // First validate required fields setErrors([]); var validationErrors = []; options.forEach(function (option) { if (option.required) { // For backward compatibility: if no fileName is specified in mappings, assume single-file mode var mappedField = map.find(function (item) { return item.value === option.value && (!item.fileName || item.fileName === data.name); }); if (!mappedField || !mappedField.saved) { validationErrors.push({ option: option, message: "".concat(option.label, " is required and must be saved"), type: 'validation' }); } } }); // If there are validation errors, set them and prevent finishing if (validationErrors.length > 0) { setErrors(validationErrors); announce("Cannot finish: ".concat(validationErrors.length, " required fields are not properly mapped"), 'error'); return; } // If no validation errors, proceed with finishing // For backward compatibility: if no fileName is specified in mappings, include all mappings var result = map .filter(function (item) { return !item.fileName || item.fileName === data.name; }) .map(function (_a) { var field = _a.field, value = _a.value; return ({ field: field, value: value, }); }); announce('Mapping completed successfully', 'success'); onFinish(__assign(__assign({}, data), { map: result })); }, [map, onFinish, options, announce]); /** * Resets all state to initial values */ var reset = (0, react_1.useCallback)(function () { setMap([]); setErrors([]); setProcessedFiles([]); setFileProcessingStates([]); setPerformanceMetrics([]); setIsProcessing(false); activeProcessingCount.current = 0; announce('All data has been reset', 'info'); }, [announce]); /** * Gets performance summary for all processed files */ var getPerformanceSummary = (0, react_1.useCallback)(function () { if (performanceMetrics.length === 0) return null; var totalSize = performanceMetrics.reduce(function (sum, metric) { return sum + metric.fileSize; }, 0); var totalTime = performanceMetrics.reduce(function (sum, metric) { return sum + metric.processingTime; }, 0); var totalRows = performanceMetrics.reduce(function (sum, metric) { return sum + metric.rowCount; }, 0); var avgProcessingTime = totalTime / performanceMetrics.length; return { totalFiles: performanceMetrics.length, totalSize: totalSize, totalProcessingTime: totalTime, averageProcessingTime: avgProcessingTime, totalRows: totalRows, averageFileSize: totalSize / performanceMetrics.length }; }, [performanceMetrics]); return { map: map, errors: errors, processedFiles: processedFiles, fileProcessingStates: fileProcessingStates, isProcessing: isProcessing, performanceMetrics: performanceMetrics, updateOrCreate: updateOrCreate, save: save, finish: finish, handleFiles: handleFiles, handleFileFinish: handleFileFinish, reset: reset, getPerformanceSummary: getPerformanceSummary, // Accessibility helpers announce: announce }; }; exports.default = useSpreadsheetMapper;