@topconsultnpm/sdkui-react 6.22.0-t1 → 6.22.0-t2

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 (159) hide show
  1. package/lib/components/NewComponents/ContextMenu/TMContextMenu.js +1 -1
  2. package/lib/components/NewComponents/ContextMenu/styles.d.ts +45 -43
  3. package/lib/components/NewComponents/FloatingMenuBar/TMFloatingMenuBar.js +1 -1
  4. package/lib/components/NewComponents/FloatingMenuBar/styles.d.ts +67 -60
  5. package/lib/components/base/Styled.d.ts +92 -76
  6. package/lib/components/base/TMButton.d.ts +13 -0
  7. package/lib/components/base/TMButton.js +137 -5
  8. package/lib/components/base/TMDataGrid.d.ts +2 -0
  9. package/lib/components/base/TMDataGrid.js +260 -8
  10. package/lib/components/base/TMDataGridExportForm.js +39 -30
  11. package/lib/components/base/TMEditorBase.d.ts +2 -0
  12. package/lib/components/base/TMFileManager.js +5 -3
  13. package/lib/components/base/TMFileManagerDataGridView.js +11 -2
  14. package/lib/components/base/TMFileManagerThumbnailItems.js +19 -9
  15. package/lib/components/base/TMFileManagerUtils.d.ts +6 -6
  16. package/lib/components/base/TMFileManagerUtils.js +37 -1
  17. package/lib/components/base/TMModal.d.ts +11 -0
  18. package/lib/components/base/TMModal.js +47 -13
  19. package/lib/components/base/TMPanel.d.ts +6 -0
  20. package/lib/components/base/TMPanel.js +138 -25
  21. package/lib/components/base/TMPopUp.js +3 -0
  22. package/lib/components/base/TMTooltip.d.ts +2 -1
  23. package/lib/components/base/TMTooltip.js +24 -8
  24. package/lib/components/base/TMTreeView.d.ts +13 -7
  25. package/lib/components/base/TMTreeView.js +108 -67
  26. package/lib/components/choosers/TMDcmtTypeChooser.js +4 -3
  27. package/lib/components/choosers/TMDistinctValues.d.ts +9 -7
  28. package/lib/components/choosers/TMDistinctValues.js +149 -142
  29. package/lib/components/choosers/TMMetadataChooser.js +1 -1
  30. package/lib/components/choosers/TMQuickSearchInfo.d.ts +13 -0
  31. package/lib/components/choosers/TMQuickSearchInfo.js +279 -0
  32. package/lib/components/choosers/TMQuickSearchSettingsForm.d.ts +16 -0
  33. package/lib/components/choosers/TMQuickSearchSettingsForm.js +94 -0
  34. package/lib/components/choosers/TMSelectedValuesSummary.d.ts +13 -0
  35. package/lib/components/choosers/TMSelectedValuesSummary.js +91 -0
  36. package/lib/components/editors/TMDateBox.d.ts +16 -1
  37. package/lib/components/editors/TMDateBox.js +90 -22
  38. package/lib/components/editors/TMDropDown.d.ts +3 -0
  39. package/lib/components/editors/TMDropDown.js +197 -5
  40. package/lib/components/editors/TMEditorStyled.d.ts +20 -42
  41. package/lib/components/editors/TMMetadataEditor.d.ts +5 -1
  42. package/lib/components/editors/TMMetadataEditor.js +27 -23
  43. package/lib/components/editors/TMTextArea.d.ts +1 -0
  44. package/lib/components/editors/TMTextArea.js +23 -15
  45. package/lib/components/editors/TMTextBox.d.ts +2 -0
  46. package/lib/components/editors/TMTextBox.js +30 -19
  47. package/lib/components/features/archive/TMArchive.js +17 -3
  48. package/lib/components/features/documents/TMBatchUpdateForm.js +19 -5
  49. package/lib/components/features/documents/TMDcmtBlog.d.ts +3 -0
  50. package/lib/components/features/documents/TMDcmtBlog.js +4 -4
  51. package/lib/components/features/documents/TMDcmtForm.d.ts +7 -0
  52. package/lib/components/features/documents/TMDcmtForm.js +232 -53
  53. package/lib/components/features/documents/TMDcmtFormActionButtons.d.ts +11 -0
  54. package/lib/components/features/documents/TMDcmtFormActionButtons.js +77 -7
  55. package/lib/components/features/documents/TMDcmtIcon.js +5 -3
  56. package/lib/components/features/documents/TMDcmtPreview.d.ts +7 -5
  57. package/lib/components/features/documents/TMDcmtPreview.js +10 -6
  58. package/lib/components/features/documents/TMMasterDetailDcmts.js +95 -33
  59. package/lib/components/features/documents/TMMasterInfoFields.d.ts +20 -0
  60. package/lib/components/features/documents/TMMasterInfoFields.js +186 -0
  61. package/lib/components/features/documents/TMMergeToPdfForm.d.ts +1 -2
  62. package/lib/components/features/documents/TMMergeToPdfForm.js +30 -5
  63. package/lib/components/features/documents/TMMultiMasterDetailDcmtsForm.d.ts +46 -0
  64. package/lib/components/features/documents/TMMultiMasterDetailDcmtsForm.js +323 -0
  65. package/lib/components/features/documents/TMMultiMasterDetailDcmtsUtils.d.ts +370 -0
  66. package/lib/components/features/documents/TMMultiMasterDetailDcmtsUtils.js +283 -0
  67. package/lib/components/features/documents/TMRelationViewer.d.ts +14 -1
  68. package/lib/components/features/documents/TMRelationViewer.js +82 -52
  69. package/lib/components/features/search/TMSearch.js +103 -24
  70. package/lib/components/features/search/TMSearchQueryEditor.js +5 -3
  71. package/lib/components/features/search/TMSearchQueryPanel.d.ts +14 -4
  72. package/lib/components/features/search/TMSearchQueryPanel.js +152 -57
  73. package/lib/components/features/search/TMSearchResult.d.ts +16 -0
  74. package/lib/components/features/search/TMSearchResult.js +438 -145
  75. package/lib/components/features/search/TMTreeSelector.js +109 -64
  76. package/lib/components/features/workflow/TMWorkflowPopup.d.ts +10 -0
  77. package/lib/components/features/workflow/TMWorkflowPopup.js +8 -4
  78. package/lib/components/features/workflow/diagram/WorkitemRecipientsEditor.d.ts +3 -1
  79. package/lib/components/forms/Login/ChangePasswordInputs.d.ts +3 -1
  80. package/lib/components/grids/TMBlogAttachments.js +13 -2
  81. package/lib/components/grids/TMBlogHeader.js +5 -1
  82. package/lib/components/grids/TMBlogsPost.d.ts +2 -0
  83. package/lib/components/grids/TMBlogsPost.js +31 -4
  84. package/lib/components/grids/TMBlogsPostUtils.d.ts +1 -0
  85. package/lib/components/grids/TMBlogsPostUtils.js +14 -3
  86. package/lib/components/layout/panelManager/TMPanelManagerContainer.d.ts +1 -0
  87. package/lib/components/layout/panelManager/TMPanelManagerContainer.js +24 -14
  88. package/lib/components/layout/panelManager/TMPanelManagerContext.js +68 -93
  89. package/lib/components/layout/panelManager/TMPanelManagerToolbar.d.ts +4 -5
  90. package/lib/components/layout/panelManager/TMPanelWrapper.js +2 -2
  91. package/lib/components/layout/panelManager/types.d.ts +3 -0
  92. package/lib/components/layout/panelManager/utils.js +13 -11
  93. package/lib/components/query/TMQueryEditor.d.ts +14 -10
  94. package/lib/components/sidebar/TMCommandsPanel.d.ts +4 -4
  95. package/lib/components/sidebar/TMHeader.js +30 -20
  96. package/lib/components/viewers/TMMidViewer.js +5 -2
  97. package/lib/components/viewers/TMTidViewer.js +86 -25
  98. package/lib/css/tm-sdkui.css +1 -1
  99. package/lib/devextreme-license.d.ts +1 -0
  100. package/lib/devextreme-license.js +3 -0
  101. package/lib/helper/Globalization.d.ts +1 -1
  102. package/lib/helper/SDKUI_Globals.d.ts +110 -0
  103. package/lib/helper/SDKUI_Globals.js +151 -0
  104. package/lib/helper/SDKUI_Localizator.d.ts +55 -0
  105. package/lib/helper/SDKUI_Localizator.js +550 -0
  106. package/lib/helper/TMAppUpdatedModal.d.ts +2 -0
  107. package/lib/helper/TMAppUpdatedModal.js +134 -0
  108. package/lib/helper/TMIcons.d.ts +1 -0
  109. package/lib/helper/TMIcons.js +3 -0
  110. package/lib/helper/TMUtils.d.ts +51 -4
  111. package/lib/helper/TMUtils.js +272 -11
  112. package/lib/helper/dataGridSearchHelper.d.ts +28 -0
  113. package/lib/helper/dataGridSearchHelper.js +51 -0
  114. package/lib/helper/dcmtsHelper.d.ts +20 -0
  115. package/lib/helper/dcmtsHelper.js +58 -1
  116. package/lib/helper/helpers.d.ts +23 -1
  117. package/lib/helper/helpers.js +57 -1
  118. package/lib/helper/index.d.ts +2 -0
  119. package/lib/helper/index.js +2 -0
  120. package/lib/helper/queryHelper.d.ts +6 -1
  121. package/lib/helper/queryHelper.js +8 -3
  122. package/lib/hooks/tmDistinctValuesGridHelper.d.ts +16 -0
  123. package/lib/hooks/tmDistinctValuesGridHelper.js +31 -0
  124. package/lib/hooks/useArchiveListForm.d.ts +39 -0
  125. package/lib/hooks/useArchiveListForm.js +46 -0
  126. package/lib/hooks/useCaseFlowApprove.d.ts +56 -0
  127. package/lib/hooks/useCaseFlowApprove.js +157 -0
  128. package/lib/hooks/useDataUserIdItem.d.ts +1 -0
  129. package/lib/hooks/useDataUserIdItem.js +1 -0
  130. package/lib/hooks/useDocumentOperations.d.ts +4 -0
  131. package/lib/hooks/useDocumentOperations.js +52 -10
  132. package/lib/hooks/useMultiMasterDetailDcmts.d.ts +113 -0
  133. package/lib/hooks/useMultiMasterDetailDcmts.js +760 -0
  134. package/lib/hooks/useRelatedDocuments.d.ts +1 -0
  135. package/lib/hooks/useRelatedDocuments.js +22 -27
  136. package/lib/hooks/useShowDossiersV5.d.ts +12 -0
  137. package/lib/hooks/useShowDossiersV5.js +37 -0
  138. package/lib/hooks/useTMDistinctValuesMetadataDisplay.d.ts +18 -0
  139. package/lib/hooks/useTMDistinctValuesMetadataDisplay.js +103 -0
  140. package/lib/hooks/useTMDistinctValuesQuickSearch.d.ts +49 -0
  141. package/lib/hooks/useTMDistinctValuesQuickSearch.js +307 -0
  142. package/lib/hooks/useTMDistinctValuesSelection.d.ts +42 -0
  143. package/lib/hooks/useTMDistinctValuesSelection.js +103 -0
  144. package/lib/hooks/useTMDistinctValuesSource.d.ts +29 -0
  145. package/lib/hooks/useTMDistinctValuesSource.js +105 -0
  146. package/lib/index.d.ts +2 -0
  147. package/lib/index.js +5 -2
  148. package/lib/services/caseflow/CaseFlowService.d.ts +236 -0
  149. package/lib/services/caseflow/CaseFlowService.js +693 -0
  150. package/lib/services/caseflow/CaseFlowTemplateCacheService.d.ts +28 -0
  151. package/lib/services/caseflow/CaseFlowTemplateCacheService.js +121 -0
  152. package/lib/services/caseflow/caseFlowNameCache.d.ts +32 -0
  153. package/lib/services/caseflow/caseFlowNameCache.js +116 -0
  154. package/lib/services/caseflow/caseFlowWorkItemUtils.d.ts +61 -0
  155. package/lib/services/caseflow/caseFlowWorkItemUtils.js +113 -0
  156. package/lib/services/index.d.ts +4 -0
  157. package/lib/services/index.js +4 -0
  158. package/lib/services/platform_services.d.ts +1 -1
  159. package/package.json +9 -5
