@topconsultnpm/sdkui-react 6.22.0-dev2.21 → 6.22.0-dev2.23
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.
- package/lib/components/base/TMDataGrid.js +1 -1
- package/lib/components/features/documents/TMMasterInfoFields.js +10 -4
- package/lib/components/features/documents/TMMultiMasterDetailDcmtsForm.js +4 -4
- package/lib/components/features/documents/TMMultiMasterDetailDcmtsUtils.d.ts +11 -11
- package/lib/components/features/documents/TMMultiMasterDetailDcmtsUtils.js +27 -20
- package/lib/hooks/useMultiMasterDetailDcmts.js +19 -7
- package/package.json +1 -1
|
@@ -386,7 +386,7 @@ const TMDataGrid = React.forwardRef((props, ref) => {
|
|
|
386
386
|
// Temporary: iOS-specific styles to prevent default long-press menu and text selection
|
|
387
387
|
WebkitTouchCallout: 'none',
|
|
388
388
|
WebkitUserSelect: 'none'
|
|
389
|
-
}, children: [dataColumns.map((column, index) => (_jsx(Column, { ...column }, column.caption + index.toString()))), sorting && _jsx(Sorting, { ...sorting }), selection && _jsx(Selection, { ...selection }), scrolling && _jsx(Scrolling, { ...scrolling }), summary && _jsx(Summary, { ...summary }), showHeaderFilter && _jsx(HeaderFilter, { visible: true, ...headerFilter }), rowDragging && _jsx(RowDragging, { ...rowDragging }), filterRow && _jsx(FilterRow, { ...filterRow }), showFilterPanel && _jsx(FilterPanel, { visible: true }), showHeaderColumnChooser && _jsxs(ColumnChooser, { height: "400px", enabled: !showHeaderColumnChooser, mode: "select", children: [_jsx(Position, { my: "center", at: "center", of: window }), _jsx(ColumnChooserSearch, { enabled: true }), _jsx(ColumnChooserSelection, { allowSelectAll: false, selectByClick: true, recursive: true })] }), stateStoring && _jsx(StateStoring, { ...stateStoring }), groupPanel && _jsx(GroupPanel, { ...groupPanel }), _jsx(Grouping, { contextMenuEnabled: true, ...grouping }), _jsx(LoadPanel, { enabled: showLoadPanel }), _jsx(SearchPanel, { visible: showSearchPanel, searchVisibleColumnsOnly: true, highlightSearchText: true }), editing && _jsx(Editing, { ...editing }), paging && _jsx(Paging, { ...paging }), effectivePager && _jsx(Pager, { ...effectivePager, visible: totalRecordCount > pageSize }), masterDetail && _jsx(MasterDetail, { ...masterDetail })] }) }), counterConfig.show && _jsx("div", { style: { width: "100%", height: "25px", display: "flex", alignItems: "center", gap: "15px", backgroundColor: "#e0e0e0" }, children: _jsx(TMCounterContainer, { items: counterValues, bgColorContainer: counterConfig.bgColorContainer, bgColorItem: counterConfig.bgColorItem, hoverColorItem: counterConfig.hoverColorItem, textColorItem: counterConfig.textColorItem }) }), customContextMenuItems && (_jsx(TMContextMenu, { target: ".dx-data-row", items: processCustomContextMenuItems(customContextMenuItems, customContextMenuRowKey), externalControl: {
|
|
389
|
+
}, children: [dataColumns.map((column, index) => (_jsx(Column, { ...column }, column.caption + index.toString()))), sorting && _jsx(Sorting, { ...sorting }), selection && _jsx(Selection, { ...selection }), scrolling && _jsx(Scrolling, { ...scrolling }), summary && _jsx(Summary, { ...summary }), showHeaderFilter && _jsx(HeaderFilter, { visible: true, ...headerFilter, search: { enabled: true, ...headerFilter?.search } }), rowDragging && _jsx(RowDragging, { ...rowDragging }), filterRow && _jsx(FilterRow, { ...filterRow }), showFilterPanel && _jsx(FilterPanel, { visible: true }), showHeaderColumnChooser && _jsxs(ColumnChooser, { height: "400px", enabled: !showHeaderColumnChooser, mode: "select", children: [_jsx(Position, { my: "center", at: "center", of: window }), _jsx(ColumnChooserSearch, { enabled: true }), _jsx(ColumnChooserSelection, { allowSelectAll: false, selectByClick: true, recursive: true })] }), stateStoring && _jsx(StateStoring, { ...stateStoring }), groupPanel && _jsx(GroupPanel, { ...groupPanel }), _jsx(Grouping, { contextMenuEnabled: true, ...grouping }), _jsx(LoadPanel, { enabled: showLoadPanel }), _jsx(SearchPanel, { visible: showSearchPanel, searchVisibleColumnsOnly: true, highlightSearchText: true }), editing && _jsx(Editing, { ...editing }), paging && _jsx(Paging, { ...paging }), effectivePager && _jsx(Pager, { ...effectivePager, visible: totalRecordCount > pageSize }), masterDetail && _jsx(MasterDetail, { ...masterDetail })] }) }), counterConfig.show && _jsx("div", { style: { width: "100%", height: "25px", display: "flex", alignItems: "center", gap: "15px", backgroundColor: "#e0e0e0" }, children: _jsx(TMCounterContainer, { items: counterValues, bgColorContainer: counterConfig.bgColorContainer, bgColorItem: counterConfig.bgColorItem, hoverColorItem: counterConfig.hoverColorItem, textColorItem: counterConfig.textColorItem }) }), customContextMenuItems && (_jsx(TMContextMenu, { target: ".dx-data-row", items: processCustomContextMenuItems(customContextMenuItems, customContextMenuRowKey), externalControl: {
|
|
390
390
|
visible: customContextMenuVisible,
|
|
391
391
|
position: customContextMenuPosition,
|
|
392
392
|
onClose: () => setCustomContextMenuVisible(false)
|
|
@@ -13,11 +13,17 @@ const styles = {
|
|
|
13
13
|
wrapper: { display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0, borderTop: `1px solid ${TMColors.card_background}` },
|
|
14
14
|
body: { flex: 1, minHeight: 0, padding: '13px 10px 9px', display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(185px, 1fr))', gridAutoRows: 'max-content', alignContent: 'start', columnGap: '10px', rowGap: '13px', overflow: 'auto' },
|
|
15
15
|
item: { position: 'relative', display: 'flex', alignItems: 'center', minWidth: 0, minHeight: '30px', padding: '5px 10px 4px', borderRadius: '8px', border: `1px solid ${TMColors.card_background}`, background: TMColors.default_background, transition: 'border-color 0.15s ease, box-shadow 0.15s ease' },
|
|
16
|
-
|
|
16
|
+
// Come per il pulsante copia, la posizione sta sull'ancora del tooltip; il troncamento con i
|
|
17
|
+
// puntini deve invece stare sull'elemento che contiene il testo
|
|
18
|
+
label: { position: 'absolute', top: '-7px', left: '8px', maxWidth: 'calc(100% - 16px)', minWidth: 0, padding: '0 5px', background: TMColors.default_background },
|
|
19
|
+
labelText: { maxWidth: '100%', fontSize: '9px', fontWeight: 700, lineHeight: '13px', letterSpacing: '0.5px', textTransform: 'uppercase', color: TMColors.label_normal, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' },
|
|
17
20
|
value: { display: 'block', width: '100%', fontSize: '12.5px', fontWeight: 500, color: TMColors.text_normal, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', userSelect: 'text', cursor: 'text', minHeight: '17px', lineHeight: '17px' },
|
|
18
21
|
emptyValue: { color: TMColors.disabled, fontWeight: 400 },
|
|
19
22
|
// Pulsante di copia: pastiglia sul bordo superiore della card, come la caption
|
|
20
|
-
|
|
23
|
+
// La posizione sta sull'ancora e non sul pulsante: è il contenitore del tooltip, che deve stare
|
|
24
|
+
// dove si vede il pulsante, altrimenti il tooltip si aprirebbe altrove
|
|
25
|
+
copyBtnAnchor: { position: 'absolute', top: '-9px', right: '6px', transition: 'opacity 0.15s ease' },
|
|
26
|
+
copyBtn: { display: 'flex', alignItems: 'center', justifyContent: 'center', width: '18px', height: '18px', padding: 0, borderRadius: '50%', border: `1px solid ${TMColors.card_background}`, background: TMColors.default_background, color: TMColors.label_normal, cursor: 'pointer', transition: 'background 0.15s ease, color 0.15s ease, border-color 0.15s ease' },
|
|
21
27
|
placeholder: { gridColumn: '1 / -1', padding: '6px 2px', color: TMColors.label_normal, fontSize: '12px' },
|
|
22
28
|
// Barra di ricerca
|
|
23
29
|
searchBar: { display: 'flex', alignItems: 'center', gap: '8px', padding: '12px 14px 0', flexShrink: 0 },
|
|
@@ -58,7 +64,7 @@ const MetadataCard = ({ caption, copyText, children }) => {
|
|
|
58
64
|
};
|
|
59
65
|
// Il pulsante resta visibile durante il feedback e quando ha il focus (accessibilità da tastiera)
|
|
60
66
|
const isCopyVisible = copyText !== '' && (!SUPPORTS_HOVER || isHovered || isCopied);
|
|
61
|
-
return (_jsxs("div", { style: styles.item, onMouseEnter: (e) => { setIsHovered(true); Object.assign(e.currentTarget.style, { borderColor: TMColors.primary, boxShadow: '0 2px 8px rgba(37, 89, 165, 0.14)' }); }, onMouseLeave: (e) => { setIsHovered(false); Object.assign(e.currentTarget.style, { borderColor: TMColors.card_background, boxShadow: 'none' }); }, children: [_jsx(
|
|
67
|
+
return (_jsxs("div", { style: styles.item, onMouseEnter: (e) => { setIsHovered(true); Object.assign(e.currentTarget.style, { borderColor: TMColors.primary, boxShadow: '0 2px 8px rgba(37, 89, 165, 0.14)' }); }, onMouseLeave: (e) => { setIsHovered(false); Object.assign(e.currentTarget.style, { borderColor: TMColors.card_background, boxShadow: 'none' }); }, children: [_jsx(TMTooltip, { content: caption, parentStyle: { ...styles.label, maxWidth: isCopyVisible ? 'calc(100% - 38px)' : 'calc(100% - 16px)' }, childStyle: { maxWidth: '100%', minWidth: 0 }, children: _jsx("span", { style: styles.labelText, children: caption }) }), children, copyText !== '' && (_jsx(TMTooltip, { content: isCopied ? SDKUI_Localizator.CopiedSuccessfully : SDKUI_Localizator.CopyToClipboard, parentStyle: { ...styles.copyBtnAnchor, opacity: isCopyVisible ? 1 : 0, pointerEvents: isCopyVisible ? 'auto' : 'none' }, children: _jsx("button", { type: 'button', style: { ...styles.copyBtn, ...(isCopied ? COPY_BTN_COPIED : COPY_BTN_IDLE) }, "aria-label": SDKUI_Localizator.CopyToClipboard, onClick: copyAsync, onFocus: () => setIsHovered(true), onBlur: () => setIsHovered(false), ...hoverStyle(isCopied ? COPY_BTN_COPIED : COPY_BTN_HOVER, isCopied ? COPY_BTN_COPIED : COPY_BTN_IDLE), children: isCopied ? _jsx(IconSuccess, { fontSize: 11 }) : _jsx(IconCopy, { fontSize: 11 }) }) }))] }));
|
|
62
68
|
};
|
|
63
69
|
// Riepilogo dei metadati di un documento in card compatte
|
|
64
70
|
const TMMasterInfoFields = ({ dtd, dcmt, getValuesByMidAsync, reloadToken, isExpandedView = false }) => {
|
|
@@ -157,7 +163,7 @@ const TMMasterInfoFields = ({ dtd, dcmt, getValuesByMidAsync, reloadToken, isExp
|
|
|
157
163
|
e.preventDefault();
|
|
158
164
|
e.stopPropagation();
|
|
159
165
|
setFilter('');
|
|
160
|
-
} }, onFocus: (e) => Object.assign(e.currentTarget.parentElement?.style ?? {}, { borderColor: TMColors.primary, boxShadow: '0 0 0 2px rgba(37, 89, 165, 0.14)' }), onBlur: (e) => Object.assign(e.currentTarget.parentElement?.style ?? {}, { borderColor: TMColors.card_background, boxShadow: 'none' }) }), filter !== '' && (_jsx("button", { type: 'button', style: styles.searchClear,
|
|
166
|
+
} }, onFocus: (e) => Object.assign(e.currentTarget.parentElement?.style ?? {}, { borderColor: TMColors.primary, boxShadow: '0 0 0 2px rgba(37, 89, 165, 0.14)' }), onBlur: (e) => Object.assign(e.currentTarget.parentElement?.style ?? {}, { borderColor: TMColors.card_background, boxShadow: 'none' }) }), filter !== '' && (_jsx(TMTooltip, { content: SDKUI_Localizator.Clear, parentStyle: { display: 'flex', flexShrink: 0 }, children: _jsx("button", { type: 'button', style: styles.searchClear, "aria-label": SDKUI_Localizator.Clear, onClick: () => setFilter(''), ...hoverStyle({ background: TMColors.primary_container, color: TMColors.primary }, { background: 'transparent', color: TMColors.label_normal }), children: _jsx(IconClearButton, { fontSize: 13 }) }) }))] }), _jsxs("span", { style: styles.searchCount, children: [filteredRows.length, "/", rows.length] })] }));
|
|
161
167
|
// La ricerca sta solo nella vista ingrandita: in linea lo spazio è poco e l'elenco è limitato
|
|
162
168
|
// all'anteprima, con il pulsante per aprirlo tutto
|
|
163
169
|
const hasSearch = isExpandedView && rows.length > PREVIEW_COUNT;
|
|
@@ -104,7 +104,7 @@ const TMMultiMasterDetailDcmtsForm = (props) => {
|
|
|
104
104
|
allowSorting: false, allowFiltering: false, allowHeaderFiltering: false, allowResizing: false,
|
|
105
105
|
allowReordering: false, allowHiding: false, showInColumnChooser: false, allowEditing: false,
|
|
106
106
|
cssClass: 'tm-row-actions',
|
|
107
|
-
cellRender: (cell) => (_jsxs("div", { style: styles.actionsCell, children: [_jsx("span", { style: styles.actionIcon,
|
|
107
|
+
cellRender: (cell) => (_jsxs("div", { style: styles.actionsCell, children: [_jsx(TMTooltip, { content: SDKUI_Localizator.Update, children: _jsx("span", { style: styles.actionIcon, onClick: () => openEditForm(cell.data), children: _jsx(IconEdit, { fontSize: 18, color: TMColors.primary }) }) }), _jsx(TMTooltip, { content: SDKUI_Localizator.Duplicate, children: _jsx("span", { style: styles.actionIcon, onClick: () => duplicateRow(cell.data), children: _jsx(IconDuplicate, { fontSize: 18, color: TMColors.tertiary }) }) }), _jsx(TMTooltip, { content: SDKUI_Localizator.Deletion, children: _jsx("span", { style: styles.actionIcon, onClick: () => deleteRow(cell.data), children: _jsx(IconDelete, { fontSize: 18, color: TMColors.error }) }) })] })),
|
|
108
108
|
};
|
|
109
109
|
return [actionsColumn, iconColumn, ...detailColumns, ...(detailSupportsFile ? [fileColumn] : [])];
|
|
110
110
|
}, [detailColumns, detailSupportsFile]);
|
|
@@ -148,13 +148,13 @@ const TMMultiMasterDetailDcmtsForm = (props) => {
|
|
|
148
148
|
const renderOptionsMenu = () => {
|
|
149
149
|
if (!isSummaryPhase || isMobile)
|
|
150
150
|
return undefined;
|
|
151
|
-
return (_jsx(ContextMenu, { trigger: 'left', items: optionsMenuItems, children: _jsx("span", { style: styles.titleMenuIcon,
|
|
151
|
+
return (_jsx(ContextMenu, { trigger: 'left', items: optionsMenuItems, children: _jsx(TMTooltip, { content: SDKUI_Localizator.Options, children: _jsx("span", { style: styles.titleMenuIcon, "aria-label": SDKUI_Localizator.Options, children: _jsx(IconMenuVertical, { fontSize: 16, color: "white" }) }) }) }));
|
|
152
152
|
};
|
|
153
153
|
// ----- Sezione: informazioni del master -----------------------------------------------------
|
|
154
154
|
const renderMasterInfo = (panel) => {
|
|
155
155
|
const isOpen = sectionsOpen.masterInfo;
|
|
156
156
|
const editMasterButton = canUpdateMaster
|
|
157
|
-
? (_jsx("button", { type: 'button', onClick: (e) => { e.stopPropagation(); setIsOpenMasterForm(true); }, style: accordionStyles.accordionExpandBtn,
|
|
157
|
+
? (_jsx(TMTooltip, { content: `${SDKUI_Localizator.Update}: ${getDtdDisplayName(dtd)}`, children: _jsx("button", { type: 'button', onClick: (e) => { e.stopPropagation(); setIsOpenMasterForm(true); }, style: accordionStyles.accordionExpandBtn, ...hoverStyle({ background: TMColors.primary_container, color: TMColors.primary }, { background: 'transparent', color: TMColors.label_normal }), children: _jsx(IconEdit, { fontSize: 16 }) }) }))
|
|
158
158
|
: undefined;
|
|
159
159
|
return (_jsxs("div", { style: { ...accordionStyles.accordion, ...accordionStyles.accordionInPanel }, children: [renderAccordionHeader({
|
|
160
160
|
section: 'masterInfo',
|
|
@@ -172,7 +172,7 @@ const TMMultiMasterDetailDcmtsForm = (props) => {
|
|
|
172
172
|
const isOpen = sectionsOpen.archivedDetails;
|
|
173
173
|
const hasResults = archivedDetailResults.length > 0 && archivedDetailsCount > 0;
|
|
174
174
|
// Ricarica i dettagli già archiviati: la sezione va aperta, altrimenti l'esito non sarebbe visibile
|
|
175
|
-
const refreshArchivedButton = (_jsx("button", { type: 'button', onClick: (e) => { e.stopPropagation(); setSectionOpen('archivedDetails', true); refreshArchivedDetailsAsync(); }, disabled: isLoadingArchivedDetails, style: { ...accordionStyles.accordionExpandBtn, ...(isLoadingArchivedDetails ? { cursor: 'default', opacity: 0.5 } : {}) },
|
|
175
|
+
const refreshArchivedButton = (_jsx(TMTooltip, { content: SDKUI_Localizator.Refresh, children: _jsx("button", { type: 'button', onClick: (e) => { e.stopPropagation(); setSectionOpen('archivedDetails', true); refreshArchivedDetailsAsync(); }, disabled: isLoadingArchivedDetails, style: { ...accordionStyles.accordionExpandBtn, ...(isLoadingArchivedDetails ? { cursor: 'default', opacity: 0.5 } : {}) }, "aria-label": SDKUI_Localizator.Refresh, ...(isLoadingArchivedDetails ? {} : hoverStyle({ background: TMColors.primary_container, color: TMColors.primary }, { background: 'transparent', color: TMColors.label_normal })), children: _jsx(IconRefresh, { fontSize: 16 }) }) }));
|
|
176
176
|
return (_jsxs("div", { style: { ...accordionStyles.accordion, ...accordionStyles.accordionInPanel }, children: [renderAccordionHeader({
|
|
177
177
|
section: 'archivedDetails',
|
|
178
178
|
isOpen,
|
|
@@ -46,13 +46,14 @@ export interface ArchiveRow {
|
|
|
46
46
|
* stringhe, quindi il `value` generico di DevExtreme si restringe
|
|
47
47
|
*/
|
|
48
48
|
export type ArchiveRowCell = Omit<DataGridTypes.ColumnCellTemplateData<ArchiveRow, number>, 'value'> & {
|
|
49
|
-
readonly value?: string;
|
|
49
|
+
readonly value?: string | number;
|
|
50
50
|
};
|
|
51
51
|
/**
|
|
52
52
|
* Modifiche di una riga in arrivo dalla griglia (mid -> nuovo valore): in modifica diretta ci sono
|
|
53
|
-
*
|
|
53
|
+
* i metadati testuali e numerici (vedi isInlineEditableMetadata), quindi stringhe e numeri,
|
|
54
|
+
* e l'editor svuotato restituisce null
|
|
54
55
|
*/
|
|
55
|
-
export type ArchiveRowChanges = Record<string, string | null | undefined>;
|
|
56
|
+
export type ArchiveRowChanges = Record<string, string | number | null | undefined>;
|
|
56
57
|
/** Aggiornamento di una riga della griglia, con le sole modifiche possibili in linea */
|
|
57
58
|
export type ArchiveRowUpdatingEvent = Omit<DataGridTypes.RowUpdatingEvent<ArchiveRow, number>, 'newData'> & {
|
|
58
59
|
newData: ArchiveRowChanges;
|
|
@@ -70,10 +71,15 @@ export declare const canArchive: (dtd?: DcmtTypeDescriptor) => boolean;
|
|
|
70
71
|
*/
|
|
71
72
|
export declare const INLINE_EDITABLE_CELL_CLASS = "tm-cell-inline-editable";
|
|
72
73
|
/**
|
|
73
|
-
* Modifica diretta in griglia:
|
|
74
|
-
* hanno editor e vincoli propri, date
|
|
74
|
+
* Modifica diretta in griglia: testo libero e numeri. Liste dati, calcolati, numeratori e speciali
|
|
75
|
+
* hanno editor e vincoli propri, le date una formattazione dedicata: restano al form
|
|
75
76
|
*/
|
|
76
77
|
export declare const isInlineEditableMetadata: (md: MetadataDescriptor) => boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Valore di una cella numerica: l'editor della griglia vuole un numero, non la stringa che
|
|
80
|
+
* la riga porta in `__values` per l'archiviazione. Vuoto o non numerico: cella vuota
|
|
81
|
+
*/
|
|
82
|
+
export declare const toGridNumber: (value: unknown) => number | undefined;
|
|
77
83
|
/** Colonne della griglia di riepilogo dai metadati (utente) del tipo documento */
|
|
78
84
|
export declare const buildColumnsFromDtd: (dtd: DcmtTypeDescriptor | undefined, renderDataListCell: (value: string | Date | number | undefined, dataListID: number, viewMode: DataListViewModes) => React.ReactElement) => Array<IColumnProps>;
|
|
79
85
|
export declare const getDetailDtdAsync: (detailTID: number | undefined) => Promise<DcmtTypeDescriptor | undefined>;
|
|
@@ -205,7 +211,6 @@ export declare const multiDetailStyles: {
|
|
|
205
211
|
lineHeight: number;
|
|
206
212
|
};
|
|
207
213
|
};
|
|
208
|
-
/** Stili delle sezioni collassabili: qui perché la testata comune (renderAccordionHeader) vive in questo file */
|
|
209
214
|
export declare const accordionStyles: {
|
|
210
215
|
accordion: {
|
|
211
216
|
display: "flex";
|
|
@@ -245,11 +250,6 @@ export declare const accordionStyles: {
|
|
|
245
250
|
display: "inline-flex";
|
|
246
251
|
alignItems: "center";
|
|
247
252
|
justifyContent: "center";
|
|
248
|
-
minWidth: string;
|
|
249
|
-
height: string;
|
|
250
|
-
padding: string;
|
|
251
|
-
borderRadius: string;
|
|
252
|
-
background: string;
|
|
253
253
|
color: string;
|
|
254
254
|
fontSize: string;
|
|
255
255
|
fontWeight: number;
|
|
@@ -27,12 +27,22 @@ export const canArchive = (dtd) => dtd?.perm?.canArchive === AccessLevelsEx.Yes
|
|
|
27
27
|
*/
|
|
28
28
|
export const INLINE_EDITABLE_CELL_CLASS = 'tm-cell-inline-editable';
|
|
29
29
|
/**
|
|
30
|
-
* Modifica diretta in griglia:
|
|
31
|
-
* hanno editor e vincoli propri, date
|
|
30
|
+
* Modifica diretta in griglia: testo libero e numeri. Liste dati, calcolati, numeratori e speciali
|
|
31
|
+
* hanno editor e vincoli propri, le date una formattazione dedicata: restano al form
|
|
32
32
|
*/
|
|
33
|
-
export const isInlineEditableMetadata = (md) => md.dataType === MetadataDataTypes.Varchar
|
|
33
|
+
export const isInlineEditableMetadata = (md) => (md.dataType === MetadataDataTypes.Varchar || md.dataType === MetadataDataTypes.Number)
|
|
34
34
|
&& (md.dataDomain === undefined || md.dataDomain === MetadataDataDomains.None)
|
|
35
35
|
&& md.isSystem !== 1 && md.isSystemDerived !== 1;
|
|
36
|
+
/**
|
|
37
|
+
* Valore di una cella numerica: l'editor della griglia vuole un numero, non la stringa che
|
|
38
|
+
* la riga porta in `__values` per l'archiviazione. Vuoto o non numerico: cella vuota
|
|
39
|
+
*/
|
|
40
|
+
export const toGridNumber = (value) => {
|
|
41
|
+
if (value === undefined || value === null || value === '')
|
|
42
|
+
return undefined;
|
|
43
|
+
const parsed = typeof value === 'number' ? value : Number(value);
|
|
44
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
45
|
+
};
|
|
36
46
|
/** Colonne della griglia di riepilogo dai metadati (utente) del tipo documento */
|
|
37
47
|
export const buildColumnsFromDtd = (dtd, renderDataListCell) => getUserMetadataSorted(dtd).map(md => {
|
|
38
48
|
const dataListID = md.dataListID ?? 0;
|
|
@@ -41,21 +51,25 @@ export const buildColumnsFromDtd = (dtd, renderDataListCell) => getUserMetadataS
|
|
|
41
51
|
const dataType = md.dataType;
|
|
42
52
|
const format = md.format?.format;
|
|
43
53
|
const formatCulture = md.format?.formatCulture;
|
|
44
|
-
// Date e numeri usano la formattazione scalare; le liste dati il render comune
|
|
45
|
-
const needsScalarFormat = !isDataList && (dataType === MetadataDataTypes.DateTime || dataType === MetadataDataTypes.Number);
|
|
46
54
|
const isInlineEditable = isInlineEditableMetadata(md);
|
|
55
|
+
const isNumber = dataType === MetadataDataTypes.Number;
|
|
56
|
+
// Date e numeri usano la formattazione scalare; le liste dati il render comune. La cella
|
|
57
|
+
// modificabile mostra sempre l'editor, quindi non ha un proprio render
|
|
58
|
+
const needsScalarFormat = !isDataList && !isInlineEditable && (dataType === MetadataDataTypes.DateTime || isNumber);
|
|
47
59
|
return {
|
|
48
60
|
dataField: String(md.id),
|
|
49
61
|
caption: (SDK_Globals.useLocalizedName ? md.nameLoc : md.name) ?? md.name ?? '',
|
|
50
62
|
// La colonna modificabile dichiara il tipo: senza valori DevExtreme non saprebbe che editor usare
|
|
51
63
|
...(isInlineEditable
|
|
52
64
|
? {
|
|
53
|
-
|
|
65
|
+
// I numeri arrivano in cella già come numeri (vedi buildArchiveRow): l'editor è la casella numerica
|
|
66
|
+
dataType: isNumber ? 'number' : 'string',
|
|
54
67
|
allowEditing: true,
|
|
55
68
|
showEditorAlways: true,
|
|
56
69
|
cssClass: INLINE_EDITABLE_CELL_CLASS,
|
|
57
|
-
// Lunghezza e obbligatorietà del metadato valgono anche in griglia
|
|
58
|
-
|
|
70
|
+
// Lunghezza e obbligatorietà del metadato valgono anche in griglia; sui numeri
|
|
71
|
+
// i limiti restano all'API, in cella basta che sia un numero
|
|
72
|
+
editorOptions: !isNumber && md.length ? { maxLength: md.length } : undefined,
|
|
59
73
|
validationRules: String(md.isRequired) === '1' ? [{ type: 'required', message: SDKUI_Localizator.RequiredField }] : undefined,
|
|
60
74
|
}
|
|
61
75
|
: { allowEditing: false }),
|
|
@@ -114,15 +128,13 @@ export const multiDetailStyles = {
|
|
|
114
128
|
typeCardIcon: { width: '34px', height: '34px', borderRadius: '9px', background: TMColors.primary_container, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 },
|
|
115
129
|
typeCardName: { flex: 1, fontSize: '14px', fontWeight: 500, color: TMColors.text_normal, whiteSpace: 'normal', wordBreak: 'break-word', lineHeight: 1.3 },
|
|
116
130
|
};
|
|
117
|
-
|
|
131
|
+
// Stili delle sezioni collassabili
|
|
118
132
|
export const accordionStyles = {
|
|
119
133
|
accordion: { display: 'flex', flexDirection: 'column', minHeight: 0, borderRadius: '8px', border: `1px solid ${TMColors.card_background}`, background: TMColors.default_background, overflow: 'hidden' },
|
|
120
|
-
// Sezione dentro un pannello: l'altezza è quella del pannello, che la sezione riempie. Serve `flex`
|
|
121
|
-
// e non `height: 100%`, perché il pannello chiuso ha altezza 0 e vive del suo ingombro minimo
|
|
122
134
|
accordionInPanel: { flex: 1 },
|
|
123
135
|
accordionHeader: { display: 'flex', alignItems: 'center', gap: '8px', padding: '4px 8px', cursor: 'pointer', userSelect: 'none', transition: 'background 0.15s ease', outline: 'none', flexShrink: 0, minHeight: '28px' },
|
|
124
136
|
accordionTitle: { flex: 1, fontSize: '12px', fontWeight: 600, color: TMColors.text_normal, display: 'flex', alignItems: 'center', gap: '6px', minWidth: 0 },
|
|
125
|
-
accordionBadge: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
|
137
|
+
accordionBadge: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: TMColors.primary, fontSize: '13px', fontWeight: 700, flexShrink: 0 },
|
|
126
138
|
accordionListBody: { position: 'relative', flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', borderTop: `1px solid ${TMColors.card_background}` },
|
|
127
139
|
accordionListPlaceholder: { display: 'flex', flex: 1, alignItems: 'center', justifyContent: 'center', gap: '10px', padding: '16px', color: TMColors.label_normal, fontSize: '13px' },
|
|
128
140
|
accordionExpandBtn: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '20px', height: '20px', padding: 0, borderRadius: '6px', border: 'none', background: 'transparent', color: TMColors.label_normal, cursor: 'pointer', flexShrink: 0, transition: 'background 0.15s ease, color 0.15s ease' },
|
|
@@ -172,12 +184,10 @@ export const renderAccordionHeader = ({ section, isOpen, tooltip, count, title,
|
|
|
172
184
|
const canMaximize = panel.canMaximize;
|
|
173
185
|
const onToggle = () => onToggleOpen(section, !isOpen);
|
|
174
186
|
const expandTooltip = panel.isMaximized ? SDKUI_Localizator.Minimize : SDKUI_Localizator.ZoomIn;
|
|
175
|
-
return (_jsxs("div", { role: canToggle ? 'button' : undefined, tabIndex: canToggle ? 0 : undefined, "aria-expanded": canToggle ? isOpen : undefined, style: { ...accordionStyles.accordionHeader, background: restBackground, ...(canToggle ? {} : { cursor: 'default' }) }, onClick: canToggle ? onToggle : undefined,
|
|
176
|
-
// Doppio click sulla testata: ingrandisce e riduce, come nei pannelli del prodotto
|
|
177
|
-
onDoubleClick: canMaximize ? () => panel.toggleMaximize() : undefined, onKeyDown: canToggle ? (e) => { if (e.key === 'Enter' || e.key === ' ') {
|
|
187
|
+
return (_jsxs("div", { role: canToggle ? 'button' : undefined, tabIndex: canToggle ? 0 : undefined, "aria-expanded": canToggle ? isOpen : undefined, style: { ...accordionStyles.accordionHeader, background: restBackground, ...(canToggle ? {} : { cursor: 'default' }) }, onClick: canToggle ? onToggle : undefined, onDoubleClick: canMaximize ? () => panel.toggleMaximize() : undefined, onKeyDown: canToggle ? (e) => { if (e.key === 'Enter' || e.key === ' ') {
|
|
178
188
|
e.preventDefault();
|
|
179
189
|
onToggle();
|
|
180
|
-
} } : undefined,
|
|
190
|
+
} } : undefined, ...(canToggle ? hoverStyle({ background: TMColors.primary_container }, { background: restBackground }) : {}), children: [_jsx(TMTooltip, { content: canToggle ? tooltip : undefined, parentStyle: { flex: 1, minWidth: 0 }, childStyle: { maxWidth: '100%' }, children: _jsxs("span", { style: accordionStyles.accordionTitle, children: [title, _jsx("span", { style: accordionStyles.accordionBadge, children: count })] }) }), actions, canMaximize && (_jsx(TMTooltip, { content: expandTooltip, children: _jsx("button", { type: 'button', onClick: (e) => { e.stopPropagation(); panel.toggleMaximize(); }, style: accordionStyles.accordionExpandBtn, "aria-label": expandTooltip, ...hoverStyle({ background: TMColors.primary_container, color: TMColors.primary }, { background: 'transparent', color: TMColors.label_normal }), children: panel.isMaximized ? _jsx(IconWindowMinimize, { fontSize: 14 }) : _jsx(IconWindowMaximize, { fontSize: 14 }) }) })), canToggle && (_jsx(IconChevronRight, { fontSize: 16, color: TMColors.label_normal, style: { transform: isOpen ? 'rotate(90deg)' : 'none', transition: 'transform 0.2s ease', flexShrink: 0 } }))] }));
|
|
181
191
|
};
|
|
182
192
|
/**
|
|
183
193
|
* Contenuto di un pannello: il contesto del panel manager è leggibile solo dentro l'albero del
|
|
@@ -222,10 +232,7 @@ export const TMSectionPanelsHeights = ({ sections, sectionsOpen }) => {
|
|
|
222
232
|
}
|
|
223
233
|
if (hasMaximizedPanel)
|
|
224
234
|
return;
|
|
225
|
-
// Solo i pannelli a schermo si spartiscono l'altezza
|
|
226
|
-
// resta visibile uno solo) al pannello a schermo toccherebbe la sua sola quota nominale,
|
|
227
|
-
// lasciando vuoto lo spazio dei pannelli che non ci sono. Chi è fuori schermo resta alle proprie
|
|
228
|
-
// quote, che sono quelle da cui il panel manager riparte quando lo si torna a mostrare
|
|
235
|
+
// Solo i pannelli a schermo si spartiscono l'altezza, altrimenti resterebbe vuoto lo spazio dei nascosti
|
|
229
236
|
const visibleSections = sections.filter(section => panelVisibility[section] !== false);
|
|
230
237
|
const heights = sectionPanelHeights(visibleSections, sectionsOpen, dimensionsRef.current);
|
|
231
238
|
visibleSections.forEach(section => setPanelDimensionsById(section, heights[section].width, heights[section].height));
|
|
@@ -8,7 +8,7 @@ import TMSpinner from '../components/base/TMSpinner';
|
|
|
8
8
|
import ShowAlert from '../components/base/TMAlert';
|
|
9
9
|
import { TMResultManager } from '../components/forms/TMResultDialog';
|
|
10
10
|
import { useDataListItem } from './useDataListItem';
|
|
11
|
-
import { buildColumnsFromDtd, canArchive, canUpdateMasterCallback, getDetailDtdAsync } from '../components/features/documents/TMMultiMasterDetailDcmtsUtils';
|
|
11
|
+
import { buildColumnsFromDtd, canArchive, canUpdateMasterCallback, getDetailDtdAsync, toGridNumber } from '../components/features/documents/TMMultiMasterDetailDcmtsUtils';
|
|
12
12
|
/** Fasi del flusso di archiviazione multipla dei dettagli */
|
|
13
13
|
export var MultiDetailArchivePhase;
|
|
14
14
|
(function (MultiDetailArchivePhase) {
|
|
@@ -44,6 +44,8 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
|
|
|
44
44
|
// Struttura del tipo documento selezionato
|
|
45
45
|
const [detailColumns, setDetailColumns] = useState([]);
|
|
46
46
|
const [detailSupportsFile, setDetailSupportsFile] = useState(false);
|
|
47
|
+
// Mid numerici del tipo di dettaglio: in griglia le loro celle portano il numero (vedi buildArchiveRow)
|
|
48
|
+
const numericMidsRef = useRef(new Set());
|
|
47
49
|
// Dettagli già archiviati del tipo selezionato, collegati al master
|
|
48
50
|
const [archivedDetailResults, setArchivedDetailResults] = useState([]);
|
|
49
51
|
const [isLoadingArchivedDetails, setIsLoadingArchivedDetails] = useState(false);
|
|
@@ -146,6 +148,7 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
|
|
|
146
148
|
if (!selectedRelation?.detailTID) {
|
|
147
149
|
setDetailColumns([]);
|
|
148
150
|
setDetailSupportsFile(false);
|
|
151
|
+
numericMidsRef.current = new Set();
|
|
149
152
|
return;
|
|
150
153
|
}
|
|
151
154
|
try {
|
|
@@ -153,16 +156,21 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
|
|
|
153
156
|
setDetailSupportsFile(detailDtd?.archiveConstraint !== ArchiveConstraints.OnlyMetadata);
|
|
154
157
|
// Liste dati referenziate dai metadati, servono al render delle celle
|
|
155
158
|
const dataListIDs = new Set();
|
|
159
|
+
const numericMids = new Set();
|
|
156
160
|
(detailDtd?.metadata ?? []).forEach(md => {
|
|
157
161
|
if (md.dataDomain === MetadataDataDomains.DataList && md.dataListID)
|
|
158
162
|
dataListIDs.add(md.dataListID);
|
|
163
|
+
if (md.dataType === MetadataDataTypes.Number && md.id !== undefined)
|
|
164
|
+
numericMids.add(md.id);
|
|
159
165
|
});
|
|
166
|
+
numericMidsRef.current = numericMids;
|
|
160
167
|
await loadDataListsAsync(dataListIDs);
|
|
161
168
|
setDetailColumns(buildColumnsFromDtd(detailDtd, renderDataListCell));
|
|
162
169
|
}
|
|
163
170
|
catch {
|
|
164
171
|
setDetailColumns([]);
|
|
165
172
|
setDetailSupportsFile(false);
|
|
173
|
+
numericMidsRef.current = new Set();
|
|
166
174
|
}
|
|
167
175
|
};
|
|
168
176
|
loadDetailStructureAsync();
|
|
@@ -315,11 +323,13 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
|
|
|
315
323
|
},
|
|
316
324
|
});
|
|
317
325
|
}, []);
|
|
326
|
+
// I valori del form sono stringhe: le celle numeriche vogliono il numero, richiesto dal loro editor
|
|
318
327
|
const buildArchiveRow = useCallback((id, tid, values, file) => {
|
|
319
328
|
const row = { id, __tid: tid, __values: values, __file: file };
|
|
320
329
|
for (const v of values) {
|
|
321
|
-
if (v.mid
|
|
322
|
-
|
|
330
|
+
if (v.mid === undefined)
|
|
331
|
+
continue;
|
|
332
|
+
row[String(v.mid)] = numericMidsRef.current.has(v.mid) ? toGridNumber(v.value) : (v.value ?? '');
|
|
323
333
|
}
|
|
324
334
|
return row;
|
|
325
335
|
}, []);
|
|
@@ -338,7 +348,7 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
|
|
|
338
348
|
closeAddForm();
|
|
339
349
|
};
|
|
340
350
|
/**
|
|
341
|
-
* Modifica diretta in griglia di uno o più metadati testuali (changes: mid -> valore).
|
|
351
|
+
* Modifica diretta in griglia di uno o più metadati testuali o numerici (changes: mid -> valore).
|
|
342
352
|
* La riga e i suoi __values sono ricreati: il duplicato, che condivide __values per
|
|
343
353
|
* riferimento, resta invariato
|
|
344
354
|
*/
|
|
@@ -346,9 +356,10 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
|
|
|
346
356
|
const changedMids = Object.keys(changes);
|
|
347
357
|
if (changedMids.length === 0)
|
|
348
358
|
return;
|
|
349
|
-
//
|
|
359
|
+
// I __values vanno a sistema come stringhe, anche quelli che in griglia sono numeri. La griglia
|
|
360
|
+
// restituisce null sull'editor svuotato: a sistema il metadato va vuoto, non nullo
|
|
350
361
|
const normalized = {};
|
|
351
|
-
changedMids.forEach(mid => { normalized[mid] = changes[mid]
|
|
362
|
+
changedMids.forEach(mid => { normalized[mid] = changes[mid] === null || changes[mid] === undefined ? '' : String(changes[mid]); });
|
|
352
363
|
setArchiveRows(prev => prev.map(r => {
|
|
353
364
|
if (r.id !== rowId)
|
|
354
365
|
return r;
|
|
@@ -364,7 +375,8 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
|
|
|
364
375
|
values.push(added);
|
|
365
376
|
});
|
|
366
377
|
const updated = { ...r, __values: values };
|
|
367
|
-
|
|
378
|
+
// In cella il numero resta un numero, come lo vuole il suo editor
|
|
379
|
+
changedMids.forEach(mid => { updated[mid] = numericMidsRef.current.has(Number(mid)) ? toGridNumber(normalized[mid]) : normalized[mid]; });
|
|
368
380
|
return updated;
|
|
369
381
|
}));
|
|
370
382
|
}, []);
|