@topconsultnpm/sdkui-react 6.19.0-dev2.41 → 6.19.0-dev2.43

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.
@@ -7,5 +7,5 @@ type TMCustomButtonProps = {
7
7
  selectedItems?: Array<any>;
8
8
  onClose?: () => void;
9
9
  };
10
- declare const TMCustomButton: (props: TMCustomButtonProps) => import("react/jsx-runtime").JSX.Element | null;
10
+ declare const TMCustomButton: (props: TMCustomButtonProps) => false | import("react/jsx-runtime").JSX.Element;
11
11
  export default TMCustomButton;
@@ -2,8 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useEffect, useRef, useState, useMemo } from 'react';
3
3
  import TMModal from './TMModal';
4
4
  import styled from 'styled-components';
5
- import { TMLayoutWaitingContainer } from '../..';
6
- import { processButtonAttributes } from '../../helper/dcmtsHelper';
5
+ import { SDKUI_Localizator, TMLayoutWaitingContainer } from '../..';
6
+ import { getButtonAttributes, getSelectedItem } from '../../helper/dcmtsHelper';
7
7
  const IframeContainer = styled.div `
8
8
  display: flex;
9
9
  height: 100%;
@@ -18,16 +18,14 @@ const TMCustomButton = (props) => {
18
18
  const { button, isModal = true, formData, selectedItems, onClose } = props;
19
19
  const { appName: scriptUrl, arguments: args } = button;
20
20
  const iframeRef = useRef(null);
21
- const attributes = useMemo(() => processButtonAttributes(args, formData, selectedItems), [args, formData, selectedItems]);
22
- const selectedItemsProcessed = []; //da eliminare
23
- //const selectedItemsProcessed = useMemo(() => processSelectedItems(args, formData, selectedItems), [args, formData, selectedItems]);
21
+ const attributes = useMemo(() => getButtonAttributes(args, formData, selectedItems), [args, formData, selectedItems]);
24
22
  const RunOnce = button.mode === "RunOnce";
25
23
  const [loading, setLoading] = useState(true);
26
24
  const [error, setError] = useState(false);
27
- const selectedItemsCount = selectedItems?.length || 1;
25
+ const selectedItemsCount = selectedItems?.length || 0;
28
26
  // Stati per il wait panel
29
- const [showWaitPanel, setShowWaitPanel] = useState(true);
30
- const [waitPanelText, setWaitPanelText] = useState("Esecuzione azione 1/" + selectedItemsCount.toString());
27
+ const [showWaitPanel, setShowWaitPanel] = useState(selectedItemsCount > 0 && !RunOnce);
28
+ const [waitPanelText, setWaitPanelText] = useState(SDKUI_Localizator.CustomButtonActions.replaceParams(1, selectedItemsCount));
31
29
  const [waitPanelValue, setWaitPanelValue] = useState(0);
32
30
  const [waitPanelMaxValue, setWaitPanelMaxValue] = useState(selectedItemsCount);
33
31
  const [abortController, setAbortController] = useState(undefined);
@@ -48,20 +46,21 @@ const TMCustomButton = (props) => {
48
46
  setError(true);
49
47
  };
50
48
  const executeSequentially = async (controller) => {
51
- if (!selectedItemsProcessed)
49
+ if (!selectedItems)
52
50
  return;
53
- const processedItems = selectedItemsProcessed.length === 0 ? [...selectedItemsProcessed, attributes] : selectedItemsProcessed;
54
- for (const [index, item] of processedItems.entries()) {
51
+ for (const [index, item] of selectedItems.entries()) {
55
52
  if (controller.signal.aborted)
56
53
  break;
57
- setWaitPanelText("Esecuzione azione " + (index + 1).toString() + "/" + selectedItemsCount.toString());
54
+ setWaitPanelText(SDKUI_Localizator.CustomButtonActions.replaceParams(index + 1, selectedItemsCount));
58
55
  setWaitPanelValue(index);
59
56
  // Attendi che l'iframe sia pronto e invia il messaggio
60
57
  await new Promise((resolve) => {
61
58
  const checkIframe = setInterval(() => {
62
59
  if (iframeRef.current?.contentWindow) {
63
60
  clearInterval(checkIframe);
64
- iframeRef.current.contentWindow.postMessage({ "options": item }, targetOrigin);
61
+ //devo convertire item in formData
62
+ const processedItem = getSelectedItem(args, formData, item);
63
+ postMessageIframe(processedItem);
65
64
  // Attendi prima di passare al prossimo
66
65
  setTimeout(() => {
67
66
  setWaitPanelValue(index + 1);
@@ -74,12 +73,17 @@ const TMCustomButton = (props) => {
74
73
  setShowWaitPanel(false);
75
74
  onClose?.();
76
75
  };
76
+ const postMessageIframe = (data) => {
77
+ console.log("TMCustomButton - postMessageIframe - data:", data);
78
+ if (iframeRef.current?.contentWindow) {
79
+ iframeRef.current.contentWindow.postMessage({ "options": data }, targetOrigin);
80
+ }
81
+ };
77
82
  useEffect(() => {
78
83
  if (loading || error)
79
84
  return;
80
85
  //if(error) clearTimeout(timeoutIframe);
81
- console.log("TMCustomButton - useEffect - loading:", loading, " error:", error, " RunOnce:", RunOnce);
82
- if (!RunOnce) {
86
+ if (!RunOnce && selectedItemsCount > 0) {
83
87
  // esegui per ogni item selezionato
84
88
  const controller = new AbortController();
85
89
  controller.signal.addEventListener('abort', () => {
@@ -90,9 +94,15 @@ const TMCustomButton = (props) => {
90
94
  setWaitPanelMaxValue(selectedItemsCount);
91
95
  executeSequentially(controller);
92
96
  }
93
- else if (iframeRef.current?.contentWindow) {
97
+ else {
94
98
  // Modalità RunOnce: invia dati all'iframe quando è caricato
95
- iframeRef.current.contentWindow.postMessage({ "options": attributes }, targetOrigin);
99
+ postMessageIframe(attributes);
100
+ if (!RunOnce) {
101
+ //chiudo l'iframe dopo 2 secondi
102
+ setTimeout(() => {
103
+ onClose?.();
104
+ }, 2000);
105
+ }
96
106
  //clearTimeout(timeoutIframe);
97
107
  }
98
108
  }, [loading, error, RunOnce]);
@@ -103,7 +113,6 @@ const TMCustomButton = (props) => {
103
113
  }
104
114
  }, []);
105
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 })] }));
106
- return isModal && RunOnce ? (_jsx(TMModal, { title: button.title, width: '60%', height: '70%', resizable: true, onClose: onClose, children: iframeContent })) : !RunOnce && showWaitPanel ? (_jsxs(_Fragment, { children: [_jsx(TMLayoutWaitingContainer, { showWaitPanel: true, waitPanelTitle: "Operazione in corso", showWaitPanelPrimary: true, waitPanelTextPrimary: waitPanelText, waitPanelValuePrimary: waitPanelValue, waitPanelMaxValuePrimary: waitPanelMaxValue, showWaitPanelSecondary: false, isCancelable: true, abortController: abortController, children: undefined }), iframeContent] }))
107
- : null;
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] }));
108
117
  };
109
118
  export default TMCustomButton;
@@ -8,6 +8,7 @@ interface ITMModal {
8
8
  fontSize?: string;
9
9
  isModal?: boolean;
10
10
  resizable?: boolean;
11
+ expandable?: boolean;
11
12
  onClose?: () => void;
12
13
  hidePopup?: boolean;
13
14
  askClosingConfirm?: boolean;
@@ -4,6 +4,7 @@ import { Popup } from 'devextreme-react';
4
4
  import styled from 'styled-components';
5
5
  import TMLayoutContainer, { TMCard, TMLayoutItem } from './TMLayout';
6
6
  import { FontSize, TMColors } from '../../utils/theme';
7
+ import { IconWindowMaximize, IconWindowMinimize, svgToString } from '../../helper';
7
8
  const StyledModal = styled.div `
