@topconsultnpm/sdkui-react 6.22.0-dev1.8 → 6.22.0-dev2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/lib/components/base/TMButton.d.ts +13 -0
  2. package/lib/components/base/TMButton.js +137 -5
  3. package/lib/components/base/TMDataGridExportForm.js +39 -30
  4. package/lib/components/base/TMModal.js +1 -1
  5. package/lib/components/choosers/TMDynDataListItemChooser.d.ts +2 -0
  6. package/lib/components/choosers/TMDynDataListItemChooser.js +4 -6
  7. package/lib/components/editors/TMDateBox.d.ts +16 -1
  8. package/lib/components/editors/TMDateBox.js +90 -22
  9. package/lib/components/editors/TMMetadataEditor.d.ts +5 -1
  10. package/lib/components/editors/TMMetadataEditor.js +27 -23
  11. package/lib/components/editors/TMTextArea.d.ts +1 -0
  12. package/lib/components/editors/TMTextArea.js +23 -15
  13. package/lib/components/editors/TMTextBox.d.ts +2 -0
  14. package/lib/components/editors/TMTextBox.js +38 -22
  15. package/lib/components/features/documents/TMDcmtForm.d.ts +2 -1
  16. package/lib/components/features/documents/TMDcmtForm.js +112 -25
  17. package/lib/components/features/documents/TMRelationViewer.js +13 -32
  18. package/lib/components/features/search/TMSearch.d.ts +2 -1
  19. package/lib/components/features/search/TMSearch.js +31 -18
  20. package/lib/components/features/search/TMSearchQueryPanel.d.ts +1 -1
  21. package/lib/components/features/search/TMSearchQueryPanel.js +28 -7
  22. package/lib/components/features/search/TMSearchResult.d.ts +2 -1
  23. package/lib/components/features/search/TMSearchResult.js +16 -41
  24. package/lib/components/features/search/TMSignatureInfoContent.d.ts +4 -2
  25. package/lib/components/features/search/TMSignatureInfoContent.js +373 -79
  26. package/lib/components/forms/Login/TMLoginForm.d.ts +2 -0
  27. package/lib/components/forms/Login/TMLoginForm.js +17 -3
  28. package/lib/components/forms/Login/TextBox.d.ts +3 -0
  29. package/lib/components/forms/Login/TextBox.js +2 -2
  30. package/lib/components/pages/TMPage.js +3 -1
  31. package/lib/components/query/TMQueryEditor.js +1 -1
  32. package/lib/components/viewers/TMMidViewer.js +1 -1
  33. package/lib/helper/Globalization.d.ts +1 -1
  34. package/lib/helper/SDKUI_Globals.js +22 -2
  35. package/lib/helper/SDKUI_Localizator.d.ts +11 -0
  36. package/lib/helper/SDKUI_Localizator.js +110 -0
  37. package/lib/helper/TMUtils.d.ts +29 -1
  38. package/lib/helper/TMUtils.js +249 -10
  39. package/lib/helper/grafometricSignaturesCache.d.ts +45 -0
  40. package/lib/helper/grafometricSignaturesCache.js +56 -0
  41. package/lib/helper/helpers.d.ts +11 -0
  42. package/lib/helper/helpers.js +32 -0
  43. package/lib/helper/index.d.ts +1 -0
  44. package/lib/helper/index.js +1 -0
  45. package/lib/hooks/useDocumentOperations.d.ts +2 -1
  46. package/lib/hooks/useDocumentOperations.js +15 -15
  47. package/lib/hooks/usePreventFileDrop.js +14 -3
  48. package/lib/ts/graphometricTypes.d.ts +62 -0
  49. package/lib/ts/graphometricTypes.js +1 -0
  50. package/lib/ts/index.d.ts +1 -0
  51. package/lib/ts/index.js +1 -0
  52. package/package.json +7 -2
@@ -3,6 +3,17 @@ import { ITMEditorBase } from './TMEditorBase';
3
3
  export type ButtonColors = 'error' | 'warning' | 'success' | 'info' | 'primary' | 'secondary' | 'tertiary' | 'standard' | 'primaryOutline' | 'secondaryOutline' | 'tertiaryOutline' | 'errorOutline' | 'warningOutline' | 'successOutline' | 'infoOutline';
4
4
  export type ButtonStyle = 'normal' | 'toolbar' | 'icon' | 'text' | 'advanced';
5
5
  export type AdvancedButtonType = 'success' | 'error' | 'tertiary' | 'primary';
6
+ /** Azione secondaria mostrata nel dropdown "split button" (freccia a destra) */
7
+ export interface ITMButtonSecondaryAction {
8
+ /** Etichetta mostrata nella voce di menu */
9
+ name: string;
10
+ /** Icona opzionale a sinistra della voce */
11
+ icon?: React.ReactNode;
12
+ /** Callback al click della voce */
13
+ onClick?: () => void;
14
+ /** Disabilita la singola voce */
15
+ disabled?: boolean;
16
+ }
6
17
  export interface ITMButton extends ITMEditorBase {
7
18
  color?: ButtonColors;
8
19
  btnStyle?: ButtonStyle;
@@ -17,6 +28,8 @@ export interface ITMButton extends ITMEditorBase {
17
28
  showTooltip?: boolean;
18
29
  onClick?: VoidFunction;
19
30
  onMouseDown?: (e: any) => any;
31
+ /** Azioni secondarie: se valorizzate, il bottone mostra una freccia a destra che apre un dropdown con queste azioni */
32
+ secondaryActions?: ITMButtonSecondaryAction[];
20
33
  }
21
34
  declare const TMButton: React.ForwardRefExoticComponent<ITMButton & React.RefAttributes<HTMLButtonElement>>;
22
35
  export default TMButton;
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useState, forwardRef } from 'react';
2
+ import { useState, forwardRef, useRef, useEffect } from 'react';
3
+ import { createPortal } from 'react-dom';
3
4
  import styled from 'styled-components';
