@topconsultnpm/sdkui-react 6.19.0-test2 → 6.20.0-dev1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/lib/components/base/TMAccordion.js +2 -2
  2. package/lib/components/choosers/TMDynDataListItemChooser.js +5 -4
  3. package/lib/components/editors/TMHtmlEditor.js +1 -1
  4. package/lib/components/editors/TMMetadataValues.js +34 -12
  5. package/lib/components/features/assistant/ToppyDraggableHelpCenter.js +74 -63
  6. package/lib/components/features/documents/TMDcmtBlog.d.ts +1 -7
  7. package/lib/components/features/documents/TMDcmtBlog.js +29 -2
  8. package/lib/components/features/documents/TMDcmtForm.d.ts +1 -0
  9. package/lib/components/features/documents/TMDcmtForm.js +24 -34
  10. package/lib/components/features/documents/TMDcmtPreview.js +93 -64
  11. package/lib/components/features/search/TMSavedQuerySelector.js +1 -1
  12. package/lib/components/features/search/TMSearchQueryPanel.js +1 -1
  13. package/lib/components/features/search/TMSearchResult.js +249 -58
  14. package/lib/components/features/search/TMSearchResultCheckoutInfoForm.d.ts +8 -0
  15. package/lib/components/features/search/TMSearchResultCheckoutInfoForm.js +129 -0
  16. package/lib/components/features/search/TMSearchResultsMenuItems.d.ts +2 -2
  17. package/lib/components/features/search/TMSearchResultsMenuItems.js +41 -63
  18. package/lib/components/features/search/TMTreeSelector.js +1 -1
  19. package/lib/components/features/search/TMViewHistoryDcmt.d.ts +18 -0
  20. package/lib/components/features/search/TMViewHistoryDcmt.js +285 -0
  21. package/lib/components/grids/TMRecentsManager.js +1 -1
  22. package/lib/helper/SDKUI_Globals.d.ts +3 -7
  23. package/lib/helper/SDKUI_Globals.js +1 -0
  24. package/lib/helper/SDKUI_Localizator.d.ts +16 -0
  25. package/lib/helper/SDKUI_Localizator.js +209 -6
  26. package/lib/helper/TMIcons.d.ts +3 -1
  27. package/lib/helper/TMIcons.js +9 -1
  28. package/lib/helper/TMUtils.d.ts +3 -1
  29. package/lib/helper/TMUtils.js +51 -0
  30. package/lib/helper/checkinCheckoutManager.d.ts +55 -0
  31. package/lib/helper/checkinCheckoutManager.js +266 -0
  32. package/lib/helper/helpers.d.ts +7 -0
  33. package/lib/helper/helpers.js +37 -5
  34. package/lib/helper/index.d.ts +1 -0
  35. package/lib/helper/index.js +1 -0
  36. package/lib/helper/queryHelper.js +13 -1
  37. package/lib/services/platform_services.d.ts +1 -1
  38. package/package.json +52 -52
@@ -47,12 +47,12 @@ const StyledGroupTemplate = styled.div `
47
47
  &::after {
48
48
  content: '';
49
49
  display: block;
50
- width: 90%;
50
+ width: calc(100% - 35px);
51
51
  margin: 0 auto;
52
52
  border-bottom: 1px solid #00A99D;
53
53
  margin-top: 8px;
54
54
  position: absolute;
55
- left: 5%;
55
+ left: 35px;
56
56
  bottom: 0;
57
57
  }
58
58
  `;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
3
  import { DataColumnTypes, SDK_Localizator, DataListCacheService, SDK_Globals, LayoutModes } from '@topconsultnpm/sdk-ts';
4
- import { IconDetails, IconSearch, getDataColumnName, Globalization, SDKUI_Localizator, searchResultDescriptorToSimpleArray, IsParametricQuery, IconWarning } from '../../helper';
4
+ import { IconDetails, IconSearch, generateUniqueColumnKeys, Globalization, SDKUI_Localizator, searchResultDescriptorToSimpleArray, IsParametricQuery, IconWarning } from '../../helper';
5
5
  import { StyledDivHorizontal } from '../base/Styled';
6
6
  import TMSpinner from '../base/TMSpinner';
7
7
  import TMSummary from '../editors/TMSummary';