@@ -0,0 +1,94 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { ResultTypes, SavedQueryCacheService, SDK_Localizator, ValidationItem } from '@topconsultnpm/sdk-ts';
4
+ import styled from 'styled-components';
5
+ import { calcIsModified, calcResponsiveSizes, DEFAULT_QUICK_SEARCH_MAX_DCMTS, IconClear, IconDelete, IconMetadata, SDKUI_Localizator, SYS_ALL_DCMTS_SQD_ID } from '../../helper';
6
+ import { FormModes } from '../../ts';
7
+ import ShowAlert from '../base/TMAlert';
8
+ import TMButton from '../base/TMButton';
9
+ import { useDeviceType } from '../base/TMDeviceProvider';
10
+ import { TMExceptionBoxManager } from '../base/TMPopUp';
11
+ import TMSpinner from '../base/TMSpinner';
12
+ import TMDropDown from '../editors/TMDropDown';
13
+ import TMTextBox from '../editors/TMTextBox';
14
+ import TMSaveForm from '../forms/TMSaveForm';
15
+ import TMDcmtTypeChooser from './TMDcmtTypeChooser';
16
+ import TMMetadataChooser from './TMMetadataChooser';
17
+ import { useTMQuickSearchInfo } from './TMQuickSearchInfo';
18
+ const StyledFormContent = styled.div `display: flex; flex-direction: column; gap: 12px; padding: 10px 15px;`;
19
+ /** Impostazione vuota da cui parte la configurazione: solo il numero massimo di documenti ha un valore predefinito */
20
+ export const newQuickSearchSettings = () => ({
21
+ tid: undefined,
22
+ sqdId: undefined,
23
+ mid: undefined,
24
+ maxDcmtsToBeReturned: DEFAULT_QUICK_SEARCH_MAX_DCMTS,
25
+ });
26
+ const TMQuickSearchSettingsForm = ({ settings, onSave, onDelete, onClose }) => {
27
+ const initialSettings = useMemo(() => settings ?? newQuickSearchSettings(), [settings]);
28
+ const deviceType = useDeviceType();
29
+ const [formData, setFormData] = useState(initialSettings);
30
+ // Finché è undefined la cache delle ricerche rapide non è ancora stata caricata
31
+ const [allSQDs, setAllSQDs] = useState();
32
+ // Icona informativa della ricerca rapida scelta: sta fra i pulsanti del suo editor, il dettaglio si apre a parte
33
+ const { infoButton, detailForm } = useTMQuickSearchInfo({ tid: formData.tid, sqdId: formData.sqdId });
34
+ useEffect(() => { loadSQDsAsync(); }, []);
35
+ // L'avviso ha senso solo a cache caricata: prima le ricerche rapide risulterebbero assenti per tutti i tipi documento
36
+ useEffect(() => {
37
+ if (!formData.tid || !allSQDs)
38
+ return;
39
+ if (allSQDs.some(o => o.masterTID === formData.tid))
40
+ return;
41
+ ShowAlert({ mode: 'warning', title: SDK_Localizator.SavedQuery, message: 'Non ci sono ricerche rapide per il tipo documento selezionato', duration: 4000 });
42
+ }, [formData.tid, allSQDs]);
43
+ const loadSQDsAsync = async () => {
44
+ try {
45
+ TMSpinner.show({ description: `${SDKUI_Localizator.Loading} - ${SDK_Localizator.SavedQuery} ...` });
46
+ setAllSQDs(await SavedQueryCacheService.GetAllAsync() ?? []);
47
+ }
48
+ catch (e) {
49
+ TMExceptionBoxManager.show({ exception: e });
50
+ }
51
+ finally {
52
+ TMSpinner.hide();
53
+ }
54
+ };
55
+ // Stessa logica di TMSearch: in testa all'elenco c'è la ricerca di sistema con tutti i documenti del tipo documento
56
+ const sqdsDataSource = useMemo(() => {
57
+ if (!formData.tid)
58
+ return [];
59
+ return [
60
+ { value: SYS_ALL_DCMTS_SQD_ID, display: SDKUI_Localizator.AllDcmts },
61
+ ...(allSQDs ?? []).filter(o => o.masterTID === formData.tid).map(o => ({ value: o.id, display: o.name ?? `ID ${o.id}` })),
62
+ ];
63
+ }, [allSQDs, formData.tid]);
64
+ const validationItems = useMemo(() => {
65
+ const vil = [];
66
+ if (!formData.tid)
67
+ vil.push(new ValidationItem(ResultTypes.ERROR, SDKUI_Localizator.DcmtType, SDKUI_Localizator.RequiredField));
68
+ if (!formData.sqdId)
69
+ vil.push(new ValidationItem(ResultTypes.ERROR, SDK_Localizator.SavedQuery, SDKUI_Localizator.RequiredField));
70
+ if (!formData.mid)
71
+ vil.push(new ValidationItem(ResultTypes.ERROR, SDK_Localizator.Metadata, SDKUI_Localizator.RequiredField));
72
+ if (!formData.maxDcmtsToBeReturned || formData.maxDcmtsToBeReturned <= 0)
73
+ vil.push(new ValidationItem(ResultTypes.ERROR, SDKUI_Localizator.MaxDcmtsToBeReturned, SDKUI_Localizator.RequiredField));
74
+ return vil;
75
+ }, [formData]);
76
+ // Ogni editor mostra solo gli errori del proprio campo
77
+ const validationItemsOf = (propertyName) => validationItems.filter(o => o.PropertyName === propertyName);
78
+ const patchFormData = (changes) => setFormData(prev => ({ ...prev, ...changes }));
79
+ /** Cambiare tipo documento rende privi di senso la ricerca rapida e il metadato scelti prima */
80
+ const onTidChanged = (tids) => {
81
+ const tid = tids?.[0];
82
+ if (tid === formData.tid)
83
+ return;
84
+ patchFormData({ tid, sqdId: undefined, mid: undefined });
85
+ };
86
+ const onSaveAsync = async () => { onSave?.(formData); };
87
+ /* Ricomincia da capo la configurazione: cambiare tipo documento azzera già ricerca rapida e metadato, ma non
88
+ permette di tornare alla scelta vuota, da cui l'elenco delle ricerche rapide riparte senza filtri */
89
+ const isFormDataEmpty = !formData.tid && !formData.sqdId && !formData.mid && formData.maxDcmtsToBeReturned === DEFAULT_QUICK_SEARCH_MAX_DCMTS;
90
+ const onClearClick = () => setFormData(newQuickSearchSettings());
91
+ return (_jsx(TMSaveForm, { title: `${SDKUI_Localizator.DistinctValues} - ${SDK_Localizator.SavedQuery}`, showTitleFormMode: false, formMode: FormModes.Update, isModal: true, width: calcResponsiveSizes(deviceType, '600px', '90%', '95%'), height: calcResponsiveSizes(deviceType, '500px', '90%', '95%'), showBackButton: false, hasNavigation: false, skipIsModifiedCheck: true, validationItems: validationItems, isModified: calcIsModified(formData, initialSettings), customToolbarElements: _jsxs(_Fragment, { children: [_jsx(TMButton, { btnStyle: 'toolbar', caption: SDKUI_Localizator.Clear, icon: _jsx(IconClear, {}), disabled: isFormDataEmpty, onClick: onClearClick }), onDelete && settings &&
92
+ _jsx(TMButton, { btnStyle: 'toolbar', color: 'error', caption: SDKUI_Localizator.Delete, icon: _jsx(IconDelete, {}), onClick: onDelete })] }), onUndo: () => setFormData(initialSettings), onSaveAsync: onSaveAsync, onClose: onClose, children: _jsxs(StyledFormContent, { children: [_jsx(TMDcmtTypeChooser, { label: SDKUI_Localizator.DcmtType, placeHolder: SDKUI_Localizator.DcmtTypeSelect, accessFilter: 'canSearch', values: formData.tid ? [formData.tid] : [], isModifiedWhen: formData.tid !== initialSettings.tid, validationItems: validationItemsOf(SDKUI_Localizator.DcmtType), onValueChanged: onTidChanged }), _jsx(TMDropDown, { searchEnabled: true, usePortal: true, label: SDK_Localizator.SavedQuery, placeHolder: `<${SDKUI_Localizator.NoneSelection}>`, disabled: !formData.tid, value: formData.sqdId ?? '', dataSource: sqdsDataSource, buttons: infoButton ? [infoButton] : [], isModifiedWhen: formData.sqdId !== initialSettings.sqdId, validationItems: validationItemsOf(SDK_Localizator.SavedQuery), onValueChanged: (e) => patchFormData({ sqdId: Number(e.target.value) }) }), _jsx(TMMetadataChooser, { icon: _jsx(IconMetadata, {}), label: SDK_Localizator.Metadata, placeHolder: SDKUI_Localizator.SelectMetadata, disabled: !formData.tid || !formData.sqdId, allowSysMetadata: false, tids: formData.tid ? [formData.tid] : [], values: formData.mid ? [{ tid: formData.tid, mid: formData.mid }] : [], isModifiedWhen: formData.mid !== initialSettings.mid, validationItems: validationItemsOf(SDK_Localizator.Metadata), onValueChanged: (values) => patchFormData({ mid: values?.[0]?.mid }) }), _jsx(TMTextBox, { type: 'number', label: SDKUI_Localizator.MaxDcmtsToBeReturned, minValue: 1, value: formData.maxDcmtsToBeReturned, isModifiedWhen: formData.maxDcmtsToBeReturned !== initialSettings.maxDcmtsToBeReturned, validationItems: validationItemsOf(SDKUI_Localizator.MaxDcmtsToBeReturned), onValueChanged: (e) => patchFormData({ maxDcmtsToBeReturned: Number(e.target.value) || DEFAULT_QUICK_SEARCH_MAX_DCMTS }) }), detailForm] }) }));
93
+ };
94
+ export default TMQuickSearchSettingsForm;
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ /** Valore selezionato: value è quello che finisce nel campo del form, display quello mostrato all'utente */
3
+ export type SelectedValueItem = {
4
+ value: string;
5
+ display: string;
6
+ };
7
+ interface ITMSelectedValuesSummary {
8
+ items: Array<SelectedValueItem>;
9
+ onRemove: (value: string) => void;
10
+ onClear: () => void;
11
+ }
12
+ declare const TMSelectedValuesSummary: React.FC<ITMSelectedValuesSummary>;
13
+ export default TMSelectedValuesSummary;
@@ -0,0 +1,91 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import styled from 'styled-components';
3
+ import { IconCloseOutline, IconEraser, IconSelected, SDKUI_Localizator } from '../../helper';
4
+ import TMButton from '../base/TMButton';
5
+ import TMTooltip from '../base/TMTooltip';
6
+ import { TMColors } from '../../utils/theme';
7
+ /* Riepilogo dei valori selezionati: sta sopra la griglia perché è il contenuto che finirà nel campo del form.
8
+ Titolo, chip e pulsante di pulizia stanno su un'unica riga per rubare il minor spazio possibile alla griglia */
9
+ const StyledSelectionSummary = styled.div `
10
+ display: flex;
11
+ flex-direction: row;
12
+ /* Titolo, chip e azioni centrati fra loro: con una sola riga di chip la barra non risulta schiacciata in alto */
13
+ align-items: center;
14
+ gap: 8px;
15
+ padding: 8px;
16
+ background-color: ${TMColors.primary_container};
17
+ border: 1px solid ${TMColors.primary};
18
+ border-radius: 6px;
19
+ `;
20
+ const StyledSelectionTitle = styled.div `
21
+ display: flex;
22
+ flex-direction: row;
23
+ align-items: center;
24
+ flex-shrink: 0;
25
+ gap: 4px;
26
+ min-height: 28px;
27
+ font-size: 0.8rem;
28
+ font-weight: 600;
29
+ text-transform: uppercase;
30
+ letter-spacing: 0.3px;
31
+ white-space: nowrap;
32
+ color: ${TMColors.primary};
33
+
34
+ svg { flex-shrink: 0; }
35
+ `;
36
+ const StyledSelectionCounter = styled.span `
37
+ display: inline-flex;
38
+ align-items: center;
39
+ justify-content: center;
40
+ min-width: 20px;
41
+ height: 20px;
42
+ padding: 0 5px;
43
+ border-radius: 10px;
44
+ background-color: ${TMColors.primary};
45
+ color: white;
46
+ font-size: 0.75rem;
47
+ font-weight: 600;
48
+ `;
49
+ /* max-height: due righe di chip (28px) più il gap: oltre si scorre invece di rubare spazio alla griglia */
50
+ const StyledSelectionChips = styled.div `display: flex; flex: 1; flex-direction: row; flex-wrap: wrap; align-content: flex-start; gap: 4px; max-height: 60px; overflow-y: auto;`;
51
+ const StyledChip = styled.div `
52
+ display: inline-flex;
53
+ flex-direction: row;
54
+ align-items: center;
55
+ gap: 4px;
56
+ max-width: 320px;
57
+ height: 28px;
58
+ padding: 0 4px 0 10px;
59
+ border: 1px solid ${TMColors.primary};
60
+ border-radius: 14px;
61
+ background-color: ${TMColors.default_background};
62
+ color: ${TMColors.text_normal};
63
+ font-size: 0.875rem;
64
+ `;
65
+ const StyledChipLabel = styled.span `overflow: hidden; text-overflow: ellipsis; white-space: nowrap;`;
66
+ const StyledChipRemove = styled.button `
67
+ display: inline-flex;
68
+ align-items: center;
69
+ justify-content: center;
70
+ flex-shrink: 0;
71
+ width: 20px;
72
+ height: 20px;
73
+ padding: 0;
74
+ border: none;
75
+ border-radius: 50%;
76
+ background-color: transparent;
77
+ color: ${TMColors.button_icon};
78
+ cursor: pointer;
79
+
80
+ &:hover { background-color: ${TMColors.primary}; color: white; }
81
+ `;
82
+ /* Il pulsante di pulizia non deve scorrere con i chip né stringersi */
83
+ const StyledSelectionActions = styled.div `display: flex; flex-shrink: 0; align-items: center; min-height: 28px;`;
84
+ const TMSelectedValuesSummary = ({ items, onRemove, onClear }) => {
85
+ if (items.length <= 0)
86
+ return null;
87
+ return (_jsxs(StyledSelectionSummary, { children: [_jsxs(StyledSelectionTitle, { children: [_jsx(IconSelected, { fontSize: 17, color: TMColors.primary }), _jsx("span", { children: SDKUI_Localizator.Selected }), _jsx(StyledSelectionCounter, { children: items.length })] }), _jsx(StyledSelectionChips, { children: items.map((item) =>
88
+ /* Il valore lungo viene troncato nel chip: il tooltip è l'unico modo per leggerlo per intero */
89
+ _jsx(TMTooltip, { content: item.display, children: _jsxs(StyledChip, { children: [_jsx(StyledChipLabel, { children: item.display }), _jsx(StyledChipRemove, { type: 'button', title: SDKUI_Localizator.Remove, onClick: () => onRemove(item.value), children: _jsx(IconCloseOutline, { fontSize: 13 }) })] }) }, item.value)) }), _jsx(StyledSelectionActions, { children: _jsx(TMButton, { btnStyle: 'toolbar', caption: SDKUI_Localizator.Clear, icon: _jsx(IconEraser, {}), onClick: onClear }) })] }));
90
+ };
91
+ export default TMSelectedValuesSummary;
@@ -1,17 +1,32 @@
1
1
  import React from 'react';
