рефакторинг

This commit is contained in:
2025-07-15 16:06:10 +03:00
parent df12bde43f
commit 90ebcd3772
14 changed files with 1722 additions and 1867 deletions

View File

@@ -2,8 +2,8 @@
"trailingComma": "all", "trailingComma": "all",
"printWidth": 80, "printWidth": 80,
"tabWidth": 2, "tabWidth": 2,
"semi": true, "semi": false,
"singleQuote": false, "singleQuote": true,
"bracketSpacing": true, "bracketSpacing": true,
"bracketSameLine": false "bracketSameLine": false
} }

85
package-lock.json generated
View File

@@ -18,12 +18,15 @@
"@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-toast": "^1.2.14", "@radix-ui/react-toast": "^1.2.14",
"@reduxjs/toolkit": "^1.9.5", "@reduxjs/toolkit": "^1.9.5",
"@types/react-grid-layout": "^1.3.5",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"immer": "^10.1.1",
"lucide-react": "^0.525.0", "lucide-react": "^0.525.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1", "react-beautiful-dnd": "^13.1.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-grid-layout": "^1.5.2",
"redux": "^4.2.1", "redux": "^4.2.1",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"xlsx": "^0.18.5" "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": { "node_modules/@remix-run/router": {
"version": "1.7.1", "version": "1.7.1",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.7.1.tgz", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.7.1.tgz",
@@ -1309,6 +1322,15 @@
"@types/react": "*" "@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": { "node_modules/@types/react-redux": {
"version": "7.1.34", "version": "7.1.34",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz", "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz",
@@ -2415,6 +2437,12 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/fast-glob": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz",
@@ -2721,9 +2749,9 @@
} }
}, },
"node_modules/immer": { "node_modules/immer": {
"version": "9.0.21", "version": "10.1.1",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
@@ -3673,6 +3701,38 @@
"react": "^18.2.0" "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": { "node_modules/react-is": {
"version": "18.2.0", "version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
@@ -3779,6 +3839,19 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD" "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": { "node_modules/react-router": {
"version": "6.14.1", "version": "6.14.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.14.1.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.14.1.tgz",
@@ -3894,6 +3967,12 @@
"integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==", "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==",
"license": "MIT" "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": { "node_modules/resolve": {
"version": "1.22.2", "version": "1.22.2",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",

View File

@@ -24,6 +24,7 @@
"@types/react-grid-layout": "^1.3.5", "@types/react-grid-layout": "^1.3.5",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"immer": "^10.1.1",
"lucide-react": "^0.525.0", "lucide-react": "^0.525.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1", "react-beautiful-dnd": "^13.1.1",

File diff suppressed because it is too large Load Diff

View File

@@ -93,7 +93,7 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
Редактирование шаблона: {editedTemplate.name} Редактирование шаблона: {editedTemplate.name}
</h1> </h1>
<p className="text-sm text-gray-600 mt-1"> <p className="text-sm text-gray-600 mt-1">
Настройте Excel шаблон и создайте элементы интерфейса Настройте Excel шаблон и значения отчета
</p> </p>
</div> </div>
</div> </div>

View File

@@ -45,19 +45,19 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({ onTemplateSele
setSelectedTemplate(updatedTemplate); setSelectedTemplate(updatedTemplate);
}; };
const handleTemplateDelete = (templateId: string) => { // const handleTemplateDelete = (templateId: string) => {
deleteTemplate(templateId); // deleteTemplate(templateId);
if (selectedTemplate?.id === templateId) { // if (selectedTemplate?.id === templateId) {
setSelectedTemplate(null); // setSelectedTemplate(null);
setIsEditMode(false); // setIsEditMode(false);
} // }
}; // };
const handleTemplateSelect = (template: Template) => { // const handleTemplateSelect = (template: Template) => {
setSelectedTemplate(template); // setSelectedTemplate(template);
setIsEditMode(false); // setIsEditMode(false);
onTemplateSelect?.(template); // onTemplateSelect?.(template);
}; // };
const handleEditTemplate = (template: Template) => { const handleEditTemplate = (template: Template) => {
setSelectedTemplate(template); setSelectedTemplate(template);
@@ -191,7 +191,7 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({ onTemplateSele
Создан: {template.createdAt.toLocaleDateString('ru-RU')} Создан: {template.createdAt.toLocaleDateString('ru-RU')}
</div> </div>
</div> </div>
<div className="flex gap-2 mt-4"> {/* <div className="flex gap-2 mt-4">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
@@ -200,7 +200,7 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({ onTemplateSele
> >
Предпросмотр Предпросмотр
</Button> </Button>
</div> </div> */}
</CardContent> </CardContent>
</Card> </Card>
))} ))}

View File

@@ -1,239 +1,252 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react'
import { CellValue, Workbook } from '../lib/spreadsheet-engine'; import {
CellValue,
Workbook,
} from '../lib/spreadsheet-engine/spreadsheet-engine'
interface CellData { interface CellData {
value: string; value: string
isSelected: boolean; isSelected: boolean
} }
interface SheetConfig { interface SheetConfig {
name: string; name: string
rows: number; rows: number
cols: number; cols: number
} }
interface UseMultiSheetEngineProps { interface UseMultiSheetEngineProps {
sheets: SheetConfig[]; sheets: SheetConfig[]
initialData?: Record<string, Record<string, any>>; initialData?: Record<string, Record<string, any>>
} }
export function useMultiSheetEngine({ export function useMultiSheetEngine({
sheets, sheets,
initialData = {} initialData = {},
}: UseMultiSheetEngineProps) { }: UseMultiSheetEngineProps) {
const workbookRef = useRef<Workbook | null>(null); const workbookRef = useRef<Workbook | null>(null)
const [isCalculating, setIsCalculating] = useState(false); const [isCalculating, setIsCalculating] = useState(false)
const [sheetCells, setSheetCells] = useState<Record<string, CellData[][]>>({}); const [revision, setRevision] = useState(0) // Новый счетчик для обновлений
const debouncedRecalcTimeoutRef = useRef<number | null>(null); const debouncedRecalcTimeoutRef = useRef<number | null>(null)
const isRecalculatingRef = useRef(false); const isRecalculatingRef = useRef(false)
// Инициализация workbook и листов // Инициализация workbook и листов
useEffect(() => { useEffect(() => {
if (!workbookRef.current) { if (!workbookRef.current) {
workbookRef.current = new Workbook(); workbookRef.current = new Workbook()
// Создаем листы
sheets.forEach(sheetConfig => {
workbookRef.current!.addSheet(sheetConfig.name);
});
// Инициализируем состояние ячеек для каждого листа // Создаем листы
const initialSheetCells: Record<string, CellData[][]> = {}; sheets.forEach((sheetConfig) => {
sheets.forEach(sheetConfig => { workbookRef.current!.addSheet(sheetConfig.name)
initialSheetCells[sheetConfig.name] = Array(sheetConfig.rows).fill(null).map(() => })
Array(sheetConfig.cols).fill(null).map(() => ({ value: '', isSelected: false }))
);
});
setSheetCells(initialSheetCells);
// Загружаем начальные данные // Загружаем начальные данные
if (Object.keys(initialData).length > 0) { if (Object.keys(initialData).length > 0) {
workbookRef.current.fromJSON(initialData); workbookRef.current.fromJSON(initialData)
syncAllSheetsFromEngine(); // Первый пересчет для инициализации всех значений
safeRecalc()
} else {
// Увеличиваем ревизию, чтобы вызвать начальную отрисовку
setRevision((r) => r + 1)
} }
} }
}, [sheets, initialData]); // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) // Зависимости убраны, чтобы инициализация проходила один раз
// Синхронизация всех листов из движка // Синхронизация всех листов из движка
const syncAllSheetsFromEngine = useCallback(() => { const syncAllSheetsFromEngine = useCallback(() => {
if (!workbookRef.current || isRecalculatingRef.current) return; if (!workbookRef.current || isRecalculatingRef.current) return
const newSheetCells: Record<string, CellData[][]> = {}; const newSheetCells: Record<string, CellData[][]> = {}
sheets.forEach(sheetConfig => {
const sheet = workbookRef.current!.getSheet(sheetConfig.name);
if (!sheet) return;
const cells = Array(sheetConfig.rows).fill(null).map(() => sheets.forEach((sheetConfig) => {
Array(sheetConfig.cols).fill(null).map(() => ({ value: '', isSelected: false })) 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 row = 0; row < sheetConfig.rows; row++) {
for (let col = 0; col < sheetConfig.cols; col++) { for (let col = 0; col < sheetConfig.cols; col++) {
const cellName = columnToLabel(col) + (row + 1); const cellName = columnToLabel(col) + (row + 1)
const cell = sheet.getCell(cellName); const cell = sheet.getCell(cellName)
if (cell.raw !== null && cell.raw !== undefined) { 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); // setSheetCells(newSheetCells) // Удалено, так как sheetCells убрано
}, [sheets]); }, [sheets])
// Безопасная функция для вычислений // Безопасная функция для вычислений
const safeRecalc = useCallback(() => { const safeRecalc = useCallback(() => {
if (!workbookRef.current || isRecalculatingRef.current) return; if (!workbookRef.current || isRecalculatingRef.current) return
isRecalculatingRef.current = true; isRecalculatingRef.current = true
setIsCalculating(true); setIsCalculating(true)
try { try {
workbookRef.current.recalc(); workbookRef.current.recalc()
syncAllSheetsFromEngine();
} catch (error) { } catch (error) {
console.warn('Ошибка при вычислении:', error); console.warn('Ошибка при вычислении:', error)
} finally { } finally {
// Небольшая задержка для визуального индикатора // Небольшая задержка для визуального индикатора
setTimeout(() => { setTimeout(() => {
setIsCalculating(false); setIsCalculating(false)
isRecalculatingRef.current = false; isRecalculatingRef.current = false
}, 150); // Увеличиваем ревизию, чтобы сообщить компонентам о необходимости обновления
setRevision((r) => r + 1)
}, 150)
} }
}, [syncAllSheetsFromEngine]); }, [])
// Отложенный пересчет с дебаунсом // Отложенный пересчет с дебаунсом
const debouncedRecalc = useCallback(() => { const debouncedRecalc = useCallback(() => {
if (!workbookRef.current || isRecalculatingRef.current) return; if (!workbookRef.current || isRecalculatingRef.current) return
// Очищаем предыдущий отложенный пересчет // Очищаем предыдущий отложенный пересчет
if (debouncedRecalcTimeoutRef.current) { if (debouncedRecalcTimeoutRef.current) {
clearTimeout(debouncedRecalcTimeoutRef.current); clearTimeout(debouncedRecalcTimeoutRef.current)
} }
// Запускаем пересчет через 300ms // Запускаем пересчет через 300ms
debouncedRecalcTimeoutRef.current = setTimeout(() => { debouncedRecalcTimeoutRef.current = setTimeout(() => {
safeRecalc(); safeRecalc()
}, 300); }, 300)
}, [safeRecalc]); }, [safeRecalc])
// Установка значения ячейки без немедленного пересчета // Установка значения ячейки без немедленного пересчета
const setCellValueWithoutRecalc = useCallback((sheetName: string, row: number, col: number, value: string) => { const setCellValueWithoutRecalc = useCallback(
if (!workbookRef.current || isRecalculatingRef.current) return; (sheetName: string, row: number, col: number, value: string) => {
if (!workbookRef.current || isRecalculatingRef.current) return
const cellName = columnToLabel(col) + (row + 1); const cellName = columnToLabel(col) + (row + 1)
const qualifiedName = `${sheetName}!${cellName}`; const qualifiedName = `${sheetName}!${cellName}`
// Устанавливаем значение в движке без пересчета // Устанавливаем значение в движке без пересчета
workbookRef.current.setCell(qualifiedName, value); 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 setCellValue = useCallback((sheetName: string, row: number, col: number, value: string) => { const setCellValue = useCallback(
if (!workbookRef.current || isRecalculatingRef.current) return; (sheetName: string, row: number, col: number, value: string) => {
if (!workbookRef.current || isRecalculatingRef.current) return
const cellName = columnToLabel(col) + (row + 1); const cellName = columnToLabel(col) + (row + 1)
const qualifiedName = `${sheetName}!${cellName}`; const qualifiedName = `${sheetName}!${cellName}`
// Устанавливаем значение в движке // Устанавливаем значение в движке
workbookRef.current.setCell(qualifiedName, value); workbookRef.current.setCell(qualifiedName, value)
// Запускаем безопасный пересчет // Запускаем безопасный пересчет
safeRecalc(); safeRecalc()
}, [safeRecalc]); },
[safeRecalc],
)
// Получение значения ячейки (вычисленное) // Получение значения ячейки (вычисленное)
const getCellValue = useCallback((sheetName: string, row: number, col: number): CellValue => { const getCellValue = useCallback(
if (!workbookRef.current) return ''; (sheetName: string, row: number, col: number): CellValue => {
if (!workbookRef.current) return ''
const cellName = columnToLabel(col) + (row + 1); const cellName = columnToLabel(col) + (row + 1)
const qualifiedName = `${sheetName}!${cellName}`; const qualifiedName = `${sheetName}!${cellName}`
return workbookRef.current.getValue(qualifiedName); return workbookRef.current.getValue(qualifiedName)
}, []); },
[],
)
// Получение сырого значения ячейки (формула) // Получение сырого значения ячейки (формула)
const getCellRawValue = useCallback((sheetName: string, row: number, col: number): string => { const getCellRawValue = useCallback(
if (!workbookRef.current) return ''; (sheetName: string, row: number, col: number): string => {
if (!workbookRef.current) return ''
const sheet = workbookRef.current.getSheet(sheetName); const sheet = workbookRef.current.getSheet(sheetName)
if (!sheet) return ''; if (!sheet) return ''
const cellName = columnToLabel(col) + (row + 1); const cellName = columnToLabel(col) + (row + 1)
const cell = sheet.getCell(cellName); const cell = sheet.getCell(cellName)
return String(cell.raw || ''); return String(cell.raw || '')
}, []); },
[],
)
// Получение всех измененных ячеек для листа // Получение всех измененных ячеек для листа
const getModifiedCells = useCallback((sheetName: string): Record<string, CellValue> => { const getModifiedCells = useCallback(
if (!workbookRef.current) return {}; (sheetName: string): Record<string, CellValue> => {
if (!workbookRef.current) return {}
const sheet = workbookRef.current.getSheet(sheetName); const sheet = workbookRef.current.getSheet(sheetName)
if (!sheet) return {}; if (!sheet) return {}
return sheet.toJSON(); return sheet.toJSON()
}, []); },
[],
)
// Получение всех измененных ячеек для всех листов // Получение всех измененных ячеек для всех листов
const getAllModifiedCells = useCallback((): Record<string, Record<string, CellValue>> => { const getAllModifiedCells = useCallback((): Record<
if (!workbookRef.current) return {}; string,
Record<string, CellValue>
> => {
if (!workbookRef.current) return {}
return workbookRef.current.toJSON(); return workbookRef.current.toJSON()
}, []); }, [])
// Очистка всех данных // Очистка всех данных
const clearAllData = useCallback(() => { const clearAllData = useCallback(() => {
if (!workbookRef.current) return; if (!workbookRef.current) return
workbookRef.current.clear(); workbookRef.current.clear()
syncAllSheetsFromEngine(); setRevision((r) => r + 1)
}, [syncAllSheetsFromEngine]); }, [])
// Загрузка данных из JSON // Загрузка данных из JSON
const loadData = useCallback((data: Record<string, Record<string, any>>) => { const loadData = useCallback(
if (!workbookRef.current) return; (data: Record<string, Record<string, any>>) => {
if (!workbookRef.current) return
workbookRef.current.fromJSON(data); workbookRef.current.fromJSON(data)
syncAllSheetsFromEngine(); safeRecalc()
}, [syncAllSheetsFromEngine]); },
[safeRecalc],
)
// Пересчет всех формул // Пересчет всех формул
const recalculate = useCallback(() => { const recalculate = useCallback(() => {
if (!workbookRef.current) return; if (!workbookRef.current) return
safeRecalc(); safeRecalc()
}, [safeRecalc]); }, [safeRecalc])
// Очистка таймаутов при размонтировании // Очистка таймаутов при размонтировании
useEffect(() => { useEffect(() => {
return () => { return () => {
if (debouncedRecalcTimeoutRef.current) { if (debouncedRecalcTimeoutRef.current) {
clearTimeout(debouncedRecalcTimeoutRef.current); clearTimeout(debouncedRecalcTimeoutRef.current)
} }
isRecalculatingRef.current = false; isRecalculatingRef.current = false
}; }
}, []); }, [])
return { return {
sheetCells, // sheetCells убран
setCellValue, setCellValue,
setCellValueWithoutRecalc, setCellValueWithoutRecalc,
debouncedRecalc, debouncedRecalc,
@@ -245,11 +258,12 @@ export function useMultiSheetEngine({
loadData, loadData,
recalculate, recalculate,
isCalculating, isCalculating,
workbook: workbookRef.current workbook: workbookRef.current,
}; revision, // Экспортируем ревизию
}
} }
// Вспомогательная функция для конвертации индекса колонки в букву // Вспомогательная функция для конвертации индекса колонки в букву
function columnToLabel(index: number): string { function columnToLabel(index: number): string {
return String.fromCharCode(65 + index); return String.fromCharCode(65 + index)
} }

View File

@@ -1,286 +1,321 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react'
import { CellUtils, CellValue, Workbook } from '../lib/spreadsheet-engine'; import {
import { addCustomFunctions } from '../lib/spreadsheet-example'; CellUtils,
CellValue,
Workbook,
} from '../lib/spreadsheet-engine/spreadsheet-engine'
import { addCustomFunctions } from '../lib/spreadsheet-example'
interface CellData { interface CellData {
value: string; value: string
isSelected: boolean; isSelected: boolean
} }
interface UseSpreadsheetEngineProps { interface UseSpreadsheetEngineProps {
rows?: number; rows?: number
cols?: number; cols?: number
sheetName?: string; sheetName?: string
initialData?: string[][]; initialData?: string[][]
} }
export function useSpreadsheetEngine({ export function useSpreadsheetEngine({
rows = 50, rows = 50,
cols = 20, cols = 20,
sheetName = 'Sheet1', sheetName = 'Sheet1',
initialData initialData,
}: UseSpreadsheetEngineProps = {}) { }: UseSpreadsheetEngineProps = {}) {
const workbookRef = useRef<Workbook | null>(null); const workbookRef = useRef<Workbook | null>(null)
const [cells, setCells] = useState<CellData[][]>(() => { const [cells, setCells] = useState<CellData[][]>(() => {
return Array(rows).fill(null).map(() => return Array(rows)
Array(cols).fill(null).map(() => ({ value: '', isSelected: false })) .fill(null)
); .map(() =>
}); Array(cols)
const [isCalculating, setIsCalculating] = useState(false); .fill(null)
.map(() => ({ value: '', isSelected: false })),
)
})
const [isCalculating, setIsCalculating] = useState(false)
// Инициализация workbook // Инициализация workbook
useEffect(() => { useEffect(() => {
if (!workbookRef.current) { if (!workbookRef.current) {
workbookRef.current = new Workbook(); workbookRef.current = new Workbook()
workbookRef.current.addSheet(sheetName); workbookRef.current.addSheet(sheetName)
addCustomFunctions(workbookRef.current); addCustomFunctions(workbookRef.current)
} }
// Загружаем начальные данные если они есть // Загружаем начальные данные если они есть
if (initialData) { if (initialData) {
loadDataFromArray(initialData); loadDataFromArray(initialData)
} }
}, [sheetName, initialData]); }, [sheetName, initialData])
// Конвертация данных из движка в React состояние // Конвертация данных из движка в React состояние
const syncFromEngine = useCallback(() => { const syncFromEngine = useCallback(() => {
if (!workbookRef.current) return; if (!workbookRef.current) return
const sheet = workbookRef.current.getSheet(sheetName); const sheet = workbookRef.current.getSheet(sheetName)
if (!sheet) return; if (!sheet) return
const newCells = Array(rows).fill(null).map(() => const newCells = Array(rows)
Array(cols).fill(null).map(() => ({ value: '', isSelected: false })) .fill(null)
); .map(() =>
Array(cols)
.fill(null)
.map(() => ({ value: '', isSelected: false })),
)
// Получаем все ячейки из движка // Получаем все ячейки из движка
for (let row = 0; row < rows; row++) { for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) { for (let col = 0; col < cols; col++) {
const cellName = CellUtils.createCellName(col, row + 1); const cellName = CellUtils.createCellName(col, row + 1)
const cell = sheet.getCell(cellName); const cell = sheet.getCell(cellName)
// Показываем сырое значение (raw) в UI, а не вычисленное // Показываем сырое значение (raw) в UI, а не вычисленное
if (cell.raw !== null && cell.raw !== undefined) { if (cell.raw !== null && cell.raw !== undefined) {
newCells[row][col].value = String(cell.raw); newCells[row][col].value = String(cell.raw)
} }
} }
} }
setCells(newCells); setCells(newCells)
}, [rows, cols, sheetName]); }, [rows, cols, sheetName])
// Установка значения ячейки // Установка значения ячейки
const setCellValue = useCallback((row: number, col: number, value: string) => { const setCellValue = useCallback(
if (!workbookRef.current) return; (row: number, col: number, value: string) => {
if (!workbookRef.current) return
const cellName = CellUtils.createCellName(col, row + 1); const cellName = CellUtils.createCellName(col, row + 1)
const qualifiedName = `${sheetName}!${cellName}`; const qualifiedName = `${sheetName}!${cellName}`
setIsCalculating(true); setIsCalculating(true)
try { try {
// Конвертируем пустую строку в null // Конвертируем пустую строку в null
const cellValue: CellValue = value === '' ? null : value; const cellValue: CellValue = value === '' ? null : value
// Если значение начинается с =, то это формула // Если значение начинается с =, то это формула
if (typeof cellValue === 'string' && cellValue.startsWith('=')) { if (typeof cellValue === 'string' && cellValue.startsWith('=')) {
workbookRef.current.setCell(qualifiedName, cellValue); workbookRef.current.setCell(qualifiedName, cellValue)
} else {
// Пытаемся конвертировать в число, если возможно
const numValue = Number(cellValue);
if (!isNaN(numValue) && cellValue !== null && cellValue !== '') {
workbookRef.current.setCell(qualifiedName, numValue);
} else { } else {
workbookRef.current.setCell(qualifiedName, cellValue); // Пытаемся конвертировать в число, если возможно
} const numValue = Number(cellValue)
} if (!isNaN(numValue) && cellValue !== null && cellValue !== '') {
workbookRef.current.setCell(qualifiedName, numValue)
// Пересчитываем все формулы } else {
workbookRef.current.recalc(); workbookRef.current.setCell(qualifiedName, cellValue)
// Синхронизируем с 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);
} }
} }
// Пересчитываем все формулы
workbookRef.current.recalc()
// Синхронизируем с React состоянием
syncFromEngine()
} catch (error) {
console.error('Error setting cell value:', error)
} finally {
setIsCalculating(false)
} }
},
// Пересчитываем все формулы [sheetName, syncFromEngine],
workbookRef.current.recalc(); )
// Синхронизируем с React состоянием // Получение вычисленного значения ячейки
syncFromEngine(); const getCellValue = useCallback(
} catch (error) { (row: number, col: number): CellValue => {
console.error('Error loading data:', error); if (!workbookRef.current) return null
} finally {
setIsCalculating(false); const cellName = CellUtils.createCellName(col, row + 1)
} const qualifiedName = `${sheetName}!${cellName}`
}, [rows, cols, sheetName, syncFromEngine]);
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[][] => { const exportToArray = useCallback((): string[][] => {
if (!workbookRef.current) return []; if (!workbookRef.current) return []
const sheet = workbookRef.current.getSheet(sheetName); const sheet = workbookRef.current.getSheet(sheetName)
if (!sheet) return []; 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 row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) { for (let col = 0; col < cols; col++) {
const cellName = CellUtils.createCellName(col, row + 1); const cellName = CellUtils.createCellName(col, row + 1)
const cell = sheet.getCell(cellName); const cell = sheet.getCell(cellName)
if (cell.raw !== null && cell.raw !== undefined) { if (cell.raw !== null && cell.raw !== undefined) {
result[row][col] = String(cell.raw); result[row][col] = String(cell.raw)
} }
} }
} }
return result; return result
}, [rows, cols, sheetName]); }, [rows, cols, sheetName])
// Экспорт в JSON для бэкенда // Экспорт в JSON для бэкенда
const exportToJSON = useCallback((): string => { const exportToJSON = useCallback((): string => {
if (!workbookRef.current) return '{}'; if (!workbookRef.current) return '{}'
return JSON.stringify(workbookRef.current.toJSON()); return JSON.stringify(workbookRef.current.toJSON())
}, []); }, [])
// Импорт из JSON с бэкенда // Импорт из JSON с бэкенда
const importFromJSON = useCallback((jsonData: string) => { const importFromJSON = useCallback(
if (!workbookRef.current) return; (jsonData: string) => {
if (!workbookRef.current) return
setIsCalculating(true);
setIsCalculating(true)
try {
const data = JSON.parse(jsonData); try {
workbookRef.current.fromJSON(data); const data = JSON.parse(jsonData)
addCustomFunctions(workbookRef.current); workbookRef.current.fromJSON(data)
workbookRef.current.recalc(); addCustomFunctions(workbookRef.current)
syncFromEngine(); workbookRef.current.recalc()
} catch (error) { syncFromEngine()
console.error('Error importing from JSON:', error); } catch (error) {
} finally { console.error('Error importing from JSON:', error)
setIsCalculating(false); } finally {
} setIsCalculating(false)
}, [syncFromEngine]); }
},
[syncFromEngine],
)
// Добавление пользовательской функции // Добавление пользовательской функции
const addFunction = useCallback((name: string, func: (...args: any[]) => any) => { const addFunction = useCallback(
if (!workbookRef.current) return; (name: string, func: (...args: any[]) => any) => {
workbookRef.current.addFunction(name, func); if (!workbookRef.current) return
}, []); workbookRef.current.addFunction(name, func)
},
[],
)
// Пересчёт всех формул // Пересчёт всех формул
const recalculate = useCallback(() => { const recalculate = useCallback(() => {
if (!workbookRef.current) return; if (!workbookRef.current) return
setIsCalculating(true); setIsCalculating(true)
try { try {
workbookRef.current.recalc(); workbookRef.current.recalc()
syncFromEngine(); syncFromEngine()
} catch (error) { } catch (error) {
console.error('Error recalculating:', error); console.error('Error recalculating:', error)
} finally { } finally {
setIsCalculating(false); setIsCalculating(false)
} }
}, [syncFromEngine]); }, [syncFromEngine])
// Получение информации о зависимостях ячейки // Получение информации о зависимостях ячейки
const getCellDependencies = useCallback((row: number, col: number) => { const getCellDependencies = useCallback(
if (!workbookRef.current) return { precedents: [], dependents: [] }; (row: number, col: number) => {
if (!workbookRef.current) return { precedents: [], dependents: [] }
const cellName = CellUtils.createCellName(col, row + 1); const cellName = CellUtils.createCellName(col, row + 1)
const sheet = workbookRef.current.getSheet(sheetName); const sheet = workbookRef.current.getSheet(sheetName)
if (!sheet) return { precedents: [], dependents: [] }; if (!sheet) return { precedents: [], dependents: [] }
const cell = sheet.getCell(cellName); const cell = sheet.getCell(cellName)
return { return {
precedents: Array.from(cell.precedents), precedents: Array.from(cell.precedents),
dependents: Array.from(cell.dependents) dependents: Array.from(cell.dependents),
}; }
}, [sheetName]); },
[sheetName],
)
// Получение статистики workbook // Получение статистики workbook
const getWorkbookStats = useCallback(() => { 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()) { for (const sheet of workbookRef.current.sheets.values()) {
totalCells += sheet.cells.size; totalCells += sheet.cells.size
for (const cell of sheet.cells.values()) { for (const cell of sheet.cells.values()) {
if (typeof cell.raw === 'string' && cell.raw.startsWith('=')) { if (typeof cell.raw === 'string' && cell.raw.startsWith('=')) {
totalFormulas++; totalFormulas++
} }
} }
} }
return { return {
sheets: workbookRef.current.sheets.size, sheets: workbookRef.current.sheets.size,
cells: totalCells, cells: totalCells,
formulas: totalFormulas formulas: totalFormulas,
}; }
}, []); }, [])
return { return {
cells, cells,
@@ -296,6 +331,6 @@ export function useSpreadsheetEngine({
getCellDependencies, getCellDependencies,
getWorkbookStats, getWorkbookStats,
isCalculating, isCalculating,
workbook: workbookRef.current workbook: workbookRef.current,
}; }
} }

View File

@@ -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<string, ExcelFunction> = {
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<string> = new Set();
dependents: Set<string> = 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<string, Cell> = 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<string, Cell> {
return this.cells;
}
toJSON(): Record<string, CellValue> {
const result: Record<string, CellValue> = {};
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<string, CellValue>): void {
for (const [name, value] of Object.entries(data)) {
this.setCell(name, value);
}
}
}
export class Workbook {
sheets: Map<string, Sheet> = new Map();
private _dirty: Set<string> = new Set();
private _functions: Record<string, ExcelFunction> = { ...DEFAULT_FUNCTIONS };
private _volatileCells: Set<string> = 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<string> {
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<string, ExcelFunction> {
return this._functions;
}
// Помечаем все волатильные ячейки как "грязные"
private markAllVolatileCellsDirty(): void {
for (const volatileCell of this._volatileCells) {
this.markDirty(volatileCell);
}
}
// Сериализация
toJSON(): Record<string, Record<string, CellValue>> {
const result: Record<string, Record<string, CellValue>> = {};
for (const [name, sheet] of this.sheets) {
result[name] = sheet.toJSON();
}
return result;
}
fromJSON(data: Record<string, Record<string, CellValue>>): 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 };

View File

@@ -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<string, ExcelFunction> = {
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
},
}

View File

@@ -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<string> = new Set()
dependents: Set<string> = 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<string, Cell> = 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<string, Cell> {
return this.cells
}
toJSON(): Record<string, CellValue> {
const result: Record<string, CellValue> = {}
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<string, CellValue>): void {
for (const [name, value] of Object.entries(data)) {
this.setCell(name, value)
}
}
}
export class Workbook {
sheets: Map<string, Sheet> = new Map()
private _dirty: Set<string> = new Set()
private _functions: Record<string, ExcelFunction> = { ...DEFAULT_FUNCTIONS }
private _volatileCells: Set<string> = 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<string> {
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<string, ExcelFunction> {
return this._functions
}
// Помечаем все волатильные ячейки как "грязные"
private markAllVolatileCellsDirty(): void {
for (const volatileCell of this._volatileCells) {
this.markDirty(volatileCell)
}
}
// Сериализация
toJSON(): Record<string, Record<string, CellValue>> {
const result: Record<string, Record<string, CellValue>> = {}
for (const [name, sheet] of this.sheets) {
result[name] = sheet.toJSON()
}
return result
}
fromJSON(data: Record<string, Record<string, CellValue>>): 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 }

View File

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

View File

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

251
yarn.lock
View File

@@ -19,116 +19,11 @@
dependencies: dependencies:
regenerator-runtime "^0.13.11" 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": "@esbuild/darwin-arm64@0.17.19":
version "0.17.19" version "0.17.19"
resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz" resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz"
integrity sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg== 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": "@eslint-community/eslint-utils@^4.2.0":
version "4.4.0" version "4.4.0"
resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz" 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" resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz"
integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== 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": "@jridgewell/sourcemap-codec@^1.4.10":
version "1.4.15" version "1.4.15"
resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz" resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz"
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== 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": "@jridgewell/trace-mapping@^0.3.9":
version "0.3.18" version "0.3.18"
resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz" resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz"
@@ -252,7 +147,7 @@
"@nodelib/fs.stat" "2.0.5" "@nodelib/fs.stat" "2.0.5"
run-parallel "^1.1.9" 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" version "2.0.5"
resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== 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" resolved "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.67.tgz"
integrity sha512-zCT2mCkOBVNf5uJDcQ3A9KDoO1OEaGdfjsRTZTo7sejDd9AXLfJg+xgyCBBrK2jNS/uWcT21IvSv3LqKp4K8pA== 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": "@swc/core@^1.3.61":
version "1.3.67" version "1.3.67"
resolved "https://registry.npmjs.org/@swc/core/-/core-1.3.67.tgz" resolved "https://registry.npmjs.org/@swc/core/-/core-1.3.67.tgz"
@@ -730,12 +580,12 @@
"@types/react-beautiful-dnd@^13.1.8": "@types/react-beautiful-dnd@^13.1.8":
version "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== integrity sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==
dependencies: dependencies:
"@types/react" "*" "@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" version "18.2.6"
resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.6.tgz" resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.6.tgz"
integrity sha512-2et4PDvg6PVCyS7fuTc4gPoksV58bW0RwSxWKcPRcHZf0PRUGq03TKcD/rUHe3azfV6/5/biUBJw+HhCQjaP0A== integrity sha512-2et4PDvg6PVCyS7fuTc4gPoksV58bW0RwSxWKcPRcHZf0PRUGq03TKcD/rUHe3azfV6/5/biUBJw+HhCQjaP0A==
@@ -744,7 +594,7 @@
"@types/react-grid-layout@^1.3.5": "@types/react-grid-layout@^1.3.5":
version "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== integrity sha512-WH/po1gcEcoR6y857yAnPGug+ZhkF4PaTUxgAbwfeSH/QOgVSakKHBXoPGad/sEznmkiaK3pqHk+etdWisoeBQ==
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
@@ -759,7 +609,7 @@
hoist-non-react-statics "^3.3.0" hoist-non-react-statics "^3.3.0"
redux "^4.0.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" version "18.2.14"
resolved "https://registry.npmjs.org/@types/react/-/react-18.2.14.tgz" resolved "https://registry.npmjs.org/@types/react/-/react-18.2.14.tgz"
integrity sha512-A0zjq+QN/O0Kpe30hA1GidzyFjatVvrpIvWLxD+xv67Vt91TWWgco9IvrJBkeyHm1trGaFS/FSGqPlhyeZRm0g== integrity sha512-A0zjq+QN/O0Kpe30hA1GidzyFjatVvrpIvWLxD+xv67Vt91TWWgco9IvrJBkeyHm1trGaFS/FSGqPlhyeZRm0g==
@@ -799,7 +649,7 @@
semver "^7.3.7" semver "^7.3.7"
tsutils "^3.21.0" 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" version "5.60.1"
resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.60.1.tgz" resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.60.1.tgz"
integrity sha512-pHWlc3alg2oSMGwsU/Is8hbm3XFbcrb6P5wIxcQW9NsYBfnrubl/GhVVD/Jm/t8HXhA2WncoIRfBtnCgRGV96Q== 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" resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== 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" version "8.9.0"
resolved "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz" resolved "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz"
integrity sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ== integrity sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==
@@ -983,7 +833,7 @@ braces@^3.0.2, braces@~3.0.2:
dependencies: dependencies:
fill-range "^7.0.1" fill-range "^7.0.1"
browserslist@^4.21.5: browserslist@^4.21.5, "browserslist@>= 4.21.0":
version "4.21.9" version "4.21.9"
resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz"
integrity sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg== 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" resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz"
integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== 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" version "8.44.0"
resolved "https://registry.npmjs.org/eslint/-/eslint-8.44.0.tgz" resolved "https://registry.npmjs.org/eslint/-/eslint-8.44.0.tgz"
integrity sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A== 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: fast-equals@^4.0.3:
version "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== integrity sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==
fast-glob@^3.2.12, fast-glob@^3.2.9: fast-glob@^3.2.12, fast-glob@^3.2.9:
@@ -1452,18 +1302,6 @@ glob-parent@^6.0.2:
dependencies: dependencies:
is-glob "^4.0.3" 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: glob@^7.1.3:
version "7.2.3" version "7.2.3"
resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz"
@@ -1476,6 +1314,18 @@ glob@^7.1.3:
once "^1.3.0" once "^1.3.0"
path-is-absolute "^1.0.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: globals@^13.19.0:
version "13.20.0" version "13.20.0"
resolved "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz" 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" resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz"
integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== 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: immer@^9.0.21:
version "9.0.21" version "9.0.21"
resolved "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz" 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" resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== 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" version "8.4.24"
resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz" resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz"
integrity sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg== 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" resolved "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.4.1.tgz"
integrity sha512-hwn2EiJmv8M+AW4YDkbjJ6HlZCTzLyz1QlySn9sMuKV/Px0fjwldlB7tol8GzdgqtkdPtzT3iJ4UzdnYXP25Ag== integrity sha512-hwn2EiJmv8M+AW4YDkbjJ6HlZCTzLyz1QlySn9sMuKV/Px0fjwldlB7tol8GzdgqtkdPtzT3iJ4UzdnYXP25Ag==
prettier@2.8.8: "prettier@^2.2 || ^3.0", prettier@2.8.8:
version "2.8.8" version "2.8.8"
resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz" resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz"
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== 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" resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz"
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== 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" version "15.8.1"
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -1953,7 +1808,7 @@ raf-schd@^4.0.2:
react-beautiful-dnd@^13.1.1: react-beautiful-dnd@^13.1.1:
version "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== integrity sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==
dependencies: dependencies:
"@babel/runtime" "^7.9.2" "@babel/runtime" "^7.9.2"
@@ -1964,7 +1819,7 @@ react-beautiful-dnd@^13.1.1:
redux "^4.0.4" redux "^4.0.4"
use-memo-one "^1.1.1" 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" version "18.2.0"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz" resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz"
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== 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: react-draggable@^4.0.3, react-draggable@^4.4.6:
version "4.5.0" 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== integrity sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==
dependencies: dependencies:
clsx "^2.1.1" clsx "^2.1.1"
@@ -1982,7 +1837,7 @@ react-draggable@^4.0.3, react-draggable@^4.4.6:
react-grid-layout@^1.5.2: react-grid-layout@^1.5.2:
version "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== integrity sha512-vT7xmQqszTT+sQw/LfisrEO4le1EPNnSEMVHy6sBZyzS3yGkMywdOd+5iEFFwQwt0NSaGkxuRmYwa1JsP6OJdw==
dependencies: dependencies:
clsx "^2.1.1" clsx "^2.1.1"
@@ -1992,7 +1847,12 @@ react-grid-layout@^1.5.2:
react-resizable "^3.0.5" react-resizable "^3.0.5"
resize-observer-polyfill "^1.5.1" 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" version "16.13.1"
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -2019,7 +1879,7 @@ react-redux@^7.2.0:
prop-types "^15.7.2" prop-types "^15.7.2"
react-is "^17.0.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" version "8.1.1"
resolved "https://registry.npmjs.org/react-redux/-/react-redux-8.1.1.tgz" resolved "https://registry.npmjs.org/react-redux/-/react-redux-8.1.1.tgz"
integrity sha512-5W0QaKtEhj+3bC0Nj0NkqkhIv8gLADH/2kYFMTHxCVqQILiWzLv6MaLuV5wJU3BQEdHKzTfcvPN0WMS6SC1oyA== integrity sha512-5W0QaKtEhj+3bC0Nj0NkqkhIv8gLADH/2kYFMTHxCVqQILiWzLv6MaLuV5wJU3BQEdHKzTfcvPN0WMS6SC1oyA==
@@ -2052,7 +1912,7 @@ react-remove-scroll@^2.6.3:
react-resizable@^3.0.5: react-resizable@^3.0.5:
version "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== integrity sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==
dependencies: dependencies:
prop-types "15.x" 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" get-nonce "^1.0.0"
tslib "^2.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" version "18.2.0"
resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz" resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz"
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== 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" resolved "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz"
integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q== 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" version "4.2.1"
resolved "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz" resolved "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz"
integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==
@@ -2126,7 +1986,7 @@ reselect@^4.1.8:
resize-observer-polyfill@^1.5.1: resize-observer-polyfill@^1.5.1:
version "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== integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
resolve-from@^4.0.0: resolve-from@^4.0.0:
@@ -2324,7 +2184,12 @@ tslib@^1.8.1:
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== 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" version "2.8.1"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== 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" resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz"
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== 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" version "5.1.6"
resolved "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz" resolved "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz"
integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==
@@ -2405,7 +2270,7 @@ util@^0.10.3:
dependencies: dependencies:
inherits "2.0.3" inherits "2.0.3"
vite@^4.3.9: vite@^4, vite@^4.3.9:
version "4.3.9" version "4.3.9"
resolved "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz" resolved "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz"
integrity sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg== integrity sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==