4
- import { getColor } from '../../helper';
5
+ import { getColor, IconChevronDown } from '../../helper';
5
6
  import { FontSize, TMColors } from '../../utils/theme';
6
7
  import TMTooltip from './TMTooltip';
7
8
  const StyledNormalButton = styled.button.withConfig({ shouldForwardProp: prop => !['text'].includes(prop) }) `
@@ -149,10 +150,132 @@ const StyledAdvancedButtonText = styled.div `
149
150
  border-top-right-radius: 10px;
150
151
  border-bottom-right-radius: 10px;
151
152
  padding: 5px;
153
+ padding-right: ${props => props.$reserveArrow ? '26px' : '5px'};
154
+ `;
155
+ const StyledSplitContainer = styled.div `
156
+ display: inline-flex;
157
+ align-items: stretch;
158
+ position: relative;
159
+ `;
160
+ const StyledSplitArrow = styled.button `
161
+ position: absolute;
162
+ right: 4px;
163
+ top: 50%;
164
+ transform: translateY(-50%);
165
+ z-index: 2;
166
+ display: flex;
167
+ align-items: center;
168
+ justify-content: center;
169
+ width: 20px;
170
+ height: 20px;
171
+ padding: 0;
172
+ border: none;
173
+ border-radius: 6px;
174
+ cursor: ${props => props.$isDisabled ? 'default' : 'pointer'};
175
+ color: white;
176
+ background-color: rgba(255, 255, 255, 0.22);
177
+ transition: background-color ease 150ms;
178
+ &:hover { background-color: ${props => props.$isDisabled ? 'rgba(255, 255, 255, 0.22)' : 'rgba(255, 255, 255, 0.38)'}; }
179
+ & svg {
180
+ transition: transform ease 150ms;
181
+ transform: rotate(${props => props.$isOpen ? '180deg' : '0deg'});
182
+ }
183
+ `;
184
+ const StyledDropdown = styled.div `
185
+ position: fixed;
186
+ left: ${props => props.$left}px;
187
+ top: ${props => props.$top}px;
188
+ transform: translateY(-100%);
189
+ transform-origin: bottom left;
190
+ min-width: ${props => props.$minWidth}px;
191
+ background: #ffffff;
192
+ border-radius: 14px;
193
+ box-shadow: 0 10px 30px rgba(20, 40, 80, 0.20);
194
+ border: 1px solid rgba(74, 150, 210, 0.18);
195
+ padding: 5px;
196
+ z-index: 100000;
197
+ display: flex;
198
+ flex-direction: column;
199
+ gap: 2px;
200
+ animation: tmSplitDropdownIn cubic-bezier(0.2, 0.8, 0.2, 1) 150ms;
201
+ @keyframes tmSplitDropdownIn {
202
+ from { opacity: 0; transform: translateY(calc(-100% + 8px)); }
203
+ to { opacity: 1; transform: translateY(-100%); }
204
+ }
205
+ `;
206
+ const StyledDropdownItem = styled.button `
207
+ display: flex;
208
+ align-items: center;
209
+ gap: 9px;
210
+ width: 100%;
211
+ padding: 5px 9px;
212
+ border: none;
213
+ border-radius: 8px;
214
+ background: transparent;
215
+ text-align: left;
216
+ font-size: ${FontSize.defaultFontSize};
217
+ font-weight: 500;
218
+ color: ${props => props.$disabled ? '#b4b4b4' : '#2c3e50'};
219
+ cursor: ${props => props.$disabled ? 'default' : 'pointer'};
220
+ transition: background ease 150ms, color ease 150ms;
221
+ &:hover {
222
+ background: ${props => props.$disabled ? 'transparent' : 'linear-gradient(135deg, rgba(74, 150, 210, 0.16) 0%, rgba(37, 89, 165, 0.16) 100%)'};
223
+ color: ${props => props.$disabled ? '#b4b4b4' : '#2559A5'};
224
+ }
225
+ & > .tm-split-item-icon {
226
+ display: flex;
227
+ align-items: center;
228
+ justify-content: center;
229
+ width: 22px;
230
+ height: 22px;
231
+ flex-shrink: 0;
232
+ border-radius: 6px;
233
+ background: rgba(74, 150, 210, 0.14);
234
+ color: #4A96D2;
235
+ transition: background ease 150ms, color ease 150ms;
236
+ }
237
+ &:hover > .tm-split-item-icon {
238
+ background: ${props => props.$disabled ? 'rgba(74, 150, 210, 0.14)' : '#4A96D2'};
239
+ color: ${props => props.$disabled ? '#4A96D2' : '#ffffff'};
240
+ }
152
241
  `;
153
242
  const TMButton = forwardRef((props, ref) => {
154
- const { width, height, keyGesture, btnStyle = 'normal', advancedColor, advancedType = 'primary', color = 'primary', fontSize = FontSize.defaultFontSize, disabled = false, showTooltip = true, caption, icon, description, padding, elementStyle, onClick = () => { }, onMouseDown = () => { } } = props;
243
+ const { width, height, keyGesture, btnStyle = 'normal', advancedColor, advancedType = 'primary', color = 'primary', fontSize = FontSize.defaultFontSize, disabled = false, showTooltip = true, caption, icon, description, padding, elementStyle, onClick = () => { }, onMouseDown = () => { }, secondaryActions } = props;
155
244
  const [isHovered, setIsHovered] = useState(false);
245
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
246
+ const [dropdownPos, setDropdownPos] = useState({ left: 0, top: 0, minWidth: 200 });
247
+ const splitContainerRef = useRef(null);
248
+ const dropdownRef = useRef(null);
249
+ const hasSecondaryActions = !!secondaryActions && secondaryActions.length > 0;
250
+ useEffect(() => {
251
+ if (!isDropdownOpen)
252
+ return;
253
+ const handlePointerDown = (e) => {
254
+ const target = e.target;
255
+ if (splitContainerRef.current?.contains(target) || dropdownRef.current?.contains(target))
256
+ return;
257
+ setIsDropdownOpen(false);
258
+ };
259
+ const handleKeyDown = (e) => { if (e.key === 'Escape')
260
+ setIsDropdownOpen(false); };
261
+ document.addEventListener('mousedown', handlePointerDown);
262
+ document.addEventListener('touchstart', handlePointerDown);
263
+ document.addEventListener('keydown', handleKeyDown);
264
+ return () => {
265
+ document.removeEventListener('mousedown', handlePointerDown);
266
+ document.removeEventListener('touchstart', handlePointerDown);
267
+ document.removeEventListener('keydown', handleKeyDown);
268
+ };
269
+ }, [isDropdownOpen]);
270
+ const toggleDropdown = () => {
271
+ if (disabled)
272
+ return;
273
+ if (!isDropdownOpen && splitContainerRef.current) {
274
+ const rect = splitContainerRef.current.getBoundingClientRect();
275
+ setDropdownPos({ left: rect.left, top: rect.top - 6, minWidth: Math.max(rect.width, 200) });
276
+ }
277
+ setIsDropdownOpen(prev => !prev);
278
+ };
156
279
  const renderedButton = () => {
157
280
  if (btnStyle === 'normal') {
158
281
  return (_jsx(StyledNormalButton, { ref: ref, width: width, height: height, color: color, caption: caption, disabled: disabled, fontSize: fontSize, onClick: onClick, onMouseDown: (e) => !disabled && onMouseDown?.(e), children: caption }));
@@ -164,7 +287,7 @@ const TMButton = forwardRef((props, ref) => {
164
287
  return (_jsx(StyledIconButton, { ref: ref, color: color, caption: caption, disabled: disabled, fontSize: fontSize, padding: padding, onClick: onClick, onMouseDown: (e) => !disabled && onMouseDown?.(e), children: icon }));
165
288
  }
166
289
  else if (btnStyle === 'advanced') {
167
- return (_jsxs(StyledAdvancedButton, { ref: ref, "$width": width ?? '90px', "$height": height ?? '30px', onMouseOver: () => !disabled && setIsHovered(true), onMouseOut: () => setIsHovered(false), "$isDisabled": disabled, "$isPrimaryOutline": advancedType === 'primary' && (advancedColor === 'primaryOutline' || color === 'primaryOutline'), onClick: () => !disabled && onClick?.(), onMouseDown: (e) => !disabled && onMouseDown?.(e), children: [_jsx(StyledAdvancedButtonIcon, { "$color": advancedColor, "$isVisible": isHovered, "$isDisabled": disabled, "$type": advancedType, children: icon }), _jsx(StyledAdvancedButtonText, { "$color": advancedColor, "$isDisabled": disabled, "$type": advancedType, children: caption })] }));
290
+ return (_jsxs(StyledAdvancedButton, { ref: ref, "$width": width ?? '90px', "$height": height ?? '30px', onMouseOver: () => !disabled && setIsHovered(true), onMouseOut: () => setIsHovered(false), "$isDisabled": disabled, "$isPrimaryOutline": advancedType === 'primary' && (advancedColor === 'primaryOutline' || color === 'primaryOutline'), onClick: () => !disabled && onClick?.(), onMouseDown: (e) => !disabled && onMouseDown?.(e), children: [_jsx(StyledAdvancedButtonIcon, { "$color": advancedColor, "$isVisible": isHovered, "$isDisabled": disabled, "$type": advancedType, children: icon }), _jsx(StyledAdvancedButtonText, { "$color": advancedColor, "$isDisabled": disabled, "$type": advancedType, "$reserveArrow": hasSecondaryActions, children: caption })] }));
168
291
  }
169
292
  else {
170
293
  return (_jsx(StyledTextButton, { ref: ref, color: color, caption: caption, disabled: disabled, fontSize: fontSize, onClick: onClick, onMouseDown: (e) => !disabled && onMouseDown?.(e), children: caption }));
@@ -175,6 +298,15 @@ const TMButton = forwardRef((props, ref) => {
175
298
  _jsxs(_Fragment, { children: [caption &&
176
299
  _jsx(StyledButtonTooltipHeader, { "$description": description, children: _jsx(StyledButtonTooltipItem, { "$color": color, children: caption }) }), description && _jsx(StyledButtonTooltipItem, { children: description })] }));
177
300
  };
178
- return (_jsx("div", { style: elementStyle, children: showTooltip ? _jsx(TMTooltip, { hideAfterDelay: true, content: renderTooltip(), children: renderedButton() }) : renderedButton() }));
301
+ const buttonWithTooltip = showTooltip
302
+ ? _jsx(TMTooltip, { hideAfterDelay: true, content: renderTooltip(), children: renderedButton() })
303
+ : renderedButton();
304
+ const renderSplitButton = () => (_jsxs(StyledSplitContainer, { ref: splitContainerRef, children: [buttonWithTooltip, _jsx(StyledSplitArrow, { type: 'button', "$isDisabled": disabled, "$isOpen": isDropdownOpen, "aria-haspopup": 'menu', "aria-expanded": isDropdownOpen, onClick: (e) => { e.stopPropagation(); toggleDropdown(); }, onMouseDown: (e) => e.stopPropagation(), children: _jsx(IconChevronDown, { fontSize: 14 }) }), isDropdownOpen && createPortal(_jsx(StyledDropdown, { ref: dropdownRef, role: 'menu', "$left": dropdownPos.left, "$top": dropdownPos.top, "$minWidth": dropdownPos.minWidth, children: secondaryActions.map((action, idx) => (_jsxs(StyledDropdownItem, { type: 'button', role: 'menuitem', "$disabled": action.disabled, disabled: action.disabled, onClick: () => {
305
+ if (action.disabled)
306
+ return;
307
+ setIsDropdownOpen(false);
308
+ action.onClick?.();
309
+ }, children: [action.icon && _jsx("span", { className: 'tm-split-item-icon', children: action.icon }), _jsx("span", { children: action.name })] }, `${action.name}-${idx}`))) }), document.body)] }));
310
+ return (_jsx("div", { style: elementStyle, children: hasSecondaryActions ? renderSplitButton() : buttonWithTooltip }));
179
311
  });
180
312
  export default TMButton;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import { Workbook } from 'devextreme-exceljs-fork';
4
- import { buildValueToLabelMapFromDataColumns, getExceptionMessage, SDKUI_Localizator } from '../../helper';
4
+ import { buildValueToLabelMapFromDataColumns, findDataColumnByField, formatValueForExport, getExceptionMessage, SDKUI_Localizator } from '../../helper';
5
5
  import TMCheckBox from '../editors/TMCheckBox';
6
6
  import TMButton from './TMButton';
7
7
  import TMModal from './TMModal';
@@ -96,43 +96,52 @@ const TMDataGridExportForm = (props) => {
96
96
  if (mapForField) {
97
97
  result = mapForField.get(value) ?? value;
98
98
  }
99
- else {
100
- result = value;
101
- }
102
- }
103
- // If the column is a datetime type, attempt to format it as a locale date string
104
- if (col.dataType === 'datetime' && result) {
105
- const parsedDate = new Date(result);
106
- if (!isNaN(parsedDate.getTime())) {
107
- result = parsedDate.toLocaleDateString();
108
- }
109
99
  }
110
- // Return the cleaned string value, stripping double quotes to avoid CSV formatting issues
111
- return (result ?? '').toString().replace(/"/g, '');
100
+ // Find column and format value
101
+ const dataCol = findDataColumnByField(columns, col.dataField, col.caption);
102
+ return formatValueForExport(result, dataCol, col.dataType);
112
103
  };
113
104
  switch (formatSelected) {
114
105
  case 'csv': {
115
- const getHeaderName = (col) => col.caption ?? col.dataField;
116
- const headers = !exportSelectedFormatColumns ? visibleColumns.map(getHeaderName) : [SDKUI_Localizator.SelectedSingular, ...visibleColumns.map(getHeaderName)];
117
- const csvRows = [];
118
- csvRows.push(headers.join(';'));
106
+ // Escape dei valori CSV per gestire separatori, virgolette e ritorni a capo
107
+ const escapeCsv = (v) => { const s = String(v ?? ''); return /[;"\r\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; };
108
+ // Recupera il nome della colonna e lo prepara per il formato CSV
109
+ const getHeader = (c) => escapeCsv(c.caption ?? c.dataField);
110
+ // Crea la prima riga del CSV con le intestazioni delle colonne
111
+ const rows = [
112
+ (exportSelectedFormatColumns
113
+ ? [SDKUI_Localizator.SelectedSingular, ...visibleColumns.map(getHeader)]
114
+ : visibleColumns.map(getHeader)).join(';')
115
+ ];
116
+ // Scorre tutte le righe da esportare
119
117
  rowsToExport.forEach((item, idx) => {
118
+ // Recupera l'indice originale della riga per verificare la selezione
120
119
  const originalIndex = exportSelectedOnly ? selectedRowKeys[idx] : dataSource.indexOf(item);
121
- const rowValues = [];
122
- if (exportSelectedFormatColumns) {
123
- const status = selectedSet.has(originalIndex) ? SDKUI_Localizator.SelectedSingular : SDKUI_Localizator.Deselected;
124
- rowValues.push(status || '');
125
- }
126
- visibleColumns.forEach(col => {
127
- if (col.dataField) {
128
- rowValues.push(getValue(col, item[col.dataField]));
129
- }
130
- });
131
- csvRows.push(rowValues.join(';'));
120
+ // Costruisce i valori della riga corrente
121
+ const values = [
122
+ // Aggiunge lo stato selezionato/deselezionato se richiesto
123
+ ...(exportSelectedFormatColumns
124
+ ? [selectedSet.has(originalIndex) ? SDKUI_Localizator.SelectedSingular : SDKUI_Localizator.Deselected]
125
+ : []),
126
+ // Aggiunge i valori delle colonne applicando l'escape CSV
127
+ ...visibleColumns.map(col => escapeCsv(col.dataField ? getValue(col, item[col.dataField]) : ''))
128
+ ];
129
+ // Aggiunge la riga completa al contenuto CSV
130
+ rows.push(values.join(';'));
132
131
  });
133
- const blob = new Blob([csvRows.join('\r\n')], { type: 'text/csv;charset=utf-8;' });
132
+ // Crea il file CSV aggiungendo il BOM UTF-8 per la corretta lettura degli accenti in Excel
133
+ const blob = new Blob(['\uFEFF', rows.join('\r\n')], { type: 'text/csv;charset=utf-8;' });
134
+ // Avvia il download del file generato
134
135
  downloadFile(blob, formatSelected);
135
- result.push({ rowIndex: 1, id1: 1, id2: 0, description: SDKUI_Localizator.OperationSuccess, resultType: ResultTypes.SUCCESS });
136
+ // Registra il risultato positivo dell'operazione di esportazione
137
+ result.push({
138
+ rowIndex: 1,
139
+ id1: 1,
140
+ id2: 0,
141
+ description: SDKUI_Localizator.OperationSuccess,
142
+ resultType: ResultTypes.SUCCESS
143
+ });
144
+ // Termina il caso CSV nello switch
136
145
  break;
137
146
  }
138
147
  case 'xlsx': {
@@ -101,7 +101,7 @@ const TMModal = ({ resizable = true, expandable = false, isModal = true, title =
101
101
  setShowPopup(false);
102
102
  onClose && onClose();
103
103
  };
104
- return (_jsx(_Fragment, { children: isModal ? (_jsx(Popup, { ref: popupRef, showCloseButton: showHeader ? showCloseButton : false, showTitle: showHeader, animation: undefined, minWidth: minWidth, minHeight: minHeight, maxHeight: '95%', maxWidth: '95%', dragEnabled: !isResizing, resizeEnabled: resizable, width: expandable && isFullScreen ? '95%' : initialWidth, height: expandable && isFullScreen ? '95%' : initialHeight, title: title, visible: showPopup, onShown: handleShown, onResizeStart: handleResizeStart, onResizeEnd: handleResizeEnd, onHiding: onHiding, toolbarItems: showHeader && expandable ? [
104
+ return (_jsx(_Fragment, { children: isModal ? (_jsx(Popup, { ref: popupRef, showCloseButton: showHeader ? showCloseButton : false, showTitle: showHeader, animation: undefined, minWidth: minWidth, minHeight: minHeight, maxHeight: '95%', maxWidth: '95%', dragEnabled: !isResizing && !isFullScreen, resizeEnabled: resizable && !isFullScreen, position: expandable && isFullScreen ? { my: 'center', at: 'center', of: window } : undefined, width: expandable && isFullScreen ? '95%' : initialWidth, height: expandable && isFullScreen ? '95%' : initialHeight, title: title, visible: showPopup, onShown: handleShown, onResizeStart: handleResizeStart, onResizeEnd: handleResizeEnd, onHiding: onHiding, toolbarItems: showHeader && expandable ? [
105
105
  {
106
106
  widget: 'dxButton',
107
107
  location: 'after',
@@ -38,5 +38,7 @@ interface ITMDynDataListItemChooserFormProps extends ITMChooserFormProps<DataLis
38
38
  layoutMode?: LayoutModes;
39
39
  dynDL?: DynamicDataListDescriptor;
40
40
  searchResult?: SearchResultDescriptor;
41
+ /** Se presente, sono i parametri da passare alla query per recuperare il dataSource */
42
+ queryParamsDynDataList?: string[];
41
43
  }
42
44
  export declare const TMDynDataListItemChooserForm: (props: ITMDynDataListItemChooserFormProps) => import("react/jsx-runtime").JSX.Element;
@@ -122,7 +122,7 @@ const TMDynDataListItemChooser = ({ tid, md, width = '100%', titleForm, openChoo
122
122
  updateIsModalOpen?.(true);
123
123
  }
124
124
  }, elementStyle: elementStyle, isModifiedWhen: isModifiedWhen, openEditorOnSummaryClick: openChooserBySingleClick, label: label, template: renderTemplate(), onClearClick: showClearButton ? () => { onValueChanged?.([]); } : undefined, validationItems: validationItems }), showChooser &&
125
- _jsx(TMDynDataListItemChooserForm, { TID: tid, MID: md?.id, dynDL: dynDl, title: titleForm, allowMultipleSelection: allowMultipleSelection, searchResult: dataSource, selectedIDs: values, onClose: () => {
125
+ _jsx(TMDynDataListItemChooserForm, { TID: tid, MID: md?.id, dynDL: dynDl, title: titleForm, allowMultipleSelection: allowMultipleSelection, searchResult: dataSource, selectedIDs: values, queryParamsDynDataList: queryParamsDynDataList, onClose: () => {
126
126
  setShowChooser(false);
127
127
  updateIsModalOpen?.(false);
128
128
  summaryInputRef.current?.focus();
@@ -167,7 +167,7 @@ const TMDynDataListItemChooser = ({ tid, md, width = '100%', titleForm, openChoo
167
167
  export default TMDynDataListItemChooser;
168
168
  const cellRenderIcon = () => _jsx(IconDetails, {});
169
169
  export const TMDynDataListItemChooserForm = (props) => {
170
- const { TID, MID, layoutMode, dynDL, searchResult, selectedIDs, title, width, height, onChoose } = props;
170
+ const { TID, MID, layoutMode, dynDL, searchResult, selectedIDs, title, width, height, onChoose, queryParamsDynDataList } = props;
171
171
  // Generate unique keys for all columns
172
172
  const uniqueKeys = generateUniqueColumnKeys(searchResult?.dtdResult?.columns, searchResult?.fromTID);
173
173
  const dataColumns = searchResult?.dtdResult?.columns?.map((col, index) => {
@@ -184,12 +184,10 @@ export const TMDynDataListItemChooserForm = (props) => {
184
184
  });
185
185
  const keyValue = uniqueKeys[dynDL?.selectItemForValue ?? 0] ?? '';
186
186
  const getItems = async (refreshCache) => {
187
- if (!searchResult)
188
- return [];
189
187
  if (refreshCache)
190
188
  DataListCacheService.RemoveAll();
191
189
  TMSpinner.show({ description: `${SDKUI_Localizator.Loading} - ${SDK_Localizator.DataList} ...` });
192
- let result = await SDK_Globals.tmSession?.NewSearchEngine().GetDynDataListValuesAsync(TID, MID, layoutMode, [])
190
+ let result = await SDK_Globals.tmSession?.NewSearchEngine().GetDynDataListValuesAsync(TID, MID, layoutMode, queryParamsDynDataList ?? [])
193
191
  .catch((err) => { TMSpinner.hide(); TMExceptionBoxManager.show({ exception: err }); });
194
192
  TMSpinner.hide();
195
193
  return result ? searchResultDescriptorToSimpleArray(result) ?? [] : [];
@@ -200,5 +198,5 @@ export const TMDynDataListItemChooserForm = (props) => {
200
198
  titleDataList += `: ${title}`;
201
199
  return titleDataList;
202
200
  };
203
- return (_jsx(TMChooserForm, { title: getTitle(), allowMultipleSelection: props.allowMultipleSelection, width: width, height: height, keyName: keyValue ?? '', showDefaultColumns: false, hasShowId: false, columns: dataColumns, selectedIDs: selectedIDs, cellRenderIcon: cellRenderIcon, dataSource: searchResultDescriptorToSimpleArray(searchResult) ?? [], getItems: getItems, onClose: props.onClose, onChoose: (IDs) => onChoose?.(IDs) }));
201
+ return (_jsx(TMChooserForm, { title: getTitle(), allowMultipleSelection: props.allowMultipleSelection, width: width, height: height, keyName: keyValue ?? '', showDefaultColumns: false, hasShowId: false, columns: dataColumns, selectedIDs: selectedIDs, cellRenderIcon: cellRenderIcon, getItems: getItems, onClose: props.onClose, onChoose: (IDs) => onChoose?.(IDs) }));
204
202
  };
@@ -1,17 +1,32 @@
1
1
  import React from 'react';
2
- import { ValidationItem } from '@topconsultnpm/sdk-ts';
2
+ import { MetadataFormats, ValidationItem } from '@topconsultnpm/sdk-ts';
3
3
  import { IDateBoxOptions } from 'devextreme-react/date-box';
4
4
  import { DateDisplayTypes } from '../../helper';
5
5
  interface ITMDateBoxProps extends IDateBoxOptions {
6
+ /** Tipo di picker da mostrare: Date (solo calendario), Time (solo ora), DateTime (entrambi). Determina quali controlli appaiono nel dropdown. */
6
7
  dateDisplayType?: DateDisplayTypes;
8
+ /** Lista di errori/warning di validazione da mostrare sotto il campo */
7
9
  validationItems?: ValidationItem[];
10
+ /** Se true, evidenzia il campo come modificato (bordo e testo colorati) */
8
11
  isModifiedWhen?: boolean;
12
+ /** Se true, serializza la data nel formato ISO 'yyyy-MM-ddTHH:mm:ss' */
9
13
  useDateSerializationFormat?: boolean;
14
+ /** Elemento DOM contenitore per il dropdown del calendario */
10
15
  containerElement?: Element;
16
+ /** Pulsanti aggiuntivi da mostrare nel campo */
11
17
  buttons?: any;
18
+ /** Se true (default), resetta l'ora a 00:00:00 quando si preme un tasto */
12
19
  resetTimeToZeroOnKeyPress?: boolean;
20
+ /** Icona da mostrare a sinistra del campo */
13
21
  icon?: any;
22
+ /** Padding del contenitore esterno */
14
23
  padding?: string;
24
+ /** Cultura/locale per la formattazione (es. 'en-gb', 'it-it'). Usata per tradurre i nomi dei mesi/giorni nella lingua corretta. */
25
+ formatCulture?: string;
26
+ /** Formato di visualizzazione della data (es. ShortDate='17/07/2026', LongDate='17 July 2026'). Determina come la data viene rappresentata come stringa nel campo. */
27
+ metadataFormat?: MetadataFormats;
28
+ /** Messaggio personalizzato per data non valida. Se non specificato, genera automaticamente un messaggio con esempio del formato (es. 'Formato richiesto: 20/07/2026') */
29
+ invalidDateMessage?: string;
15
30
  }
16
31
  declare const TMDateBox: React.FC<ITMDateBoxProps>;
17
32
  export default TMDateBox;
@@ -1,35 +1,103 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useId, useRef } from 'react';
2
+ import { useEffect, useId, useMemo, useRef } from 'react';
3
3
  import { DateBox } from 'devextreme-react';
4
- import { DateDisplayTypes, Globalization } from '../../helper';
4
+ import { MetadataFormats } from '@topconsultnpm/sdk-ts';
5
+ import { DateDisplayTypes, Globalization, SDKUI_Localizator } from '../../helper';
5
6
  import { TMColors } from '../../utils/theme';
6
7
  import TMVilViewer from '../base/TMVilViewer';
8
+ import { formatDateTimeByMetadataFormat } from '../../helper/TMUtils';
7
9
  const TMDateBox = (props) => {
8
- const resetTimeToZero = props.resetTimeToZeroOnKeyPress ?? true;
10
+ const { dateDisplayType, validationItems, isModifiedWhen, useDateSerializationFormat, containerElement, resetTimeToZeroOnKeyPress, icon, padding, formatCulture, metadataFormat, invalidDateMessage, readOnly, showClearButton, disabled, displayFormat, label, type, value, width, onValueChange, onInitialized, onContentReady, placeholder, name, inputAttr } = props;
11
+ const resetTimeToZero = resetTimeToZeroOnKeyPress ?? true;
9
12
  const autoId = useId();
10
- const effectiveInputAttr = { id: autoId, ...props.inputAttr };
13
+ const effectiveInputAttr = { id: autoId, ...inputAttr };
14
+ // Determina se il formato richiede nomi testuali (mesi/giorni) che dipendono dalla cultura
15
+ const requiresTextualFormat = useMemo(() => {
16
+ // Solo i formati con "Long" nella data hanno nomi di mesi/giorni che variano per cultura
17
+ return metadataFormat === MetadataFormats.LongDate ||
18
+ metadataFormat === MetadataFormats.LongDateShortTime ||
19
+ metadataFormat === MetadataFormats.LongDateLongTime;
20
+ }, [metadataFormat]);
21
+ // Funzione di formattazione personalizzata per rispettare formatCulture
22
+ // Usata SOLO per formati con nomi testuali (LongDate, LongDateShortTime, LongDateLongTime) e se formatCulture è specificato
23
+ const customDisplayFormat = useMemo(() => {
24
+ // Usa funzione custom solo se c'è formatCulture E il formato richiede nomi testuali
25
+ if (!formatCulture || !requiresTextualFormat)
26
+ return undefined;
27
+ // Restituisce una funzione che DevExtreme userà per formattare la data
28
+ return (date) => {
29
+ try {
30
+ if (!date)
31
+ return '';
32
+ // Assicurati che sia un oggetto Date valido
33
+ const dateObj = date instanceof Date ? date : new Date(date);
34
+ if (isNaN(dateObj.getTime()))
35
+ return '';
36
+ return formatDateTimeByMetadataFormat(dateObj, metadataFormat, formatCulture);
37
+ }
38
+ catch {
39
+ // Fallback: restituisce stringa ISO o vuota in caso di errore
40
+ try {
41
+ return date instanceof Date ? date.toISOString() : String(date ?? '');
42
+ }
43
+ catch {
44
+ return '';
45
+ }
46
+ }
47
+ };
48
+ }, [formatCulture, metadataFormat, requiresTextualFormat]);
49
+ // Genera il messaggio di errore con esempio del formato supportato
50
+ const computedInvalidDateMessage = useMemo(() => {
51
+ try {
52
+ if (invalidDateMessage)
53
+ return invalidDateMessage;
54
+ // Genera un esempio usando la data corrente
55
+ const exampleDate = new Date();
56
+ let exampleFormatted;
57
+ if (customDisplayFormat) {
58
+ // Usa la funzione custom se disponibile
59
+ exampleFormatted = customDisplayFormat(exampleDate);
60
+ }
61
+ else if (typeof displayFormat === 'string') {
62
+ // Usa formatDateTimeByMetadataFormat se abbiamo un formato stringa
63
+ exampleFormatted = formatDateTimeByMetadataFormat(exampleDate, metadataFormat, formatCulture) || displayFormat;
64
+ }
65
+ else {
66
+ // Fallback: usa il formato di Globalization o localizzazione standard
67
+ const globFormat = Globalization.getDateDisplayFormat(dateDisplayType);
68
+ exampleFormatted = typeof globFormat === 'string' ? globFormat : exampleDate.toLocaleDateString(formatCulture || 'it-IT');
69
+ }
70
+ if (!exampleFormatted)
71
+ return SDKUI_Localizator.ValueMustBeDateOrTime;
72
+ return `${SDKUI_Localizator.RequiredFormat}: ${exampleFormatted}`;
73
+ }
74
+ catch {
75
+ // In caso di errore, usa il messaggio generico tradotto
76
+ return SDKUI_Localizator.ValueMustBeDateOrTime;
77
+ }
78
+ }, [invalidDateMessage, customDisplayFormat, displayFormat, dateDisplayType, metadataFormat, formatCulture]);
11
79
  useEffect(() => {
12
80
  let htmlElement = dateBoxRef?.current?.instance().element();
13
81
  if (!htmlElement)
14
82
  return;
15
- let borderColor = props.isModifiedWhen ? TMColors.tertiary : TMColors.border_normal;
16
- let textColor = props.isModifiedWhen ? TMColors.isModified : TMColors.text_normal;
83
+ let borderColor = isModifiedWhen ? TMColors.tertiary : TMColors.border_normal;
84
+ let textColor = isModifiedWhen ? TMColors.isModified : TMColors.text_normal;
17
85
  let inputContainer = htmlElement.getElementsByClassName("dx-texteditor-input-container")[0];
18
86
  if (inputContainer) {
19
- inputContainer.style.background = props.readOnly ? 'linear-gradient(white 20%, #d6d6d6 80%)' : '';
20
- inputContainer.style.color = props.readOnly ? '#525252' : textColor;
87
+ inputContainer.style.background = readOnly ? 'linear-gradient(white 20%, #d6d6d6 80%)' : '';
88
+ inputContainer.style.color = readOnly ? '#525252' : textColor;
21
89
  }
22
90
  let input = htmlElement.querySelector(".dx-texteditor-input");
23
91
  if (input) {
24
- if (!props.readOnly) {
92
+ if (!readOnly) {
25
93
  input.style.color = textColor;
26
94
  }
27
95
  input.style.fontSize = 'var(--base-font-size)';
28
96
  input.style.fontFamily = 'var(--base-font-family)';
29
97
  }
30
- let label = htmlElement.getElementsByClassName("dx-label")[0];
31
- if (label) {
32
- label.style.borderBottomColor = borderColor;
98
+ let labelEl = htmlElement.getElementsByClassName("dx-label")[0];
99
+ if (labelEl) {
100
+ labelEl.style.borderBottomColor = borderColor;
33
101
  }
34
102
  let labelBefore = htmlElement.getElementsByClassName("dx-label-before")[0];
35
103
  if (labelBefore) {
@@ -43,13 +111,13 @@ const TMDateBox = (props) => {
43
111
  labelAfter.style.borderTopColor = borderColor;
44
112
  labelAfter.style.borderInlineEndColor = borderColor;
45
113
  }
46
- }, [props.isModifiedWhen, props.readOnly]);
114
+ }, [isModifiedWhen, readOnly]);
47
115
  const dateBoxRef = useRef(null);
48
116
  // soluzione tratta da https://supportcenter.devexpress.com/ticket/details/t1025617/setting-default-time-in-datebox
49
117
  const dropDownOptions = {
50
- container: props.containerElement,
118
+ container: containerElement,
51
119
  onShown: (e) => {
52
- if (props.value != undefined)
120
+ if (value != undefined)
53
121
  return;
54
122
  let dateBoxInstance = dateBoxRef.current?.instance();
55
123
  const currentDate = new Date();
@@ -59,13 +127,13 @@ const TMDateBox = (props) => {
59
127
  }
60
128
  };
61
129
  const getType = () => {
62
- if (props.type != undefined)
63
- return props.type;
64
- if (!props.dateDisplayType || props.dateDisplayType == DateDisplayTypes.DateTime)
130
+ if (type != undefined)
131
+ return type;
132
+ if (!dateDisplayType || dateDisplayType == DateDisplayTypes.DateTime)
65
133
  return "datetime";
66
- return props.dateDisplayType == DateDisplayTypes.Date ? "date" : "time";
134
+ return dateDisplayType == DateDisplayTypes.Date ? "date" : "time";
67
135
  };
68
- return (_jsxs("div", { style: { display: 'flex', alignItems: 'center', width: '100%', padding: props.padding }, children: [props.icon && (_jsx("span", { style: { marginRight: '8px', marginTop: '8px', display: 'flex', alignItems: 'center' }, children: props.icon })), _jsxs("div", { onContextMenu: (e) => e.stopPropagation(), style: { display: 'flex', flexDirection: 'column', gap: '5px', width: '100%' }, children: [_jsx(DateBox, { readOnly: props.readOnly, ref: dateBoxRef, showClearButton: props.showClearButton, dateSerializationFormat: props.useDateSerializationFormat ? 'yyyy-MM-ddTHH:mm:ss' : undefined, disabled: props.disabled, displayFormat: props.displayFormat ?? Globalization.getDateDisplayFormat(props.dateDisplayType), dropDownOptions: dropDownOptions, label: props.label, labelMode: 'static', type: getType(), useMaskBehavior: true, height: '28px', value: props.value, width: props.width, valueChangeEvent: 'keyup input change', onValueChange: (e) => { props.onValueChange?.(e); }, onInitialized: (e) => { props.onInitialized?.(e); }, onContentReady: (e) => { props.onContentReady?.(e); }, placeholder: props.placeholder, ...(props.name && { name: props.name }), inputAttr: effectiveInputAttr, onKeyUp: (e) => {
136
+ return (_jsxs("div", { style: { display: 'flex', alignItems: 'center', width: '100%', padding: padding }, children: [icon && (_jsx("span", { style: { marginRight: '8px', marginTop: '8px', display: 'flex', alignItems: 'center' }, children: icon })), _jsxs("div", { onContextMenu: (e) => e.stopPropagation(), style: { display: 'flex', flexDirection: 'column', gap: '5px', width: '100%' }, children: [_jsx(DateBox, { readOnly: readOnly, ref: dateBoxRef, showClearButton: showClearButton, dateSerializationFormat: useDateSerializationFormat ? 'yyyy-MM-ddTHH:mm:ss' : undefined, disabled: disabled, displayFormat: customDisplayFormat ?? displayFormat ?? Globalization.getDateDisplayFormat(dateDisplayType), dropDownOptions: dropDownOptions, invalidDateMessage: computedInvalidDateMessage, label: label, labelMode: 'static', type: getType(), useMaskBehavior: !customDisplayFormat, height: '28px', value: value, width: width, valueChangeEvent: 'keyup input change', onValueChange: (e) => { onValueChange?.(e); }, onInitialized: (e) => { onInitialized?.(e); }, onContentReady: (e) => { onContentReady?.(e); }, placeholder: placeholder, ...(name && { name: name }), inputAttr: effectiveInputAttr, onKeyUp: (e) => {
69
137
  if (e.event?.code == "Space") {
70
138
  const currentDate = new Date();
71
139
  currentDate.setHours(0, 0, 0, 0);
@@ -73,7 +141,7 @@ const TMDateBox = (props) => {
73
141
  }
74
142
  }, onValueChanged: (e) => {
75
143
  if (resetTimeToZero) {
76
- if (props.value != undefined)
144
+ if (value != undefined)
77
145
  return;
78
146
  if (!e.value)
79
147
  return;
@@ -81,6 +149,6 @@ const TMDateBox = (props) => {
81
149
  currentDate.setHours(0, 0, 0, 0);
82
150
  e.component.option("value", currentDate.toISOString());
83
151
  }
84
- } }), _jsx(TMVilViewer, { vil: props.validationItems })] })] }));
152
+ } }), _jsx(TMVilViewer, { vil: validationItems })] })] }));
85
153
  };
86
154
  export default TMDateBox;
@@ -1,9 +1,13 @@
1
1
  import React from 'react';
2
- import { ValidationItem, LayoutModes, QueryOperators } from '@topconsultnpm/sdk-ts';
2
+ import { MetadataDescriptor, ValidationItem, LayoutModes, QueryOperators } from '@topconsultnpm/sdk-ts';
3
3
  import { DynDataListsToBeRefreshed, MIDsToBeUpdated } from '../choosers/TMDynDataListItemChooser';
4
4
  interface ITMMetadataEditorProps {
5
5
  tid: number | undefined;
6
6
  mid?: number;
7
+ /** MetadataDescriptor esplicito. Se valorizzato, l'editor lo usa direttamente
8
+ * e salta la risoluzione interna via tid/mid (utile per metadati non legati
9
+ * a un tipo documento, es. copertina CaseFlow). */
10
+ md?: MetadataDescriptor;
7
11
  value: string | undefined;
8
12
  queryParamsDynDataList?: string[];
9
13
  autoFocus?: boolean;