- added import individual sheets;
- fixed bugs reated to init of univerjs;
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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' }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}*/
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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>0, A1<100)</code> — {__('valore numerico tra 0 e 100', 'gepafin')}</li>
|
||||
<li><code>=LEN(TRIM(B2))>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"
|
||||
|
||||
@@ -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}/>)
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user