рефакторинг
This commit is contained in:
@@ -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
85
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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 => {
|
sheets.forEach((sheetConfig) => {
|
||||||
workbookRef.current!.addSheet(sheetConfig.name);
|
workbookRef.current!.addSheet(sheetConfig.name)
|
||||||
});
|
})
|
||||||
|
|
||||||
// Инициализируем состояние ячеек для каждого листа
|
|
||||||
const initialSheetCells: Record<string, CellData[][]> = {};
|
|
||||||
sheets.forEach(sheetConfig => {
|
|
||||||
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 => {
|
sheets.forEach((sheetConfig) => {
|
||||||
const sheet = workbookRef.current!.getSheet(sheetConfig.name);
|
const sheet = workbookRef.current!.getSheet(sheetConfig.name)
|
||||||
if (!sheet) return;
|
if (!sheet) return
|
||||||
|
|
||||||
const cells = Array(sheetConfig.rows).fill(null).map(() =>
|
const cells = Array(sheetConfig.rows)
|
||||||
Array(sheetConfig.cols).fill(null).map(() => ({ value: '', isSelected: false }))
|
.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)
|
||||||
}
|
}
|
||||||
@@ -1,276 +1,311 @@
|
|||||||
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 {
|
} else {
|
||||||
// Пытаемся конвертировать в число, если возможно
|
// Пытаемся конвертировать в число, если возможно
|
||||||
const numValue = Number(cellValue);
|
const numValue = Number(cellValue)
|
||||||
if (!isNaN(numValue) && cellValue !== null && cellValue !== '') {
|
if (!isNaN(numValue) && cellValue !== null && cellValue !== '') {
|
||||||
workbookRef.current.setCell(qualifiedName, numValue);
|
workbookRef.current.setCell(qualifiedName, numValue)
|
||||||
} else {
|
} else {
|
||||||
workbookRef.current.setCell(qualifiedName, cellValue);
|
workbookRef.current.setCell(qualifiedName, cellValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Пересчитываем все формулы
|
// Пересчитываем все формулы
|
||||||
workbookRef.current.recalc();
|
workbookRef.current.recalc()
|
||||||
|
|
||||||
// Синхронизируем с React состоянием
|
// Синхронизируем с React состоянием
|
||||||
syncFromEngine();
|
syncFromEngine()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting cell value:', error);
|
console.error('Error setting cell value:', error)
|
||||||
} finally {
|
} finally {
|
||||||
setIsCalculating(false);
|
setIsCalculating(false)
|
||||||
}
|
}
|
||||||
}, [sheetName, syncFromEngine]);
|
},
|
||||||
|
[sheetName, syncFromEngine],
|
||||||
|
)
|
||||||
|
|
||||||
// Получение вычисленного значения ячейки
|
// Получение вычисленного значения ячейки
|
||||||
const getCellValue = useCallback((row: number, col: number): CellValue => {
|
const getCellValue = useCallback(
|
||||||
if (!workbookRef.current) return null;
|
(row: number, col: number): CellValue => {
|
||||||
|
if (!workbookRef.current) return null
|
||||||
|
|
||||||
const cellName = CellUtils.createCellName(col, row + 1);
|
const cellName = CellUtils.createCellName(col, row + 1)
|
||||||
const qualifiedName = `${sheetName}!${cellName}`;
|
const qualifiedName = `${sheetName}!${cellName}`
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return workbookRef.current.getValue(qualifiedName);
|
return workbookRef.current.getValue(qualifiedName)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting cell value:', error);
|
console.error('Error getting cell value:', error)
|
||||||
return '#ERROR!';
|
return '#ERROR!'
|
||||||
}
|
}
|
||||||
}, [sheetName]);
|
},
|
||||||
|
[sheetName],
|
||||||
|
)
|
||||||
|
|
||||||
// Получение сырого значения ячейки (для отображения в строке формул)
|
// Получение сырого значения ячейки (для отображения в строке формул)
|
||||||
const getCellRawValue = useCallback((row: number, col: number): string => {
|
const getCellRawValue = useCallback(
|
||||||
if (!workbookRef.current) return '';
|
(row: number, col: number): string => {
|
||||||
|
if (!workbookRef.current) return ''
|
||||||
|
|
||||||
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 '';
|
if (!sheet) return ''
|
||||||
|
|
||||||
const cell = sheet.getCell(cellName);
|
const cell = sheet.getCell(cellName)
|
||||||
return cell.raw !== null && cell.raw !== undefined ? String(cell.raw) : '';
|
return cell.raw !== null && cell.raw !== undefined ? String(cell.raw) : ''
|
||||||
}, [sheetName]);
|
},
|
||||||
|
[sheetName],
|
||||||
|
)
|
||||||
|
|
||||||
// Загрузка данных из массива
|
// Загрузка данных из массива
|
||||||
const loadDataFromArray = useCallback((data: string[][]) => {
|
const loadDataFromArray = useCallback(
|
||||||
if (!workbookRef.current) return;
|
(data: string[][]) => {
|
||||||
|
if (!workbookRef.current) return
|
||||||
|
|
||||||
const sheet = workbookRef.current.getSheet(sheetName);
|
const sheet = workbookRef.current.getSheet(sheetName)
|
||||||
if (!sheet) return;
|
if (!sheet) return
|
||||||
|
|
||||||
setIsCalculating(true);
|
setIsCalculating(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Очищаем лист
|
// Очищаем лист
|
||||||
sheet.cells.clear();
|
sheet.cells.clear()
|
||||||
|
|
||||||
// Загружаем данные
|
// Загружаем данные
|
||||||
for (let row = 0; row < data.length && row < rows; row++) {
|
for (let row = 0; row < data.length && row < rows; row++) {
|
||||||
for (let col = 0; col < data[row].length && col < cols; col++) {
|
for (let col = 0; col < data[row].length && col < cols; col++) {
|
||||||
const value = data[row][col];
|
const value = data[row][col]
|
||||||
if (value !== null && value !== undefined && value !== '') {
|
if (value !== null && value !== undefined && value !== '') {
|
||||||
const cellName = CellUtils.createCellName(col, row + 1);
|
const cellName = CellUtils.createCellName(col, row + 1)
|
||||||
sheet.setCell(cellName, value);
|
sheet.setCell(cellName, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Пересчитываем все формулы
|
// Пересчитываем все формулы
|
||||||
workbookRef.current.recalc();
|
workbookRef.current.recalc()
|
||||||
|
|
||||||
// Синхронизируем с React состоянием
|
// Синхронизируем с React состоянием
|
||||||
syncFromEngine();
|
syncFromEngine()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading data:', error);
|
console.error('Error loading data:', error)
|
||||||
} finally {
|
} finally {
|
||||||
setIsCalculating(false);
|
setIsCalculating(false)
|
||||||
}
|
}
|
||||||
}, [rows, cols, sheetName, syncFromEngine]);
|
},
|
||||||
|
[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 {
|
try {
|
||||||
const data = JSON.parse(jsonData);
|
const data = JSON.parse(jsonData)
|
||||||
workbookRef.current.fromJSON(data);
|
workbookRef.current.fromJSON(data)
|
||||||
addCustomFunctions(workbookRef.current);
|
addCustomFunctions(workbookRef.current)
|
||||||
workbookRef.current.recalc();
|
workbookRef.current.recalc()
|
||||||
syncFromEngine();
|
syncFromEngine()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error importing from JSON:', error);
|
console.error('Error importing from JSON:', error)
|
||||||
} finally {
|
} finally {
|
||||||
setIsCalculating(false);
|
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 totalCells = 0
|
||||||
let totalFormulas = 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++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,9 +313,9 @@ export function useSpreadsheetEngine({
|
|||||||
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,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
@@ -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 };
|
|
||||||
88
src/lib/spreadsheet-engine/functions.ts
Normal file
88
src/lib/spreadsheet-engine/functions.ts
Normal 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
|
||||||
|
},
|
||||||
|
}
|
||||||
478
src/lib/spreadsheet-engine/spreadsheet-engine.ts
Normal file
478
src/lib/spreadsheet-engine/spreadsheet-engine.ts
Normal 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 }
|
||||||
@@ -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'));
|
|
||||||
}
|
|
||||||
@@ -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
251
yarn.lock
@@ -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==
|
||||||
|
|||||||
Reference in New Issue
Block a user