2
- import { ValidationItem } from '@topconsultnpm/sdk-ts';
2
+ import { MetadataFormats, ValidationItem } from '@topconsultnpm/sdk-ts';
3
3
  import { IDateBoxOptions } from 'devextreme-react/date-box';
4
4
  import { DateDisplayTypes } from '../../helper';
5
5
  interface ITMDateBoxProps extends IDateBoxOptions {
6
+ /** Tipo di picker da mostrare: Date (solo calendario), Time (solo ora), DateTime (entrambi). Determina quali controlli appaiono nel dropdown. */
6
7
  dateDisplayType?: DateDisplayTypes;
8
+ /** Lista di errori/warning di validazione da mostrare sotto il campo */
7
9
  validationItems?: ValidationItem[];
10
+ /** Se true, evidenzia il campo come modificato (bordo e testo colorati) */
8
11
  isModifiedWhen?: boolean;
12
+ /** Se true, serializza la data nel formato ISO 'yyyy-MM-ddTHH:mm:ss' */
9
13
  useDateSerializationFormat?: boolean;
14
+ /** Elemento DOM contenitore per il dropdown del calendario */
10
15
  containerElement?: Element;
16
+ /** Pulsanti aggiuntivi da mostrare nel campo */
11
17
  buttons?: any;
18
+ /** Se true (default), resetta l'ora a 00:00:00 quando si preme un tasto */
12
19
  resetTimeToZeroOnKeyPress?: boolean;
20
+ /** Icona da mostrare a sinistra del campo */
13
21
  icon?: any;
22
+ /** Padding del contenitore esterno */
14
23
  padding?: string;
24
+ /** Cultura/locale per la formattazione (es. 'en-gb', 'it-it'). Usata per tradurre i nomi dei mesi/giorni nella lingua corretta. */
25
+ formatCulture?: string;
26
+ /** Formato di visualizzazione della data (es. ShortDate='17/07/2026', LongDate='17 July 2026'). Determina come la data viene rappresentata come stringa nel campo. */
27
+ metadataFormat?: MetadataFormats;
28
+ /** Messaggio personalizzato per data non valida. Se non specificato, genera automaticamente un messaggio con esempio del formato (es. 'Formato richiesto: 20/07/2026') */
29
+ invalidDateMessage?: string;
15
30
  }
