Merge branch 'spreadsheet' into develop

This commit is contained in:
Vitalii Kiiko
2026-03-25 14:23:52 +01:00
19 changed files with 780 additions and 6 deletions

View File

@@ -11,6 +11,9 @@
"@sentry/browser": "9.11.0",
"@stomp/stompjs": "7.1.1",
"@tanstack/react-table": "8.21.2",
"@univerjs/preset-docs-core": "^0.18.0",
"@univerjs/preset-sheets-core": "^0.18.0",
"@univerjs/presets": "^0.18.0",
"@wordpress/i18n": "5.21.0",
"@wordpress/react-i18n": "4.21.0",
"codice-fiscale-js": "2.3.22",
@@ -42,6 +45,7 @@
"recharts": "2.15.2",
"sockjs-client": "1.6.1",
"validate.js": "0.13.1",
"xlsx": "^0.18.5",
"zustand": "5.0.6",
"zustand-x": "6.1.1"
},
@@ -88,4 +92,4 @@
"last 1 safari version"
]
}
}
}

View File

@@ -176,6 +176,19 @@
width: 40rem;
}
.formBuilder__elementSettings--wide {
width: 90%;
}
// Univer renders context menus / popups with position:fixed and z-index:1070,
// but PrimeReact Sidebar (modal type) sits at z-index ~1100, hiding them.
// Raise all Univer popup/floating layers above the sidebar.
.univer-popup,
.univer-z-\[1020\],
.univer-z-\[1080\] {
z-index: 9999 !important;
}
.formElementSettings {
display: flex;
flex-direction: column;

View File

@@ -0,0 +1,84 @@
import React, { useEffect, useRef } from 'react';
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';
const parseWorkbook = (val) => {
if (!val) return null;
if (typeof val === 'object' && val.sheets) return val;
if (typeof val === 'string') {
try {
const parsed = JSON.parse(val);
return parsed && parsed.sheets ? parsed : null;
} catch {
return null;
}
}
return null;
};
const Spreadsheet = ({ fieldName, defaultValue, setDataFn, template }) => {
const containerRef = useRef(null);
const univerRef = useRef(null);
const univerAPIRef = useRef(null);
const saveTimerRef = 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,
}),
],
});
univerRef.current = univer;
univerAPIRef.current = univerAPI;
const initialData = parseWorkbook(defaultValue) || parseWorkbook(template) || { name: 'Sheet' };
univerAPI.createWorkbook(initialData);
univerAPI.addEvent(univerAPI.Event.BeforeCommandExecute, () => {
clearTimeout(saveTimerRef.current);
saveTimerRef.current = setTimeout(() => {
const wb = univerAPIRef.current?.getActiveWorkbook();
if (wb) {
setDataFn(fieldName, wb.save());
}
}, 300);
});
return () => {
clearTimeout(saveTimerRef.current);
if (univerRef.current) {
univerRef.current.dispose();
univerRef.current = null;
univerAPIRef.current = null;
}
};
}, []); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const handler = (e) => { e.stopPropagation(); e.preventDefault(); };
el.addEventListener('wheel', handler, { passive: false });
return () => el.removeEventListener('wheel', handler);
}, []);
return (
<div
ref={containerRef}
style={{ width: '100%', height: '500px' }}
/>
);
};
export default Spreadsheet;

View File