@@ -125,8 +125,9 @@ const TMDynDataListItemChooser = ({ tid, md, width = '100%', titleForm, openChoo
125
125
  export default TMDynDataListItemChooser;
126
126
  const cellRenderIcon = () => _jsx(IconDetails, {});
127
127
  export const TMDynDataListItemChooserForm = (props) => {
128
+ // Generate unique keys for all columns
129
+ const uniqueKeys = generateUniqueColumnKeys(props.searchResult?.dtdResult?.columns, props.searchResult?.fromTID);
128
130
  const dataColumns = props.searchResult?.dtdResult?.columns?.map((col, index) => {
129
- let keyField = getDataColumnName(props.searchResult?.fromTID, col);
130
131
  const isVisible = col.extendedProperties?.["Visibility"] != "Hidden";
131
132
  const dataType = () => {
132
133
  switch (col.dataType) {
@@ -136,9 +137,9 @@ export const TMDynDataListItemChooserForm = (props) => {
136
137
  default: return "string";
137
138
  }
138
139
  };
139
- return { dataField: keyField, caption: col.caption, visible: isVisible, dataType: dataType(), format: col.dataType === DataColumnTypes.DateTime ? Globalization.getDateDisplayFormat() : "" };
140
+ return { dataField: uniqueKeys[index], caption: col.caption, visible: isVisible, dataType: dataType(), format: col.dataType === DataColumnTypes.DateTime ? Globalization.getDateDisplayFormat() : "" };
140
141
  });
141
- const keyValue = props.searchResult ? getDataColumnName(props.searchResult?.fromTID, props.searchResult?.dtdResult?.columns?.[props.dynDL?.selectItemForValue ?? 0]) : '';
142
+ const keyValue = uniqueKeys[props.dynDL?.selectItemForValue ?? 0] ?? '';
142
143
  const getItems = async (refreshCache) => {
143
144
  if (!props.searchResult)
144
145
  return [];
@@ -170,7 +170,7 @@ const TMHtmlEditor = (props) => {
170
170
  justifyContent: 'flex-end',
171
171
  fontSize: 12,
172
172
  color: '#6c757d',
173
- marginTop: 4,
173
+ marginTop: showInfoIcon ? 0 : 4,
174
174
  gap: 4,
175
175
  }, children: [`${Math.max(charactersRemaining, 0)} ${SDKUI_Localizator.CharactersRemaining}`, showInfoIcon && (_jsx(TMTooltip, { content: 'Markup HTML', children: _jsx("span", { className: "dx-icon-codeblock", style: { fontSize: 22, cursor: 'pointer' }, onClick: () => {
176
176
  TMMessageBoxManager.show({
@@ -10,7 +10,7 @@ import TMTooltip from "../base/TMTooltip";
10
10
  import TMCheckBox from "./TMCheckBox";
11
11
  import TMMetadataEditor, { useMetadataEditableList } from "./TMMetadataEditor";
12
12
  import { FormulaHelper } from "./TMFormulaEditor";
13
- import { DraftsMIDs, DSAttachsMIDs } from "../../ts";
13
+ import { ChronologyMIDs, DraftsMIDs, DSAttachsMIDs } from "../../ts";
14
14
  import { TMNothingToShow } from "../features/documents/TMDcmtPreview";
15
15
  import TMAccordion from "../base/TMAccordion";
16
16
  import TabPanel, { Item } from 'devextreme-react/tab-panel';
@@ -388,6 +388,20 @@ const TMMetadataValues = ({ showCheckBoxes = ShowCheckBoxesMode.Never, checkPerm
388
388
  });
389
389
  return (_jsxs("div", { style: { width: '100%' }, children: [draftData.length > 0 && _jsx(TMAccordion, { title: SDKUI_Localizator.Draft, children: draftData.map(item => renderMetadataItem(item, isReadOnly)) }), checkOutData.length > 0 && _jsx(TMAccordion, { title: `${SDKUI_Localizator.CheckIn}/${SDKUI_Localizator.CheckOut}`, children: checkOutData.map(item => renderMetadataItem(item, true)) })] }));
390
390
  }, [metadataValues, showCheckBoxes, showNullValueCheckBoxes, isReadOnly, dynDataListsToBeRefreshed, validationItems, selectedMID, isOpenDistinctValues, openChooserBySingleClick, metadataValuesOrig]);
391
+ const layoutChronology = useMemo(() => {
392
+ const chronologyData = [];
393
+ metadataValues.forEach(item => {
394
+ switch (item.md?.id) {
395
+ case ChronologyMIDs.Ver:
396
+ case ChronologyMIDs.AuthorID:
397
+ chronologyData.push(item);
398
+ break;
399
+ default:
400
+ break;
401
+ }
402
+ });
403
+ return (_jsx("div", { style: { width: '100%' }, children: chronologyData.length > 0 && chronologyData.map(item => renderMetadataItem(item, isReadOnly)) }));
404
+ }, [metadataValues, showCheckBoxes, showNullValueCheckBoxes, isReadOnly, dynDataListsToBeRefreshed, validationItems, selectedMID, isOpenDistinctValues, openChooserBySingleClick, metadataValuesOrig]);
391
405
  const layoutDsAttachs = useMemo(() => {
392
406
  const dsAttachsData = [];
393
407
  metadataValues.forEach(item => {
@@ -443,10 +457,10 @@ const TMMetadataValues = ({ showCheckBoxes = ShowCheckBoxesMode.Never, checkPerm
443
457
  if (layoutItem.type === LayoutItemTypes.LayoutRoot) {
444
458
  const children = getChildren(layoutItem.layoutItemID);
445
459
  if (layout.showTab) {
446
- return (_jsx(React.Fragment, { children: _jsx(TabPanel, { width: "100%", height: "100%", children: children.map(child => renderLayoutItem(child, depth, visited)) }) }, `root-${layoutItem.layoutItemID}`));
460
+ return (_jsx(React.Fragment, { children: _jsx(TabPanel, { width: "100%", height: "100%", children: children.map(child => (_jsx(React.Fragment, { children: renderLayoutItem(child, depth, visited) }, `child-${child.layoutItemID}`))) }) }, `root-${layoutItem.layoutItemID}`));
447
461
  }
448
462
  else {
449
- return (_jsx(React.Fragment, { children: children.map(child => renderLayoutItem(child, depth, visited)) }, `root-${layoutItem.layoutItemID}`));
463
+ return (_jsx(React.Fragment, { children: children.map(child => (_jsx(React.Fragment, { children: renderLayoutItem(child, depth, visited) }, `child-${child.layoutItemID}`))) }, `root-${layoutItem.layoutItemID}`));
450
464
  }
451
465
  }
452
466
  // Check if this is a LayoutGroup
@@ -458,25 +472,32 @@ const TMMetadataValues = ({ showCheckBoxes = ShowCheckBoxesMode.Never, checkPerm
458
472
  const isCollapsed = false; // LayoutGroupDescriptor doesn't have collapsed property
459
473
  //Layout Grou da trasformare in tab
460
474
  if (groupDescriptor.orientation == LayoutGroupOrientations.Vertical && layout.showTab && layoutItem.parentID === 0) {
461
- return (_jsx(Item, { title: groupTitle, children: _jsx("div", { style: { width: '100%' }, children: children.map(child => renderLayoutItem(child, depth + 1, visited)) }) }, `group-tab-${layoutItem.layoutItemID}`));
475
+ return (_jsx(Item, { title: groupTitle, children: _jsx("div", { style: { width: '100%' }, children: children.map(child => (_jsx(React.Fragment, { children: renderLayoutItem(child, depth + 1, visited) }, `child-${child.layoutItemID}`))) }) }, `group-tab-${layoutItem.layoutItemID}`));
462
476
  }
463
477
  if (groupIsNoBorder && groupDescriptor.orientation !== LayoutGroupOrientations.Horizontal && children.length == 0) {
464
478
  return (_jsx("br", {}));
465
479
  }
480
+ if (!groupIsNoBorder && groupDescriptor.orientation === LayoutGroupOrientations.Horizontal && children.length > 0 && groupTitle.length > 0) {
481
+ return (
482
+ // <div key={`group-horizontal-${layoutItem.layoutItemID}`} style={{ display: 'flex', flexDirection: 'row', flexWrap: 'wrap', gap: '3px' }}>
483
+ _jsxs("div", { style: { position: 'relative', border: '2px solid #CAD9EB', borderRadius: '3px', padding: '12px 6px 6px', margin: '10px', display: 'flex', flexDirection: 'row', flexWrap: 'wrap', gap: '3px' }, children: [_jsx("div", { style: { position: 'absolute', top: '-10px', left: '10px', padding: '0 8px', backgroundColor: '#fff', fontWeight: 600, fontSize: '1rem', color: TMColors.primaryColor, }, children: groupTitle }), children.map(child => (_jsx(React.Fragment, { children: renderLayoutItem(child, depth + 1, visited) }, `child-${child.layoutItemID}`))), " "] }, `group-horizontal-${layoutItem.layoutItemID}`)
484
+ // </div>
485
+ );
486
+ }
466
487
  if (groupIsNoBorder && groupDescriptor.orientation === LayoutGroupOrientations.Vertical) {
467
- return (_jsx("div", { style: { width: '100%', border: 'none', padding: 0 }, children: children.map(child => renderLayoutItem(child, depth + 1, visited)) }));
488
+ return (_jsx("div", { style: { width: '100%', border: 'none', padding: 0 }, children: children.map(child => (_jsx(React.Fragment, { children: renderLayoutItem(child, depth + 1, visited) }, `child-${child.layoutItemID}`))) }));
468
489
  }
469
490
  if (!groupIsNoBorder && groupDescriptor.orientation === LayoutGroupOrientations.Vertical) {
470
491
  if (groupDescriptor.borderStyle == LayoutGroupBorderStyles.Group) {
471
- return (_jsxs("div", { style: { position: 'relative', border: '2px solid #CAD9EB', borderRadius: '3px', padding: '12px 6px 6px', margin: '10px', display: 'flex', flexDirection: 'column', flexWrap: 'wrap', gap: '3px', }, children: [_jsx("div", { style: { position: 'absolute', top: '-10px', left: '10px', padding: '0 8px', backgroundColor: '#fff', fontWeight: 600, fontSize: '1rem', color: TMColors.primaryColor, }, children: groupTitle }), children.map(child => renderLayoutItem(child, depth + 1, visited))] }));
492
+ return (_jsxs("div", { style: { position: 'relative', border: '2px solid #CAD9EB', borderRadius: '3px', padding: '12px 6px 6px', margin: '10px', display: 'flex', flexDirection: 'column', flexWrap: 'wrap', gap: '3px', }, children: [_jsx("div", { style: { position: 'absolute', top: '-10px', left: '10px', padding: '0 8px', backgroundColor: '#fff', fontWeight: 600, fontSize: '1rem', color: TMColors.primaryColor, }, children: groupTitle }), children.map(child => (_jsx(React.Fragment, { children: renderLayoutItem(child, depth + 1, visited) }, `child-${child.layoutItemID}`)))] }));
472
493
  }
473
494
  if (groupDescriptor.borderStyle == LayoutGroupBorderStyles.GroupBox || groupDescriptor.borderStyle == LayoutGroupBorderStyles.Tabbed) {
474
- return (_jsx(TMAccordion, { title: groupTitle, titleSize: "Small", children: children.map(child => renderLayoutItem(child, depth + 1, visited)) }));
495
+ return (_jsx(TMAccordion, { title: groupTitle, titleSize: "Small", children: children.map(child => (_jsx(React.Fragment, { children: renderLayoutItem(child, depth + 1, visited) }, `child-${child.layoutItemID}`))) }));
475
496
  }
476
497
  }
477
498
  if (groupDescriptor.orientation === LayoutGroupOrientations.Horizontal) {
478
499
  return (_jsx("div", { style: { display: 'flex', flexDirection: 'row', flexWrap: 'wrap', gap: '3px' }, children: groupDescriptor.borderStyle == LayoutGroupBorderStyles.GroupBox || groupDescriptor.borderStyle == LayoutGroupBorderStyles.Tabbed ?
479
- _jsx(TMAccordion, { title: groupTitle, titleSize: "Small", children: children.map(child => renderLayoutItem(child, depth + 1, visited)) }) :
500
+ _jsx(TMAccordion, { title: groupTitle, titleSize: "Small", children: children.map(child => (_jsx(React.Fragment, { children: renderLayoutItem(child, depth + 1, visited) }, `child-${child.layoutItemID}`))) }) :
480
501
  groupDescriptor.borderStyle == LayoutGroupBorderStyles.Group ?
481
502
  _jsxs("div", { style: { position: 'relative', border: '2px solid #CAD9EB', borderRadius: '3px', padding: '12px 6px 6px', margin: '10px', display: 'flex', flexDirection: 'column', flexWrap: 'wrap', gap: '3px', }, children: [_jsx("div", { style: { position: 'absolute', top: '-10px', left: '10px', padding: '0 8px', backgroundColor: '#fff', fontWeight: 600, fontSize: '1rem', color: TMColors.primaryColor, }, children: groupTitle }), children.map(child => (_jsx("div", { style: { flex: '1 1 0', minWidth: 0 }, children: renderLayoutItem(child, depth + 1, visited) }, child.layoutItemID)))] })
482
503
  :
@@ -484,7 +505,7 @@ const TMMetadataValues = ({ showCheckBoxes = ShowCheckBoxesMode.Never, checkPerm
484
505
  }
485
506
  // Apply indentation only to subgroups (depth > 0), not to root groups
486
507
  const indentationPx = depth > 0 ? depth * 10 : 0;
487
- return (_jsx("div", { style: { paddingLeft: `${indentationPx}px` }, children: _jsx(TMAccordion, { title: groupTitle, defaultCollapsed: isCollapsed, titleSize: "Small", children: children.map(child => renderLayoutItem(child, depth + 1, visited)) }) }, `group-wrapper-${layoutItem.layoutItemID}`));
508
+ return (_jsx("div", { style: { paddingLeft: `${indentationPx}px` }, children: _jsx(TMAccordion, { title: groupTitle, defaultCollapsed: isCollapsed, titleSize: "Small", children: children.map(child => (_jsx(React.Fragment, { children: renderLayoutItem(child, depth + 1, visited) }, `child-${child.layoutItemID}`))) }) }, `group-wrapper-${layoutItem.layoutItemID}`));
488
509
  }
489
510
  // Check if this is a LayoutControlItem (metadata field)
490
511
  else if (layoutItem.type === LayoutItemTypes.LayoutControlItem && layoutItem.lcid) {
@@ -496,7 +517,8 @@ const TMMetadataValues = ({ showCheckBoxes = ShowCheckBoxesMode.Never, checkPerm
496
517
  const parent = layout.items?.filter(p => p.layoutItemID === layoutItem.parentID);
497
518
  if (parent && parent.length > 0 && parent[0].type === LayoutItemTypes.LayoutGroup && parent[0].lgd) {
498
519
  const groupDescriptor = parent[0].lgd;
499
- if (groupDescriptor.borderStyle == LayoutGroupBorderStyles.Group)
520
+ if (groupDescriptor.borderStyle == LayoutGroupBorderStyles.Group
521
+ || groupDescriptor.borderStyle == LayoutGroupBorderStyles.NoBorder)
500
522
  isAccordion = false;
501
523
  }
502
524
  // Find the metadata value for this MID
@@ -515,7 +537,7 @@ const TMMetadataValues = ({ showCheckBoxes = ShowCheckBoxesMode.Never, checkPerm
515
537
  };
516
538
  return (_jsx("div", { style: { width: '100%' }, children: (() => {
517
539
  const visited = new Set();
518
- return rootItems.map(item => renderLayoutItem(item, 0, visited));
540
+ return rootItems.map(item => (_jsx(React.Fragment, { children: renderLayoutItem(item, 0, visited) }, `root-item-${item.layoutItemID}`)));
519
541
  })() }));
520
542
  }, [layout, metadataValues, showCheckBoxes, showNullValueCheckBoxes, isReadOnly, dynDataListsToBeRefreshed, validationItems, selectedMID, isOpenDistinctValues, openChooserBySingleClick, metadataValuesOrig]);
521
543
  const renderForm = useMemo(() => {
@@ -533,7 +555,7 @@ const TMMetadataValues = ({ showCheckBoxes = ShowCheckBoxesMode.Never, checkPerm
533
555
  }
534
556
  switch (currentDTD?.id) {
535
557
  case SystemTIDs.Drafts: return layoutDraft;
536
- // case SystemTIDs.Chronology: break;
558
+ case SystemTIDs.Chronology: return layoutChronology;
537
559
  case SystemTIDs.DSAttachs: return layoutDsAttachs;
538
560
  default:
539
561
  // Se è presente un layout personalizzato, usalo, altrimenti usa il rendering standard
@@ -33,7 +33,7 @@ const ToppyButton = styled.div.attrs((props) => ({
33
33
  /* Posizionamento di default quando non è draggato (x e y non sono definiti) */
34
34
  ${(props) => props.$x === undefined || props.$y === undefined
35
35
  ? `
36
- bottom: ${props.$isMobile ? '0px' : '-20px'};
36
+ bottom: ${props.$isMobile ? (props.$isCollapsed ? '-20px' : '-50px') : '-20px'};
37
37
  ${props.$align === 'left' ? 'left: 10px;' : 'right: 10px;'};
38
38
  `
39
39
  : ''}
@@ -48,15 +48,9 @@ const ToppyButton = styled.div.attrs((props) => ({
48
48
  /* Dimensioni dinamiche in base allo stato collassato e al tipo di dispositivo
49
49
  Usa min() per adattarsi su schermi piccoli */
50
50
  width: ${(props) => {
51
- if (props.$isMobile) {
52
- return props.$isCollapsed ? 'min(40px, 100%)' : '80px';
53
- }
54
51
  return props.$isCollapsed ? 'min(60px, 100%)' : '120px';
55
52
  }};
56
53
  height: ${(props) => {
57
- if (props.$isMobile) {
58
- return props.$isCollapsed ? 'min(45px, 100%)' : '95px';
59
- }
60
54
  return props.$isCollapsed ? 'min(70px, 100%)' : '140px';
61
55
  }};
62
56
  max-width: 100%;
@@ -66,18 +60,8 @@ const ToppyButton = styled.div.attrs((props) => ({
66
60
 
67
61
  img {
68
62
  /* Dimensioni dell'immagine in base allo stato collassato e al tipo di dispositivo */
69
- width: ${(props) => {
70
- if (props.$isMobile) {
71
- return props.$isCollapsed ? '40px' : '80px';
72
- }
73
- return props.$isCollapsed ? '60px' : '120px';
74
- }};
75
- height: ${(props) => {
76
- if (props.$isMobile) {
77
- return props.$isCollapsed ? '40px' : '95px';
78
- }
79
- return props.$isCollapsed ? '60px' : '140px';
80
- }};
63
+ width: ${(props) => props.$isCollapsed ? (props.$isMobile ? '40px' : '60px') : (props.$isMobile ? '80px' : '120px')};
64
+ height: ${(props) => props.$isCollapsed ? (props.$isMobile ? '40px' : '60px') : (props.$isMobile ? '100px' : '140px')};
81
65
  pointer-events: ${(props) => (props.$isMobile ? 'auto' : 'none')};
82
66
  border-radius: 50%; /* Rende l'immagine circolare */
83
67
  /* Rotazione leggera in base all'allineamento */
@@ -169,6 +153,16 @@ const DragOverlay = styled.div `
169
153
  * contenitore e può essere collassato/espanso con un doppio click.
170
154
  */
171
155
  const ToppyDraggableHelpCenter = ({ content, deviceType, align = 'right', onToppyImageClick, initialIsCollapsed, isVisible = true, usePortal = false, }) => {
156
+ // Configurazione del layout e dei limiti di posizionamento di Toppy
157
+ const LAYOUT_CONFIG = {
158
+ bottomOffsetDesktop: 20, // Quanto Toppy può uscire dal bordo inferiore su desktop
159
+ bottomOffsetMobileCollapsed: 20, // Quanto Toppy può uscire dal bordo inferiore su mobile quando è collassato
160
+ bottomOffsetMobileExpanded: 50, // Quanto Toppy può uscire dal bordo inferiore su mobile quando è espanso
161
+ buttonOffset: 8, // Spazio extra per i bottoni posizionati a -8px (ExpandButton, CloseButton)
162
+ closeButtonOffset: 8, // Offset dei bottoni che sporgono dalla bubble (CloseButton sulla bubble)
163
+ bubbleBuffer: 5, // Buffer di sicurezza per evitare sovrapposizioni con la bubble
164
+ minTopOffset: 8 // Limite minimo dall'alto per evitare che i bottoni superiori escano
165
+ };
172
166
  // Ref per il contenitore principale
173
167
  const buttonRef = useRef(null);
174
168
  // Stato per controllare se il componente è collassato o espanso
@@ -205,20 +199,16 @@ const ToppyDraggableHelpCenter = ({ content, deviceType, align = 'right', onTopp
205
199
  return;
206
200
  const rect = buttonRef.current.getBoundingClientRect();
207
201
  // Spazio extra occupato dalla bubble quando non è collassato
208
- // Include anche l'offset dei bottoni che sporgono dalla bubble (8px)
209
- const closeButtonOffset = 8;
210
- const extraHeight = !isCollapsed ? bubbleSize.height + closeButtonOffset : 0;
211
- const extraWidth = !isCollapsed ? bubbleSize.width + closeButtonOffset : 0;
202
+ const extraHeight = !isCollapsed ? bubbleSize.height + LAYOUT_CONFIG.closeButtonOffset : 0;
203
+ const extraWidth = !isCollapsed ? bubbleSize.width + LAYOUT_CONFIG.closeButtonOffset : 0;
212
204
  let minX = 0;
213
205
  let maxX;
214
206
  let maxY;
215
- const bubbleBuffer = 5;
216
- const buttonOffset = 8; // Spazio extra per i bottoni posizionati a -8px
217
- const minY = buttonOffset; // Limite minimo per evitare che i bottoni superiori escano
207
+ const minY = LAYOUT_CONFIG.minTopOffset;
218
208
  if (usePortal) {
219
209
  // Calcola i limiti usando le dimensioni del viewport
220
- maxX = window.innerWidth - rect.width - buttonOffset; // Restringo per evitare che i bottoni a destra escano
221
- maxY = window.innerHeight - rect.height + 20;
210
+ maxX = window.innerWidth - rect.width - LAYOUT_CONFIG.buttonOffset;
211
+ maxY = window.innerHeight - rect.height + (isMobile ? (isCollapsed ? LAYOUT_CONFIG.bottomOffsetMobileCollapsed : LAYOUT_CONFIG.bottomOffsetMobileExpanded) : LAYOUT_CONFIG.bottomOffsetDesktop);
222
212
  }
223
213
  else {
224
214
  // Calcola i limiti usando le dimensioni del parent
@@ -226,8 +216,8 @@ const ToppyDraggableHelpCenter = ({ content, deviceType, align = 'right', onTopp
226
216
  if (!parent)
227
217
  return;
228
218
  const parentRect = parent.getBoundingClientRect();
229
- maxX = parentRect.width - rect.width - buttonOffset; // Restringo per evitare che i bottoni a destra escano
230
- maxY = parentRect.height - rect.height + 20;
219
+ maxX = parentRect.width - rect.width - LAYOUT_CONFIG.buttonOffset;
220
+ maxY = parentRect.height - rect.height + (isMobile ? (isCollapsed ? LAYOUT_CONFIG.bottomOffsetMobileCollapsed : LAYOUT_CONFIG.bottomOffsetMobileExpanded) : LAYOUT_CONFIG.bottomOffsetDesktop);
231
221
  }
232
222
  if (!isCollapsed) {
233
223
  if (align === 'right') {
@@ -240,12 +230,12 @@ const ToppyDraggableHelpCenter = ({ content, deviceType, align = 'right', onTopp
240
230
  // Verifica se la posizione corrente è fuori dai limiti
241
231
  const isOutOfBounds = position.x < minX ||
242
232
  position.x > maxX ||
243
- position.y < Math.max(minY, extraHeight + bubbleBuffer) ||
233
+ position.y < Math.max(minY, extraHeight + LAYOUT_CONFIG.bubbleBuffer) ||
244
234
  position.y > maxY;
245
235
  // Se è fuori dai limiti, aggiusta la posizione
246
236
  if (isOutOfBounds) {
247
237
  const adjustedX = Math.max(minX, Math.min(position.x, maxX));
248
- const adjustedY = Math.max(Math.max(minY, extraHeight + bubbleBuffer), Math.min(position.y, maxY));
238
+ const adjustedY = Math.max(Math.max(minY, extraHeight + LAYOUT_CONFIG.bubbleBuffer), Math.min(position.y, maxY));
249
239
  setPosition({ x: adjustedX, y: adjustedY });
250
240
  }
251
241
  }, [isCollapsed, bubbleSize, usePortal, align]);
@@ -259,27 +249,24 @@ const ToppyDraggableHelpCenter = ({ content, deviceType, align = 'right', onTopp
259
249
  if (!buttonRef.current || !position)
260
250
  return;
261
251
  const rect = buttonRef.current.getBoundingClientRect();
262
- // Include anche l'offset dei bottoni che sporgono dalla bubble (8px)
263
- const closeButtonOffset = 8;
264
- const extraHeight = !isCollapsed ? bubbleSize.height + closeButtonOffset : 0;
265
- const extraWidth = !isCollapsed ? bubbleSize.width + closeButtonOffset : 0;
252
+ // Spazio extra occupato dalla bubble quando non è collassato
253
+ const extraHeight = !isCollapsed ? bubbleSize.height + LAYOUT_CONFIG.closeButtonOffset : 0;
254
+ const extraWidth = !isCollapsed ? bubbleSize.width + LAYOUT_CONFIG.closeButtonOffset : 0;
266
255
  let minX = 0;
267
256
  let maxX;
268
257
  let maxY;
269
- const bubbleBuffer = 5;
270
- const buttonOffset = 8; // Spazio extra per i bottoni posizionati a -8px
271
- const minY = buttonOffset; // Limite minimo per evitare che i bottoni superiori escano
258
+ const minY = LAYOUT_CONFIG.minTopOffset;
272
259
  if (usePortal) {
273
- maxX = window.innerWidth - rect.width - buttonOffset; // Restringo per evitare che i bottoni a destra escano
274
- maxY = window.innerHeight - rect.height + 20;
260
+ maxX = window.innerWidth - rect.width - LAYOUT_CONFIG.buttonOffset;
261
+ maxY = window.innerHeight - rect.height + (isMobile ? (isCollapsed ? LAYOUT_CONFIG.bottomOffsetMobileCollapsed : LAYOUT_CONFIG.bottomOffsetMobileExpanded) : LAYOUT_CONFIG.bottomOffsetDesktop);
275
262
  }
276
263
  else {
277
264
  const parent = buttonRef.current.offsetParent;
278
265
  if (!parent)
279
266
  return;
280
267
  const parentRect = parent.getBoundingClientRect();
281
- maxX = parentRect.width - rect.width - buttonOffset; // Restringo per evitare che i bottoni a destra escano
282
- maxY = parentRect.height - rect.height + 20;
268
+ maxX = parentRect.width - rect.width - LAYOUT_CONFIG.buttonOffset;
269
+ maxY = parentRect.height - rect.height + (isMobile ? (isCollapsed ? LAYOUT_CONFIG.bottomOffsetMobileCollapsed : LAYOUT_CONFIG.bottomOffsetMobileExpanded) : LAYOUT_CONFIG.bottomOffsetDesktop);
283
270
  }
284
271
  if (!isCollapsed) {
285
272
  if (align === 'right') {
@@ -291,7 +278,7 @@ const ToppyDraggableHelpCenter = ({ content, deviceType, align = 'right', onTopp
291
278
  }
292
279
  const isOutOfBounds = position.x < minX ||
293
280
  position.x > maxX ||
294
- position.y < Math.max(minY, extraHeight + bubbleBuffer) ||
281
+ position.y < Math.max(minY, extraHeight + LAYOUT_CONFIG.bubbleBuffer) ||
295
282
  position.y > maxY;
296
283
  if (isOutOfBounds) {
297
284
  setPosition(null);
@@ -347,6 +334,22 @@ const ToppyDraggableHelpCenter = ({ content, deviceType, align = 'right', onTopp
347
334
  setIsDragging(true);
348
335
  e.preventDefault();
349
336
  };
337
+ /**
338
+ * Gestisce l'inizio del trascinamento per eventi touch
339
+ */
340
+ const handleTouchStart = (e) => {
341
+ if (!buttonRef.current)
342
+ return;
343
+ const touch = e.touches[0];
344
+ const rect = buttonRef.current.getBoundingClientRect();
345
+ // Calcola l'offset tra il punto di touch e l'angolo superiore sinistro del componente
346
+ dragOffset.current = {
347
+ x: touch.clientX - rect.left,
348
+ y: touch.clientY - rect.top,
349
+ };
350
+ setIsDragging(true);
351
+ e.preventDefault();
352
+ };
350
353
  /**
351
354
  * Gestisce il movimento durante il trascinamento
352
355
  * Calcola la nuova posizione rispettando i limiti del parent o viewport
@@ -355,24 +358,23 @@ const ToppyDraggableHelpCenter = ({ content, deviceType, align = 'right', onTopp
355
358
  const handleMouseMove = (e) => {
356
359
  if (!isDragging || !buttonRef.current)
357
360
  return;
361
+ // Estrae le coordinate dal tipo di evento appropriato
362
+ const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
363
+ const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
358
364
  const rect = buttonRef.current.getBoundingClientRect();
359
365
  // Spazio extra occupato dalla bubble quando non è collassato
360
- // Include anche l'offset dei bottoni che sporgono dalla bubble (8px)
361
- const closeButtonOffset = 8;
362
- const extraHeight = !isCollapsed ? bubbleSize.height + closeButtonOffset : 0;
363
- const extraWidth = !isCollapsed ? bubbleSize.width + closeButtonOffset : 0;
366
+ const extraHeight = !isCollapsed ? bubbleSize.height + LAYOUT_CONFIG.closeButtonOffset : 0;
367
+ const extraWidth = !isCollapsed ? bubbleSize.width + LAYOUT_CONFIG.closeButtonOffset : 0;
364
368
  let minX = 0;
365
369
  let maxX;
366
370
  let maxY;
367
371
  let newX;
368
372
  let newY;
369
- const bubbleBuffer = 5;
370
- const buttonOffset = 8; // Spazio extra per i bottoni posizionati a -8px
371
- const minY = buttonOffset; // Limite minimo per evitare che i bottoni superiori escano
373
+ const minY = LAYOUT_CONFIG.minTopOffset;
372
374
  if (usePortal) {
373
375
  // Calcola i limiti usando il viewport
374
- maxX = window.innerWidth - rect.width - buttonOffset; // Restringo per evitare che i bottoni a destra escano
375
- maxY = window.innerHeight - rect.height + 20;
376
+ maxX = window.innerWidth - rect.width - LAYOUT_CONFIG.buttonOffset;
377
+ maxY = window.innerHeight - rect.height + (isMobile ? (isCollapsed ? LAYOUT_CONFIG.bottomOffsetMobileCollapsed : LAYOUT_CONFIG.bottomOffsetMobileExpanded) : LAYOUT_CONFIG.bottomOffsetDesktop);
376
378
  if (!isCollapsed) {
377
379
  if (align === 'right') {
378
380
  minX = Math.max(0, extraWidth - rect.width);
@@ -381,8 +383,8 @@ const ToppyDraggableHelpCenter = ({ content, deviceType, align = 'right', onTopp
381
383
  maxX = Math.min(maxX, window.innerWidth - extraWidth);
382
384
  }
383
385
  }
384
- newX = Math.max(minX, Math.min(e.clientX - dragOffset.current.x, maxX));
385
- newY = Math.max(Math.max(minY, extraHeight + bubbleBuffer), Math.min(e.clientY - dragOffset.current.y, maxY));
386
+ newX = Math.max(minX, Math.min(clientX - dragOffset.current.x, maxX));
387
+ newY = Math.max(Math.max(minY, extraHeight + LAYOUT_CONFIG.bubbleBuffer), Math.min(clientY - dragOffset.current.y, maxY));
386
388
  }
387
389
  else {
388
390
  // Calcola i limiti usando il parent
@@ -390,8 +392,8 @@ const ToppyDraggableHelpCenter = ({ content, deviceType, align = 'right', onTopp
390
392
  if (!parent)
391
393
  return;
392
394
  const parentRect = parent.getBoundingClientRect();
393
- maxX = parentRect.width - rect.width - buttonOffset; // Restringo per evitare che i bottoni a destra escano
394
- maxY = parentRect.height - rect.height + 20;
395
+ maxX = parentRect.width - rect.width - LAYOUT_CONFIG.buttonOffset;
396
+ maxY = parentRect.height - rect.height + (isMobile ? (isCollapsed ? LAYOUT_CONFIG.bottomOffsetMobileCollapsed : LAYOUT_CONFIG.bottomOffsetMobileExpanded) : LAYOUT_CONFIG.bottomOffsetDesktop);
395
397
  if (!isCollapsed) {
396
398
  if (align === 'right') {
397
399
  minX = Math.max(0, extraWidth - rect.width);
@@ -400,8 +402,8 @@ const ToppyDraggableHelpCenter = ({ content, deviceType, align = 'right', onTopp
400
402
  maxX = Math.min(maxX, parentRect.width - extraWidth);
401
403
  }
402
404
  }
403
- newX = Math.max(minX, Math.min(e.clientX - parentRect.left - dragOffset.current.x, maxX));
404
- newY = Math.max(Math.max(minY, extraHeight + bubbleBuffer), Math.min(e.clientY - parentRect.top - dragOffset.current.y, maxY));
405
+ newX = Math.max(minX, Math.min(clientX - parentRect.left - dragOffset.current.x, maxX));
406
+ newY = Math.max(Math.max(minY, extraHeight + LAYOUT_CONFIG.bubbleBuffer), Math.min(clientY - parentRect.top - dragOffset.current.y, maxY));
405
407
  }
406
408
  setPosition({ x: newX, y: newY });
407
409
  };
@@ -413,10 +415,13 @@ const ToppyDraggableHelpCenter = ({ content, deviceType, align = 'right', onTopp
413
415
  const handleMouseUp = (e) => {
414
416
  if (isDragging) {
415
417
  setIsDragging(false);
418
+ // Estrae le coordinate dal tipo di evento appropriato
419
+ const clientX = 'changedTouches' in e ? e.changedTouches[0].clientX : e.clientX;
420
+ const clientY = 'changedTouches' in e ? e.changedTouches[0].clientY : e.clientY;
416
421
  const rect = buttonRef.current?.getBoundingClientRect();
417
422
  if (rect) {
418
423
  // Calcola la distanza totale del movimento usando il teorema di Pitagora
419
- const moveDistance = Math.hypot(e.clientX - (rect.left + dragOffset.current.x), e.clientY - (rect.top + dragOffset.current.y));
424
+ const moveDistance = Math.hypot(clientX - (rect.left + dragOffset.current.x), clientY - (rect.top + dragOffset.current.y));
420
425
  // Se il movimento è stato minimo, trattalo come un click
421
426
  if (moveDistance < 5 && onToppyImageClick) {
422
427
  onToppyImageClick();
@@ -427,22 +432,28 @@ const ToppyDraggableHelpCenter = ({ content, deviceType, align = 'right', onTopp
427
432
  /**
428
433
  * Effect per gestire gli event listener durante il trascinamento
429
434
  * Gli eventi sono registrati sul document per catturare il movimento
430
- * anche quando il mouse esce dal componente
435
+ * anche quando il mouse o il touch esce dal componente
431
436
  */
432
437
  useEffect(() => {
433
438
  if (isDragging) {
439
+ // Eventi mouse per desktop
434
440
  document.addEventListener('mousemove', handleMouseMove);
435
441
  document.addEventListener('mouseup', handleMouseUp);
442
+ // Eventi touch per mobile
443
+ document.addEventListener('touchmove', handleMouseMove);
444
+ document.addEventListener('touchend', handleMouseUp);
436
445
  return () => {
437
446
  document.removeEventListener('mousemove', handleMouseMove);
438
447
  document.removeEventListener('mouseup', handleMouseUp);
448
+ document.removeEventListener('touchmove', handleMouseMove);
449
+ document.removeEventListener('touchend', handleMouseUp);
439
450
  };
440
451
  }
441
452
  return undefined;
442
453
  }, [isDragging]);
443
454
  // Renderizza l'overlay solo durante il drag
444
455
  const renderDragOverlay = isDragging && _jsx(DragOverlay, {});
445
- const toppyContent = (_jsxs(_Fragment, { children: [renderDragOverlay, _jsxs(ToppyButton, { ref: buttonRef, "$align": align, "$isDragging": isDragging, "$x": position?.x, "$y": position?.y, "$isVisible": isVisible, "$isCollapsed": isCollapsed, "$isMobile": isMobile, "$usePortal": usePortal, onMouseDown: !isMobile ? handleMouseDown : undefined, onContextMenu: (e) => e.preventDefault(), onDoubleClick: !isMobile ? toggleCollapse : undefined, children: [(content && !isCollapsed) && (_jsx(ToppySpeechBubble, { ref: bubbleRef, align: align, onClose: toggleCollapse, children: content })), (content && isCollapsed) && (_jsx(ExpandButton, { onMouseDown: (e) => {
456
+ const toppyContent = (_jsxs(_Fragment, { children: [renderDragOverlay, _jsxs(ToppyButton, { ref: buttonRef, "$align": align, "$isDragging": isDragging, "$x": position?.x, "$y": position?.y, "$isVisible": isVisible, "$isCollapsed": isCollapsed, "$isMobile": isMobile, "$usePortal": usePortal, onMouseDown: !isMobile ? handleMouseDown : undefined, onTouchStart: isMobile ? handleTouchStart : undefined, onContextMenu: (e) => e.preventDefault(), onDoubleClick: !isMobile ? toggleCollapse : undefined, children: [(content && !isCollapsed) && (_jsx(ToppySpeechBubble, { ref: bubbleRef, align: align, onClose: toggleCollapse, children: content })), (content && isCollapsed) && (_jsx(ExpandButton, { onMouseDown: (e) => {
446
457
  isExpandButtonDraggingRef.current = false;
447
458
  expandButtonMouseDownPosRef.current = { x: e.clientX, y: e.clientY };
448
459
  }, onMouseMove: (e) => {
@@ -464,7 +475,7 @@ const ToppyDraggableHelpCenter = ({ content, deviceType, align = 'right', onTopp
464
475
  }
465
476
  e.stopPropagation();
466
477
  toggleCollapse(e);
467
- }, onContextMenu: (e) => e.preventDefault(), "aria-label": SDKUI_Localizator.Maximize, title: SDKUI_Localizator.Maximize, type: "button", children: _jsx(IconWindowMaximize, {}) })), _jsx("img", { src: Toppy, alt: "Toppy Help", draggable: false, onClick: isMobile ? toggleCollapse : undefined })] })] }));
478
+ }, onContextMenu: (e) => e.preventDefault(), "aria-label": SDKUI_Localizator.Maximize, title: SDKUI_Localizator.Maximize, type: "button", children: _jsx(IconWindowMaximize, {}) })), _jsx("img", { src: Toppy, alt: "Toppy Help", draggable: false })] })] }));
468
479
  // Renderizza nel document.body usando un Portal se usePortal è true, altrimenti renderizza normalmente
469
480
  return usePortal ? ReactDOM.createPortal(toppyContent, document.body) : toppyContent;
470
481
  };
@@ -1,16 +1,10 @@
1
1
  import React from 'react';
2
2
  import { HomeBlogPost, TaskDescriptor } from '@topconsultnpm/sdk-ts';
3
3
  interface ITMDcmtBlogProps {
4
- blogsDatasource: HomeBlogPost[];
5
- setBlogsDatasource: (posts: HomeBlogPost[]) => void;
6
- hasLoadedDataOnce: boolean;
7
- setHasLoadedDataOnce: (loaded: boolean) => void;
8
- lastLoadedDid: number | undefined;
9
- setLastLoadedDid: (did: number | undefined) => void;
10
4
  tid: number | undefined;
11
5
  did: number | undefined;
12
- fetchBlogDataAsync: (tid: number | undefined, did: number | undefined) => Promise<void>;
13
6
  isVisible?: boolean;
7
+ fetchBlogDataTrigger?: number;
14
8
  allTasks?: Array<TaskDescriptor>;
15
9
  getAllTasks?: () => Promise<void>;
16
10
  deleteTaskByIdsCallback?: (deletedTaskIds: Array<number>) => Promise<void>;
@@ -6,13 +6,40 @@ import { TMNothingToShow } from './TMDcmtPreview';
6
6
  import { IconBoard, SDKUI_Localizator } from '../../../helper';
7
7
  import TMBlogCommentForm from '../blog/TMBlogCommentForm';
8
8
  import TMBlogsPost from '../../grids/TMBlogsPost';
9
- const TMDcmtBlog = ({ blogsDatasource, setBlogsDatasource, hasLoadedDataOnce, setHasLoadedDataOnce, lastLoadedDid, setLastLoadedDid, tid, did, fetchBlogDataAsync, isVisible, allTasks = [], getAllTasks, deleteTaskByIdsCallback, addTaskCallback, editTaskCallback, handleNavigateToWGs, handleNavigateToDossiers }) => {
9
+ import TMSpinner from '../../base/TMSpinner';
10
+ import { TMExceptionBoxManager } from '../../base/TMPopUp';
11
+ const TMDcmtBlog = ({ tid, did, isVisible, fetchBlogDataTrigger, allTasks = [], getAllTasks, deleteTaskByIdsCallback, addTaskCallback, editTaskCallback, handleNavigateToWGs, handleNavigateToDossiers }) => {
12
+ const [blogsDatasource, setBlogsDatasource] = useState([]);
13
+ const [hasLoadedDataOnce, setHasLoadedDataOnce] = useState(false); //traccia se *qualsiasi* dato è stato caricato per la prima volta
14
+ const [lastLoadedDid, setLastLoadedDid] = useState(undefined); // `lastLoadedDid` tiene traccia dell'ultimo `did` per cui abbiamo caricato i dati
10
15
  // State to manage show comment form selected file
11
16
  const [showCommentForm, setShowCommentForm] = useState(false);
12
17
  const [externalBlogPost, setExternalBlogPost] = useState(undefined);
18
+ const fetchBlogDataAsync = useCallback(async (tid, did) => {
19
+ try {
20
+ TMSpinner.show({ description: 'Caricamento - Bacheca...' });
21
+ const res = await SDK_Globals.tmSession?.NewSearchEngine().BlogRetrieveAsync(tid, did);
22
+ setBlogsDatasource(res ?? []);
23
+ setHasLoadedDataOnce(true);
24
+ setLastLoadedDid(did);
25
+ }
26
+ catch (e) {
27
+ let err = e;
28
+ TMExceptionBoxManager.show({ exception: err });
29
+ }
30
+ finally {
31
+ TMSpinner.hide();
32
+ }
33
+ }, []);
13
34
  const showCommentFormCallback = useCallback(() => {
14
35
  setShowCommentForm(true);
15
36
  }, []);
37
+ // useEffect per triggerare il fetch dall'esterno tramite props
38
+ useEffect(() => {
39
+ if (fetchBlogDataTrigger !== undefined && fetchBlogDataTrigger > 0) {
40
+ fetchBlogDataAsync(tid, did);
41
+ }
42
+ }, [fetchBlogDataTrigger, fetchBlogDataAsync, tid, did]);
16
43
  useEffect(() => {
17
44
  if (!tid || !did) {
18
45
  setBlogsDatasource([]);
@@ -21,7 +48,7 @@ const TMDcmtBlog = ({ blogsDatasource, setBlogsDatasource, hasLoadedDataOnce, se
21
48
  }
22
49
  // Condizione per eseguire il fetch:
23
50
  // 1. Il pannello è visibile
24
- // 2. E (non abbiamo ancora caricato dati O il `did` è cambiato rispetto all'ultima volta)
51
+ // 2. E (non abbiamo ancora caricato dati o il `did` è cambiato rispetto all'ultima volta)
25
52
  const shouldFetch = isVisible && (!hasLoadedDataOnce || did !== lastLoadedDid);
26
53
  // Esegui la chiamata API solo se il pannello è visibile E i dati non sono già stati caricati
27
54
  // O, se vuoi ricaricare ogni volta che diventa visibile (ma è meno efficiente per "pesante")
@@ -46,6 +46,7 @@ interface ITMDcmtFormProps {
46
46
  mid: number;
47
47
  value: string;
48
48
  }>;
49
+ openS4TViewer?: boolean;
49
50
  onOpenS4TViewerRequest?: (dcmtInfo: Array<DcmtInfo>, onRefreshSearchAsync?: (() => Promise<void>)) => void;
50
51
  s4TViewerDialogComponent?: React.ReactNode;
51
52
  enableDragDropOverlay?: boolean;