- added import individual sheets;
- fixed bugs reated to init of univerjs;
This commit is contained in:
@@ -204,14 +204,14 @@
|
|||||||
|
|
||||||
.formElementSettings__fieldDescription, .formElementSettings__fieldVarsList {
|
.formElementSettings__fieldDescription, .formElementSettings__fieldVarsList {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
background-color: #ffe0c5;
|
background-color: #fdf1e5;
|
||||||
border: 1px solid #e6a973;
|
border: 1px solid #e6a973;
|
||||||
|
|
||||||
p {
|
p, li {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #c68e5e;
|
color: #c68e5e;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 1.5;
|
line-height: 1.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
|
|||||||
@@ -1,11 +1,28 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { classNames } from 'primereact/utils';
|
||||||
import { createUniver, LocaleType, mergeLocales } from '@univerjs/presets';
|
import { createUniver, LocaleType, mergeLocales } from '@univerjs/presets';
|
||||||
import { UniverSheetsCorePreset } from '@univerjs/preset-sheets-core';
|
import { UniverSheetsCorePreset } from '@univerjs/preset-sheets-core';
|
||||||
import UniverPresetSheetsCoreEnUS from '@univerjs/preset-sheets-core/locales/en-US';
|
import UniverPresetSheetsCoreEnUS from '@univerjs/preset-sheets-core/locales/en-US';
|
||||||
import '@univerjs/preset-sheets-core/lib/index.css';
|
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 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 buildTagMap = (workbookData) => {
|
||||||
const map = {};
|
const map = {};
|
||||||
if (!workbookData?.sheets) return map;
|
if (!workbookData?.sheets) return map;
|
||||||
@@ -36,17 +53,33 @@ const parseWorkbook = (val) => {
|
|||||||
return null;
|
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 containerRef = useRef(null);
|
||||||
const univerRef = useRef(null);
|
const univerRef = useRef(null);
|
||||||
const univerAPIRef = useRef(null);
|
const univerAPIRef = useRef(null);
|
||||||
const saveTimerRef = useRef(null);
|
const saveTimerRef = useRef(null);
|
||||||
const isRestoringRef = useRef(false);
|
const isRestoringRef = useRef(false);
|
||||||
const tagCellMapRef = useRef({});
|
const tagCellMapRef = useRef({});
|
||||||
|
const reinitializeRef = useRef(null);
|
||||||
|
const fileInputRef = useRef(null);
|
||||||
|
const addSheetsFileInputRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerRef.current) return;
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
const doInit = (workbookData) => {
|
||||||
|
if (univerRef.current) {
|
||||||
|
univerRef.current.dispose();
|
||||||
|
univerRef.current = null;
|
||||||
|
univerAPIRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
const { univer, univerAPI } = createUniver({
|
const { univer, univerAPI } = createUniver({
|
||||||
locale: LocaleType.EN_US,
|
locale: LocaleType.EN_US,
|
||||||
locales: {
|
locales: {
|
||||||
@@ -62,9 +95,25 @@ const Spreadsheet = ({ fieldName, defaultValue, setDataFn, template, register, c
|
|||||||
univerRef.current = univer;
|
univerRef.current = univer;
|
||||||
univerAPIRef.current = univerAPI;
|
univerAPIRef.current = univerAPI;
|
||||||
|
|
||||||
const initialData = parseWorkbook(defaultValue) || parseWorkbook(template) || { name: 'Sheet' };
|
tagCellMapRef.current = buildTagMap(workbookData);
|
||||||
tagCellMapRef.current = buildTagMap(initialData);
|
univerAPI.createWorkbook(workbookData);
|
||||||
univerAPI.createWorkbook(initialData);
|
|
||||||
|
// 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 = () => {
|
const restoreTagCells = () => {
|
||||||
if (isRestoringRef.current) return;
|
if (isRestoringRef.current) return;
|
||||||
@@ -97,7 +146,20 @@ const Spreadsheet = ({ fieldName, defaultValue, setDataFn, template, register, c
|
|||||||
};
|
};
|
||||||
|
|
||||||
let restoreTimer;
|
let restoreTimer;
|
||||||
univerAPI.addEvent(univerAPI.Event.BeforeCommandExecute, () => {
|
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);
|
clearTimeout(saveTimerRef.current);
|
||||||
saveTimerRef.current = setTimeout(() => {
|
saveTimerRef.current = setTimeout(() => {
|
||||||
const wb = univerAPIRef.current?.getActiveWorkbook();
|
const wb = univerAPIRef.current?.getActiveWorkbook();
|
||||||
@@ -108,10 +170,14 @@ const Spreadsheet = ({ fieldName, defaultValue, setDataFn, template, register, c
|
|||||||
clearTimeout(restoreTimer);
|
clearTimeout(restoreTimer);
|
||||||
restoreTimer = setTimeout(restoreTagCells, 80);
|
restoreTimer = setTimeout(restoreTagCells, 80);
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialData = parseWorkbook(defaultValue) || templateData || { name: 'Sheet' };
|
||||||
|
doInit(initialData);
|
||||||
|
reinitializeRef.current = doInit;
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearTimeout(saveTimerRef.current);
|
clearTimeout(saveTimerRef.current);
|
||||||
clearTimeout(restoreTimer);
|
|
||||||
if (univerRef.current) {
|
if (univerRef.current) {
|
||||||
univerRef.current.dispose();
|
univerRef.current.dispose();
|
||||||
univerRef.current = null;
|
univerRef.current = null;
|
||||||
@@ -132,11 +198,111 @@ const Spreadsheet = ({ fieldName, defaultValue, setDataFn, template, register, c
|
|||||||
register(fieldName, config);
|
register(fieldName, config);
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
}, []); // 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 (
|
return (
|
||||||
|
<>
|
||||||
|
{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
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
style={{ width: '100%', height: '500px' }}
|
style={{ width: '100%', height: '500px' }}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,28 @@ export const maxChecks = (v, num) => {
|
|||||||
return is(Array, v) ? v.length <= parseInt(num) : false;
|
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 = []) => {
|
export const nonEmptyTables = (v = [], tableCfg = []) => {
|
||||||
const colsCfg = pathOr([], ['stateFieldData'], tableCfg);
|
const colsCfg = pathOr([], ['stateFieldData'], tableCfg);
|
||||||
const nonPredefinedCells = colsCfg
|
const nonPredefinedCells = colsCfg
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
isEmail,
|
isEmail,
|
||||||
isEmailPEC,
|
isEmailPEC,
|
||||||
isUrl,
|
isUrl,
|
||||||
isMarcaDaBollo, minChecks, maxChecks, nonEmptyTables
|
isMarcaDaBollo, minChecks, maxChecks, nonEmptyTables, createSpreadsheetValidator
|
||||||
} from '../../helpers/validators';
|
} from '../../helpers/validators';
|
||||||
import renderHtmlContent from '../../helpers/renderHtmlContent';
|
import renderHtmlContent from '../../helpers/renderHtmlContent';
|
||||||
import set404FromErrorResponse from '../../helpers/set404FromErrorResponse';
|
import set404FromErrorResponse from '../../helpers/set404FromErrorResponse';
|
||||||
@@ -761,6 +761,14 @@ const BandoApplication = () => {
|
|||||||
return acc;
|
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') {
|
/*if (o.name === 'fileselect') {
|
||||||
console.log('options::', options)
|
console.log('options::', options)
|
||||||
}*/
|
}*/
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ const BandoEditFormStep3 = forwardRef(function () {
|
|||||||
const getFormsCallback = (resp) => {
|
const getFormsCallback = (resp) => {
|
||||||
if (resp.status === 'SUCCESS') {
|
if (resp.status === 'SUCCESS') {
|
||||||
const EXCLUDED_TYPES = new Set(['fileupload', 'fileselect', 'spreadsheet']);
|
const EXCLUDED_TYPES = new Set(['fileupload', 'fileselect', 'spreadsheet']);
|
||||||
console.log('resp.data', resp.data)
|
|
||||||
const raw = (resp.data ?? []).flatMap(form =>
|
const raw = (resp.data ?? []).flatMap(form =>
|
||||||
(form.content ?? [])
|
(form.content ?? [])
|
||||||
.filter(f => !EXCLUDED_TYPES.has(f.name))
|
.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 });
|
if (!byLabel.has(f.label)) byLabel.set(f.label, { label: f.label, ids: [], placeholder: f.placeholder });
|
||||||
byLabel.get(f.label).ids.push(f.id);
|
byLabel.get(f.label).ids.push(f.id);
|
||||||
});
|
});
|
||||||
console.log('byLabel', Array.from(byLabel.values()))
|
|
||||||
storeSet('callFormFields', 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 ElementSettingSpreadsheet = React.lazy(() => import('../ElementSettingSpreadsheet'));
|
||||||
|
|
||||||
|
|
||||||
const ElementSetting = ({ setting, changeFn, updateDataFn, bandoStatus }) => {
|
const ElementSetting = ({ setting, changeFn, updateDataFn, bandoStatus, context }) => {
|
||||||
const [existingVars, setExistingVars] = useState([]);
|
const [existingVars, setExistingVars] = useState([]);
|
||||||
const documentCategories = useStoreValue('documentCategories');
|
const documentCategories = useStoreValue('documentCategories');
|
||||||
|
|
||||||
@@ -163,6 +163,7 @@ const ElementSetting = ({ setting, changeFn, updateDataFn, bandoStatus }) => {
|
|||||||
<ElementSettingSpreadsheet
|
<ElementSettingSpreadsheet
|
||||||
value={setting.value}
|
value={setting.value}
|
||||||
name={setting.name}
|
name={setting.name}
|
||||||
|
context={context}
|
||||||
setDataFn={updateDataFn}/>
|
setDataFn={updateDataFn}/>
|
||||||
</Suspense>;
|
</Suspense>;
|
||||||
} else if (setting.name === 'formula') {
|
} else if (setting.name === 'formula') {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { xlsxToWorkbookData } from '../../../../../BandoEdit/components/BandoEdi
|
|||||||
|
|
||||||
const TAG_RE = /^\{\{gepafin_field:([^|]+)\|([^}]+)}}$/;
|
const TAG_RE = /^\{\{gepafin_field:([^|]+)\|([^}]+)}}$/;
|
||||||
|
|
||||||
const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => {
|
const ElementSettingSpreadsheet = ({ value, name, setDataFn, context }) => {
|
||||||
const callFormFields = useStoreValue('callFormFields');
|
const callFormFields = useStoreValue('callFormFields');
|
||||||
const [tagTooltip, setTagTooltip] = useState(null);
|
const [tagTooltip, setTagTooltip] = useState(null);
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
@@ -27,6 +27,10 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => {
|
|||||||
const taggedCellsRef = useRef(new Set());
|
const taggedCellsRef = useRef(new Set());
|
||||||
const saveTimerRef = useRef(null);
|
const saveTimerRef = useRef(null);
|
||||||
const setDataFnRef = useRef(setDataFn);
|
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]);
|
useEffect(() => { setDataFnRef.current = setDataFn; }, [setDataFn]);
|
||||||
|
|
||||||
@@ -43,6 +47,70 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => {
|
|||||||
range.setBackground('#dbeafe');
|
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) => {
|
const initializeUniver = useCallback((workbookData) => {
|
||||||
if (univerRef.current) {
|
if (univerRef.current) {
|
||||||
univerRef.current.dispose();
|
univerRef.current.dispose();
|
||||||
@@ -52,6 +120,12 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => {
|
|||||||
|
|
||||||
if (!containerRef.current) return;
|
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({
|
const { univer, univerAPI } = createUniver({
|
||||||
locale: LocaleType.EN_US,
|
locale: LocaleType.EN_US,
|
||||||
locales: {
|
locales: {
|
||||||
@@ -84,6 +158,23 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => {
|
|||||||
}
|
}
|
||||||
taggedCellsRef.current = initialTagged;
|
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;
|
// Restore tag cells whose display value was overwritten by raw tag on blur;
|
||||||
// also clear background for cells whose tag formula was removed.
|
// also clear background for cells whose tag formula was removed.
|
||||||
const restoreTagCells = () => {
|
const restoreTagCells = () => {
|
||||||
@@ -152,21 +243,23 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => {
|
|||||||
|
|
||||||
let restoreTimer;
|
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) => {
|
univerAPI.addEvent(univerAPI.Event.BeforeCommandExecute, (event) => {
|
||||||
if (event.id === 'sheet.command.insert-sheet') {
|
|
||||||
event.cancel = true;
|
|
||||||
}
|
|
||||||
clearTimeout(saveTimerRef.current);
|
clearTimeout(saveTimerRef.current);
|
||||||
saveTimerRef.current = setTimeout(() => {
|
saveTimerRef.current = setTimeout(() => {
|
||||||
const wb = univerAPIRef.current?.getActiveWorkbook();
|
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);
|
}, 300);
|
||||||
clearTimeout(restoreTimer);
|
clearTimeout(restoreTimer);
|
||||||
restoreTimer = setTimeout(restoreTagCells, 80);
|
restoreTimer = setTimeout(restoreTagCells, 80);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Context menu: "Inserisci variabile GEPAFIN"
|
// Context menu: "Inserisci variabile GEPAFIN" — only for evaluation form builder (context="call")
|
||||||
|
if (contextRef.current === 'call') {
|
||||||
const fields = formFieldsRef.current;
|
const fields = formFieldsRef.current;
|
||||||
if (fields.length > 0) {
|
if (fields.length > 0) {
|
||||||
const submenu = univerAPI.createSubmenu({
|
const submenu = univerAPI.createSubmenu({
|
||||||
@@ -195,6 +288,22 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => {
|
|||||||
|
|
||||||
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
|
// Tooltip on hover over tagged cells — read formula field to detect tag
|
||||||
univerAPI.addEvent(univerAPI.Event.CellHover, (params) => {
|
univerAPI.addEvent(univerAPI.Event.CellHover, (params) => {
|
||||||
@@ -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)
|
// 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.
|
// 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;
|
formFieldsRef.current = callFormFields;
|
||||||
if (callFormFields.length > 0 && univerAPIRef.current) {
|
if (callFormFields.length > 0 && univerAPIRef.current) {
|
||||||
const currentWorkbook = univerAPIRef.current.getActiveWorkbook()?.save() ?? null;
|
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]);
|
}, [callFormFields, initializeUniver]);
|
||||||
|
|
||||||
@@ -242,8 +355,6 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => {
|
|||||||
}, []); // run once on mount
|
}, []); // run once on mount
|
||||||
|
|
||||||
// Flush workbook data to parent immediately when the user clicks outside the spreadsheet.
|
// 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(() => {
|
useEffect(() => {
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
@@ -251,11 +362,15 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => {
|
|||||||
if (container.contains(e.target)) return;
|
if (container.contains(e.target)) return;
|
||||||
clearTimeout(saveTimerRef.current);
|
clearTimeout(saveTimerRef.current);
|
||||||
const wb = univerAPIRef.current?.getActiveWorkbook();
|
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);
|
document.addEventListener('click', flushOnExternalClick, true);
|
||||||
return () => document.removeEventListener('click', flushOnExternalClick, true);
|
return () => document.removeEventListener('click', flushOnExternalClick, true);
|
||||||
}, [name]); // name is stable ('template')
|
}, [name]);
|
||||||
|
|
||||||
// Prevent page scroll while hovering over the spreadsheet
|
// Prevent page scroll while hovering over the spreadsheet
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -278,13 +393,56 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => {
|
|||||||
e.target.value = '';
|
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 handleSave = () => {
|
||||||
const workbookData = univerAPIRef.current?.getActiveWorkbook()?.save() ?? {};
|
const wb = univerAPIRef.current?.getActiveWorkbook();
|
||||||
setDataFn(name, workbookData);
|
if (wb) {
|
||||||
|
const raw = wb.save();
|
||||||
|
const m = gepafinMetaRef.current;
|
||||||
|
setDataFn(name, { ...raw, gepafin: { ...m, inputOnlyMode: m.editableCells.length > 0 } });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: '100%' }}>
|
<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' }}>
|
<div style={{ marginBottom: '8px', display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
@@ -293,13 +451,29 @@ const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => {
|
|||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
onChange={handleImport}
|
onChange={handleImport}
|
||||||
/>
|
/>
|
||||||
|
<input
|
||||||
|
ref={addSheetsFileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".xlsx,.xls,.csv,.ods"
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={handleImportAsSheets}
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="p-button p-button-outlined p-button-sm"
|
className="p-button p-button-outlined p-button-sm"
|
||||||
|
disabled={inputOnlyActive}
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
>
|
>
|
||||||
{__('Importa foglio', 'gepafin')}
|
{__('Importa foglio', 'gepafin')}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="p-button p-button-outlined p-button-sm"
|
||||||
|
disabled={inputOnlyActive}
|
||||||
|
onClick={() => addSheetsFileInputRef.current?.click()}
|
||||||
|
>
|
||||||
|
{__('Aggiungi fogli', 'gepafin')}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="p-button p-button-sm"
|
className="p-button p-button-sm"
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ const BuilderElementSettings = ({ closeSettingsFn, callStatus, context }) => {
|
|||||||
.map((o) => <ElementSetting
|
.map((o) => <ElementSetting
|
||||||
key={o.name}
|
key={o.name}
|
||||||
setting={o}
|
setting={o}
|
||||||
|
context={context}
|
||||||
callStatus={callStatus}
|
callStatus={callStatus}
|
||||||
changeFn={onChange}
|
changeFn={onChange}
|
||||||
updateDataFn={onUpdateOptions}/>)
|
updateDataFn={onUpdateOptions}/>)
|
||||||
@@ -260,6 +261,7 @@ const BuilderElementSettings = ({ closeSettingsFn, callStatus, context }) => {
|
|||||||
.map((o) => <ElementSetting
|
.map((o) => <ElementSetting
|
||||||
key={o.name}
|
key={o.name}
|
||||||
setting={o}
|
setting={o}
|
||||||
|
context={context}
|
||||||
callStatus={callStatus}
|
callStatus={callStatus}
|
||||||
changeFn={onChange}
|
changeFn={onChange}
|
||||||
updateDataFn={onUpdateOptions}/>)
|
updateDataFn={onUpdateOptions}/>)
|
||||||
@@ -272,6 +274,7 @@ const BuilderElementSettings = ({ closeSettingsFn, callStatus, context }) => {
|
|||||||
.map((o) => <ElementSetting
|
.map((o) => <ElementSetting
|
||||||
key={o.name}
|
key={o.name}
|
||||||
setting={o}
|
setting={o}
|
||||||
|
context={context}
|
||||||
callStatus={callStatus}
|
callStatus={callStatus}
|
||||||
changeFn={onChange}
|
changeFn={onChange}
|
||||||
updateDataFn={onUpdateOptions}/>)
|
updateDataFn={onUpdateOptions}/>)
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ const BandoFormsPreview = () => {
|
|||||||
if (!tableColumns) {
|
if (!tableColumns) {
|
||||||
tableColumns = head(o.settings.filter(o => o.name === 'criteria_table_columns'));
|
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 step = head(o.settings.filter(o => o.name === 'step'));
|
||||||
const mime = head(o.settings.filter(o => o.name === 'mime'));
|
const mime = head(o.settings.filter(o => o.name === 'mime'));
|
||||||
const formula = head(o.settings.filter(o => o.name === 'formula'));
|
const formula = head(o.settings.filter(o => o.name === 'formula'));
|
||||||
@@ -233,6 +234,7 @@ const BandoFormsPreview = () => {
|
|||||||
sourceId={0}
|
sourceId={0}
|
||||||
useGrouping={false}
|
useGrouping={false}
|
||||||
tableColumns={tableColumns ? tableColumns.value : {}}
|
tableColumns={tableColumns ? tableColumns.value : {}}
|
||||||
|
template={template ? template.value : null}
|
||||||
/>
|
/>
|
||||||
}) : null}
|
}) : null}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user