@@ -19,6 +19,7 @@ import Table from './components/Table';
import PasswordField from './components/PasswordField';
import CriteriaTable from './components/CriteriaTable';
import FileSelect from './components/FileSelect';
import Spreadsheet from './components/Spreadsheet';
const FormField = (props) => {
const fields = {
@@ -38,6 +39,7 @@ const FormField = (props) => {
criteria_table: CriteriaTable,
password: PasswordField,
fileselect: FileSelect,
spreadsheet: Spreadsheet,
}
const Comp = !isNil(fields[props.type]) ? fields[props.type] : null;

View File

@@ -730,6 +730,7 @@ const BandoApplication = () => {
if (!tableColumns) {
tableColumns = head(o.settings.filter(o => o.name === 'criteria_table_columns'));
}
const template = head(o.settings.filter(s => s.name === 'template'));
const step = head(o.settings.filter(o => o.name === 'step'));
const mime = head(o.settings.filter(o => o.name === 'mime'));
const documentCategories = head(o.settings.filter(o => o.name === 'documentCategories'));
@@ -794,6 +795,7 @@ const BandoApplication = () => {
sourceId={getApplicationId()}
useGrouping={false}
tableColumns={tableColumns ? tableColumns.value : {}}
template={template ? template.value : null}
/>
})
: null}

View File

@@ -378,6 +378,7 @@ const BandoApplicationPreview = () => {
if (!tableColumns) {
tableColumns = head(o.settings.filter(o => o.name === 'criteria_table_columns'));
}
const template = head(o.settings.filter(s => s.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'));
@@ -435,6 +436,7 @@ const BandoApplicationPreview = () => {
sourceId={getApplicationId()}
useGrouping={false}
tableColumns={tableColumns ? tableColumns.value : {}}
template={template ? template.value : null}
/>
})}

View File

@@ -92,6 +92,29 @@ const BandoEditFormStep3 = forwardRef(function () {
storeSet('unsetAsyncRequest');
}
const getFormsCallback = (resp) => {
if (resp.status === 'SUCCESS') {
const EXCLUDED_TYPES = new Set(['fileupload', 'fileselect', 'table', 'criteria_table', 'spreadsheet']);
const raw = (resp.data ?? []).flatMap(form =>
(form.content ?? [])
.filter(f => !EXCLUDED_TYPES.has(f.name))
.map(f => ({
id: f.id,
label: f.label,
placeholder: f.settings?.find(s => s.name === 'placeholder')?.value ?? ''
}))
);
const byLabel = new Map();
raw.forEach(f => {
if (!byLabel.has(f.label)) byLabel.set(f.label, { label: f.label, ids: [], placeholder: f.placeholder });
byLabel.get(f.label).ids.push(f.id);
});
storeSet('callFormFields', Array.from(byLabel.values()));
}
}
const errGetFormsCallback = () => {}
const getFormCallback = (resp) => {
if (resp.status === 'SUCCESS') {
storeSet('formId', resp.data.id);
@@ -117,12 +140,14 @@ const BandoEditFormStep3 = forwardRef(function () {
useEffect(() => {
storeSet('setAsyncRequest');
FormsService.getElementItems(getElementItemsCallback, errGetElementItemsCallbacks);
FormsService.getFormsForCall(id, getFormsCallback, errGetFormsCallback);
return () => {
storeSet('formId', 0);
storeSet('formElements', []);
storeSet('activeElement', '');
storeSet('selectedElement', '');
storeSet('callFormFields', []);
}
}, []);

View File

@@ -0,0 +1,338 @@
import React, { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
import { __ } from '@wordpress/i18n';
import { useNavigate, useParams } from 'react-router-dom';
import { klona } from 'klona';
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';
// api
import EvaluationFormsService from '../../../../service/evaluation-forms-service';
import FormsService from '../../../../service/forms-service';
// store
import { storeGet, storeSet } from '../../../../store';
// tools
import set404FromErrorResponse from '../../../../helpers/set404FromErrorResponse';
import { xlsxToWorkbookData } from './xlsxToWorkbookData';
// components
import BandoEditFormActions from '../BandoEditFormActions';
import { Toast } from 'primereact/toast';
const BandoEditFormStep3Excel = forwardRef(function () {
const navigate = useNavigate();
const { id } = useParams();
const [formName, setFormName] = useState('');
const [bandoStatus, setBandoStatus] = useState('');
const [tagTooltip, setTagTooltip] = useState(null); // { label } | null
const toast = useRef(null);
const univerRef = useRef(null);
const containerRef = useRef(null);
const univerAPIRef = useRef(null);
const formFieldsRef = useRef([]);
const fieldsLoadedRef = useRef(false);
const pendingWorkbookRef = useRef(undefined); // undefined = eval form not yet returned, null = returned with no workbook
const mousePos = useRef({ x: 0, y: 0 });
const fileInputRef = useRef(null);
const getBandoId = () => {
const parsed = parseInt(id)
return !isNaN(parsed) ? parsed : 0;
}
const onSaveDraft = () => {
const workbookData = univerAPIRef.current?.getActiveWorkbook()?.save() ?? null;
const formId = storeGet('formId');
const formData = {
label: formName,
content: workbookData,
}
storeSet('setAsyncRequest');
EvaluationFormsService.updateForm(formId, formData, updateFormCallback, errUpdateFormCallback)
}
const updateFormCallback = (resp) => {
if (resp.status === 'SUCCESS') {
setBandoStatus(resp.data.callStatus);
if (toast.current) {
toast.current.show({
severity: 'success',
summary: '',
detail: __('Il bando è stato aggiornato correttamente!', 'gepafin')
});
}
}
storeSet('unsetAsyncRequest');
}
const errUpdateFormCallback = (resp) => {
set404FromErrorResponse(resp);
storeSet('unsetAsyncRequest');
}
const openPreview = () => {
const bandoId = getBandoId();
navigate(`/bandi/${bandoId}/preview`);
}
const openPreviewEvaluation = () => {
const bandoId = getBandoId();
navigate(`/bandi/${bandoId}/preview-evaluation`);
}
const getFormCallback = (resp) => {
if (resp.status === 'SUCCESS') {
storeSet('formId', resp.data.id);
storeSet('formLabel', resp.data.label);
setFormName(resp.data.label);
setBandoStatus(resp.data.callStatus);
const content = resp.data.content ? klona(resp.data.content) : null;
storeSet('formElements', content);
const workbook = content && content.sheets ? content : null;
if (fieldsLoadedRef.current) {
initializeUniver(workbook);
} else {
pendingWorkbookRef.current = workbook; // null or workbook; signals eval form returned
}
} else {
if (!fieldsLoadedRef.current) {
pendingWorkbookRef.current = null; // eval form done, no workbook
}
}
storeSet('unsetAsyncRequest');
}
const getFormsCallback = (resp) => {
if (resp.status === 'SUCCESS') {
const EXCLUDED_TYPES = new Set(['fileupload', 'fileselect', 'table', 'criteria_table']);
const raw = (resp.data ?? []).flatMap(form =>
(form.content ?? [])
.filter(f => !EXCLUDED_TYPES.has(f.name))
.map(f => ({ id: f.id, label: f.label }))
);
// Group by label: same label across forms → one entry with multiple ids
const byLabel = new Map();
raw.forEach(f => {
if (!byLabel.has(f.label)) byLabel.set(f.label, { label: f.label, ids: [] });
byLabel.get(f.label).ids.push(f.id);
});
formFieldsRef.current = Array.from(byLabel.values());
}
fieldsLoadedRef.current = true;
if (pendingWorkbookRef.current !== undefined) {
// eval form already returned; initialize with its workbook (may be null)
initializeUniver(pendingWorkbookRef.current);
pendingWorkbookRef.current = undefined;
}
// else: eval form hasn't returned yet; getFormCallback will trigger initializeUniver
}
const errGetFormsCallback = () => {
fieldsLoadedRef.current = true;
if (pendingWorkbookRef.current !== undefined) {
initializeUniver(pendingWorkbookRef.current);
pendingWorkbookRef.current = undefined;
}
}
const errGetFormCallback = (resp) => {
set404FromErrorResponse(resp);
if (!fieldsLoadedRef.current) {
pendingWorkbookRef.current = null;
}
storeSet('unsetAsyncRequest');
}
useEffect(() => {
storeSet('setAsyncRequest');
EvaluationFormsService.getFormForCall(id, getFormCallback, errGetFormCallback)
}, [id]);
useEffect(() => {
FormsService.getFormsForCall(id, getFormsCallback, errGetFormsCallback);
}, [id]);
const insertFieldTag = (ids, type) => {
const api = univerAPIRef.current;
if (!api) return;
const range = api.getActiveWorkbook()?.getActiveSheet()?.getActiveRange();
if (!range) return;
range.setValue(`{{gepafin_field:${ids.join(',')}|${type}}}`);
range.setBackground('#dbeafe');
};
const initializeUniver = useCallback((workbookData) => {
if (univerRef.current) {
univerRef.current.dispose();
univerRef.current = null;
univerAPIRef.current = null;
}
if (!containerRef.current) return;
const { univer, univerAPI } = createUniver({
locale: LocaleType.EN_US,
locales: {
[LocaleType.EN_US]: mergeLocales(UniverPresetSheetsCoreEnUS),
},
presets: [
UniverSheetsCorePreset({
container: containerRef.current,
}),
],
});
univerAPI.createWorkbook(workbookData || { name: 'Sheet' });
univerRef.current = univer;
univerAPIRef.current = univerAPI;
// Disable adding new sheets
univerAPI.addEvent(univerAPI.Event.BeforeCommandExecute, (event) => {
if (event.id === 'sheet.command.insert-sheet') {
event.cancel = true;
}
});
// Context menu: "Inserisci variabile GEPAFIN" with per-field submenus
const fields = formFieldsRef.current;
const submenu = univerAPI.createSubmenu({
id: 'gepafin-insert-variable',
title: 'Inserisci variabile GEPAFIN',
});
fields.forEach((field, index) => {
if (index > 0) submenu.addSeparator();
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'),
}));
submenu.addSubmenu(univerAPI.createMenu({
id: `gepafin-field-${menuKey}-value`,
title: `${field.label}${countLabel} (valore)`,
action: () => insertFieldTag(field.ids, 'value'),
}));
});
submenu.appendTo(['contextMenu.mainArea', 'contextMenu.others']);
// Tooltip on hover over tagged cells
univerAPI.addEvent(univerAPI.Event.CellHover, (params) => {
const { row, column, worksheet } = params;
const cellValue = worksheet?.getRange(row, column)?.getValue?.();
if (typeof cellValue === 'string' && cellValue.startsWith('{{gepafin_field:')) {
const match = cellValue.match(/\{\{gepafin_field:([^|]+)\|([^}]+)\}\}/);
if (match) {
const tagIds = match[1].split(',');
const field = formFieldsRef.current.find(f => f.ids.some(id => tagIds.includes(id)));
const countSuffix = tagIds.length > 1 ? `${tagIds.length} moduli` : '';
setTagTooltip({ label: `${field ? field.label : match[1]} (${match[2]})${countSuffix}` });
}
} else {
setTagTooltip(null);
}
});
}, []); // containerRef, formFieldsRef, univerRef, univerAPIRef are stable refs
useEffect(() => {
return () => {
if (univerRef.current) {
univerRef.current.dispose();
univerRef.current = null;
univerAPIRef.current = null;
}
};
}, []);
// Prevent page scroll while hovering over the spreadsheet
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const handler = (e) => { e.stopPropagation(); e.preventDefault(); };
el.addEventListener('wheel', handler, { passive: false });
return () => el.removeEventListener('wheel', handler);
}, []);
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);
initializeUniver(data);
};
reader.readAsArrayBuffer(file);
e.target.value = ''; // reset so same file can be re-imported
};
return (
<div className="appForm">
<div className="appPageSection">
<div style={{ marginBottom: '8px', display: 'flex', justifyContent: 'flex-end' }}>
<input
ref={fileInputRef}
type="file"
accept=".xlsx,.xls,.csv,.ods"
style={{ display: 'none' }}
onChange={handleImport}
/>
<button
type="button"
className="p-button p-button-outlined p-button-sm"
onClick={() => fileInputRef.current?.click()}
>
{__('Importa foglio', 'gepafin')}
</button>
</div>
<div
ref={containerRef}
style={{ width: '100%', height: '700px' }}
onMouseMove={(e) => { mousePos.current = { x: e.clientX, y: e.clientY }; }}
onMouseLeave={() => setTagTooltip(null)}
/>
{tagTooltip && (
<div style={{
position: 'fixed',
top: mousePos.current.y - 36,
left: mousePos.current.x + 12,
background: '#1e293b',
color: '#fff',
padding: '4px 8px',
borderRadius: '4px',
fontSize: '12px',
pointerEvents: 'none',
zIndex: 9999,
whiteSpace: 'nowrap',
}}>
{tagTooltip.label}
</div>
)}
</div>
<div className="appPage__spacer"></div>
<div className="appPageSection__hr">
<span>{__('Azioni', 'gepafin')}</span>
</div>
<Toast ref={toast} />
<BandoEditFormActions
id={id}
status={bandoStatus}
submitFn={onSaveDraft}
openPreview={openPreview}
openPreviewEvaluation={openPreviewEvaluation}/>
</div>
)
})
export default BandoEditFormStep3Excel;

