@topconsultnpm/sdkui-react 6.20.0-dev1.3 → 6.20.0-dev1.31

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 (65) hide show
  1. package/lib/components/NewComponents/ContextMenu/TMContextMenu.js +29 -11
  2. package/lib/components/NewComponents/ContextMenu/hooks.d.ts +1 -0
  3. package/lib/components/NewComponents/ContextMenu/hooks.js +8 -4
  4. package/lib/components/NewComponents/ContextMenu/styles.d.ts +5 -1
  5. package/lib/components/NewComponents/ContextMenu/styles.js +56 -28
  6. package/lib/components/NewComponents/ContextMenu/types.d.ts +10 -0
  7. package/lib/components/NewComponents/FloatingMenuBar/TMFloatingMenuBar.js +39 -51
  8. package/lib/components/NewComponents/FloatingMenuBar/styles.d.ts +8 -0
  9. package/lib/components/NewComponents/FloatingMenuBar/styles.js +29 -19
  10. package/lib/components/NewComponents/FloatingMenuBar/types.d.ts +0 -1
  11. package/lib/components/base/TMAccordion.js +2 -2
  12. package/lib/components/base/TMCustomButton.js +61 -17
  13. package/lib/components/base/TMDataGrid.d.ts +5 -2
  14. package/lib/components/base/TMDataGrid.js +98 -7
  15. package/lib/components/editors/TMHtmlEditor.js +1 -1
  16. package/lib/components/editors/TMMetadataValues.js +20 -2
  17. package/lib/components/features/documents/TMDcmtBlog.d.ts +1 -7
  18. package/lib/components/features/documents/TMDcmtBlog.js +29 -2
  19. package/lib/components/features/documents/TMDcmtForm.js +268 -168
  20. package/lib/components/features/documents/TMDcmtPreview.js +37 -66
  21. package/lib/components/features/search/TMDcmtCheckoutInfoForm.d.ts +8 -0
  22. package/lib/components/features/search/{TMSearchResultCheckoutInfoForm.js → TMDcmtCheckoutInfoForm.js} +6 -11
  23. package/lib/components/features/search/TMSearchQueryPanel.js +13 -12
  24. package/lib/components/features/search/TMSearchResult.js +66 -117
  25. package/lib/components/features/search/TMSearchResultsMenuItems.d.ts +3 -3
  26. package/lib/components/features/search/TMSearchResultsMenuItems.js +163 -180
  27. package/lib/components/features/search/TMSignatureInfoContent.d.ts +6 -0
  28. package/lib/components/features/search/TMSignatureInfoContent.js +140 -0
  29. package/lib/components/forms/Login/LoginValidatorService.d.ts +2 -0
  30. package/lib/components/forms/Login/LoginValidatorService.js +7 -2
  31. package/lib/components/forms/Login/TMLoginForm.js +34 -6
  32. package/lib/css/tm-sdkui.css +1 -1
  33. package/lib/helper/SDKUI_Globals.d.ts +12 -14
  34. package/lib/helper/SDKUI_Globals.js +8 -0
  35. package/lib/helper/SDKUI_Localizator.d.ts +8 -0
  36. package/lib/helper/SDKUI_Localizator.js +98 -0
  37. package/lib/helper/TMPdfViewer.d.ts +8 -0
  38. package/lib/helper/TMPdfViewer.js +187 -0
  39. package/lib/helper/TMUtils.d.ts +3 -1
  40. package/lib/helper/TMUtils.js +51 -0
  41. package/lib/helper/checkinCheckoutManager.d.ts +85 -0
  42. package/lib/helper/checkinCheckoutManager.js +348 -0
  43. package/lib/helper/devextremeCustomMessages.d.ts +30 -0
  44. package/lib/helper/devextremeCustomMessages.js +30 -0
  45. package/lib/helper/helpers.js +7 -1
  46. package/lib/helper/index.d.ts +2 -0
  47. package/lib/helper/index.js +2 -0
  48. package/lib/helper/queryHelper.js +29 -0
  49. package/lib/hooks/useCheckInOutOperations.d.ts +28 -0
  50. package/lib/hooks/useCheckInOutOperations.js +223 -0
  51. package/lib/services/platform_services.d.ts +1 -1
  52. package/package.json +5 -2
  53. package/lib/components/NewComponents/Notification/Notification.d.ts +0 -4
  54. package/lib/components/NewComponents/Notification/Notification.js +0 -60
  55. package/lib/components/NewComponents/Notification/NotificationContainer.d.ts +0 -8
  56. package/lib/components/NewComponents/Notification/NotificationContainer.js +0 -33
  57. package/lib/components/NewComponents/Notification/index.d.ts +0 -2
  58. package/lib/components/NewComponents/Notification/index.js +0 -2
  59. package/lib/components/NewComponents/Notification/styles.d.ts +0 -21
  60. package/lib/components/NewComponents/Notification/styles.js +0 -180
  61. package/lib/components/NewComponents/Notification/types.d.ts +0 -18
  62. package/lib/components/NewComponents/Notification/types.js +0 -1
  63. package/lib/components/features/search/TMSearchResultCheckoutInfoForm.d.ts +0 -8
  64. package/lib/helper/cicoHelper.d.ts +0 -31
  65. package/lib/helper/cicoHelper.js +0 -155
