@topconsultnpm/sdkui-react 6.22.0-dev2.17 → 6.22.0-dev2.19
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/TMEditorBase.d.ts +2 -0
- package/lib/components/base/TMTooltip.d.ts +2 -1
- package/lib/components/base/TMTooltip.js +23 -6
- package/lib/components/choosers/TMDcmtTypeChooser.js +4 -3
- package/lib/components/choosers/TMDistinctValues.d.ts +9 -7
- package/lib/components/choosers/TMDistinctValues.js +147 -195
- package/lib/components/choosers/TMMetadataChooser.js +1 -1
- package/lib/components/choosers/TMQuickSearchInfo.d.ts +12 -0
- package/lib/components/choosers/TMQuickSearchInfo.js +279 -0
- package/lib/components/choosers/TMQuickSearchSettingsForm.d.ts +16 -0
- package/lib/components/choosers/TMQuickSearchSettingsForm.js +94 -0
- package/lib/components/choosers/TMSelectedValuesSummary.d.ts +13 -0
- package/lib/components/choosers/TMSelectedValuesSummary.js +91 -0
- package/lib/components/editors/TMDropDown.d.ts +3 -0
- package/lib/components/editors/TMDropDown.js +197 -5
- package/lib/components/features/search/TMSearchResult.js +55 -4
- package/lib/components/viewers/TMMidViewer.js +4 -1
- package/lib/components/viewers/TMTidViewer.js +25 -11
- package/lib/helper/SDKUI_Globals.d.ts +20 -0
- package/lib/helper/SDKUI_Globals.js +44 -0
- package/lib/helper/SDKUI_Localizator.d.ts +1 -0
- package/lib/helper/SDKUI_Localizator.js +10 -0
- package/lib/helper/dataGridSearchHelper.d.ts +28 -0
- package/lib/helper/dataGridSearchHelper.js +51 -0
- package/lib/helper/index.d.ts +1 -0
- package/lib/helper/index.js +1 -0
- package/lib/helper/queryHelper.d.ts +2 -0
- package/lib/helper/queryHelper.js +3 -1
- package/lib/hooks/tmDistinctValuesGridHelper.d.ts +16 -0
- package/lib/hooks/tmDistinctValuesGridHelper.js +31 -0
- package/lib/hooks/useTMDistinctValuesMetadataDisplay.d.ts +18 -0
- package/lib/hooks/useTMDistinctValuesMetadataDisplay.js +103 -0
- package/lib/hooks/useTMDistinctValuesQuickSearch.d.ts +49 -0
- package/lib/hooks/useTMDistinctValuesQuickSearch.js +307 -0
- package/lib/hooks/useTMDistinctValuesSelection.d.ts +42 -0
- package/lib/hooks/useTMDistinctValuesSelection.js +103 -0
- package/lib/hooks/useTMDistinctValuesSource.d.ts +29 -0
- package/lib/hooks/useTMDistinctValuesSource.js +105 -0
- package/lib/services/caseflow/CaseFlowService.d.ts +28 -0
- package/lib/services/caseflow/CaseFlowService.js +70 -0
- package/lib/services/platform_services.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { DcmtTypeListCacheService, PlatformObjectValidator, SavedQueryCacheService, SDK_Globals, SDK_Localizator, SearchEngine } from '@topconsultnpm/sdk-ts';
|
|
4
|
+
import styled from 'styled-components';
|
|
5
|
+
import { displayMetadataValue, getSysAllDcmtsSQD, IconInfo, IconSavedQuery, LocalizeQueryOperators, SDKUI_Localizator, SYS_ALL_DCMTS_SQD_ID } from '../../helper';
|
|
6
|
+
import { needsDisplayValue, useTMDistinctValuesMetadataDisplay } from '../../hooks/useTMDistinctValuesMetadataDisplay';
|
|
7
|
+
import TMModal from '../base/TMModal';
|
|
8
|
+
import { TMColors } from '../../utils/theme';
|
|
9
|
+
const TOOLTIP_MAX_CONDITIONS = 8;
|
|
10
|
+
const QUERY_PARAM_PREFIX = '{@QueryParam';
|
|
11
|
+
/* Il riepilogo è lo stesso nel tooltip e nel dettaglio: cambia solo lo spazio a disposizione.
|
|
12
|
+
Nel tooltip la larghezza è limitata perché una riga lunghissima è illeggibile, nel dettaglio il contenuto scorre */
|
|
13
|
+
const StyledSummary = styled.div `
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
gap: 8px;
|
|
17
|
+
${props => props.$isDetail ? 'height: 100%; overflow-y: auto;' : 'min-width: 320px; max-width: 580px;'}
|
|
18
|
+
padding: ${props => props.$isDetail ? '14px 18px' : '10px 12px'};
|
|
19
|
+
text-align: left;
|
|
20
|
+
/* Nel tooltip il testo è un po' più piccolo: è una lettura di passaggio, il dettaglio è quello da leggere con calma */
|
|
21
|
+
font-size: ${props => props.$isDetail ? '1rem' : '0.9375rem'};
|
|
22
|
+
line-height: 1.5;
|
|
23
|
+
color: ${TMColors.text_normal};
|
|
24
|
+
`;
|
|
25
|
+
/* Intestazione del tooltip: nel dettaglio il nome della ricerca è già nel titolo del modale */
|
|
26
|
+
const StyledSummaryTitle = styled.div `
|
|
27
|
+
display: flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
gap: 7px;
|
|
30
|
+
padding-bottom: 8px;
|
|
31
|
+
border-bottom: 1px solid ${TMColors.border_normal};
|
|
32
|
+
font-size: 1.0625em;
|
|
33
|
+
font-weight: 600;
|
|
34
|
+
color: ${TMColors.primary};
|
|
35
|
+
|
|
36
|
+
svg { flex-shrink: 0; }
|
|
37
|
+
`;
|
|
38
|
+
const StyledSectionTitle = styled.div `
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
gap: 7px;
|
|
42
|
+
font-size: 0.8125em;
|
|
43
|
+
font-weight: 700;
|
|
44
|
+
text-transform: uppercase;
|
|
45
|
+
letter-spacing: 0.5px;
|
|
46
|
+
color: ${TMColors.label_normal};
|
|
47
|
+
`;
|
|
48
|
+
const StyledCounter = styled.span `
|
|
49
|
+
display: inline-flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
justify-content: center;
|
|
52
|
+
min-width: 21px;
|
|
53
|
+
height: 21px;
|
|
54
|
+
padding: 0 6px;
|
|
55
|
+
border-radius: 11px;
|
|
56
|
+
background-color: ${TMColors.primary};
|
|
57
|
+
color: white;
|
|
58
|
+
font-weight: 600;
|
|
59
|
+
letter-spacing: 0;
|
|
60
|
+
`;
|
|
61
|
+
const StyledConditions = styled.div `display: flex; flex-direction: column; gap: 5px;`;
|
|
62
|
+
/* Ogni condizione è una riga a sé: il fondo e il filetto colorato la staccano dalle altre e rendono
|
|
63
|
+
leggibile l'elenco anche quando le condizioni sono molte */
|
|
64
|
+
const StyledCondition = styled.div `
|
|
65
|
+
display: flex;
|
|
66
|
+
flex-direction: row;
|
|
67
|
+
align-items: baseline;
|
|
68
|
+
flex-wrap: wrap;
|
|
69
|
+
gap: 7px;
|
|
70
|
+
padding: 7px 10px;
|
|
71
|
+
border-left: 3px solid ${TMColors.primary};
|
|
72
|
+
border-radius: 4px;
|
|
73
|
+
background-color: ${TMColors.toolbar_background};
|
|
74
|
+
`;
|
|
75
|
+
/* La congiunzione con la condizione precedente: a larghezza fissa, così i nomi dei metadati restano incolonnati */
|
|
76
|
+
const StyledConjunction = styled.span `
|
|
77
|
+
flex-shrink: 0;
|
|
78
|
+
width: 42px;
|
|
79
|
+
font-size: 0.8125em;
|
|
80
|
+
font-weight: 700;
|
|
81
|
+
letter-spacing: 0.5px;
|
|
82
|
+
color: ${TMColors.label_normal};
|
|
83
|
+
`;
|
|
84
|
+
/* Nome del metadato, operatore e valore restano sulla stessa riga finché c'è spazio, poi vanno a capo insieme */
|
|
85
|
+
const StyledConditionText = styled.span `
|
|
86
|
+
display: flex;
|
|
87
|
+
flex: 1;
|
|
88
|
+
flex-direction: row;
|
|
89
|
+
flex-wrap: wrap;
|
|
90
|
+
align-items: baseline;
|
|
91
|
+
gap: 5px;
|
|
92
|
+
min-width: 0;
|
|
93
|
+
`;
|
|
94
|
+
const StyledMetadataName = styled.span `font-weight: 600; word-break: break-word;`;
|
|
95
|
+
const StyledOperator = styled.span `color: ${TMColors.label_normal}; white-space: nowrap;`;
|
|
96
|
+
const StyledValue = styled.span `
|
|
97
|
+
padding: 2px 7px;
|
|
98
|
+
border: 1px solid ${TMColors.border_normal};
|
|
99
|
+
border-radius: 3px;
|
|
100
|
+
background-color: ${TMColors.default_background};
|
|
101
|
+
font-weight: 600;
|
|
102
|
+
color: ${TMColors.primary};
|
|
103
|
+
word-break: break-word;
|
|
104
|
+
`;
|
|
105
|
+
/* Il valore chiesto a ogni esecuzione non è un valore di ricerca: il bordo tratteggiato dice che è ancora da riempire */
|
|
106
|
+
const StyledParameter = styled(StyledValue) `
|
|
107
|
+
border-style: dashed;
|
|
108
|
+
border-color: ${TMColors.tertiary};
|
|
109
|
+
background-color: transparent;
|
|
110
|
+
font-style: italic;
|
|
111
|
+
font-weight: normal;
|
|
112
|
+
color: ${TMColors.tertiary};
|
|
113
|
+
`;
|
|
114
|
+
const StyledNote = styled.div `
|
|
115
|
+
display: flex;
|
|
116
|
+
align-items: center;
|
|
117
|
+
gap: 6px;
|
|
118
|
+
font-size: 0.875em;
|
|
119
|
+
color: ${TMColors.label_normal};
|
|
120
|
+
|
|
121
|
+
svg { flex-shrink: 0; }
|
|
122
|
+
`;
|
|
123
|
+
/* Invito ad aprire il dettaglio: nel tooltip il testo è più piccolo e l'elenco può essere troncato */
|
|
124
|
+
const StyledHint = styled(StyledNote) `
|
|
125
|
+
padding-top: 7px;
|
|
126
|
+
border-top: 1px solid ${TMColors.border_normal};
|
|
127
|
+
color: ${TMColors.primary};
|
|
128
|
+
font-weight: 600;
|
|
129
|
+
`;
|
|
130
|
+
const getDcmtTypeName = (dtd) => (SDK_Globals.useLocalizedName ? dtd?.nameLoc : dtd?.name) ?? dtd?.name ?? dtd?.nameLoc ?? undefined;
|
|
131
|
+
const getMetadataName = (md) => (SDK_Globals.useLocalizedName ? md?.nameLoc : md?.name) ?? md?.name ?? md?.nameLoc ?? undefined;
|
|
132
|
+
const isQueryParam = (value) => Boolean(value?.startsWith(QUERY_PARAM_PREFIX));
|
|
133
|
+
/** Valori scritti nella condizione: gli operatori a elenco ne contengono più di uno, separati da virgola e fra apici */
|
|
134
|
+
const splitValues = (value) => (value ?? '').split(',').map(item => {
|
|
135
|
+
const trimmed = item.trim();
|
|
136
|
+
return trimmed.startsWith("'") && trimmed.endsWith("'") ? trimmed.slice(1, -1) : trimmed;
|
|
137
|
+
});
|
|
138
|
+
/* Ogni condizione dell'editor di ricerca nasce racchiusa in una coppia di parentesi che non raggruppa nulla:
|
|
139
|
+
nel riepilogo sono solo rumore, restano quelle che aprono o chiudono un raggruppamento vero */
|
|
140
|
+
const meaningfulBrackets = (brackets) => (brackets && brackets.length > 1) ? brackets : undefined;
|
|
141
|
+
const findMetadata = (dcmtTypes, tid, mid) => dcmtTypes.find(o => o.id === tid)?.metadata?.find(o => o.id === mid);
|
|
142
|
+
const formatValue = (md, value, getDisplayValue) => {
|
|
143
|
+
// Il segnaposto di un parametro non è un valore del metadato: va mostrato com'è
|
|
144
|
+
if (isQueryParam(value))
|
|
145
|
+
return value;
|
|
146
|
+
/* Liste dati e utenti sono archiviati per chiave: nel riepilogo va la descrizione, come nelle griglie
|
|
147
|
+
del pannello, altrimenti il filtro resterebbe illeggibile (es. TD01 invece di FATTURA) */
|
|
148
|
+
return splitValues(value)
|
|
149
|
+
.map(o => needsDisplayValue(md) ? String(getDisplayValue(md, o) ?? o) : displayMetadataValue(md, o))
|
|
150
|
+
.join(', ');
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Valore mostrato per la condizione: il numero di operandi dell'operatore dice quali valori sono significativi
|
|
154
|
+
* e come vanno letti, perché non tutti sono valori del metadato.
|
|
155
|
+
*/
|
|
156
|
+
const buildConditionValue = (wi, md, operands, getDisplayValue) => {
|
|
157
|
+
switch (operands) {
|
|
158
|
+
// L'operatore non ha valori (è nullo, oggi, questo mese, ...): la condizione è tutta nell'operatore
|
|
159
|
+
case 0: return undefined;
|
|
160
|
+
// Numero di ore/giorni/... e condizione scritta a mano: non sono valori del metadato e vanno lasciati come sono
|
|
161
|
+
case 12:
|
|
162
|
+
case 99: return wi.value1;
|
|
163
|
+
case 2: return `${formatValue(md, wi.value1, getDisplayValue)} … ${formatValue(md, wi.value2, getDisplayValue)}`;
|
|
164
|
+
// Valore singolo o elenco di valori: in entrambi i casi sono valori del metadato
|
|
165
|
+
default: return formatValue(md, wi.value1, getDisplayValue);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
const buildCondition = (wi, md, index, dcmtTypes, fromTid, getDisplayValue) => {
|
|
169
|
+
const operands = PlatformObjectValidator.GetNumberOfOperands(wi.operator);
|
|
170
|
+
return {
|
|
171
|
+
conjunction: index <= 0 ? undefined : (wi.or ? 'OR' : 'AND'),
|
|
172
|
+
leftBrackets: meaningfulBrackets(wi.leftBrackets),
|
|
173
|
+
rightBrackets: meaningfulBrackets(wi.rightBrackets),
|
|
174
|
+
// Con i join il nome del metadato da solo non basta a capire su quale tipo documento si sta filtrando
|
|
175
|
+
dcmtTypeName: wi.tid && wi.tid !== fromTid ? (wi.alias ?? getDcmtTypeName(dcmtTypes.find(o => o.id === wi.tid))) : undefined,
|
|
176
|
+
metadataName: getMetadataName(md) ?? `MID ${wi.mid ?? 0}`,
|
|
177
|
+
operator: LocalizeQueryOperators(wi.operator),
|
|
178
|
+
value: buildConditionValue(wi, md, operands, getDisplayValue),
|
|
179
|
+
isParameter: isQueryParam(wi.value1) || isQueryParam(wi.value2),
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* Filtri effettivamente applicati dalla ricerca: in modalità easy quelli senza valore vengono scartati prima
|
|
184
|
+
* della ricerca (sono i campi lasciati vuoti dall'utente) e mostrarli farebbe credere che la ricerca filtri
|
|
185
|
+
* su tutti i metadati, che è il caso di "Tutti i documenti".
|
|
186
|
+
*/
|
|
187
|
+
const getEffectiveWhere = (sqd, qd) => sqd?.isEasyWhere === 1
|
|
188
|
+
? (qd?.where ?? []).filter(o => PlatformObjectValidator.WhereItemHasValues(o))
|
|
189
|
+
: (qd?.where ?? []);
|
|
190
|
+
/**
|
|
191
|
+
* Righe da cui vengono ricavate le chiavi per cui caricare le descrizioni: hanno la stessa forma delle righe
|
|
192
|
+
* di un risultato di ricerca (un valore per condizione), perché è quella attesa dalle cache del pannello.
|
|
193
|
+
*/
|
|
194
|
+
const buildDisplayRows = (where) => {
|
|
195
|
+
const rows = [];
|
|
196
|
+
where.forEach((wi, index) => {
|
|
197
|
+
[...splitValues(wi.value1), ...splitValues(wi.value2)].forEach((value, valueIndex) => {
|
|
198
|
+
rows[valueIndex] ??= [];
|
|
199
|
+
rows[valueIndex][index] = value;
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
return rows;
|
|
203
|
+
};
|
|
204
|
+
const TMQuickSearchCondition = ({ condition }) => (_jsxs(StyledCondition, { children: [_jsx(StyledConjunction, { children: condition.conjunction }), _jsxs(StyledConditionText, { children: [condition.leftBrackets && _jsx(StyledOperator, { children: condition.leftBrackets }), _jsxs(StyledMetadataName, { children: [condition.dcmtTypeName && _jsxs(StyledOperator, { children: [condition.dcmtTypeName, "."] }), condition.metadataName] }), _jsx(StyledOperator, { children: condition.operator }), condition.value && (condition.isParameter
|
|
205
|
+
? _jsx(StyledParameter, { children: condition.value })
|
|
206
|
+
: _jsx(StyledValue, { children: condition.value })), condition.rightBrackets && _jsx(StyledOperator, { children: condition.rightBrackets })] })] }));
|
|
207
|
+
/** Riepilogo mostrato nel tooltip e nel dettaglio: nel dettaglio il nome della ricerca è già nel titolo del modale */
|
|
208
|
+
const TMQuickSearchSummary = ({ summary, isDetail = false }) => {
|
|
209
|
+
if (!summary) {
|
|
210
|
+
return (_jsx(StyledSummary, { "$isDetail": isDetail, children: _jsxs(StyledNote, { children: [SDKUI_Localizator.Loading, " ..."] }) }));
|
|
211
|
+
}
|
|
212
|
+
if (summary.isUnavailable) {
|
|
213
|
+
return (_jsxs(StyledSummary, { "$isDetail": isDetail, children: [!isDetail && _jsxs(StyledSummaryTitle, { children: [_jsx(IconSavedQuery, { fontSize: 19 }), summary.name] }), _jsxs(StyledNote, { children: [_jsx(IconInfo, { fontSize: 17 }), "La ricerca rapida configurata non \u00E8 pi\u00F9 disponibile"] })] }));
|
|
214
|
+
}
|
|
215
|
+
// Nel tooltip le condizioni oltre il limite vengono troncate: il tooltip non è scorribile
|
|
216
|
+
const shownConditions = isDetail ? summary.conditions : summary.conditions.slice(0, TOOLTIP_MAX_CONDITIONS);
|
|
217
|
+
const hiddenConditionsCount = summary.conditions.length - shownConditions.length;
|
|
218
|
+
const hasParameters = shownConditions.some(o => o.isParameter);
|
|
219
|
+
return (_jsxs(StyledSummary, { "$isDetail": isDetail, children: [!isDetail && _jsxs(StyledSummaryTitle, { children: [_jsx(IconSavedQuery, { fontSize: 19 }), summary.name] }), summary.conditions.length <= 0
|
|
220
|
+
? _jsxs(StyledNote, { children: [_jsx(IconInfo, { fontSize: 17 }), "Nessun filtro: la ricerca restituisce tutti i documenti"] })
|
|
221
|
+
: _jsxs(_Fragment, { children: [_jsxs(StyledSectionTitle, { children: [SDK_Localizator.QueryWhere, _jsx(StyledCounter, { children: summary.conditions.length })] }), _jsx(StyledConditions, { children: shownConditions.map((condition, index) => _jsx(TMQuickSearchCondition, { condition: condition }, index)) }), hiddenConditionsCount > 0 &&
|
|
222
|
+
_jsxs(StyledNote, { children: [_jsx(IconInfo, { fontSize: 17 }), "Altre ", hiddenConditionsCount, " condizioni non mostrate"] })] }), hasParameters && _jsxs(StyledNote, { children: [_jsx(IconInfo, { fontSize: 17 }), "I valori tratteggiati vengono chiesti a ogni esecuzione della ricerca"] }), !isDetail && _jsxs(StyledHint, { children: [_jsx(IconInfo, { fontSize: 17 }), "Fare clic per aprire il dettaglio e leggerlo meglio"] })] }));
|
|
223
|
+
};
|
|
224
|
+
export const useTMQuickSearchInfo = ({ tid, sqdId }) => {
|
|
225
|
+
// Le descrizioni di liste dati e utenti vengono dalle stesse cache usate dalle griglie del pannello
|
|
226
|
+
const { loadDisplayCachesAsync, getMetadataDisplayValue } = useTMDistinctValuesMetadataDisplay();
|
|
227
|
+
const [summary, setSummary] = useState();
|
|
228
|
+
const [showDetail, setShowDetail] = useState(false);
|
|
229
|
+
// Identifica la richiesta corrente: dopo ogni await le risposte delle richieste obsolete vengono scartate
|
|
230
|
+
const requestIdRef = useRef(0);
|
|
231
|
+
useEffect(() => { loadAsync(); }, [tid, sqdId]);
|
|
232
|
+
const loadAsync = async () => {
|
|
233
|
+
const requestId = ++requestIdRef.current;
|
|
234
|
+
const isStale = () => requestId !== requestIdRef.current;
|
|
235
|
+
setSummary(undefined);
|
|
236
|
+
if (!sqdId)
|
|
237
|
+
return;
|
|
238
|
+
try {
|
|
239
|
+
const sqd = sqdId === SYS_ALL_DCMTS_SQD_ID
|
|
240
|
+
? await getSysAllDcmtsSQD(tid, false)
|
|
241
|
+
: await SavedQueryCacheService.GetAsync(sqdId);
|
|
242
|
+
const qd = SearchEngine.NormalizeQueryDescriptor(sqd?.qd);
|
|
243
|
+
const name = sqd?.name ?? (sqd?.id ? `ID ${sqd.id}` : SDK_Localizator.SavedQuery);
|
|
244
|
+
if (!qd) {
|
|
245
|
+
if (!isStale())
|
|
246
|
+
setSummary({ name, conditions: [], isUnavailable: true });
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
// I nomi dei metadati stanno sui tipi documento della ricerca: con i join le condizioni ne riguardano più di uno
|
|
250
|
+
const dcmtTypes = await DcmtTypeListCacheService.GetFromQdAsync(qd);
|
|
251
|
+
const where = getEffectiveWhere(sqd, qd);
|
|
252
|
+
const whereMd = where.map(wi => findMetadata(dcmtTypes, wi.tid, wi.mid));
|
|
253
|
+
// Le descrizioni si leggono dalle cache: vanno caricate prima di comporre il riepilogo
|
|
254
|
+
await loadDisplayCachesAsync(whereMd, buildDisplayRows(where));
|
|
255
|
+
if (isStale())
|
|
256
|
+
return;
|
|
257
|
+
setSummary({
|
|
258
|
+
name,
|
|
259
|
+
conditions: where.map((wi, index) => buildCondition(wi, whereMd[index], index, dcmtTypes, qd.from?.tid, getMetadataDisplayValue)),
|
|
260
|
+
isUnavailable: false,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
/* Il riepilogo è un di più: un errore qui non deve disturbare con una finestra di eccezione,
|
|
265
|
+
basta dire nel tooltip che la ricerca non è leggibile */
|
|
266
|
+
if (!isStale())
|
|
267
|
+
setSummary({ name: SDK_Localizator.SavedQuery, conditions: [], isUnavailable: true });
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
const detailTitle = useMemo(() => `${SDK_Localizator.SavedQuery}${summary?.name ? ` - ${summary.name}` : ''}`, [summary?.name]);
|
|
271
|
+
const infoButton = !sqdId ? undefined : {
|
|
272
|
+
icon: _jsx(IconInfo, {}),
|
|
273
|
+
text: SDK_Localizator.SavedQuery,
|
|
274
|
+
tooltipContent: _jsx(TMQuickSearchSummary, { summary: summary }),
|
|
275
|
+
onClick: () => setShowDetail(true),
|
|
276
|
+
};
|
|
277
|
+
const detailForm = !showDetail ? null : (_jsx(TMModal, { title: detailTitle, width: '620px', height: '520px', onClose: () => setShowDetail(false), children: _jsx(TMQuickSearchSummary, { summary: summary, isDetail: true }) }));
|
|
278
|
+
return { infoButton, detailForm };
|
|
279
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { DistinctValuesQuickSearchSettings } from '../../helper';
|
|
3
|
+
/** Impostazione della ricerca rapida usata dal pannello dei valori distinti: è la stessa che viene persistita nelle impostazioni utente */
|
|
4
|
+
export type QuickSearchSettings = DistinctValuesQuickSearchSettings;
|
|
5
|
+
/** Impostazione vuota da cui parte la configurazione: solo il numero massimo di documenti ha un valore predefinito */
|
|
6
|
+
export declare const newQuickSearchSettings: () => QuickSearchSettings;
|
|
7
|
+
interface ITMQuickSearchSettingsForm {
|
|
8
|
+
/** Impostazione di partenza: se assente il form parte vuoto */
|
|
9
|
+
settings?: QuickSearchSettings;
|
|
10
|
+
onSave?: (settings: QuickSearchSettings) => void;
|
|
11
|
+
/** Chiede l'eliminazione dell'impostazione salvata: la conferma spetta al chiamante. Senza il gestore il pulsante di eliminazione non viene mostrato */
|
|
12
|
+
onDelete?: () => void;
|
|
13
|
+
onClose?: () => void;
|
|
14
|
+
}
|
|
15
|
+
declare const TMQuickSearchSettingsForm: React.FC<ITMQuickSearchSettingsForm>;
|
|
16
|
+
export default TMQuickSearchSettingsForm;
|
|
@@ -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;
|
|
@@ -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>;
|