From 90ebcd3772fcf43a180487308101ee2d6142f60c Mon Sep 17 00:00:00 2001 From: tlartem Date: Tue, 15 Jul 2025 16:06:10 +0300 Subject: [PATCH] =?UTF-8?q?=D1=80=D0=B5=D1=84=D0=B0=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .prettierrc.json | 4 +- package-lock.json | 85 +- package.json | 1 + .../DualSpreadsheet/DualSpreadsheet.tsx | 1117 +++++++++-------- .../TemplateManager/TemplateEditor.tsx | 2 +- .../TemplateManager/TemplateManager.tsx | 28 +- src/hooks/useMultiSheetEngine.ts | 302 ++--- src/hooks/useSpreadsheetEngine.ts | 437 ++++--- src/lib/spreadsheet-engine.ts | 548 -------- src/lib/spreadsheet-engine/functions.ts | 88 ++ .../spreadsheet-engine/spreadsheet-engine.ts | 478 +++++++ src/lib/spreadsheet-example.ts | 191 --- src/lib/template-data.ts | 57 - yarn.lock | 251 +--- 14 files changed, 1722 insertions(+), 1867 deletions(-) delete mode 100644 src/lib/spreadsheet-engine.ts create mode 100644 src/lib/spreadsheet-engine/functions.ts create mode 100644 src/lib/spreadsheet-engine/spreadsheet-engine.ts delete mode 100644 src/lib/spreadsheet-example.ts delete mode 100644 src/lib/template-data.ts diff --git a/.prettierrc.json b/.prettierrc.json index ca4382f..48fd274 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -2,8 +2,8 @@ "trailingComma": "all", "printWidth": 80, "tabWidth": 2, - "semi": true, - "singleQuote": false, + "semi": false, + "singleQuote": true, "bracketSpacing": true, "bracketSameLine": false } diff --git a/package-lock.json b/package-lock.json index 6c0175d..4e313b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,12 +18,15 @@ "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-toast": "^1.2.14", "@reduxjs/toolkit": "^1.9.5", + "@types/react-grid-layout": "^1.3.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "immer": "^10.1.1", "lucide-react": "^0.525.0", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", "react-dom": "^18.2.0", + "react-grid-layout": "^1.5.2", "redux": "^4.2.1", "tailwind-merge": "^3.3.1", "xlsx": "^0.18.5" @@ -1193,6 +1196,16 @@ } } }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@remix-run/router": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.7.1.tgz", @@ -1309,6 +1322,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-grid-layout": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/react-grid-layout/-/react-grid-layout-1.3.5.tgz", + "integrity": "sha512-WH/po1gcEcoR6y857yAnPGug+ZhkF4PaTUxgAbwfeSH/QOgVSakKHBXoPGad/sEznmkiaK3pqHk+etdWisoeBQ==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-redux": { "version": "7.1.34", "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz", @@ -2415,6 +2437,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-equals": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz", + "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==", + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz", @@ -2721,9 +2749,9 @@ } }, "node_modules/immer": { - "version": "9.0.21", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", - "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", "license": "MIT", "funding": { "type": "opencollective", @@ -3673,6 +3701,38 @@ "react": "^18.2.0" } }, + "node_modules/react-draggable": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz", + "integrity": "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/react-grid-layout": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-1.5.2.tgz", + "integrity": "sha512-vT7xmQqszTT+sQw/LfisrEO4le1EPNnSEMVHy6sBZyzS3yGkMywdOd+5iEFFwQwt0NSaGkxuRmYwa1JsP6OJdw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1", + "fast-equals": "^4.0.3", + "prop-types": "^15.8.1", + "react-draggable": "^4.4.6", + "react-resizable": "^3.0.5", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -3779,6 +3839,19 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/react-resizable": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.5.tgz", + "integrity": "sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==", + "license": "MIT", + "dependencies": { + "prop-types": "15.x", + "react-draggable": "^4.0.3" + }, + "peerDependencies": { + "react": ">= 16.3" + } + }, "node_modules/react-router": { "version": "6.14.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.14.1.tgz", @@ -3894,6 +3967,12 @@ "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==", "license": "MIT" }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", diff --git a/package.json b/package.json index d39533f..9ea16ef 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@types/react-grid-layout": "^1.3.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "immer": "^10.1.1", "lucide-react": "^0.525.0", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", diff --git a/src/components/DualSpreadsheet/DualSpreadsheet.tsx b/src/components/DualSpreadsheet/DualSpreadsheet.tsx index 0acfdd1..ce649ee 100644 --- a/src/components/DualSpreadsheet/DualSpreadsheet.tsx +++ b/src/components/DualSpreadsheet/DualSpreadsheet.tsx @@ -1,425 +1,482 @@ -import { FC, useEffect, useRef, useState } from 'react'; -import { useMultiSheetEngine } from '../../hooks/useMultiSheetEngine'; +import { produce } from 'immer' +import { + FC, + memo, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react' +import { useMultiSheetEngine } from '../../hooks/useMultiSheetEngine' -interface CellData { - value: string; - isSelected: boolean; - isModified: boolean; - templateValue?: string; -} +const ROW_COUNT = 90 +const COL_COUNT = 20 +const ROW_HEIGHT = 32 +const SHEET_NAMES = { + report: 'L', + calculations: 'R', +} as const + +type SheetType = keyof typeof SHEET_NAMES + +// --- Helper Functions --- +const getColumnLabel = (index: number) => String.fromCharCode(65 + index) + +// --- Interfaces --- interface DualSpreadsheetProps { - templateData?: Record>; + templateData?: Record> } -type SheetType = 'report' | 'calculations'; +// --- Child Components --- +interface CellProps { + sheetType: SheetType + rowIndex: number + colIndex: number + columnWidth: number + isSelected: boolean + isEditing: boolean + displayValue: string // Отображаемое значение (вычисленное) + rawValue: string // "Сырое" значение (формула) + isModified: boolean + onCellClick: (row: number, col: number) => void + onCellDoubleClick: (row: number, col: number) => void + onCellValueChange: (newValue: string) => void // Изменение значения во время редактирования + stopEditing: (save: boolean) => void + setActiveSheet: (type: SheetType) => void + activeCellRef: React.RefObject +} -const DualSpreadsheet: FC = ({ templateData = {} }) => { - const multiSheetEngine = useMultiSheetEngine({ - sheets: [ - { name: 'Report', rows: 90, cols: 20 }, - { name: 'Calculations', rows: 90, cols: 20 } - ], - initialData: templateData || {} - }); +const Cell = memo( + ({ + sheetType, + rowIndex, + colIndex, + columnWidth, + isSelected, + isEditing, + displayValue, + rawValue, + isModified, + onCellClick, + onCellDoubleClick, + onCellValueChange, + stopEditing, + setActiveSheet, + activeCellRef, + }: CellProps) => { + const className = `border border-gray-200 ${ + isSelected ? 'bg-blue-100' : '' + } ${isModified ? 'bg-yellow-50' : ''}` - const [activeSheet, setActiveSheet] = useState('report'); - - // Объединенные состояния для обеих таблиц - const [sheetData, setSheetData] = useState>({ - report: Array(90).fill(null).map(() => - Array(20).fill(null).map(() => ({ - value: '', - isSelected: false, - isModified: false, - templateValue: '' - })) - ), - calculations: Array(90).fill(null).map(() => - Array(20).fill(null).map(() => ({ - value: '', - isSelected: false, - isModified: false - })) + const style = { + minWidth: `${columnWidth}px`, + width: `${columnWidth}px`, + height: `${ROW_HEIGHT}px`, + overflow: 'hidden', + padding: 0, + position: 'relative' as const, + } + + const handleClick = useCallback(() => { + setActiveSheet(sheetType) + onCellClick(rowIndex, colIndex) + }, [setActiveSheet, sheetType, onCellClick, rowIndex, colIndex]) + + const handleDoubleClick = useCallback( + () => onCellDoubleClick(rowIndex, colIndex), + [onCellDoubleClick, rowIndex, colIndex], ) - }); - const [selectedCell, setSelectedCell] = useState<{row: number, col: number} | null>(null); - const [isEditing, setIsEditing] = useState(false); - const [formulaBarValue, setFormulaBarValue] = useState(''); - const [hasEditingChanges, setHasEditingChanges] = useState(false); - const [initialEditingValue, setInitialEditingValue] = useState(''); - - // Объединенные размеры колонок - const [columnWidths, setColumnWidths] = useState>({ - report: Array(20).fill(96), - calculations: Array(20).fill(96) - }); - - const [rowHeights] = useState(Array(90).fill(32)); - - // Объединенные refs - const containerRefs = useRef>({ - report: null, - calculations: null - }); - - const inputRefs = useRef>({ - report: [], - calculations: [] - }); - - const formulaBarRefs = useRef>({ - report: null, - calculations: null - }); - - // Инициализация refs - useEffect(() => { - inputRefs.current.report = Array(90).fill(null).map(() => Array(20).fill(null)); - inputRefs.current.calculations = Array(90).fill(null).map(() => Array(20).fill(null)); - }, []); - - // Загрузка шаблонных данных - useEffect(() => { - if (templateData?.Report && Object.keys(templateData.Report).length > 0) { - setSheetData(prev => ({ - ...prev, - report: prev.report.map((row, rowIndex) => - row.map((cell, colIndex) => { - const cellName = getColumnLabel(colIndex) + (rowIndex + 1); - const templateValue = templateData.Report[cellName]; - return templateValue !== undefined ? { - ...cell, - value: String(templateValue), - templateValue: String(templateValue) - } : cell; - }) - ) - })); - - // Загружаем данные в движок - Object.entries(templateData.Report).forEach(([cellName, value]) => { - const match = cellName.match(/^([A-Z]+)(\d+)$/); - if (match) { - const col = match[1].charCodeAt(0) - 65; - const row = parseInt(match[2]) - 1; - if (row >= 0 && row < 50 && col >= 0 && col < 20) { - multiSheetEngine.setCellValue('Report', row, col, String(value)); - } - } - }); - } - }, [templateData]); - - // Синхронизация с движком - useEffect(() => { - const updateSheetData = (sheetType: SheetType, sheetName: string) => { - const sheetCells = multiSheetEngine.sheetCells[sheetName]; - if (sheetCells) { - setSheetData(prev => { - const newData = prev[sheetType].map((row, rowIndex) => - row.map((cell, colIndex) => { - const rawValue = multiSheetEngine.getCellRawValue(sheetName, rowIndex, colIndex); - return rawValue !== cell.value ? { ...cell, value: rawValue } : cell; - }) - ); - - // Проверяем, действительно ли есть изменения - const hasChanges = newData.some((row, rowIndex) => - row.some((cell, colIndex) => cell.value !== prev[sheetType][rowIndex][colIndex].value) - ); - - if (hasChanges) { - return { ...prev, [sheetType]: newData }; - } - return prev; - }); - } - }; - - // Обновляем только если не в процессе пересчета - if (!multiSheetEngine.isCalculating) { - updateSheetData('report', 'Report'); - updateSheetData('calculations', 'Calculations'); - } - }, [multiSheetEngine.sheetCells, multiSheetEngine.isCalculating]); - - // Синхронизация formula bar - useEffect(() => { - if (selectedCell) { - const sheetName = activeSheet === 'report' ? 'Report' : 'Calculations'; - const rawValue = multiSheetEngine.getCellRawValue(sheetName, selectedCell.row, selectedCell.col); - setFormulaBarValue(rawValue); - } else { - setFormulaBarValue(''); - } - }, [selectedCell, activeSheet, multiSheetEngine.sheetCells, multiSheetEngine.getCellRawValue]); - - const getColumnLabel = (index: number) => String.fromCharCode(65 + index); - - const getCurrentSheetName = () => activeSheet === 'report' ? 'Report' : 'Calculations'; - - const getChangedCells = () => { - const reportChanges: Record = {}; - - sheetData.report.forEach((row, rowIndex) => { - row.forEach((cell, colIndex) => { - if (cell.isModified) { - const cellName = getColumnLabel(colIndex) + (rowIndex + 1); - const rawValue = multiSheetEngine.getCellRawValue('Report', rowIndex, colIndex); - const calculatedValue = multiSheetEngine.getCellValue('Report', rowIndex, colIndex); - - reportChanges[cellName] = { rawValue, calculatedValue }; - } - }); - }); - - return reportChanges; - }; - - useEffect(() => { - if (typeof window !== 'undefined') { - (window as any).getChangedCells = getChangedCells; - } - }, [multiSheetEngine]); - - const handleExportChanges = () => { - const changes = getChangedCells(); - console.log('Измененные ячейки отчета для отправки на бэкенд:', changes); - - const changedCount = Object.keys(changes).length; - if (changedCount > 0) { - alert(`Изменения экспортированы!\nИзмененных ячеек в отчете: ${changedCount}\n\nПроверьте консоль для подробностей.`); - } else { - alert('Нет изменений для экспорта.'); - } - }; - - const handleCellClick = (row: number, col: number) => { - setSelectedCell({ row, col }); - setIsEditing(false); - containerRefs.current[activeSheet]?.focus(); - }; - - const handleCellDoubleClick = (row: number, col: number) => { - setSelectedCell({ row, col }); - const currentValue = sheetData[activeSheet][row][col].value; - setInitialEditingValue(currentValue); - setHasEditingChanges(false); - setIsEditing(true); - setTimeout(() => { - const input = inputRefs.current[activeSheet][row]?.[col]; - if (input) { - input.focus(); - input.setSelectionRange(input.value.length, input.value.length); - } - }, 10); - }; - - const handleCellChange = (row: number, col: number, value: string, shouldRecalc: boolean = false) => { - const sheetName = getCurrentSheetName(); - - // Отслеживаем изменения во время редактирования - if (isEditing && selectedCell?.row === row && selectedCell?.col === col) { - setHasEditingChanges(value !== initialEditingValue); - } - - setSheetData(prev => ({ - ...prev, - [activeSheet]: prev[activeSheet].map((rowData, rowIndex) => - rowIndex === row ? rowData.map((cell, colIndex) => { - if (colIndex === col) { - const isModified = activeSheet === 'report' - ? value !== (cell.templateValue || '') - : false; - return { ...cell, value, isModified }; - } - return cell; - }) : rowData - ) - })); - - if (selectedCell && selectedCell.row === row && selectedCell.col === col) { - setFormulaBarValue(value); - } - - if (shouldRecalc) { - multiSheetEngine.setCellValue(sheetName, row, col, value); - } else { - multiSheetEngine.setCellValueWithoutRecalc(sheetName, row, col, value); - } - }; - - const restoreTemplateValue = (row: number, col: number) => { - if (activeSheet !== 'report') return; - - const cell = sheetData.report[row][col]; - if (cell.templateValue) { - const restoredValue = cell.templateValue; - handleCellChange(row, col, restoredValue, true); - } - }; - - const moveSelection = (deltaRow: number, deltaCol: number) => { - if (!selectedCell) return; - - const newRow = Math.max(0, Math.min(49, selectedCell.row + deltaRow)); - const newCol = Math.max(0, Math.min(19, selectedCell.col + deltaCol)); - - setSelectedCell({ row: newRow, col: newCol }); - setIsEditing(false); - }; - - const startEditing = (focusTarget?: 'cell' | 'formula') => { - if (!selectedCell) return; - const currentValue = sheetData[activeSheet][selectedCell.row][selectedCell.col].value; - setInitialEditingValue(currentValue); - setHasEditingChanges(false); - setIsEditing(true); - - if (focusTarget === 'cell') { - setTimeout(() => { - const input = inputRefs.current[activeSheet][selectedCell.row]?.[selectedCell.col]; - if (input) { - input.focus(); - input.select(); - } - }, 0); - } else if (focusTarget === 'formula') { - setTimeout(() => { - formulaBarRefs.current[activeSheet]?.focus(); - formulaBarRefs.current[activeSheet]?.select(); - }, 0); - } - }; - - const stopEditing = (save: boolean = true) => { - if (save && selectedCell && (hasEditingChanges || formulaBarValue.startsWith('='))) { - multiSheetEngine.debouncedRecalc(); - } - setIsEditing(false); - setHasEditingChanges(false); - setInitialEditingValue(''); - containerRefs.current[activeSheet]?.focus(); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (!selectedCell) return; - - if (isEditing) { - if (e.key === 'Enter') { - e.preventDefault(); - stopEditing(true); - moveSelection(1, 0); - } else if (e.key === 'Escape') { - e.preventDefault(); - stopEditing(false); - } - return; - } - - const keyHandlers: Record void> = { - 'ArrowUp': () => moveSelection(-1, 0), - 'ArrowDown': () => moveSelection(1, 0), - 'ArrowLeft': () => moveSelection(0, -1), - 'ArrowRight': () => moveSelection(0, 1), - 'Enter': () => startEditing('cell'), - 'F2': () => startEditing('cell'), - 'Delete': () => handleCellChange(selectedCell.row, selectedCell.col, '', true), - 'Backspace': () => handleCellChange(selectedCell.row, selectedCell.col, '', true), - }; - - if (keyHandlers[e.key]) { - e.preventDefault(); - keyHandlers[e.key](); - } else if (e.key === 'z' && (e.ctrlKey || e.metaKey) && activeSheet === 'report') { - e.preventDefault(); - restoreTemplateValue(selectedCell.row, selectedCell.col); - } else if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) { - e.preventDefault(); - handleCellChange(selectedCell.row, selectedCell.col, e.key, false); - startEditing('cell'); - } - }; - - const handleColumnResize = (colIndex: number, newWidth: number) => { - setColumnWidths(prev => ({ - ...prev, - [activeSheet]: prev[activeSheet].map((width, index) => - index === colIndex ? Math.max(60, newWidth) : width - ) - })); - }; - - // Универсальный компонент для Formula Bar - const FormulaBar = ({ sheetType }: { sheetType: SheetType }) => ( -
-
-
- {selectedCell && activeSheet === sheetType ? - `${getColumnLabel(selectedCell.col)}${selectedCell.row + 1}` : ''} -
- {multiSheetEngine.isCalculating && ( -
Вычисляется...
- )} -
+ return ( + + {isEditing && isSelected ? ( formulaBarRefs.current[sheetType] = el} + ref={activeCellRef} type="text" - className={`w-full px-2 py-1 text-sm border rounded focus:outline-none ${ - isEditing && activeSheet === sheetType - ? 'border-blue-400 focus:ring-2 focus:ring-blue-300' - : 'border-gray-300 focus:ring-2 focus:ring-blue-500' - }`} - placeholder="Введите формулу..." - value={activeSheet === sheetType ? formulaBarValue : ''} - onChange={(e) => { - if (activeSheet === sheetType) { - const newValue = e.target.value; - setFormulaBarValue(newValue); - - // Отслеживаем изменения для formula bar - if (isEditing) { - setHasEditingChanges(newValue !== initialEditingValue); - } - - if (selectedCell) { - handleCellChange(selectedCell.row, selectedCell.col, newValue, false); - } - } - }} - onFocus={() => { - if (selectedCell && activeSheet === sheetType) { - startEditing('formula'); - } - }} + value={rawValue} // Во время редактирования показываем "сырое" значение + onChange={(e) => onCellValueChange(e.target.value)} onBlur={() => stopEditing(true)} onKeyDown={(e) => { if (e.key === 'Enter') { - e.preventDefault(); - stopEditing(true); - containerRefs.current[sheetType]?.focus(); + e.preventDefault() + stopEditing(true) } else if (e.key === 'Escape') { - e.preventDefault(); - stopEditing(false); + e.preventDefault() + stopEditing(false) } }} + className="absolute left-0 top-0 z-20 box-border h-full w-full border-2 border-blue-500 p-2" /> + ) : ( +
+ {displayValue} +
+ )} + + ) + }, +) + +interface FormulaBarProps { + sheetType: SheetType + activeSheet: SheetType + selectedCell: { row: number; col: number } | null + formulaBarValue: string + isCalculating: boolean + isEditing: boolean + onValueChange: (value: string) => void + onFocus: () => void + onBlur: () => void + onKeyDown: (e: React.KeyboardEvent) => void + formulaBarRef: (el: HTMLInputElement | null) => void +} + +const FormulaBar = memo( + ({ + sheetType, + activeSheet, + selectedCell, + formulaBarValue, + isCalculating, + isEditing, + onValueChange, + onFocus, + onBlur, + onKeyDown, + formulaBarRef, + }: FormulaBarProps) => { + if (activeSheet !== sheetType) { + return ( +
+
+
+ ) + } + + return ( +
+
+
+ + {selectedCell + ? `${getColumnLabel(selectedCell.col)}${selectedCell.row + 1}` + : ''} + +
+ {isCalculating && ( +
+ Вычисляется... +
+ )} +
+ onValueChange(e.target.value)} + onFocus={onFocus} + onBlur={onBlur} + onKeyDown={onKeyDown} + /> +
-
- ); + ) + }, +) - // Универсальный рендер таблицы +// --- Main Component --- +const DualSpreadsheet: FC = ({ templateData = {} }) => { + const { revision, ...multiSheetEngine } = useMultiSheetEngine({ + sheets: [ + { name: SHEET_NAMES.report, rows: ROW_COUNT, cols: COL_COUNT }, + { name: SHEET_NAMES.calculations, rows: ROW_COUNT, cols: COL_COUNT }, + ], + initialData: templateData || {}, + }) + + const [activeSheet, setActiveSheet] = useState('report') + const [selectedCell, setSelectedCell] = useState<{ + row: number + col: number + } | null>(null) + const [isEditing, setIsEditing] = useState(false) + const [editingValue, setEditingValue] = useState('') // Локальное значение во время редактирования + + const [columnWidths, setColumnWidths] = useState>( + () => ({ + report: Array(COL_COUNT).fill(96), + calculations: Array(COL_COUNT).fill(96), + }), + ) + + const containerRefs = useRef>({ + report: null, + calculations: null, + }) + const activeCellInputRef = useRef(null) + const formulaBarRefs = useRef>({ + report: null, + calculations: null, + }) + const initialTemplateValues = useRef>({}) + + // Загрузка данных шаблона один раз при инициализации + useEffect(() => { + const reportTemplate = templateData?.[SHEET_NAMES.report] + if (reportTemplate) { + Object.entries(reportTemplate).forEach(([cellName, value]) => { + initialTemplateValues.current[cellName] = String(value) + }) + } + }, [templateData]) + + // --- Синхронизация UI --- + + // Синхронизация строки формул + const formulaBarValue = useMemo(() => { + if (isEditing) { + return editingValue + } + if (selectedCell) { + const sheetName = SHEET_NAMES[activeSheet] + return multiSheetEngine.getCellRawValue( + sheetName, + selectedCell.row, + selectedCell.col, + ) + } + return '' + }, [ + isEditing, + editingValue, + selectedCell, + activeSheet, + multiSheetEngine, + revision, + ]) + + // Фокус на инпуте при начале редактирования + useEffect(() => { + if (isEditing) { + if (activeCellInputRef.current) { + activeCellInputRef.current.focus() + activeCellInputRef.current.select() + } + } + }, [isEditing]) + + // --- Функции-обработчики --- + + const startEditing = useCallback(() => { + if (!selectedCell) return + const sheetName = SHEET_NAMES[activeSheet] + const rawValue = multiSheetEngine.getCellRawValue( + sheetName, + selectedCell.row, + selectedCell.col, + ) + setIsEditing(true) + setEditingValue(rawValue) + }, [activeSheet, selectedCell, multiSheetEngine]) + + const stopEditing = useCallback( + (save: boolean) => { + if (save && selectedCell) { + const sheetName = SHEET_NAMES[activeSheet] + multiSheetEngine.setCellValueWithoutRecalc( + sheetName, + selectedCell.row, + selectedCell.col, + editingValue, + ) + multiSheetEngine.debouncedRecalc() + } + setIsEditing(false) + setEditingValue('') // Очищаем временное значение + containerRefs.current[activeSheet]?.focus() + }, + [activeSheet, selectedCell, editingValue, multiSheetEngine], + ) + + const handleCellChange = useCallback( + (newValue: string) => { + if (isEditing) { + setEditingValue(newValue) + } + }, + [isEditing], + ) + + const handleClearCell = useCallback(() => { + if (!selectedCell) return + const sheetName = SHEET_NAMES[activeSheet] + multiSheetEngine.setCellValue( + sheetName, + selectedCell.row, + selectedCell.col, + '', + ) + }, [activeSheet, selectedCell, multiSheetEngine]) + + const moveSelection = useCallback( + (deltaRow: number, deltaCol: number) => { + if (!selectedCell) { + // Если ничего не выбрано, выбираем A1 + setSelectedCell({ row: 0, col: 0 }) + return + } + + if (isEditing) { + stopEditing(true) + } + + const newRow = Math.max( + 0, + Math.min(ROW_COUNT - 1, selectedCell.row + deltaRow), + ) + const newCol = Math.max( + 0, + Math.min(COL_COUNT - 1, selectedCell.col + deltaCol), + ) + + setSelectedCell({ row: newRow, col: newCol }) + }, + [selectedCell, isEditing, stopEditing], + ) + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (isEditing) { + // Логика внутри Cell компонента + return + } + + if (!selectedCell && e.key.match(/^(Arrow|Enter|F2|Delete|Backspace)$/)) { + moveSelection(0, 0) + e.preventDefault() + return + } + if (!selectedCell) return + + const keyHandlers: Record void> = { + ArrowUp: () => moveSelection(-1, 0), + ArrowDown: () => moveSelection(1, 0), + ArrowLeft: () => moveSelection(0, -1), + ArrowRight: () => moveSelection(0, 1), + Enter: () => startEditing(), + F2: () => startEditing(), + Delete: handleClearCell, + Backspace: handleClearCell, + } + + if (keyHandlers[e.key]) { + e.preventDefault() + keyHandlers[e.key]() + } else if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) { + e.preventDefault() + setEditingValue(e.key) // Начинаем редактирование с введенного символа + setIsEditing(true) + } + }, + [selectedCell, isEditing, moveSelection, startEditing, handleClearCell], + ) + + const handleCellClick = useCallback( + (row: number, col: number) => { + if (isEditing) { + stopEditing(true) + } + setSelectedCell({ row, col }) + containerRefs.current[activeSheet]?.focus() + }, + [isEditing, stopEditing, activeSheet], + ) + + const handleCellDoubleClick = useCallback( + (row: number, col: number) => { + setSelectedCell({ row, col }) + startEditing() + }, + [startEditing], + ) + + const handleColumnResize = useCallback( + (sheetType: SheetType, colIndex: number, newWidth: number) => { + setColumnWidths( + produce((draft) => { + draft[sheetType][colIndex] = Math.max(60, newWidth) + }), + ) + }, + [], + ) + + // --- Formula Bar Specific Handlers --- + const handleFormulaBarChange = useCallback( + (newValue: string) => { + if (selectedCell) { + if (!isEditing) { + setIsEditing(true) + } + setEditingValue(newValue) + } + }, + [selectedCell, isEditing], + ) + + const handleFormulaBarFocus = useCallback(() => { + if (selectedCell && !isEditing) { + startEditing() + } + }, [selectedCell, isEditing, startEditing]) + + const handleFormulaBarBlur = useCallback(() => { + if (isEditing) { + stopEditing(true) + } + }, [isEditing, stopEditing]) + + const handleFormulaBarKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault() + stopEditing(true) + moveSelection(1, 0) + } else if (e.key === 'Escape') { + e.preventDefault() + stopEditing(false) + containerRefs.current[activeSheet]?.focus() + } + }, + [stopEditing, moveSelection, activeSheet], + ) + + // --- Render Functions --- const renderSpreadsheet = (sheetType: SheetType) => { - const data = sheetData[sheetType]; - const widths = columnWidths[sheetType]; - + const widths = columnWidths[sheetType] + const sheetName = SHEET_NAMES[sheetType] + return ( -
containerRefs.current[sheetType] = el} - className="bg-white rounded-lg shadow-md overflow-auto border border-gray-300" +
(containerRefs.current[sheetType] = el)} + className="overflow-auto rounded-lg border border-gray-300 bg-white shadow-md" style={{ width: '100%', height: '100%', @@ -428,143 +485,177 @@ const DualSpreadsheet: FC = ({ templateData = {} }) => { onKeyDown={handleKeyDown} tabIndex={0} > - +
- - {Array.from({ length: 20 }, (_, i) => ( - - ))} + + {Array.from({ length: COL_COUNT }, (_, i) => { + const handleMouseDown = (e: React.MouseEvent) => { + e.preventDefault() + const startX = e.clientX + const startWidth = widths[i] + + const handleMouseMove = (me: MouseEvent) => { + const newWidth = startWidth + (me.clientX - startX) + handleColumnResize(sheetType, i, newWidth) + } + + const handleMouseUp = () => { + document.removeEventListener('mousemove', handleMouseMove) + document.removeEventListener('mouseup', handleMouseUp) + } + + document.addEventListener('mousemove', handleMouseMove) + document.addEventListener('mouseup', handleMouseUp) + } + return ( + + ) + })} - {data.map((row, rowIndex) => ( + {Array.from({ length: ROW_COUNT }).map((_, rowIndex) => ( - - {row.map((cell, colIndex) => ( - - ))} + {Array.from({ length: COL_COUNT }).map((_, colIndex) => { + const isCellSelected = + selectedCell?.row === rowIndex && + selectedCell?.col === colIndex && + activeSheet === sheetType + const isCellEditing = isCellSelected && isEditing + + const rawValue = isCellEditing + ? editingValue + : multiSheetEngine.getCellRawValue( + sheetName, + rowIndex, + colIndex, + ) + const displayValue = isCellEditing + ? '' // Не показывать вычисленное значение во время редактирования + : String( + multiSheetEngine.getCellValue( + sheetName, + rowIndex, + colIndex, + ) || '', + ) + + const cellName = getColumnLabel(colIndex) + (rowIndex + 1) + const templateValue = + initialTemplateValues.current[cellName] ?? '' + const isModified = + sheetType === 'report' && + templateValue !== '' && + rawValue !== templateValue + + return ( + + ) + })} ))}
- {getColumnLabel(i)} -
{ - e.preventDefault(); - const startX = e.clientX; - const startWidth = widths[i]; - - const handleMouseMove = (e: MouseEvent) => { - const newWidth = startWidth + (e.clientX - startX); - handleColumnResize(i, newWidth); - }; - - const handleMouseUp = () => { - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); - }; - - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); - }} - /> -
+ {getColumnLabel(i)} +
+
+ {rowIndex + 1} { - if (activeSheet !== sheetType) setActiveSheet(sheetType); - handleCellClick(rowIndex, colIndex); - }} - onDoubleClick={() => handleCellDoubleClick(rowIndex, colIndex)} - > - {isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex ? ( - { - if (inputRefs.current[sheetType][rowIndex]) { - inputRefs.current[sheetType][rowIndex][colIndex] = el; - } - }} - type="text" - value={sheetData[sheetType][rowIndex][colIndex].value} - onChange={(e) => handleCellChange(rowIndex, colIndex, e.target.value)} - onBlur={() => stopEditing(true)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - stopEditing(true); - } else if (e.key === 'Escape') { - stopEditing(false); - } - }} - className="absolute top-0 left-0 w-full h-full p-2 border-2 border-blue-500 z-20 box-border" - /> - ) : ( -
- {String(multiSheetEngine.getCellValue(sheetType === 'report' ? 'Report' : 'Calculations', rowIndex, colIndex) || '')} -
- )} -
- ); - }; + ) + } + + // Используем revision в качестве зависимости, чтобы гарантировать перерисовку + // при изменении данных в движке. + const memoizedReportSheet = useMemo( + () => renderSpreadsheet('report'), + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + columnWidths.report, + activeSheet, + selectedCell, + isEditing, + editingValue, + revision, + ], + ) + + const memoizedCalculationsSheet = useMemo( + () => renderSpreadsheet('calculations'), + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + columnWidths.calculations, + activeSheet, + selectedCell, + isEditing, + editingValue, + revision, + ], + ) return ( -
+
{/* Двойная панель */} -
+
{/* Левая панель - Отчет */} -
-

Отчет

- {renderSpreadsheet('report')} +
+

Отчет

+ {memoizedReportSheet}
{/* Правая панель - Вычисления */} -
-

Расчеты

- {renderSpreadsheet('calculations')} +
+

Расчеты

+ {memoizedCalculationsSheet}
- -
- -
-
- +
+ (formulaBarRefs.current[activeSheet] = el)} + />
- ); -}; + ) +} -export default DualSpreadsheet; \ No newline at end of file +export default DualSpreadsheet diff --git a/src/components/TemplateManager/TemplateEditor.tsx b/src/components/TemplateManager/TemplateEditor.tsx index 1171e03..c639e0d 100644 --- a/src/components/TemplateManager/TemplateEditor.tsx +++ b/src/components/TemplateManager/TemplateEditor.tsx @@ -93,7 +93,7 @@ export const TemplateEditor: React.FC = ({ Редактирование шаблона: {editedTemplate.name}

- Настройте Excel шаблон и создайте элементы интерфейса + Настройте Excel шаблон и значения отчета

diff --git a/src/components/TemplateManager/TemplateManager.tsx b/src/components/TemplateManager/TemplateManager.tsx index 8f4dc3e..810d7d9 100644 --- a/src/components/TemplateManager/TemplateManager.tsx +++ b/src/components/TemplateManager/TemplateManager.tsx @@ -45,19 +45,19 @@ export const TemplateManager: React.FC = ({ onTemplateSele setSelectedTemplate(updatedTemplate); }; - const handleTemplateDelete = (templateId: string) => { - deleteTemplate(templateId); - if (selectedTemplate?.id === templateId) { - setSelectedTemplate(null); - setIsEditMode(false); - } - }; + // const handleTemplateDelete = (templateId: string) => { + // deleteTemplate(templateId); + // if (selectedTemplate?.id === templateId) { + // setSelectedTemplate(null); + // setIsEditMode(false); + // } + // }; - const handleTemplateSelect = (template: Template) => { - setSelectedTemplate(template); - setIsEditMode(false); - onTemplateSelect?.(template); - }; + // const handleTemplateSelect = (template: Template) => { + // setSelectedTemplate(template); + // setIsEditMode(false); + // onTemplateSelect?.(template); + // }; const handleEditTemplate = (template: Template) => { setSelectedTemplate(template); @@ -191,7 +191,7 @@ export const TemplateManager: React.FC = ({ onTemplateSele Создан: {template.createdAt.toLocaleDateString('ru-RU')}
-
+ {/*
-
+
*/} ))} diff --git a/src/hooks/useMultiSheetEngine.ts b/src/hooks/useMultiSheetEngine.ts index bc58a59..d36dbe8 100644 --- a/src/hooks/useMultiSheetEngine.ts +++ b/src/hooks/useMultiSheetEngine.ts @@ -1,239 +1,252 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; -import { CellValue, Workbook } from '../lib/spreadsheet-engine'; +import { useCallback, useEffect, useRef, useState } from 'react' +import { + CellValue, + Workbook, +} from '../lib/spreadsheet-engine/spreadsheet-engine' interface CellData { - value: string; - isSelected: boolean; + value: string + isSelected: boolean } interface SheetConfig { - name: string; - rows: number; - cols: number; + name: string + rows: number + cols: number } interface UseMultiSheetEngineProps { - sheets: SheetConfig[]; - initialData?: Record>; + sheets: SheetConfig[] + initialData?: Record> } -export function useMultiSheetEngine({ - sheets, - initialData = {} +export function useMultiSheetEngine({ + sheets, + initialData = {}, }: UseMultiSheetEngineProps) { - const workbookRef = useRef(null); - const [isCalculating, setIsCalculating] = useState(false); - const [sheetCells, setSheetCells] = useState>({}); - const debouncedRecalcTimeoutRef = useRef(null); - const isRecalculatingRef = useRef(false); + const workbookRef = useRef(null) + const [isCalculating, setIsCalculating] = useState(false) + const [revision, setRevision] = useState(0) // Новый счетчик для обновлений + const debouncedRecalcTimeoutRef = useRef(null) + const isRecalculatingRef = useRef(false) // Инициализация workbook и листов useEffect(() => { if (!workbookRef.current) { - workbookRef.current = new Workbook(); - - // Создаем листы - sheets.forEach(sheetConfig => { - workbookRef.current!.addSheet(sheetConfig.name); - }); + workbookRef.current = new Workbook() - // Инициализируем состояние ячеек для каждого листа - const initialSheetCells: Record = {}; - sheets.forEach(sheetConfig => { - initialSheetCells[sheetConfig.name] = Array(sheetConfig.rows).fill(null).map(() => - Array(sheetConfig.cols).fill(null).map(() => ({ value: '', isSelected: false })) - ); - }); - setSheetCells(initialSheetCells); + // Создаем листы + sheets.forEach((sheetConfig) => { + workbookRef.current!.addSheet(sheetConfig.name) + }) // Загружаем начальные данные if (Object.keys(initialData).length > 0) { - workbookRef.current.fromJSON(initialData); - syncAllSheetsFromEngine(); + workbookRef.current.fromJSON(initialData) + // Первый пересчет для инициализации всех значений + safeRecalc() + } else { + // Увеличиваем ревизию, чтобы вызвать начальную отрисовку + setRevision((r) => r + 1) } } - }, [sheets, initialData]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) // Зависимости убраны, чтобы инициализация проходила один раз // Синхронизация всех листов из движка const syncAllSheetsFromEngine = useCallback(() => { - if (!workbookRef.current || isRecalculatingRef.current) return; + if (!workbookRef.current || isRecalculatingRef.current) return - const newSheetCells: Record = {}; - - sheets.forEach(sheetConfig => { - const sheet = workbookRef.current!.getSheet(sheetConfig.name); - if (!sheet) return; + const newSheetCells: Record = {} - const cells = Array(sheetConfig.rows).fill(null).map(() => - Array(sheetConfig.cols).fill(null).map(() => ({ value: '', isSelected: false })) - ); + sheets.forEach((sheetConfig) => { + const sheet = workbookRef.current!.getSheet(sheetConfig.name) + if (!sheet) return + + const cells = Array(sheetConfig.rows) + .fill(null) + .map(() => + Array(sheetConfig.cols) + .fill(null) + .map(() => ({ value: '', isSelected: false })), + ) // Получаем все ячейки из движка for (let row = 0; row < sheetConfig.rows; row++) { for (let col = 0; col < sheetConfig.cols; col++) { - const cellName = columnToLabel(col) + (row + 1); - const cell = sheet.getCell(cellName); - + const cellName = columnToLabel(col) + (row + 1) + const cell = sheet.getCell(cellName) + if (cell.raw !== null && cell.raw !== undefined) { - cells[row][col].value = String(cell.raw); + cells[row][col].value = String(cell.raw) } } } - newSheetCells[sheetConfig.name] = cells; - }); + newSheetCells[sheetConfig.name] = cells + }) - setSheetCells(newSheetCells); - }, [sheets]); + // setSheetCells(newSheetCells) // Удалено, так как sheetCells убрано + }, [sheets]) // Безопасная функция для вычислений const safeRecalc = useCallback(() => { - if (!workbookRef.current || isRecalculatingRef.current) return; + if (!workbookRef.current || isRecalculatingRef.current) return - isRecalculatingRef.current = true; - setIsCalculating(true); + isRecalculatingRef.current = true + setIsCalculating(true) try { - workbookRef.current.recalc(); - syncAllSheetsFromEngine(); + workbookRef.current.recalc() } catch (error) { - console.warn('Ошибка при вычислении:', error); + console.warn('Ошибка при вычислении:', error) } finally { // Небольшая задержка для визуального индикатора setTimeout(() => { - setIsCalculating(false); - isRecalculatingRef.current = false; - }, 150); + setIsCalculating(false) + isRecalculatingRef.current = false + // Увеличиваем ревизию, чтобы сообщить компонентам о необходимости обновления + setRevision((r) => r + 1) + }, 150) } - }, [syncAllSheetsFromEngine]); + }, []) // Отложенный пересчет с дебаунсом const debouncedRecalc = useCallback(() => { - if (!workbookRef.current || isRecalculatingRef.current) return; + if (!workbookRef.current || isRecalculatingRef.current) return // Очищаем предыдущий отложенный пересчет if (debouncedRecalcTimeoutRef.current) { - clearTimeout(debouncedRecalcTimeoutRef.current); + clearTimeout(debouncedRecalcTimeoutRef.current) } // Запускаем пересчет через 300ms debouncedRecalcTimeoutRef.current = setTimeout(() => { - safeRecalc(); - }, 300); - }, [safeRecalc]); + safeRecalc() + }, 300) + }, [safeRecalc]) // Установка значения ячейки без немедленного пересчета - const setCellValueWithoutRecalc = useCallback((sheetName: string, row: number, col: number, value: string) => { - if (!workbookRef.current || isRecalculatingRef.current) return; + const setCellValueWithoutRecalc = useCallback( + (sheetName: string, row: number, col: number, value: string) => { + if (!workbookRef.current || isRecalculatingRef.current) return - const cellName = columnToLabel(col) + (row + 1); - const qualifiedName = `${sheetName}!${cellName}`; - - // Устанавливаем значение в движке без пересчета - workbookRef.current.setCell(qualifiedName, value); - - // Обновляем только локальное состояние ячейки - setSheetCells(prev => { - const newSheetCells = { ...prev }; - if (newSheetCells[sheetName]) { - const newCells = [...newSheetCells[sheetName]]; - newCells[row] = [...newCells[row]]; - newCells[row][col] = { ...newCells[row][col], value }; - newSheetCells[sheetName] = newCells; - } - return newSheetCells; - }); - }, []); + const cellName = columnToLabel(col) + (row + 1) + const qualifiedName = `${sheetName}!${cellName}` + + // Устанавливаем значение в движке без пересчета + workbookRef.current.setCell(qualifiedName, value) + }, + [], + ) // Установка значения ячейки с немедленным пересчетом - const setCellValue = useCallback((sheetName: string, row: number, col: number, value: string) => { - if (!workbookRef.current || isRecalculatingRef.current) return; + const setCellValue = useCallback( + (sheetName: string, row: number, col: number, value: string) => { + if (!workbookRef.current || isRecalculatingRef.current) return - const cellName = columnToLabel(col) + (row + 1); - const qualifiedName = `${sheetName}!${cellName}`; - - // Устанавливаем значение в движке - workbookRef.current.setCell(qualifiedName, value); - - // Запускаем безопасный пересчет - safeRecalc(); - }, [safeRecalc]); + const cellName = columnToLabel(col) + (row + 1) + const qualifiedName = `${sheetName}!${cellName}` + + // Устанавливаем значение в движке + workbookRef.current.setCell(qualifiedName, value) + + // Запускаем безопасный пересчет + safeRecalc() + }, + [safeRecalc], + ) // Получение значения ячейки (вычисленное) - const getCellValue = useCallback((sheetName: string, row: number, col: number): CellValue => { - if (!workbookRef.current) return ''; + const getCellValue = useCallback( + (sheetName: string, row: number, col: number): CellValue => { + if (!workbookRef.current) return '' - const cellName = columnToLabel(col) + (row + 1); - const qualifiedName = `${sheetName}!${cellName}`; - - return workbookRef.current.getValue(qualifiedName); - }, []); + const cellName = columnToLabel(col) + (row + 1) + const qualifiedName = `${sheetName}!${cellName}` + + return workbookRef.current.getValue(qualifiedName) + }, + [], + ) // Получение сырого значения ячейки (формула) - const getCellRawValue = useCallback((sheetName: string, row: number, col: number): string => { - if (!workbookRef.current) return ''; + const getCellRawValue = useCallback( + (sheetName: string, row: number, col: number): string => { + if (!workbookRef.current) return '' - const sheet = workbookRef.current.getSheet(sheetName); - if (!sheet) return ''; + const sheet = workbookRef.current.getSheet(sheetName) + if (!sheet) return '' - const cellName = columnToLabel(col) + (row + 1); - const cell = sheet.getCell(cellName); - - return String(cell.raw || ''); - }, []); + const cellName = columnToLabel(col) + (row + 1) + const cell = sheet.getCell(cellName) + + return String(cell.raw || '') + }, + [], + ) // Получение всех измененных ячеек для листа - const getModifiedCells = useCallback((sheetName: string): Record => { - if (!workbookRef.current) return {}; + const getModifiedCells = useCallback( + (sheetName: string): Record => { + if (!workbookRef.current) return {} - const sheet = workbookRef.current.getSheet(sheetName); - if (!sheet) return {}; + const sheet = workbookRef.current.getSheet(sheetName) + if (!sheet) return {} - return sheet.toJSON(); - }, []); + return sheet.toJSON() + }, + [], + ) // Получение всех измененных ячеек для всех листов - const getAllModifiedCells = useCallback((): Record> => { - if (!workbookRef.current) return {}; + const getAllModifiedCells = useCallback((): Record< + string, + Record + > => { + if (!workbookRef.current) return {} - return workbookRef.current.toJSON(); - }, []); + return workbookRef.current.toJSON() + }, []) // Очистка всех данных const clearAllData = useCallback(() => { - if (!workbookRef.current) return; + if (!workbookRef.current) return - workbookRef.current.clear(); - syncAllSheetsFromEngine(); - }, [syncAllSheetsFromEngine]); + workbookRef.current.clear() + setRevision((r) => r + 1) + }, []) // Загрузка данных из JSON - const loadData = useCallback((data: Record>) => { - if (!workbookRef.current) return; + const loadData = useCallback( + (data: Record>) => { + if (!workbookRef.current) return - workbookRef.current.fromJSON(data); - syncAllSheetsFromEngine(); - }, [syncAllSheetsFromEngine]); + workbookRef.current.fromJSON(data) + safeRecalc() + }, + [safeRecalc], + ) // Пересчет всех формул const recalculate = useCallback(() => { - if (!workbookRef.current) return; + if (!workbookRef.current) return - safeRecalc(); - }, [safeRecalc]); + safeRecalc() + }, [safeRecalc]) // Очистка таймаутов при размонтировании useEffect(() => { return () => { if (debouncedRecalcTimeoutRef.current) { - clearTimeout(debouncedRecalcTimeoutRef.current); + clearTimeout(debouncedRecalcTimeoutRef.current) } - isRecalculatingRef.current = false; - }; - }, []); + isRecalculatingRef.current = false + } + }, []) return { - sheetCells, + // sheetCells убран setCellValue, setCellValueWithoutRecalc, debouncedRecalc, @@ -245,11 +258,12 @@ export function useMultiSheetEngine({ loadData, recalculate, isCalculating, - workbook: workbookRef.current - }; + workbook: workbookRef.current, + revision, // Экспортируем ревизию + } } // Вспомогательная функция для конвертации индекса колонки в букву function columnToLabel(index: number): string { - return String.fromCharCode(65 + index); -} \ No newline at end of file + return String.fromCharCode(65 + index) +} diff --git a/src/hooks/useSpreadsheetEngine.ts b/src/hooks/useSpreadsheetEngine.ts index e401e7d..d394bf0 100644 --- a/src/hooks/useSpreadsheetEngine.ts +++ b/src/hooks/useSpreadsheetEngine.ts @@ -1,286 +1,321 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; -import { CellUtils, CellValue, Workbook } from '../lib/spreadsheet-engine'; -import { addCustomFunctions } from '../lib/spreadsheet-example'; +import { useCallback, useEffect, useRef, useState } from 'react' +import { + CellUtils, + CellValue, + Workbook, +} from '../lib/spreadsheet-engine/spreadsheet-engine' +import { addCustomFunctions } from '../lib/spreadsheet-example' interface CellData { - value: string; - isSelected: boolean; + value: string + isSelected: boolean } interface UseSpreadsheetEngineProps { - rows?: number; - cols?: number; - sheetName?: string; - initialData?: string[][]; + rows?: number + cols?: number + sheetName?: string + initialData?: string[][] } export function useSpreadsheetEngine({ rows = 50, cols = 20, sheetName = 'Sheet1', - initialData + initialData, }: UseSpreadsheetEngineProps = {}) { - const workbookRef = useRef(null); + const workbookRef = useRef(null) const [cells, setCells] = useState(() => { - return Array(rows).fill(null).map(() => - Array(cols).fill(null).map(() => ({ value: '', isSelected: false })) - ); - }); - const [isCalculating, setIsCalculating] = useState(false); + return Array(rows) + .fill(null) + .map(() => + Array(cols) + .fill(null) + .map(() => ({ value: '', isSelected: false })), + ) + }) + const [isCalculating, setIsCalculating] = useState(false) // Инициализация workbook useEffect(() => { if (!workbookRef.current) { - workbookRef.current = new Workbook(); - workbookRef.current.addSheet(sheetName); - addCustomFunctions(workbookRef.current); + workbookRef.current = new Workbook() + workbookRef.current.addSheet(sheetName) + addCustomFunctions(workbookRef.current) } // Загружаем начальные данные если они есть if (initialData) { - loadDataFromArray(initialData); + loadDataFromArray(initialData) } - }, [sheetName, initialData]); + }, [sheetName, initialData]) // Конвертация данных из движка в React состояние const syncFromEngine = useCallback(() => { - if (!workbookRef.current) return; + if (!workbookRef.current) return - const sheet = workbookRef.current.getSheet(sheetName); - if (!sheet) return; + const sheet = workbookRef.current.getSheet(sheetName) + if (!sheet) return - const newCells = Array(rows).fill(null).map(() => - Array(cols).fill(null).map(() => ({ value: '', isSelected: false })) - ); + const newCells = Array(rows) + .fill(null) + .map(() => + Array(cols) + .fill(null) + .map(() => ({ value: '', isSelected: false })), + ) // Получаем все ячейки из движка for (let row = 0; row < rows; row++) { for (let col = 0; col < cols; col++) { - const cellName = CellUtils.createCellName(col, row + 1); - const cell = sheet.getCell(cellName); - + const cellName = CellUtils.createCellName(col, row + 1) + const cell = sheet.getCell(cellName) + // Показываем сырое значение (raw) в UI, а не вычисленное if (cell.raw !== null && cell.raw !== undefined) { - newCells[row][col].value = String(cell.raw); + newCells[row][col].value = String(cell.raw) } } } - setCells(newCells); - }, [rows, cols, sheetName]); + setCells(newCells) + }, [rows, cols, sheetName]) // Установка значения ячейки - const setCellValue = useCallback((row: number, col: number, value: string) => { - if (!workbookRef.current) return; + const setCellValue = useCallback( + (row: number, col: number, value: string) => { + if (!workbookRef.current) return - const cellName = CellUtils.createCellName(col, row + 1); - const qualifiedName = `${sheetName}!${cellName}`; - - setIsCalculating(true); - - try { - // Конвертируем пустую строку в null - const cellValue: CellValue = value === '' ? null : value; - - // Если значение начинается с =, то это формула - if (typeof cellValue === 'string' && cellValue.startsWith('=')) { - workbookRef.current.setCell(qualifiedName, cellValue); - } else { - // Пытаемся конвертировать в число, если возможно - const numValue = Number(cellValue); - if (!isNaN(numValue) && cellValue !== null && cellValue !== '') { - workbookRef.current.setCell(qualifiedName, numValue); + const cellName = CellUtils.createCellName(col, row + 1) + const qualifiedName = `${sheetName}!${cellName}` + + setIsCalculating(true) + + try { + // Конвертируем пустую строку в null + const cellValue: CellValue = value === '' ? null : value + + // Если значение начинается с =, то это формула + if (typeof cellValue === 'string' && cellValue.startsWith('=')) { + workbookRef.current.setCell(qualifiedName, cellValue) } else { - workbookRef.current.setCell(qualifiedName, cellValue); - } - } - - // Пересчитываем все формулы - workbookRef.current.recalc(); - - // Синхронизируем с React состоянием - syncFromEngine(); - } catch (error) { - console.error('Error setting cell value:', error); - } finally { - setIsCalculating(false); - } - }, [sheetName, syncFromEngine]); - - // Получение вычисленного значения ячейки - const getCellValue = useCallback((row: number, col: number): CellValue => { - if (!workbookRef.current) return null; - - const cellName = CellUtils.createCellName(col, row + 1); - const qualifiedName = `${sheetName}!${cellName}`; - - try { - return workbookRef.current.getValue(qualifiedName); - } catch (error) { - console.error('Error getting cell value:', error); - return '#ERROR!'; - } - }, [sheetName]); - - // Получение сырого значения ячейки (для отображения в строке формул) - const getCellRawValue = useCallback((row: number, col: number): string => { - if (!workbookRef.current) return ''; - - const cellName = CellUtils.createCellName(col, row + 1); - const sheet = workbookRef.current.getSheet(sheetName); - - if (!sheet) return ''; - - const cell = sheet.getCell(cellName); - return cell.raw !== null && cell.raw !== undefined ? String(cell.raw) : ''; - }, [sheetName]); - - // Загрузка данных из массива - const loadDataFromArray = useCallback((data: string[][]) => { - if (!workbookRef.current) return; - - const sheet = workbookRef.current.getSheet(sheetName); - if (!sheet) return; - - setIsCalculating(true); - - try { - // Очищаем лист - sheet.cells.clear(); - - // Загружаем данные - for (let row = 0; row < data.length && row < rows; row++) { - for (let col = 0; col < data[row].length && col < cols; col++) { - const value = data[row][col]; - if (value !== null && value !== undefined && value !== '') { - const cellName = CellUtils.createCellName(col, row + 1); - sheet.setCell(cellName, value); + // Пытаемся конвертировать в число, если возможно + const numValue = Number(cellValue) + if (!isNaN(numValue) && cellValue !== null && cellValue !== '') { + workbookRef.current.setCell(qualifiedName, numValue) + } else { + workbookRef.current.setCell(qualifiedName, cellValue) } } + + // Пересчитываем все формулы + workbookRef.current.recalc() + + // Синхронизируем с React состоянием + syncFromEngine() + } catch (error) { + console.error('Error setting cell value:', error) + } finally { + setIsCalculating(false) } - - // Пересчитываем все формулы - workbookRef.current.recalc(); - - // Синхронизируем с React состоянием - syncFromEngine(); - } catch (error) { - console.error('Error loading data:', error); - } finally { - setIsCalculating(false); - } - }, [rows, cols, sheetName, syncFromEngine]); + }, + [sheetName, syncFromEngine], + ) + + // Получение вычисленного значения ячейки + const getCellValue = useCallback( + (row: number, col: number): CellValue => { + if (!workbookRef.current) return null + + const cellName = CellUtils.createCellName(col, row + 1) + const qualifiedName = `${sheetName}!${cellName}` + + try { + return workbookRef.current.getValue(qualifiedName) + } catch (error) { + console.error('Error getting cell value:', error) + return '#ERROR!' + } + }, + [sheetName], + ) + + // Получение сырого значения ячейки (для отображения в строке формул) + const getCellRawValue = useCallback( + (row: number, col: number): string => { + if (!workbookRef.current) return '' + + const cellName = CellUtils.createCellName(col, row + 1) + const sheet = workbookRef.current.getSheet(sheetName) + + if (!sheet) return '' + + const cell = sheet.getCell(cellName) + return cell.raw !== null && cell.raw !== undefined ? String(cell.raw) : '' + }, + [sheetName], + ) + + // Загрузка данных из массива + const loadDataFromArray = useCallback( + (data: string[][]) => { + if (!workbookRef.current) return + + const sheet = workbookRef.current.getSheet(sheetName) + if (!sheet) return + + setIsCalculating(true) + + try { + // Очищаем лист + sheet.cells.clear() + + // Загружаем данные + for (let row = 0; row < data.length && row < rows; row++) { + for (let col = 0; col < data[row].length && col < cols; col++) { + const value = data[row][col] + if (value !== null && value !== undefined && value !== '') { + const cellName = CellUtils.createCellName(col, row + 1) + sheet.setCell(cellName, value) + } + } + } + + // Пересчитываем все формулы + workbookRef.current.recalc() + + // Синхронизируем с React состоянием + syncFromEngine() + } catch (error) { + console.error('Error loading data:', error) + } finally { + setIsCalculating(false) + } + }, + [rows, cols, sheetName, syncFromEngine], + ) // Экспорт данных в массив (для сохранения) const exportToArray = useCallback((): string[][] => { - if (!workbookRef.current) return []; + if (!workbookRef.current) return [] - const sheet = workbookRef.current.getSheet(sheetName); - if (!sheet) return []; + const sheet = workbookRef.current.getSheet(sheetName) + if (!sheet) return [] + + const result: string[][] = Array(rows) + .fill(null) + .map(() => Array(cols).fill('')) - const result: string[][] = Array(rows).fill(null).map(() => Array(cols).fill('')); - for (let row = 0; row < rows; row++) { for (let col = 0; col < cols; col++) { - const cellName = CellUtils.createCellName(col, row + 1); - const cell = sheet.getCell(cellName); - + const cellName = CellUtils.createCellName(col, row + 1) + const cell = sheet.getCell(cellName) + if (cell.raw !== null && cell.raw !== undefined) { - result[row][col] = String(cell.raw); + result[row][col] = String(cell.raw) } } } - - return result; - }, [rows, cols, sheetName]); + + return result + }, [rows, cols, sheetName]) // Экспорт в JSON для бэкенда const exportToJSON = useCallback((): string => { - if (!workbookRef.current) return '{}'; - return JSON.stringify(workbookRef.current.toJSON()); - }, []); + if (!workbookRef.current) return '{}' + return JSON.stringify(workbookRef.current.toJSON()) + }, []) // Импорт из JSON с бэкенда - const importFromJSON = useCallback((jsonData: string) => { - if (!workbookRef.current) return; - - setIsCalculating(true); - - try { - const data = JSON.parse(jsonData); - workbookRef.current.fromJSON(data); - addCustomFunctions(workbookRef.current); - workbookRef.current.recalc(); - syncFromEngine(); - } catch (error) { - console.error('Error importing from JSON:', error); - } finally { - setIsCalculating(false); - } - }, [syncFromEngine]); + const importFromJSON = useCallback( + (jsonData: string) => { + if (!workbookRef.current) return + + setIsCalculating(true) + + try { + const data = JSON.parse(jsonData) + workbookRef.current.fromJSON(data) + addCustomFunctions(workbookRef.current) + workbookRef.current.recalc() + syncFromEngine() + } catch (error) { + console.error('Error importing from JSON:', error) + } finally { + setIsCalculating(false) + } + }, + [syncFromEngine], + ) // Добавление пользовательской функции - const addFunction = useCallback((name: string, func: (...args: any[]) => any) => { - if (!workbookRef.current) return; - workbookRef.current.addFunction(name, func); - }, []); + const addFunction = useCallback( + (name: string, func: (...args: any[]) => any) => { + if (!workbookRef.current) return + workbookRef.current.addFunction(name, func) + }, + [], + ) // Пересчёт всех формул const recalculate = useCallback(() => { - if (!workbookRef.current) return; - - setIsCalculating(true); - + if (!workbookRef.current) return + + setIsCalculating(true) + try { - workbookRef.current.recalc(); - syncFromEngine(); + workbookRef.current.recalc() + syncFromEngine() } catch (error) { - console.error('Error recalculating:', error); + console.error('Error recalculating:', error) } finally { - setIsCalculating(false); + setIsCalculating(false) } - }, [syncFromEngine]); + }, [syncFromEngine]) // Получение информации о зависимостях ячейки - const getCellDependencies = useCallback((row: number, col: number) => { - if (!workbookRef.current) return { precedents: [], dependents: [] }; + const getCellDependencies = useCallback( + (row: number, col: number) => { + if (!workbookRef.current) return { precedents: [], dependents: [] } - const cellName = CellUtils.createCellName(col, row + 1); - const sheet = workbookRef.current.getSheet(sheetName); - - if (!sheet) return { precedents: [], dependents: [] }; - - const cell = sheet.getCell(cellName); - - return { - precedents: Array.from(cell.precedents), - dependents: Array.from(cell.dependents) - }; - }, [sheetName]); + const cellName = CellUtils.createCellName(col, row + 1) + const sheet = workbookRef.current.getSheet(sheetName) + + if (!sheet) return { precedents: [], dependents: [] } + + const cell = sheet.getCell(cellName) + + return { + precedents: Array.from(cell.precedents), + dependents: Array.from(cell.dependents), + } + }, + [sheetName], + ) // Получение статистики workbook const getWorkbookStats = useCallback(() => { - if (!workbookRef.current) return { sheets: 0, cells: 0, formulas: 0 }; + if (!workbookRef.current) return { sheets: 0, cells: 0, formulas: 0 } + + let totalCells = 0 + let totalFormulas = 0 - let totalCells = 0; - let totalFormulas = 0; - for (const sheet of workbookRef.current.sheets.values()) { - totalCells += sheet.cells.size; - + totalCells += sheet.cells.size + for (const cell of sheet.cells.values()) { if (typeof cell.raw === 'string' && cell.raw.startsWith('=')) { - totalFormulas++; + totalFormulas++ } } } - + return { sheets: workbookRef.current.sheets.size, cells: totalCells, - formulas: totalFormulas - }; - }, []); + formulas: totalFormulas, + } + }, []) return { cells, @@ -296,6 +331,6 @@ export function useSpreadsheetEngine({ getCellDependencies, getWorkbookStats, isCalculating, - workbook: workbookRef.current - }; -} \ No newline at end of file + workbook: workbookRef.current, + } +} diff --git a/src/lib/spreadsheet-engine.ts b/src/lib/spreadsheet-engine.ts deleted file mode 100644 index 7ab2fbd..0000000 --- a/src/lib/spreadsheet-engine.ts +++ /dev/null @@ -1,548 +0,0 @@ -// Регулярные выражения для ссылок -const QUALIFIED_REF_RE = /\b[A-Za-z0-9_]+![A-Z]+[0-9]+\b/g; // SheetName!A1 -const LOCAL_REF_RE = /\b[A-Z]+[0-9]+\b/g; // A1, B2, etc. -const RANGE_RE = /\b[A-Z]+[0-9]+:[A-Z]+[0-9]+\b/g; // A1:B2, D5:D7, etc. -const QUALIFIED_RANGE_RE = /\b[A-Za-z0-9_]+![A-Z]+[0-9]+:[A-Z]+[0-9]+\b/g; // SheetName!A1:B2 - -// Типы для значений ячеек -export type CellValue = string | number | boolean | null | undefined; - -// Интерфейс для функций Excel -export interface ExcelFunction { - (...args: any[]): CellValue; -} - -// Волатильные функции - пересчитываются при любом изменении -const VOLATILE_FUNCTIONS = new Set(['RAND', 'RANDBETWEEN', 'NOW', 'TODAY']); - -// Стандартные функции Excel -const DEFAULT_FUNCTIONS: Record = { - RAND: () => Math.random(), - RANDBETWEEN: (bottom: number, top: number) => - Math.floor(Math.random() * (top - bottom + 1)) + bottom, - SUM: (...args: any[]) => { - const flatArgs = args.flat(); - return flatArgs.reduce((sum, val) => sum + (typeof val === 'number' ? val : 0), 0); - }, - AVERAGE: (...args: any[]) => { - const flatArgs = args.flat(); - const validArgs = flatArgs.filter(val => typeof val === 'number'); - return validArgs.length > 0 ? validArgs.reduce((sum, val) => sum + val, 0) / validArgs.length : 0; - }, - COUNT: (...args: any[]) => { - const flatArgs = args.flat(); - return flatArgs.filter(val => typeof val === 'number').length; - }, - MAX: (...args: any[]) => { - const flatArgs = args.flat(); - const validArgs = flatArgs.filter(val => typeof val === 'number'); - return validArgs.length > 0 ? Math.max(...validArgs) : 0; - }, - MIN: (...args: any[]) => { - const flatArgs = args.flat(); - const validArgs = flatArgs.filter(val => typeof val === 'number'); - return validArgs.length > 0 ? Math.min(...validArgs) : 0; - }, - IF: (condition: boolean, trueValue: CellValue, falseValue: CellValue) => - condition ? trueValue : falseValue, - AND: (...args: boolean[]) => args.every(Boolean), - OR: (...args: boolean[]) => args.some(Boolean), - NOT: (value: boolean) => !value, - CONCATENATE: (...args: string[]) => args.join(''), - LEN: (text: string) => text ? text.length : 0, - UPPER: (text: string) => text ? text.toUpperCase() : '', - LOWER: (text: string) => text ? text.toLowerCase() : '', - TRIM: (text: string) => text ? text.trim() : '', - - // Функции округления - ROUND: (number: number, digits: number = 0) => { - const factor = Math.pow(10, digits); - return Math.round(number * factor) / factor; - }, - ROUNDUP: (number: number, digits: number = 0) => { - const factor = Math.pow(10, digits); - return Math.ceil(number * factor) / factor; - }, - ROUNDDOWN: (number: number, digits: number = 0) => { - const factor = Math.pow(10, digits); - return Math.floor(number * factor) / factor; - }, - CEILING: (number: number, significance: number = 1) => { - if (significance === 0) return 0; - return Math.ceil(number / significance) * significance; - }, - FLOOR: (number: number, significance: number = 1) => { - if (significance === 0) return 0; - return Math.floor(number / significance) * significance; - }, - INT: (number: number) => Math.floor(number), - TRUNC: (number: number, digits: number = 0) => { - const factor = Math.pow(10, digits); - return Math.trunc(number * factor) / factor; - }, - MROUND: (number: number, multiple: number) => { - if (multiple === 0) return 0; - return Math.round(number / multiple) * multiple; - }, -}; - -export class Cell { - sheet: Sheet; - name: string; - raw: CellValue = null; - value: CellValue = null; - precedents: Set = new Set(); - dependents: Set = new Set(); - isVolatile: boolean = false; // Флаг для волатильных ячеек - - constructor(sheet: Sheet, name: string) { - this.sheet = sheet; - this.name = name; - } - - // Функция для развертывания диапазона в массив значений - private expandRange(range: string, sheetName?: string): number[] { - const parts = range.split(':'); - if (parts.length !== 2) return []; - - const startCell = parts[0]; - const endCell = parts[1]; - - const startCol = startCell.match(/[A-Z]+/)?.[0] || ''; - const startRow = parseInt(startCell.match(/[0-9]+/)?.[0] || '0'); - const endCol = endCell.match(/[A-Z]+/)?.[0] || ''; - const endRow = parseInt(endCell.match(/[0-9]+/)?.[0] || '0'); - - const startColIndex = startCol.charCodeAt(0) - 65; - const endColIndex = endCol.charCodeAt(0) - 65; - - const values: number[] = []; - - for (let row = startRow; row <= endRow; row++) { - for (let col = startColIndex; col <= endColIndex; col++) { - const cellName = String.fromCharCode(65 + col) + row; - const qualifiedName = sheetName ? `${sheetName}!${cellName}` : `${this.sheet.name}!${cellName}`; - const cellValue = this.sheet.workbook.getCell(qualifiedName).evaluate(); - const numValue = typeof cellValue === 'number' ? cellValue : parseFloat(String(cellValue)) || 0; - values.push(numValue); - } - } - - return values; - } - - get qualifiedName(): string { - return `${this.sheet.name}!${this.name}`; - } - - set(raw: CellValue): void { - // Удаляем старые зависимости - for (const ref of this.precedents) { - this.sheet.workbook.getCell(ref).dependents.delete(this.qualifiedName); - } - - // Убираем ячейку из списка волатильных если она там была - if (this.isVolatile) { - this.sheet.workbook.removeVolatileCell(this.qualifiedName); - this.isVolatile = false; - } - - this.raw = raw; - this.precedents.clear(); - - // Помечаем себя и всех зависимых ячеек "грязными" - this.sheet.workbook.markDirty(this.qualifiedName); - } - - evaluate(): CellValue { - // Если не формула — просто возвращаем значение - if (typeof this.raw !== 'string' || !this.raw.startsWith('=')) { - this.value = this.raw; - return this.value; - } - - const expr = this.raw.substring(1); - - // Проверяем, содержит ли формула волатильные функции - this.checkVolatileFunctions(expr); - - // Регистрируем зависимости на другие ячейки - // Сначала обрабатываем квалифицированные ссылки (SheetName!A1) - const qualifiedMatches = expr.match(QUALIFIED_REF_RE); - if (qualifiedMatches) { - for (const ref of qualifiedMatches) { - this.precedents.add(ref); - const cell = this.sheet.workbook.getCell(ref); - cell.dependents.add(this.qualifiedName); - } - } - - // Затем обрабатываем локальные ссылки (A1, B2) - const localMatches = expr.match(LOCAL_REF_RE); - if (localMatches) { - for (const ref of localMatches) { - // Избегаем конфликтов с функциями (например, SUM, MAX) - if (!this.isFunction(ref)) { - const qualifiedRef = `${this.sheet.name}!${ref}`; - this.precedents.add(qualifiedRef); - const cell = this.sheet.workbook.getCell(qualifiedRef); - cell.dependents.add(this.qualifiedName); - } - } - } - - try { - // Заменяем ссылки на вычисленные значения - let code = expr; - - // ВАЖНО: Сначала заменяем диапазоны ячеек на массивы значений, - // затем отдельные ячейки, чтобы избежать конфликтов - code = code.replace(QUALIFIED_RANGE_RE, (match) => { - const [sheetName, range] = match.split('!'); - const values = this.expandRange(range, sheetName); - return `[${values.join(',')}]`; - }); - - code = code.replace(RANGE_RE, (match) => { - const values = this.expandRange(match); - return `[${values.join(',')}]`; - }); - - // Заменяем квалифицированные ссылки на ячейки - code = code.replace(QUALIFIED_REF_RE, (match) => { - try { - const [sheetName] = match.split('!'); - const sheet = this.sheet.workbook.getSheet(sheetName); - if (!sheet) { - return '"#REF!"'; - } - const cellValue = this.sheet.workbook.getCell(match).evaluate(); - return typeof cellValue === 'string' ? `"${cellValue}"` : String(cellValue ?? 0); - } catch (error) { - return '"#REF!"'; - } - }); - - // Заменяем локальные ссылки на ячейки - code = code.replace(LOCAL_REF_RE, (match) => { - // Избегаем замены функций - if (this.isFunction(match)) { - return match; - } - try { - const qualifiedRef = `${this.sheet.name}!${match}`; - const cellValue = this.sheet.workbook.getCell(qualifiedRef).evaluate(); - return typeof cellValue === 'string' ? `"${cellValue}"` : String(cellValue ?? 0); - } catch (error) { - return '"#REF!"'; - } - }); - - // Заменяем функции Excel - const functions = this.sheet.workbook.getFunctions(); - for (const [funcName] of Object.entries(functions)) { - const funcRegex = new RegExp(`\\b${funcName}\\s*\\(`, 'gi'); - code = code.replace(funcRegex, `__functions__.${funcName}(`); - } - - // Создаём контекст для выполнения - const context = { - __functions__: functions, - wb: this.sheet.workbook, - }; - - // Выполняем код в безопасном контексте - this.value = this.evaluateExpression(code, context); - return this.value; - } catch (error) { - console.error(`Error evaluating formula in ${this.qualifiedName}:`, error); - this.value = '#ERROR!'; - return this.value; - } - } - - private checkVolatileFunctions(expr: string): void { - // Проверяем, содержит ли формула волатильные функции - const functions = this.sheet.workbook.getFunctions(); - let hasVolatileFunction = false; - - for (const funcName of Object.keys(functions)) { - if (VOLATILE_FUNCTIONS.has(funcName.toUpperCase())) { - const funcRegex = new RegExp(`\\b${funcName}\\s*\\(`, 'gi'); - if (funcRegex.test(expr)) { - hasVolatileFunction = true; - break; - } - } - } - - if (hasVolatileFunction && !this.isVolatile) { - this.isVolatile = true; - this.sheet.workbook.addVolatileCell(this.qualifiedName); - } else if (!hasVolatileFunction && this.isVolatile) { - this.isVolatile = false; - this.sheet.workbook.removeVolatileCell(this.qualifiedName); - } - } - - private isFunction(ref: string): boolean { - const functions = this.sheet.workbook.getFunctions(); - return Object.keys(functions).includes(ref.toUpperCase()); - } - - private evaluateExpression(expr: string, context: any): CellValue { - // Простая реализация eval для математических выражений и функций - // В реальном проекте лучше использовать специализированную библиотеку - // для парсинга и выполнения формул - - try { - // Создаём функцию с контекстом - const func = new Function( - ...Object.keys(context), - `return ${expr}` - ); - - return func(...Object.values(context)); - } catch (error) { - throw new Error(`Invalid expression: ${expr}`); - } - } -} - -export class Sheet { - name: string; - workbook: Workbook; - cells: Map = new Map(); - - constructor(name: string, workbook: Workbook) { - this.name = name; - this.workbook = workbook; - } - - getCell(name: string): Cell { - if (!this.cells.has(name)) { - this.cells.set(name, new Cell(this, name)); - } - return this.cells.get(name)!; - } - - setCell(name: string, raw: CellValue): void { - const cell = this.getCell(name); - cell.set(raw); - } - - getCellValue(name: string): CellValue { - return this.getCell(name).value; - } - - getAllCells(): Map { - return this.cells; - } - - toJSON(): Record { - const result: Record = {}; - for (const [name, cell] of this.cells) { - if (cell.raw !== null && cell.raw !== undefined && cell.raw !== '') { - result[name] = cell.raw; - } - } - return result; - } - - fromJSON(data: Record): void { - for (const [name, value] of Object.entries(data)) { - this.setCell(name, value); - } - } -} - -export class Workbook { - sheets: Map = new Map(); - private _dirty: Set = new Set(); - private _functions: Record = { ...DEFAULT_FUNCTIONS }; - private _volatileCells: Set = new Set(); // Список волатильных ячеек - - addSheet(name: string): Sheet { - const sheet = new Sheet(name, this); - this.sheets.set(name, sheet); - return sheet; - } - - getSheet(name: string): Sheet | undefined { - return this.sheets.get(name); - } - - getCell(qualifiedName: string): Cell { - const [sheetName, cellName] = qualifiedName.split('!'); - const sheet = this.sheets.get(sheetName); - if (!sheet) { - throw new Error(`Sheet "${sheetName}" not found`); - } - return sheet.getCell(cellName); - } - - setCell(qualifiedName: string, raw: CellValue): void { - const cell = this.getCell(qualifiedName); - cell.set(raw); - } - - markDirty(qualifiedName: string): void { - const toVisit = new Set([qualifiedName]); - - while (toVisit.size > 0) { - const current = toVisit.values().next().value; - toVisit.delete(current); - - if (!this._dirty.has(current)) { - this._dirty.add(current); - // Добавляем всех зависимых ячеек - for (const dep of this.getCell(current).dependents) { - toVisit.add(dep); - } - } - } - - // Также помечаем все волатильные ячейки как "грязные" - for (const volatileCell of this._volatileCells) { - if (!this._dirty.has(volatileCell)) { - this._dirty.add(volatileCell); - // Добавляем всех зависимых от волатильных ячеек - for (const dep of this.getCell(volatileCell).dependents) { - toVisit.add(dep); - } - } - } - - // Обрабатываем зависимых от волатильных ячеек - while (toVisit.size > 0) { - const current = toVisit.values().next().value; - toVisit.delete(current); - - if (!this._dirty.has(current)) { - this._dirty.add(current); - // Добавляем всех зависимых ячеек - for (const dep of this.getCell(current).dependents) { - toVisit.add(dep); - } - } - } - } - - recalc(): void { - // Пересчёт всех "грязных" ячеек - while (this._dirty.size > 0) { - const current = this._dirty.values().next().value; - this._dirty.delete(current); - this.getCell(current).evaluate(); - } - } - - getValue(qualifiedName: string): CellValue { - if (this._dirty.has(qualifiedName)) { - this.recalc(); - } - return this.getCell(qualifiedName).value; - } - - // Управление волатильными ячейками - addVolatileCell(qualifiedName: string): void { - this._volatileCells.add(qualifiedName); - } - - removeVolatileCell(qualifiedName: string): void { - this._volatileCells.delete(qualifiedName); - } - - getVolatileCells(): Set { - return new Set(this._volatileCells); - } - - // Управление функциями - addFunction(name: string, func: ExcelFunction): void { - this._functions[name.toUpperCase()] = func; - - // Если добавляем волатильную функцию, обновляем список - if (VOLATILE_FUNCTIONS.has(name.toUpperCase())) { - this.markAllVolatileCellsDirty(); - } - } - - removeFunction(name: string): void { - delete this._functions[name.toUpperCase()]; - } - - getFunctions(): Record { - return this._functions; - } - - // Помечаем все волатильные ячейки как "грязные" - private markAllVolatileCellsDirty(): void { - for (const volatileCell of this._volatileCells) { - this.markDirty(volatileCell); - } - } - - // Сериализация - toJSON(): Record> { - const result: Record> = {}; - for (const [name, sheet] of this.sheets) { - result[name] = sheet.toJSON(); - } - return result; - } - - fromJSON(data: Record>): void { - for (const [sheetName, sheetData] of Object.entries(data)) { - if (!this.sheets.has(sheetName)) { - this.addSheet(sheetName); - } - this.sheets.get(sheetName)!.fromJSON(sheetData); - } - } - - // Очистка всех данных - clear(): void { - this.sheets.clear(); - this._dirty.clear(); - this._volatileCells.clear(); - } -} - -// Утилиты для работы с координатами ячеек -export class CellUtils { - static columnToIndex(column: string): number { - let result = 0; - for (let i = 0; i < column.length; i++) { - result = result * 26 + (column.charCodeAt(i) - 65 + 1); - } - return result - 1; - } - - static indexToColumn(index: number): string { - let result = ''; - while (index >= 0) { - result = String.fromCharCode(65 + (index % 26)) + result; - index = Math.floor(index / 26) - 1; - } - return result; - } - - static parseCellName(cellName: string): { column: string; row: number } { - const match = cellName.match(/^([A-Z]+)(\d+)$/); - if (!match) { - throw new Error(`Invalid cell name: ${cellName}`); - } - return { - column: match[1], - row: parseInt(match[2], 10), - }; - } - - static createCellName(column: string | number, row: number): string { - const col = typeof column === 'number' ? CellUtils.indexToColumn(column) : column; - return `${col}${row}`; - } -} - -// Экспорт для удобства использования -export { DEFAULT_FUNCTIONS, VOLATILE_FUNCTIONS }; diff --git a/src/lib/spreadsheet-engine/functions.ts b/src/lib/spreadsheet-engine/functions.ts new file mode 100644 index 0000000..53f2ef4 --- /dev/null +++ b/src/lib/spreadsheet-engine/functions.ts @@ -0,0 +1,88 @@ +export type CellValue = string | number | boolean | null | undefined + +export interface ExcelFunction { + (...args: any[]): CellValue +} + +// Волатильные функции - пересчитываются при любом изменении +export const VOLATILE_FUNCTIONS = new Set([ + 'RAND', + 'RANDBETWEEN', + 'NOW', + 'TODAY', +]) + +export const DEFAULT_FUNCTIONS: Record = { + RAND: () => Math.random(), + RANDBETWEEN: (bottom: number, top: number) => + Math.floor(Math.random() * (top - bottom + 1)) + bottom, + SUM: (...args: any[]) => { + const flatArgs = args.flat() + return flatArgs.reduce( + (sum, val) => sum + (typeof val === 'number' ? val : 0), + 0, + ) + }, + AVERAGE: (...args: any[]) => { + const flatArgs = args.flat() + const validArgs = flatArgs.filter((val) => typeof val === 'number') + return validArgs.length > 0 + ? validArgs.reduce((sum, val) => sum + val, 0) / validArgs.length + : 0 + }, + COUNT: (...args: any[]) => { + const flatArgs = args.flat() + return flatArgs.filter((val) => typeof val === 'number').length + }, + MAX: (...args: any[]) => { + const flatArgs = args.flat() + const validArgs = flatArgs.filter((val) => typeof val === 'number') + return validArgs.length > 0 ? Math.max(...validArgs) : 0 + }, + MIN: (...args: any[]) => { + const flatArgs = args.flat() + const validArgs = flatArgs.filter((val) => typeof val === 'number') + return validArgs.length > 0 ? Math.min(...validArgs) : 0 + }, + IF: (condition: boolean, trueValue: CellValue, falseValue: CellValue) => + condition ? trueValue : falseValue, + AND: (...args: boolean[]) => args.every(Boolean), + OR: (...args: boolean[]) => args.some(Boolean), + NOT: (value: boolean) => !value, + CONCATENATE: (...args: string[]) => args.join(''), + LEN: (text: string) => (text ? text.length : 0), + UPPER: (text: string) => (text ? text.toUpperCase() : ''), + LOWER: (text: string) => (text ? text.toLowerCase() : ''), + TRIM: (text: string) => (text ? text.trim() : ''), + + // Функции округления + ROUND: (number: number, digits: number = 0) => { + const factor = Math.pow(10, digits) + return Math.round(number * factor) / factor + }, + ROUNDUP: (number: number, digits: number = 0) => { + const factor = Math.pow(10, digits) + return Math.ceil(number * factor) / factor + }, + ROUNDDOWN: (number: number, digits: number = 0) => { + const factor = Math.pow(10, digits) + return Math.floor(number * factor) / factor + }, + CEILING: (number: number, significance: number = 1) => { + if (significance === 0) return 0 + return Math.ceil(number / significance) * significance + }, + FLOOR: (number: number, significance: number = 1) => { + if (significance === 0) return 0 + return Math.floor(number / significance) * significance + }, + INT: (number: number) => Math.floor(number), + TRUNC: (number: number, digits: number = 0) => { + const factor = Math.pow(10, digits) + return Math.trunc(number * factor) / factor + }, + MROUND: (number: number, multiple: number) => { + if (multiple === 0) return 0 + return Math.round(number / multiple) * multiple + }, +} diff --git a/src/lib/spreadsheet-engine/spreadsheet-engine.ts b/src/lib/spreadsheet-engine/spreadsheet-engine.ts new file mode 100644 index 0000000..7edec78 --- /dev/null +++ b/src/lib/spreadsheet-engine/spreadsheet-engine.ts @@ -0,0 +1,478 @@ +import { DEFAULT_FUNCTIONS, VOLATILE_FUNCTIONS } from './functions' + +// Регулярные выражения для ссылок +const QUALIFIED_REF_RE = /\b[A-Za-z0-9_]+![A-Z]+[0-9]+\b/g // SheetName!A1 +const LOCAL_REF_RE = /\b[A-Z]+[0-9]+\b/g // A1, B2, etc. +const RANGE_RE = /\b[A-Z]+[0-9]+:[A-Z]+[0-9]+\b/g // A1:B2, D5:D7, etc. +const QUALIFIED_RANGE_RE = /\b[A-Za-z0-9_]+![A-Z]+[0-9]+:[A-Z]+[0-9]+\b/g // SheetName!A1:B2 + +// Типы для значений ячеек +export type CellValue = string | number | boolean | null | undefined + +export class Cell { + sheet: Sheet + name: string + raw: CellValue = null + value: CellValue = null + precedents: Set = new Set() + dependents: Set = new Set() + isVolatile: boolean = false + + constructor(sheet: Sheet, name: string) { + this.sheet = sheet + this.name = name + } + + get qualifiedName(): string { + return `${this.sheet.name}!${this.name}` + } + + set(raw: CellValue): void { + // Удаляем старые зависимости + for (const ref of this.precedents) { + this.sheet.workbook.getCell(ref).dependents.delete(this.qualifiedName) + } + + // Убираем ячейку из списка волатильных если она там была + if (this.isVolatile) { + this.sheet.workbook.removeVolatileCell(this.qualifiedName) + this.isVolatile = false + } + + this.raw = raw + this.precedents.clear() + + // Помечаем себя и всех зависимых ячеек "грязными" + this.sheet.workbook.markDirty(this.qualifiedName) + } + + evaluate(): CellValue { + // Если не формула — просто возвращаем значение + if (typeof this.raw !== 'string' || !this.raw.startsWith('=')) { + this.value = this.raw + return this.value + } + + const expr = this.raw.substring(1) + + // Проверяем, содержит ли формула волатильные функции + this.checkVolatileFunctions(expr) + + // Регистрируем зависимости на другие ячейки + // Сначала обрабатываем квалифицированные ссылки (SheetName!A1) + const qualifiedMatches = expr.match(QUALIFIED_REF_RE) + if (qualifiedMatches) { + for (const ref of qualifiedMatches) { + this.precedents.add(ref) + const cell = this.sheet.workbook.getCell(ref) + cell.dependents.add(this.qualifiedName) + } + } + + // Затем обрабатываем локальные ссылки (A1, B2) + const localMatches = expr.match(LOCAL_REF_RE) + if (localMatches) { + for (const ref of localMatches) { + // Избегаем конфликтов с функциями (например, SUM, MAX) + if (!this.isFunction(ref)) { + const qualifiedRef = `${this.sheet.name}!${ref}` + this.precedents.add(qualifiedRef) + const cell = this.sheet.workbook.getCell(qualifiedRef) + cell.dependents.add(this.qualifiedName) + } + } + } + + try { + // Заменяем ссылки на вычисленные значения + let code = expr + + // ВАЖНО: Сначала заменяем диапазоны ячеек на массивы значений, + // затем отдельные ячейки, чтобы избежать конфликтов + code = code.replace(QUALIFIED_RANGE_RE, (match) => { + const [sheetName, range] = match.split('!') + const values = this.expandRange(range, sheetName) + return `[${values.join(',')}]` + }) + + code = code.replace(RANGE_RE, (match) => { + const values = this.expandRange(match) + return `[${values.join(',')}]` + }) + + // Заменяем квалифицированные ссылки на ячейки + code = code.replace(QUALIFIED_REF_RE, (match) => { + try { + const [sheetName] = match.split('!') + const sheet = this.sheet.workbook.getSheet(sheetName) + if (!sheet) { + return '"#REF!"' + } + const cellValue = this.sheet.workbook.getCell(match).evaluate() + return typeof cellValue === 'string' + ? `"${cellValue}"` + : String(cellValue ?? 0) + } catch (error) { + return '"#REF!"' + } + }) + + // Заменяем локальные ссылки на ячейки + code = code.replace(LOCAL_REF_RE, (match) => { + // Избегаем замены функций + if (this.isFunction(match)) { + return match + } + try { + const qualifiedRef = `${this.sheet.name}!${match}` + const cellValue = this.sheet.workbook.getCell(qualifiedRef).evaluate() + return typeof cellValue === 'string' + ? `"${cellValue}"` + : String(cellValue ?? 0) + } catch (error) { + return '"#REF!"' + } + }) + + // Заменяем функции Excel + const functions = this.sheet.workbook.getFunctions() + for (const [funcName] of Object.entries(functions)) { + const funcRegex = new RegExp(`\\b${funcName}\\s*\\(`, 'gi') + code = code.replace(funcRegex, `__functions__.${funcName}(`) + } + + // Создаём контекст для выполнения + const context = { + __functions__: functions, + wb: this.sheet.workbook, + } + + // Выполняем код в безопасном контексте + this.value = this.evaluateExpression(code, context) + return this.value + } catch (error) { + console.error(`Error evaluating formula in ${this.qualifiedName}:`, error) + this.value = '#ERROR!' + return this.value + } + } + + private checkVolatileFunctions(expr: string): void { + // Проверяем, содержит ли формула волатильные функции + const functions = this.sheet.workbook.getFunctions() + let hasVolatileFunction = false + + for (const funcName of Object.keys(functions)) { + if (VOLATILE_FUNCTIONS.has(funcName.toUpperCase())) { + const funcRegex = new RegExp(`\\b${funcName}\\s*\\(`, 'gi') + if (funcRegex.test(expr)) { + hasVolatileFunction = true + break + } + } + } + + if (hasVolatileFunction && !this.isVolatile) { + this.isVolatile = true + this.sheet.workbook.addVolatileCell(this.qualifiedName) + } else if (!hasVolatileFunction && this.isVolatile) { + this.isVolatile = false + this.sheet.workbook.removeVolatileCell(this.qualifiedName) + } + } + + private isFunction(ref: string): boolean { + const functions = this.sheet.workbook.getFunctions() + return Object.keys(functions).includes(ref.toUpperCase()) + } + + private evaluateExpression(expr: string, context: any): CellValue { + // Простая реализация eval для математических выражений и функций + // В реальном проекте лучше использовать специализированную библиотеку + // для парсинга и выполнения формул + + try { + // Создаём функцию с контекстом + const func = new Function(...Object.keys(context), `return ${expr}`) + + return func(...Object.values(context)) + } catch (error) { + throw new Error(`Invalid expression: ${expr}`) + } + } + + // Функция для развертывания диапазона в массив значений + private expandRange(range: string, sheetName?: string): number[] { + const parts = range.split(':') + if (parts.length !== 2) return [] + + const startCell = parts[0] + const endCell = parts[1] + + const startCol = startCell.match(/[A-Z]+/)?.[0] || '' + const startRow = parseInt(startCell.match(/[0-9]+/)?.[0] || '0') + const endCol = endCell.match(/[A-Z]+/)?.[0] || '' + const endRow = parseInt(endCell.match(/[0-9]+/)?.[0] || '0') + + const startColIndex = startCol.charCodeAt(0) - 65 + const endColIndex = endCol.charCodeAt(0) - 65 + + const values: number[] = [] + + for (let row = startRow; row <= endRow; row++) { + for (let col = startColIndex; col <= endColIndex; col++) { + const cellName = String.fromCharCode(65 + col) + row + const qualifiedName = sheetName + ? `${sheetName}!${cellName}` + : `${this.sheet.name}!${cellName}` + const cellValue = this.sheet.workbook.getCell(qualifiedName).evaluate() + const numValue = + typeof cellValue === 'number' + ? cellValue + : parseFloat(String(cellValue)) || 0 + values.push(numValue) + } + } + + return values + } +} + +export class Sheet { + name: string + workbook: Workbook + cells: Map = new Map() + + constructor(name: string, workbook: Workbook) { + this.name = name + this.workbook = workbook + } + + getCell(name: string): Cell { + if (!this.cells.has(name)) { + this.cells.set(name, new Cell(this, name)) + } + return this.cells.get(name)! + } + + setCell(name: string, raw: CellValue): void { + const cell = this.getCell(name) + cell.set(raw) + } + + getCellValue(name: string): CellValue { + return this.getCell(name).value + } + + getAllCells(): Map { + return this.cells + } + + toJSON(): Record { + const result: Record = {} + for (const [name, cell] of this.cells) { + if (cell.raw !== null && cell.raw !== undefined && cell.raw !== '') { + result[name] = cell.raw + } + } + return result + } + + fromJSON(data: Record): void { + for (const [name, value] of Object.entries(data)) { + this.setCell(name, value) + } + } +} + +export class Workbook { + sheets: Map = new Map() + private _dirty: Set = new Set() + private _functions: Record = { ...DEFAULT_FUNCTIONS } + private _volatileCells: Set = new Set() // Список волатильных ячеек + + addSheet(name: string): Sheet { + const sheet = new Sheet(name, this) + this.sheets.set(name, sheet) + return sheet + } + + getSheet(name: string): Sheet | undefined { + return this.sheets.get(name) + } + + getCell(qualifiedName: string): Cell { + const [sheetName, cellName] = qualifiedName.split('!') + const sheet = this.sheets.get(sheetName) + if (!sheet) { + throw new Error(`Sheet "${sheetName}" not found`) + } + return sheet.getCell(cellName) + } + + setCell(qualifiedName: string, raw: CellValue): void { + const cell = this.getCell(qualifiedName) + cell.set(raw) + } + + markDirty(qualifiedName: string): void { + const toVisit = new Set([qualifiedName]) + + while (toVisit.size > 0) { + const current = toVisit.values().next().value + toVisit.delete(current) + + if (!this._dirty.has(current)) { + this._dirty.add(current) + // Добавляем всех зависимых ячеек + for (const dep of this.getCell(current).dependents) { + toVisit.add(dep) + } + } + } + + // Также помечаем все волатильные ячейки как "грязные" + for (const volatileCell of this._volatileCells) { + if (!this._dirty.has(volatileCell)) { + this._dirty.add(volatileCell) + // Добавляем всех зависимых от волатильных ячеек + for (const dep of this.getCell(volatileCell).dependents) { + toVisit.add(dep) + } + } + } + + // Обрабатываем зависимых от волатильных ячеек + while (toVisit.size > 0) { + const current = toVisit.values().next().value + toVisit.delete(current) + + if (!this._dirty.has(current)) { + this._dirty.add(current) + // Добавляем всех зависимых ячеек + for (const dep of this.getCell(current).dependents) { + toVisit.add(dep) + } + } + } + } + + recalc(): void { + // Пересчёт всех "грязных" ячеек + while (this._dirty.size > 0) { + const current = this._dirty.values().next().value + this._dirty.delete(current) + this.getCell(current).evaluate() + } + } + + getValue(qualifiedName: string): CellValue { + if (this._dirty.has(qualifiedName)) { + this.recalc() + } + return this.getCell(qualifiedName).value + } + + // Управление волатильными ячейками + addVolatileCell(qualifiedName: string): void { + this._volatileCells.add(qualifiedName) + } + + removeVolatileCell(qualifiedName: string): void { + this._volatileCells.delete(qualifiedName) + } + + getVolatileCells(): Set { + return new Set(this._volatileCells) + } + + // Управление функциями + addFunction(name: string, func: ExcelFunction): void { + this._functions[name.toUpperCase()] = func + + // Если добавляем волатильную функцию, обновляем список + if (VOLATILE_FUNCTIONS.has(name.toUpperCase())) { + this.markAllVolatileCellsDirty() + } + } + + removeFunction(name: string): void { + delete this._functions[name.toUpperCase()] + } + + getFunctions(): Record { + return this._functions + } + + // Помечаем все волатильные ячейки как "грязные" + private markAllVolatileCellsDirty(): void { + for (const volatileCell of this._volatileCells) { + this.markDirty(volatileCell) + } + } + + // Сериализация + toJSON(): Record> { + const result: Record> = {} + for (const [name, sheet] of this.sheets) { + result[name] = sheet.toJSON() + } + return result + } + + fromJSON(data: Record>): void { + for (const [sheetName, sheetData] of Object.entries(data)) { + if (!this.sheets.has(sheetName)) { + this.addSheet(sheetName) + } + this.sheets.get(sheetName)!.fromJSON(sheetData) + } + } + + // Очистка всех данных + clear(): void { + this.sheets.clear() + this._dirty.clear() + this._volatileCells.clear() + } +} + +// Утилиты для работы с координатами ячеек +export class CellUtils { + static columnToIndex(column: string): number { + let result = 0 + for (let i = 0; i < column.length; i++) { + result = result * 26 + (column.charCodeAt(i) - 65 + 1) + } + return result - 1 + } + + static indexToColumn(index: number): string { + let result = '' + while (index >= 0) { + result = String.fromCharCode(65 + (index % 26)) + result + index = Math.floor(index / 26) - 1 + } + return result + } + + static parseCellName(cellName: string): { column: string; row: number } { + const match = cellName.match(/^([A-Z]+)(\d+)$/) + if (!match) { + throw new Error(`Invalid cell name: ${cellName}`) + } + return { + column: match[1], + row: parseInt(match[2], 10), + } + } + + static createCellName(column: string | number, row: number): string { + const col = + typeof column === 'number' ? CellUtils.indexToColumn(column) : column + return `${col}${row}` + } +} + +// Экспорт для удобства использования +export { DEFAULT_FUNCTIONS, VOLATILE_FUNCTIONS } diff --git a/src/lib/spreadsheet-example.ts b/src/lib/spreadsheet-example.ts deleted file mode 100644 index 2dabbdf..0000000 --- a/src/lib/spreadsheet-example.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { CellUtils, Workbook } from './spreadsheet-engine'; - -// Пример использования движка электронной таблицы -export function createExampleWorkbook(): Workbook { - const wb = new Workbook(); - - // Создаём листы - wb.addSheet('Sheet1'); - - // Заполняем данные с локальными ссылками - wb.setCell('Sheet1!A1', 10); - wb.setCell('Sheet1!A2', 20); - wb.setCell('Sheet1!A3', '=A1+A2'); // Локальная ссылка - wb.setCell('Sheet1!B1', '=A1*2'); // Локальная ссылка - wb.setCell('Sheet1!B2', '=SUM(A1,A2)'); // Функция с локальными ссылками - wb.setCell('Sheet1!C1', '="Результат: " + A3'); // Строковая операция с локальной ссылкой - - // Пример с квалифицированными ссылками (если есть несколько листов) - wb.addSheet('Counts'); - wb.setCell('Counts!A1', 'N455'); - wb.setCell('Sheet1!D1', '="Отчет по протоколу " + Counts!A1'); - - // Пересчитываем все формулы - wb.recalc(); - - return wb; -} - -// Пример добавления пользовательской функции -export function addCustomFunctions(wb: Workbook): void { - // Функция для округления - wb.addFunction('ROUND', (number: number, digits: number = 0): number => { - const factor = Math.pow(10, digits); - return Math.round(number * factor) / factor; - }); - - // Функция для проверки на пустое значение - wb.addFunction('ISEMPTY', (value: any): boolean => { - return value === null || value === undefined || value === ''; - }); - - // Волатильные функции для работы с датой и временем - wb.addFunction('TODAY', (): string => { - return new Date().toLocaleDateString('ru-RU'); - }); - - wb.addFunction('NOW', (): string => { - return new Date().toLocaleString('ru-RU'); - }); - - // Функция для работы с текстом - wb.addFunction('LEFT', (text: string, numChars: number): string => { - return text ? text.substring(0, numChars) : ''; - }); - - wb.addFunction('RIGHT', (text: string, numChars: number): string => { - return text ? text.substring(text.length - numChars) : ''; - }); - - wb.addFunction('MID', (text: string, startPos: number, numChars: number): string => { - return text ? text.substring(startPos - 1, startPos - 1 + numChars) : ''; - }); - - // Математические функции - wb.addFunction('ABS', (number: number): number => Math.abs(number)); - wb.addFunction('SQRT', (number: number): number => Math.sqrt(number)); - wb.addFunction('POWER', (base: number, exponent: number): number => Math.pow(base, exponent)); - wb.addFunction('MOD', (dividend: number, divisor: number): number => dividend % divisor); - - // Функции для работы с датами - wb.addFunction('YEAR', (dateString: string): number => { - const date = new Date(dateString); - return date.getFullYear(); - }); - - wb.addFunction('MONTH', (dateString: string): number => { - const date = new Date(dateString); - return date.getMonth() + 1; // JavaScript months are 0-based - }); - - wb.addFunction('DAY', (dateString: string): number => { - const date = new Date(dateString); - return date.getDate(); - }); -} - -// Пример интеграции с React компонентом -export interface SpreadsheetData { - [sheetName: string]: { - [cellName: string]: any; - }; -} - -// Функция для конвертации данных из React компонента в движок -export function convertFromReactData(reactData: string[][], sheetName: string = 'Sheet1'): Workbook { - const wb = new Workbook(); - const sheet = wb.addSheet(sheetName); - - for (let row = 0; row < reactData.length; row++) { - for (let col = 0; col < reactData[row].length; col++) { - const cellName = CellUtils.createCellName(col, row + 1); - const value = reactData[row][col]; - - if (value !== null && value !== undefined && value !== '') { - sheet.setCell(cellName, value); - } - } - } - - wb.recalc(); - return wb; -} - -// Функция для конвертации данных из движка в React компонент -export function convertToReactData(wb: Workbook, sheetName: string = 'Sheet1', maxRows: number = 50, maxCols: number = 20): string[][] { - const sheet = wb.getSheet(sheetName); - if (!sheet) { - return Array(maxRows).fill(null).map(() => Array(maxCols).fill('')); - } - - const result: string[][] = Array(maxRows).fill(null).map(() => Array(maxCols).fill('')); - - for (let row = 0; row < maxRows; row++) { - for (let col = 0; col < maxCols; col++) { - const cellName = CellUtils.createCellName(col, row + 1); - const cell = sheet.getCell(cellName); - const value = cell.value; - - if (value !== null && value !== undefined) { - result[row][col] = String(value); - } - } - } - - return result; -} - -// Функция для сохранения в JSON для бэкенда -export function saveToBackend(wb: Workbook): string { - return JSON.stringify(wb.toJSON()); -} - -// Функция для загрузки из JSON с бэкенда -export function loadFromBackend(jsonData: string): Workbook { - const wb = new Workbook(); - const data = JSON.parse(jsonData); - wb.fromJSON(data); - addCustomFunctions(wb); // Добавляем пользовательские функции - wb.recalc(); // Пересчитываем все формулы - return wb; -} - -// Пример использования для тестирования -export function runExample(): void { - console.log('=== Пример использования движка электронной таблицы ==='); - - // Создаём workbook - const wb = createExampleWorkbook(); - addCustomFunctions(wb); - - // Выводим результаты с локальными ссылками - console.log('Sheet1!A1:', wb.getValue('Sheet1!A1')); // 10 - console.log('Sheet1!A2:', wb.getValue('Sheet1!A2')); // 20 - console.log('Sheet1!A3:', wb.getValue('Sheet1!A3')); // 30 (A1+A2) - console.log('Sheet1!B1:', wb.getValue('Sheet1!B1')); // 20 (A1*2) - console.log('Sheet1!B2:', wb.getValue('Sheet1!B2')); // 30 (SUM(A1,A2)) - console.log('Sheet1!C1:', wb.getValue('Sheet1!C1')); // "Результат: 30" - console.log('Sheet1!D1:', wb.getValue('Sheet1!D1')); // "Отчет по протоколу N455" - - // Пример добавления новых данных с локальными ссылками - wb.setCell('Sheet1!E1', '=A1+B1+A3'); - wb.setCell('Sheet1!E2', '=CONCATENATE("Общая сумма: ", E1)'); - - console.log('Sheet1!E1:', wb.getValue('Sheet1!E1')); // 60 - console.log('Sheet1!E2:', wb.getValue('Sheet1!E2')); // "Общая сумма: 60" - - // Пример использования пользовательских функций - wb.setCell('Sheet1!F1', '=ROUND(3.14159, 2)'); - wb.setCell('Sheet1!F2', '=TODAY()'); - - console.log('Sheet1!F1:', wb.getValue('Sheet1!F1')); - console.log('Sheet1!F2:', wb.getValue('Sheet1!F2')); - - // Сериализация для бэкенда - const serialized = saveToBackend(wb); - console.log('Serialized data length:', serialized.length); - - // Десериализация с бэкенда - const loaded = loadFromBackend(serialized); - console.log('Loaded Sheet1!A3:', loaded.getValue('Sheet1!A3')); -} \ No newline at end of file diff --git a/src/lib/template-data.ts b/src/lib/template-data.ts deleted file mode 100644 index 5982ed1..0000000 --- a/src/lib/template-data.ts +++ /dev/null @@ -1,57 +0,0 @@ -export const exampleTemplateData = { - A1: 'Отчет о продажах', - A2: 'Период: Январь 2024', - A3: '', - - A4: 'Товар', - B4: 'Количество', - C4: 'Цена', - D4: 'Сумма', - - A5: 'Товар A', - B5: 100, - C5: 1500, - D5: 150000, - - A6: 'Товар B', - B6: 50, - C6: 2000, - D6: 100000, - - A7: 'Товар C', - B7: 75, - C7: 1200, - D7: 90000, - - A8: '', - A9: 'Итого:', - D9: 340000, - - A11: 'НДС (20%):', - D11: 68000, - - A12: 'Всего к оплате:', - D12: 408000, - - A14: 'Скидка из расчетов:', - D14: '', - - A15: 'Итого со скидкой:', - D15: '' -}; - -export const exampleCalculationsData = { - A1: 'Расчеты', - A2: 'Скидка:', - B2: '=Report!D9*0.05', - - A4: 'Дополнительные расчеты:', - A5: 'Средняя цена:', - B5: '=AVERAGE(Report!C5:C7)', - - A6: 'Максимальная сумма:', - B6: '=MAX(Report!D5:D7)', - - A7: 'Минимальная сумма:', - B7: '=MIN(Report!D5:D7)', -}; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 9d485f5..bc84d20 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19,116 +19,11 @@ dependencies: regenerator-runtime "^0.13.11" -"@esbuild/android-arm64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz#bafb75234a5d3d1b690e7c2956a599345e84a2fd" - integrity sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA== - -"@esbuild/android-arm@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.19.tgz#5898f7832c2298bc7d0ab53701c57beb74d78b4d" - integrity sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A== - -"@esbuild/android-x64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.19.tgz#658368ef92067866d95fb268719f98f363d13ae1" - integrity sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww== - "@esbuild/darwin-arm64@0.17.19": version "0.17.19" resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz" integrity sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg== -"@esbuild/darwin-x64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz#7751d236dfe6ce136cce343dce69f52d76b7f6cb" - integrity sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw== - -"@esbuild/freebsd-arm64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz#cacd171665dd1d500f45c167d50c6b7e539d5fd2" - integrity sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ== - -"@esbuild/freebsd-x64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz#0769456eee2a08b8d925d7c00b79e861cb3162e4" - integrity sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ== - -"@esbuild/linux-arm64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz#38e162ecb723862c6be1c27d6389f48960b68edb" - integrity sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg== - -"@esbuild/linux-arm@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz#1a2cd399c50040184a805174a6d89097d9d1559a" - integrity sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA== - -"@esbuild/linux-ia32@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz#e28c25266b036ce1cabca3c30155222841dc035a" - integrity sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ== - -"@esbuild/linux-loong64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz#0f887b8bb3f90658d1a0117283e55dbd4c9dcf72" - integrity sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ== - -"@esbuild/linux-mips64el@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz#f5d2a0b8047ea9a5d9f592a178ea054053a70289" - integrity sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A== - -"@esbuild/linux-ppc64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz#876590e3acbd9fa7f57a2c7d86f83717dbbac8c7" - integrity sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg== - -"@esbuild/linux-riscv64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz#7f49373df463cd9f41dc34f9b2262d771688bf09" - integrity sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA== - -"@esbuild/linux-s390x@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz#e2afd1afcaf63afe2c7d9ceacd28ec57c77f8829" - integrity sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q== - -"@esbuild/linux-x64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz#8a0e9738b1635f0c53389e515ae83826dec22aa4" - integrity sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw== - -"@esbuild/netbsd-x64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz#c29fb2453c6b7ddef9a35e2c18b37bda1ae5c462" - integrity sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q== - -"@esbuild/openbsd-x64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz#95e75a391403cb10297280d524d66ce04c920691" - integrity sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g== - -"@esbuild/sunos-x64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz#722eaf057b83c2575937d3ffe5aeb16540da7273" - integrity sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg== - -"@esbuild/win32-arm64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz#9aa9dc074399288bdcdd283443e9aeb6b9552b6f" - integrity sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag== - -"@esbuild/win32-ia32@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz#95ad43c62ad62485e210f6299c7b2571e48d2b03" - integrity sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw== - -"@esbuild/win32-x64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz#8cfaf2ff603e9aabb910e9c0558c26cf32744061" - integrity sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA== - "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz" @@ -226,16 +121,16 @@ resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz" integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== -"@jridgewell/sourcemap-codec@1.4.14": - version "1.4.14" - resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz" - integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== - "@jridgewell/sourcemap-codec@^1.4.10": version "1.4.15" resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== +"@jridgewell/sourcemap-codec@1.4.14": + version "1.4.14" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + "@jridgewell/trace-mapping@^0.3.9": version "0.3.18" resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz" @@ -252,7 +147,7 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": +"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": version "2.0.5" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== @@ -649,51 +544,6 @@ resolved "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.67.tgz" integrity sha512-zCT2mCkOBVNf5uJDcQ3A9KDoO1OEaGdfjsRTZTo7sejDd9AXLfJg+xgyCBBrK2jNS/uWcT21IvSv3LqKp4K8pA== -"@swc/core-darwin-x64@1.3.67": - version "1.3.67" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.3.67.tgz#49da279b06232a388e9c9179db1cbff81d3dee18" - integrity sha512-hXTVsfTatPEec5gFVyjGj3NccKZsYj/OXyHn6XA+l3Q76lZzGm2ISHdku//XNwXu8OmJ0HhS7LPsC4XXwxXQhg== - -"@swc/core-linux-arm-gnueabihf@1.3.67": - version "1.3.67" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.67.tgz#668645ac62ea7beb4319d177f43cdefb0326cd90" - integrity sha512-l8AKL0RkDL5FRTeWMmjoz9zvAc37amxC+0rheaNwE+gZya7ObyNjnIYz5FwN+3y+z6JFU7LS2x/5f6iwruv6pg== - -"@swc/core-linux-arm64-gnu@1.3.67": - version "1.3.67" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.67.tgz#709bccc5ced37b64ab5ae479bf73fc2ab5ef0b48" - integrity sha512-S8zOB1AXEpb7kmtgMaFNeLAj01VOky4B0RNZ+uJWigdrDiFT67FeZzNHUNmNSOU0QM79G+Lie/xD/beqEw0vDg== - -"@swc/core-linux-arm64-musl@1.3.67": - version "1.3.67" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.67.tgz#9187378e17200b1ffb3d06b78c4a33f85dd12efb" - integrity sha512-Fex8J8ASrt13pmOr2xWh41tEeKWwXYGk3sV8L/aGHiYtIJEUi2f+RtMx3jp7LIdOD8pQptor7i5WBlfR9jhp8A== - -"@swc/core-linux-x64-gnu@1.3.67": - version "1.3.67" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.67.tgz#bcdaf46c430bc85a59ae9b38ab9bd540aa1fbd2d" - integrity sha512-9bz9/bMphrv5vDg0os/d8ve0QgFpDzJgZgHUaHiGwcmfnlgdOSAaYJLIvWdcGTjZuQeV4L0m+iru357D9TXEzA== - -"@swc/core-linux-x64-musl@1.3.67": - version "1.3.67" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.67.tgz#fbb63517cd72eaa3250726a4209c179ada520a57" - integrity sha512-ED0H6oLvQmhgo9zs8usmEA/lcZPGTu7K9og9K871b7HhHX0h/R+Xg2pb5KD7S/GyUHpfuopxjVROm+h6X1jMUA== - -"@swc/core-win32-arm64-msvc@1.3.67": - version "1.3.67" - resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.67.tgz#dadce08f9245c57e9c54c1bbcc815c4bd2077fba" - integrity sha512-J1yFDLgPFeRtA8t5E159OXX+ww1gbkFg70yr4OP7EsOkOD1uMkuTf9yK/woHfsaVJlUYjJHzw7MkUIEgQBucqQ== - -"@swc/core-win32-ia32-msvc@1.3.67": - version "1.3.67" - resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.67.tgz#be026a3a389e64c24fe67a329c04eccf744ac45e" - integrity sha512-bK11/KtasewqHxzkjKUBXRE9MSAidbZCxrgJUd49bItG2N/DHxkwMYu8Xkh5VDHdTYWv/2idYtf/VM9Yi+53qw== - -"@swc/core-win32-x64-msvc@1.3.67": - version "1.3.67" - resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.67.tgz#6fe2f3854d91b58f6e0b00f99366cfd84334b2ea" - integrity sha512-GxzUU3+NA3cPcYxCxtfSQIS2ySD7Z8IZmKTVaWA9GOUQbKLyCE8H5js31u39+0op/1gNgxOgYFDoj2lUyvLCqw== - "@swc/core@^1.3.61": version "1.3.67" resolved "https://registry.npmjs.org/@swc/core/-/core-1.3.67.tgz" @@ -730,12 +580,12 @@ "@types/react-beautiful-dnd@^13.1.8": version "13.1.8" - resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.8.tgz#f52d3ea07e1e19159d6c3c4a48c8da3d855e60b4" + resolved "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.8.tgz" integrity sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ== dependencies: "@types/react" "*" -"@types/react-dom@^18.0.11": +"@types/react-dom@*", "@types/react-dom@^16.8 || ^17.0 || ^18.0", "@types/react-dom@^18.0.11": version "18.2.6" resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.6.tgz" integrity sha512-2et4PDvg6PVCyS7fuTc4gPoksV58bW0RwSxWKcPRcHZf0PRUGq03TKcD/rUHe3azfV6/5/biUBJw+HhCQjaP0A== @@ -744,7 +594,7 @@ "@types/react-grid-layout@^1.3.5": version "1.3.5" - resolved "https://registry.yarnpkg.com/@types/react-grid-layout/-/react-grid-layout-1.3.5.tgz#f4b52bf27775290ee0523214be0987be14e66823" + resolved "https://registry.npmjs.org/@types/react-grid-layout/-/react-grid-layout-1.3.5.tgz" integrity sha512-WH/po1gcEcoR6y857yAnPGug+ZhkF4PaTUxgAbwfeSH/QOgVSakKHBXoPGad/sEznmkiaK3pqHk+etdWisoeBQ== dependencies: "@types/react" "*" @@ -759,7 +609,7 @@ hoist-non-react-statics "^3.3.0" redux "^4.0.0" -"@types/react@*", "@types/react@^18.0.37": +"@types/react@*", "@types/react@^16.8 || ^17.0 || ^18.0", "@types/react@^18.0.37": version "18.2.14" resolved "https://registry.npmjs.org/@types/react/-/react-18.2.14.tgz" integrity sha512-A0zjq+QN/O0Kpe30hA1GidzyFjatVvrpIvWLxD+xv67Vt91TWWgco9IvrJBkeyHm1trGaFS/FSGqPlhyeZRm0g== @@ -799,7 +649,7 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/parser@^5.59.0": +"@typescript-eslint/parser@^5.0.0", "@typescript-eslint/parser@^5.59.0": version "5.60.1" resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.60.1.tgz" integrity sha512-pHWlc3alg2oSMGwsU/Is8hbm3XFbcrb6P5wIxcQW9NsYBfnrubl/GhVVD/Jm/t8HXhA2WncoIRfBtnCgRGV96Q== @@ -879,7 +729,7 @@ acorn-jsx@^5.3.2: resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.9.0: +"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.9.0: version "8.9.0" resolved "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz" integrity sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ== @@ -983,7 +833,7 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" -browserslist@^4.21.5: +browserslist@^4.21.5, "browserslist@>= 4.21.0": version "4.21.9" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz" integrity sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg== @@ -1247,7 +1097,7 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz" integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== -eslint@^8.38.0: +eslint@*, "eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "eslint@^6.0.0 || ^7.0.0 || ^8.0.0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", eslint@^8.38.0, eslint@>=7: version "8.44.0" resolved "https://registry.npmjs.org/eslint/-/eslint-8.44.0.tgz" integrity sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A== @@ -1337,7 +1187,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: fast-equals@^4.0.3: version "4.0.3" - resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-4.0.3.tgz#72884cc805ec3c6679b99875f6b7654f39f0e8c7" + resolved "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz" integrity sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg== fast-glob@^3.2.12, fast-glob@^3.2.9: @@ -1452,18 +1302,6 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob@7.1.6: - version "7.1.6" - resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - glob@^7.1.3: version "7.2.3" resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" @@ -1476,6 +1314,18 @@ glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" +glob@7.1.6: + version "7.1.6" + resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + globals@^13.19.0: version "13.20.0" resolved "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz" @@ -1529,6 +1379,11 @@ ignore@^5.2.0: resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== +immer@^10.1.1: + version "10.1.1" + resolved "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz" + integrity sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw== + immer@^9.0.21: version "9.0.21" resolved "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz" @@ -1898,7 +1753,7 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0: resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.4.23, postcss@^8.4.24: +postcss@^8, postcss@^8.0.0, postcss@^8.1.0, postcss@^8.2.14, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.24, postcss@>=8.0.9: version "8.4.24" resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz" integrity sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg== @@ -1917,7 +1772,7 @@ prettier-plugin-tailwindcss@^0.4.1: resolved "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.4.1.tgz" integrity sha512-hwn2EiJmv8M+AW4YDkbjJ6HlZCTzLyz1QlySn9sMuKV/Px0fjwldlB7tol8GzdgqtkdPtzT3iJ4UzdnYXP25Ag== -prettier@2.8.8: +"prettier@^2.2 || ^3.0", prettier@2.8.8: version "2.8.8" resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== @@ -1927,7 +1782,7 @@ process@^0.11.1: resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== -prop-types@15.x, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.7.2, prop-types@^15.8.1, prop-types@15.x: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -1953,7 +1808,7 @@ raf-schd@^4.0.2: react-beautiful-dnd@^13.1.1: version "13.1.1" - resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz#b0f3087a5840920abf8bb2325f1ffa46d8c4d0a2" + resolved "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz" integrity sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ== dependencies: "@babel/runtime" "^7.9.2" @@ -1964,7 +1819,7 @@ react-beautiful-dnd@^13.1.1: redux "^4.0.4" use-memo-one "^1.1.1" -react-dom@^18.2.0: +"react-dom@^16.8 || ^17.0 || ^18.0", "react-dom@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom@^16.8.5 || ^17.0.0 || ^18.0.0", react-dom@^18.2.0, "react-dom@>= 16.3.0", react-dom@>=16.8, react-dom@>=16.8.0: version "18.2.0" resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz" integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== @@ -1974,7 +1829,7 @@ react-dom@^18.2.0: react-draggable@^4.0.3, react-draggable@^4.4.6: version "4.5.0" - resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.5.0.tgz#0b274ccb6965fcf97ed38fcf7e3cc223bc48cdf5" + resolved "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz" integrity sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw== dependencies: clsx "^2.1.1" @@ -1982,7 +1837,7 @@ react-draggable@^4.0.3, react-draggable@^4.4.6: react-grid-layout@^1.5.2: version "1.5.2" - resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-1.5.2.tgz#d5a6775446ce540c0df3985c41b5d64622fc6f87" + resolved "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-1.5.2.tgz" integrity sha512-vT7xmQqszTT+sQw/LfisrEO4le1EPNnSEMVHy6sBZyzS3yGkMywdOd+5iEFFwQwt0NSaGkxuRmYwa1JsP6OJdw== dependencies: clsx "^2.1.1" @@ -1992,7 +1847,12 @@ react-grid-layout@^1.5.2: react-resizable "^3.0.5" resize-observer-polyfill "^1.5.1" -react-is@^16.13.1, react-is@^16.7.0: +react-is@^16.13.1: + version "16.13.1" + resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-is@^16.7.0: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -2019,7 +1879,7 @@ react-redux@^7.2.0: prop-types "^15.7.2" react-is "^17.0.2" -react-redux@^8.1.1: +"react-redux@^7.2.1 || ^8.0.2", react-redux@^8.1.1: version "8.1.1" resolved "https://registry.npmjs.org/react-redux/-/react-redux-8.1.1.tgz" integrity sha512-5W0QaKtEhj+3bC0Nj0NkqkhIv8gLADH/2kYFMTHxCVqQILiWzLv6MaLuV5wJU3BQEdHKzTfcvPN0WMS6SC1oyA== @@ -2052,7 +1912,7 @@ react-remove-scroll@^2.6.3: react-resizable@^3.0.5: version "3.0.5" - resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-3.0.5.tgz#362721f2efbd094976f1780ae13f1ad7739786c1" + resolved "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.5.tgz" integrity sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w== dependencies: prop-types "15.x" @@ -2081,7 +1941,7 @@ react-style-singleton@^2.2.2, react-style-singleton@^2.2.3: get-nonce "^1.0.0" tslib "^2.0.0" -react@^18.2.0: +"react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8 || ^17.0 || ^18.0", "react@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.8.3 || ^17 || ^18", "react@^16.8.5 || ^17.0.0 || ^18.0.0", "react@^16.9.0 || ^17.0.0 || ^18", react@^18.2.0, "react@>= 16.3", "react@>= 16.3.0", react@>=16.8, react@>=16.8.0: version "18.2.0" resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz" integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== @@ -2107,7 +1967,7 @@ redux-thunk@^2.4.2: resolved "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz" integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q== -redux@^4.0.0, redux@^4.0.4, redux@^4.2.1: +redux@^4, "redux@^4 || ^5.0.0-beta.0", redux@^4.0.0, redux@^4.0.4, redux@^4.2.1: version "4.2.1" resolved "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz" integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== @@ -2126,7 +1986,7 @@ reselect@^4.1.8: resize-observer-polyfill@^1.5.1: version "1.5.1" - resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + resolved "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz" integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== resolve-from@^4.0.0: @@ -2324,7 +2184,12 @@ tslib@^1.8.1: resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.1.0: +tslib@^2.0.0: + version "2.8.1" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +tslib@^2.1.0: version "2.8.1" resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== @@ -2348,7 +2213,7 @@ type-fest@^0.20.2: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -typescript@^5.0.2: +typescript@^5.0.2, "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta": version "5.1.6" resolved "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz" integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== @@ -2405,7 +2270,7 @@ util@^0.10.3: dependencies: inherits "2.0.3" -vite@^4.3.9: +vite@^4, vite@^4.3.9: version "4.3.9" resolved "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz" integrity sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==