@@ -55,7 +55,7 @@ export const FloatingContainer = styled.div.attrs(props => ({
55
55
  transition: none;
56
56
 
57
57
  &:hover {
58
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
58
+ background: linear-gradient(135deg, #0071BC 0%, #1B1464 100%);
59
59
  border: 1px solid #667eea;
60
60
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3),
61
61
  0 6px 16px rgba(0, 0, 0, 0.2);
@@ -98,6 +98,13 @@ export const GripHandle = styled.div `
98
98
  height: 14px;
99
99
  }
100
100
  `;
101
+ export const Separator = styled.div `
102
+ background: rgba(255, 255, 255, 0.25);
103
+ width: ${props => props.$orientation === 'horizontal' ? '1px' : '100%'};
104
+ height: ${props => props.$orientation === 'horizontal' ? '24px' : '1px'};
105
+ margin: ${props => props.$orientation === 'horizontal' ? '0 4px' : '4px 0'};
106
+ flex-shrink: 0;
107
+ `;
101
108
  export const MenuButton = styled.button `
102
109
  display: flex;
103
110
  align-items: center;
@@ -110,23 +117,21 @@ export const MenuButton = styled.button `
110
117
  color: white;
111
118
  font-size: 16px;
112
119
  cursor: pointer;
113
- transition: all 0.2s ease;
120
+ transition: background 0.2s ease;
114
121
  position: relative;
115
122
 
116
123
  &:hover:not(:disabled) {
117
124
  background: rgba(255, 255, 255, 0.2);
118
- transform: scale(1.1);
119
125
  }
120
126
 
121
127
  &:active:not(:disabled) {
122
- transform: scale(0.95);
128
+ opacity: 0.8;
123
129
  }
124
130
 
125
131
  &:disabled {
126
- opacity: 0.3;
132
+ opacity: 0.5;
127
133
  cursor: not-allowed;
128
- background: rgba(255, 255, 255, 0.05);
129
- color: rgba(255, 255, 255, 0.4);
134
+ color: rgba(255, 255, 255, 0.6);
130
135
  }
131
136
 
132
137
  svg {
@@ -138,8 +143,8 @@ export const ConfigButton = styled.button `
138
143
  display: flex;
139
144
  align-items: center;
140
145
  justify-content: center;
141
- width: 34px;
142
- height: 34px;
146
+ width: 28px;
147
+ height: 28px;
143
148
  background: ${props => props.$isActive
144
149
  ? 'rgba(0, 0, 0, 0.2)'
145
150
  : 'rgba(0, 0, 0, 0.1)'};
@@ -148,9 +153,9 @@ export const ConfigButton = styled.button `
148
153
  : 'rgba(255, 255, 255, 0.15)'};
149
154
  border-radius: 8px;
150
155
  color: white;
151
- font-size: 16px;
156
+ font-size: 14px;
152
157
  cursor: pointer;
153
- transition: all 0.2s ease;
158
+ transition: background 0.2s ease, border-color 0.2s ease;
154
159
  position: relative;
155
160
 
156
161
  &:hover {
@@ -158,16 +163,20 @@ export const ConfigButton = styled.button `
158
163
  ? 'rgba(0, 0, 0, 0.25)'
159
164
  : 'rgba(0, 0, 0, 0.15)'};
160
165
  border-color: rgba(255, 255, 255, 0.35);
161
- transform: scale(1.05);
162
166
  }
163
167
 
164
168
  &:active {
165
- transform: scale(0.95);
169
+ opacity: 0.8;
166
170
  }
167
171
 
168
172
  svg {
169
- width: 20px;
170
- height: 20px;
173
+ width: 16px;
174
+ height: 16px;
175
+ }
176
+ `;
177
+ export const ContextMenuButton = styled(MenuButton) `
178
+ svg {
179
+ transform: translateY(0);
171
180
  }
172
181
  `;
173
182
  export const RemoveButton = styled.button `
@@ -177,25 +186,26 @@ export const RemoveButton = styled.button `
177
186
  width: 20px;
178
187
  height: 20px;
179
188
  background: #ef4444;
180
- border: 2px solid white;
189
+ border: none;
181
190
  border-radius: 50%;
182
191
  color: white;
183
- font-size: 12px;
192
+ font-size: 14px;
184
193
  font-weight: bold;
194
+ line-height: 1;
185
195
  cursor: pointer;
186
196
  display: flex;
187
197
  align-items: center;
188
198
  justify-content: center;
199
+ padding: 0;
189
200
  transition: all 0.2s ease;
190
201
  z-index: 1;
191
202
 
192
203
  &:hover {
193
204
  background: #dc2626;
194
- transform: scale(1.15);
195
205
  }
196
206
 
197
207
  &:active {
198
- transform: scale(0.9);
208
+ background: #b91c1c;
199
209
  }
200
210
  `;
201
211
  export const OrientationToggle = styled.button `
@@ -11,7 +11,6 @@ export interface TMFloatingMenuItem {
11
11
  export interface TMFloatingMenuBarProps {
12
12
  containerRef: React.RefObject<HTMLElement | null>;
13
13
  contextMenuItems?: TMContextMenuItemProps[];
14
- storageKey?: string;
15
14
  isConstrained?: boolean;
16
15
  defaultPosition?: Position;
17
16
  maxItems?: number;
@@ -47,12 +47,12 @@ const StyledGroupTemplate = styled.div `
47
47
  &::after {
48
48
  content: '';
49
49
  display: block;
50
- width: 90%;
50
+ width: calc(100% - 35px);
51
51
  margin: 0 auto;
52
52
  border-bottom: 1px solid #00A99D;
53
53
  margin-top: 8px;
54
54
  position: absolute;
55
- left: 5%;
55
+ left: 35px;
56
56
  bottom: 0;
57
57
  }
58
58
  `;
@@ -4,31 +4,68 @@ import TMModal from './TMModal';
4
4
  import styled from 'styled-components';
5
5
  import { SDKUI_Localizator, TMLayoutWaitingContainer } from '../..';
6
6
  import { getButtonAttributes, getSelectedItem } from '../../helper/dcmtsHelper';
7
+ import { DeviceType, useDeviceType } from './TMDeviceProvider';
7
8
  const IframeContainer = styled.div `
8
9
  display: flex;
9
10
  height: 100%;
10
11
  flex-direction: column;
11
- padding-left: 15px;
12
+ position: relative;
12
13
  `;
13
14
  const StyledIframe = styled.iframe `
14
15
  border: none;
15
16
  flex: 1;
16
17
  `;
18
+ const LoadingOverlay = styled.div `
19
+ position: absolute;
20
+ top: 0;
21
+ left: 0;
22
+ right: 0;
23
+ bottom: 0;
24
+ background-color: rgba(128, 128, 128, 0.3);
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: center;
28
+ z-index: 1000;
29
+
30
+ &::after {
31
+ content: '';
32
+ width: 40px;
33
+ height: 40px;
34
+ border: 4px solid rgba(255, 255, 255, 0.3);
35
+ border-top-color: #fff;
36
+ border-radius: 50%;
37
+ animation: spin 0.8s linear infinite;
38
+ }
39
+
40
+ @keyframes spin {
41
+ to {
42
+ transform: rotate(360deg);
43
+ }
44
+ }
45
+ `;
17
46
  const TMCustomButton = (props) => {
18
47
  const { button, isModal = true, formData, selectedItems, onClose } = props;
19
48
  const { appName: scriptUrl, arguments: args } = button;
49
+ const Device = useDeviceType();
20
50
  const iframeRef = useRef(null);
21
51
  const attributes = useMemo(() => getButtonAttributes(args, formData, selectedItems), [args, formData, selectedItems]);
22
52
  const RunOnce = button.mode === "RunOnce";
23
53
  const [loading, setLoading] = useState(true);
24
54
  const [error, setError] = useState(false);
25
- const selectedItemsCount = selectedItems?.length || 0;
55
+ const itemsToProcess = useMemo(() => selectedItems && selectedItems.length > 0 ? selectedItems : [attributes], [selectedItems, attributes]);
26
56
  // Stati per il wait panel
27
- const [showWaitPanel, setShowWaitPanel] = useState(selectedItemsCount > 0 && !RunOnce);
28
- const [waitPanelText, setWaitPanelText] = useState(SDKUI_Localizator.CustomButtonActions.replaceParams(1, selectedItemsCount));
57
+ const [showWaitPanel, setShowWaitPanel] = useState(itemsToProcess.length > 0 && !RunOnce);
58
+ const [waitPanelText, setWaitPanelText] = useState(SDKUI_Localizator.CustomButtonActions.replaceParams(1, itemsToProcess.length));
29
59
  const [waitPanelValue, setWaitPanelValue] = useState(0);
30
- const [waitPanelMaxValue, setWaitPanelMaxValue] = useState(selectedItemsCount);
60
+ const [waitPanelMaxValue, setWaitPanelMaxValue] = useState(itemsToProcess.length);
31
61
  const [abortController, setAbortController] = useState(undefined);
62
+ // Aggiungi timestamp all'URL per evitare cache
63
+ const iframeUrl = useMemo(() => {
64
+ if (!scriptUrl)
65
+ return '';
66
+ const separator = scriptUrl.includes('?') ? '&' : '?';
67
+ return `${scriptUrl}${separator}t=${Date.now()}`;
68
+ }, [scriptUrl]);
32
69
  const targetOrigin = useMemo(() => {
33
70
  if (!scriptUrl)
34
71
  return '*';
@@ -41,26 +78,35 @@ const TMCustomButton = (props) => {
41
78
  }
42
79
  }, [scriptUrl]);
43
80
  const handleLoad = () => setLoading(false);
81
+ const isMobile = Device === DeviceType.MOBILE;
44
82
  const handleError = () => {
45
83
  setLoading(false);
46
84
  setError(true);
47
85
  };
48
86
  const executeSequentially = async (controller) => {
49
- if (!selectedItems)
87
+ if (!itemsToProcess)
50
88
  return;
51
- for (const [index, item] of selectedItems.entries()) {
89
+ for (const [index, item] of itemsToProcess.entries()) {
52
90
  if (controller.signal.aborted)
53
91
  break;
54
- setWaitPanelText(SDKUI_Localizator.CustomButtonActions.replaceParams(index + 1, selectedItemsCount));
92
+ setWaitPanelText(SDKUI_Localizator.CustomButtonActions.replaceParams(index + 1, itemsToProcess.length));
55
93
  setWaitPanelValue(index);
56
94
  // Attendi che l'iframe sia pronto e invia il messaggio
57
95
  await new Promise((resolve) => {
58
96
  const checkIframe = setInterval(() => {
59
97
  if (iframeRef.current?.contentWindow) {
60
98
  clearInterval(checkIframe);
61
- //devo convertire item in formData
62
- const processedItem = getSelectedItem(args, formData, item);
63
- postMessageIframe(processedItem);
99
+ if (selectedItems && selectedItems.length > 0) {
100
+ //devo convertire item in formData
101
+ const processedItem = getSelectedItem(args, formData, item);
102
+ postMessageIframe(processedItem);
103
+ }
104
+ else {
105
+ postMessageIframe(item);
106
+ }
107
+ //imposta 100% se sono all'ultimo item
108
+ if (index === itemsToProcess.length - 1)
109
+ setWaitPanelValue(index + 1);
64
110
  // Attendi prima di passare al prossimo
65
111
  setTimeout(() => {
66
112
  setWaitPanelValue(index + 1);
@@ -82,8 +128,7 @@ const TMCustomButton = (props) => {
82
128
  useEffect(() => {
83
129
  if (loading || error)
84
130
  return;
85
- //if(error) clearTimeout(timeoutIframe);
86
- if (!RunOnce && selectedItemsCount > 0) {
131
+ if (!RunOnce && itemsToProcess.length > 0) {
87
132
  // esegui per ogni item selezionato
88
133
  const controller = new AbortController();
89
134
  controller.signal.addEventListener('abort', () => {
@@ -91,7 +136,7 @@ const TMCustomButton = (props) => {
91
136
  onClose?.();
92
137
  });
93
138
  setAbortController(controller);
94
- setWaitPanelMaxValue(selectedItemsCount);
139
+ setWaitPanelMaxValue(itemsToProcess.length);
95
140
  executeSequentially(controller);
96
141
  }
97
142
  else {
@@ -103,7 +148,6 @@ const TMCustomButton = (props) => {
103
148
  onClose?.();
104
149
  }, 2000);
105
150
  }
106
- //clearTimeout(timeoutIframe);
107
151
  }
108
152
  }, [loading, error, RunOnce]);
109
153
  useEffect(() => {
@@ -112,7 +156,7 @@ const TMCustomButton = (props) => {
112
156
  onClose?.();
113
157
  }
114
158
  }, []);
115
- const iframeContent = (_jsxs(IframeContainer, { style: !RunOnce ? { visibility: 'hidden' } : {}, children: [error && _jsx("div", { children: "Si \u00E8 verificato un errore nel caricamento del contenuto." }), !error && _jsx(StyledIframe, { ref: iframeRef, loading: 'lazy', onLoad: handleLoad, onError: handleError, src: scriptUrl })] }));
116
- return isModal && RunOnce ? (_jsx(TMModal, { title: button.title, width: '60%', height: '70%', resizable: true, expandable: true, onClose: onClose, children: iframeContent })) : !RunOnce && (_jsxs(_Fragment, { children: [_jsx(TMLayoutWaitingContainer, { showWaitPanel: showWaitPanel, waitPanelTitle: SDKUI_Localizator.CustomButtonAction, showWaitPanelPrimary: true, waitPanelTextPrimary: waitPanelText, waitPanelValuePrimary: waitPanelValue, waitPanelMaxValuePrimary: waitPanelMaxValue, showWaitPanelSecondary: false, isCancelable: true, abortController: abortController, children: undefined }), iframeContent] }));
159
+ const iframeContent = (_jsxs(IframeContainer, { style: !RunOnce ? { visibility: 'hidden' } : {}, children: [loading && _jsx(LoadingOverlay, {}), error && _jsx("div", { children: "Si \u00E8 verificato un errore nel caricamento del contenuto." }), !error && _jsx(StyledIframe, { ref: iframeRef, loading: 'lazy', onLoad: handleLoad, onError: handleError, src: iframeUrl })] }));
160
+ return isModal && RunOnce ? (_jsx(TMModal, { title: button.title, width: isMobile ? '95%' : '60%', height: isMobile ? '95%' : '70%', resizable: isMobile ? false : true, expandable: isMobile ? false : true, onClose: onClose, children: iframeContent })) : !RunOnce && (_jsxs(_Fragment, { children: [_jsx(TMLayoutWaitingContainer, { showWaitPanel: showWaitPanel, waitPanelTitle: SDKUI_Localizator.CustomButtonAction, showWaitPanelPrimary: true, waitPanelTextPrimary: waitPanelText, waitPanelValuePrimary: waitPanelValue, waitPanelMaxValuePrimary: waitPanelMaxValue, showWaitPanelSecondary: false, isCancelable: true, abortController: abortController, children: undefined }), iframeContent] }));
117
161
  };
118
162
  export default TMCustomButton;
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import { IColumnProps, IDataGridOptions, IMasterDetailProps } from 'devextreme-react/data-grid';
3
3
  import dxDataGrid from 'devextreme/ui/data_grid';
4
4
  import { ITMCounterContainerProps } from './TMCounterContainer';
5
+ import { TMContextMenuItemProps } from '../NewComponents/ContextMenu/types';
5
6
  export interface TMDataGridContextMenuItem {
6
7
  text: string;
7
8
  icon: string;
@@ -39,8 +40,8 @@ export interface TMDataGridProps<T> extends IDataGridOptions {
39
40
  showFilterPanel?: boolean;
40
41
  /** Show the load panel */
41
42
  showLoadPanel?: boolean;
42
- /** Show the column chooser */
43
- showColumnChooser?: boolean;
43
+ /** Show the header column chooser in context menu */
44
+ showHeaderColumnChooser?: boolean;
44
45
  /** Show the search panel */
45
46
  showSearchPanel?: boolean;
46
47
  /** Show the group panel */
@@ -51,6 +52,8 @@ export interface TMDataGridProps<T> extends IDataGridOptions {
51
52
  masterDetail?: IMasterDetailProps;
52
53
  /** On Has Filters Change */
53
54
  onHasFiltersChange?: (hasFilters: boolean) => void;
55
+ /** Custom context menu items - when provided, replaces DevExtreme's native context menu with TMContextMenu */
56
+ customContextMenuItems?: TMContextMenuItemProps[];
54
57
  }
55
58
  declare const TMDataGrid: React.ForwardRefExoticComponent<TMDataGridProps<unknown> & React.RefAttributes<dxDataGrid<any, any>>>;
56
59
  export default TMDataGrid;
@@ -1,9 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
3
- import DataGrid, { Column, HeaderFilter, Selection, Scrolling, LoadPanel, SearchPanel, Pager, Sorting, Paging, FilterPanel, ColumnChooser, Grouping, GroupPanel, Summary, Editing, FilterRow, StateStoring, RowDragging, MasterDetail } from 'devextreme-react/data-grid';
3
+ import DataGrid, { Column, HeaderFilter, Selection, Scrolling, LoadPanel, SearchPanel, Pager, Sorting, Paging, FilterPanel, ColumnChooser, Grouping, GroupPanel, Summary, Editing, FilterRow, StateStoring, RowDragging, MasterDetail, Position, ColumnChooserSearch, ColumnChooserSelection } from 'devextreme-react/data-grid';
4
4
  import DataSource from 'devextreme/data/data_source';
5
5
  import { IconAll, IconSelected, IconVisible, SDKUI_Globals, SDKUI_Localizator } from '../../helper';
6
6
  import TMCounterContainer, { CounterItemKey } from './TMCounterContainer';
7
+ import TMContextMenu from '../NewComponents/ContextMenu/TMContextMenu';
7
8
  ;
8
9
  export var TMDataGridPageSize;
9
10
  (function (TMDataGridPageSize) {
@@ -16,9 +17,9 @@ const TMDataGrid = React.forwardRef((props, ref) => {
16
17
  // main properties
17
18
  keyExpr = 'id', dataSource, focusedRowEnabled = true, hoverStateEnabled = true, focusedRowKey, selectedRowKeys = [],
18
19
  // custom options
19
- dataColumns = [], pageSize = TMDataGridPageSize.Large, showHeaderFilter = true, showFilterPanel = true, showLoadPanel = true, showSearchPanel = true, searchPanelToolbarPosition = 'before', searchPanelFocusStarting = false, counterConfig = { show: false, items: new Map() }, onHasFiltersChange,
20
+ dataColumns = [], pageSize = TMDataGridPageSize.Large, showHeaderFilter = true, showFilterPanel = true, showHeaderColumnChooser = false, showLoadPanel = true, showSearchPanel = true, searchPanelToolbarPosition = 'before', searchPanelFocusStarting = false, counterConfig = { show: false, items: new Map() }, onHasFiltersChange, customContextMenuItems,
20
21
  // events and callbacks
21
- onSelectionChanged, onFocusedRowChanged, onRowDblClick, onRowClick, onCellClick, onCellDblClick, onOptionChanged, onContentReady, onContextMenuPreparing, onInitialized, onEditorPreparing, onCellPrepared, onRowPrepared, onRowUpdating, onRowExpanded, onRowCollapsed, onRowUpdated, onSaved, onEditCanceled, onEditingStart, onEditingChange, customizeColumns, onKeyDown, scrolling = { mode: 'standard', useNative: SDKUI_Globals.userSettings?.themeSettings.gridSettings.useNativeScrollbar === 1 }, paging = { enabled: true, pageSize: pageSize }, pager = { visible: true, showInfo: true, showNavigationButtons: true }, selection = { mode: 'multiple', showCheckBoxesMode: "always", selectAllMode: "allPages" }, sorting, summary, stateStoring, columnChooser, grouping, groupPanel, filterRow, headerFilter, editing, rowDragging, masterDetail,
22
+ onSelectionChanged, onFocusedRowChanged, onRowDblClick, onRowClick, onCellClick, onCellDblClick, onOptionChanged, onContentReady, onContextMenuPreparing, onInitialized, onEditorPreparing, onCellPrepared, onRowPrepared, onRowUpdating, onRowExpanded, onRowCollapsed, onRowUpdated, onSaved, onEditCanceled, onEditingStart, onEditingChange, customizeColumns, onKeyDown, scrolling = { mode: 'standard', useNative: SDKUI_Globals.userSettings?.themeSettings.gridSettings.useNativeScrollbar === 1 }, paging = { enabled: true, pageSize: pageSize }, pager = { visible: true, showInfo: true, showNavigationButtons: true }, selection = { mode: 'multiple', showCheckBoxesMode: "always", selectAllMode: "allPages" }, sorting, summary, stateStoring, grouping, groupPanel, filterRow, headerFilter, editing, rowDragging, masterDetail,
22
23
  // other properties
23
24
  disabled = false, autoNavigateToFocusedRow = true, columnResizingMode = 'widget', columnHidingEnabled = true, columnAutoWidth = true, allowColumnResizing = true, allowColumnReordering = true, showBorders = true, showRowLines = SDKUI_Globals.userSettings?.themeSettings.gridSettings.showRowLines === 1, showColumnLines = SDKUI_Globals.userSettings?.themeSettings.gridSettings.showColumnLines === 1, showColumnHeaders = true, rowAlternationEnabled = false, wordWrapEnabled = false, noDataText,
24
25
  // styles
@@ -29,10 +30,42 @@ const TMDataGrid = React.forwardRef((props, ref) => {
29
30
  const [totalRecordCount, setTotalRecordCount] = useState(0);
30
31
  const [visibleItemsCount, setVisibleItemsCount] = useState(0);
31
32
  const [hasFilters, setHasFilters] = useState(false);
33
+ // Custom context menu states
34
+ const [customContextMenuVisible, setCustomContextMenuVisible] = useState(false);
35
+ const [customContextMenuPosition, setCustomContextMenuPosition] = useState({ x: 0, y: 0 });
36
+ const [customContextMenuRowKey, setCustomContextMenuRowKey] = useState(undefined);
37
+ const gridContainerRef = useRef(null);
32
38
  useEffect(() => {
33
39
  const count = getRecordCount(dataSource);
34
40
  setTotalRecordCount(count);
35
41
  }, [dataSource]);
42
+ // Handle custom context menu (only when customContextMenuItems is provided)
43
+ useEffect(() => {
44
+ if (!customContextMenuItems || !gridContainerRef.current)
45
+ return;
46
+ const gridContainer = gridContainerRef.current;
47
+ const handleContextMenu = (e) => {
48
+ e.preventDefault();
49
+ e.stopPropagation();
50
+ // Get the clicked row
51
+ const target = e.target;
52
+ const rowElement = target.closest('.dx-data-row');
53
+ if (rowElement && internalRef.current) {
54
+ const rowIndex = Array.from(rowElement.parentElement?.children || []).indexOf(rowElement);
55
+ const rowKey = internalRef.current.instance().getKeyByRowIndex(rowIndex);
56
+ // Change focused row
57
+ internalRef.current.instance().option('focusedRowKey', rowKey);
58
+ // Show custom context menu
59
+ setCustomContextMenuVisible(true);
60
+ setCustomContextMenuPosition({ x: e.clientX, y: e.clientY });
61
+ setCustomContextMenuRowKey(rowKey);
62
+ }
63
+ };
64
+ gridContainer.addEventListener('contextmenu', handleContextMenu);
65
+ return () => {
66
+ gridContainer.removeEventListener('contextmenu', handleContextMenu);
67
+ };
68
+ }, [customContextMenuItems]);
36
69
  // Creating a ref to store the timestamp of the last selection change
37
70
  const lastSelectionChangeTime = useRef(Date.now());
38
71
  useEffect(() => {
@@ -101,6 +134,8 @@ const TMDataGrid = React.forwardRef((props, ref) => {
101
134
  }
102
135
  return {
103
136
  ...item,
137
+ // Ensure icon is not null/undefined to prevent DevExtreme errors
138
+ icon: item.icon || '',
104
139
  disabled: disabled || disabledCalculation, // An item is disabled if it's explicitly set to `true` or if the calculation above determines so
105
140
  // Define the behavior when the menu item is clicked
106
141
  onClick: () => {
@@ -132,10 +167,51 @@ const TMDataGrid = React.forwardRef((props, ref) => {
132
167
  };
133
168
  });
134
169
  }, [focusedRowEnabled, focusedRowKey, selectedRowKeys]);
170
+ // Process custom context menu items (for TMContextMenuItemProps)
171
+ const processCustomContextMenuItems = useCallback((items, rowID) => {
172
+ return items.map(item => {
173
+ let disabled = item.disabled ?? false;
174
+ let disabledCalculation = false;
175
+ const id = focusedRowEnabled ? focusedRowKey : rowID;
176
+ if (item.operationType === 'singleRow') {
177
+ disabledCalculation = selectedRowKeys.length > 1 || id === undefined;
178
+ }
179
+ if (item.operationType === 'multiRow') {
180
+ disabledCalculation = selectedRowKeys.length === 0 && id === undefined;
181
+ }
182
+ const originalOnClick = item.onClick;
183
+ return {
184
+ ...item,
185
+ disabled: disabled || disabledCalculation,
186
+ onClick: originalOnClick ? () => {
187
+ if (item.operationType === 'singleRow' && id !== undefined) {
188
+ originalOnClick(id);
189
+ }
190
+ else if (item.operationType === 'multiRow' && id !== undefined) {
191
+ if (selectedRowKeys.length > 0) {
192
+ originalOnClick(selectedRowKeys);
193
+ }
194
+ else {
195
+ originalOnClick([id]);
196
+ }
197
+ }
198
+ else {
199
+ originalOnClick();
200
+ }
201
+ } : undefined,
202
+ submenu: item.submenu ? processCustomContextMenuItems(item.submenu, id) : undefined,
203
+ };
204
+ });
205
+ }, [focusedRowEnabled, focusedRowKey, selectedRowKeys]);
135
206
  // Handle context menu preparation
136
207
  const onContextMenuPreparingCallback = useCallback((e) => {
137
208
  if (e === undefined)
138
209
  return;
210
+ // If custom context menu is enabled, completely disable DevExtreme's native context menu
211
+ if (customContextMenuItems && e.target === 'content') {
212
+ e.items = undefined;
213
+ return;
214
+ }
139
215
  if (onContextMenuPreparing)
140
216
  onContextMenuPreparing(e);
141
217
  if (e.target === 'content') {
@@ -146,7 +222,18 @@ const TMDataGrid = React.forwardRef((props, ref) => {
146
222
  e.items = [...updatedContextMenuItems];
147
223
  }
148
224
  }
149
- }, [updateContextMenuItems, onContextMenuPreparing]);
225
+ // Add column chooser to header context menu
226
+ if (e.target === 'header' && showHeaderColumnChooser) {
227
+ e.items = e.items || [];
228
+ e.items.push({
229
+ text: SDKUI_Localizator.ShowColumnSelection,
230
+ icon: 'columnchooser',
231
+ onItemClick: () => {
232
+ internalRef.current?.instance().showColumnChooser();
233
+ }
234
+ });
235
+ }
236
+ }, [updateContextMenuItems, onContextMenuPreparing, showHeaderColumnChooser, customContextMenuItems]);
150
237
  // Handle toolbar preparation, especially for the search panel
151
238
  const onToolbarPreparingCallback = useCallback((e) => {
152
239
  if (e === undefined || e.toolbarOptions === undefined || e.toolbarOptions.items === undefined)
@@ -215,15 +302,19 @@ const TMDataGrid = React.forwardRef((props, ref) => {
215
302
  // Propaga l'evento originale
216
303
  onOptionChanged?.(e);
217
304
  }, [onOptionChanged, onHasFiltersChange]);
218
- return _jsxs("div", { style: { width: "100%", height: "100%" }, children: [_jsx("div", { style: { width: "100%", height: counterConfig.show ? "calc(100% - 25px)" : "100%" }, children: _jsxs(DataGrid, { ref: internalRef, id: id, className: `tm-datagrid ${hasFilters ? 'has-filters' : ''}`,
305
+ return _jsxs("div", { style: { width: "100%", height: "100%" }, children: [_jsx("div", { ref: gridContainerRef, style: { width: "100%", height: counterConfig.show ? "calc(100% - 25px)" : "100%" }, children: _jsxs(DataGrid, { ref: internalRef, id: id, className: `tm-datagrid ${hasFilters ? 'has-filters' : ''}`,
219
306
  // main properties
220
307
  keyExpr: keyExpr, dataSource: dataSource, selectedRowKeys: selectedRowKeys, focusedRowEnabled: focusedRowEnabled, hoverStateEnabled: hoverStateEnabled,
221
308
  // events and callbacks
222
- onSelectionChanged: onSelectionChangedCallback, onRowDblClick: onRowDblClickCallback, onRowPrepared: onRowPrepared, onContextMenuPreparing: onContextMenuPreparingCallback, onToolbarPreparing: onToolbarPreparingCallback, onFocusedRowChanged: onFocusedRowChanged, onRowClick: onRowClick, onCellClick: onCellClick, onCellDblClick: onCellDblClick, onOptionChanged: onOptionChangedCallback, onContentReady: onContentReadyCallback, onInitialized: onInitialized, customizeColumns: customizeColumns, onEditorPreparing: onEditorPreparing, onCellPrepared: onCellPrepared, onRowUpdating: onRowUpdating, onRowExpanded: onRowExpanded, onRowCollapsed: onRowCollapsed, onRowUpdated: onRowUpdated, onSaved: onSaved, onEditCanceled: onEditCanceled, onEditingStart: onEditingStart, onEditingChange: onEditingChange, onKeyDown: onKeyDown,
309
+ onSelectionChanged: onSelectionChangedCallback, onRowDblClick: onRowDblClickCallback, onRowPrepared: onRowPrepared, onContextMenuPreparing: customContextMenuItems ? undefined : onContextMenuPreparingCallback, onToolbarPreparing: onToolbarPreparingCallback, onFocusedRowChanged: onFocusedRowChanged, onRowClick: onRowClick, onCellClick: onCellClick, onCellDblClick: onCellDblClick, onOptionChanged: onOptionChangedCallback, onContentReady: onContentReadyCallback, onInitialized: onInitialized, customizeColumns: customizeColumns, onEditorPreparing: onEditorPreparing, onCellPrepared: onCellPrepared, onRowUpdating: onRowUpdating, onRowExpanded: onRowExpanded, onRowCollapsed: onRowCollapsed, onRowUpdated: onRowUpdated, onSaved: onSaved, onEditCanceled: onEditCanceled, onEditingStart: onEditingStart, onEditingChange: onEditingChange, onKeyDown: onKeyDown,
223
310
  // other properties
224
311
  disabled: disabled, autoNavigateToFocusedRow: autoNavigateToFocusedRow, focusedRowKey: focusedRowKey, columnHidingEnabled: columnHidingEnabled, columnResizingMode: columnResizingMode, columnAutoWidth: columnAutoWidth, allowColumnResizing: allowColumnResizing, allowColumnReordering: allowColumnReordering, showBorders: showBorders, showRowLines: showRowLines, showColumnLines: showColumnLines, showColumnHeaders: showColumnHeaders, rowAlternationEnabled: rowAlternationEnabled, wordWrapEnabled: wordWrapEnabled, noDataText: noDataText,
225
312
  // styles
226
- width: width, height: height, style: { userSelect: 'none' }, children: [dataColumns.map((column, index) => (_jsx(Column, { ...column }, column.caption + index.toString()))), sorting && _jsx(Sorting, { ...sorting }), selection && _jsx(Selection, { ...selection }), scrolling && _jsx(Scrolling, { ...scrolling }), summary && _jsx(Summary, { ...summary }), showHeaderFilter && _jsx(HeaderFilter, { visible: true, ...headerFilter }), rowDragging && _jsx(RowDragging, { ...rowDragging }), filterRow && _jsx(FilterRow, { ...filterRow }), showFilterPanel && _jsx(FilterPanel, { visible: true }), columnChooser && _jsx(ColumnChooser, { ...columnChooser }), stateStoring && _jsx(StateStoring, { ...stateStoring }), groupPanel && _jsx(GroupPanel, { ...groupPanel }), _jsx(Grouping, { contextMenuEnabled: true, ...grouping }), _jsx(LoadPanel, { enabled: showLoadPanel }), _jsx(SearchPanel, { visible: showSearchPanel, searchVisibleColumnsOnly: true, highlightSearchText: true }), editing && _jsx(Editing, { ...editing }), paging && _jsx(Paging, { ...paging }), pager && _jsx(Pager, { ...pager, visible: totalRecordCount > pageSize }), masterDetail && _jsx(MasterDetail, { ...masterDetail })] }) }), counterConfig.show && _jsx("div", { style: { width: "100%", height: "25px", display: "flex", alignItems: "center", gap: "15px", backgroundColor: "#e0e0e0" }, children: _jsx(TMCounterContainer, { items: counterValues, bgColorContainer: counterConfig.bgColorContainer, bgColorItem: counterConfig.bgColorItem, hoverColorItem: counterConfig.hoverColorItem, textColorItem: counterConfig.textColorItem }) })] });
313
+ width: width, height: height, style: { userSelect: 'none' }, children: [dataColumns.map((column, index) => (_jsx(Column, { ...column }, column.caption + index.toString()))), sorting && _jsx(Sorting, { ...sorting }), selection && _jsx(Selection, { ...selection }), scrolling && _jsx(Scrolling, { ...scrolling }), summary && _jsx(Summary, { ...summary }), showHeaderFilter && _jsx(HeaderFilter, { visible: true, ...headerFilter }), rowDragging && _jsx(RowDragging, { ...rowDragging }), filterRow && _jsx(FilterRow, { ...filterRow }), showFilterPanel && _jsx(FilterPanel, { visible: true }), showHeaderColumnChooser && _jsxs(ColumnChooser, { height: "400px", enabled: !showHeaderColumnChooser, mode: "select", children: [_jsx(Position, { my: "center", at: "center", of: window }), _jsx(ColumnChooserSearch, { enabled: true }), _jsx(ColumnChooserSelection, { allowSelectAll: false, selectByClick: true, recursive: true })] }), stateStoring && _jsx(StateStoring, { ...stateStoring }), groupPanel && _jsx(GroupPanel, { ...groupPanel }), _jsx(Grouping, { contextMenuEnabled: true, ...grouping }), _jsx(LoadPanel, { enabled: showLoadPanel }), _jsx(SearchPanel, { visible: showSearchPanel, searchVisibleColumnsOnly: true, highlightSearchText: true }), editing && _jsx(Editing, { ...editing }), paging && _jsx(Paging, { ...paging }), pager && _jsx(Pager, { ...pager, visible: totalRecordCount > pageSize }), masterDetail && _jsx(MasterDetail, { ...masterDetail })] }) }), counterConfig.show && _jsx("div", { style: { width: "100%", height: "25px", display: "flex", alignItems: "center", gap: "15px", backgroundColor: "#e0e0e0" }, children: _jsx(TMCounterContainer, { items: counterValues, bgColorContainer: counterConfig.bgColorContainer, bgColorItem: counterConfig.bgColorItem, hoverColorItem: counterConfig.hoverColorItem, textColorItem: counterConfig.textColorItem }) }), customContextMenuItems && (_jsx(TMContextMenu, { items: processCustomContextMenuItems(customContextMenuItems, customContextMenuRowKey), externalControl: {
314
+ visible: customContextMenuVisible,
315
+ position: customContextMenuPosition,
316
+ onClose: () => setCustomContextMenuVisible(false)
317
+ } }))] });
227
318
  });
228
319
  export default TMDataGrid;
229
320
  const getRecordCount = (dataSource) => {
@@ -170,7 +170,7 @@ const TMHtmlEditor = (props) => {
170
170
  justifyContent: 'flex-end',
171
171
  fontSize: 12,
172
172
  color: '#6c757d',
173
- marginTop: 4,
173
+ marginTop: showInfoIcon ? 0 : 4,
174
174
  gap: 4,
175
175
  }, children: [`${Math.max(charactersRemaining, 0)} ${SDKUI_Localizator.CharactersRemaining}`, showInfoIcon && (_jsx(TMTooltip, { content: 'Markup HTML', children: _jsx("span", { className: "dx-icon-codeblock", style: { fontSize: 22, cursor: 'pointer' }, onClick: () => {
176
176
  TMMessageBoxManager.show({
@@ -389,17 +389,35 @@ const TMMetadataValues = ({ showCheckBoxes = ShowCheckBoxesMode.Never, checkPerm
389
389
  return (_jsxs("div", { style: { width: '100%' }, children: [draftData.length > 0 && _jsx(TMAccordion, { title: SDKUI_Localizator.Draft, children: draftData.map(item => renderMetadataItem(item, isReadOnly)) }), checkOutData.length > 0 && _jsx(TMAccordion, { title: `${SDKUI_Localizator.CheckIn}/${SDKUI_Localizator.CheckOut}`, children: checkOutData.map(item => renderMetadataItem(item, true)) })] }));
390
390
  }, [metadataValues, showCheckBoxes, showNullValueCheckBoxes, isReadOnly, dynDataListsToBeRefreshed, validationItems, selectedMID, isOpenDistinctValues, openChooserBySingleClick, metadataValuesOrig]);
391
391
  const layoutChronology = useMemo(() => {
392
- const chronologyData = [];
392
+ // Definiamo l'ordine desiderato: Version, Tipo, Dimensione, Autore, Data Ultima modifica
393
+ const desiredChronologyOrder = [
394
+ ChronologyMIDs.Ver,
395
+ SystemMIDsAsNumber.FileExt,
396
+ SystemMIDsAsNumber.FileSize,
397
+ ChronologyMIDs.AuthorID,
398
+ ChronologyMIDs.CheckInTime,
399
+ ];
400
+ const tempChronologyDataMap = {};
393
401
  metadataValues.forEach(item => {
394
402
  switch (item.md?.id) {
395
403
  case ChronologyMIDs.Ver:
396
404
  case ChronologyMIDs.AuthorID:
397
- chronologyData.push(item);
405
+ case ChronologyMIDs.CheckInTime:
406
+ case SystemMIDsAsNumber.FileExt:
407
+ case SystemMIDsAsNumber.FileSize:
408
+ tempChronologyDataMap[item.md.id] = item;
398
409
  break;
399
410
  default:
400
411
  break;
401
412
  }
402
413
  });
414
+ // Visualizziamo nell'ordine desiderato
415
+ const chronologyData = [];
416
+ desiredChronologyOrder.forEach(id => {
417
+ if (tempChronologyDataMap[id]) {
418
+ chronologyData.push(tempChronologyDataMap[id]);
419
+ }
420
+ });
403
421
  return (_jsx("div", { style: { width: '100%' }, children: chronologyData.length > 0 && chronologyData.map(item => renderMetadataItem(item, isReadOnly)) }));
404
422
  }, [metadataValues, showCheckBoxes, showNullValueCheckBoxes, isReadOnly, dynDataListsToBeRefreshed, validationItems, selectedMID, isOpenDistinctValues, openChooserBySingleClick, metadataValuesOrig]);
405
423
  const layoutDsAttachs = useMemo(() => {
@@ -1,16 +1,10 @@
1
1
  import React from 'react';
2
2
  import { HomeBlogPost, TaskDescriptor } from '@topconsultnpm/sdk-ts';
3
3
  interface ITMDcmtBlogProps {
4
- blogsDatasource: HomeBlogPost[];
5
- setBlogsDatasource: (posts: HomeBlogPost[]) => void;
6
- hasLoadedDataOnce: boolean;
7
- setHasLoadedDataOnce: (loaded: boolean) => void;
8
- lastLoadedDid: number | undefined;
9
- setLastLoadedDid: (did: number | undefined) => void;
10
4
  tid: number | undefined;
11
5
  did: number | undefined;
12
- fetchBlogDataAsync: (tid: number | undefined, did: number | undefined) => Promise<void>;
13
6
  isVisible?: boolean;
7
+ fetchBlogDataTrigger?: number;
14
8
  allTasks?: Array<TaskDescriptor>;
15
9
  getAllTasks?: () => Promise<void>;
16
10
  deleteTaskByIdsCallback?: (deletedTaskIds: Array<number>) => Promise<void>;
@@ -6,13 +6,40 @@ import { TMNothingToShow } from './TMDcmtPreview';
6
6
  import { IconBoard, SDKUI_Localizator } from '../../../helper';
7
7
  import TMBlogCommentForm from '../blog/TMBlogCommentForm';
8
8
  import TMBlogsPost from '../../grids/TMBlogsPost';
9
- const TMDcmtBlog = ({ blogsDatasource, setBlogsDatasource, hasLoadedDataOnce, setHasLoadedDataOnce, lastLoadedDid, setLastLoadedDid, tid, did, fetchBlogDataAsync, isVisible, allTasks = [], getAllTasks, deleteTaskByIdsCallback, addTaskCallback, editTaskCallback, handleNavigateToWGs, handleNavigateToDossiers }) => {
9
+ import TMSpinner from '../../base/TMSpinner';
10
+ import { TMExceptionBoxManager } from '../../base/TMPopUp';
11
+ const TMDcmtBlog = ({ tid, did, isVisible, fetchBlogDataTrigger, allTasks = [], getAllTasks, deleteTaskByIdsCallback, addTaskCallback, editTaskCallback, handleNavigateToWGs, handleNavigateToDossiers }) => {
12
+ const [blogsDatasource, setBlogsDatasource] = useState([]);
13
+ const [hasLoadedDataOnce, setHasLoadedDataOnce] = useState(false); //traccia se *qualsiasi* dato è stato caricato per la prima volta
14
+ const [lastLoadedDid, setLastLoadedDid] = useState(undefined); // `lastLoadedDid` tiene traccia dell'ultimo `did` per cui abbiamo caricato i dati
10
15
  // State to manage show comment form selected file
11
16
  const [showCommentForm, setShowCommentForm] = useState(false);
12
17
  const [externalBlogPost, setExternalBlogPost] = useState(undefined);
18
+ const fetchBlogDataAsync = useCallback(async (tid, did) => {
19
+ try {
20
+ TMSpinner.show({ description: 'Caricamento - Bacheca...' });
21
+ const res = await SDK_Globals.tmSession?.NewSearchEngine().BlogRetrieveAsync(tid, did);
22
+ setBlogsDatasource(res ?? []);
23
+ setHasLoadedDataOnce(true);
24
+ setLastLoadedDid(did);
25
+ }
26
+ catch (e) {
27
+ let err = e;
28
+ TMExceptionBoxManager.show({ exception: err });
29
+ }
30
+ finally {
31
+ TMSpinner.hide();
32
+ }
33
+ }, []);
13
34
  const showCommentFormCallback = useCallback(() => {
14
35
  setShowCommentForm(true);
15
36
  }, []);
37
+ // useEffect per triggerare il fetch dall'esterno tramite props
38
+ useEffect(() => {
39
+ if (fetchBlogDataTrigger !== undefined && fetchBlogDataTrigger > 0) {
40
+ fetchBlogDataAsync(tid, did);
41
+ }
42
+ }, [fetchBlogDataTrigger, fetchBlogDataAsync, tid, did]);
16
43
  useEffect(() => {
17
44
  if (!tid || !did) {
18
45
  setBlogsDatasource([]);
@@ -21,7 +48,7 @@ const TMDcmtBlog = ({ blogsDatasource, setBlogsDatasource, hasLoadedDataOnce, se
21
48
  }
22
49
  // Condizione per eseguire il fetch:
23
50
  // 1. Il pannello è visibile
24
- // 2. E (non abbiamo ancora caricato dati O il `did` è cambiato rispetto all'ultima volta)
51
+ // 2. E (non abbiamo ancora caricato dati o il `did` è cambiato rispetto all'ultima volta)
25
52
  const shouldFetch = isVisible && (!hasLoadedDataOnce || did !== lastLoadedDid);
26
53
  // Esegui la chiamata API solo se il pannello è visibile E i dati non sono già stati caricati
27
54
  // O, se vuoi ricaricare ogni volta che diventa visibile (ma è meno efficiente per "pesante")