@topconsultnpm/sdkui-react 6.20.0-dev1.30 → 6.20.0-dev1.32

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.
@@ -2,7 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useState, useRef, useEffect } from 'react';
3
3
  import * as S from './styles';
4
4
  import { useIsMobile, useMenuPosition } from './hooks';
5
- const TMContextMenu = ({ items, trigger = 'right', children }) => {
5
+ import { IconArrowLeft } from '../../../helper';
6
+ const TMContextMenu = ({ items, trigger = 'right', children, externalControl }) => {
6
7
  const [menuState, setMenuState] = useState({
7
8
  visible: false,
8
9
  position: { x: 0, y: 0 },
@@ -16,14 +17,31 @@ const TMContextMenu = ({ items, trigger = 'right', children }) => {
16
17
  const submenuTimeoutRef = useRef(null);
17
18
  const { openLeft, openUp, isCalculated } = useMenuPosition(menuRef, menuState.position);
18
19
  const handleClose = () => {
19
- setMenuState(prev => ({
20
- ...prev,
21
- visible: false,
22
- submenuStack: [items],
23
- parentNames: [],
24
- }));
20
+ if (externalControl) {
21
+ externalControl.onClose();
22
+ }
23
+ else {
24
+ setMenuState(prev => ({
25
+ ...prev,
26
+ visible: false,
27
+ submenuStack: [items],
28
+ parentNames: [],
29
+ }));
30
+ }
25
31
  setHoveredSubmenus([]);
26
32
  };
33
+ // Sync with external control when provided
34
+ useEffect(() => {
35
+ if (externalControl) {
36
+ setMenuState(prev => ({
37
+ ...prev,
38
+ visible: externalControl.visible,
39
+ position: externalControl.position,
40
+ submenuStack: [items],
41
+ parentNames: [],
42
+ }));
43
+ }
44
+ }, [externalControl, items]);
27
45
  useEffect(() => {
28
46
  if (!menuState.visible)
29
47
  return;
@@ -178,10 +196,10 @@ const TMContextMenu = ({ items, trigger = 'right', children }) => {
178
196
  };
179
197
  const currentMenu = menuState.submenuStack.at(-1) || items;
180
198
  const currentParentName = menuState.parentNames.at(-1) || '';
181
- return (_jsxs(_Fragment, { children: [_jsx("div", { ref: triggerRef, onContextMenu: handleContextMenu, onClick: handleClick, onKeyDown: (e) => {
199
+ return (_jsxs(_Fragment, { children: [!externalControl && children && (_jsx("div", { ref: triggerRef, onContextMenu: handleContextMenu, onClick: handleClick, onKeyDown: (e) => {
182
200
  if (e.key === 'Enter' || e.key === ' ') {
183
201
  handleClick(e);
184
202
  }
185
- }, role: "button", tabIndex: 0, style: { display: 'inline-block' }, children: children }), menuState.visible && (_jsxs(_Fragment, { children: [_jsx(S.Overlay, { onClick: handleClose }), _jsxs(S.MenuContainer, { ref: menuRef, "$x": menuState.position.x, "$y": menuState.position.y, "$openLeft": openLeft, "$openUp": openUp, "$isPositioned": isCalculated, children: [isMobile && menuState.parentNames.length > 0 && (_jsxs(S.MobileMenuHeader, { children: [_jsx(S.BackButton, { onClick: handleBack, "aria-label": "Go back", children: "\u2190" }), _jsx(S.HeaderTitle, { children: currentParentName })] })), renderMenuItems(currentMenu, 0)] }), !isMobile && hoveredSubmenus.map((submenu, idx) => (_jsx(S.Submenu, { "$parentRect": submenu.parentRect, "$openUp": submenu.openUp, "data-submenu": "true", onMouseEnter: handleSubmenuMouseEnter, onMouseLeave: () => handleMouseLeave(submenu.depth), children: renderMenuItems(submenu.items, submenu.depth) }, `submenu-${submenu.depth}-${idx}`)))] }))] }));
203
+ }, role: "button", tabIndex: 0, style: { display: 'inline-block' }, children: children })), menuState.visible && (_jsxs(_Fragment, { children: [_jsx(S.Overlay, { onClick: handleClose }), _jsxs(S.MenuContainer, { ref: menuRef, "$x": menuState.position.x, "$y": menuState.position.y, "$openLeft": openLeft, "$openUp": openUp, "$isPositioned": isCalculated, "$externalControl": !!externalControl, children: [isMobile && menuState.parentNames.length > 0 && (_jsxs(S.MobileMenuHeader, { children: [_jsx(S.BackButton, { onClick: handleBack, "aria-label": "Go back", children: _jsx(IconArrowLeft, {}) }), _jsx(S.HeaderTitle, { children: currentParentName })] })), renderMenuItems(currentMenu, 0)] }), !isMobile && hoveredSubmenus.map((submenu, idx) => (_jsx(S.Submenu, { "$parentRect": submenu.parentRect, "$openUp": submenu.openUp, "data-submenu": "true", onMouseEnter: handleSubmenuMouseEnter, onMouseLeave: () => handleMouseLeave(submenu.depth), children: renderMenuItems(submenu.items, submenu.depth) }, `submenu-${submenu.depth}-${idx}`)))] }))] }));
186
204
  };
187
205
  export default TMContextMenu;
@@ -4,6 +4,7 @@ export declare const MenuContainer: import("styled-components/dist/types").IStyl
4
4
  $openLeft: boolean;
5
5
  $openUp: boolean;
6
6
  $isPositioned: boolean;
7
+ $externalControl?: boolean;
7
8
  }>> & string;
8
9
  export declare const MenuItem: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Substitute<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {
9
10
  $disabled?: boolean;
@@ -54,6 +54,16 @@ export const MenuContainer = styled.div `
54
54
  [data-theme='dark'] & *:not(svg):not(.right-icon-btn):not(.right-icon-btn *) {
55
55
  color: #e0e0e0 !important;
56
56
  }
57
+
58
+ ${props => props.$externalControl && `
59
+ @media (max-width: 768px) {
60
+ left: 100px !important;
61
+ right: 100px !important;
62
+ max-width: calc(100vw - 200px);
63
+ width: auto;
64
+ min-width: auto;
65
+ }
66
+ `}
57
67
  `;
58
68
  export const MenuItem = styled.div `
59
69
  display: flex;
@@ -123,8 +133,8 @@ export const MenuItem = styled.div `
123
133
  }
124
134
 
125
135
  @media (max-width: 768px) {
126
- padding: 14px 16px;
127
- font-size: 15px;
136
+ padding: 4px 10px;
137
+ font-size: 12px;
128
138
  }
129
139
  `;
130
140
  export const MenuItemContent = styled.div `
@@ -266,7 +276,7 @@ export const Submenu = styled.div `
266
276
  export const MobileMenuHeader = styled.div `
267
277
  display: flex;
268
278
  align-items: center;
269
- padding: 12px 16px;
279
+ padding: 4px 8px;
270
280
  border-bottom: 1px solid rgba(0, 0, 0, 0.08);
271
281
  margin-bottom: 8px;
272
282
  gap: 12px;
@@ -282,33 +292,18 @@ export const BackButton = styled.button `
282
292
  display: flex;
283
293
  align-items: center;
284
294
  justify-content: center;
285
- background: #0066cc;
286
- color: white;
287
295
  border: none;
288
296
  border-radius: 8px;
289
297
  width: 32px;
290
298
  height: 32px;
291
299
  cursor: pointer;
292
- font-size: 18px;
293
300
  transition: all 0.15s ease;
294
-
295
- &:hover {
296
- background: #0052a3;
297
- transform: scale(1.05);
298
- }
301
+ font-size: 16px;
302
+ transform: translateY(-2px);
299
303
 
300
304
  &:active {
301
305
  transform: scale(0.95);
302
306
  }
303
-
304
- [data-theme='dark'] & {
305
- background: #4db8ff;
306
- color: #1a1a1a;
307
-
308
- &:hover {
309
- background: #66c2ff;
310
- }
311
- }
312
307
  `;
313
308
  export const HeaderTitle = styled.h3 `
314
309
  margin: 0;
@@ -9,11 +9,20 @@ export interface TMContextMenuItemProps {
9
9
  onRightIconClick?: () => void;
10
10
  beginGroup?: boolean;
11
11
  tooltip?: string;
12
+ operationType?: 'singleRow' | 'multiRow';
12
13
  }
13
14
  export interface TMContextMenuProps {
14
15
  items: TMContextMenuItemProps[];
15
16
  trigger?: 'right' | 'left';
16
17
  children?: React.ReactNode;
18
+ externalControl?: {
19
+ visible: boolean;
20
+ position: {
21
+ x: number;
22
+ y: number;
23
+ };
24
+ onClose: () => void;
25
+ };
17
26
  }
18
27
  export interface Position {
19
28
  x: number;
@@ -6,30 +6,75 @@ import TMTooltip from '../../base/TMTooltip';
6
6
  import * as S from './styles';
7
7
  import { IconApply, IconMenuKebab, IconMenuVertical, IconPencil, IconPin, SDKUI_Globals } from '../../../helper';
8
8
  const IconDraggableDots = (props) => (_jsx("svg", { fontSize: 18, viewBox: "0 0 24 24", fill: "currentColor", height: "1em", width: "1em", ...props, children: _jsx("path", { d: "M9 3a2 2 0 11-4 0 2 2 0 014 0zm0 9a2 2 0 11-4 0 2 2 0 014 0zm0 9a2 2 0 11-4 0 2 2 0 014 0zm10-18a2 2 0 11-4 0 2 2 0 014 0zm0 9a2 2 0 11-4 0 2 2 0 014 0zm0 9a2 2 0 11-4 0 2 2 0 014 0z" }) }));
9
- const TMFloatingMenuBar = ({ containerRef, contextMenuItems = [], storageKey = 'floatingMenuBar-config', // Kept for backward compatibility but not used
10
- isConstrained = false, defaultPosition = { x: 100, y: 100 }, maxItems = 8, }) => {
9
+ const TMFloatingMenuBar = ({ containerRef, contextMenuItems = [], isConstrained = false, defaultPosition = { x: 100, y: 100 }, maxItems = 8, }) => {
10
+ const getDefaultConfig = () => ({
11
+ orientation: 'horizontal',
12
+ savedItemIds: [],
13
+ position: defaultPosition,
14
+ });
15
+ const resetFloatingBarSettings = () => {
16
+ // Reset the floatingMenuBar settings in SDKUI_Globals to trigger save to localStorage
17
+ SDKUI_Globals.userSettings.searchSettings.floatingMenuBar = {
18
+ orientation: 'horizontal',
19
+ itemIds: [],
20
+ position: defaultPosition,
21
+ };
22
+ };
11
23
  const loadConfig = () => {
12
24
  try {
13
25
  const settings = SDKUI_Globals.userSettings.searchSettings.floatingMenuBar;
26
+ // Validate that settings object exists and has required properties with correct types
27
+ if (!settings || typeof settings !== 'object') {
28
+ console.warn('FloatingMenuBar: Invalid settings object, resetting to defaults');
29
+ resetFloatingBarSettings();
30
+ return getDefaultConfig();
31
+ }
32
+ // Validate position
33
+ const hasValidPosition = settings.position &&
34
+ typeof settings.position.x === 'number' &&
35
+ typeof settings.position.y === 'number' &&
36
+ !isNaN(settings.position.x) &&
37
+ !isNaN(settings.position.y) &&
38
+ isFinite(settings.position.x) &&
39
+ isFinite(settings.position.y);
40
+ if (!hasValidPosition) {
41
+ console.warn('FloatingMenuBar: Invalid position, resetting to defaults');
42
+ resetFloatingBarSettings();
43
+ return getDefaultConfig();
44
+ }
45
+ // Ensure position is within reasonable viewport bounds
46
+ const maxX = globalThis.window?.innerWidth ? globalThis.window.innerWidth - 50 : 1000;
47
+ const maxY = globalThis.window?.innerHeight ? globalThis.window.innerHeight - 50 : 800;
48
+ if (settings.position.x < 0 || settings.position.x > maxX ||
49
+ settings.position.y < 0 || settings.position.y > maxY) {
50
+ console.warn('FloatingMenuBar: Position out of bounds, resetting to defaults');
51
+ resetFloatingBarSettings();
52
+ return getDefaultConfig();
53
+ }
54
+ // Validate orientation
55
+ const validOrientation = (settings.orientation === 'horizontal' || settings.orientation === 'vertical')
56
+ ? settings.orientation
57
+ : 'horizontal';
58
+ // Validate itemIds
59
+ const validItemIds = Array.isArray(settings.itemIds) ? settings.itemIds : [];
14
60
  // Check if position was actually saved (not just the default class value)
15
- const hasSavedPosition = settings.position &&
16
- (settings.position.x !== 100 || settings.position.y !== 100 ||
17
- (settings.itemIds && settings.itemIds.length > 0));
61
+ const hasSavedPosition = settings.position.x !== 100 || settings.position.y !== 100 || validItemIds.length > 0;
18
62
  return {
19
- orientation: settings.orientation || 'horizontal',
20
- pinnedItemIds: new Set(settings.pinnedItemIds || []),
21
- savedItemIds: settings.itemIds || [],
63
+ orientation: validOrientation,
64
+ savedItemIds: validItemIds,
22
65
  position: hasSavedPosition ? settings.position : defaultPosition,
23
66
  };
24
67
  }
25
68
  catch (error) {
26
69
  console.error('Failed to load FloatingMenuBar config:', error);
27
- return {
28
- orientation: 'horizontal',
29
- pinnedItemIds: new Set(),
30
- savedItemIds: [],
31
- position: defaultPosition,
32
- };
70
+ // Reset to defaults on any error
71
+ try {
72
+ resetFloatingBarSettings();
73
+ }
74
+ catch (e) {
75
+ console.error('Failed to reset FloatingMenuBar settings:', e);
76
+ }
77
+ return getDefaultConfig();
33
78
  }
34
79
  };
35
80
  const initialConfig = loadConfig();
@@ -43,7 +88,6 @@ isConstrained = false, defaultPosition = { x: 100, y: 100 }, maxItems = 8, }) =>
43
88
  });
44
89
  const floatingRef = useRef(null);
45
90
  const dragOffset = useRef({ x: 0, y: 0 });
46
- const [pinnedItemIds, setPinnedItemIds] = useState(initialConfig.pinnedItemIds);
47
91
  const [dragOverIndex, setDragOverIndex] = useState(null);
48
92
  // Use refs to track item IDs without causing re-renders
49
93
  const floatingBarItemIds = useRef(new Set());
@@ -60,13 +104,15 @@ isConstrained = false, defaultPosition = { x: 100, y: 100 }, maxItems = 8, }) =>
60
104
  const itemId = `${parentPath}${item.name}-${index}`;
61
105
  // Only add items that have onClick (final actions, not submenu parents)
62
106
  if (item.onClick && !item.submenu) {
107
+ // Check if item is currently in the floating bar
108
+ const isPinned = state.items.some(i => i.id === itemId || i.name === item.name);
63
109
  result.push({
64
110
  id: itemId,
65
111
  name: item.name,
66
112
  icon: item.icon || _jsx(IconPin, {}),
67
113
  onClick: item.onClick,
68
114
  disabled: item.disabled,
69
- isPinned: pinnedItemIds.has(itemId),
115
+ isPinned: isPinned,
70
116
  originalMenuItem: item,
71
117
  });
72
118
  }
@@ -76,7 +122,7 @@ isConstrained = false, defaultPosition = { x: 100, y: 100 }, maxItems = 8, }) =>
76
122
  }
77
123
  });
78
124
  return result;
79
- }, [pinnedItemIds]);
125
+ }, [state.items]);
80
126
  // Restore items on mount from savedItemIds
81
127
  useEffect(() => {
82
128
  if (contextMenuItems.length > 0) {
@@ -112,17 +158,6 @@ isConstrained = false, defaultPosition = { x: 100, y: 100 }, maxItems = 8, }) =>
112
158
  return { ...s, items: [...s.items, item] };
113
159
  }
114
160
  });
115
- // Update pinned IDs for context menu items
116
- setPinnedItemIds(prev => {
117
- const newSet = new Set(prev);
118
- if (newSet.has(item.id)) {
119
- newSet.delete(item.id);
120
- }
121
- else {
122
- newSet.add(item.id);
123
- }
124
- return newSet;
125
- });
126
161
  }, [maxItems]);
127
162
  // Get current item state (disabled and onClick) from contextMenuItems
128
163
  const getCurrentItemState = useCallback((itemName) => {
@@ -242,7 +277,6 @@ isConstrained = false, defaultPosition = { x: 100, y: 100 }, maxItems = 8, }) =>
242
277
  // Replace the entire object to trigger the Proxy
243
278
  SDKUI_Globals.userSettings.searchSettings.floatingMenuBar = {
244
279
  orientation: state.orientation,
245
- pinnedItemIds: Array.from(pinnedItemIds),
246
280
  itemIds: state.items.map(item => item.id),
247
281
  position: state.position,
248
282
  };
@@ -250,7 +284,7 @@ isConstrained = false, defaultPosition = { x: 100, y: 100 }, maxItems = 8, }) =>
250
284
  catch (error) {
251
285
  console.error('Failed to save FloatingMenuBar config:', error);
252
286
  }
253
- }, [state.orientation, state.items, state.position, pinnedItemIds]);
287
+ }, [state.orientation, state.items, state.position]);
254
288
  const toggleConfigMode = () => {
255
289
  setState(s => ({ ...s, isConfigMode: !s.isConfigMode }));
256
290
  };
@@ -294,12 +328,6 @@ isConstrained = false, defaultPosition = { x: 100, y: 100 }, maxItems = 8, }) =>
294
328
  ...s,
295
329
  items: s.items.filter(item => item.id !== itemId),
296
330
  }));
297
- // Also remove from pinned items if it was pinned
298
- setPinnedItemIds(prev => {
299
- const newSet = new Set(prev);
300
- newSet.delete(itemId);
301
- return newSet;
302
- });
303
331
  };
304
332
  // Drag and drop for reordering
305
333
  const handleDragStart = (e, index) => {
@@ -373,6 +401,6 @@ isConstrained = false, defaultPosition = { x: 100, y: 100 }, maxItems = 8, }) =>
373
401
  currentOnClick();
374
402
  }
375
403
  }, disabled: isDisabled, children: item.icon }) })), state.isConfigMode && (_jsx(S.RemoveButton, { onClick: () => removeItem(item.id), children: "\u00D7" }))] }, item.id));
376
- }), !state.isConfigMode && contextMenuItems.length > 0 && (_jsx(ContextMenu, { items: enhancedContextMenuItems(), trigger: "left", children: _jsx(S.ContextMenuButton, { children: _jsx(IconMenuVertical, {}) }) }, Array.from(pinnedItemIds).join(','))), _jsx(S.ConfigButton, { onClick: toggleConfigMode, "$isActive": state.isConfigMode, children: state.isConfigMode ? _jsx(IconApply, {}) : _jsx(IconPencil, {}) }), !state.isConfigMode && (_jsx(S.OrientationToggle, { "$orientation": state.orientation, onClick: toggleOrientation, children: _jsx(IconMenuKebab, {}) }))] })] }));
404
+ }), !state.isConfigMode && contextMenuItems.length > 0 && (_jsx(ContextMenu, { items: enhancedContextMenuItems(), trigger: "left", children: _jsx(S.ContextMenuButton, { children: _jsx(IconMenuVertical, {}) }) }, state.items.map(i => i.id).join(','))), _jsx(S.ConfigButton, { onClick: toggleConfigMode, "$isActive": state.isConfigMode, children: state.isConfigMode ? _jsx(IconApply, {}) : _jsx(IconPencil, {}) }), !state.isConfigMode && (_jsx(S.OrientationToggle, { "$orientation": state.orientation, onClick: toggleOrientation, children: _jsx(IconMenuKebab, {}) }))] })] }));
377
405
  };
378
406
  export default TMFloatingMenuBar;
@@ -137,7 +137,6 @@ export const MenuButton = styled.button `
137
137
  svg {
138
138
  width: 18px;
139
139
  height: 18px;
140
- transform: translateY(2px);
141
140
  }
142
141
  `;
143
142
  export const ConfigButton = 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;
@@ -52,12 +52,12 @@ const TMCustomButton = (props) => {
52
52
  const RunOnce = button.mode === "RunOnce";
53
53
  const [loading, setLoading] = useState(true);
54
54
  const [error, setError] = useState(false);
55
- const selectedItemsCount = selectedItems?.length || 0;
55
+ const itemsToProcess = useMemo(() => selectedItems && selectedItems.length > 0 ? selectedItems : [attributes], [selectedItems, attributes]);
56
56
  // Stati per il wait panel
57
- const [showWaitPanel, setShowWaitPanel] = useState(selectedItemsCount > 0 && !RunOnce);
58
- 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));
59
59
  const [waitPanelValue, setWaitPanelValue] = useState(0);
60
- const [waitPanelMaxValue, setWaitPanelMaxValue] = useState(selectedItemsCount);
60
+ const [waitPanelMaxValue, setWaitPanelMaxValue] = useState(itemsToProcess.length);
61
61
  const [abortController, setAbortController] = useState(undefined);
62
62
  // Aggiungi timestamp all'URL per evitare cache
63
63
  const iframeUrl = useMemo(() => {
@@ -84,21 +84,29 @@ const TMCustomButton = (props) => {
84
84
  setError(true);
85
85
  };
86
86
  const executeSequentially = async (controller) => {
87
- if (!selectedItems)
87
+ if (!itemsToProcess)
88
88
  return;
89
- for (const [index, item] of selectedItems.entries()) {
89
+ for (const [index, item] of itemsToProcess.entries()) {
90
90
  if (controller.signal.aborted)
91
91
  break;
92
- setWaitPanelText(SDKUI_Localizator.CustomButtonActions.replaceParams(index + 1, selectedItemsCount));
92
+ setWaitPanelText(SDKUI_Localizator.CustomButtonActions.replaceParams(index + 1, itemsToProcess.length));
93
93
  setWaitPanelValue(index);
94
94
  // Attendi che l'iframe sia pronto e invia il messaggio
95
95
  await new Promise((resolve) => {
96
96
  const checkIframe = setInterval(() => {
97
97
  if (iframeRef.current?.contentWindow) {
98
98
  clearInterval(checkIframe);
99
- //devo convertire item in formData
100
- const processedItem = getSelectedItem(args, formData, item);
101
- 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);
102
110
  // Attendi prima di passare al prossimo
103
111
  setTimeout(() => {
104
112
  setWaitPanelValue(index + 1);
@@ -120,7 +128,7 @@ const TMCustomButton = (props) => {
120
128
  useEffect(() => {
121
129
  if (loading || error)
122
130
  return;
123
- if (!RunOnce && selectedItemsCount > 0) {
131
+ if (!RunOnce && itemsToProcess.length > 0) {
124
132
  // esegui per ogni item selezionato
125
133
  const controller = new AbortController();
126
134
  controller.signal.addEventListener('abort', () => {
@@ -128,7 +136,7 @@ const TMCustomButton = (props) => {
128
136
  onClose?.();
129
137
  });
130
138
  setAbortController(controller);
131
- setWaitPanelMaxValue(selectedItemsCount);
139
+ setWaitPanelMaxValue(itemsToProcess.length);
132
140
  executeSequentially(controller);
133
141
  }
134
142
  else {
@@ -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;
@@ -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;
@@ -4,6 +4,7 @@ import DataGrid, { Column, HeaderFilter, Selection, Scrolling, LoadPanel, Search
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,7 +17,7 @@ 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, showHeaderColumnChooser = false, 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
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
@@ -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') {
@@ -157,7 +233,7 @@ const TMDataGrid = React.forwardRef((props, ref) => {
157
233
  }
158
234
  });
159
235
  }
160
- }, [updateContextMenuItems, onContextMenuPreparing, showHeaderColumnChooser]);
236
+ }, [updateContextMenuItems, onContextMenuPreparing, showHeaderColumnChooser, customContextMenuItems]);
161
237
  // Handle toolbar preparation, especially for the search panel
162
238
  const onToolbarPreparingCallback = useCallback((e) => {
163
239
  if (e === undefined || e.toolbarOptions === undefined || e.toolbarOptions.items === undefined)
@@ -226,15 +302,19 @@ const TMDataGrid = React.forwardRef((props, ref) => {
226
302
  // Propaga l'evento originale
227
303
  onOptionChanged?.(e);
228
304
  }, [onOptionChanged, onHasFiltersChange]);
229
- 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' : ''}`,
230
306
  // main properties
231
307
  keyExpr: keyExpr, dataSource: dataSource, selectedRowKeys: selectedRowKeys, focusedRowEnabled: focusedRowEnabled, hoverStateEnabled: hoverStateEnabled,
232
308
  // events and callbacks
233
- 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,
234
310
  // other properties
235
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,
236
312
  // styles
237
- 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 }) })] });
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
+ } }))] });
238
318
  });
239
319
  export default TMDataGrid;
240
320
  const getRecordCount = (dataSource) => {