8
9
  width: ${props => props.$width};
9
10
  height: ${props => props.$height};
@@ -38,11 +39,12 @@ const StyledModalContext = styled.div `
38
39
  overflow: auto;
39
40
  height: 100%;
40
41
  `;
41
- const TMModal = ({ resizable = true, isModal = true, title = '', toolbar, onClose, children, width = '100%', height = '100%', fontSize = FontSize.defaultFontSize, hidePopup = true, askClosingConfirm = false, showCloseButton = true }) => {
42
+ const TMModal = ({ resizable = true, expandable = false, isModal = true, title = '', toolbar, onClose, children, width = '100%', height = '100%', fontSize = FontSize.defaultFontSize, hidePopup = true, askClosingConfirm = false, showCloseButton = true }) => {
42
43
  const [initialWidth, setInitialWidth] = useState(width);
43
44
  const [initialHeight, setInitialHeight] = useState(height);
44
45
  const [showPopup, setShowPopup] = useState(false);
45
46
  const [isResizing, setIsResizing] = useState(false);
47
+ const [isFullScreen, setIsFullScreen] = useState(false);
46
48
  useEffect(() => {
47
49
  setShowPopup(isModal);
48
50
  }, [isModal]);
@@ -65,6 +67,15 @@ const TMModal = ({ resizable = true, isModal = true, title = '', toolbar, onClos
65
67
  setShowPopup(false);
66
68
  onClose && onClose();
67
69
  };
68
- return (_jsx(_Fragment, { children: isModal ? (_jsx(Popup, { showCloseButton: showCloseButton, animation: undefined, maxHeight: '95%', maxWidth: '95%', dragEnabled: !isResizing, resizeEnabled: resizable, width: initialWidth, height: initialHeight, title: title, visible: showPopup, onResizeStart: handleResizeStart, onResizeEnd: handleResizeEnd, onHiding: onHiding, children: _jsxs(TMLayoutContainer, { children: [toolbar && (_jsx(TMLayoutItem, { height: "40px", children: _jsx(StyledModalToolbar, { children: toolbar }) })), _jsx(TMLayoutItem, { height: toolbar ? 'calc(100% - 50px)' : 'calc(100% - 10px)', children: _jsx(TMCard, { showBorder: false, padding: false, scrollY: true, children: children }) })] }) })) : (_jsxs(StyledModal, { "$isModal": isModal, className: "temp-modal", "$fontSize": fontSize, "$width": initialWidth, "$height": initialHeight, children: [toolbar ? _jsx(StyledModalToolbar, { children: toolbar }) : _jsx(_Fragment, {}), _jsx(StyledModalContext, { children: children })] })) }));
70
+ return (_jsx(_Fragment, { children: isModal ? (_jsx(Popup, { showCloseButton: showCloseButton, animation: undefined, maxHeight: '95%', maxWidth: '95%', dragEnabled: !isResizing, resizeEnabled: resizable, width: expandable && isFullScreen ? '95%' : initialWidth, height: expandable && isFullScreen ? '95%' : initialHeight, title: title, visible: showPopup, onResizeStart: handleResizeStart, onResizeEnd: handleResizeEnd, onHiding: onHiding, toolbarItems: expandable ? [
71
+ {
72
+ widget: 'dxButton',
73
+ location: 'after',
74
+ options: {
75
+ icon: isFullScreen ? svgToString(_jsx(IconWindowMinimize, {})) : svgToString(_jsx(IconWindowMaximize, {})),
76
+ onClick: () => setIsFullScreen(!isFullScreen)
77
+ }
78
+ }
79
+ ] : undefined, children: _jsxs(TMLayoutContainer, { children: [toolbar && (_jsx(TMLayoutItem, { height: "40px", children: _jsx(StyledModalToolbar, { children: toolbar }) })), _jsx(TMLayoutItem, { height: toolbar ? 'calc(100% - 50px)' : 'calc(100% - 10px)', children: _jsx(TMCard, { showBorder: false, padding: false, scrollY: true, children: children }) })] }) })) : (_jsxs(StyledModal, { "$isModal": isModal, className: "temp-modal", "$fontSize": fontSize, "$width": initialWidth, "$height": initialHeight, children: [toolbar ? _jsx(StyledModalToolbar, { children: toolbar }) : _jsx(_Fragment, {}), _jsx(StyledModalContext, { children: children })] })) }));
69
80
  };
70
81
  export default TMModal;
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import { DeviceType } from '../../base/TMDeviceProvider';
3
+ /**
4
+ * Proprietà del componente ToppyDraggableHelpCenter
5
+ */
6
+ interface ToppyDraggableHelpCenterProps {
7
+ /** Contenuto da visualizzare nella speech bubble */
8
+ content?: React.ReactNode;
9
+ /** Tipo di dispositivo per adattare il comportamento */
10
+ deviceType?: DeviceType;
11
+ /** Se utilizzare un portal per il rendering (non utilizzato attualmente) */
12
+ usePortal?: boolean;
13
+ /** Allineamento iniziale del componente (destra o sinistra) */
14
+ align?: 'right' | 'left';
15
+ /** Stato iniziale collassato/espanso */
16
+ initialIsCollapsed?: boolean;
17
+ /** Callback chiamato quando si clicca sull'immagine di Toppy */
18
+ onToppyImageClick?: () => void;
19
+ /** Visibilità del componente */
20
+ isVisible?: boolean;
21
+ }
22
+ /**
23
+ * Componente ToppyDraggableHelpCenter
24
+ *
25
+ * Renderizza un assistente virtuale (Toppy) draggable che può mostrare contenuti
26
+ * in una speech bubble. Il componente può essere trascinato all'interno del suo
27
+ * contenitore e può essere collassato/espanso con un doppio click.
28
+ */
29
+ declare const ToppyDraggableHelpCenter: React.FC<ToppyDraggableHelpCenterProps>;
30
+ export default ToppyDraggableHelpCenter;
@@ -0,0 +1,283 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef, useState, useEffect } from 'react';
3
+ import styled from 'styled-components';
4
+ import Toppy from '../../../assets/Toppy-generico.png';
5
+ import { DeviceType } from '../../base/TMDeviceProvider';
6
+ import ToppySpeechBubble from './ToppySpeechBubble';
7
+ /**
8
+ * Styled component per il pulsante di Toppy
9
+ * Gestisce il posizionamento, le dimensioni e le animazioni
10
+ */
11
+ const ToppyButton = styled.button.attrs((props) => ({
12
+ style: {
13
+ // Applica left/top come stili inline per evitare la generazione di troppe classi CSS
14
+ ...(props.$x !== undefined && props.$y !== undefined
15
+ ? {
16
+ left: `${props.$x}px`,
17
+ top: `${props.$y}px`,
18
+ bottom: 'auto',
19
+ right: 'auto',
20
+ }
21
+ : {}),
22
+ // Cursore applicato come stile inline
23
+ cursor: props.$isDragging ? 'grabbing' : 'grab',
24
+ },
25
+ })) `
26
+ /* Visibilità controllata dalla prop */
27
+ display: ${(props) => (props.$isVisible ? 'flex' : 'none')};
28
+ position: absolute;
29
+
30
+ /* Posizionamento di default quando non è draggato (x e y non sono definiti) */
31
+ ${(props) => props.$x === undefined || props.$y === undefined
32
+ ? `
33
+ bottom: -20px;
34
+ ${props.$align === 'left' ? 'left: 10px;' : 'right: 10px;'};
35
+ `
36
+ : ''}
37
+
38
+ /* Z-index alto per assicurare che sia sempre in primo piano */
39
+ z-index: 2147483647;
40
+ background-color: transparent;
41
+ border: none;
42
+
43
+ /* Dimensioni dinamiche in base allo stato collassato
44
+ Usa min() per adattarsi su schermi piccoli */
45
+ width: ${(props) => (props.$isCollapsed ? 'min(60px, 100%)' : '120px')};
46
+ height: ${(props) => (props.$isCollapsed ? 'min(70px, 100%)' : '140px')};
47
+ max-width: 100%;
48
+ max-height: 100%;
49
+
50
+ user-select: none;
51
+
52
+ img {
53
+ /* Dimensioni dell'immagine in base allo stato collassato */
54
+ width: ${(props) => (props.$isCollapsed ? '60px' : '120px')};
55
+ height: ${(props) => (props.$isCollapsed ? '60px' : '140px')};
56
+ pointer-events: none;
57
+ border-radius: 50%; /* Rende l'immagine circolare */
58
+ /* Rotazione leggera in base all'allineamento */
59
+ transform: ${(props) => props.$align === 'left' ? 'rotate(20deg)' : 'rotate(-20deg)'};
60
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
61
+
62
+ /* Animazione di pulsazione quando è collassato per attirare l'attenzione */
63
+ ${(props) => props.$isCollapsed &&
64
+ `
65
+ animation: toppyPulse 1.5s infinite;
66
+ `}
67
+ }
68
+
69
+ /* Keyframes per l'animazione di pulsazione */
70
+ @keyframes toppyPulse {
71
+ 0% {
72
+ /* Dimensione normale all'inizio */
73
+ transform: scale(1) rotate(${(props) => props.$align === 'left' ? '20deg' : '-20deg'});
74
+ box-shadow: 0 0 0 rgba(0, 113, 188, 0.5);
75
+ }
76
+ 50% {
77
+ /* Ingrandimento e glow al 50% dell'animazione */
78
+ transform: scale(1.1) rotate(${(props) => props.$align === 'left' ? '20deg' : '-20deg'});
79
+ box-shadow: 0 0 15px 5px rgba(0, 113, 188, 0.6);
80
+ }
81
+ 100% {
82
+ /* Ritorno alla dimensione normale */
83
+ transform: scale(1) rotate(${(props) => props.$align === 'left' ? '20deg' : '-20deg'});
84
+ box-shadow: 0 0 0 rgba(0, 113, 188, 0.5);
85
+ }
86
+ }
87
+ `;
88
+ /**
89
+ * Componente ToppyDraggableHelpCenter
90
+ *
91
+ * Renderizza un assistente virtuale (Toppy) draggable che può mostrare contenuti
92
+ * in una speech bubble. Il componente può essere trascinato all'interno del suo
93
+ * contenitore e può essere collassato/espanso con un doppio click.
94
+ */
95
+ const ToppyDraggableHelpCenter = ({ content, deviceType, align = 'right', onToppyImageClick, initialIsCollapsed, isVisible = true, }) => {
96
+ // Ref per il pulsante principale
97
+ const buttonRef = useRef(null);
98
+ // Stato per controllare se il componente è collassato o espanso
99
+ const [isCollapsed, setIsCollapsed] = useState(initialIsCollapsed ?? false);
100
+ // Stato per tracciare se il componente è in fase di trascinamento
101
+ const [isDragging, setIsDragging] = useState(false);
102
+ // Posizione corrente del componente (null = posizione di default)
103
+ const [position, setPosition] = useState(null);
104
+ // Offset del mouse rispetto all'angolo superiore sinistro del componente durante il drag
105
+ const dragOffset = useRef({ x: 0, y: 0 });
106
+ // Ref e stato per tracciare le dimensioni della speech bubble
107
+ const bubbleRef = useRef(null);
108
+ const [bubbleSize, setBubbleSize] = useState({ width: 0, height: 0 });
109
+ /**
110
+ * Effect per aggiornare le dimensioni della bubble quando cambia il contenuto
111
+ * o lo stato di collassamento. Necessario per calcolare correttamente i limiti
112
+ * di trascinamento.
113
+ */
114
+ useEffect(() => {
115
+ if (bubbleRef.current) {
116
+ const rect = bubbleRef.current.getBoundingClientRect();
117
+ setBubbleSize({ width: rect.width, height: rect.height });
118
+ }
119
+ }, [content, isCollapsed]);
120
+ /**
121
+ * Effect per resettare la posizione quando il parent o la finestra vengono ridimensionati.
122
+ * Questo previene che il componente finisca fuori dai bordi dopo un resize.
123
+ */
124
+ useEffect(() => {
125
+ if (!buttonRef.current)
126
+ return;
127
+ const parent = buttonRef.current.offsetParent;
128
+ if (!parent)
129
+ return;
130
+ // Funzione per verificare e aggiustare la posizione dopo un resize
131
+ const handleResize = () => {
132
+ if (!buttonRef.current || !position)
133
+ return;
134
+ const parentRect = parent.getBoundingClientRect();
135
+ const rect = buttonRef.current.getBoundingClientRect();
136
+ // Spazio extra occupato dalla bubble quando non è collassato
137
+ const extraHeight = !isCollapsed ? bubbleSize.height : 0;
138
+ const extraWidth = !isCollapsed ? bubbleSize.width : 0;
139
+ // Calcola i nuovi limiti
140
+ let minX = 0;
141
+ let maxX = parentRect.width - rect.width;
142
+ if (!isCollapsed) {
143
+ if (align === 'right') {
144
+ minX = Math.max(0, extraWidth - rect.width);
145
+ }
146
+ else {
147
+ maxX = Math.min(maxX, parentRect.width - extraWidth);
148
+ }
149
+ }
150
+ const maxY = parentRect.height - rect.height;
151
+ // Verifica se la posizione corrente è fuori dai limiti
152
+ const isOutOfBounds = position.x < minX ||
153
+ position.x > maxX ||
154
+ position.y < extraHeight ||
155
+ position.y > maxY;
156
+ // Se è fuori dai limiti, resetta alla posizione default
157
+ if (isOutOfBounds) {
158
+ setPosition(null);
159
+ }
160
+ };
161
+ // Observer per monitorare i cambiamenti di dimensione del parent
162
+ const resizeObserver = new ResizeObserver(handleResize);
163
+ resizeObserver.observe(parent);
164
+ // Listener per il resize della finestra
165
+ window.addEventListener('resize', handleResize);
166
+ return () => {
167
+ resizeObserver.disconnect();
168
+ window.removeEventListener('resize', handleResize);
169
+ };
170
+ }, [position, isCollapsed, bubbleSize, align]);
171
+ /**
172
+ * Effect per impostare automaticamente lo stato collassato su dispositivi mobile
173
+ * se non è stato specificato un valore iniziale.
174
+ */
175
+ useEffect(() => {
176
+ if (initialIsCollapsed === undefined && deviceType === DeviceType.MOBILE) {
177
+ setIsCollapsed(true);
178
+ }
179
+ }, [deviceType, initialIsCollapsed]);
180
+ /**
181
+ * Gestisce il toggle dello stato collassato/espanso
182
+ * Chiamato dal doppio click sul componente
183
+ */
184
+ const toggleCollapse = (e) => {
185
+ e.stopPropagation();
186
+ setIsCollapsed(!isCollapsed);
187
+ onToppyImageClick?.();
188
+ };
189
+ /**
190
+ * Gestisce l'inizio del trascinamento
191
+ * Salva la posizione relativa del mouse rispetto al componente (offset)
192
+ * per mantenere il punto di presa durante il trascinamento
193
+ */
194
+ const handleMouseDown = (e) => {
195
+ if (!buttonRef.current)
196
+ return;
197
+ const rect = buttonRef.current.getBoundingClientRect();
198
+ const parentRect = buttonRef.current.offsetParent?.getBoundingClientRect();
199
+ if (!parentRect)
200
+ return;
201
+ // Calcola l'offset tra il punto di click e l'angolo superiore sinistro del componente
202
+ dragOffset.current = {
203
+ x: e.clientX - rect.left,
204
+ y: e.clientY - rect.top,
205
+ };
206
+ setIsDragging(true);
207
+ e.preventDefault();
208
+ };
209
+ /**
210
+ * Gestisce il movimento durante il trascinamento
211
+ * Calcola la nuova posizione rispettando i limiti del parent container
212
+ * e tenendo conto delle dimensioni della speech bubble quando espansa
213
+ */
214
+ const handleMouseMove = (e) => {
215
+ if (!isDragging || !buttonRef.current)
216
+ return;
217
+ const parentRect = buttonRef.current.offsetParent?.getBoundingClientRect();
218
+ if (!parentRect)
219
+ return;
220
+ const rect = buttonRef.current.getBoundingClientRect();
221
+ // Spazio extra occupato dalla bubble quando non è collassato
222
+ const extraHeight = !isCollapsed ? bubbleSize.height : 0;
223
+ const extraWidth = !isCollapsed ? bubbleSize.width : 0;
224
+ // Calcola i limiti orizzontali considerando la bubble
225
+ let minX = 0;
226
+ let maxX = parentRect.width - rect.width;
227
+ if (!isCollapsed) {
228
+ if (align === 'right') {
229
+ // La bubble si estende verso sinistra: aumenta il limite minimo
230
+ // per evitare che la bubble esca dal bordo sinistro
231
+ minX = Math.max(0, extraWidth - rect.width);
232
+ }
233
+ else {
234
+ // La bubble si estende verso destra: riduce il limite massimo
235
+ // per evitare che la bubble esca dal bordo destro
236
+ maxX = Math.min(maxX, parentRect.width - extraWidth);
237
+ }
238
+ }
239
+ // Calcola la nuova posizione X rispettando i limiti
240
+ const newX = Math.max(minX, Math.min(e.clientX - parentRect.left - dragOffset.current.x, maxX));
241
+ // Calcola la nuova posizione Y rispettando i limiti
242
+ // Il limite superiore tiene conto dell'altezza della bubble
243
+ const newY = Math.max(extraHeight, Math.min(e.clientY - parentRect.top - dragOffset.current.y, parentRect.height - rect.height));
244
+ setPosition({ x: newX, y: newY });
245
+ };
246
+ /**
247
+ * Gestisce il rilascio del mouse alla fine del trascinamento
248
+ * Se il movimento è stato minimo (< 5px), viene interpretato come un click
249
+ * e viene chiamato il callback onToppyImageClick
250
+ */
251
+ const handleMouseUp = (e) => {
252
+ if (isDragging) {
253
+ setIsDragging(false);
254
+ const rect = buttonRef.current?.getBoundingClientRect();
255
+ if (rect) {
256
+ // Calcola la distanza totale del movimento usando il teorema di Pitagora
257
+ const moveDistance = Math.hypot(e.clientX - (rect.left + dragOffset.current.x), e.clientY - (rect.top + dragOffset.current.y));
258
+ // Se il movimento è stato minimo, trattalo come un click
259
+ if (moveDistance < 5 && onToppyImageClick) {
260
+ onToppyImageClick();
261
+ }
262
+ }
263
+ }
264
+ };
265
+ /**
266
+ * Effect per gestire gli event listener durante il trascinamento
267
+ * Gli eventi sono registrati sul document per catturare il movimento
268
+ * anche quando il mouse esce dal componente
269
+ */
270
+ useEffect(() => {
271
+ if (isDragging) {
272
+ document.addEventListener('mousemove', handleMouseMove);
273
+ document.addEventListener('mouseup', handleMouseUp);
274
+ return () => {
275
+ document.removeEventListener('mousemove', handleMouseMove);
276
+ document.removeEventListener('mouseup', handleMouseUp);
277
+ };
278
+ }
279
+ return undefined;
280
+ }, [isDragging]);
281
+ return (_jsxs(ToppyButton, { ref: buttonRef, "$align": align, "$isDragging": isDragging, "$x": position?.x, "$y": position?.y, "$isVisible": isVisible, "$isCollapsed": isCollapsed, onMouseDown: handleMouseDown, onContextMenu: (e) => e.preventDefault(), onDoubleClick: toggleCollapse, children: [(content && !isCollapsed) && (_jsx(ToppySpeechBubble, { ref: bubbleRef, align: align, children: content })), _jsx("img", { src: Toppy, alt: "Toppy Help", draggable: false })] }));
282
+ };
283
+ export default ToppyDraggableHelpCenter;
@@ -162,7 +162,7 @@ const ToppyHelpCenter = ({ content, deviceType, usePortal = true, align = 'right
162
162
  onToppyImageClick?.();
163
163
  };
164
164
  const isMobile = deviceType === DeviceType.MOBILE;
165
- const toppyComponent = (_jsxs(ToppyContainer, { "$isMobile": isMobile, "$isCollapsed": isCollapsed, "$fixed": usePortal, "$align": align, children: [_jsx(ToppyImage, { "$isMobile": isMobile, "$isCollapsed": isCollapsed, "$align": align, onClick: toggleCollapse, src: Toppy, alt: "Toppy" }), _jsx(ToppyContent, { "$isCollapsed": isCollapsed, "$isMobile": isMobile, "$align": align, children: content })] }));
165
+ const toppyComponent = (_jsxs(ToppyContainer, { "$isMobile": isMobile, "$isCollapsed": isCollapsed, "$fixed": usePortal, "$align": align, onContextMenu: (e) => e.preventDefault(), children: [_jsx(ToppyImage, { "$isMobile": isMobile, "$isCollapsed": isCollapsed, "$align": align, onClick: toggleCollapse, src: Toppy, alt: "Toppy" }), _jsx(ToppyContent, { "$isCollapsed": isCollapsed, "$isMobile": isMobile, "$align": align, children: content })] }));
166
166
  if (usePortal) {
167
167
  if (!portalContainer)
168
168
  return null;
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ interface ToppySpeechBubbleProps {
3
+ align?: 'left' | 'right';
4
+ children: React.ReactNode;
5
+ className?: string;
6
+ }
7
+ declare const ToppySpeechBubble: React.ForwardRefExoticComponent<ToppySpeechBubbleProps & React.RefAttributes<HTMLDivElement>>;
8
+ export default ToppySpeechBubble;
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef } from 'react';
3
+ import styled from 'styled-components';
4
+ // Styled component
5
+ const Bubble = styled.div `
6
+ position: absolute;
7
+ bottom: 145px;
8
+ ${({ $align }) => ($align === 'left' ? 'left: 0px;' : 'right: 0px;')}
9
+ width: max-content;
10
+ padding: 10px;
11
+ background: linear-gradient(180deg, rgba(0, 113, 188, 0.45) 0%,rgba(27, 20, 100, 0.65) 100%);
12
+ border-radius: 18px;
13
+ box-shadow: 0 10px 28px rgba(0, 0, 0, 0.18);
14
+ font-size: 14px;
15
+ line-height: 1.4;
16
+ color: #333;
17
+ z-index: 10000;
18
+
19
+ &::after {
20
+ transform: ${({ $align }) => ($align === 'left' ? 'skewX(15deg)' : 'skewX(-15deg)')};
21
+ content: "";
22
+ position: absolute;
23
+ top: 100%;
24
+ ${({ $align }) => ($align === 'left' ? 'left: 20px;' : 'right: 15px;')}
25
+ border-width: 32px 32px 0 0;
26
+ border-style: solid;
27
+ border-color: #FFFFFF transparent;
28
+ display: block;
29
+ width: 0;
30
+ height: 0;
31
+ z-index: 1;
32
+ }
33
+
34
+ &::before {
35
+ transform: ${({ $align }) => ($align === 'left' ? 'skewX(15deg)' : 'skewX(-15deg)')};
36
+ content: "";
37
+ position: absolute;
38
+ top: 100%;
39
+ ${({ $align }) => ($align === 'left' ? 'left: 20px;' : 'right: 15px;')}
40
+ border-width: 32px 32px 0 0;
41
+ border-style: solid;
42
+ border-color: rgba(27, 20, 100, 0.65) transparent;
43
+ display: block;
44
+ width: 0;
45
+ height: 0;
46
+ z-index: 2;
47
+ }
48
+ `;
49
+ // Componente con forwardRef
50
+ const ToppySpeechBubble = forwardRef(({ align = 'right', children, className }, ref) => {
51
+ return (_jsx(Bubble, { ref: ref, "$align": align, className: className, children: children }));
52
+ });
53
+ export default ToppySpeechBubble;
@@ -67,6 +67,8 @@ export declare class SDKUI_Localizator {
67
67
  static get AutoAdjust(): "Automatische Anpassung" | "Auto Adjust" | "Ajuste automático" | "Ajustement automatique" | "Regolazione automatica";
68
68
  static get Author(): string;
69
69
  static get CustomButtons(): string;
70
+ static get CustomButtonAction(): string;
71
+ static get CustomButtonActions(): string;
70
72
  static get Back(): "Zurück" | "Back" | "Atrás" | "Dos" | "Voltar" | "Indietro";
71
73
  static get BatchUpdate(): "Mehrfachbearbeitung" | "Multiple modification" | "Modificación múltiple" | "Modifie multiple" | "Editar múltipla" | "Modifica multipla";
72
74
  static get BlogCase(): "Anschlagbrett" | "Blog board" | "Tablón" | "Tableau d'affichage" | "Bakeca" | "Bacheca";
@@ -625,6 +625,26 @@ export class SDKUI_Localizator {
625
625
  default: return "Bottoni personalizzati";
626
626
  }
627
627
  }
628
+ static get CustomButtonAction() {
629
+ switch (this._cultureID) {
630
+ case CultureIDs.De_DE: return "Vorgang läuft";
631
+ case CultureIDs.En_US: return "Operation in progress";
632
+ case CultureIDs.Es_ES: return "Operación en curso";
633
+ case CultureIDs.Fr_FR: return "Opération en cours";
634
+ case CultureIDs.Pt_PT: return "Operação em curso";
635
+ default: return "Operazione in corso";
636
+ }
637
+ }
638
+ static get CustomButtonActions() {
639
+ switch (this._cultureID) {
640
+ case CultureIDs.De_DE: return "Aktion {{0}} von {{1}} wird ausgeführt";
641
+ case CultureIDs.En_US: return "Executing action {{0}} of {{1}}";
642
+ case CultureIDs.Es_ES: return "Ejecutando acción {{0}} de {{1}}";
643
+ case CultureIDs.Fr_FR: return "Exécution de l'action {{0}} sur {{1}}";
644
+ case CultureIDs.Pt_PT: return "Executando ação {{0}} de {{1}}";
645
+ default: return "Esecuzione azione {{0}} di {{1}}";
646
+ }
647
+ }
628
648
  static get Back() {
629
649
  switch (this._cultureID) {
630
650
  case CultureIDs.De_DE: return "Zurück";
@@ -4,4 +4,5 @@ export declare const hasDetailRelations: (mTID: number | undefined) => Promise<b
4
4
  /** Check if dcmtType (mTID) has configured Master or Many-to-Many relations */
5
5
  export declare const hasMasterRelations: (mTID: number | undefined) => Promise<boolean>;
6
6
  export declare const isXMLFileExt: (fileExt: string | undefined) => boolean;
7
- export declare const processButtonAttributes: (args: string | undefined, formData: MetadataValueDescriptorEx[] | undefined, selectedItems: Array<any> | undefined) => Record<string, any> | undefined;
7
+ export declare const getButtonAttributes: (args: string | undefined, formData: MetadataValueDescriptorEx[] | undefined, selectedItems: Array<any> | undefined) => Record<string, any> | undefined;
8
+ export declare const getSelectedItem: (args: string | undefined, formData: MetadataValueDescriptorEx[] | undefined, item: any) => Record<string, any> | undefined;
@@ -24,67 +24,58 @@ export const isXMLFileExt = (fileExt) => {
24
24
  }
25
25
  };
26
26
  /*utility functions for TMCustomButton*/
27
- export const processButtonAttributes = (args, formData, selectedItems) => args && formData ? formDataMap(formData, args, selectedItems) : undefined;
28
- const processSelectedItems = (selectedItems) => selectedItems && selectedItems.map(item => item["DID"]) || [];
27
+ export const getButtonAttributes = (args, formData, selectedItems) => args && formData ? formDataMap(formData, args, selectedItems) : undefined;
28
+ const getSelectedItems = (selectedItems) => selectedItems && selectedItems.map(item => item["DID"]) || [];
29
+ export const getSelectedItem = (args, formData, item) => {
30
+ //converto item in formData
31
+ const formDataConverted = [];
32
+ for (const key in item) {
33
+ const md = formData?.find(md => `${item["TID"]}_${md.mid}` === key);
34
+ if (md) { // aggiungo solo i metadati
35
+ const name = md.md?.name || "";
36
+ formDataConverted.push({ md: { name: name }, value: item[key] });
37
+ }
38
+ }
39
+ return args && formDataConverted ?
40
+ formDataMap(formDataConverted, args, []) : undefined;
41
+ };
29
42
  const formDataMap = (data, args, selectedItems) => {
30
43
  const session = SDK_Globals.tmSession;
31
44
  const sessionDescr = session?.SessionDescr;
32
45
  const result = {};
33
- // Regex per catturare: chiave=[{@campo} ...] o chiave={valore} o {@campo}
34
- const keyValueRegex = /(\w+)=\[([^\]]+)\]|(\w+)=\{([^}]+)\}|\{@([^}]+)\}/g;
35
- let match;
36
- while ((match = keyValueRegex.exec(args)) !== null) {
46
+ // Helper per estrarre il valore di un campo
47
+ const getParamValue = (fieldName) => {
48
+ const md = data.find(md => md.md?.name === fieldName);
49
+ switch (fieldName) {
50
+ case 'SelectedDIDs': return getSelectedItems(selectedItems);
51
+ case 'AuthenticationMode': return sessionDescr?.authenticationMode ?? null;
52
+ case 'ArchiveID': return sessionDescr?.archiveID ?? null;
53
+ case 'CultureID': return sessionDescr?.cultureID ?? null;
54
+ case 'Domain': return sessionDescr?.domain ?? null;
55
+ case 'UserID': return sessionDescr?.userID ?? null;
56
+ case 'UserName': return sessionDescr?.userName ?? null;
57
+ case 'Session': return session ?? null;
58
+ default: return md?.value;
59
+ }
60
+ };
61
+ // Regex per catturare: chiave=[...] o chiave={...} o {@campo}
62
+ const keyValueRegex = /(\w+)=\[([^\]]+)\]|(\w+)=\{@?([^}]+)\}|\{@([^}]+)\}/g;
63
+ for (const match of args.matchAll(keyValueRegex)) {
37
64
  if (match[1]) {
38
- // Formato: chiave=[{@campo} {@campo} ...]
65
+ // Formato: chiave=[{@campo} testo {@campo} ...]
39
66
  const key = match[1];
40
- const content = match[2];
41
- // Estrai tutti i {@campo} e concatenali
42
- const fields = content.match(/\{@([^}]+)\}/g) || [];
43
- const values = fields.map(field => {
44
- const fieldName = field.slice(2, -1);
45
- const md = data.find(md => md.md?.name === fieldName);
46
- return md?.value ?? '';
47
- }).filter(v => v);
48
- result[key] = values.join(' ');
67
+ const content = match[2].replace(/\{@([^}]+)\}/g, (_, fieldName) => data.find(md => md.md?.name === fieldName)?.value ?? '');
68
+ result[key] = content;
49
69
  }
