@topconsultnpm/sdkui-react 6.22.0-dev2.3 → 6.22.0-dev2.5

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.
@@ -3,6 +3,17 @@ import { ITMEditorBase } from './TMEditorBase';
3
3
  export type ButtonColors = 'error' | 'warning' | 'success' | 'info' | 'primary' | 'secondary' | 'tertiary' | 'standard' | 'primaryOutline' | 'secondaryOutline' | 'tertiaryOutline' | 'errorOutline' | 'warningOutline' | 'successOutline' | 'infoOutline';
4
4
  export type ButtonStyle = 'normal' | 'toolbar' | 'icon' | 'text' | 'advanced';
5
5
  export type AdvancedButtonType = 'success' | 'error' | 'tertiary' | 'primary';
6
+ /** Azione secondaria mostrata nel dropdown "split button" (freccia a destra) */
7
+ export interface ITMButtonSecondaryAction {
8
+ /** Etichetta mostrata nella voce di menu */
9
+ name: string;
10
+ /** Icona opzionale a sinistra della voce */
11
+ icon?: React.ReactNode;
12
+ /** Callback al click della voce */
13
+ onClick?: () => void;
14
+ /** Disabilita la singola voce */
15
+ disabled?: boolean;
16
+ }
6
17
  export interface ITMButton extends ITMEditorBase {
7
18
  color?: ButtonColors;
8
19
  btnStyle?: ButtonStyle;
@@ -17,6 +28,8 @@ export interface ITMButton extends ITMEditorBase {
17
28
  showTooltip?: boolean;
18
29
  onClick?: VoidFunction;
19
30
  onMouseDown?: (e: any) => any;
31
+ /** Azioni secondarie: se valorizzate, il bottone mostra una freccia a destra che apre un dropdown con queste azioni */
32
+ secondaryActions?: ITMButtonSecondaryAction[];
20
33
  }
21
34
  declare const TMButton: React.ForwardRefExoticComponent<ITMButton & React.RefAttributes<HTMLButtonElement>>;
22
35
  export default TMButton;
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useState, forwardRef } from 'react';
2
+ import { useState, forwardRef, useRef, useEffect } from 'react';
3
+ import { createPortal } from 'react-dom';
3
4
  import styled from 'styled-components';
4
- import { getColor } from '../../helper';
5
+ import { getColor, IconChevronDown } from '../../helper';
5
6
  import { FontSize, TMColors } from '../../utils/theme';
6
7
  import TMTooltip from './TMTooltip';
7
8
  const StyledNormalButton = styled.button.withConfig({ shouldForwardProp: prop => !['text'].includes(prop) }) `
@@ -149,10 +150,132 @@ const StyledAdvancedButtonText = styled.div `
149
150
  border-top-right-radius: 10px;
150
151
  border-bottom-right-radius: 10px;
151
152
  padding: 5px;