View File

@@ -0,0 +1,58 @@
import * as XLSX from 'xlsx';
export function xlsxToWorkbookData(arrayBuffer) {
const wb = XLSX.read(arrayBuffer, { type: 'array', cellStyles: true, sheetStubs: true });
const sheets = {};
const sheetOrder = [];
wb.SheetNames.forEach((sheetName, sheetIndex) => {
const ws = wb.Sheets[sheetName];
const sheetId = `sheet_${sheetIndex}`;
sheetOrder.push(sheetId);
const cellData = {};
const ref = ws['!ref'];
if (!ref) {
sheets[sheetId] = { id: sheetId, name: sheetName, cellData };
return;
}
const range = XLSX.utils.decode_range(ref);
for (let r = range.s.r; r <= range.e.r; r++) {
for (let c = range.s.c; c <= range.e.c; c++) {
const cellAddress = XLSX.utils.encode_cell({ r, c });
const cell = ws[cellAddress];
if (!cell) continue;
// CellValueType: 1=STRING, 2=NUMBER, 3=BOOLEAN
let t;
if (cell.t === 'n') t = 2;
else if (cell.t === 'b') t = 3;
else t = 1;
if (!cellData[r]) cellData[r] = {};
cellData[r][c] = { v: cell.v !== undefined ? cell.v : (cell.w || ''), t };
}
}
const mergeData = (ws['!merges'] || []).map(m => ({
startRow: m.s.r,
startColumn: m.s.c,
endRow: m.e.r,
endColumn: m.e.c,
}));
sheets[sheetId] = { id: sheetId, name: sheetName, cellData, mergeData };
});
return {
id: 'imported-workbook',
name: wb.SheetNames[0] || 'Sheet',
appVersion: '0.18.0',
locale: 'enUS',
styles: {},
sheetOrder,
sheets,
};
}