50
70
  else if (match[3]) {
51
- // Formato: chiave={valore}
71
+ // Formato: chiave={@campo} o chiave={valore}
52
72
  const key = match[3];
53
73
  const value = match[4];
54
- result[key] = value;
74
+ result[key] = value.startsWith('@') ? getParamValue(value.substring(1)) : value;
55
75
  }
56
76
  else if (match[5]) {
57
77
  // Formato: {@campo}
58
- const fieldName = match[5];
59
- const md = data.find(md => md.md?.name === fieldName);
60
- switch (fieldName) {
61
- case 'SelectedDIDs':
62
- result[fieldName] = processSelectedItems(selectedItems);
63
- break;
64
- case 'AuthenticationMode':
65
- result[fieldName] = sessionDescr?.authenticationMode ?? null;
66
- break;
67
- case 'ArchiveID':
68
- result[fieldName] = sessionDescr?.archiveID ?? null;
69
- break;
70
- case 'CultureID':
71
- result[fieldName] = sessionDescr?.cultureID ?? null;
72
- break;
73
- case 'Domain':
74
- result[fieldName] = sessionDescr?.domain ?? null;
75
- break;
76
- case 'UserID':
77
- result[fieldName] = sessionDescr?.userID ?? null;
78
- break;
79
- case 'UserName':
80
- result[fieldName] = sessionDescr?.userName ?? null;
81
- break;
82
- case 'Session':
83
- result[fieldName] = session ?? null;
84
- break;
85
- default:
86
- result[fieldName] = md?.value;
87
- }
78
+ result[match[5]] = getParamValue(match[5]);
88
79
  }
89
80
  }
90
81
  return result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topconsultnpm/sdkui-react",
3
- "version": "6.19.0-dev2.41",
3
+ "version": "6.19.0-dev2.43",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",