- 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

@@ -204,14 +204,14 @@
.formElementSettings__fieldDescription, .formElementSettings__fieldVarsList {
padding: 15px;
background-color: #ffe0c5;
background-color: #fdf1e5;
border: 1px solid #e6a973;
p {
p, li {
margin: 0;
color: #c68e5e;
font-size: 15px;
line-height: 1.5;
line-height: 1.8;
}
code {

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

View File

@@ -66,6 +66,28 @@ export const maxChecks = (v, num) => {
return is(Array, v) ? v.length <= parseInt(num) : false;
}
export const createSpreadsheetValidator = (gepafin) => (workbookValue) => {
if (!gepafin || !workbookValue?.sheets) return true;
const sheetId = workbookValue.sheetOrder?.[0];
const sheet = sheetId
? workbookValue.sheets[sheetId]
: Object.values(workbookValue.sheets)[0];
const cellData = sheet?.cellData ?? {};
const getCellValue = (key) => {
const [r, c] = key.split(':');
return cellData[r]?.[c]?.v;
};
for (const { key, errorMessage } of (gepafin.validationCells ?? [])) {
const v = getCellValue(key);
if (v === undefined || v === null || v === 0 || v === false || v === 'FALSE' || v === '') {
return errorMessage || 'Validazione foglio di calcolo fallita';
}
}
return true;
};
export const nonEmptyTables = (v = [], tableCfg = []) => {
const colsCfg = pathOr([], ['stateFieldData'], tableCfg);
const nonPredefinedCells = colsCfg

View File

@@ -25,7 +25,7 @@ import {
isEmail,
isEmailPEC,
isUrl,
isMarcaDaBollo, minChecks, maxChecks, nonEmptyTables
isMarcaDaBollo, minChecks, maxChecks, nonEmptyTables, createSpreadsheetValidator
} from '../../helpers/validators';
import renderHtmlContent from '../../helpers/renderHtmlContent';
import set404FromErrorResponse from '../../helpers/set404FromErrorResponse';
@@ -761,6 +761,14 @@ const BandoApplication = () => {
return acc;
}, {});
if (o.name === 'spreadsheet') {
const gepafin = template?.value?.gepafin;
if (gepafin?.validationCells?.length) {
if (!validations.validate) validations.validate = {};
validations.validate.spreadsheetInputValid = createSpreadsheetValidator(gepafin);
}
}
/*if (o.name === 'fileselect') {
console.log('options::', options)
}*/

View File

@@ -95,7 +95,6 @@ const BandoEditFormStep3 = forwardRef(function () {
const getFormsCallback = (resp) => {
if (resp.status === 'SUCCESS') {
const EXCLUDED_TYPES = new Set(['fileupload', 'fileselect', 'spreadsheet']);
console.log('resp.data', resp.data)
const raw = (resp.data ?? []).flatMap(form =>
(form.content ?? [])
.filter(f => !EXCLUDED_TYPES.has(f.name))
@@ -110,7 +109,6 @@ const BandoEditFormStep3 = forwardRef(function () {
if (!byLabel.has(f.label)) byLabel.set(f.label, { label: f.label, ids: [], placeholder: f.placeholder });
byLabel.get(f.label).ids.push(f.id);
});
console.log('byLabel', Array.from(byLabel.values()))
storeSet('callFormFields', Array.from(byLabel.values()));
}
}

View File

@@ -26,7 +26,7 @@ import getTokens from '../../../../../../helpers/getTokens';
const ElementSettingSpreadsheet = React.lazy(() => import('../ElementSettingSpreadsheet'));
const ElementSetting = ({ setting, changeFn, updateDataFn, bandoStatus }) => {
const ElementSetting = ({ setting, changeFn, updateDataFn, bandoStatus, context }) => {
const [existingVars, setExistingVars] = useState([]);
const documentCategories = useStoreValue('documentCategories');
@@ -163,6 +163,7 @@ const ElementSetting = ({ setting, changeFn, updateDataFn, bandoStatus }) => {
<ElementSettingSpreadsheet
value={setting.value}
name={setting.name}
context={context}
setDataFn={updateDataFn}/>
</Suspense>;
} else if (setting.name === 'formula') {

View File

@@ -14,7 +14,7 @@ import { xlsxToWorkbookData } from '../../../../../BandoEdit/components/BandoEdi
const TAG_RE = /^\{\{gepafin_field:([^|]+)\|([^}]+)}}$/;
const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => {
const ElementSettingSpreadsheet = ({ value, name, setDataFn, context }) => {
const callFormFields = useStoreValue('callFormFields');
const [tagTooltip, setTagTooltip] = useState(null);
const containerRef = useRef(null);
@@ -27,6 +27,10 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => {
const taggedCellsRef = useRef(new Set());
const saveTimerRef = useRef(null);
const setDataFnRef = useRef(setDataFn);
const gepafinMetaRef = useRef({ inputOnlyMode: false, editableCells: [], validationCells: [] });
const contextRef = useRef(context);
const addSheetsFileInputRef = useRef(null);
const [inputOnlyActive, setInputOnlyActive] = useState(false);
useEffect(() => { setDataFnRef.current = setDataFn; }, [setDataFn]);
@@ -43,6 +47,70 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => {
range.setBackground('#dbeafe');
};
const getActiveCellKey = () => {
const api = univerAPIRef.current;
if (!api) return null;
const range = api.getActiveWorkbook()?.getActiveSheet()?.getActiveRange();
if (!range) return null;
return `${range.getRow()}:${range.getColumn()}`;
};
const saveWithMeta = useCallback(() => {
const wb = univerAPIRef.current?.getActiveWorkbook();
if (wb) {
const raw = wb.save();
const meta = gepafinMetaRef.current;
// inputOnlyMode is derived: active whenever any editable cells are defined
setDataFnRef.current(name, {
...raw,
gepafin: { ...meta, inputOnlyMode: meta.editableCells.length > 0 }
});
}
}, [name]);
const toggleEditableCell = useCallback(() => {
const key = getActiveCellKey();
if (!key) return;
const meta = gepafinMetaRef.current;
const api = univerAPIRef.current;
const sheet = api?.getActiveWorkbook()?.getActiveSheet();
const [r, c] = key.split(':').map(Number);
if (meta.editableCells.includes(key)) {
meta.editableCells = meta.editableCells.filter(k => k !== key);
sheet?.getRange(r, c, 1, 1).setBackground('');
} else {
meta.editableCells = [...meta.editableCells, key];
sheet?.getRange(r, c, 1, 1).setBackground('#dcfce7');
}
setInputOnlyActive(meta.editableCells.length > 0);
saveWithMeta();
}, [saveWithMeta]);
const toggleValidationCell = useCallback(() => {
const key = getActiveCellKey();
if (!key) return;
const meta = gepafinMetaRef.current;
const api = univerAPIRef.current;
const sheet = api?.getActiveWorkbook()?.getActiveSheet();
const [r, c] = key.split(':').map(Number);
const existing = meta.validationCells.find(vc => (vc.key ?? vc) === key);
if (existing) {
meta.validationCells = meta.validationCells.filter(vc => (vc.key ?? vc) !== key);
sheet?.getRange(r, c, 1, 1).setBackground('');
} else {
// eslint-disable-next-line no-alert
const errorMessage = window.prompt(
__('Messaggio di errore di validazione (lascia vuoto per il default):', 'gepafin'),
''
) ?? '';
meta.validationCells = [...meta.validationCells, { key, errorMessage: errorMessage || 'Validazione foglio di calcolo fallita' }];
sheet?.getRange(r, c, 1, 1).setBackground('#fef3c7');
}
saveWithMeta();
}, [saveWithMeta]);
const initializeUniver = useCallback((workbookData) => {
if (univerRef.current) {
univerRef.current.dispose();
@@ -52,6 +120,12 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => {
if (!containerRef.current) return;
// Initialise gepafin metadata from workbook
gepafinMetaRef.current = workbookData?.gepafin
? { inputOnlyMode: false, editableCells: [], validationCells: [], ...workbookData.gepafin }
: { inputOnlyMode: false, editableCells: [], validationCells: [] };
setInputOnlyActive(gepafinMetaRef.current.editableCells.length > 0);
const { univer, univerAPI } = createUniver({
locale: LocaleType.EN_US,
locales: {
@@ -84,6 +158,23 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => {
}
taggedCellsRef.current = initialTagged;
// Re-apply gepafin cell background markers after a short delay (Univer needs to settle)
setTimeout(() => {
const wb = univerAPIRef.current?.getActiveWorkbook();
const sheet = wb?.getActiveSheet();
if (!sheet) return;
const meta = gepafinMetaRef.current;
meta.editableCells.forEach((key) => {
const [r, c] = key.split(':').map(Number);
sheet.getRange(r, c, 1, 1).setBackground('#dcfce7');
});
meta.validationCells.forEach((vc) => {
const key = vc.key ?? vc;
const [r, c] = key.split(':').map(Number);
sheet.getRange(r, c, 1, 1).setBackground('#fef3c7');
});
}, 300);
// Restore tag cells whose display value was overwritten by raw tag on blur;
// also clear background for cells whose tag formula was removed.
const restoreTagCells = () => {
@@ -152,48 +243,66 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => {
let restoreTimer;
// Disable adding new sheets; auto-save on every command; schedule tag restoration
// Auto-save on every command; schedule tag restoration
univerAPI.addEvent(univerAPI.Event.BeforeCommandExecute, (event) => {
if (event.id === 'sheet.command.insert-sheet') {
event.cancel = true;
}
clearTimeout(saveTimerRef.current);
saveTimerRef.current = setTimeout(() => {
const wb = univerAPIRef.current?.getActiveWorkbook();
if (wb) setDataFnRef.current(name, wb.save());
if (wb) {
const raw = wb.save();
const m = gepafinMetaRef.current;
setDataFnRef.current(name, { ...raw, gepafin: { ...m, inputOnlyMode: m.editableCells.length > 0 } });
}
}, 300);
clearTimeout(restoreTimer);
restoreTimer = setTimeout(restoreTagCells, 80);
});
// Context menu: "Inserisci variabile GEPAFIN"
const fields = formFieldsRef.current;
if (fields.length > 0) {
const submenu = univerAPI.createSubmenu({
id: 'gepafin-insert-variable',
title: 'Inserisci variabile GEPAFIN',
});
// Context menu: "Inserisci variabile GEPAFIN" — only for evaluation form builder (context="call")
if (contextRef.current === 'call') {
const fields = formFieldsRef.current;
if (fields.length > 0) {
const submenu = univerAPI.createSubmenu({
id: 'gepafin-insert-variable',
title: 'Inserisci variabile GEPAFIN',
});
fields.forEach((field, index) => {
if (index > 0) submenu.addSeparator();
fields.forEach((field, index) => {
if (index > 0) submenu.addSeparator();
const menuKey = field.ids[0];
const countLabel = field.ids.length > 1 ? ` [${field.ids.length}]` : '';
const menuKey = field.ids[0];
const countLabel = field.ids.length > 1 ? ` [${field.ids.length}]` : '';
submenu.addSubmenu(univerAPI.createMenu({
id: `gepafin-field-${menuKey}-label`,
title: `${field.label}${countLabel} (etichetta)`,
action: () => insertFieldTag(field.ids, 'label', field),
}));
submenu.addSubmenu(univerAPI.createMenu({
id: `gepafin-field-${menuKey}-label`,
title: `${field.label}${countLabel} (etichetta)`,
action: () => insertFieldTag(field.ids, 'label', field),
}));
submenu.addSubmenu(univerAPI.createMenu({
id: `gepafin-field-${menuKey}-value`,
title: `${field.label}${countLabel} (valore)`,
action: () => insertFieldTag(field.ids, 'value', field),
}));
});
submenu.addSubmenu(univerAPI.createMenu({
id: `gepafin-field-${menuKey}-value`,
title: `${field.label}${countLabel} (valore)`,
action: () => insertFieldTag(field.ids, 'value', field),
}));
});
submenu.appendTo(['contextMenu.mainArea', 'contextMenu.others']);
submenu.appendTo(['contextMenu.mainArea', 'contextMenu.others']);
}
}
// Context menu: input-only mode management — only for regular form builder (context="application")
if (contextRef.current === 'application') {
univerAPI.createMenu({
id: 'gepafin-toggle-editable-cell',
title: 'Modalità inserimento: segna/rimuovi cella modificabile',
action: toggleEditableCell,
}).appendTo(['contextMenu.mainArea', 'contextMenu.others']);
univerAPI.createMenu({
id: 'gepafin-toggle-validation-cell',
title: 'Modalità inserimento: segna/rimuovi cella di validazione',
action: toggleValidationCell,
}).appendTo(['contextMenu.mainArea', 'contextMenu.others']);
}
// Tooltip on hover over tagged cells — read formula field to detect tag
@@ -213,7 +322,7 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => {
}
});
}, []); // refs are stable
}, [toggleEditableCell, toggleValidationCell]); // refs are stable
// Keep formFieldsRef in sync with store value; rebuild Univer (and its context menu)
// once fields arrive, because the menu is constructed at init time from formFieldsRef.current.
@@ -222,7 +331,11 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => {
formFieldsRef.current = callFormFields;
if (callFormFields.length > 0 && univerAPIRef.current) {
const currentWorkbook = univerAPIRef.current.getActiveWorkbook()?.save() ?? null;
initializeUniver(currentWorkbook);
// Preserve gepafin metadata across reinit
const withMeta = currentWorkbook
? { ...currentWorkbook, gepafin: gepafinMetaRef.current }
: null;
initializeUniver(withMeta);
}
}, [callFormFields, initializeUniver]);
@@ -242,8 +355,6 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => {
}, []); // run once on mount
// Flush workbook data to parent immediately when the user clicks outside the spreadsheet.
// This runs in the capture phase — before the clicked element's own handler — so the
// parent's store is up to date by the time any outer "Salva" button handler reads it.
useEffect(() => {
const container = containerRef.current;
if (!container) return;
@@ -251,11 +362,15 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => {
if (container.contains(e.target)) return;
clearTimeout(saveTimerRef.current);
const wb = univerAPIRef.current?.getActiveWorkbook();
if (wb) setDataFnRef.current(name, wb.save());
if (wb) {
const raw = wb.save();
const m = gepafinMetaRef.current;
setDataFnRef.current(name, { ...raw, gepafin: { ...m, inputOnlyMode: m.editableCells.length > 0 } });
}
};
document.addEventListener('click', flushOnExternalClick, true);
return () => document.removeEventListener('click', flushOnExternalClick, true);
}, [name]); // name is stable ('template')
}, [name]);
// Prevent page scroll while hovering over the spreadsheet
useEffect(() => {
@@ -278,13 +393,56 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => {
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 meta = gepafinMetaRef.current;
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);
});
initializeUniver({ ...current, sheets: mergedSheets, sheetOrder: mergedOrder, gepafin: meta });
};
reader.readAsArrayBuffer(file);
e.target.value = '';
};
const handleSave = () => {
const workbookData = univerAPIRef.current?.getActiveWorkbook()?.save() ?? {};
setDataFn(name, workbookData);
const wb = univerAPIRef.current?.getActiveWorkbook();
if (wb) {
const raw = wb.save();
const m = gepafinMetaRef.current;
setDataFn(name, { ...raw, gepafin: { ...m, inputOnlyMode: m.editableCells.length > 0 } });
}
};
return (
<div style={{ width: '100%' }}>
{context === 'application' && <div className="formElementSettings__fieldDescription" style={{ marginBottom: '12px' }}>
<p><strong>{__('Celle modificabili (verde)', 'gepafin')}</strong> {__('Clicca destro su una cella e scegli "Segna/rimuovi cella modificabile" per definire quali celle il beneficiario potrà compilare. La modalità inserimento dati si attiva automaticamente non appena viene definita almeno una cella modificabile.', 'gepafin')}</p>
<p style={{ marginTop: '8px' }}><strong>{__('Celle di validazione (ambra)', 'gepafin')}</strong> {__('Segna una cella come "cella di validazione" e inserisci una formula che restituisce TRUE/FALSE. Se il risultato non è vero al momento dell\'invio, il campo viene considerato non valido. Esempi di formule utili:', 'gepafin')}</p>
<ul style={{ marginTop: '4px', paddingLeft: '20px' }}>
<li><code>=NOT(ISBLANK(A1))</code> {__('cella A1 obbligatoria', 'gepafin')}</li>
<li><code>=AND(A1&gt;0, A1&lt;100)</code> {__('valore numerico tra 0 e 100', 'gepafin')}</li>
<li><code>=LEN(TRIM(B2))&gt;0</code> {__('testo non vuoto in B2', 'gepafin')}</li>
</ul>
</div>}
<div style={{ marginBottom: '8px', display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
<input
ref={fileInputRef}
@@ -293,13 +451,29 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => {
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"
disabled={inputOnlyActive}
onClick={() => fileInputRef.current?.click()}
>
{__('Importa foglio', 'gepafin')}
</button>
<button
type="button"
className="p-button p-button-outlined p-button-sm"
disabled={inputOnlyActive}
onClick={() => addSheetsFileInputRef.current?.click()}
>
{__('Aggiungi fogli', 'gepafin')}
</button>
<button
type="button"
className="p-button p-button-sm"

View File

@@ -163,6 +163,7 @@ const BuilderElementSettings = ({ closeSettingsFn, callStatus, context }) => {
.map((o) => <ElementSetting
key={o.name}
setting={o}
context={context}
callStatus={callStatus}
changeFn={onChange}
updateDataFn={onUpdateOptions}/>)
@@ -260,6 +261,7 @@ const BuilderElementSettings = ({ closeSettingsFn, callStatus, context }) => {
.map((o) => <ElementSetting
key={o.name}
setting={o}
context={context}
callStatus={callStatus}
changeFn={onChange}
updateDataFn={onUpdateOptions}/>)
@@ -272,6 +274,7 @@ const BuilderElementSettings = ({ closeSettingsFn, callStatus, context }) => {
.map((o) => <ElementSetting
key={o.name}
setting={o}
context={context}
callStatus={callStatus}
changeFn={onChange}
updateDataFn={onUpdateOptions}/>)

View File

@@ -180,6 +180,7 @@ const BandoFormsPreview = () => {
if (!tableColumns) {
tableColumns = head(o.settings.filter(o => o.name === 'criteria_table_columns'));
}
const template = head(o.settings.filter(o => o.name === 'template'));
const step = head(o.settings.filter(o => o.name === 'step'));
const mime = head(o.settings.filter(o => o.name === 'mime'));
const formula = head(o.settings.filter(o => o.name === 'formula'));
@@ -233,6 +234,7 @@ const BandoFormsPreview = () => {
sourceId={0}
useGrouping={false}
tableColumns={tableColumns ? tableColumns.value : {}}
template={template ? template.value : null}
/>
}) : null}
</form>