View File

@@ -23,8 +23,8 @@ import BandoEditFormStep2 from './components/BandoEditFormStep2';
import { Messages } from 'primereact/messages';
import BlockingOverlay from '../../components/BlockingOverlay';
import { Toast } from 'primereact/toast';
import BandoEditFormStep3 from './components/BandoEditFormStep3';
import { ConfirmPopup, confirmPopup } from 'primereact/confirmpopup';
import BandoEditFormStep3 from './components/BandoEditFormStep3';
const BandoEdit = () => {
const isAsyncRequest = useStoreValue('isAsyncRequest');

View File

@@ -18,6 +18,7 @@ import { InputSwitch } from 'primereact/inputswitch';
import ElementSettingChips from '../ElementSettingChips';
import ElementSettingCriteriaTableColumns from '../ElementSettingCriteriaTableColumns';
import ElementSettingTableColumnsForCsv from '../ElementSettingTableColumnsForCsv';
import ElementSettingSpreadsheet from '../ElementSettingSpreadsheet';
import { mimeTypes } from '../../../../../../configData';
import ElementSettingReportHeader from '../ElementSettingReportHeader';
@@ -47,7 +48,8 @@ const ElementSetting = ({ setting, changeFn, updateDataFn, bandoStatus }) => {
isChecklistItem: __('Fa parte di "checklist"?', 'gepafin'),
reportEnable: __('Aggiungere nel report CSV?', 'gepafin'),
reportHeader: __('Nome della colonna nel CSV', 'gepafin'),
reportColumns: __('', 'gepafin')
reportColumns: __('', 'gepafin'),
template: __('Template foglio', 'gepafin')
}
const settingDescription = {
@@ -156,6 +158,11 @@ const ElementSetting = ({ setting, changeFn, updateDataFn, bandoStatus }) => {
name={setting.name}
bandoStatus={bandoStatus}
setDataFn={updateDataFn}/>
} else if (setting.name === 'template') {
return <ElementSettingSpreadsheet
value={setting.value}
name={setting.name}
setDataFn={updateDataFn}/>;
} else if (setting.name === 'formula') {
const isInvalid = invalidFormula(setting.value);
return <>

View File

@@ -0,0 +1,215 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { __ } from '@wordpress/i18n';
import { isEmpty } from 'ramda';
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';
// store
import { useStoreValue } from '../../../../../../store';
// tools
import { xlsxToWorkbookData } from '../../../../../BandoEdit/components/BandoEditFormStep3Excel/xlsxToWorkbookData';
const ElementSettingSpreadsheet = ({ value, name, setDataFn }) => {
const callFormFields = useStoreValue('callFormFields');
const [tagTooltip, setTagTooltip] = useState(null);
const containerRef = useRef(null);
const univerRef = useRef(null);
const univerAPIRef = useRef(null);
const formFieldsRef = useRef([]);
const mousePos = useRef({ x: 0, y: 0 });
const fileInputRef = useRef(null);
const insertFieldTag = (ids, type, field) => {
const api = univerAPIRef.current;
if (!api) return;
const range = api.getActiveWorkbook()?.getActiveSheet()?.getActiveRange();
if (!range) return;
const displayValue = type === 'value' ? (field.placeholder || '') : field.label;
range.setValues([[{
v: displayValue,
f: `{{gepafin_field:${ids.join(',')}|${type}}}`
}]]);
range.setBackground('#dbeafe');
};
const initializeUniver = useCallback((workbookData) => {
if (univerRef.current) {
univerRef.current.dispose();
univerRef.current = null;
univerAPIRef.current = null;
}
if (!containerRef.current) return;
const { univer, univerAPI } = createUniver({
locale: LocaleType.EN_US,
locales: {
[LocaleType.EN_US]: mergeLocales(UniverPresetSheetsCoreEnUS),
},
presets: [
UniverSheetsCorePreset({
container: containerRef.current,
}),
],
});
univerAPI.createWorkbook(workbookData || { name: 'Sheet' });
univerRef.current = univer;
univerAPIRef.current = univerAPI;
// Disable adding new sheets
univerAPI.addEvent(univerAPI.Event.BeforeCommandExecute, (event) => {
if (event.id === 'sheet.command.insert-sheet') {
event.cancel = true;
}
});
// 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',
});
fields.forEach((field, index) => {
if (index > 0) submenu.addSeparator();
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}-value`,
title: `${field.label}${countLabel} (valore)`,
action: () => insertFieldTag(field.ids, 'value', field),
}));
});
submenu.appendTo(['contextMenu.mainArea', 'contextMenu.others']);
}
// Tooltip on hover over tagged cells — read formula field to detect tag
univerAPI.addEvent(univerAPI.Event.CellHover, (params) => {
const { row, column, worksheet } = params;
const formula = worksheet?.getRange(row, column)?.getFormulas()?.[0]?.[0];
if (typeof formula === 'string' && formula.startsWith('{{gepafin_field:')) {
const match = formula.match(/\{\{gepafin_field:([^|]+)\|([^}]+)\}\}/);
if (match) {
const tagIds = match[1].split(',');
const field = formFieldsRef.current.find(f => f.ids.some(fid => tagIds.includes(fid)));
const countSuffix = tagIds.length > 1 ? `${tagIds.length} moduli` : '';
setTagTooltip({ label: `${field ? field.label : match[1]} (${match[2]})${countSuffix}` });
}
} else {
setTagTooltip(null);
}
});
}, []); // refs are stable
// Keep formFieldsRef in sync with store value
useEffect(() => {
formFieldsRef.current = callFormFields;
}, [callFormFields]);
// Initialize Univer on mount
useEffect(() => {
const workbook = value && !isEmpty(value) && value.sheets ? value : null;
initializeUniver(workbook);
return () => {
if (univerRef.current) {
univerRef.current.dispose();
univerRef.current = null;
univerAPIRef.current = null;
}
};
}, []); // run once on mount
// Prevent page scroll while hovering over the spreadsheet
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const handler = (e) => { e.stopPropagation(); e.preventDefault(); };
el.addEventListener('wheel', handler, { passive: false });
return () => el.removeEventListener('wheel', handler);
}, []);
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);
initializeUniver(data);
};
reader.readAsArrayBuffer(file);
e.target.value = '';
};
const handleSave = () => {
const workbookData = univerAPIRef.current?.getActiveWorkbook()?.save() ?? {};
setDataFn(name, workbookData);
};
return (
<div style={{ width: '100%' }}>
<div style={{ marginBottom: '8px', display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
<input
ref={fileInputRef}
type="file"
accept=".xlsx,.xls,.csv,.ods"
style={{ display: 'none' }}
onChange={handleImport}
/>
<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-sm"
onClick={handleSave}
>
{__('Salva template', 'gepafin')}
</button>
</div>
<div
ref={containerRef}
style={{ width: '100%', height: '500px' }}
onMouseMove={(e) => { mousePos.current = { x: e.clientX, y: e.clientY }; }}
onMouseLeave={() => setTagTooltip(null)}
/>
{tagTooltip && (
<div style={{
position: 'fixed',
top: mousePos.current.y - 36,
left: mousePos.current.x + 12,
background: '#1e293b',
color: '#fff',
padding: '4px 8px',
borderRadius: '4px',
fontSize: '12px',
pointerEvents: 'none',
zIndex: 9999,
whiteSpace: 'nowrap',
}}>
{tagTooltip.label}
</div>
)}
</div>
);
};
export default ElementSettingSpreadsheet;

View File

@@ -18,6 +18,7 @@ const FormBuilder = ({ callStatus, context }) => {
const elementItems = useStoreValue('elementItems');
const activeElement = useStoreValue('activeElement');
const isAsyncRequest = useStoreValue('isAsyncRequest');
const isSpreadsheetActive = elements.find(el => el.id === activeElement)?.name === 'spreadsheet';
const renderField = useCallback((field, index) => {
return (
@@ -56,7 +57,7 @@ const FormBuilder = ({ callStatus, context }) => {
return (
<>
<Sidebar visible={!isEmpty(activeElement)} onHide={closeSettings} dismissable={false} className="formBuilder__elementSettings">
<Sidebar visible={!isEmpty(activeElement)} onHide={closeSettings} dismissable={false} className={`formBuilder__elementSettings${isSpreadsheetActive ? ' formBuilder__elementSettings--wide' : ''}`}>
<h2>{__('Impostazioni del campo modulo', 'gepafin')}</h2>
{!isEmpty(activeElement)
? <BuilderElementSettings closeSettingsFn={closeSettings} callStatus={callStatus} context={context}/>

View File

@@ -1586,6 +1586,7 @@ const DomandaEditInstructorManager = () => {
if (!tableColumns) {
tableColumns = head(o.settings.filter(o => o.name === 'criteria_table_columns'));
}
const template = head(o.settings.filter(s => s.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'));
@@ -1645,6 +1646,7 @@ const DomandaEditInstructorManager = () => {
sourceId={id}
useGrouping={false}
tableColumns={tableColumns ? tableColumns.value : {}}
template={template ? template.value : null}
/>
})}
</form>

View File

@@ -1493,6 +1493,7 @@ const DomandaEditPreInstructor = () => {
if (!tableColumns) {
tableColumns = head(o.settings.filter(o => o.name === 'criteria_table_columns'));
}
const template = head(o.settings.filter(s => s.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'));
@@ -1552,6 +1553,7 @@ const DomandaEditPreInstructor = () => {
sourceId={id}
useGrouping={false}
tableColumns={tableColumns ? tableColumns.value : {}}
template={template ? template.value : null}
/>
})}
</form>

View File

@@ -73,7 +73,7 @@ const LoginConfidi = () => {
}
useEffect(() => {
if (!isEmpty(token)) {
if (!isEmpty(token) && !AuthenticationService.isExpired()) {
setLoading(true);
window.location.replace('/')
}

View File

@@ -1,4 +1,4 @@
import { storeGet, storeSet } from '../store';
import { storeGet } from '../store';
import logMsgWithSentry from '../helpers/logMsgWithSentry';
import { isEmpty } from 'ramda';

View File

@@ -17,6 +17,7 @@ const initialStore = {
formLabel: '',
formElements: [],
elementItems: [],
callFormFields: [],
activeElement: '',
selectedElement: '',
draggingElementId: 0,

View File

@@ -675,5 +675,23 @@ export const elementItems = [
validators: {
isRequired: false
}
},
{
id: 24,
sortOrder: 24,
name: 'spreadsheet',
label: 'Foglio di Calcolo',
description: 'Modello di foglio di calcolo con variabili dinamiche',
settings: [
{
name: "label",
value: "Foglio di Calcolo"
},
{
name: "template",
value: {}
}
],
validators: {}
}
]