- added import individual sheets;

- fixed bugs reated to init of univerjs;
This commit is contained in:
Vitalii Kiiko
2026-04-08 10:57:16 +02:00
parent 2c4886323f
commit c52d0c8fd9
9 changed files with 476 additions and 102 deletions

View File

@@ -1,11 +1,28 @@
import React, { useEffect, useRef } from 'react';
import { classNames } from 'primereact/utils';
import { createUniver, LocaleType, mergeLocales } from '@univerjs/presets';
import { UniverSheetsCorePreset } from '@univerjs/preset-sheets-core';
import UniverPresetSheetsCoreEnUS from '@univerjs/preset-sheets-core/locales/en-US';
import '@univerjs/preset-sheets-core/lib/index.css';
import { __ } from '@wordpress/i18n';
// tools
import { xlsxToWorkbookData } from '../../../../pages/BandoEdit/components/BandoEditFormStep3Excel/xlsxToWorkbookData';
const TAG_RE = /^\{\{gepafin_field:[^|]+\|[^}]+}}$/;
const BLOCKED_COMMANDS = new Set([
'sheet.command.set-range-values',
'sheet.command.clear-selection-content',
'sheet.command.clear-selection-all',
'sheet.command.delete-range',
'sheet.command.insert-range',
'sheet.command.set-range-format',
'sheet.command.insert-sheet',
'sheet.command.delete-sheet',
'sheet.command.rename-sheet',
]);
const buildTagMap = (workbookData) => {
const map = {};
if (!workbookData?.sheets) return map;
@@ -36,82 +53,131 @@ const parseWorkbook = (val) => {
return null;
};
const Spreadsheet = ({ fieldName, defaultValue, setDataFn, template, register, config = {} }) => {
const Spreadsheet = ({ fieldName, label, errors = {}, defaultValue, setDataFn, template, register, config = {} }) => {
const containerRef = useRef(null);
const univerRef = useRef(null);
const univerAPIRef = useRef(null);
const saveTimerRef = useRef(null);
const isRestoringRef = useRef(false);
const tagCellMapRef = useRef({});
const reinitializeRef = useRef(null);
const fileInputRef = useRef(null);
const addSheetsFileInputRef = useRef(null);
useEffect(() => {
if (!containerRef.current) return;
const { univer, univerAPI } = createUniver({
locale: LocaleType.EN_US,
locales: {
[LocaleType.EN_US]: mergeLocales(UniverPresetSheetsCoreEnUS),
},
presets: [
UniverSheetsCorePreset({
container: containerRef.current,
}),
],
});
const templateData = parseWorkbook(template);
const gepafin = templateData?.gepafin ?? {};
const editableSet = new Set(gepafin.editableCells ?? []);
const inputOnly = editableSet.size > 0;
const validationCellKeys = new Set((gepafin.validationCells ?? []).map(vc => vc.key ?? vc));
univerRef.current = univer;
univerAPIRef.current = univerAPI;
const doInit = (workbookData) => {
if (univerRef.current) {
univerRef.current.dispose();
univerRef.current = null;
univerAPIRef.current = null;
}
const initialData = parseWorkbook(defaultValue) || parseWorkbook(template) || { name: 'Sheet' };
tagCellMapRef.current = buildTagMap(initialData);
univerAPI.createWorkbook(initialData);
const { univer, univerAPI } = createUniver({
locale: LocaleType.EN_US,
locales: {
[LocaleType.EN_US]: mergeLocales(UniverPresetSheetsCoreEnUS),
},
presets: [
UniverSheetsCorePreset({
container: containerRef.current,
}),
],
});
const restoreTagCells = () => {
if (isRestoringRef.current) return;
const wb = univerAPIRef.current?.getActiveWorkbook();
if (!wb) return;
const sheet = wb.getActiveSheet();
if (!sheet) return;
const saved = wb.save();
if (!saved?.sheets) return;
const sheetId = saved.sheetOrder?.[0];
const sheetData = sheetId ? saved.sheets[sheetId] : Object.values(saved.sheets)[0];
if (!sheetData?.cellData) return;
univerRef.current = univer;
univerAPIRef.current = univerAPI;
const restorations = [];
Object.entries(sheetData.cellData).forEach(([r, row]) => {
Object.entries(row).forEach(([c, cell]) => {
if (typeof cell.v === 'string' && TAG_RE.test(cell.v)) {
const orig = tagCellMapRef.current[`${r}:${c}`];
if (orig) restorations.push({ r: +r, c: +c, v: orig.v, f: orig.f });
}
tagCellMapRef.current = buildTagMap(workbookData);
univerAPI.createWorkbook(workbookData);
// Apply visual markers for editable and validation cells in input-only mode
if (inputOnly && (editableSet.size > 0 || validationCellKeys.size > 0)) {
setTimeout(() => {
const wb = univerAPIRef.current?.getActiveWorkbook();
const sheet = wb?.getActiveSheet();
if (!sheet) return;
editableSet.forEach((key) => {
const [r, c] = key.split(':').map(Number);
sheet.getRange(r, c, 1, 1).setBackground('#dcfce7');
});
validationCellKeys.forEach((key) => {
const [r, c] = key.split(':').map(Number);
sheet.getRange(r, c, 1, 1).setBackground('#fef3c7');
});
}, 300);
}
const restoreTagCells = () => {
if (isRestoringRef.current) return;
const wb = univerAPIRef.current?.getActiveWorkbook();
if (!wb) return;
const sheet = wb.getActiveSheet();
if (!sheet) return;
const saved = wb.save();
if (!saved?.sheets) return;
const sheetId = saved.sheetOrder?.[0];
const sheetData = sheetId ? saved.sheets[sheetId] : Object.values(saved.sheets)[0];
if (!sheetData?.cellData) return;
const restorations = [];
Object.entries(sheetData.cellData).forEach(([r, row]) => {
Object.entries(row).forEach(([c, cell]) => {
if (typeof cell.v === 'string' && TAG_RE.test(cell.v)) {
const orig = tagCellMapRef.current[`${r}:${c}`];
if (orig) restorations.push({ r: +r, c: +c, v: orig.v, f: orig.f });
}
});
});
});
if (restorations.length === 0) return;
isRestoringRef.current = true;
restorations.forEach(({ r, c, v, f }) => {
sheet.getRange(r, c, 1, 1).setValues([[{ v, f }]]);
if (restorations.length === 0) return;
isRestoringRef.current = true;
restorations.forEach(({ r, c, v, f }) => {
sheet.getRange(r, c, 1, 1).setValues([[{ v, f }]]);
});
isRestoringRef.current = false;
};
let restoreTimer;
univerAPI.addEvent(univerAPI.Event.BeforeCommandExecute, (event) => {
// Input-only mode: block write commands on non-editable cells
if (inputOnly && BLOCKED_COMMANDS.has(event.id)) {
const sheet = univerAPIRef.current?.getActiveWorkbook()?.getActiveSheet();
const range = sheet?.getActiveRange();
if (range) {
const key = `${range.getRow()}:${range.getColumn()}`;
if (!editableSet.has(key)) {
event.cancel = true;
return;
}
}
}
clearTimeout(saveTimerRef.current);
saveTimerRef.current = setTimeout(() => {
const wb = univerAPIRef.current?.getActiveWorkbook();
if (wb) {
setDataFn(fieldName, wb.save(), { shouldValidate: true });
}
}, 300);
clearTimeout(restoreTimer);
restoreTimer = setTimeout(restoreTagCells, 80);
});
isRestoringRef.current = false;
};
let restoreTimer;
univerAPI.addEvent(univerAPI.Event.BeforeCommandExecute, () => {
clearTimeout(saveTimerRef.current);
saveTimerRef.current = setTimeout(() => {
const wb = univerAPIRef.current?.getActiveWorkbook();
if (wb) {
setDataFn(fieldName, wb.save(), { shouldValidate: true });
}
}, 300);
clearTimeout(restoreTimer);
restoreTimer = setTimeout(restoreTagCells, 80);
});
const initialData = parseWorkbook(defaultValue) || templateData || { name: 'Sheet' };
doInit(initialData);
reinitializeRef.current = doInit;
return () => {
clearTimeout(saveTimerRef.current);
clearTimeout(restoreTimer);
if (univerRef.current) {
univerRef.current.dispose();
univerRef.current = null;
@@ -132,11 +198,111 @@ const Spreadsheet = ({ fieldName, defaultValue, setDataFn, template, register, c
register(fieldName, config);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const handleImport = (e) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
const data = xlsxToWorkbookData(ev.target.result);
reinitializeRef.current?.(data);
setTimeout(() => {
const wb = univerAPIRef.current?.getActiveWorkbook();
if (wb) setDataFn(fieldName, wb.save(), { shouldValidate: true });
}, 400);
};
reader.readAsArrayBuffer(file);
e.target.value = '';
};
const handleImportAsSheets = (e) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
const imported = xlsxToWorkbookData(ev.target.result);
const wb = univerAPIRef.current?.getActiveWorkbook();
if (!wb) return;
const current = wb.save();
const existingIds = new Set(current.sheetOrder || []);
const mergedSheets = { ...(current.sheets || {}) };
const mergedOrder = [...(current.sheetOrder || [])];
(imported.sheetOrder || []).forEach((importedId) => {
let newId = importedId;
let counter = 0;
while (existingIds.has(newId)) {
counter++;
newId = `${importedId}_${counter}`;
}
existingIds.add(newId);
mergedSheets[newId] = { ...imported.sheets[importedId], id: newId };
mergedOrder.push(newId);
});
const merged = { ...current, sheets: mergedSheets, sheetOrder: mergedOrder };
reinitializeRef.current?.(merged);
setTimeout(() => {
const wbAfter = univerAPIRef.current?.getActiveWorkbook();
if (wbAfter) setDataFn(fieldName, wbAfter.save(), { shouldValidate: true });
}, 400);
};
reader.readAsArrayBuffer(file);
e.target.value = '';
};
const templateData = parseWorkbook(template);
const inputOnly = (templateData?.gepafin?.editableCells?.length ?? 0) > 0;
return (
<div
ref={containerRef}
style={{ width: '100%', height: '500px' }}
/>
<>
{label
? <label htmlFor={fieldName} className={classNames({ 'p-error': errors[fieldName] })}>
{label}{config.required || config.isRequired ?
<span className="appForm__field--required">*</span> : null}
</label>
: null}
{inputOnly && (
<div className="appPageSection__message warning">
<span>
{__('Modalità inserimento dati — modifica solo le celle evidenziate in verde', 'gepafin')}
</span>
</div>
)}
{!inputOnly && (
<div style={{ marginBottom: '8px', display: 'flex', gap: '8px' }}>
<input
ref={fileInputRef}
type="file"
accept=".xlsx,.xls,.csv,.ods"
style={{ display: 'none' }}
onChange={handleImport}
/>
<input
ref={addSheetsFileInputRef}
type="file"
accept=".xlsx,.xls,.csv,.ods"
style={{ display: 'none' }}
onChange={handleImportAsSheets}
/>
<button
type="button"
className="p-button p-button-outlined p-button-sm"
onClick={() => fileInputRef.current?.click()}
>
{__('Importa foglio', 'gepafin')}
</button>
<button
type="button"
className="p-button p-button-outlined p-button-sm"
onClick={() => addSheetsFileInputRef.current?.click()}
>
{__('Aggiungi fogli', 'gepafin')}
</button>
</div>
)}
<div
ref={containerRef}
style={{ width: '100%', height: '500px' }}
/>
</>
);
};