удаление неиспользуемого
This commit is contained in:
@@ -1,10 +1,5 @@
|
||||
import {
|
||||
getFileData,
|
||||
getFilesByTemplateId,
|
||||
getLatestFileForTemplate,
|
||||
parseExcelFile,
|
||||
} from '@/service/fileApiService'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { getFileData, getLatestFileForTemplate } from '@/service/fileApiService'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
// Ключи для React Query
|
||||
export const fileKeys = {
|
||||
@@ -16,17 +11,6 @@ export const fileKeys = {
|
||||
data: (fileId: string) => [...fileKeys.all, 'data', fileId] as const,
|
||||
}
|
||||
|
||||
// Хук для получения файлов по template_id с кешированием
|
||||
export function useFilesByTemplate(templateId: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: fileKeys.byTemplate(templateId || ''),
|
||||
queryFn: () => getFilesByTemplateId(templateId!),
|
||||
enabled: !!templateId,
|
||||
staleTime: 5 * 60 * 1000, // 5 минут
|
||||
gcTime: 10 * 60 * 1000, // 10 минут кеш
|
||||
})
|
||||
}
|
||||
|
||||
// Хук для получения последнего файла для шаблона
|
||||
export function useLatestFileForTemplate(templateId: string | undefined) {
|
||||
return useQuery({
|
||||
@@ -48,38 +32,3 @@ export function useFileData(fileId: string | undefined) {
|
||||
gcTime: 30 * 60 * 1000, // 30 минут кеш
|
||||
})
|
||||
}
|
||||
|
||||
// Хук для загрузки Excel файла
|
||||
export function useParseExcelFile() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ file, templateId }: { file: File; templateId: string }) =>
|
||||
parseExcelFile(file, templateId),
|
||||
onSuccess: (data, variables) => {
|
||||
// Инвалидируем связанные кеши после успешной загрузки
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: fileKeys.byTemplate(variables.templateId),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: fileKeys.latest(variables.templateId),
|
||||
})
|
||||
|
||||
// Сохраняем данные файла в кеш
|
||||
queryClient.setQueryData(fileKeys.data(data.fileId), data)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Хук для предзагрузки данных файла
|
||||
export function usePrefetchFileData() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return (fileId: string) => {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: fileKeys.data(fileId),
|
||||
queryFn: () => getFileData(fileId),
|
||||
staleTime: 10 * 60 * 1000,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,14 +69,7 @@ export function useMultiSheetEngine({
|
||||
if (!workbookRef.current) return
|
||||
|
||||
// Предотвращаем случайную очистку данных при смене вкладки
|
||||
if (document.hidden) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log(
|
||||
'⚠️ useMultiSheetEngine: предотвращаем загрузку данных когда страница скрыта'
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (document.hidden) return
|
||||
|
||||
// Создаем ключ для проверки изменений данных
|
||||
const dataKey = JSON.stringify(initialData)
|
||||
@@ -85,21 +78,10 @@ export function useMultiSheetEngine({
|
||||
if (
|
||||
loadedDataRef.current === dataKey &&
|
||||
Object.keys(initialData).length > 0
|
||||
) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log('⏭️ useMultiSheetEngine: данные уже загружены, пропускаем')
|
||||
}
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (Object.keys(initialData).length > 0) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log(
|
||||
'🔄 useMultiSheetEngine: загрузка initialData:',
|
||||
Object.keys(initialData)
|
||||
)
|
||||
}
|
||||
|
||||
// Очищаем только те листы, для которых есть новые данные
|
||||
for (const sheetName of Object.keys(initialData)) {
|
||||
const sheet = workbookRef.current.getSheet(sheetName)
|
||||
@@ -117,42 +99,6 @@ export function useMultiSheetEngine({
|
||||
}
|
||||
}, [initialData, safeRecalc])
|
||||
|
||||
// Синхронизация всех листов из движка
|
||||
// const syncAllSheetsFromEngine = useCallback(() => {
|
||||
// if (!workbookRef.current || isRecalculatingRef.current) return
|
||||
|
||||
// const newSheetCells: Record<string, CellData[][]> = {}
|
||||
|
||||
// sheets.forEach((sheetConfig) => {
|
||||
// const sheet = workbookRef.current!.getSheet(sheetConfig.name)
|
||||
// if (!sheet) return
|
||||
|
||||
// const cells = Array(sheetConfig.rows)
|
||||
// .fill(null)
|
||||
// .map(() =>
|
||||
// Array(sheetConfig.cols)
|
||||
// .fill(null)
|
||||
// .map(() => ({ value: '', isSelected: false })),
|
||||
// )
|
||||
|
||||
// // Получаем все ячейки из движка
|
||||
// for (let row = 0; row < sheetConfig.rows; row++) {
|
||||
// for (let col = 0; col < sheetConfig.cols; col++) {
|
||||
// const cellName = columnToLabel(col) + (row + 1)
|
||||
// const cell = sheet.getCell(cellName)
|
||||
|
||||
// if (cell.raw !== null && cell.raw !== undefined) {
|
||||
// cells[row][col] = String(cell.raw)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// newSheetCells[sheetConfig.name] = cells
|
||||
// })
|
||||
|
||||
// // setSheetCells(newSheetCells) // Удалено, так как sheetCells убрано
|
||||
// }, [sheets])
|
||||
|
||||
// Отложенный пересчет с дебаунсом
|
||||
const debouncedRecalc = useCallback(() => {
|
||||
if (!workbookRef.current || isRecalculatingRef.current) return
|
||||
@@ -276,15 +222,6 @@ export function useMultiSheetEngine({
|
||||
const setTemplateData = useCallback(
|
||||
(data: Record<string, Record<string, any>>) => {
|
||||
templateDataRef.current = JSON.parse(JSON.stringify(data)) // Глубокая копия
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log('📋 Установлены базовые данные шаблона:', {
|
||||
sheets: Object.keys(templateDataRef.current),
|
||||
cellCount: Object.values(templateDataRef.current).reduce(
|
||||
(total, sheet) => total + Object.keys(sheet).length,
|
||||
0
|
||||
),
|
||||
})
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
@@ -333,16 +270,6 @@ export function useMultiSheetEngine({
|
||||
if (normalizedCurrent !== normalizedTemplate) {
|
||||
// Если текущего значения нет (ячейка очищена), отправляем ''
|
||||
modifiedSheet[cellAddress] = currentVal
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log(
|
||||
`🔧 Пользовательское изменение ${sheetName}:${cellAddress}:`,
|
||||
{
|
||||
template: normalizedTemplate,
|
||||
current: normalizedCurrent,
|
||||
value: currentVal,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -351,22 +278,6 @@ export function useMultiSheetEngine({
|
||||
}
|
||||
})
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const totalModified = Object.values(modifiedData).reduce(
|
||||
(total, sheet) => total + Object.keys(sheet).length,
|
||||
0
|
||||
)
|
||||
console.log(`📊 Найдено ${totalModified} измененных ячеек:`, {
|
||||
sheets: Object.keys(modifiedData),
|
||||
details: Object.fromEntries(
|
||||
Object.entries(modifiedData).map(([sheetName, cells]) => [
|
||||
sheetName,
|
||||
Object.keys(cells).length,
|
||||
])
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return modifiedData
|
||||
}, [])
|
||||
|
||||
@@ -405,7 +316,6 @@ export function useMultiSheetEngine({
|
||||
}, [])
|
||||
|
||||
return {
|
||||
// sheetCells убран
|
||||
setCellValue,
|
||||
setCellValueWithoutRecalc,
|
||||
debouncedRecalc,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {
|
||||
createSheet,
|
||||
getSheet,
|
||||
getSheets,
|
||||
getSheetsByTemplate,
|
||||
patchSheet,
|
||||
type ApiSheet,
|
||||
@@ -18,16 +16,6 @@ export const sheetKeys = {
|
||||
detail: (sheetId: string) => [...sheetKeys.all, 'detail', sheetId] as const,
|
||||
}
|
||||
|
||||
// Хук для получения всех листов
|
||||
export function useSheets() {
|
||||
return useQuery({
|
||||
queryKey: sheetKeys.all,
|
||||
queryFn: getSheets,
|
||||
staleTime: 2 * 60 * 1000, // 2 минуты
|
||||
gcTime: 10 * 60 * 1000, // 10 минут кеш
|
||||
})
|
||||
}
|
||||
|
||||
// Хук для получения листов по template_id с кешированием
|
||||
export function useSheetsByTemplate(templateId: string | undefined) {
|
||||
return useQuery({
|
||||
@@ -40,17 +28,6 @@ export function useSheetsByTemplate(templateId: string | undefined) {
|
||||
})
|
||||
}
|
||||
|
||||
// Хук для получения конкретного листа
|
||||
export function useSheet(sheetId: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: sheetKeys.detail(sheetId || ''),
|
||||
queryFn: () => getSheet(sheetId!),
|
||||
enabled: !!sheetId,
|
||||
staleTime: 5 * 60 * 1000, // 5 минут
|
||||
gcTime: 10 * 60 * 1000, // 10 минут кеш
|
||||
})
|
||||
}
|
||||
|
||||
// Хук для создания листа
|
||||
export function useCreateSheet() {
|
||||
const queryClient = useQueryClient()
|
||||
@@ -149,48 +126,3 @@ export function usePatchSheet() {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Хук для массового обновления листов (для автосохранения)
|
||||
export function useBatchUpdateSheets() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (
|
||||
updates: Array<{
|
||||
sheetId: string
|
||||
data: PatchSheetInput
|
||||
templateId: string
|
||||
}>
|
||||
) => {
|
||||
// Выполняем все обновления параллельно
|
||||
const results = await Promise.allSettled(
|
||||
updates.map(({ sheetId, data }) => patchSheet(sheetId, data))
|
||||
)
|
||||
|
||||
return results
|
||||
},
|
||||
onSuccess: (results, variables) => {
|
||||
// Обновляем кеши для всех затронутых шаблонов
|
||||
const templateIds = new Set(variables.map(v => v.templateId))
|
||||
|
||||
templateIds.forEach(templateId => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: sheetKeys.byTemplate(templateId),
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Хук для предзагрузки листов шаблона
|
||||
export function usePrefetchSheetsByTemplate() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return (templateId: string) => {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: sheetKeys.byTemplate(templateId),
|
||||
queryFn: () => getSheetsByTemplate(templateId),
|
||||
staleTime: 1 * 60 * 1000,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,335 +0,0 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
CellUtils,
|
||||
CellValue,
|
||||
Workbook,
|
||||
} from '../feature/excelEngine/excelEngine'
|
||||
|
||||
interface CellData {
|
||||
value: string
|
||||
isSelected: boolean
|
||||
}
|
||||
|
||||
interface UseSpreadsheetEngineProps {
|
||||
rows?: number
|
||||
cols?: number
|
||||
sheetName?: string
|
||||
initialData?: string[][]
|
||||
}
|
||||
|
||||
export function useSpreadsheetEngine({
|
||||
rows = 50,
|
||||
cols = 20,
|
||||
sheetName = 'Sheet1',
|
||||
initialData,
|
||||
}: UseSpreadsheetEngineProps = {}) {
|
||||
const workbookRef = useRef<Workbook | null>(null)
|
||||
const [cells, setCells] = useState<CellData[][]>(() => {
|
||||
return Array(rows)
|
||||
.fill(null)
|
||||
.map(() =>
|
||||
Array(cols)
|
||||
.fill(null)
|
||||
.map(() => ({ value: '', isSelected: false }))
|
||||
)
|
||||
})
|
||||
const [isCalculating, setIsCalculating] = useState(false)
|
||||
|
||||
// Инициализация workbook
|
||||
useEffect(() => {
|
||||
if (!workbookRef.current) {
|
||||
workbookRef.current = new Workbook()
|
||||
workbookRef.current.addSheet(sheetName)
|
||||
// addCustomFunctions(workbookRef.current)
|
||||
}
|
||||
|
||||
// Загружаем начальные данные если они есть
|
||||
if (initialData) {
|
||||
loadDataFromArray(initialData)
|
||||
}
|
||||
}, [sheetName, initialData])
|
||||
|
||||
// Конвертация данных из движка в React состояние
|
||||
const syncFromEngine = useCallback(() => {
|
||||
if (!workbookRef.current) return
|
||||
|
||||
const sheet = workbookRef.current.getSheet(sheetName)
|
||||
if (!sheet) return
|
||||
|
||||
const newCells = Array(rows)
|
||||
.fill(null)
|
||||
.map(() =>
|
||||
Array(cols)
|
||||
.fill(null)
|
||||
.map(() => ({ value: '', isSelected: false }))
|
||||
)
|
||||
|
||||
// Получаем все ячейки из движка
|
||||
for (let row = 0; row < rows; row++) {
|
||||
for (let col = 0; col < cols; col++) {
|
||||
const cellName = CellUtils.createCellName(col, row + 1)
|
||||
const cell = sheet.getCell(cellName)
|
||||
|
||||
// Показываем сырое значение (raw) в UI, а не вычисленное
|
||||
if (cell.raw !== null && cell.raw !== undefined) {
|
||||
newCells[row][col].value = String(cell.raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setCells(newCells)
|
||||
}, [rows, cols, sheetName])
|
||||
|
||||
// Установка значения ячейки
|
||||
const setCellValue = useCallback(
|
||||
(row: number, col: number, value: string) => {
|
||||
if (!workbookRef.current) return
|
||||
|
||||
const cellName = CellUtils.createCellName(col, row + 1)
|
||||
const qualifiedName = `${sheetName}!${cellName}`
|
||||
|
||||
setIsCalculating(true)
|
||||
|
||||
try {
|
||||
// Конвертируем пустую строку в null
|
||||
const cellValue: CellValue = value === '' ? null : value
|
||||
|
||||
// Если значение начинается с =, то это формула
|
||||
if (typeof cellValue === 'string' && cellValue.startsWith('=')) {
|
||||
workbookRef.current.setCell(qualifiedName, cellValue)
|
||||
} else {
|
||||
// Пытаемся конвертировать в число, если возможно
|
||||
const numValue = Number(cellValue)
|
||||
if (!isNaN(numValue) && cellValue !== null && cellValue !== '') {
|
||||
workbookRef.current.setCell(qualifiedName, numValue)
|
||||
} else {
|
||||
workbookRef.current.setCell(qualifiedName, cellValue)
|
||||
}
|
||||
}
|
||||
|
||||
// Пересчитываем все формулы
|
||||
workbookRef.current.recalc()
|
||||
|
||||
// Синхронизируем с React состоянием
|
||||
syncFromEngine()
|
||||
} catch (error) {
|
||||
console.error('Error setting cell value:', error)
|
||||
} finally {
|
||||
setIsCalculating(false)
|
||||
}
|
||||
},
|
||||
[sheetName, syncFromEngine]
|
||||
)
|
||||
|
||||
// Получение вычисленного значения ячейки
|
||||
const getCellValue = useCallback(
|
||||
(row: number, col: number): CellValue => {
|
||||
if (!workbookRef.current) return null
|
||||
|
||||
const cellName = CellUtils.createCellName(col, row + 1)
|
||||
const qualifiedName = `${sheetName}!${cellName}`
|
||||
|
||||
try {
|
||||
return workbookRef.current.getValue(qualifiedName)
|
||||
} catch (error) {
|
||||
console.error('Error getting cell value:', error)
|
||||
return '#ERROR!'
|
||||
}
|
||||
},
|
||||
[sheetName]
|
||||
)
|
||||
|
||||
// Получение сырого значения ячейки (для отображения в строке формул)
|
||||
const getCellRawValue = useCallback(
|
||||
(row: number, col: number): string => {
|
||||
if (!workbookRef.current) return ''
|
||||
|
||||
const cellName = CellUtils.createCellName(col, row + 1)
|
||||
const sheet = workbookRef.current.getSheet(sheetName)
|
||||
|
||||
if (!sheet) return ''
|
||||
|
||||
const cell = sheet.getCell(cellName)
|
||||
return cell.raw !== null && cell.raw !== undefined ? String(cell.raw) : ''
|
||||
},
|
||||
[sheetName]
|
||||
)
|
||||
|
||||
// Загрузка данных из массива
|
||||
const loadDataFromArray = useCallback(
|
||||
(data: string[][]) => {
|
||||
if (!workbookRef.current) return
|
||||
|
||||
const sheet = workbookRef.current.getSheet(sheetName)
|
||||
if (!sheet) return
|
||||
|
||||
setIsCalculating(true)
|
||||
|
||||
try {
|
||||
// Очищаем лист
|
||||
sheet.cells.clear()
|
||||
|
||||
// Загружаем данные
|
||||
for (let row = 0; row < data.length && row < rows; row++) {
|
||||
for (let col = 0; col < data[row].length && col < cols; col++) {
|
||||
const value = data[row][col]
|
||||
if (value !== null && value !== undefined && value !== '') {
|
||||
const cellName = CellUtils.createCellName(col, row + 1)
|
||||
sheet.setCell(cellName, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Пересчитываем все формулы
|
||||
workbookRef.current.recalc()
|
||||
|
||||
// Синхронизируем с React состоянием
|
||||
syncFromEngine()
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error)
|
||||
} finally {
|
||||
setIsCalculating(false)
|
||||
}
|
||||
},
|
||||
[rows, cols, sheetName, syncFromEngine]
|
||||
)
|
||||
|
||||
// Экспорт данных в массив (для сохранения)
|
||||
const exportToArray = useCallback((): string[][] => {
|
||||
if (!workbookRef.current) return []
|
||||
|
||||
const sheet = workbookRef.current.getSheet(sheetName)
|
||||
if (!sheet) return []
|
||||
|
||||
const result: string[][] = Array(rows)
|
||||
.fill(null)
|
||||
.map(() => Array(cols).fill(''))
|
||||
|
||||
for (let row = 0; row < rows; row++) {
|
||||
for (let col = 0; col < cols; col++) {
|
||||
const cellName = CellUtils.createCellName(col, row + 1)
|
||||
const cell = sheet.getCell(cellName)
|
||||
|
||||
if (cell.raw !== null && cell.raw !== undefined) {
|
||||
result[row][col] = String(cell.raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}, [rows, cols, sheetName])
|
||||
|
||||
// Экспорт в JSON для бэкенда
|
||||
const exportToJSON = useCallback((): string => {
|
||||
if (!workbookRef.current) return '{}'
|
||||
return JSON.stringify(workbookRef.current.toJSON())
|
||||
}, [])
|
||||
|
||||
// Импорт из JSON с бэкенда
|
||||
const importFromJSON = useCallback(
|
||||
(jsonData: string) => {
|
||||
if (!workbookRef.current) return
|
||||
|
||||
setIsCalculating(true)
|
||||
|
||||
try {
|
||||
const data = JSON.parse(jsonData)
|
||||
workbookRef.current.fromJSON(data)
|
||||
// addCustomFunctions(workbookRef.current)
|
||||
workbookRef.current.recalc()
|
||||
syncFromEngine()
|
||||
} catch (error) {
|
||||
console.error('Error importing from JSON:', error)
|
||||
} finally {
|
||||
setIsCalculating(false)
|
||||
}
|
||||
},
|
||||
[syncFromEngine]
|
||||
)
|
||||
|
||||
// Добавление пользовательской функции
|
||||
const addFunction = useCallback(
|
||||
(name: string, func: (...args: any[]) => any) => {
|
||||
if (!workbookRef.current) return
|
||||
workbookRef.current.addFunction(name, func)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
// Пересчёт всех формул
|
||||
const recalculate = useCallback(() => {
|
||||
if (!workbookRef.current) return
|
||||
|
||||
setIsCalculating(true)
|
||||
|
||||
try {
|
||||
workbookRef.current.recalc()
|
||||
syncFromEngine()
|
||||
} catch (error) {
|
||||
console.error('Error recalculating:', error)
|
||||
} finally {
|
||||
setIsCalculating(false)
|
||||
}
|
||||
}, [syncFromEngine])
|
||||
|
||||
// Получение информации о зависимостях ячейки
|
||||
const getCellDependencies = useCallback(
|
||||
(row: number, col: number) => {
|
||||
if (!workbookRef.current) return { precedents: [], dependents: [] }
|
||||
|
||||
const cellName = CellUtils.createCellName(col, row + 1)
|
||||
const sheet = workbookRef.current.getSheet(sheetName)
|
||||
|
||||
if (!sheet) return { precedents: [], dependents: [] }
|
||||
|
||||
const cell = sheet.getCell(cellName)
|
||||
|
||||
return {
|
||||
precedents: Array.from(cell.precedents),
|
||||
dependents: Array.from(cell.dependents),
|
||||
}
|
||||
},
|
||||
[sheetName]
|
||||
)
|
||||
|
||||
// Получение статистики workbook
|
||||
const getWorkbookStats = useCallback(() => {
|
||||
if (!workbookRef.current) return { sheets: 0, cells: 0, formulas: 0 }
|
||||
|
||||
let totalCells = 0
|
||||
let totalFormulas = 0
|
||||
|
||||
for (const sheet of workbookRef.current.sheets.values()) {
|
||||
totalCells += sheet.cells.size
|
||||
|
||||
for (const cell of sheet.cells.values()) {
|
||||
if (typeof cell.raw === 'string' && cell.raw.startsWith('=')) {
|
||||
totalFormulas++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sheets: workbookRef.current.sheets.size,
|
||||
cells: totalCells,
|
||||
formulas: totalFormulas,
|
||||
}
|
||||
}, [])
|
||||
|
||||
return {
|
||||
cells,
|
||||
setCellValue,
|
||||
getCellValue,
|
||||
getCellRawValue,
|
||||
loadDataFromArray,
|
||||
exportToArray,
|
||||
exportToJSON,
|
||||
importFromJSON,
|
||||
addFunction,
|
||||
recalculate,
|
||||
getCellDependencies,
|
||||
getWorkbookStats,
|
||||
isCalculating,
|
||||
workbook: workbookRef.current,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user