16
31
  declare const TMDateBox: React.FC<ITMDateBoxProps>;
17
32
  export default TMDateBox;
@@ -1,35 +1,103 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useId, useRef } from 'react';
2
+ import { useEffect, useId, useMemo, useRef } from 'react';
3
3
  import { DateBox } from 'devextreme-react';
4
- import { DateDisplayTypes, Globalization } from '../../helper';
4
+ import { MetadataFormats } from '@topconsultnpm/sdk-ts';
5
+ import { DateDisplayTypes, Globalization, SDKUI_Localizator } from '../../helper';
5
6
  import { TMColors } from '../../utils/theme';
6
7
  import TMVilViewer from '../base/TMVilViewer';
8
+ import { formatDateTimeByMetadataFormat } from '../../helper/TMUtils';
7
9
  const TMDateBox = (props) => {
8
- const resetTimeToZero = props.resetTimeToZeroOnKeyPress ?? true;
10
+ const { dateDisplayType, validationItems, isModifiedWhen, useDateSerializationFormat, containerElement, resetTimeToZeroOnKeyPress, icon, padding, formatCulture, metadataFormat, invalidDateMessage, readOnly, showClearButton, disabled, displayFormat, label, type, value, width, onValueChange, onInitialized, onContentReady, placeholder, name, inputAttr } = props;
11
+ const resetTimeToZero = resetTimeToZeroOnKeyPress ?? true;
9
12
  const autoId = useId();
10
- const effectiveInputAttr = { id: autoId, ...props.inputAttr };
13
+ const effectiveInputAttr = { id: autoId, ...inputAttr };
14
+ // Determina se il formato richiede nomi testuali (mesi/giorni) che dipendono dalla cultura
15
+ const requiresTextualFormat = useMemo(() => {
16
+ // Solo i formati con "Long" nella data hanno nomi di mesi/giorni che variano per cultura
17
+ return metadataFormat === MetadataFormats.LongDate ||
18
+ metadataFormat === MetadataFormats.LongDateShortTime ||
19
+ metadataFormat === MetadataFormats.LongDateLongTime;
20
+ }, [metadataFormat]);
21
+ // Funzione di formattazione personalizzata per rispettare formatCulture
22
+ // Usata SOLO per formati con nomi testuali (LongDate, LongDateShortTime, LongDateLongTime) e se formatCulture è specificato
23
+ const customDisplayFormat = useMemo(() => {
24
+ // Usa funzione custom solo se c'è formatCulture E il formato richiede nomi testuali
25
+ if (!formatCulture || !requiresTextualFormat)
26
+ return undefined;
27
+ // Restituisce una funzione che DevExtreme userà per formattare la data
28
+ return (date) => {
29
+ try {
30
+ if (!date)
31
+ return '';
32
+ // Assicurati che sia un oggetto Date valido
33
+ const dateObj = date instanceof Date ? date : new Date(date);
34
+ if (isNaN(dateObj.getTime()))
35
+ return '';
36
+ return formatDateTimeByMetadataFormat(dateObj, metadataFormat, formatCulture);
37
+ }
38
+ catch {
39
+ // Fallback: restituisce stringa ISO o vuota in caso di errore
40
+ try {
41
+ return date instanceof Date ? date.toISOString() : String(date ?? '');
42
+ }
43
+ catch {
44
+ return '';
45
+ }
46
+ }
47
+ };
48
+ }, [formatCulture, metadataFormat, requiresTextualFormat]);
49
+ // Genera il messaggio di errore con esempio del formato supportato
50
+ const computedInvalidDateMessage = useMemo(() => {
51
+ try {
52
+ if (invalidDateMessage)
53
+ return invalidDateMessage;
54
+ // Genera un esempio usando la data corrente
55
+ const exampleDate = new Date();
56
+ let exampleFormatted;
57
+ if (customDisplayFormat) {
58
+ // Usa la funzione custom se disponibile
59
+ exampleFormatted = customDisplayFormat(exampleDate);
60
+ }
61
+ else if (typeof displayFormat === 'string') {
62
+ // Usa formatDateTimeByMetadataFormat se abbiamo un formato stringa
63
+ exampleFormatted = formatDateTimeByMetadataFormat(exampleDate, metadataFormat, formatCulture) || displayFormat;
64
+ }
65
+ else {
66
+ // Fallback: usa il formato di Globalization o localizzazione standard
67
+ const globFormat = Globalization.getDateDisplayFormat(dateDisplayType);
68
+ exampleFormatted = typeof globFormat === 'string' ? globFormat : exampleDate.toLocaleDateString(formatCulture || 'it-IT');
69
+ }
70
+ if (!exampleFormatted)
71
+ return SDKUI_Localizator.ValueMustBeDateOrTime;
72
+ return `${SDKUI_Localizator.RequiredFormat}: ${exampleFormatted}`;
73
+ }
74
+ catch {
75
+ // In caso di errore, usa il messaggio generico tradotto
76
+ return SDKUI_Localizator.ValueMustBeDateOrTime;
77
+ }
78
+ }, [invalidDateMessage, customDisplayFormat, displayFormat, dateDisplayType, metadataFormat, formatCulture]);
11
79
  useEffect(() => {
12
80
  let htmlElement = dateBoxRef?.current?.instance().element();
13
81
  if (!htmlElement)
14
82
  return;
15
- let borderColor = props.isModifiedWhen ? TMColors.tertiary : TMColors.border_normal;
16
- let textColor = props.isModifiedWhen ? TMColors.isModified : TMColors.text_normal;
83
+ let borderColor = isModifiedWhen ? TMColors.tertiary : TMColors.border_normal;
84
+ let textColor = isModifiedWhen ? TMColors.isModified : TMColors.text_normal;
17
85
  let inputContainer = htmlElement.getElementsByClassName("dx-texteditor-input-container")[0];
18
86
  if (inputContainer) {
19
- inputContainer.style.background = props.readOnly ? 'linear-gradient(white 20%, #d6d6d6 80%)' : '';
20
- inputContainer.style.color = props.readOnly ? '#525252' : textColor;
87
+ inputContainer.style.background = readOnly ? 'linear-gradient(white 20%, #d6d6d6 80%)' : '';
88
+ inputContainer.style.color = readOnly ? '#525252' : textColor;
21
89
  }
22
90
  let input = htmlElement.querySelector(".dx-texteditor-input");
23
91
  if (input) {
24
- if (!props.readOnly) {
92
+ if (!readOnly) {
25
93
  input.style.color = textColor;
26
94
  }
27
95
  input.style.fontSize = 'var(--base-font-size)';
28
96
  input.style.fontFamily = 'var(--base-font-family)';
29
97
  }
30
- let label = htmlElement.getElementsByClassName("dx-label")[0];
31
- if (label) {
32
- label.style.borderBottomColor = borderColor;
98
+ let labelEl = htmlElement.getElementsByClassName("dx-label")[0];
99
+ if (labelEl) {
100
+ labelEl.style.borderBottomColor = borderColor;
33
101
  }
34
102
  let labelBefore = htmlElement.getElementsByClassName("dx-label-before")[0];
35
103
  if (labelBefore) {
@@ -43,13 +111,13 @@ const TMDateBox = (props) => {
43
111
  labelAfter.style.borderTopColor = borderColor;
44
112
  labelAfter.style.borderInlineEndColor = borderColor;
45
113
  }
46
- }, [props.isModifiedWhen, props.readOnly]);
114
+ }, [isModifiedWhen, readOnly]);
47
115
  const dateBoxRef = useRef(null);
48
116
  // soluzione tratta da https://supportcenter.devexpress.com/ticket/details/t1025617/setting-default-time-in-datebox
49
117
  const dropDownOptions = {
50
- container: props.containerElement,
118
+ container: containerElement,
51
119
  onShown: (e) => {
52
- if (props.value != undefined)
120
+ if (value != undefined)
53
121
  return;
54
122
  let dateBoxInstance = dateBoxRef.current?.instance();
55
123
  const currentDate = new Date();
@@ -59,13 +127,13 @@ const TMDateBox = (props) => {
59
127
  }
60
128
  };
61
129
  const getType = () => {
62
- if (props.type != undefined)
63
- return props.type;
64
- if (!props.dateDisplayType || props.dateDisplayType == DateDisplayTypes.DateTime)
130
+ if (type != undefined)
131
+ return type;
132
+ if (!dateDisplayType || dateDisplayType == DateDisplayTypes.DateTime)
65
133
  return "datetime";
66
- return props.dateDisplayType == DateDisplayTypes.Date ? "date" : "time";
134
+ return dateDisplayType == DateDisplayTypes.Date ? "date" : "time";
67
135
  };
68
- return (_jsxs("div", { style: { display: 'flex', alignItems: 'center', width: '100%', padding: props.padding }, children: [props.icon && (_jsx("span", { style: { marginRight: '8px', marginTop: '8px', display: 'flex', alignItems: 'center' }, children: props.icon })), _jsxs("div", { onContextMenu: (e) => e.stopPropagation(), style: { display: 'flex', flexDirection: 'column', gap: '5px', width: '100%' }, children: [_jsx(DateBox, { readOnly: props.readOnly, ref: dateBoxRef, showClearButton: props.showClearButton, dateSerializationFormat: props.useDateSerializationFormat ? 'yyyy-MM-ddTHH:mm:ss' : undefined, disabled: props.disabled, displayFormat: props.displayFormat ?? Globalization.getDateDisplayFormat(props.dateDisplayType), dropDownOptions: dropDownOptions, label: props.label, labelMode: 'static', type: getType(), useMaskBehavior: true, height: '28px', value: props.value, width: props.width, valueChangeEvent: 'keyup input change', onValueChange: (e) => { props.onValueChange?.(e); }, onInitialized: (e) => { props.onInitialized?.(e); }, onContentReady: (e) => { props.onContentReady?.(e); }, placeholder: props.placeholder, ...(props.name && { name: props.name }), inputAttr: effectiveInputAttr, onKeyUp: (e) => {
136
+ return (_jsxs("div", { style: { display: 'flex', alignItems: 'center', width: '100%', padding: padding }, children: [icon && (_jsx("span", { style: { marginRight: '8px', marginTop: '8px', display: 'flex', alignItems: 'center' }, children: icon })), _jsxs("div", { onContextMenu: (e) => e.stopPropagation(), style: { display: 'flex', flexDirection: 'column', gap: '5px', width: '100%' }, children: [_jsx(DateBox, { readOnly: readOnly, ref: dateBoxRef, showClearButton: showClearButton, dateSerializationFormat: useDateSerializationFormat ? 'yyyy-MM-ddTHH:mm:ss' : undefined, disabled: disabled, displayFormat: customDisplayFormat ?? displayFormat ?? Globalization.getDateDisplayFormat(dateDisplayType), dropDownOptions: dropDownOptions, invalidDateMessage: computedInvalidDateMessage, label: label, labelMode: 'static', type: getType(), useMaskBehavior: !customDisplayFormat, height: '28px', value: value, width: width, valueChangeEvent: 'keyup input change', onValueChange: (e) => { onValueChange?.(e); }, onInitialized: (e) => { onInitialized?.(e); }, onContentReady: (e) => { onContentReady?.(e); }, placeholder: placeholder, ...(name && { name: name }), inputAttr: effectiveInputAttr, onKeyUp: (e) => {
69
137
  if (e.event?.code == "Space") {
70
138
  const currentDate = new Date();
71
139
  currentDate.setHours(0, 0, 0, 0);
@@ -73,7 +141,7 @@ const TMDateBox = (props) => {
73
141
  }
74
142
  }, onValueChanged: (e) => {
75
143
  if (resetTimeToZero) {
76
- if (props.value != undefined)
144
+ if (value != undefined)
77
145
  return;
78
146
  if (!e.value)
79
147
  return;
@@ -81,6 +149,6 @@ const TMDateBox = (props) => {
81
149
  currentDate.setHours(0, 0, 0, 0);
82
150
  e.component.option("value", currentDate.toISOString());
83
151
  }
84
- } }), _jsx(TMVilViewer, { vil: props.validationItems })] })] }));
152
+ } }), _jsx(TMVilViewer, { vil: validationItems })] })] }));
85
153
  };
86
154
  export default TMDateBox;
@@ -4,6 +4,9 @@ interface ITMDropDown extends ITMEditorBase {
4
4
  dataSource?: any[];
5
5
  value?: string | number | undefined;
6
6
  disabled?: boolean;
7
+ searchEnabled?: boolean;
8
+ usePortal?: boolean;
9
+ itemRender?: (item: any) => React.ReactNode;
7
10
  onValueChanged?: (e: React.ChangeEvent<HTMLSelectElement>) => void;
8
11
  }
9
12
  declare const TMDropDown: React.FC<ITMDropDown>;
@@ -1,11 +1,13 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from "react";
3
- import styled from "styled-components";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { createPortal } from "react-dom";
4
+ import styled, { css } from "styled-components";
4
5
  import { FontSize, TMColors } from '../../utils/theme';
5
6
  import { StyledEditorContainer, StyledEditorIcon, StyledEditorLabel, TMEditorsDefaultBorderRadius, editorColorManager } from "./TMEditorStyled";
6
7
  import TMLayoutContainer, { TMLayoutItem } from "../base/TMLayout";
7
8
  import TMVilViewer from "../base/TMVilViewer";
8
9
  import TMTooltip from "../base/TMTooltip";
10
+ import { SDKUI_Localizator } from "../../helper";
9
11
  const StyledDropDownWrapper = styled.div `
10
12
  position: relative;
11
13
  width: ${props => props.$width};
@@ -54,9 +56,199 @@ const StyledDropDownEditorButton = styled.div `
54
56
  align-items: center;
55
57
  cursor: pointer;
56
58
  `;
57
- const TMDropDown = ({ validationItems = [], disabled = false, elementStyle, isModifiedWhen, labelPosition = 'left', label, icon, width = '100%', fontSize = FontSize.defaultFontSize, dataSource, value, onValueChanged, buttons = [], placeHolder, }) => {
59
+ // Variante filtrabile: un input di testo con la stessa resa grafica della select nativa
60
+ const StyledSearchInput = styled.input `
61
+ width: 100%;
62
+ padding: 5px 35px 3px 9px;
63
+ border: 1px solid;
64
+ border-color: ${props => props.$isModified ? TMColors.isModified : TMColors.border_normal};
65
+ border-bottom: ${props => (props.$vil.length > 0) ? '3px' : '1px'} solid;
66
+ border-bottom-color: ${props => (props.$vil.length === 0) ? !props.$isModified ? TMColors.border_normal : TMColors.isModified : editorColorManager(props.$vil)};
67
+ color: ${props => !props.$disabled ? (props.$vil.length === 0 ? (!props.$isModified ? TMColors.text_normal : TMColors.isModified) : editorColorManager(props.$vil)) : TMColors.disabled};
68
+ border-radius: ${props => props.$borderRadius ?? TMEditorsDefaultBorderRadius};
69
+ font-size: ${props => props.$fontSize};
70
+ background-color: ${props => props.$disabled ? '#F5F5F5' : 'white'};
71
+ text-overflow: ellipsis;
72
+
73
+ &:focus {
74
+ outline: none;
75
+ border: ${props => props.$isModified ? `1px solid ${TMColors.isModified}` : '1px solid rgb(180,180,180)'};
76
+ border-bottom: 2px solid;
77
+ border-bottom-color: ${props => (props.$vil.length === 0) ? !props.$isModified ? TMColors.primary : TMColors.isModified : editorColorManager(props.$vil)};
78
+ }
79
+ `;
80
+ const OPTIONS_PANEL_MAX_HEIGHT = 220;
81
+ const OPTIONS_PANEL_MIN_HEIGHT = 80;
82
+ const OPTIONS_PANEL_MARGIN = 4;
83
+ // L'elenco aperto con usePortal esce dal contenitore dell'editor: deve stare sopra tutto ciò che può trovarsi nella stessa pagina
84
+ const DROPDOWN_OPTIONS_PANEL_Z_INDEX = 100001;
85
+ const optionsPanelCss = css `
86
+ overflow-y: auto;
87
+ background-color: white;
88
+ border: 1px solid ${TMColors.border_normal};
89
+ border-radius: ${TMEditorsDefaultBorderRadius};
90
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
91
+ `;
92
+ // Elenco ancorato all'editor: resta nel flusso del DOM e quindi soggetto al clipping dei contenitori con overflow
93
+ const StyledOptionsPanel = styled.div `
94
+ ${optionsPanelCss}
95
+ position: absolute;
96
+ top: calc(100% + 2px);
97
+ left: 0;
98
+ right: 0;
99
+ z-index: 10001;
100
+ max-height: ${OPTIONS_PANEL_MAX_HEIGHT}px;
101
+ `;
102
+ // Variante usePortal: posizione calcolata rispetto alla finestra, così l'elenco non viene tagliato da nessun contenitore
103
+ const StyledPortalOptionsPanel = styled.div `
104
+ ${optionsPanelCss}
105
+ position: fixed;
106
+ left: ${props => props.$left}px;
107
+ ${props => props.$top !== undefined ? `top: ${props.$top}px;` : ''}
108
+ ${props => props.$bottom !== undefined ? `bottom: ${props.$bottom}px;` : ''}
109
+ width: ${props => props.$width}px;
110
+ z-index: ${DROPDOWN_OPTIONS_PANEL_Z_INDEX};
111
+ max-height: ${props => props.$maxHeight}px;
112
+ `;
113
+ const StyledOption = styled.div `
114
+ padding: 5px 9px;
115
+ font-size: ${props => props.$fontSize};
116
+ cursor: pointer;
117
+ white-space: nowrap;
118
+ overflow: hidden;
119
+ text-overflow: ellipsis;
120
+ font-weight: ${props => props.$isSelected ? 600 : 'normal'};
121
+ background-color: ${props => props.$isHighlighted ? TMColors.primary_container : 'transparent'};
122
+ `;
123
+ const StyledNoOptions = styled.div `
124
+ padding: 5px 9px;
125
+ font-size: ${props => props.$fontSize};
126
+ color: ${TMColors.disabled};
127
+ `;
128
+ // L'elenco si apre sotto l'editor se c'è spazio, altrimenti sopra, e in ogni caso non esce dalla finestra
129
+ const calcOptionsPanelPosition = (rect) => {
130
+ const spaceBelow = window.innerHeight - rect.bottom - OPTIONS_PANEL_MARGIN;
131
+ const spaceAbove = rect.top - OPTIONS_PANEL_MARGIN;
132
+ const openUpwards = spaceBelow < OPTIONS_PANEL_MAX_HEIGHT && spaceAbove > spaceBelow;
133
+ const availableHeight = openUpwards ? spaceAbove : spaceBelow;
134
+ return {
135
+ left: Math.max(OPTIONS_PANEL_MARGIN, Math.min(rect.left, window.innerWidth - rect.width - OPTIONS_PANEL_MARGIN)),
136
+ top: openUpwards ? undefined : rect.bottom + OPTIONS_PANEL_MARGIN,
137
+ bottom: openUpwards ? window.innerHeight - rect.top + OPTIONS_PANEL_MARGIN : undefined,
138
+ width: rect.width,
139
+ maxHeight: Math.max(OPTIONS_PANEL_MIN_HEIGHT, Math.min(OPTIONS_PANEL_MAX_HEIGHT, availableHeight)),
140
+ };
141
+ };
142
+ const TMDropDown = ({ validationItems = [], disabled = false, elementStyle, isModifiedWhen, labelPosition = 'left', label, icon, width = '100%', fontSize = FontSize.defaultFontSize, dataSource, value, searchEnabled = false, usePortal = false, itemRender, onValueChanged, buttons = [], placeHolder, }) => {
58
143
  const [isFocused, setIsFocused] = useState(false);
59
- const renderedLeftLabelTextBox = () => (_jsxs(TMLayoutContainer, { direction: 'horizontal', children: [icon && _jsx(TMLayoutItem, { width: '20px', children: _jsx(StyledEditorIcon, { "$disabled": disabled, "$vil": validationItems, "$isModified": isModifiedWhen, children: icon }) }), _jsx(TMLayoutItem, { children: _jsxs(StyledEditorContainer, { "$width": width, children: [label && _jsx(StyledEditorLabel, { "$isFocused": isFocused, "$labelPosition": labelPosition, "$disabled": disabled, children: label }), _jsxs(StyledDropDownWrapper, { "$width": width, children: [_jsxs(StyledDropDown, { value: value, onFocus: () => setIsFocused(true), onBlur: () => setIsFocused(false), "$disabled": disabled, disabled: disabled, "$fontSize": fontSize, "$isModified": isModifiedWhen, "$vil": validationItems, onChange: onValueChanged, children: [placeHolder && _jsx("option", { value: "", disabled: true, hidden: true, children: placeHolder }), dataSource?.map((data, index) => (_jsx("option", { value: data.value, children: data.display }, index)))] }), buttons.length > 0 && (_jsx(ButtonsContainer, { children: buttons.map((buttonItem) => (_jsx(StyledDropDownEditorButton, { onClick: buttonItem.onClick, children: _jsx(TMTooltip, { content: buttonItem.text, children: buttonItem.icon }) }, buttonItem.text))) })), _jsx(CustomArrow, {})] }), _jsx(TMVilViewer, { vil: validationItems })] }) })] }));
144
+ const [isOpen, setIsOpen] = useState(false);
145
+ const [searchText, setSearchText] = useState('');
146
+ const [highlightedIndex, setHighlightedIndex] = useState(-1);
147
+ const [optionsPanelPosition, setOptionsPanelPosition] = useState();
148
+ const optionsPanelRef = useRef(null);
149
+ const dropDownWrapperRef = useRef(null);
150
+ const selectedItem = dataSource?.find(o => String(o.value) === String(value));
151
+ // A elenco chiuso l'input mostra la voce selezionata, ad elenco aperto il testo digitato
152
+ const filteredDataSource = (!isOpen || !searchText) ? (dataSource ?? []) : (dataSource ?? []).filter(o => String(o.display ?? '').toLocaleLowerCase().includes(searchText.toLocaleLowerCase()));
153
+ // Mantiene visibile la voce evidenziata durante la navigazione da tastiera
154
+ useEffect(() => {
155
+ if (!isOpen || highlightedIndex < 0)
156
+ return;
157
+ const option = optionsPanelRef.current?.children[highlightedIndex];
158
+ option?.scrollIntoView({ block: 'nearest' });
159
+ }, [isOpen, highlightedIndex]);
160
+ // Solo nel portal la posizione va ricalcolata: essendo fissa non segue lo scorrimento o il ridimensionamento del contenitore
161
+ useEffect(() => {
162
+ if (!isOpen || !usePortal)
163
+ return;
164
+ const updatePosition = () => updateOptionsPanelPosition();
165
+ window.addEventListener('scroll', updatePosition, true);
166
+ window.addEventListener('resize', updatePosition);
167
+ return () => {
168
+ window.removeEventListener('scroll', updatePosition, true);
169
+ window.removeEventListener('resize', updatePosition);
170
+ };
171
+ }, [isOpen, usePortal]);
172
+ const updateOptionsPanelPosition = () => {
173
+ const rect = dropDownWrapperRef.current?.getBoundingClientRect();
174
+ if (rect)
175
+ setOptionsPanelPosition(calcOptionsPanelPosition(rect));
176
+ };
177
+ const openOptions = () => {
178
+ if (disabled)
179
+ return;
180
+ if (usePortal)
181
+ updateOptionsPanelPosition();
182
+ setIsOpen(true);
183
+ setSearchText('');
184
+ setHighlightedIndex(dataSource?.findIndex(o => String(o.value) === String(value)) ?? -1);
185
+ };
186
+ const closeOptions = () => {
187
+ setIsOpen(false);
188
+ setSearchText('');
189
+ setHighlightedIndex(-1);
190
+ };
191
+ const selectItem = (item) => {
192
+ closeOptions();
193
+ // La select nativa notifica un ChangeEvent: qui viene ricostruito il solo dato letto dai consumatori (target.value)
194
+ onValueChanged?.({ target: { value: String(item.value) } });
195
+ };
196
+ const onSearchKeyDown = (e) => {
197
+ switch (e.key) {
198
+ case 'ArrowDown':
199
+ case 'ArrowUp':
200
+ e.preventDefault();
201
+ if (!isOpen) {
202
+ openOptions();
203
+ return;
204
+ }
205
+ if (filteredDataSource.length <= 0)
206
+ return;
207
+ setHighlightedIndex(prev => {
208
+ const delta = e.key === 'ArrowDown' ? 1 : -1;
209
+ const next = prev + delta;
210
+ if (next < 0)
211
+ return filteredDataSource.length - 1;
212
+ if (next >= filteredDataSource.length)
213
+ return 0;
214
+ return next;
215
+ });
216
+ break;
217
+ case 'Enter': {
218
+ if (!isOpen)
219
+ return;
220
+ e.preventDefault();
221
+ const item = filteredDataSource[highlightedIndex] ?? filteredDataSource[0];
222
+ if (item)
223
+ selectItem(item);
224
+ break;
225
+ }
226
+ case 'Escape':
227
+ if (isOpen) {
228
+ e.preventDefault();
229
+ closeOptions();
230
+ }
231
+ break;
232
+ }
233
+ };
234
+ const renderOptions = () => (filteredDataSource.length > 0
235
+ ? filteredDataSource.map((data, index) => (_jsx(StyledOption, { "$fontSize": fontSize, "$isSelected": String(data.value) === String(value), "$isHighlighted": index === highlightedIndex, onMouseEnter: () => setHighlightedIndex(index), onClick: () => selectItem(data), children: itemRender ? itemRender(data) : data.display }, index)))
236
+ : _jsx(StyledNoOptions, { "$fontSize": fontSize, children: SDKUI_Localizator.NoDataToDisplay }));
237
+ // onMouseDown va bloccato: senza preventDefault il blur dell'input chiuderebbe l'elenco prima del click
238
+ const renderOptionsPanel = () => {
239
+ if (!isOpen)
240
+ return null;
241
+ if (!usePortal) {
242
+ return (_jsx(StyledOptionsPanel, { ref: optionsPanelRef, onMouseDown: (e) => e.preventDefault(), children: renderOptions() }));
243
+ }
244
+ if (!optionsPanelPosition)
245
+ return null;
246
+ return createPortal(_jsx(StyledPortalOptionsPanel, { ref: optionsPanelRef, "$left": optionsPanelPosition.left, "$top": optionsPanelPosition.top, "$bottom": optionsPanelPosition.bottom, "$width": optionsPanelPosition.width, "$maxHeight": optionsPanelPosition.maxHeight, onMouseDown: (e) => e.preventDefault(), children: renderOptions() }), document.body);
247
+ };
248
+ const renderSearchableDropDown = () => (_jsxs(_Fragment, { children: [_jsx(StyledSearchInput, { value: isOpen ? searchText : (selectedItem?.display ?? ''), placeholder: isOpen ? SDKUI_Localizator.Search : placeHolder, readOnly: disabled, disabled: disabled, "$disabled": disabled, "$fontSize": fontSize, "$isModified": isModifiedWhen, "$vil": validationItems, onFocus: () => { setIsFocused(true); openOptions(); }, onBlur: () => { setIsFocused(false); closeOptions(); }, onClick: () => { if (!isOpen)
249
+ openOptions(); }, onChange: (e) => { setSearchText(e.target.value); setIsOpen(true); setHighlightedIndex(0); }, onKeyDown: onSearchKeyDown }), renderOptionsPanel()] }));
250
+ const renderNativeDropDown = () => (_jsxs(StyledDropDown, { value: value, onFocus: () => setIsFocused(true), onBlur: () => setIsFocused(false), "$disabled": disabled, disabled: disabled, "$fontSize": fontSize, "$isModified": isModifiedWhen, "$vil": validationItems, onChange: onValueChanged, children: [placeHolder && _jsx("option", { value: "", disabled: true, hidden: true, children: placeHolder }), dataSource?.map((data, index) => (_jsx("option", { value: data.value, children: data.display }, index)))] }));
251
+ const renderedLeftLabelTextBox = () => (_jsxs(TMLayoutContainer, { direction: 'horizontal', children: [icon && _jsx(TMLayoutItem, { width: '20px', children: _jsx(StyledEditorIcon, { "$disabled": disabled, "$vil": validationItems, "$isModified": isModifiedWhen, children: icon }) }), _jsx(TMLayoutItem, { children: _jsxs(StyledEditorContainer, { "$width": width, children: [label && _jsx(StyledEditorLabel, { "$isFocused": isFocused, "$labelPosition": labelPosition, "$disabled": disabled, children: label }), _jsxs(StyledDropDownWrapper, { ref: dropDownWrapperRef, "$width": width, children: [searchEnabled ? renderSearchableDropDown() : renderNativeDropDown(), buttons.length > 0 && (_jsx(ButtonsContainer, { children: buttons.map((buttonItem) => (_jsx(StyledDropDownEditorButton, { onClick: buttonItem.onClick, children: _jsx(TMTooltip, { content: buttonItem.tooltipContent ?? buttonItem.text, children: buttonItem.icon }) }, buttonItem.text))) })), _jsx(CustomArrow, {})] }), _jsx(TMVilViewer, { vil: validationItems })] }) })] }));
60
252
  return _jsx("div", { style: elementStyle, children: renderedLeftLabelTextBox() });
61
253
  };
62
254
  export default TMDropDown;