153
+ padding-right: ${props => props.$reserveArrow ? '26px' : '5px'};
154
+ `;
155
+ const StyledSplitContainer = styled.div `
156
+ display: inline-flex;
157
+ align-items: stretch;
158
+ position: relative;
159
+ `;
160
+ const StyledSplitArrow = styled.button `
161
+ position: absolute;
162
+ right: 4px;
163
+ top: 50%;
164
+ transform: translateY(-50%);
165
+ z-index: 2;
166
+ display: flex;
167
+ align-items: center;
168
+ justify-content: center;
169
+ width: 20px;
170
+ height: 20px;
171
+ padding: 0;
172
+ border: none;
173
+ border-radius: 6px;
174
+ cursor: ${props => props.$isDisabled ? 'default' : 'pointer'};
175
+ color: white;
176
+ background-color: rgba(255, 255, 255, 0.22);
177
+ transition: background-color ease 150ms;
178
+ &:hover { background-color: ${props => props.$isDisabled ? 'rgba(255, 255, 255, 0.22)' : 'rgba(255, 255, 255, 0.38)'}; }
179
+ & svg {
180
+ transition: transform ease 150ms;
181
+ transform: rotate(${props => props.$isOpen ? '180deg' : '0deg'});
182
+ }
183
+ `;
184
+ const StyledDropdown = styled.div `
185
+ position: fixed;
186
+ left: ${props => props.$left}px;
187
+ top: ${props => props.$top}px;
188
+ transform: translateY(-100%);
189
+ transform-origin: bottom left;
190
+ min-width: ${props => props.$minWidth}px;
191
+ background: #ffffff;
192
+ border-radius: 14px;
193
+ box-shadow: 0 10px 30px rgba(20, 40, 80, 0.20);
194
+ border: 1px solid rgba(74, 150, 210, 0.18);
195
+ padding: 5px;
196
+ z-index: 100000;
197
+ display: flex;
198
+ flex-direction: column;
199
+ gap: 2px;
200
+ animation: tmSplitDropdownIn cubic-bezier(0.2, 0.8, 0.2, 1) 150ms;
201
+ @keyframes tmSplitDropdownIn {
202
+ from { opacity: 0; transform: translateY(calc(-100% + 8px)); }
203
+ to { opacity: 1; transform: translateY(-100%); }
204
+ }
205
+ `;
206
+ const StyledDropdownItem = styled.button `
207
+ display: flex;
208
+ align-items: center;
209
+ gap: 9px;
210
+ width: 100%;
211
+ padding: 5px 9px;
212
+ border: none;
213
+ border-radius: 8px;
214
+ background: transparent;
215
+ text-align: left;
216
+ font-size: ${FontSize.defaultFontSize};
217
+ font-weight: 500;
218
+ color: ${props => props.$disabled ? '#b4b4b4' : '#2c3e50'};
219
+ cursor: ${props => props.$disabled ? 'default' : 'pointer'};
220
+ transition: background ease 150ms, color ease 150ms;
221
+ &:hover {
222
+ background: ${props => props.$disabled ? 'transparent' : 'linear-gradient(135deg, rgba(74, 150, 210, 0.16) 0%, rgba(37, 89, 165, 0.16) 100%)'};
223
+ color: ${props => props.$disabled ? '#b4b4b4' : '#2559A5'};
224
+ }
225
+ & > .tm-split-item-icon {
226
+ display: flex;
227
+ align-items: center;
228
+ justify-content: center;
229
+ width: 22px;
230
+ height: 22px;
231
+ flex-shrink: 0;
232
+ border-radius: 6px;
233
+ background: rgba(74, 150, 210, 0.14);
234
+ color: #4A96D2;
235
+ transition: background ease 150ms, color ease 150ms;
236
+ }
237
+ &:hover > .tm-split-item-icon {
238
+ background: ${props => props.$disabled ? 'rgba(74, 150, 210, 0.14)' : '#4A96D2'};
239
+ color: ${props => props.$disabled ? '#4A96D2' : '#ffffff'};
240
+ }
152
241
  `;
153
242
  const TMButton = forwardRef((props, ref) => {
154
- const { width, height, keyGesture, btnStyle = 'normal', advancedColor, advancedType = 'primary', color = 'primary', fontSize = FontSize.defaultFontSize, disabled = false, showTooltip = true, caption, icon, description, padding, elementStyle, onClick = () => { }, onMouseDown = () => { } } = props;
243
+ const { width, height, keyGesture, btnStyle = 'normal', advancedColor, advancedType = 'primary', color = 'primary', fontSize = FontSize.defaultFontSize, disabled = false, showTooltip = true, caption, icon, description, padding, elementStyle, onClick = () => { }, onMouseDown = () => { }, secondaryActions } = props;
155
244
  const [isHovered, setIsHovered] = useState(false);
245
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
246
+ const [dropdownPos, setDropdownPos] = useState({ left: 0, top: 0, minWidth: 200 });
247
+ const splitContainerRef = useRef(null);
248
+ const dropdownRef = useRef(null);
249
+ const hasSecondaryActions = !!secondaryActions && secondaryActions.length > 0;
250
+ useEffect(() => {
251
+ if (!isDropdownOpen)
252
+ return;
253
+ const handlePointerDown = (e) => {
254
+ const target = e.target;
255
+ if (splitContainerRef.current?.contains(target) || dropdownRef.current?.contains(target))
256
+ return;
257
+ setIsDropdownOpen(false);
258
+ };
259
+ const handleKeyDown = (e) => { if (e.key === 'Escape')
260
+ setIsDropdownOpen(false); };
261
+ document.addEventListener('mousedown', handlePointerDown);
262
+ document.addEventListener('touchstart', handlePointerDown);
263
+ document.addEventListener('keydown', handleKeyDown);
264
+ return () => {
265
+ document.removeEventListener('mousedown', handlePointerDown);
266
+ document.removeEventListener('touchstart', handlePointerDown);
267
+ document.removeEventListener('keydown', handleKeyDown);
268
+ };
269
+ }, [isDropdownOpen]);
270
+ const toggleDropdown = () => {
271
+ if (disabled)
272
+ return;
273
+ if (!isDropdownOpen && splitContainerRef.current) {
274
+ const rect = splitContainerRef.current.getBoundingClientRect();
275
+ setDropdownPos({ left: rect.left, top: rect.top - 6, minWidth: Math.max(rect.width, 200) });
276
+ }
277
+ setIsDropdownOpen(prev => !prev);
278
+ };
156
279
  const renderedButton = () => {
157
280
  if (btnStyle === 'normal') {
158
281
  return (_jsx(StyledNormalButton, { ref: ref, width: width, height: height, color: color, caption: caption, disabled: disabled, fontSize: fontSize, onClick: onClick, onMouseDown: (e) => !disabled && onMouseDown?.(e), children: caption }));
@@ -164,7 +287,7 @@ const TMButton = forwardRef((props, ref) => {
164
287
  return (_jsx(StyledIconButton, { ref: ref, color: color, caption: caption, disabled: disabled, fontSize: fontSize, padding: padding, onClick: onClick, onMouseDown: (e) => !disabled && onMouseDown?.(e), children: icon }));
165
288
  }
166
289
  else if (btnStyle === 'advanced') {
167
- return (_jsxs(StyledAdvancedButton, { ref: ref, "$width": width ?? '90px', "$height": height ?? '30px', onMouseOver: () => !disabled && setIsHovered(true), onMouseOut: () => setIsHovered(false), "$isDisabled": disabled, "$isPrimaryOutline": advancedType === 'primary' && (advancedColor === 'primaryOutline' || color === 'primaryOutline'), onClick: () => !disabled && onClick?.(), onMouseDown: (e) => !disabled && onMouseDown?.(e), children: [_jsx(StyledAdvancedButtonIcon, { "$color": advancedColor, "$isVisible": isHovered, "$isDisabled": disabled, "$type": advancedType, children: icon }), _jsx(StyledAdvancedButtonText, { "$color": advancedColor, "$isDisabled": disabled, "$type": advancedType, children: caption })] }));
290
+ return (_jsxs(StyledAdvancedButton, { ref: ref, "$width": width ?? '90px', "$height": height ?? '30px', onMouseOver: () => !disabled && setIsHovered(true), onMouseOut: () => setIsHovered(false), "$isDisabled": disabled, "$isPrimaryOutline": advancedType === 'primary' && (advancedColor === 'primaryOutline' || color === 'primaryOutline'), onClick: () => !disabled && onClick?.(), onMouseDown: (e) => !disabled && onMouseDown?.(e), children: [_jsx(StyledAdvancedButtonIcon, { "$color": advancedColor, "$isVisible": isHovered, "$isDisabled": disabled, "$type": advancedType, children: icon }), _jsx(StyledAdvancedButtonText, { "$color": advancedColor, "$isDisabled": disabled, "$type": advancedType, "$reserveArrow": hasSecondaryActions, children: caption })] }));
168
291
  }
169
292
  else {
170
293
  return (_jsx(StyledTextButton, { ref: ref, color: color, caption: caption, disabled: disabled, fontSize: fontSize, onClick: onClick, onMouseDown: (e) => !disabled && onMouseDown?.(e), children: caption }));
@@ -175,6 +298,15 @@ const TMButton = forwardRef((props, ref) => {
175
298
  _jsxs(_Fragment, { children: [caption &&
176
299
  _jsx(StyledButtonTooltipHeader, { "$description": description, children: _jsx(StyledButtonTooltipItem, { "$color": color, children: caption }) }), description && _jsx(StyledButtonTooltipItem, { children: description })] }));
177
300
  };
178
- return (_jsx("div", { style: elementStyle, children: showTooltip ? _jsx(TMTooltip, { hideAfterDelay: true, content: renderTooltip(), children: renderedButton() }) : renderedButton() }));
301
+ const buttonWithTooltip = showTooltip
302
+ ? _jsx(TMTooltip, { hideAfterDelay: true, content: renderTooltip(), children: renderedButton() })
303
+ : renderedButton();
304
+ const renderSplitButton = () => (_jsxs(StyledSplitContainer, { ref: splitContainerRef, children: [buttonWithTooltip, _jsx(StyledSplitArrow, { type: 'button', "$isDisabled": disabled, "$isOpen": isDropdownOpen, "aria-haspopup": 'menu', "aria-expanded": isDropdownOpen, onClick: (e) => { e.stopPropagation(); toggleDropdown(); }, onMouseDown: (e) => e.stopPropagation(), children: _jsx(IconChevronDown, { fontSize: 14 }) }), isDropdownOpen && createPortal(_jsx(StyledDropdown, { ref: dropdownRef, role: 'menu', "$left": dropdownPos.left, "$top": dropdownPos.top, "$minWidth": dropdownPos.minWidth, children: secondaryActions.map((action, idx) => (_jsxs(StyledDropdownItem, { type: 'button', role: 'menuitem', "$disabled": action.disabled, disabled: action.disabled, onClick: () => {
305
+ if (action.disabled)
306
+ return;
307
+ setIsDropdownOpen(false);
308
+ action.onClick?.();
309
+ }, children: [action.icon && _jsx("span", { className: 'tm-split-item-icon', children: action.icon }), _jsx("span", { children: action.name })] }, `${action.name}-${idx}`))) }), document.body)] }));
310
+ return (_jsx("div", { style: elementStyle, children: hasSecondaryActions ? renderSplitButton() : buttonWithTooltip }));
179
311
  });
180
312
  export default TMButton;
@@ -5,7 +5,7 @@ import TMSavedQuerySelector from './TMSavedQuerySelector';
5
5
  import TMTreeSelector from './TMTreeSelector';
6
6
  import { TabPanel, Item } from 'devextreme-react/tab-panel';
7
7
  import TMSearchQueryPanel, { refreshLastSearch } from './TMSearchQueryPanel';
8
- import { getSysAllDcmtsSQD, IconFilter, IconRecentlyViewed, IconSavedQuery, IconTree, removeMruTid, SDKUI_Globals, SDKUI_Localizator, updateMruTids } from '../../../helper';
8
+ import { getSysAllDcmtsSQD, IconFilter, IconRecentlyViewed, IconSavedQuery, IconTree, mergeSearchResultsAppend, removeMruTid, SDKUI_Globals, SDKUI_Localizator, updateMruTids } from '../../../helper';
9
9
  import TMSearchResult from './TMSearchResult';
10
10
  import TMRecentsManager from '../../grids/TMRecentsManager';
11
11
  import { SearchResultContext } from '../../../ts';
@@ -164,6 +164,32 @@ const TMSearch = ({ allTasks = [], getAllTasks, deleteTaskByIdsCallback, addTask
164
164
  console.error("Error refreshing search:", error);
165
165
  }
166
166
  };
167
+ const handleSearchCompleted = useCallback((searchResult, qd, append) => {
168
+ if (searchResult.length <= 0) {
169
+ // In ricerca normale un array vuoto resetta i risultati; in accoda si mantiene il set accumulato
170
+ if (!append)
171
+ setSearchResult(searchResult);
172
+ return;
173
+ }
174
+ const newResult = searchResult[0];
175
+ // In modalità accoda unisce il nuovo risultato a quello precedente (più recenti in cima).
176
+ setSearchResult(prev => {
177
+ if (append && prev.length > 0 && prev[0]?.fromTID === newResult?.fromTID) {
178
+ const merged = mergeSearchResultsAppend(prev[0], newResult);
179
+ return merged ? [merged] : searchResult;
180
+ }
181
+ return searchResult;
182
+ });
183
+ setLastQdSearched(qd);
184
+ setCurrentSearchView(TMSearchViews.Result);
185
+ // Salvataggio ultimi 10 TIDs
186
+ let fromTID = searchResult?.[0].fromTID;
187
+ let newMruTIDS = updateMruTids(SDKUI_Globals.userSettings.searchSettings.mruTIDs, fromTID);
188
+ SDKUI_Globals.userSettings.searchSettings.mruTIDs = newMruTIDS;
189
+ setMruTIDs(newMruTIDS);
190
+ setCurrentMruTID(fromTID);
191
+ setShowSearchResults(true);
192
+ }, []);
167
193
  const isMobile = deviceType === DeviceType.MOBILE;
168
194
  const isTabletOrMobile = deviceType === DeviceType.TABLET || deviceType === DeviceType.MOBILE;
169
195
  // --- JSX WRAPPERS ---
@@ -182,23 +208,10 @@ const TMSearch = ({ allTasks = [], getAllTasks, deleteTaskByIdsCallback, addTask
182
208
  SDKUI_Globals.userSettings.searchSettings.mruTIDs = newMruTIDS;
183
209
  setMruTIDs(newMruTIDS);
184
210
  } }), [mruTIDs, currentMruTID, deviceType]);
185
- const tmSearchQueryPanelElement = useMemo(() => _jsx(TMSearchQueryPanelWrapper, { passToArchiveCallback: passToArchiveCallback, isExpertMode: isExpertMode, showBackToResultButton: searchResult.length > 0, fromDTD: fromDTD, SQD: currentSQD, inputMids: inputMids, maxDcmtsToBeReturned: maxDcmtsToBeReturned, onBackToResult: () => { setCurrentSearchView(TMSearchViews.Result); }, onSearchCompleted: (searchResult, qd) => {
186
- setSearchResult(searchResult);
187
- if (searchResult.length <= 0)
188
- return;
189
- setLastQdSearched(qd);
190
- setCurrentSearchView(TMSearchViews.Result);
191
- // Salvataggio ultimi 10 TIDs
192
- let fromTID = searchResult?.[0].fromTID;
193
- let newMruTIDS = updateMruTids(SDKUI_Globals.userSettings.searchSettings.mruTIDs, fromTID);
194
- SDKUI_Globals.userSettings.searchSettings.mruTIDs = newMruTIDS;
195
- setMruTIDs(newMruTIDS);
196
- setCurrentMruTID(fromTID);
197
- setShowSearchResults(true);
198
- }, onSqdSaved: async (newSqd) => {
211
+ const tmSearchQueryPanelElement = useMemo(() => _jsx(TMSearchQueryPanelWrapper, { passToArchiveCallback: passToArchiveCallback, isExpertMode: isExpertMode, showBackToResultButton: searchResult.length > 0, fromDTD: fromDTD, SQD: currentSQD, inputMids: inputMids, maxDcmtsToBeReturned: maxDcmtsToBeReturned, onBackToResult: () => { setCurrentSearchView(TMSearchViews.Result); }, onSearchCompleted: handleSearchCompleted, onSqdSaved: async (newSqd) => {
199
212
  await loadDataSQDsAsync(true, newSqd.masterTID);
200
213
  await setSQDAsync(newSqd);
201
- } }), [fromDTD, showSearchResults, setShowSearchResults, currentSQD, isExpertMode, mruTIDs, searchResult, passToArchiveCallback, inputMids, maxDcmtsToBeReturned]);
214
+ } }), [fromDTD, showSearchResults, setShowSearchResults, currentSQD, isExpertMode, mruTIDs, searchResult, passToArchiveCallback, inputMids, maxDcmtsToBeReturned, handleSearchCompleted]);
202
215
  const tmSavedQuerySelectorElement = useMemo(() => _jsxs(TabPanel, { width: "100%", height: "100%", showNavButtons: true, repaintChangesOnly: true, selectedIndex: currentSQDMode, onSelectedIndexChange: (index) => setCurrentSQDMode(index), children: [(currentTID || currentSQD) ? _jsx(Item, { title: fromDTD?.nameLoc, children: _jsx(TMSavedQuerySelectorWrapper, { allowShowSearch: false, items: filteredByTIDSQDs, selectedId: currentSQD?.id, onRefreshData: () => { loadDataSQDsAsync(true); }, onItemClick: (sqd) => {
203
216
  onSQDItemClick(sqd, setSQDAsync);
204
217
  }, onDeleted: (sqd) => onSQDDeleted(sqd, sqd.id == currentSQD?.id ? filteredByTIDSQDs.find(o => o.id == 1) : currentSQD, setSQDAsync), refreshFavoriteSavedQueries: refreshFavoriteSavedQueries }) }) : _jsx(_Fragment, {}), _jsx(Item, { title: SDKUI_Localizator.AllFemale, children: _jsx(TMSavedQuerySelectorWrapper, { allowShowSearch: true, items: allSQDs, manageDefault: false, onItemClick: (sqd) => {
@@ -12,7 +12,7 @@ interface ITMSearchQueryPanelProps {
12
12
  onBack?: () => void;
13
13
  onBackToResult?: () => void;
14
14
  onSqdSaved?: (newSqd: SavedQueryDescriptor) => void;
15
- onSearchCompleted?: (searchResult: SearchResultDescriptor[], qd: QueryDescriptor | undefined) => void;
15
+ onSearchCompleted?: (searchResult: SearchResultDescriptor[], qd: QueryDescriptor | undefined, append?: boolean) => void;
16
16
  onClosePanel?: () => void;
17
17
  allowMaximize?: boolean;
18
18
  onMaximizePanel?: () => void;
@@ -46,6 +46,8 @@ const TMSearchQueryPanel = ({ fromDTD, showBackToResultButton, isExpertMode = SD
46
46
  let initialMaxItems = deviceType === DeviceType.MOBILE ? 8 : 12;
47
47
  const appliedInputMidsRef = useRef(null);
48
48
  const pendingMidsRef = useRef(null);
49
+ // Flag "accoda": impostato dall'azione secondaria del bottone Ricerca, consumato in executeSearch
50
+ const appendNextSearchRef = useRef(false);
49
51
  useEffect(() => {
50
52
  if (!SQD)
51
53
  return;
@@ -122,7 +124,10 @@ const TMSearchQueryPanel = ({ fromDTD, showBackToResultButton, isExpertMode = SD
122
124
  useEffect(() => {
123
125
  if (shouldSearch && qd) {
124
126
  const executeSearch = async () => {
125
- await searchAsync(qd, showAdvancedSearch);
127
+ // Legge e consuma il flag "accoda" impostato dall'azione secondaria del bottone Ricerca
128
+ const append = appendNextSearchRef.current;
129
+ appendNextSearchRef.current = false;
130
+ await searchAsync(qd, showAdvancedSearch, append);
126
131
  setShouldSearch(false); // Resetta il trigger dopo la ricerca
127
132
  };
128
133
  executeSearch();
@@ -167,8 +172,9 @@ const TMSearchQueryPanel = ({ fromDTD, showBackToResultButton, isExpertMode = SD
167
172
  });
168
173
  setQd({ ...qd, where: newWhere, orderBy: [] });
169
174
  };
170
- const searchAsync = async (qdInput, isAdvancedSearch) => {
171
- onSearchCompleted?.([], undefined); // reset results
175
+ const searchAsync = async (qdInput, isAdvancedSearch, append = false) => {
176
+ if (!append)
177
+ onSearchCompleted?.([], undefined); // reset results (solo in ricerca normale, non in accoda)
172
178
  let searchParams = { isAdvancedSearch: isAdvancedSearch, lastQdParams: lastQdParams, confirmQueryParams: confirmQueryParams, setLastQdParamsCallback: setLastQdParams };
173
179
  let searchResult = await searchByQdAsync(qdInput, searchParams);
174
180
  let dcmtsFound = searchResult?.result?.dcmtsFound ?? 0;
@@ -180,7 +186,7 @@ const TMSearchQueryPanel = ({ fromDTD, showBackToResultButton, isExpertMode = SD
180
186
  results.push(searchResult?.result);
181
187
  // Before notifying onSearchComplete, let's set the panel to NOT active
182
188
  setIsQueryPanelActive(false);
183
- onSearchCompleted?.(results, searchResult?.qd);
189
+ onSearchCompleted?.(results, searchResult?.qd, append);
184
190
  }
185
191
  };
186
192
  const updateIsModalOpen = useCallback((isOpen) => {
@@ -214,9 +220,24 @@ const TMSearchQueryPanel = ({ fromDTD, showBackToResultButton, isExpertMode = SD
214
220
  if (document.activeElement instanceof HTMLElement) {
215
221
  document.activeElement.blur();
216
222
  }
217
- // Attiva il flag di ricerca anche per il click del bottone
223
+ // Ricerca normale: azzera l'eventuale flag accoda e avvia
224
+ appendNextSearchRef.current = false;
218
225
  setShouldSearch(true);
219
226
  }, []);
227
+ const handleSearchAndAppendClick = useCallback(() => {
228
+ // Propagazione blur
229
+ if (document.activeElement instanceof HTMLElement) {
230
+ document.activeElement.blur();
231
+ }
232
+ // Ricerca e accoda: imposta il flag consumato da executeSearch
233
+ appendNextSearchRef.current = true;
234
+ setShouldSearch(true);
235
+ }, []);
236
+ const searchButtonSecondaryActions = useMemo(() => [{
237
+ name: SDKUI_Localizator.SearchAndAppend,
238
+ icon: _jsx(IconSearch, {}),
239
+ onClick: handleSearchAndAppendClick
240
+ }], [handleSearchAndAppendClick]);
220
241
  const handleQdChanged = useCallback((newQd) => {
221
242
  if (!deepCompare(qd, newQd)) {
222
243
  setQd(newQd);
@@ -270,7 +291,7 @@ const TMSearchQueryPanel = ({ fromDTD, showBackToResultButton, isExpertMode = SD
270
291
  }, [qd, fromDTD?.metadata, SQD?.masterTID]);
271
292
  const handleCloseOutputConfig = useCallback(() => setShowOutputConfig(false), []);
272
293
  const contextMenuItems = useMemo(() => [
273
- ...(showBackToResultButton ? [{ icon: _jsx(IconArrowRight, {}), name: "Vai a risultato", onClick: () => { onBackToResult?.(); } }] : []),
294
+ ...(showBackToResultButton ? [{ icon: _jsx(IconArrowRight, {}), name: SDKUI_Localizator.BackToResult, onClick: () => { onBackToResult?.(); } }] : []),
274
295
  { icon: _jsx(IconAddCircleOutline, {}), name: SDKUI_Localizator.SavedQueryNew, beginGroup: showBackToResultButton, onClick: () => { openSqdForm(FormModes.Create); } },
275
296
  { icon: _jsx(IconEdit, {}), name: SDKUI_Localizator.SavedQueryUpdate, disabled: (SQD && SQD.id == 1), onClick: () => { openSqdForm(FormModes.Update); } },
276
297
  { icon: showAdvancedSearch ? _jsx(IconEasy, {}) : _jsx(IconAdvanced, {}), beginGroup: true, name: showAdvancedSearch ? SDKUI_Localizator.Search_Easy : SDKUI_Localizator.Search_Advanced, onClick: () => { changeAdvancedSearchAsync(!showAdvancedSearch); } },
@@ -353,7 +374,7 @@ const TMSearchQueryPanel = ({ fromDTD, showBackToResultButton, isExpertMode = SD
353
374
  alignItems: 'center',
354
375
  gap: '10px',
355
376
  width: '100%'
356
- }, children: [_jsx(TMButton, { btnStyle: 'advanced', icon: _jsx(IconSearch, {}), showTooltip: false, width: '90px', caption: SDKUI_Localizator.Search, advancedColor: '#4A96D2', onClick: handleSearchButtonClick }), _jsx(TMButton, { width: '90px', btnStyle: 'advanced', advancedType: 'primary', showTooltip: false, caption: SDKUI_Localizator.Clear, icon: _jsx(IconClear, {}), advancedColor: 'white', color: 'primaryOutline', onClick: clearFilters }), (!showAdvancedSearch && qd?.where && qd.where.length > initialMaxItems) && (_jsx(TMButton, { width: '120px', btnStyle: isMobile ? 'icon' : 'advanced', advancedColor: TMColors.button_primary, caption: captionText, showTooltip: false, icon: isMobile ? (_jsx("div", { children: _jsx("p", { children: showAllMdWhere ? `-${diff}` : `+${diff}` }) })) : (_jsx("p", { children: showAllMdWhere ? `-${diff}` : `+${diff}` })), onClick: () => setShowAllMdWhere(!showAllMdWhere) }))] }), showFiltersConfig &&
377
+ }, children: [_jsx(TMButton, { btnStyle: 'advanced', icon: _jsx(IconSearch, {}), showTooltip: false, width: '120px', caption: SDKUI_Localizator.Search, advancedColor: '#4A96D2', onClick: handleSearchButtonClick, secondaryActions: searchButtonSecondaryActions }), _jsx(TMButton, { width: '90px', btnStyle: 'advanced', advancedType: 'primary', showTooltip: false, caption: SDKUI_Localizator.Clear, icon: _jsx(IconClear, {}), advancedColor: 'white', color: 'primaryOutline', onClick: clearFilters }), (!showAdvancedSearch && qd?.where && qd.where.length > initialMaxItems) && (_jsx(TMButton, { width: '120px', btnStyle: isMobile ? 'icon' : 'advanced', advancedColor: TMColors.button_primary, caption: captionText, showTooltip: false, icon: isMobile ? (_jsx("div", { children: _jsx("p", { children: showAllMdWhere ? `-${diff}` : `+${diff}` }) })) : (_jsx("p", { children: showAllMdWhere ? `-${diff}` : `+${diff}` })), onClick: () => setShowAllMdWhere(!showAllMdWhere) }))] }), showFiltersConfig &&
357
378
  _jsx(TMMetadataChooserForm, { allowMultipleSelection: true, height: '500px', width: '600px', allowSysMetadata: true, filterMetadata: (o => o.perm?.canSearch === AccessLevels.Yes), qd: qd, selectedIDs: qd?.where?.map((w) => ({ tid: w.tid, mid: w.mid })), onClose: handleCloseFiltersConfig, onChoose: handleChooseFilters }), showOutputConfig &&
358
379
  _jsx(TMMetadataOutputForm, { qd: qd, selectedSelectItems: qd?.select, allowSysMetadata: true, filterMetadata: (o => o.perm?.canView === AccessLevels.Yes || o.perm?.canUpdate === AccessLevels.Yes), onClose: handleCloseOutputConfig, onChoose: (selectItems) => {
359
380
  setQd({ ...qd, select: selectItems });
@@ -76,6 +76,7 @@ export declare class SDKUI_Localizator {
76
76
  static get CustomButtonAction(): string;
77
77
  static get CustomButtonActions(): string;
78
78
  static get Back(): "Zurück" | "Back" | "Atrás" | "Dos" | "Voltar" | "Indietro";
79
+ static get BackToResult(): "Zum Ergebnis" | "Go to result" | "Ir al resultado" | "Aller au résultat" | "Ir para o resultado" | "Vai a risultato";
79
80
  static get BatchUpdate(): "Mehrfachbearbeitung" | "Multiple modification" | "Modificación múltiple" | "Modifie multiple" | "Editar múltipla" | "Modifica multipla";
80
81
  static get BlogCase(): "Anschlagbrett" | "Blog board" | "Tablón" | "Tableau d'affichage" | "Bakeca" | "Bacheca";
81
82
  static get Blog_Read(): "Anzeigebrett lesen" | "Reading blog board" | "Lectura tablón" | "Lire le tableau d'affichage" | "Quadro de avisos leitura" | "Lettura bacheca";
@@ -717,6 +718,7 @@ export declare class SDKUI_Localizator {
717
718
  static get ScanFeatureUnavailableInThisContext(): "Scanfunktionen sind in diesem Kontext nicht verfugbar." | "Scanning features are not available in this context." | "Las funciones de escaneo no estan disponibles en este contexto." | "Les fonctionnalites de numerisation ne sont pas disponibles dans ce contexte." | "Os recursos de digitalizacao nao estao disponiveis neste contexto." | "Funzionalita di scansione non disponibili in questo contesto.";
718
719
  static get Search(): "Auf der Suche nach" | "Search" | "Búsqueda" | "Recherche" | "Pesquisa" | "Ricerca";
719
720
  static get SearchAction(): "Search" | "Suche" | "Buscar" | "Chercher" | "Pesquisar" | "Cerca";
721
+ static get SearchAndAppend(): "Suchen und anhängen" | "Search and append" | "Buscar y añadir" | "Rechercher et ajouter" | "Pesquisar e acrescentar" | "Ricerca e accoda";
720
722
  static get Search_Advanced(): "Erweiterte Suche" | "Advanced search" | "Búsqueda avanzada" | "Recherche avancée" | "Pesquisa Avançada" | "Ricerca avanzata";
721
723
  static get Search_Easy(): "Einfache Suche" | "Easy search" | "Búsqueda fácil" | "Recherche facile" | "Pesquisa fácil" | "Ricerca facilitata";
722
724
  static get Search_EnterValue(): "Geben Sie einen Wert ein, nach dem gesucht werden soll" | "Enter a value to search" | "Introducir un valor para buscar" | "Entrez une valeur à rechercher" | "Digite um valor para pesquisar" | "Inserire un valore da ricercare";
@@ -715,6 +715,16 @@ export class SDKUI_Localizator {
715
715
  default: return "Indietro";
716
716
  }
717
717
  }
718
+ static get BackToResult() {
719
+ switch (this._cultureID) {
720
+ case CultureIDs.De_DE: return "Zum Ergebnis";
721
+ case CultureIDs.En_US: return "Go to result";
722
+ case CultureIDs.Es_ES: return "Ir al resultado";
723
+ case CultureIDs.Fr_FR: return "Aller au résultat";
724
+ case CultureIDs.Pt_PT: return "Ir para o resultado";
725
+ default: return "Vai a risultato";
726
+ }
727
+ }
718
728
  static get BatchUpdate() {
719
729
  switch (this._cultureID) {
720
730
  case CultureIDs.De_DE: return "Mehrfachbearbeitung";
@@ -7149,6 +7159,16 @@ export class SDKUI_Localizator {
7149
7159
  default: return "Cerca";
7150
7160
  }
7151
7161
  }
7162
+ static get SearchAndAppend() {
7163
+ switch (this._cultureID) {
7164
+ case CultureIDs.De_DE: return "Suchen und anhängen";
7165
+ case CultureIDs.En_US: return "Search and append";
7166
+ case CultureIDs.Es_ES: return "Buscar y añadir";
7167
+ case CultureIDs.Fr_FR: return "Rechercher et ajouter";
7168
+ case CultureIDs.Pt_PT: return "Pesquisar e acrescentar";
7169
+ default: return "Ricerca e accoda";
7170
+ }
7171
+ }
7152
7172
  static get Search_Advanced() {
7153
7173
  switch (this._cultureID) {
7154
7174
  case CultureIDs.De_DE: return "Erweiterte Suche";
@@ -35,6 +35,17 @@ export declare const getDataColumnName: (fromTID: number | undefined, dtColumn:
35
35
  */
36
36
  export declare const generateUniqueColumnKeys: (columns: DataColumnDescriptor[] | undefined, fromTID: number | undefined) => string[];
37
37
  export declare const searchResultDescriptorToSimpleArray: (searchResult: SearchResultDescriptor | undefined) => any[] | undefined;
38
+ /**
39
+ * Unisce ("accoda") il risultato di una nuova ricerca a quello precedente, sullo stesso tipo documento.
40
+ * - Le righe della nuova ricerca vengono messe in cima (più recenti in alto)
41
+ * - Le righe duplicate sono accettate: lo stesso documento può comparire più volte
42
+ * - Aggiorna i contatori globali (dcmtsFound / dcmtsReturned) sul totale unito
43
+ *
44
+ * @param previous Risultato attualmente in memoria (può essere undefined al primo giro)
45
+ * @param next Nuovo risultato appena ottenuto
46
+ * @returns Un nuovo SearchResultDescriptor con le righe unite, oppure il singolo risultato disponibile
47
+ */
48
+ export declare const mergeSearchResultsAppend: (previous: SearchResultDescriptor | undefined, next: SearchResultDescriptor | undefined) => SearchResultDescriptor | undefined;
38
49
  export declare const getCompleteMetadataName: (dcmtTypeName: string | undefined, metadataName: string | undefined) => string;
39
50
  export declare const getQueryCountAsync: (qd: QueryDescriptor, showSpinner: boolean) => Promise<void>;
40
51
  export declare function getTIDByMID(mid: number | undefined, defaultTid?: number): number;
@@ -239,6 +239,38 @@ export const searchResultDescriptorToSimpleArray = (searchResult) => {
239
239
  });
240
240
  return result;
241
241
  };
242
+ /**
243
+ * Unisce ("accoda") il risultato di una nuova ricerca a quello precedente, sullo stesso tipo documento.
244
+ * - Le righe della nuova ricerca vengono messe in cima (più recenti in alto)
245
+ * - Le righe duplicate sono accettate: lo stesso documento può comparire più volte
246
+ * - Aggiorna i contatori globali (dcmtsFound / dcmtsReturned) sul totale unito
247
+ *
248
+ * @param previous Risultato attualmente in memoria (può essere undefined al primo giro)
249
+ * @param next Nuovo risultato appena ottenuto
250
+ * @returns Un nuovo SearchResultDescriptor con le righe unite, oppure il singolo risultato disponibile
251
+ */
252
+ export const mergeSearchResultsAppend = (previous, next) => {
253
+ // Se manca uno dei due, non c'è nulla da unire
254
+ if (!next?.dtdResult?.rows?.length)
255
+ return previous;
256
+ if (!previous?.dtdResult?.rows?.length)
257
+ return next;
258
+ // L'accodamento ha senso solo sullo stesso tipo documento (colonne compatibili)
259
+ if (previous.fromTID !== next.fromTID)
260
+ return next;
261
+ const prevRows = previous.dtdResult.rows ?? [];
262
+ const nextRows = next.dtdResult.rows ?? [];
263
+ // Nessuna deduplica: le nuove righe in cima, poi le precedenti (i duplicati sono ammessi)
264
+ const mergedRows = [...nextRows, ...prevRows];
265
+ // Costruisce il nuovo descrittore basandosi sull'ultimo risultato (colonne/qd più recenti), con le righe unite
266
+ const merged = structuredClone(next);
267
+ const mergedDtd = structuredClone(next.dtdResult);
268
+ mergedDtd.rows = mergedRows;
269
+ merged.dtdResult = mergedDtd;
270
+ merged.dcmtsReturned = mergedRows.length;
271
+ merged.dcmtsFound = mergedRows.length;
272
+ return merged;
273
+ };
242
274
  export const getCompleteMetadataName = (dcmtTypeName, metadataName) => `${dcmtTypeName}...${metadataName}`;
243
275
  export const getQueryCountAsync = async (qd, showSpinner) => {
244
276
  try {
package/package.json CHANGED
@@ -1,68 +1,68 @@
1
1
  {
2
- "name": "@topconsultnpm/sdkui-react",
3
- "version": "6.22.0-dev2.3",
4
- "description": "",
5
- "scripts": {
6
- "test": "echo \"Error: no test specified\" \u0026\u0026 exit 1",
7
- "clean": "node -e \"const fs=require(\u0027fs\u0027);fs.rmSync(\u0027lib\u0027,{recursive:true,force:true});fs.mkdirSync(\u0027lib\u0027)\"",
8
- "copy-files": "cpx \"src/{assets/*.*,assets/ImageLibrary/*.*,assets/thumbnails/*.*,assets/IconsS4t/*.*,assets/Metadata/*.*,css/tm-sdkui.css}\" lib",
9
- "tm-build": "npm run clean \u0026\u0026 tsc \u0026\u0026 npm run copy-files",
10
- "tm-watch": "tsc -w",
11
- "tm-publish": "npm publish --tag latest",
12
- "tm-publish_wl": "npm publish",
13
- "storybook": "storybook dev -p 6006",
14
- "build-storybook": "storybook build"
15
- },
16
- "author": "TopConsult",
17
- "license": "ISC",
18
- "devDependencies": {
19
- "@chromatic-com/storybook": "^5.1.2",
20
- "@storybook/addon-docs": "^10.3.5",
21
- "@storybook/addon-onboarding": "^10.3.5",
22
- "@storybook/react-vite": "^10.3.5",
23
- "@types/node": "^24.12.2",
24
- "@types/react": "^18.3.3",
25
- "@types/react-dom": "^18.3.3",
26
- "cpx2": "^9.0.0",
27
- "esbuild": "^0.25.0",
28
- "react": "^18.3.1",
29
- "react-dom": "^18.3.1",
30
- "storybook": "^10.3.5",
31
- "typescript": "^5.9.3",
32
- "vite": "^6.1.1"
33
- },
34
- "main": "dist/cjs/index.js",
35
- "types": "./index.d.ts",
36
- "module": "lib/esm/index.js",
37
- "files": [
38
- "dist",
39
- "lib"
40
- ],
41
- "dependencies": {
42
- "react-pdf": "^10.4.1",
43
- "@zip.js/zip.js": "2.8.26",
44
- "pdf-lib": "^1.17.1",
45
- "devextreme-exceljs-fork": "^4.4.10",
46
- "buffer": "^6.0.3",
47
- "@topconsultnpm/sdk-ts": "6.22.0-t1",
48
- "react-window": "^2.2.7",
49
- "devextreme": "^25.2.6",
50
- "htmlparser2": "^10.0.0",
51
- "styled-components": "^6.1.1",
52
- "devextreme-react": "^25.2.6"
53
- },
54
- "overrides": {
55
- "esbuild": "^0.25.0",
56
- "devextreme-exceljs-fork": {
57
- "archiver": "^8.0.0"
58
- },
59
- "quill-delta": {
60
- "lodash.isequal": "npm:fast-deep-equal@^3.1.3"
61
- }
62
- },
63
- "allowScripts": {
64
- "core-js@3.49.0": true,
65
- "esbuild@0.25.12": true,
66
- "inferno@8.2.3": true
67
- }
2
+ "name": "@topconsultnpm/sdkui-react",
3
+ "version": "6.22.0-dev2.5",
4
+ "description": "",
5
+ "scripts": {
6
+ "test": "echo \"Error: no test specified\" && exit 1",
7
+ "clean": "node -e \"const fs=require('fs');fs.rmSync('lib',{recursive:true,force:true});fs.mkdirSync('lib')\"",
8
+ "copy-files": "cpx \"src/{assets/*.*,assets/ImageLibrary/*.*,assets/thumbnails/*.*,assets/IconsS4t/*.*,assets/Metadata/*.*,css/tm-sdkui.css}\" lib",
9
+ "tm-build": "npm run clean && tsc && npm run copy-files",
10
+ "tm-watch": "tsc -w",
11
+ "tm-publish": "npm publish --tag latest",
12
+ "tm-publish_wl": "npm publish",
13
+ "storybook": "storybook dev -p 6006",
14
+ "build-storybook": "storybook build"
15
+ },
16
+ "author": "TopConsult",
17
+ "license": "ISC",
18
+ "devDependencies": {
19
+ "@chromatic-com/storybook": "^5.1.2",
20
+ "@storybook/addon-docs": "^10.3.5",
21
+ "@storybook/addon-onboarding": "^10.3.5",
22
+ "@storybook/react-vite": "^10.3.5",
23
+ "@types/node": "^24.12.2",
24
+ "@types/react": "^18.3.3",
25
+ "@types/react-dom": "^18.3.3",
26
+ "cpx2": "^9.0.0",
27
+ "esbuild": "^0.25.0",
28
+ "react": "^18.3.1",
29
+ "react-dom": "^18.3.1",
30
+ "storybook": "^10.3.5",
31
+ "typescript": "^5.9.3",
32
+ "vite": "^6.1.1"
33
+ },
34
+ "main": "dist/cjs/index.js",
35
+ "types": "./index.d.ts",
36
+ "module": "lib/esm/index.js",
37
+ "files": [
38
+ "dist",
39
+ "lib"
40
+ ],
41
+ "dependencies": {
42
+ "@topconsultnpm/sdk-ts": "6.22.0-dev2.2",
43
+ "@zip.js/zip.js": "2.8.26",
44
+ "buffer": "^6.0.3",
45
+ "devextreme": "^25.2.6",
46
+ "devextreme-exceljs-fork": "^4.4.10",
47
+ "devextreme-react": "^25.2.6",
48
+ "htmlparser2": "^10.0.0",
49
+ "pdf-lib": "^1.17.1",
50
+ "react-pdf": "^10.4.1",
51
+ "react-window": "^2.2.7",
52
+ "styled-components": "^6.1.1"
53
+ },
54
+ "overrides": {
55
+ "esbuild": "^0.25.0",
56
+ "devextreme-exceljs-fork": {
57
+ "archiver": "^8.0.0"
58
+ },
59
+ "quill-delta": {
60
+ "lodash.isequal": "npm:fast-deep-equal@^3.1.3"
61
+ }
62
+ },
63
+ "allowScripts": {
64
+ "core-js@3.49.0": true,
65
+ "esbuild@0.25.12": true,
66
+ "inferno@8.2.3": true
67
+ }
68
68
  }