@praxisui/core 10.0.0-rc.3 → 10.0.0-rc.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -1
- package/ai/component-registry.json +4 -3
- package/fesm2022/praxisui-core-schema.mjs +2946 -0
- package/fesm2022/praxisui-core.mjs +3188 -106
- package/package.json +14 -2
- package/theming/README.md +63 -0
- package/theming/_theming.scss +543 -0
- package/theming/index.scss +1 -0
- package/theming/theming.scss +1 -0
- package/types/praxisui-core-schema.d.ts +4418 -0
- package/types/praxisui-core.d.ts +5920 -4834
|
@@ -0,0 +1,2946 @@
|
|
|
1
|
+
function normalizeFieldAccessMetadata(value) {
|
|
2
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
3
|
+
return undefined;
|
|
4
|
+
}
|
|
5
|
+
const source = value;
|
|
6
|
+
const visibleForAuthorities = normalizeAuthorityList(source['visibleForAuthorities']);
|
|
7
|
+
const editableForAuthorities = normalizeAuthorityList(source['editableForAuthorities']);
|
|
8
|
+
const reason = typeof source['reason'] === 'string' && source['reason'].trim()
|
|
9
|
+
? source['reason'].trim()
|
|
10
|
+
: undefined;
|
|
11
|
+
if (!visibleForAuthorities && !editableForAuthorities) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
...(visibleForAuthorities ? { visibleForAuthorities } : {}),
|
|
16
|
+
...(editableForAuthorities ? { editableForAuthorities } : {}),
|
|
17
|
+
...(reason ? { reason } : {}),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function evaluateFieldAccess(fieldOrAccess, context = {}) {
|
|
21
|
+
const fieldAccess = normalizeFieldAccessMetadata(isFieldAccessMetadata(fieldOrAccess)
|
|
22
|
+
? fieldOrAccess
|
|
23
|
+
: fieldOrAccess?.fieldAccess);
|
|
24
|
+
if (!fieldAccess) {
|
|
25
|
+
return { evaluated: false, visible: true, editable: true };
|
|
26
|
+
}
|
|
27
|
+
const authorities = normalizeAuthoritySet(context.authorities);
|
|
28
|
+
if (!authorities) {
|
|
29
|
+
return {
|
|
30
|
+
evaluated: false,
|
|
31
|
+
visible: true,
|
|
32
|
+
editable: true,
|
|
33
|
+
...(fieldAccess.reason ? { reason: fieldAccess.reason } : {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const hasExplicitEditablePolicy = !!fieldAccess.editableForAuthorities?.length;
|
|
37
|
+
const editableAllowed = hasExplicitEditablePolicy
|
|
38
|
+
? matchesAuthority(fieldAccess.editableForAuthorities, authorities)
|
|
39
|
+
: false;
|
|
40
|
+
const visibleAllowed = matchesAuthority(fieldAccess.visibleForAuthorities, authorities) ||
|
|
41
|
+
editableAllowed;
|
|
42
|
+
return {
|
|
43
|
+
evaluated: true,
|
|
44
|
+
visible: visibleAllowed,
|
|
45
|
+
editable: editableAllowed || !hasExplicitEditablePolicy,
|
|
46
|
+
...(fieldAccess.reason ? { reason: fieldAccess.reason } : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function isFieldAccessMetadata(value) {
|
|
50
|
+
return !!value && ('visibleForAuthorities' in value ||
|
|
51
|
+
'editableForAuthorities' in value ||
|
|
52
|
+
'reason' in value);
|
|
53
|
+
}
|
|
54
|
+
function normalizeAuthorityList(value) {
|
|
55
|
+
if (!Array.isArray(value)) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
const normalized = Array.from(new Set(value
|
|
59
|
+
.filter((item) => typeof item === 'string')
|
|
60
|
+
.map((item) => item.trim())
|
|
61
|
+
.filter(Boolean)));
|
|
62
|
+
return normalized.length ? normalized : undefined;
|
|
63
|
+
}
|
|
64
|
+
function normalizeAuthoritySet(authorities) {
|
|
65
|
+
if (!Array.isArray(authorities)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return new Set(authorities
|
|
69
|
+
.filter((item) => typeof item === 'string')
|
|
70
|
+
.map((item) => item.trim())
|
|
71
|
+
.filter(Boolean));
|
|
72
|
+
}
|
|
73
|
+
function matchesAuthority(required, actual) {
|
|
74
|
+
if (!required || required.length === 0) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
return required.some((authority) => actual.has(authority));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const VISUALIZATION_KINDS = new Set([
|
|
81
|
+
'line',
|
|
82
|
+
'area',
|
|
83
|
+
'column',
|
|
84
|
+
'comparison',
|
|
85
|
+
'stackedBar',
|
|
86
|
+
'radial',
|
|
87
|
+
'harveyBall',
|
|
88
|
+
'bullet',
|
|
89
|
+
'delta',
|
|
90
|
+
'processFlow',
|
|
91
|
+
]);
|
|
92
|
+
const VISUALIZATION_SURFACES = new Set([
|
|
93
|
+
'table-cell',
|
|
94
|
+
'list-item',
|
|
95
|
+
'object-header',
|
|
96
|
+
'card-summary',
|
|
97
|
+
'form-presentation',
|
|
98
|
+
]);
|
|
99
|
+
const VISUALIZATION_SIZES = new Set([
|
|
100
|
+
'xs',
|
|
101
|
+
'sm',
|
|
102
|
+
'md',
|
|
103
|
+
'lg',
|
|
104
|
+
'responsive',
|
|
105
|
+
]);
|
|
106
|
+
const VISUALIZATION_TONES = new Set([
|
|
107
|
+
'neutral',
|
|
108
|
+
'info',
|
|
109
|
+
'success',
|
|
110
|
+
'warning',
|
|
111
|
+
'danger',
|
|
112
|
+
'critical',
|
|
113
|
+
]);
|
|
114
|
+
const TABLE_SAFE_KINDS = new Set([
|
|
115
|
+
'line',
|
|
116
|
+
'area',
|
|
117
|
+
'column',
|
|
118
|
+
'comparison',
|
|
119
|
+
'stackedBar',
|
|
120
|
+
'radial',
|
|
121
|
+
'harveyBall',
|
|
122
|
+
'bullet',
|
|
123
|
+
'delta',
|
|
124
|
+
'processFlow',
|
|
125
|
+
]);
|
|
126
|
+
const TABLE_CELL_RECOMMENDED_KINDS = new Set([
|
|
127
|
+
'delta',
|
|
128
|
+
'bullet',
|
|
129
|
+
'radial',
|
|
130
|
+
'harveyBall',
|
|
131
|
+
'stackedBar',
|
|
132
|
+
]);
|
|
133
|
+
/**
|
|
134
|
+
* Returns the compact-table suitability of a supported visualization kind.
|
|
135
|
+
* Trend and workflow-heavy kinds are conditional because they require enough
|
|
136
|
+
* column width and a clear decision-making purpose to remain legible.
|
|
137
|
+
*/
|
|
138
|
+
function getPraxisTableCellVisualizationGuidance(kind) {
|
|
139
|
+
return TABLE_CELL_RECOMMENDED_KINDS.has(kind) ? 'recommended' : 'conditional';
|
|
140
|
+
}
|
|
141
|
+
function getPraxisTableCellVisualizationConstraint(kind) {
|
|
142
|
+
if (kind === 'comparison')
|
|
143
|
+
return 'row-density';
|
|
144
|
+
if (kind === 'processFlow')
|
|
145
|
+
return 'step-labels';
|
|
146
|
+
if (kind === 'line' || kind === 'area' || kind === 'column') {
|
|
147
|
+
return 'horizontal-space';
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Resolucao pura e canonica de tom semantico e rotulo acessivel para micro-visualizacoes.
|
|
153
|
+
*
|
|
154
|
+
* Precedencia de avaliacao:
|
|
155
|
+
* 1. Correspondencia discreta (`equals`), tolerante a numeros e strings.
|
|
156
|
+
* 2. Faixa numerica continua (`min` e `max`, inclusiva).
|
|
157
|
+
* 3. Faixa aberta a direita (`min` ate infinito).
|
|
158
|
+
* 4. Faixa aberta a esquerda (menos infinito ate `max`).
|
|
159
|
+
* 5. Limiar de corte progressivo (`value`).
|
|
160
|
+
* 6. Fallback estatico (`fallbackTone`).
|
|
161
|
+
*/
|
|
162
|
+
function resolvePraxisMicroVisualizationTone(value, thresholds, fallbackTone) {
|
|
163
|
+
if (value === undefined || value === null || !Array.isArray(thresholds) || thresholds.length === 0) {
|
|
164
|
+
return fallbackTone ? { tone: fallbackTone } : undefined;
|
|
165
|
+
}
|
|
166
|
+
const num = typeof value === 'number' ? value : Number(value);
|
|
167
|
+
const isNum = Number.isFinite(num);
|
|
168
|
+
// 1. Pass 1: Correspondencia discreta (equals) tem prioridade maxima
|
|
169
|
+
for (const t of thresholds) {
|
|
170
|
+
if (!t || !t.tone || t.equals === undefined) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (t.equals === value || String(t.equals) === String(value)) {
|
|
174
|
+
return { tone: t.tone, label: t.label, matchedThreshold: t };
|
|
175
|
+
}
|
|
176
|
+
if (isNum && Number.isFinite(Number(t.equals)) && Number(t.equals) === num) {
|
|
177
|
+
return { tone: t.tone, label: t.label, matchedThreshold: t };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// 2. Pass 2: Faixas continuas ou limiares de corte
|
|
181
|
+
if (isNum) {
|
|
182
|
+
for (const t of thresholds) {
|
|
183
|
+
if (!t || !t.tone) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const hasMin = t.min !== undefined && Number.isFinite(t.min);
|
|
187
|
+
const hasMax = t.max !== undefined && Number.isFinite(t.max);
|
|
188
|
+
if (hasMin && hasMax) {
|
|
189
|
+
if (num >= t.min && num <= t.max) {
|
|
190
|
+
return { tone: t.tone, label: t.label, matchedThreshold: t };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else if (hasMin) {
|
|
194
|
+
if (num >= t.min) {
|
|
195
|
+
return { tone: t.tone, label: t.label, matchedThreshold: t };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else if (hasMax) {
|
|
199
|
+
if (num <= t.max) {
|
|
200
|
+
return { tone: t.tone, label: t.label, matchedThreshold: t };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else if (t.value !== undefined && Number.isFinite(t.value) && num >= t.value) {
|
|
204
|
+
return { tone: t.tone, label: t.label, matchedThreshold: t };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return fallbackTone ? { tone: fallbackTone } : undefined;
|
|
209
|
+
}
|
|
210
|
+
function normalizePraxisPresentationVisualization(value) {
|
|
211
|
+
if (!value || typeof value !== 'object') {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
const kind = normalizeEnum$1(value.kind, VISUALIZATION_KINDS);
|
|
215
|
+
const fallbackText = normalizeText$1(value.fallbackText);
|
|
216
|
+
if (!kind || !fallbackText) {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
const surface = normalizeEnum$1(value.surface, VISUALIZATION_SURFACES);
|
|
220
|
+
const size = normalizeEnum$1(value.size, VISUALIZATION_SIZES);
|
|
221
|
+
const tone = normalizeEnum$1(value.tone, VISUALIZATION_TONES);
|
|
222
|
+
const ariaLabel = normalizeText$1(value.ariaLabel);
|
|
223
|
+
const valueSuffix = normalizeText$1(value.valueSuffix);
|
|
224
|
+
const thresholdsObj = normalizeThresholds(value.thresholds);
|
|
225
|
+
let resolvedTone = tone;
|
|
226
|
+
let resolvedAriaLabel = ariaLabel;
|
|
227
|
+
if (!resolvedTone && thresholdsObj?.thresholds?.length) {
|
|
228
|
+
let evalVal = value.value;
|
|
229
|
+
if (evalVal === undefined && Array.isArray(value.points) && value.points.length > 0) {
|
|
230
|
+
const last = value.points[value.points.length - 1];
|
|
231
|
+
if (last && typeof last === 'object' && Number.isFinite(last.value)) {
|
|
232
|
+
evalVal = last.value;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else if (Array.isArray(evalVal) && evalVal.length > 0) {
|
|
236
|
+
const last = evalVal[evalVal.length - 1];
|
|
237
|
+
evalVal = typeof last === 'object' && last !== null && 'value' in last ? last.value : last;
|
|
238
|
+
}
|
|
239
|
+
if (evalVal !== undefined && evalVal !== null) {
|
|
240
|
+
const resolution = resolvePraxisMicroVisualizationTone(evalVal, thresholdsObj.thresholds);
|
|
241
|
+
if (resolution?.tone) {
|
|
242
|
+
resolvedTone = resolution.tone;
|
|
243
|
+
}
|
|
244
|
+
if (resolution?.label && !resolvedAriaLabel) {
|
|
245
|
+
resolvedAriaLabel = `${fallbackText}: ${resolution.label}`;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
kind,
|
|
251
|
+
fallbackText,
|
|
252
|
+
...(surface ? { surface } : {}),
|
|
253
|
+
...(size ? { size } : {}),
|
|
254
|
+
...(resolvedTone ? { tone: resolvedTone } : {}),
|
|
255
|
+
...(resolvedAriaLabel ? { ariaLabel: resolvedAriaLabel } : {}),
|
|
256
|
+
...(valueSuffix ? { valueSuffix } : {}),
|
|
257
|
+
...(typeof value.compactValue === 'boolean' ? { compactValue: value.compactValue } : {}),
|
|
258
|
+
...(Object.prototype.hasOwnProperty.call(value, 'value') ? { value: value.value } : {}),
|
|
259
|
+
...(normalizeExpression(value.valueExpr, 'valueExpr') ?? {}),
|
|
260
|
+
...(normalizeExpression(value.valueSuffixExpr, 'valueSuffixExpr') ?? {}),
|
|
261
|
+
...(normalizeNumber(value.total, 'total') ?? {}),
|
|
262
|
+
...(normalizeExpression(value.totalExpr, 'totalExpr') ?? {}),
|
|
263
|
+
...(normalizeNumber(value.target, 'target') ?? {}),
|
|
264
|
+
...(normalizeExpression(value.targetExpr, 'targetExpr') ?? {}),
|
|
265
|
+
...(normalizeNumber(value.baseline, 'baseline') ?? {}),
|
|
266
|
+
...(normalizeExpression(value.baselineExpr, 'baselineExpr') ?? {}),
|
|
267
|
+
...(normalizePoints(value.points) ?? {}),
|
|
268
|
+
...(normalizeExpression(value.pointsExpr, 'pointsExpr') ?? {}),
|
|
269
|
+
...(normalizeSegments(value.segments) ?? {}),
|
|
270
|
+
...(normalizeExpression(value.segmentsExpr, 'segmentsExpr') ?? {}),
|
|
271
|
+
...(normalizeThresholds(value.thresholds) ?? {}),
|
|
272
|
+
...(normalizeExpression(value.thresholdsExpr, 'thresholdsExpr') ?? {}),
|
|
273
|
+
...(normalizeItems(value.items) ?? {}),
|
|
274
|
+
...(normalizeExpression(value.itemsExpr, 'itemsExpr') ?? {}),
|
|
275
|
+
...(normalizeExpression(value.toneExpr, 'toneExpr') ?? {}),
|
|
276
|
+
...(value.stepLabelWidth !== undefined && (typeof value.stepLabelWidth === 'number' || typeof value.stepLabelWidth === 'string')
|
|
277
|
+
? { stepLabelWidth: value.stepLabelWidth }
|
|
278
|
+
: {}),
|
|
279
|
+
...(typeof value.stepLabelLineClamp === 'number' && Number.isFinite(value.stepLabelLineClamp)
|
|
280
|
+
? { stepLabelLineClamp: value.stepLabelLineClamp }
|
|
281
|
+
: {}),
|
|
282
|
+
...(normalizeExpression(value.ariaLabelExpr, 'ariaLabelExpr') ?? {}),
|
|
283
|
+
...(normalizeExpression(value.fallbackTextExpr, 'fallbackTextExpr') ?? {}),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function isPraxisPresentationVisualizationTableSafe(value) {
|
|
287
|
+
const normalized = normalizePraxisPresentationVisualization(value);
|
|
288
|
+
return !!normalized && TABLE_SAFE_KINDS.has(normalized.kind);
|
|
289
|
+
}
|
|
290
|
+
function renderPraxisPresentationVisualizationHtml(value, options = {}) {
|
|
291
|
+
const visualization = normalizePraxisPresentationVisualization(value);
|
|
292
|
+
if (!visualization) {
|
|
293
|
+
return '';
|
|
294
|
+
}
|
|
295
|
+
switch (visualization.kind) {
|
|
296
|
+
case 'comparison':
|
|
297
|
+
return renderComparisonVisualizationHtml(visualization, options);
|
|
298
|
+
case 'stackedBar':
|
|
299
|
+
return renderStackedBarVisualizationHtml(visualization, options);
|
|
300
|
+
case 'bullet':
|
|
301
|
+
return renderBulletVisualizationHtml(visualization, options);
|
|
302
|
+
case 'delta':
|
|
303
|
+
return renderDeltaVisualizationHtml(visualization, options);
|
|
304
|
+
case 'radial':
|
|
305
|
+
return renderRadialVisualizationHtml(visualization, options);
|
|
306
|
+
case 'harveyBall':
|
|
307
|
+
return renderHarveyBallVisualizationHtml(visualization, options);
|
|
308
|
+
case 'line':
|
|
309
|
+
return renderLineVisualizationHtml(visualization, options);
|
|
310
|
+
case 'area':
|
|
311
|
+
return renderAreaVisualizationHtml(visualization, options);
|
|
312
|
+
case 'column':
|
|
313
|
+
return renderColumnVisualizationHtml(visualization, options);
|
|
314
|
+
case 'processFlow':
|
|
315
|
+
return renderProcessFlowVisualizationHtml(visualization, options);
|
|
316
|
+
default:
|
|
317
|
+
return `<span class="pfx-micro-viz__fallback">${escapeHtml(visualization.fallbackText)}</span>`;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function renderComparisonVisualizationHtml(visualization, options) {
|
|
321
|
+
const points = visualization.points ?? [];
|
|
322
|
+
if (points.length === 0) {
|
|
323
|
+
return renderFallbackVisualizationHtml(visualization, visualization.tone ?? 'neutral');
|
|
324
|
+
}
|
|
325
|
+
const max = Math.max(1, ...points.map((point) => Math.max(0, point.value)));
|
|
326
|
+
const rows = points
|
|
327
|
+
.map((point) => {
|
|
328
|
+
const width = percent(point.value, max);
|
|
329
|
+
return `
|
|
330
|
+
<span class="pfx-micro-viz__row" data-tone="${escapeHtml(point.tone ?? visualization.tone ?? 'info')}">
|
|
331
|
+
<span class="pfx-micro-viz__label">${escapeHtml(point.label ?? '')}</span>
|
|
332
|
+
<span class="pfx-micro-viz__track">
|
|
333
|
+
<span class="pfx-micro-viz__bar" style="width:${width}%"></span>
|
|
334
|
+
</span>
|
|
335
|
+
<span class="pfx-micro-viz__value">${escapeHtml(formatMicroNumber(point.value, options.locale))}</span>
|
|
336
|
+
</span>`;
|
|
337
|
+
})
|
|
338
|
+
.join('');
|
|
339
|
+
return `<span class="pfx-micro-viz" role="img" aria-label="${escapeHtml(visualization.ariaLabel ?? visualization.fallbackText)}">${rows}</span>`;
|
|
340
|
+
}
|
|
341
|
+
function renderStackedBarVisualizationHtml(visualization, options) {
|
|
342
|
+
const segments = visualization.segments ?? [];
|
|
343
|
+
if (segments.length === 0) {
|
|
344
|
+
return renderFallbackVisualizationHtml(visualization, visualization.tone ?? 'neutral');
|
|
345
|
+
}
|
|
346
|
+
const total = visualization.total && visualization.total > 0
|
|
347
|
+
? visualization.total
|
|
348
|
+
: Math.max(1, segments.reduce((sum, segment) => sum + Math.max(0, segment.value), 0));
|
|
349
|
+
const bars = segments
|
|
350
|
+
.map((segment) => `
|
|
351
|
+
<span
|
|
352
|
+
class="pfx-micro-viz__segment"
|
|
353
|
+
data-tone="${escapeHtml(segment.tone ?? visualization.tone ?? 'info')}"
|
|
354
|
+
style="width:${percent(segment.value, total)}%"
|
|
355
|
+
title="${escapeHtml(`${segment.label ?? ''} ${formatMicroNumber(segment.value, options.locale)}`.trim())}"
|
|
356
|
+
></span>`)
|
|
357
|
+
.join('');
|
|
358
|
+
const meta = segments
|
|
359
|
+
.slice(0, 3)
|
|
360
|
+
.map((segment) => `<span>${escapeHtml(segment.label ?? '')}: ${escapeHtml(formatMicroNumber(segment.value, options.locale))}</span>`)
|
|
361
|
+
.join('');
|
|
362
|
+
return `
|
|
363
|
+
<span class="pfx-micro-viz" role="img" aria-label="${escapeHtml(visualization.ariaLabel ?? visualization.fallbackText)}">
|
|
364
|
+
<span class="pfx-micro-viz__segments">${bars}</span>
|
|
365
|
+
<span class="pfx-micro-viz__meta">${meta}</span>
|
|
366
|
+
</span>`;
|
|
367
|
+
}
|
|
368
|
+
function resolveBulletMax(visualization) {
|
|
369
|
+
const thresholds = visualization.thresholds ?? [];
|
|
370
|
+
const thresholdValues = thresholds
|
|
371
|
+
.map(t => t.value ?? t.max ?? t.min ?? 0)
|
|
372
|
+
.filter((v) => Number.isFinite(v) && v > 0);
|
|
373
|
+
const values = [
|
|
374
|
+
toFiniteNumber(visualization.value) ?? 0,
|
|
375
|
+
visualization.target ?? 0,
|
|
376
|
+
visualization.total ?? 0,
|
|
377
|
+
...thresholdValues
|
|
378
|
+
].filter((v) => Number.isFinite(v) && v > 0);
|
|
379
|
+
return Math.max(...values, 100);
|
|
380
|
+
}
|
|
381
|
+
function renderBulletVisualizationHtml(visualization, options) {
|
|
382
|
+
const currentValue = toFiniteNumber(visualization.value);
|
|
383
|
+
if (currentValue === undefined) {
|
|
384
|
+
return renderFallbackVisualizationHtml(visualization, visualization.tone ?? 'neutral');
|
|
385
|
+
}
|
|
386
|
+
const max = resolveBulletMax(visualization);
|
|
387
|
+
const target = toFiniteNumber(visualization.target);
|
|
388
|
+
const targetLeft = target === undefined ? null : percent(target, max);
|
|
389
|
+
const isTable = visualization.surface === 'table-cell';
|
|
390
|
+
// thresholds: sort ascending by boundary value so bands draw continuously regardless of declaration order
|
|
391
|
+
const thresholds = visualization.thresholds ?? [];
|
|
392
|
+
const sortedThresholds = [...thresholds].sort((a, b) => {
|
|
393
|
+
const aVal = a.value ?? a.max ?? a.min ?? 0;
|
|
394
|
+
const bVal = b.value ?? b.max ?? b.min ?? 0;
|
|
395
|
+
return aVal - bVal;
|
|
396
|
+
});
|
|
397
|
+
let previous = 0;
|
|
398
|
+
const bands = sortedThresholds.map(t => {
|
|
399
|
+
const tVal = t.value ?? t.max ?? t.min ?? 0;
|
|
400
|
+
const current = Math.max(tVal, previous);
|
|
401
|
+
const left = max > 0 ? percent(previous, max) : '0';
|
|
402
|
+
const width = max > 0 ? percent(current - previous, max) : '0';
|
|
403
|
+
previous = current;
|
|
404
|
+
return { left, width, tone: t.tone ?? 'info' };
|
|
405
|
+
}).filter(b => parseFloat(b.width) > 0);
|
|
406
|
+
const bandsHtml = bands.map(b => `<span class="pfx-micro-bullet__range" data-tone="${escapeHtml(b.tone)}" style="left: ${b.left}%; width: ${b.width}%;"></span>`).join('');
|
|
407
|
+
const topHtml = isTable ? '' : `
|
|
408
|
+
<div class="pfx-micro-bullet__top">
|
|
409
|
+
<span class="pfx-micro-bullet__label">${escapeHtml(visualization.fallbackText)}</span>
|
|
410
|
+
<span class="pfx-micro-bullet__value">
|
|
411
|
+
${escapeHtml(formatMicroNumber(currentValue, options.locale))}
|
|
412
|
+
${target === undefined ? '' : `<span class="pfx-micro-bullet__target-val">/ Target: ${escapeHtml(formatMicroNumber(target, options.locale))}</span>`}
|
|
413
|
+
</span>
|
|
414
|
+
</div>`;
|
|
415
|
+
const bottomHtml = isTable ? '' : `
|
|
416
|
+
<div class="pfx-micro-bullet__bottom">
|
|
417
|
+
<span>${escapeHtml(formatMicroNumber(visualization.baseline ?? 0, options.locale))}</span>
|
|
418
|
+
<span>${escapeHtml(formatMicroNumber(max, options.locale))}</span>
|
|
419
|
+
</div>`;
|
|
420
|
+
const showCompactValue = isTable && visualization.compactValue !== false;
|
|
421
|
+
const compactValueHtml = showCompactValue
|
|
422
|
+
? `<span class="pfx-micro-bullet__compact-value">${escapeHtml(formatMicroNumber(currentValue, options.locale))}${escapeHtml(visualization.valueSuffix ?? '')}</span>`
|
|
423
|
+
: '';
|
|
424
|
+
return `
|
|
425
|
+
<span class="pfx-micro-viz pfx-micro-viz__bullet" data-tone="${escapeHtml(visualization.tone ?? 'info')}" role="img" aria-label="${escapeHtml(visualization.ariaLabel ?? visualization.fallbackText)}">
|
|
426
|
+
${topHtml}
|
|
427
|
+
<span class="pfx-micro-bullet__track">
|
|
428
|
+
${bandsHtml}
|
|
429
|
+
<span class="pfx-micro-bullet__actual" data-tone="${escapeHtml(visualization.tone ?? 'info')}" style="width: ${percent(currentValue, max)}%;"></span>
|
|
430
|
+
${targetLeft === null ? '' : `<span class="pfx-micro-bullet__target" style="left: ${targetLeft}%;"></span>`}
|
|
431
|
+
</span>
|
|
432
|
+
${bottomHtml}
|
|
433
|
+
${compactValueHtml}
|
|
434
|
+
</span>`;
|
|
435
|
+
}
|
|
436
|
+
function renderDeltaVisualizationHtml(visualization, options) {
|
|
437
|
+
const value = toFiniteNumber(visualization.value);
|
|
438
|
+
const direction = value === undefined ? 'neutral' : value < 0 ? 'down' : value > 0 ? 'up' : 'neutral';
|
|
439
|
+
const symbol = direction === 'up' ? '▲' : direction === 'down' ? '▼' : '→';
|
|
440
|
+
const tone = visualization.tone ??
|
|
441
|
+
(value === undefined
|
|
442
|
+
? 'neutral'
|
|
443
|
+
: value < 0
|
|
444
|
+
? 'danger'
|
|
445
|
+
: value > 0
|
|
446
|
+
? 'success'
|
|
447
|
+
: 'neutral');
|
|
448
|
+
const valueSuffix = visualization.valueSuffix ?? '';
|
|
449
|
+
const text = value === undefined
|
|
450
|
+
? visualization.fallbackText
|
|
451
|
+
: `${formatMicroNumber(Math.abs(value), options.locale)}${valueSuffix}`;
|
|
452
|
+
const directionLabel = direction === 'up'
|
|
453
|
+
? 'up'
|
|
454
|
+
: direction === 'down'
|
|
455
|
+
? 'down'
|
|
456
|
+
: 'stable';
|
|
457
|
+
const aria = visualization.ariaLabel ?? `${visualization.fallbackText}: ${directionLabel} ${text}`;
|
|
458
|
+
return `
|
|
459
|
+
<span class="pfx-micro-viz pfx-micro-viz__delta" data-tone="${escapeHtml(tone)}" data-direction="${escapeHtml(direction)}" role="img" aria-label="${escapeHtml(aria)}">
|
|
460
|
+
<span class="pfx-micro-viz__delta-symbol" aria-hidden="true">${symbol}</span>
|
|
461
|
+
<span>${escapeHtml(text)}</span>
|
|
462
|
+
</span>`;
|
|
463
|
+
}
|
|
464
|
+
function percent(value, total) {
|
|
465
|
+
if (!Number.isFinite(value) || !Number.isFinite(total) || total <= 0) {
|
|
466
|
+
return '0';
|
|
467
|
+
}
|
|
468
|
+
return Math.max(0, Math.min(100, (value / total) * 100)).toFixed(2);
|
|
469
|
+
}
|
|
470
|
+
function toFiniteNumber(value) {
|
|
471
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
472
|
+
}
|
|
473
|
+
function formatMicroNumber(value, locale) {
|
|
474
|
+
return new Intl.NumberFormat(locale || undefined, {
|
|
475
|
+
maximumFractionDigits: Math.abs(value) < 10 ? 2 : 0,
|
|
476
|
+
}).format(value);
|
|
477
|
+
}
|
|
478
|
+
function escapeHtml(value) {
|
|
479
|
+
return String(value ?? '')
|
|
480
|
+
.replace(/&/g, '&')
|
|
481
|
+
.replace(/</g, '<')
|
|
482
|
+
.replace(/>/g, '>')
|
|
483
|
+
.replace(/"/g, '"')
|
|
484
|
+
.replace(/'/g, ''');
|
|
485
|
+
}
|
|
486
|
+
function normalizePoints(value) {
|
|
487
|
+
const points = normalizeNumericEntries(value);
|
|
488
|
+
return points.length ? { points } : undefined;
|
|
489
|
+
}
|
|
490
|
+
function normalizeSegments(value) {
|
|
491
|
+
const segments = normalizeNumericEntries(value);
|
|
492
|
+
return segments.length ? { segments } : undefined;
|
|
493
|
+
}
|
|
494
|
+
function normalizeThresholds(value) {
|
|
495
|
+
if (!Array.isArray(value)) {
|
|
496
|
+
return undefined;
|
|
497
|
+
}
|
|
498
|
+
const thresholds = value
|
|
499
|
+
.map((item) => {
|
|
500
|
+
if (!item || typeof item !== 'object')
|
|
501
|
+
return undefined;
|
|
502
|
+
const numVal = toFiniteNumber(item.value);
|
|
503
|
+
const minVal = toFiniteNumber(item.min);
|
|
504
|
+
const maxVal = toFiniteNumber(item.max);
|
|
505
|
+
const tone = normalizeEnum$1(item.tone, VISUALIZATION_TONES);
|
|
506
|
+
const label = normalizeText$1(item.label);
|
|
507
|
+
const hasCondition = numVal !== undefined || minVal !== undefined || maxVal !== undefined || item.equals !== undefined;
|
|
508
|
+
if (!hasCondition) {
|
|
509
|
+
return undefined;
|
|
510
|
+
}
|
|
511
|
+
return {
|
|
512
|
+
...(label ? { label } : {}),
|
|
513
|
+
...(numVal !== undefined ? { value: numVal } : {}),
|
|
514
|
+
...(minVal !== undefined ? { min: minVal } : {}),
|
|
515
|
+
...(maxVal !== undefined ? { max: maxVal } : {}),
|
|
516
|
+
...(item.equals !== undefined ? { equals: item.equals } : {}),
|
|
517
|
+
...(tone ? { tone } : {}),
|
|
518
|
+
};
|
|
519
|
+
})
|
|
520
|
+
.filter((item) => !!item);
|
|
521
|
+
return thresholds.length ? { thresholds } : undefined;
|
|
522
|
+
}
|
|
523
|
+
function normalizeItems(value) {
|
|
524
|
+
if (!Array.isArray(value)) {
|
|
525
|
+
return undefined;
|
|
526
|
+
}
|
|
527
|
+
const items = value
|
|
528
|
+
.map((item) => normalizeVisualizationItem(item))
|
|
529
|
+
.filter((item) => !!item);
|
|
530
|
+
return items.length ? { items } : undefined;
|
|
531
|
+
}
|
|
532
|
+
function normalizeExpression(value, key) {
|
|
533
|
+
if (typeof value === 'string') {
|
|
534
|
+
const normalized = value.trim();
|
|
535
|
+
return normalized ? { [key]: normalized } : undefined;
|
|
536
|
+
}
|
|
537
|
+
if (value && typeof value === 'object') {
|
|
538
|
+
return { [key]: value };
|
|
539
|
+
}
|
|
540
|
+
return undefined;
|
|
541
|
+
}
|
|
542
|
+
function normalizeNumericEntries(value) {
|
|
543
|
+
if (!Array.isArray(value)) {
|
|
544
|
+
return [];
|
|
545
|
+
}
|
|
546
|
+
return value
|
|
547
|
+
.map((item) => {
|
|
548
|
+
if (!item || typeof item !== 'object' || !Number.isFinite(item.value)) {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
const label = normalizeText$1(item.label);
|
|
552
|
+
const tone = normalizeEnum$1(item.tone, VISUALIZATION_TONES);
|
|
553
|
+
return {
|
|
554
|
+
value: item.value,
|
|
555
|
+
...(label ? { label } : {}),
|
|
556
|
+
...(tone ? { tone } : {}),
|
|
557
|
+
};
|
|
558
|
+
})
|
|
559
|
+
.filter((item) => !!item);
|
|
560
|
+
}
|
|
561
|
+
function normalizeVisualizationItem(item) {
|
|
562
|
+
if (!item || typeof item !== 'object') {
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
const id = normalizeText$1(item.id);
|
|
566
|
+
const label = normalizeText$1(item.label);
|
|
567
|
+
const icon = normalizeText$1(item.icon);
|
|
568
|
+
const state = normalizeText$1(item.state);
|
|
569
|
+
const tone = normalizeEnum$1(item.tone, VISUALIZATION_TONES);
|
|
570
|
+
const normalizedValue = Number.isFinite(item.value) ? item.value : undefined;
|
|
571
|
+
if (!id && !label && normalizedValue === undefined && !icon && !state && !tone) {
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
...(id ? { id } : {}),
|
|
576
|
+
...(label ? { label } : {}),
|
|
577
|
+
...(normalizedValue !== undefined ? { value: normalizedValue } : {}),
|
|
578
|
+
...(icon ? { icon } : {}),
|
|
579
|
+
...(state ? { state } : {}),
|
|
580
|
+
...(tone ? { tone } : {}),
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function normalizeNumber(value, key) {
|
|
584
|
+
return Number.isFinite(value) ? { [key]: value } : undefined;
|
|
585
|
+
}
|
|
586
|
+
function normalizeEnum$1(value, allowed) {
|
|
587
|
+
if (typeof value !== 'string') {
|
|
588
|
+
return undefined;
|
|
589
|
+
}
|
|
590
|
+
const normalized = value.trim();
|
|
591
|
+
return allowed.has(normalized) ? normalized : undefined;
|
|
592
|
+
}
|
|
593
|
+
function normalizeText$1(value) {
|
|
594
|
+
if (typeof value !== 'string') {
|
|
595
|
+
return undefined;
|
|
596
|
+
}
|
|
597
|
+
const trimmed = value.trim();
|
|
598
|
+
return trimmed.length ? trimmed : undefined;
|
|
599
|
+
}
|
|
600
|
+
function renderRadialVisualizationHtml(visualization, options) {
|
|
601
|
+
const value = toFiniteNumber(visualization.value);
|
|
602
|
+
if (value === undefined) {
|
|
603
|
+
return renderFallbackVisualizationHtml(visualization, visualization.tone ?? 'neutral');
|
|
604
|
+
}
|
|
605
|
+
const total = visualization.total && visualization.total > 0 ? visualization.total : 100;
|
|
606
|
+
const pct = Math.max(0, Math.min(100, (value / total) * 100));
|
|
607
|
+
const tone = visualization.tone ?? 'info';
|
|
608
|
+
const dashArray = `${pct.toFixed(1)}, 100`;
|
|
609
|
+
const pctText = `${Math.round(pct)}%`;
|
|
610
|
+
const aria = visualization.ariaLabel ?? `${visualization.fallbackText} (${pctText})`;
|
|
611
|
+
const isTable = visualization.surface === 'table-cell';
|
|
612
|
+
const svgText = isTable
|
|
613
|
+
? ''
|
|
614
|
+
: `<text x="18" y="21" class="pfx-micro-radial-svg__text">${escapeHtml(pctText)}</text>`;
|
|
615
|
+
const externalText = isTable
|
|
616
|
+
? `<span class="pfx-micro-radial-value">${escapeHtml(pctText)}</span>`
|
|
617
|
+
: '';
|
|
618
|
+
return `
|
|
619
|
+
<span class="pfx-micro-viz pfx-micro-viz__radial" data-tone="${escapeHtml(tone)}" role="img" aria-label="${escapeHtml(aria)}">
|
|
620
|
+
<svg class="pfx-micro-radial-svg" viewBox="0 0 36 36">
|
|
621
|
+
<circle class="pfx-micro-radial-svg__track" cx="18" cy="18" r="15.9155" />
|
|
622
|
+
<circle class="pfx-micro-radial-svg__bar" cx="18" cy="18" r="15.9155" stroke-dasharray="${dashArray}" transform="rotate(-90 18 18)" />
|
|
623
|
+
${svgText}
|
|
624
|
+
</svg>
|
|
625
|
+
${externalText}
|
|
626
|
+
</span>`;
|
|
627
|
+
}
|
|
628
|
+
function renderHarveyBallVisualizationHtml(visualization, options) {
|
|
629
|
+
const value = toFiniteNumber(visualization.value);
|
|
630
|
+
if (value === undefined) {
|
|
631
|
+
return renderFallbackVisualizationHtml(visualization, visualization.tone ?? 'neutral');
|
|
632
|
+
}
|
|
633
|
+
const total = visualization.total && visualization.total > 0 ? visualization.total : 100;
|
|
634
|
+
const pct = Math.max(0, Math.min(100, (value / total) * 100));
|
|
635
|
+
const tone = visualization.tone ?? 'info';
|
|
636
|
+
const strokeLength = pct * 0.50265;
|
|
637
|
+
const dashArray = `${strokeLength.toFixed(3)} 50.265`;
|
|
638
|
+
const aria = visualization.ariaLabel ?? `${visualization.fallbackText} (${Math.round(pct)}%)`;
|
|
639
|
+
return `
|
|
640
|
+
<span class="pfx-micro-viz pfx-micro-viz__harvey" data-tone="${escapeHtml(tone)}" role="img" aria-label="${escapeHtml(aria)}">
|
|
641
|
+
<svg class="pfx-micro-harvey-svg" viewBox="0 0 36 36">
|
|
642
|
+
<circle class="pfx-micro-harvey-svg__track" cx="18" cy="18" r="16" />
|
|
643
|
+
<circle class="pfx-micro-harvey-svg__bar" cx="18" cy="18" r="8" stroke-dasharray="${dashArray}" transform="rotate(-90 18 18)" />
|
|
644
|
+
</svg>
|
|
645
|
+
<span class="pfx-micro-harvey-fraction">
|
|
646
|
+
<strong>${escapeHtml(formatMicroNumber(value, options.locale))}</strong>
|
|
647
|
+
<span class="pfx-separator">/</span>
|
|
648
|
+
<span class="pfx-total">${escapeHtml(formatMicroNumber(total, options.locale))}</span>
|
|
649
|
+
</span>
|
|
650
|
+
</span>`;
|
|
651
|
+
}
|
|
652
|
+
function getPointsCoordinates(points, width, height, padding) {
|
|
653
|
+
if (points.length < 2)
|
|
654
|
+
return [];
|
|
655
|
+
const values = points.map(p => p.value);
|
|
656
|
+
const minVal = Math.min(...values);
|
|
657
|
+
const maxVal = Math.max(...values);
|
|
658
|
+
const valRange = maxVal - minVal === 0 ? 1 : maxVal - minVal;
|
|
659
|
+
return points.map((p, index) => {
|
|
660
|
+
const x = padding + (index / (points.length - 1)) * (width - 2 * padding);
|
|
661
|
+
const y = height - padding - ((p.value - minVal) / valRange) * (height - 2 * padding);
|
|
662
|
+
return { x, y };
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
function renderLineVisualizationHtml(visualization, options) {
|
|
666
|
+
const points = visualization.points ?? [];
|
|
667
|
+
const tone = visualization.tone ?? 'info';
|
|
668
|
+
if (points.length < 2) {
|
|
669
|
+
return renderFallbackVisualizationHtml(visualization, tone);
|
|
670
|
+
}
|
|
671
|
+
const width = 100;
|
|
672
|
+
const height = 30;
|
|
673
|
+
const padding = 4;
|
|
674
|
+
const coords = getPointsCoordinates(points, width, height, padding);
|
|
675
|
+
const pathD = coords.map((c, i) => `${i === 0 ? 'M' : 'L'} ${c.x.toFixed(1)} ${c.y.toFixed(1)}`).join(' ');
|
|
676
|
+
// Highlight extreme/boundary values
|
|
677
|
+
let minIdx = 0;
|
|
678
|
+
let maxIdx = 0;
|
|
679
|
+
for (let i = 1; i < points.length; i++) {
|
|
680
|
+
if (points[i].value < points[minIdx].value)
|
|
681
|
+
minIdx = i;
|
|
682
|
+
if (points[i].value > points[maxIdx].value)
|
|
683
|
+
maxIdx = i;
|
|
684
|
+
}
|
|
685
|
+
const markers = [
|
|
686
|
+
{ x: coords[0].x, y: coords[0].y, tone: points[0].tone ?? tone },
|
|
687
|
+
{ x: coords[coords.length - 1].x, y: coords[coords.length - 1].y, tone: points[points.length - 1].tone ?? tone }
|
|
688
|
+
];
|
|
689
|
+
if (minIdx !== 0 && minIdx !== coords.length - 1) {
|
|
690
|
+
markers.push({ x: coords[minIdx].x, y: coords[minIdx].y, tone: points[minIdx].tone ?? 'danger' });
|
|
691
|
+
}
|
|
692
|
+
if (maxIdx !== 0 && maxIdx !== coords.length - 1) {
|
|
693
|
+
markers.push({ x: coords[maxIdx].x, y: coords[maxIdx].y, tone: points[maxIdx].tone ?? 'success' });
|
|
694
|
+
}
|
|
695
|
+
const circlesHtml = markers.map(c => `<circle cx="${c.x.toFixed(1)}" cy="${c.y.toFixed(1)}" r="2.5" class="pfx-micro-trend__marker" data-tone="${escapeHtml(c.tone)}" />`).join('');
|
|
696
|
+
const isTable = visualization.surface === 'table-cell';
|
|
697
|
+
const firstPoint = points[0];
|
|
698
|
+
const lastPoint = points[points.length - 1];
|
|
699
|
+
const leftHtml = isTable ? '' : `
|
|
700
|
+
<div class="pfx-micro-trend__label-col pfx-micro-trend__label-col--start">
|
|
701
|
+
<span class="pfx-micro-trend__value">${escapeHtml(formatMicroNumber(firstPoint.value, options.locale))}</span>
|
|
702
|
+
${firstPoint.label ? `<span class="pfx-micro-trend__label">${escapeHtml(firstPoint.label)}</span>` : ''}
|
|
703
|
+
</div>`;
|
|
704
|
+
const rightHtml = isTable ? '' : `
|
|
705
|
+
<div class="pfx-micro-trend__label-col pfx-micro-trend__label-col--end">
|
|
706
|
+
<span class="pfx-micro-trend__value">${escapeHtml(formatMicroNumber(lastPoint.value, options.locale))}</span>
|
|
707
|
+
${lastPoint.label ? `<span class="pfx-micro-trend__label">${escapeHtml(lastPoint.label)}</span>` : ''}
|
|
708
|
+
</div>`;
|
|
709
|
+
return `
|
|
710
|
+
<span class="pfx-micro-viz-container">
|
|
711
|
+
${leftHtml}
|
|
712
|
+
<span class="pfx-micro-viz pfx-micro-viz__line" data-tone="${escapeHtml(tone)}" role="img" aria-label="${escapeHtml(visualization.ariaLabel ?? visualization.fallbackText)}">
|
|
713
|
+
<svg class="pfx-micro-line__svg" viewBox="0 0 100 30">
|
|
714
|
+
<path class="pfx-micro-line__path" d="${pathD}" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
|
715
|
+
${circlesHtml}
|
|
716
|
+
</svg>
|
|
717
|
+
</span>
|
|
718
|
+
${rightHtml}
|
|
719
|
+
</span>`;
|
|
720
|
+
}
|
|
721
|
+
function renderAreaVisualizationHtml(visualization, options) {
|
|
722
|
+
const points = visualization.points ?? [];
|
|
723
|
+
const tone = visualization.tone ?? 'info';
|
|
724
|
+
if (points.length < 2) {
|
|
725
|
+
return renderFallbackVisualizationHtml(visualization, tone);
|
|
726
|
+
}
|
|
727
|
+
const width = 100;
|
|
728
|
+
const height = 30;
|
|
729
|
+
const padding = 4;
|
|
730
|
+
const coords = getPointsCoordinates(points, width, height, padding);
|
|
731
|
+
const lineD = coords.map((c, i) => `${i === 0 ? 'M' : 'L'} ${c.x.toFixed(1)} ${c.y.toFixed(1)}`).join(' ');
|
|
732
|
+
const firstX = coords[0].x.toFixed(1);
|
|
733
|
+
const lastX = coords[coords.length - 1].x.toFixed(1);
|
|
734
|
+
const areaD = `${lineD} L ${lastX} ${height} L ${firstX} ${height} Z`;
|
|
735
|
+
// Highlight extreme/boundary values
|
|
736
|
+
let minIdx = 0;
|
|
737
|
+
let maxIdx = 0;
|
|
738
|
+
for (let i = 1; i < points.length; i++) {
|
|
739
|
+
if (points[i].value < points[minIdx].value)
|
|
740
|
+
minIdx = i;
|
|
741
|
+
if (points[i].value > points[maxIdx].value)
|
|
742
|
+
maxIdx = i;
|
|
743
|
+
}
|
|
744
|
+
const markers = [
|
|
745
|
+
{ x: coords[0].x, y: coords[0].y, tone: points[0].tone ?? tone },
|
|
746
|
+
{ x: coords[coords.length - 1].x, y: coords[coords.length - 1].y, tone: points[points.length - 1].tone ?? tone }
|
|
747
|
+
];
|
|
748
|
+
if (minIdx !== 0 && minIdx !== coords.length - 1) {
|
|
749
|
+
markers.push({ x: coords[minIdx].x, y: coords[minIdx].y, tone: points[minIdx].tone ?? 'danger' });
|
|
750
|
+
}
|
|
751
|
+
if (maxIdx !== 0 && maxIdx !== coords.length - 1) {
|
|
752
|
+
markers.push({ x: coords[maxIdx].x, y: coords[maxIdx].y, tone: points[maxIdx].tone ?? 'success' });
|
|
753
|
+
}
|
|
754
|
+
const circlesHtml = markers.map(c => `<circle cx="${c.x.toFixed(1)}" cy="${c.y.toFixed(1)}" r="2.5" class="pfx-micro-trend__marker" data-tone="${escapeHtml(c.tone)}" />`).join('');
|
|
755
|
+
const isTable = visualization.surface === 'table-cell';
|
|
756
|
+
const firstPoint = points[0];
|
|
757
|
+
const lastPoint = points[points.length - 1];
|
|
758
|
+
const leftHtml = isTable ? '' : `
|
|
759
|
+
<div class="pfx-micro-trend__label-col pfx-micro-trend__label-col--start">
|
|
760
|
+
<span class="pfx-micro-trend__value">${escapeHtml(formatMicroNumber(firstPoint.value, options.locale))}</span>
|
|
761
|
+
${firstPoint.label ? `<span class="pfx-micro-trend__label">${escapeHtml(firstPoint.label)}</span>` : ''}
|
|
762
|
+
</div>`;
|
|
763
|
+
const rightHtml = isTable ? '' : `
|
|
764
|
+
<div class="pfx-micro-trend__label-col pfx-micro-trend__label-col--end">
|
|
765
|
+
<span class="pfx-micro-trend__value">${escapeHtml(formatMicroNumber(lastPoint.value, options.locale))}</span>
|
|
766
|
+
${lastPoint.label ? `<span class="pfx-micro-trend__label">${escapeHtml(lastPoint.label)}</span>` : ''}
|
|
767
|
+
</div>`;
|
|
768
|
+
return `
|
|
769
|
+
<span class="pfx-micro-viz-container">
|
|
770
|
+
${leftHtml}
|
|
771
|
+
<span class="pfx-micro-viz pfx-micro-viz__area" data-tone="${escapeHtml(tone)}" role="img" aria-label="${escapeHtml(visualization.ariaLabel ?? visualization.fallbackText)}">
|
|
772
|
+
<svg class="pfx-micro-area__svg" viewBox="0 0 100 30">
|
|
773
|
+
<path class="pfx-micro-area__fill" d="${areaD}" fill="currentColor" fill-opacity="0.15" />
|
|
774
|
+
<path class="pfx-micro-area__path" d="${lineD}" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
|
775
|
+
${circlesHtml}
|
|
776
|
+
</svg>
|
|
777
|
+
</span>
|
|
778
|
+
${rightHtml}
|
|
779
|
+
</span>`;
|
|
780
|
+
}
|
|
781
|
+
function renderColumnVisualizationHtml(visualization, options) {
|
|
782
|
+
const points = visualization.points ?? [];
|
|
783
|
+
const tone = visualization.tone ?? 'info';
|
|
784
|
+
if (points.length === 0) {
|
|
785
|
+
return renderFallbackVisualizationHtml(visualization, tone);
|
|
786
|
+
}
|
|
787
|
+
const width = 100;
|
|
788
|
+
const height = 30;
|
|
789
|
+
const padding = 4;
|
|
790
|
+
const values = points.map(p => p.value);
|
|
791
|
+
const maxVal = Math.max(...values, 0);
|
|
792
|
+
const minVal = Math.min(...values, 0);
|
|
793
|
+
const valRange = maxVal - minVal === 0 ? 1 : maxVal - minVal;
|
|
794
|
+
const colWidth = Math.max(2, (width - 2 * padding) / points.length - 2);
|
|
795
|
+
const columnsHtml = points.map((p, i) => {
|
|
796
|
+
const x = padding + i * ((width - 2 * padding) / points.length) + 1;
|
|
797
|
+
const rectHeight = Math.max(2, ((p.value - minVal) / valRange) * (height - 2 * padding));
|
|
798
|
+
const rectY = height - padding - rectHeight;
|
|
799
|
+
const itemTone = p.tone ??
|
|
800
|
+
(visualization.thresholds?.length
|
|
801
|
+
? resolvePraxisMicroVisualizationTone(p.value, visualization.thresholds)?.tone ?? tone
|
|
802
|
+
: tone);
|
|
803
|
+
return `<rect class="pfx-micro-column__bar" x="${x.toFixed(1)}" y="${rectY.toFixed(1)}" width="${colWidth.toFixed(1)}" height="${rectHeight.toFixed(1)}" fill="currentColor" data-tone="${escapeHtml(itemTone)}" />`;
|
|
804
|
+
}).join('');
|
|
805
|
+
const isTable = visualization.surface === 'table-cell';
|
|
806
|
+
const firstPoint = points[0];
|
|
807
|
+
const lastPoint = points[points.length - 1];
|
|
808
|
+
const leftHtml = isTable ? '' : `
|
|
809
|
+
<div class="pfx-micro-trend__label-col pfx-micro-trend__label-col--start">
|
|
810
|
+
<span class="pfx-micro-trend__value">${escapeHtml(formatMicroNumber(firstPoint.value, options.locale))}</span>
|
|
811
|
+
${firstPoint.label ? `<span class="pfx-micro-trend__label">${escapeHtml(firstPoint.label)}</span>` : ''}
|
|
812
|
+
</div>`;
|
|
813
|
+
const rightHtml = isTable ? '' : `
|
|
814
|
+
<div class="pfx-micro-trend__label-col pfx-micro-trend__label-col--end">
|
|
815
|
+
<span class="pfx-micro-trend__value">${escapeHtml(formatMicroNumber(lastPoint.value, options.locale))}</span>
|
|
816
|
+
${lastPoint.label ? `<span class="pfx-micro-trend__label">${escapeHtml(lastPoint.label)}</span>` : ''}
|
|
817
|
+
</div>`;
|
|
818
|
+
return `
|
|
819
|
+
<span class="pfx-micro-viz-container">
|
|
820
|
+
${leftHtml}
|
|
821
|
+
<span class="pfx-micro-viz pfx-micro-viz__column" data-tone="${escapeHtml(tone)}" role="img" aria-label="${escapeHtml(visualization.ariaLabel ?? visualization.fallbackText)}">
|
|
822
|
+
<svg class="pfx-micro-column__svg" viewBox="0 0 100 30">
|
|
823
|
+
${columnsHtml}
|
|
824
|
+
</svg>
|
|
825
|
+
</span>
|
|
826
|
+
${rightHtml}
|
|
827
|
+
</span>`;
|
|
828
|
+
}
|
|
829
|
+
function renderProcessFlowVisualizationHtml(visualization, options) {
|
|
830
|
+
const items = visualization.items ?? [];
|
|
831
|
+
if (items.length === 0) {
|
|
832
|
+
return renderFallbackVisualizationHtml(visualization, visualization.tone ?? 'neutral');
|
|
833
|
+
}
|
|
834
|
+
const isTable = visualization.surface === 'table-cell';
|
|
835
|
+
const stepLabels = items.map((item, index) => item.label ?? item.id ?? `${index + 1}`);
|
|
836
|
+
const aria = visualization.ariaLabel ?? `${visualization.fallbackText}: ${stepLabels.join(' > ')}`;
|
|
837
|
+
const labelStyleAttr = [];
|
|
838
|
+
if (visualization.stepLabelWidth !== undefined) {
|
|
839
|
+
const widthVal = typeof visualization.stepLabelWidth === 'number'
|
|
840
|
+
? `${visualization.stepLabelWidth}px`
|
|
841
|
+
: String(visualization.stepLabelWidth);
|
|
842
|
+
labelStyleAttr.push(`inline-size: ${escapeHtml(widthVal)}; max-width: ${escapeHtml(widthVal)};`);
|
|
843
|
+
}
|
|
844
|
+
if (visualization.stepLabelLineClamp !== undefined) {
|
|
845
|
+
labelStyleAttr.push(`-webkit-line-clamp: ${visualization.stepLabelLineClamp};`);
|
|
846
|
+
}
|
|
847
|
+
const labelInlineStyle = labelStyleAttr.length ? ` style="${labelStyleAttr.join(' ')}"` : '';
|
|
848
|
+
const stepsHtml = items.map((item, index) => {
|
|
849
|
+
const itemTone = item.tone ?? 'neutral';
|
|
850
|
+
const itemState = item.state ?? '';
|
|
851
|
+
const isLast = index === items.length - 1;
|
|
852
|
+
const itemLabel = stepLabels[index];
|
|
853
|
+
const label = !isTable && item.label ? `<span class="pfx-micro-step__label"${labelInlineStyle}>${escapeHtml(item.label)}</span>` : '';
|
|
854
|
+
const connector = !isLast ? `<span class="pfx-micro-step__connector" data-tone="${escapeHtml(itemTone)}"></span>` : '';
|
|
855
|
+
const stepCircle = `
|
|
856
|
+
<span class="pfx-micro-step__node" data-tone="${escapeHtml(itemTone)}" aria-hidden="true">
|
|
857
|
+
${item.icon ? `<span class="material-icons pfx-micro-step__icon" aria-hidden="true">${escapeHtml(item.icon)}</span>` : `${index + 1}`}
|
|
858
|
+
</span>`;
|
|
859
|
+
return `
|
|
860
|
+
<div class="pfx-micro-step" data-state="${escapeHtml(itemState)}" data-tone="${escapeHtml(itemTone)}">
|
|
861
|
+
<div class="pfx-micro-step__node-wrapper" title="${escapeHtml(itemLabel)}">
|
|
862
|
+
${stepCircle}
|
|
863
|
+
${label}
|
|
864
|
+
</div>
|
|
865
|
+
${connector}
|
|
866
|
+
</div>`;
|
|
867
|
+
}).join('');
|
|
868
|
+
return `
|
|
869
|
+
<span class="pfx-micro-viz pfx-micro-viz__process" role="img" aria-label="${escapeHtml(aria)}">
|
|
870
|
+
${stepsHtml}
|
|
871
|
+
</span>`;
|
|
872
|
+
}
|
|
873
|
+
function renderFallbackVisualizationHtml(visualization, tone) {
|
|
874
|
+
return `
|
|
875
|
+
<span
|
|
876
|
+
class="pfx-micro-viz pfx-micro-viz__fallback"
|
|
877
|
+
data-tone="${escapeHtml(tone)}"
|
|
878
|
+
role="img"
|
|
879
|
+
aria-label="${escapeHtml(visualization.ariaLabel ?? visualization.fallbackText)}"
|
|
880
|
+
>${escapeHtml(visualization.fallbackText)}</span>`;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const TONES = new Set([
|
|
884
|
+
'neutral',
|
|
885
|
+
'info',
|
|
886
|
+
'success',
|
|
887
|
+
'warning',
|
|
888
|
+
'danger',
|
|
889
|
+
]);
|
|
890
|
+
const APPEARANCES = new Set([
|
|
891
|
+
'plain',
|
|
892
|
+
'soft',
|
|
893
|
+
'outlined',
|
|
894
|
+
'filled',
|
|
895
|
+
]);
|
|
896
|
+
const PRESENTERS = new Set([
|
|
897
|
+
'text',
|
|
898
|
+
'badge',
|
|
899
|
+
'chip',
|
|
900
|
+
'status',
|
|
901
|
+
'iconValue',
|
|
902
|
+
'progress',
|
|
903
|
+
'rating',
|
|
904
|
+
'microVisualization',
|
|
905
|
+
]);
|
|
906
|
+
/**
|
|
907
|
+
* Resolves readonly field presentation from a base semantic config plus ordered
|
|
908
|
+
* Json Logic rules. Later matching rules override earlier presentation values.
|
|
909
|
+
*/
|
|
910
|
+
function resolveFieldPresentation(base, rules, context = {}, options = {}) {
|
|
911
|
+
let resolved = normalizeFieldPresentation(base);
|
|
912
|
+
let matchedRule = false;
|
|
913
|
+
if (!Array.isArray(rules) || !options.jsonLogic) {
|
|
914
|
+
return resolved;
|
|
915
|
+
}
|
|
916
|
+
for (const rule of rules) {
|
|
917
|
+
if (!rule?.when || !matchesPresentationRule(rule, context, options.jsonLogic)) {
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
const effect = normalizeFieldPresentation(rule.set ?? rule.effect);
|
|
921
|
+
resolved = {
|
|
922
|
+
...resolved,
|
|
923
|
+
...effect,
|
|
924
|
+
};
|
|
925
|
+
matchedRule = true;
|
|
926
|
+
}
|
|
927
|
+
return matchedRule ? { ...resolved, matchedRule } : resolved;
|
|
928
|
+
}
|
|
929
|
+
function normalizeFieldPresentation(value) {
|
|
930
|
+
if (!value || typeof value !== 'object') {
|
|
931
|
+
return {};
|
|
932
|
+
}
|
|
933
|
+
const presenter = normalizePresenter(value.presenter ?? value.variant);
|
|
934
|
+
const tone = normalizeTone(value.tone);
|
|
935
|
+
const appearance = normalizeAppearance(value.appearance);
|
|
936
|
+
const icon = normalizeText(value.icon);
|
|
937
|
+
const label = normalizeText(value.label);
|
|
938
|
+
const badge = normalizeText(value.badge);
|
|
939
|
+
const tooltip = normalizeText(value.tooltip);
|
|
940
|
+
const prefix = normalizeText(value.prefix);
|
|
941
|
+
const suffix = normalizeText(value.suffix);
|
|
942
|
+
const iconTrue = normalizeText(value.iconTrue);
|
|
943
|
+
const iconFalse = normalizeText(value.iconFalse);
|
|
944
|
+
const toneTrue = normalizeTone(value.toneTrue);
|
|
945
|
+
const toneFalse = normalizeTone(value.toneFalse);
|
|
946
|
+
const labelTrue = normalizeText(value.labelTrue);
|
|
947
|
+
const labelFalse = normalizeText(value.labelFalse);
|
|
948
|
+
const visualization = normalizePraxisPresentationVisualization(value.visualization);
|
|
949
|
+
return {
|
|
950
|
+
...(presenter ? { presenter } : {}),
|
|
951
|
+
...(tone ? { tone } : {}),
|
|
952
|
+
...(appearance ? { appearance } : {}),
|
|
953
|
+
...(icon ? { icon } : {}),
|
|
954
|
+
...(label ? { label } : {}),
|
|
955
|
+
...(badge ? { badge } : {}),
|
|
956
|
+
...(tooltip ? { tooltip } : {}),
|
|
957
|
+
...(prefix ? { prefix } : {}),
|
|
958
|
+
...(suffix ? { suffix } : {}),
|
|
959
|
+
...(iconTrue ? { iconTrue } : {}),
|
|
960
|
+
...(iconFalse ? { iconFalse } : {}),
|
|
961
|
+
...(toneTrue ? { toneTrue } : {}),
|
|
962
|
+
...(toneFalse ? { toneFalse } : {}),
|
|
963
|
+
...(labelTrue ? { labelTrue } : {}),
|
|
964
|
+
...(labelFalse ? { labelFalse } : {}),
|
|
965
|
+
...(value.valuePresentation && typeof value.valuePresentation === 'object'
|
|
966
|
+
? { valuePresentation: value.valuePresentation }
|
|
967
|
+
: {}),
|
|
968
|
+
...(visualization ? { visualization } : {}),
|
|
969
|
+
...(value.interactions && typeof value.interactions === 'object'
|
|
970
|
+
? { interactions: normalizeInteractions(value.interactions) }
|
|
971
|
+
: {}),
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
function matchesPresentationRule(rule, context, jsonLogic) {
|
|
975
|
+
try {
|
|
976
|
+
const result = jsonLogic.evaluate(rule.when, context);
|
|
977
|
+
return typeof jsonLogic.truthy === 'function'
|
|
978
|
+
? jsonLogic.truthy(result)
|
|
979
|
+
: Boolean(result);
|
|
980
|
+
}
|
|
981
|
+
catch {
|
|
982
|
+
return false;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
function normalizePresenter(value) {
|
|
986
|
+
return normalizeEnum(value, PRESENTERS);
|
|
987
|
+
}
|
|
988
|
+
function normalizeTone(value) {
|
|
989
|
+
return normalizeEnum(value, TONES);
|
|
990
|
+
}
|
|
991
|
+
function normalizeAppearance(value) {
|
|
992
|
+
return normalizeEnum(value, APPEARANCES);
|
|
993
|
+
}
|
|
994
|
+
function normalizeEnum(value, allowed) {
|
|
995
|
+
if (typeof value !== 'string') {
|
|
996
|
+
return undefined;
|
|
997
|
+
}
|
|
998
|
+
const normalized = value.trim();
|
|
999
|
+
return allowed.has(normalized) ? normalized : undefined;
|
|
1000
|
+
}
|
|
1001
|
+
function normalizeText(value) {
|
|
1002
|
+
if (typeof value !== 'string') {
|
|
1003
|
+
return undefined;
|
|
1004
|
+
}
|
|
1005
|
+
const trimmed = value.trim();
|
|
1006
|
+
return trimmed.length ? trimmed : undefined;
|
|
1007
|
+
}
|
|
1008
|
+
function normalizeInteractions(value) {
|
|
1009
|
+
return {
|
|
1010
|
+
...(value.copy === true ? { copy: true } : {}),
|
|
1011
|
+
...(value.details === true ? { details: true } : {}),
|
|
1012
|
+
...(value.expand === true ? { expand: true } : {}),
|
|
1013
|
+
...(value.link === true ? { link: true } : {}),
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const VALUE_PRESENTATION_TYPES = new Set([
|
|
1018
|
+
'string',
|
|
1019
|
+
'number',
|
|
1020
|
+
'currency',
|
|
1021
|
+
'percentage',
|
|
1022
|
+
'date',
|
|
1023
|
+
'datetime',
|
|
1024
|
+
'time',
|
|
1025
|
+
'boolean',
|
|
1026
|
+
]);
|
|
1027
|
+
const VALUE_PRESENTATION_STYLES = new Set([
|
|
1028
|
+
'default',
|
|
1029
|
+
'short',
|
|
1030
|
+
'medium',
|
|
1031
|
+
'long',
|
|
1032
|
+
'full',
|
|
1033
|
+
'compact',
|
|
1034
|
+
]);
|
|
1035
|
+
const OPTION_SOURCE_TYPES = new Set([
|
|
1036
|
+
'RESOURCE_ENTITY',
|
|
1037
|
+
'DISTINCT_DIMENSION',
|
|
1038
|
+
'CATEGORICAL_BUCKET',
|
|
1039
|
+
'LIGHT_LOOKUP',
|
|
1040
|
+
'STATIC_CANONICAL',
|
|
1041
|
+
]);
|
|
1042
|
+
const OPTION_SOURCE_SEARCH_MODES = new Set([
|
|
1043
|
+
'none',
|
|
1044
|
+
'starts-with',
|
|
1045
|
+
'contains',
|
|
1046
|
+
'exact',
|
|
1047
|
+
]);
|
|
1048
|
+
const OPTION_SOURCE_SELECTED_RELOAD_POLICIES = new Set([
|
|
1049
|
+
'required',
|
|
1050
|
+
'supported',
|
|
1051
|
+
'unsupported-with-waiver',
|
|
1052
|
+
'not-applicable',
|
|
1053
|
+
]);
|
|
1054
|
+
const OPTION_SOURCE_INVALID_SORT_POLICIES = new Set([
|
|
1055
|
+
'reject',
|
|
1056
|
+
'ignore',
|
|
1057
|
+
'unsupported',
|
|
1058
|
+
]);
|
|
1059
|
+
const LOOKUP_SEARCH_STRATEGY_KINDS = new Set([
|
|
1060
|
+
'business-code',
|
|
1061
|
+
'descriptive-text',
|
|
1062
|
+
'normalized-document',
|
|
1063
|
+
]);
|
|
1064
|
+
const LOOKUP_OPEN_DETAIL_MODES = new Set([
|
|
1065
|
+
'samePage',
|
|
1066
|
+
'newTab',
|
|
1067
|
+
'drawer',
|
|
1068
|
+
'modal',
|
|
1069
|
+
'route',
|
|
1070
|
+
]);
|
|
1071
|
+
const LOOKUP_FILTER_FIELD_TYPES = new Set([
|
|
1072
|
+
'text',
|
|
1073
|
+
'enum',
|
|
1074
|
+
'date',
|
|
1075
|
+
'number',
|
|
1076
|
+
'reference',
|
|
1077
|
+
]);
|
|
1078
|
+
const LOOKUP_FILTER_OPERATORS = new Set([
|
|
1079
|
+
'contains',
|
|
1080
|
+
'startsWith',
|
|
1081
|
+
'equals',
|
|
1082
|
+
'in',
|
|
1083
|
+
'before',
|
|
1084
|
+
'after',
|
|
1085
|
+
'between',
|
|
1086
|
+
'gt',
|
|
1087
|
+
'gte',
|
|
1088
|
+
'lt',
|
|
1089
|
+
'lte',
|
|
1090
|
+
]);
|
|
1091
|
+
/**
|
|
1092
|
+
* SchemaNormalizationKernel (internal)
|
|
1093
|
+
* -----------------------
|
|
1094
|
+
* Cálculo compartilhado pelo serviço Angular, sem DI ou export público, que converte
|
|
1095
|
+
* um esquema bruto (OpenAPI + x-ui) em uma
|
|
1096
|
+
* lista tipada de FieldDefinition consumida pelo restante do sistema
|
|
1097
|
+
* (mapeadores de metadata, criador de FormGroup, carregador de campos dinâmicos, etc.).
|
|
1098
|
+
*
|
|
1099
|
+
* Entrada esperada
|
|
1100
|
+
* - Objeto JSON no formato OpenAPI/JSON Schema contendo:
|
|
1101
|
+
* - properties: { [name: string]: { type, title, description, ... , 'x-ui': { ... } } }
|
|
1102
|
+
* - Em cada propriedade, o objeto 'x-ui' define aspectos de UI/validação.
|
|
1103
|
+
*
|
|
1104
|
+
* Saída (FieldDefinition[])
|
|
1105
|
+
* - Tipagem forte de campos com funções reais (nunca strings) para regras condicionais e validadores customizados.
|
|
1106
|
+
* - Todas as propriedades de validação e comportamento consolidadas no mesmo objeto do campo.
|
|
1107
|
+
*
|
|
1108
|
+
* Segurança e observações
|
|
1109
|
+
* - Este serviço aceita funções em formato string dentro do x-ui (ex.: uniqueValidator, conditionalValidation),
|
|
1110
|
+
* e as converte via new Function. Isso exige backend confiável e controlado.
|
|
1111
|
+
* Em ambientes não confiáveis, evite funções como string ou implemente um sandbox/whitelist.
|
|
1112
|
+
* - Valores booleanos/numéricos são normalizados para tipos nativos.
|
|
1113
|
+
* - Arrays de opções são convertidos para pares { key, value } previsíveis.
|
|
1114
|
+
*
|
|
1115
|
+
* Exemplo mínimo de entrada
|
|
1116
|
+
* const schema = {
|
|
1117
|
+
* properties: {
|
|
1118
|
+
* email: {
|
|
1119
|
+
* type: 'string',
|
|
1120
|
+
* title: 'E-mail',
|
|
1121
|
+
* 'x-ui': { controlType: 'input', type: 'string', required: true, email: true }
|
|
1122
|
+
* }
|
|
1123
|
+
* }
|
|
1124
|
+
* };
|
|
1125
|
+
* const fields = normalizer.normalizeSchema(schema);
|
|
1126
|
+
* // fields: [{ name: 'email', label: 'E-mail', type: 'string', controlType: 'input', required: true, email: true }]
|
|
1127
|
+
*/
|
|
1128
|
+
class SchemaNormalizationKernel {
|
|
1129
|
+
clonePlain(value) {
|
|
1130
|
+
if (value === undefined || value === null) {
|
|
1131
|
+
return value;
|
|
1132
|
+
}
|
|
1133
|
+
if (typeof structuredClone === 'function') {
|
|
1134
|
+
try {
|
|
1135
|
+
return structuredClone(value);
|
|
1136
|
+
}
|
|
1137
|
+
catch {
|
|
1138
|
+
// Fall through to JSON clone for plain schema metadata.
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
return JSON.parse(JSON.stringify(value));
|
|
1142
|
+
}
|
|
1143
|
+
resolveSchemaRef(ref, rootSchema) {
|
|
1144
|
+
if (typeof ref !== 'string' || !ref.startsWith('#/')) {
|
|
1145
|
+
return undefined;
|
|
1146
|
+
}
|
|
1147
|
+
const segments = ref
|
|
1148
|
+
.slice(2)
|
|
1149
|
+
.split('/')
|
|
1150
|
+
.map((segment) => segment.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
1151
|
+
let cursor = rootSchema;
|
|
1152
|
+
for (const segment of segments) {
|
|
1153
|
+
if (!cursor || typeof cursor !== 'object') {
|
|
1154
|
+
return undefined;
|
|
1155
|
+
}
|
|
1156
|
+
cursor = cursor[segment];
|
|
1157
|
+
}
|
|
1158
|
+
return cursor && typeof cursor === 'object' ? cursor : undefined;
|
|
1159
|
+
}
|
|
1160
|
+
normalizeObjectSchema(schema, rootSchema) {
|
|
1161
|
+
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
|
|
1162
|
+
return [];
|
|
1163
|
+
}
|
|
1164
|
+
return this.normalizeSchemaWithRoot(schema, rootSchema);
|
|
1165
|
+
}
|
|
1166
|
+
normalizeArrayConfig(ui, prop, rootSchema) {
|
|
1167
|
+
const source = ui.array && typeof ui.array === 'object' && !Array.isArray(ui.array)
|
|
1168
|
+
? this.clonePlain(ui.array)
|
|
1169
|
+
: {};
|
|
1170
|
+
const arrayConfig = source;
|
|
1171
|
+
arrayConfig.itemType = 'object';
|
|
1172
|
+
arrayConfig.mode = 'cards';
|
|
1173
|
+
arrayConfig.deleteMode = 'removeFromPayload';
|
|
1174
|
+
delete arrayConfig['sortable'];
|
|
1175
|
+
delete arrayConfig['itemStateTracking'];
|
|
1176
|
+
if (arrayConfig.operations) {
|
|
1177
|
+
delete arrayConfig.operations['duplicate'];
|
|
1178
|
+
delete arrayConfig.operations['reorder'];
|
|
1179
|
+
}
|
|
1180
|
+
if (prop?.minItems !== undefined && arrayConfig.minItems === undefined) {
|
|
1181
|
+
arrayConfig.minItems = Number(prop.minItems);
|
|
1182
|
+
}
|
|
1183
|
+
if (prop?.maxItems !== undefined && arrayConfig.maxItems === undefined) {
|
|
1184
|
+
arrayConfig.maxItems = Number(prop.maxItems);
|
|
1185
|
+
}
|
|
1186
|
+
const itemSchemaRef = arrayConfig.itemSchemaRef ?? prop?.items?.$ref;
|
|
1187
|
+
if (itemSchemaRef !== undefined) {
|
|
1188
|
+
arrayConfig.itemSchemaRef = String(itemSchemaRef);
|
|
1189
|
+
}
|
|
1190
|
+
const explicitItemSchema = arrayConfig.itemSchema && typeof arrayConfig.itemSchema === 'object'
|
|
1191
|
+
? arrayConfig.itemSchema
|
|
1192
|
+
: undefined;
|
|
1193
|
+
if (explicitItemSchema && Array.isArray(explicitItemSchema.fields)) {
|
|
1194
|
+
const inlineFields = this.normalizeInlineItemSchemaFields(explicitItemSchema.fields, rootSchema);
|
|
1195
|
+
if (inlineFields.length) {
|
|
1196
|
+
arrayConfig.itemSchema = {
|
|
1197
|
+
...explicitItemSchema,
|
|
1198
|
+
fields: inlineFields,
|
|
1199
|
+
};
|
|
1200
|
+
return this.finalizeArrayConfig(arrayConfig, ui);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
const resolvedItemSchema = explicitItemSchema ??
|
|
1204
|
+
this.resolveSchemaRef(arrayConfig.itemSchemaRef, rootSchema) ??
|
|
1205
|
+
(prop?.items && typeof prop.items === 'object' ? prop.items : undefined);
|
|
1206
|
+
if (resolvedItemSchema && typeof resolvedItemSchema === 'object') {
|
|
1207
|
+
const fields = this.normalizeObjectSchema(resolvedItemSchema, rootSchema);
|
|
1208
|
+
if (fields.length) {
|
|
1209
|
+
arrayConfig.itemSchema = {
|
|
1210
|
+
...(explicitItemSchema ?? resolvedItemSchema),
|
|
1211
|
+
fields,
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
return this.finalizeArrayConfig(arrayConfig, ui);
|
|
1216
|
+
}
|
|
1217
|
+
finalizeArrayConfig(arrayConfig, ui) {
|
|
1218
|
+
const collectionValidation = arrayConfig.collectionValidation ??
|
|
1219
|
+
(ui.collectionValidation && typeof ui.collectionValidation === 'object'
|
|
1220
|
+
? this.clonePlain(ui.collectionValidation)
|
|
1221
|
+
: undefined);
|
|
1222
|
+
if (collectionValidation) {
|
|
1223
|
+
arrayConfig.collectionValidation = collectionValidation;
|
|
1224
|
+
}
|
|
1225
|
+
return Object.keys(arrayConfig).length ? arrayConfig : undefined;
|
|
1226
|
+
}
|
|
1227
|
+
normalizeInlineItemSchemaFields(fields, rootSchema) {
|
|
1228
|
+
return fields
|
|
1229
|
+
.map((field) => this.normalizeInlineItemSchemaField(field, rootSchema))
|
|
1230
|
+
.filter((field) => !!field);
|
|
1231
|
+
}
|
|
1232
|
+
normalizeInlineItemSchemaField(field, rootSchema) {
|
|
1233
|
+
if (!field || typeof field !== 'object' || Array.isArray(field)) {
|
|
1234
|
+
return undefined;
|
|
1235
|
+
}
|
|
1236
|
+
if (field.controlType || field.dataType || !field['x-ui']) {
|
|
1237
|
+
return this.clonePlain(field);
|
|
1238
|
+
}
|
|
1239
|
+
const name = typeof field.name === 'string' && field.name.trim() ? field.name : undefined;
|
|
1240
|
+
if (!name) {
|
|
1241
|
+
return undefined;
|
|
1242
|
+
}
|
|
1243
|
+
const { name: _name, ...schema } = field;
|
|
1244
|
+
const normalized = this.normalizeSchemaWithRoot({
|
|
1245
|
+
type: 'object',
|
|
1246
|
+
properties: {
|
|
1247
|
+
[name]: schema,
|
|
1248
|
+
},
|
|
1249
|
+
}, rootSchema);
|
|
1250
|
+
return normalized[0];
|
|
1251
|
+
}
|
|
1252
|
+
/** Convert value to boolean. Accepts boolean, string or number. */
|
|
1253
|
+
parseBoolean(value) {
|
|
1254
|
+
if (typeof value === 'boolean') {
|
|
1255
|
+
return value;
|
|
1256
|
+
}
|
|
1257
|
+
if (typeof value === 'string') {
|
|
1258
|
+
return value.toLowerCase() === 'true';
|
|
1259
|
+
}
|
|
1260
|
+
if (typeof value === 'number') {
|
|
1261
|
+
return value !== 0;
|
|
1262
|
+
}
|
|
1263
|
+
return false;
|
|
1264
|
+
}
|
|
1265
|
+
/** Ensure an array of strings from various inputs. */
|
|
1266
|
+
parseStringArray(value) {
|
|
1267
|
+
if (Array.isArray(value)) {
|
|
1268
|
+
return value.map((v) => String(v));
|
|
1269
|
+
}
|
|
1270
|
+
if (typeof value === 'string') {
|
|
1271
|
+
return [value];
|
|
1272
|
+
}
|
|
1273
|
+
return [];
|
|
1274
|
+
}
|
|
1275
|
+
parseNonBlankStringArray(value) {
|
|
1276
|
+
const normalized = this.parseStringArray(value)
|
|
1277
|
+
.map((v) => String(v).trim())
|
|
1278
|
+
.filter((v) => v.length > 0);
|
|
1279
|
+
return normalized.filter((v, index) => normalized.indexOf(v) === index);
|
|
1280
|
+
}
|
|
1281
|
+
parseStringRecord(value) {
|
|
1282
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1283
|
+
return undefined;
|
|
1284
|
+
}
|
|
1285
|
+
const normalized = Object.entries(value)
|
|
1286
|
+
.reduce((acc, [key, mapValue]) => {
|
|
1287
|
+
const normalizedKey = String(key).trim();
|
|
1288
|
+
const normalizedValue = String(mapValue ?? '').trim();
|
|
1289
|
+
if (normalizedKey && normalizedValue) {
|
|
1290
|
+
acc[normalizedKey] = normalizedValue;
|
|
1291
|
+
}
|
|
1292
|
+
return acc;
|
|
1293
|
+
}, {});
|
|
1294
|
+
return Object.keys(normalized).length ? normalized : undefined;
|
|
1295
|
+
}
|
|
1296
|
+
parseSelectionPolicy(value) {
|
|
1297
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1298
|
+
return undefined;
|
|
1299
|
+
}
|
|
1300
|
+
const policy = {};
|
|
1301
|
+
if (value.selectablePropertyPath !== undefined) {
|
|
1302
|
+
policy.selectablePropertyPath = String(value.selectablePropertyPath);
|
|
1303
|
+
}
|
|
1304
|
+
if (value.statusPropertyPath !== undefined) {
|
|
1305
|
+
policy.statusPropertyPath = String(value.statusPropertyPath);
|
|
1306
|
+
}
|
|
1307
|
+
if (value.allowedStatuses !== undefined) {
|
|
1308
|
+
policy.allowedStatuses = this.parseStringArray(value.allowedStatuses);
|
|
1309
|
+
}
|
|
1310
|
+
if (value.blockedStatuses !== undefined) {
|
|
1311
|
+
policy.blockedStatuses = this.parseStringArray(value.blockedStatuses);
|
|
1312
|
+
}
|
|
1313
|
+
if (value.allowRetainInvalidExistingValue !== undefined) {
|
|
1314
|
+
policy.allowRetainInvalidExistingValue = this.parseBoolean(value.allowRetainInvalidExistingValue);
|
|
1315
|
+
}
|
|
1316
|
+
if (value.disabledReasonTemplate !== undefined) {
|
|
1317
|
+
policy.disabledReasonTemplate = String(value.disabledReasonTemplate);
|
|
1318
|
+
}
|
|
1319
|
+
if (value.validationMessageTemplate !== undefined) {
|
|
1320
|
+
policy.validationMessageTemplate = String(value.validationMessageTemplate);
|
|
1321
|
+
}
|
|
1322
|
+
return Object.keys(policy).length ? policy : undefined;
|
|
1323
|
+
}
|
|
1324
|
+
parseLookupCapabilities(value) {
|
|
1325
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1326
|
+
return undefined;
|
|
1327
|
+
}
|
|
1328
|
+
const booleanKeys = [
|
|
1329
|
+
'filter',
|
|
1330
|
+
'byIds',
|
|
1331
|
+
'detail',
|
|
1332
|
+
'create',
|
|
1333
|
+
'edit',
|
|
1334
|
+
'navigateToDetail',
|
|
1335
|
+
'multiSelect',
|
|
1336
|
+
'recent',
|
|
1337
|
+
'favorites',
|
|
1338
|
+
'auditSnapshot',
|
|
1339
|
+
];
|
|
1340
|
+
const capabilities = {};
|
|
1341
|
+
booleanKeys.forEach((key) => {
|
|
1342
|
+
if (value[key] !== undefined) {
|
|
1343
|
+
capabilities[key] = this.parseBoolean(value[key]);
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
1346
|
+
return Object.keys(capabilities).length ? capabilities : undefined;
|
|
1347
|
+
}
|
|
1348
|
+
parseLookupDetail(value) {
|
|
1349
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1350
|
+
return undefined;
|
|
1351
|
+
}
|
|
1352
|
+
const detail = {};
|
|
1353
|
+
if (value.hrefTemplate !== undefined) {
|
|
1354
|
+
detail.hrefTemplate = String(value.hrefTemplate);
|
|
1355
|
+
}
|
|
1356
|
+
if (value.routeTemplate !== undefined) {
|
|
1357
|
+
detail.routeTemplate = String(value.routeTemplate);
|
|
1358
|
+
}
|
|
1359
|
+
if (value.openDetailMode !== undefined) {
|
|
1360
|
+
const openDetailMode = this.parseLookupOpenDetailMode(value.openDetailMode);
|
|
1361
|
+
if (openDetailMode) {
|
|
1362
|
+
detail.openDetailMode = openDetailMode;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
if (value.kind !== undefined) {
|
|
1366
|
+
detail.kind = String(value.kind);
|
|
1367
|
+
}
|
|
1368
|
+
if (value.surfaceId !== undefined) {
|
|
1369
|
+
detail.surfaceId = String(value.surfaceId);
|
|
1370
|
+
}
|
|
1371
|
+
if (value.presentation !== undefined) {
|
|
1372
|
+
detail.presentation = String(value.presentation);
|
|
1373
|
+
}
|
|
1374
|
+
if (value.preferredWidget !== undefined) {
|
|
1375
|
+
detail.preferredWidget = String(value.preferredWidget);
|
|
1376
|
+
}
|
|
1377
|
+
if (value.mode !== undefined) {
|
|
1378
|
+
detail.mode = String(value.mode);
|
|
1379
|
+
}
|
|
1380
|
+
return Object.keys(detail).length ? detail : undefined;
|
|
1381
|
+
}
|
|
1382
|
+
parseLookupDisplay(value) {
|
|
1383
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1384
|
+
return undefined;
|
|
1385
|
+
}
|
|
1386
|
+
const display = {};
|
|
1387
|
+
const stringKeys = [
|
|
1388
|
+
'preset',
|
|
1389
|
+
'usage',
|
|
1390
|
+
'density',
|
|
1391
|
+
'selectedLayout',
|
|
1392
|
+
'resultLayout',
|
|
1393
|
+
'primaryPropertyPath',
|
|
1394
|
+
'avatarPropertyPath',
|
|
1395
|
+
];
|
|
1396
|
+
stringKeys.forEach((key) => {
|
|
1397
|
+
if (value[key] !== undefined) {
|
|
1398
|
+
display[key] = String(value[key]);
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
1401
|
+
if (Array.isArray(value.fields)) {
|
|
1402
|
+
const fields = value.fields
|
|
1403
|
+
.map((field) => this.parseLookupDisplayField(field))
|
|
1404
|
+
.filter((field) => !!field);
|
|
1405
|
+
if (fields.length) {
|
|
1406
|
+
display.fields = fields;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
if (value.secondaryPropertyPaths !== undefined) {
|
|
1410
|
+
display.secondaryPropertyPaths = this.parseStringArray(value.secondaryPropertyPaths);
|
|
1411
|
+
}
|
|
1412
|
+
if (value.badgePropertyPaths !== undefined) {
|
|
1413
|
+
display.badgePropertyPaths = this.parseStringArray(value.badgePropertyPaths);
|
|
1414
|
+
}
|
|
1415
|
+
const booleanKeys = [
|
|
1416
|
+
'showAvatar',
|
|
1417
|
+
'showCode',
|
|
1418
|
+
'showDescription',
|
|
1419
|
+
'showStatus',
|
|
1420
|
+
'showBadges',
|
|
1421
|
+
'showDisabledReason',
|
|
1422
|
+
'showResultCount',
|
|
1423
|
+
];
|
|
1424
|
+
booleanKeys.forEach((key) => {
|
|
1425
|
+
if (value[key] !== undefined) {
|
|
1426
|
+
display[key] = this.parseBoolean(value[key]);
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
if (value.maxVisibleBadges !== undefined && !Number.isNaN(Number(value.maxVisibleBadges))) {
|
|
1430
|
+
display.maxVisibleBadges = Number(value.maxVisibleBadges);
|
|
1431
|
+
}
|
|
1432
|
+
return Object.keys(display).length ? display : undefined;
|
|
1433
|
+
}
|
|
1434
|
+
parseLookupDisplayField(value) {
|
|
1435
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1436
|
+
return undefined;
|
|
1437
|
+
}
|
|
1438
|
+
const field = {};
|
|
1439
|
+
['key', 'propertyPath', 'label', 'icon', 'presentation', 'tone', 'format'].forEach((key) => {
|
|
1440
|
+
if (value[key] !== undefined && String(value[key]).trim().length) {
|
|
1441
|
+
field[key] = String(value[key]);
|
|
1442
|
+
}
|
|
1443
|
+
});
|
|
1444
|
+
return Object.keys(field).length ? field : undefined;
|
|
1445
|
+
}
|
|
1446
|
+
parseLookupCreate(value) {
|
|
1447
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1448
|
+
return undefined;
|
|
1449
|
+
}
|
|
1450
|
+
const create = {};
|
|
1451
|
+
if (value.hrefTemplate !== undefined) {
|
|
1452
|
+
create.hrefTemplate = String(value.hrefTemplate);
|
|
1453
|
+
}
|
|
1454
|
+
if (value.routeTemplate !== undefined) {
|
|
1455
|
+
create.routeTemplate = String(value.routeTemplate);
|
|
1456
|
+
}
|
|
1457
|
+
if (value.openMode !== undefined) {
|
|
1458
|
+
const openMode = this.parseLookupOpenDetailMode(value.openMode);
|
|
1459
|
+
if (openMode) {
|
|
1460
|
+
create.openMode = openMode;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
return Object.keys(create).length ? create : undefined;
|
|
1464
|
+
}
|
|
1465
|
+
parseLookupFilterOperatorArray(value) {
|
|
1466
|
+
if (!Array.isArray(value)) {
|
|
1467
|
+
return [];
|
|
1468
|
+
}
|
|
1469
|
+
const normalized = value.map((item) => String(item ?? '').trim());
|
|
1470
|
+
if (normalized.some((item) => !item || !LOOKUP_FILTER_OPERATORS.has(item))) {
|
|
1471
|
+
return [];
|
|
1472
|
+
}
|
|
1473
|
+
return normalized
|
|
1474
|
+
.filter((item, index) => normalized.indexOf(item) === index)
|
|
1475
|
+
.map((item) => item);
|
|
1476
|
+
}
|
|
1477
|
+
parseLookupSortDirection(value) {
|
|
1478
|
+
const normalized = String(value ?? '').trim().toLowerCase();
|
|
1479
|
+
if (normalized === 'asc' || normalized === 'desc') {
|
|
1480
|
+
return normalized;
|
|
1481
|
+
}
|
|
1482
|
+
return undefined;
|
|
1483
|
+
}
|
|
1484
|
+
parseLookupDialogSize(value) {
|
|
1485
|
+
const normalized = String(value ?? '').trim();
|
|
1486
|
+
if (normalized === 'sm' ||
|
|
1487
|
+
normalized === 'md' ||
|
|
1488
|
+
normalized === 'lg' ||
|
|
1489
|
+
normalized === 'xl' ||
|
|
1490
|
+
normalized === 'full') {
|
|
1491
|
+
return normalized;
|
|
1492
|
+
}
|
|
1493
|
+
return undefined;
|
|
1494
|
+
}
|
|
1495
|
+
parseLookupDialog(value) {
|
|
1496
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1497
|
+
return undefined;
|
|
1498
|
+
}
|
|
1499
|
+
const dialog = {};
|
|
1500
|
+
if (value.enabled !== undefined) {
|
|
1501
|
+
dialog.enabled = this.parseBoolean(value.enabled);
|
|
1502
|
+
}
|
|
1503
|
+
if (value.title !== undefined) {
|
|
1504
|
+
const title = String(value.title).trim();
|
|
1505
|
+
if (title) {
|
|
1506
|
+
dialog.title = title;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
if (value.size !== undefined) {
|
|
1510
|
+
const size = this.parseLookupDialogSize(value.size);
|
|
1511
|
+
if (size) {
|
|
1512
|
+
dialog.size = size;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
if (value.previewPanel !== undefined) {
|
|
1516
|
+
dialog.previewPanel = this.parseBoolean(value.previewPanel);
|
|
1517
|
+
}
|
|
1518
|
+
if (value.allowColumnChooser !== undefined) {
|
|
1519
|
+
dialog.allowColumnChooser = this.parseBoolean(value.allowColumnChooser);
|
|
1520
|
+
}
|
|
1521
|
+
if (value.allowSavedViews !== undefined) {
|
|
1522
|
+
dialog.allowSavedViews = this.parseBoolean(value.allowSavedViews);
|
|
1523
|
+
}
|
|
1524
|
+
if (Array.isArray(value.resultColumns)) {
|
|
1525
|
+
const resultColumns = value.resultColumns
|
|
1526
|
+
.map((item) => this.parseLookupResultColumn(item))
|
|
1527
|
+
.filter((item) => !!item);
|
|
1528
|
+
if (resultColumns.length) {
|
|
1529
|
+
dialog.resultColumns = resultColumns;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
if (value.openActionLabel !== undefined) {
|
|
1533
|
+
const openActionLabel = String(value.openActionLabel).trim();
|
|
1534
|
+
if (openActionLabel) {
|
|
1535
|
+
dialog.openActionLabel = openActionLabel;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
if (value.applyActionLabel !== undefined) {
|
|
1539
|
+
const applyActionLabel = String(value.applyActionLabel).trim();
|
|
1540
|
+
if (applyActionLabel) {
|
|
1541
|
+
dialog.applyActionLabel = applyActionLabel;
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
if (value.cancelActionLabel !== undefined) {
|
|
1545
|
+
const cancelActionLabel = String(value.cancelActionLabel).trim();
|
|
1546
|
+
if (cancelActionLabel) {
|
|
1547
|
+
dialog.cancelActionLabel = cancelActionLabel;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
return Object.keys(dialog).length ? dialog : undefined;
|
|
1551
|
+
}
|
|
1552
|
+
parseLookupResultColumn(value) {
|
|
1553
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1554
|
+
return undefined;
|
|
1555
|
+
}
|
|
1556
|
+
const field = String(value.field ?? '').trim();
|
|
1557
|
+
if (!field) {
|
|
1558
|
+
return undefined;
|
|
1559
|
+
}
|
|
1560
|
+
const column = { field };
|
|
1561
|
+
if (value.label !== undefined) {
|
|
1562
|
+
const label = String(value.label).trim();
|
|
1563
|
+
if (label) {
|
|
1564
|
+
column.label = label;
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
if (value.kind !== undefined) {
|
|
1568
|
+
const kind = String(value.kind).trim();
|
|
1569
|
+
if (kind === 'code' ||
|
|
1570
|
+
kind === 'label' ||
|
|
1571
|
+
kind === 'description' ||
|
|
1572
|
+
kind === 'status' ||
|
|
1573
|
+
kind === 'disabledReason' ||
|
|
1574
|
+
kind === 'custom') {
|
|
1575
|
+
column.kind = kind;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
if (value.width !== undefined) {
|
|
1579
|
+
const width = String(value.width).trim();
|
|
1580
|
+
if (width) {
|
|
1581
|
+
column.width = width;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
return column;
|
|
1585
|
+
}
|
|
1586
|
+
parseLookupFilterDefinition(value) {
|
|
1587
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1588
|
+
return undefined;
|
|
1589
|
+
}
|
|
1590
|
+
const field = String(value.field ?? '').trim();
|
|
1591
|
+
const type = String(value.type ?? '').trim();
|
|
1592
|
+
if (!field || !LOOKUP_FILTER_FIELD_TYPES.has(type)) {
|
|
1593
|
+
return undefined;
|
|
1594
|
+
}
|
|
1595
|
+
const operators = this.parseLookupFilterOperatorArray(value.operators);
|
|
1596
|
+
if (!operators.length) {
|
|
1597
|
+
return undefined;
|
|
1598
|
+
}
|
|
1599
|
+
const definition = {
|
|
1600
|
+
field,
|
|
1601
|
+
type,
|
|
1602
|
+
operators,
|
|
1603
|
+
};
|
|
1604
|
+
if (value.label !== undefined) {
|
|
1605
|
+
definition.label = String(value.label);
|
|
1606
|
+
}
|
|
1607
|
+
const defaultOperator = String(value.defaultOperator ?? '').trim();
|
|
1608
|
+
if (defaultOperator && operators.includes(defaultOperator)) {
|
|
1609
|
+
definition.defaultOperator = defaultOperator;
|
|
1610
|
+
}
|
|
1611
|
+
if (value.optionsSource !== undefined) {
|
|
1612
|
+
const optionsSource = String(value.optionsSource).trim();
|
|
1613
|
+
if (optionsSource) {
|
|
1614
|
+
definition.optionsSource = optionsSource;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
if (value.required !== undefined) {
|
|
1618
|
+
definition.required = this.parseBoolean(value.required);
|
|
1619
|
+
}
|
|
1620
|
+
if (value.hidden !== undefined) {
|
|
1621
|
+
definition.hidden = this.parseBoolean(value.hidden);
|
|
1622
|
+
}
|
|
1623
|
+
return definition;
|
|
1624
|
+
}
|
|
1625
|
+
parseDefaultFilters(value) {
|
|
1626
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1627
|
+
return undefined;
|
|
1628
|
+
}
|
|
1629
|
+
const normalized = Object.entries(value).reduce((acc, [key, item]) => {
|
|
1630
|
+
const normalizedKey = String(key).trim();
|
|
1631
|
+
if (!normalizedKey) {
|
|
1632
|
+
return acc;
|
|
1633
|
+
}
|
|
1634
|
+
const values = Array.isArray(item)
|
|
1635
|
+
? item.filter((entry) => entry !== undefined)
|
|
1636
|
+
: item === undefined
|
|
1637
|
+
? []
|
|
1638
|
+
: [item];
|
|
1639
|
+
if (values.length) {
|
|
1640
|
+
acc[normalizedKey] = values;
|
|
1641
|
+
}
|
|
1642
|
+
return acc;
|
|
1643
|
+
}, {});
|
|
1644
|
+
return Object.keys(normalized).length ? normalized : undefined;
|
|
1645
|
+
}
|
|
1646
|
+
parseLookupFiltering(value) {
|
|
1647
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1648
|
+
return undefined;
|
|
1649
|
+
}
|
|
1650
|
+
const filtering = {};
|
|
1651
|
+
if (value.availableFilters !== undefined) {
|
|
1652
|
+
const availableFilters = Array.isArray(value.availableFilters)
|
|
1653
|
+
? value.availableFilters
|
|
1654
|
+
.map((item) => this.parseLookupFilterDefinition(item))
|
|
1655
|
+
.filter((item) => !!item)
|
|
1656
|
+
: [];
|
|
1657
|
+
if (availableFilters.length) {
|
|
1658
|
+
filtering.availableFilters = availableFilters;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
const defaultFilters = this.parseDefaultFilters(value.defaultFilters);
|
|
1662
|
+
if (defaultFilters) {
|
|
1663
|
+
filtering.defaultFilters = defaultFilters;
|
|
1664
|
+
}
|
|
1665
|
+
if (value.sortOptions !== undefined && Array.isArray(value.sortOptions)) {
|
|
1666
|
+
const sortOptions = value.sortOptions
|
|
1667
|
+
.map((item) => {
|
|
1668
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
1669
|
+
return undefined;
|
|
1670
|
+
}
|
|
1671
|
+
const key = String(item.key ?? '').trim();
|
|
1672
|
+
const field = String(item.field ?? '').trim();
|
|
1673
|
+
const direction = this.parseLookupSortDirection(item.direction);
|
|
1674
|
+
if (!key || !field || !direction) {
|
|
1675
|
+
return undefined;
|
|
1676
|
+
}
|
|
1677
|
+
return {
|
|
1678
|
+
key,
|
|
1679
|
+
field,
|
|
1680
|
+
direction,
|
|
1681
|
+
...(item.label !== undefined ? { label: String(item.label) } : {}),
|
|
1682
|
+
};
|
|
1683
|
+
})
|
|
1684
|
+
.filter((item) => !!item);
|
|
1685
|
+
if (sortOptions.length) {
|
|
1686
|
+
filtering.sortOptions = sortOptions;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
if (value.defaultSort !== undefined) {
|
|
1690
|
+
const defaultSort = String(value.defaultSort).trim();
|
|
1691
|
+
if (defaultSort) {
|
|
1692
|
+
filtering.defaultSort = defaultSort;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
if (value.quickFilterFields !== undefined) {
|
|
1696
|
+
const quickFilterFields = this.parseNonBlankStringArray(value.quickFilterFields);
|
|
1697
|
+
if (quickFilterFields.length) {
|
|
1698
|
+
filtering.quickFilterFields = quickFilterFields;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
if (value.searchPlaceholder !== undefined) {
|
|
1702
|
+
const searchPlaceholder = String(value.searchPlaceholder).trim();
|
|
1703
|
+
if (searchPlaceholder) {
|
|
1704
|
+
filtering.searchPlaceholder = searchPlaceholder;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
if (Array.isArray(value.searchStrategies)) {
|
|
1708
|
+
const seenKeys = new Set();
|
|
1709
|
+
const searchStrategies = value.searchStrategies
|
|
1710
|
+
.map((item) => {
|
|
1711
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
1712
|
+
return undefined;
|
|
1713
|
+
}
|
|
1714
|
+
const raw = item;
|
|
1715
|
+
const key = String(raw.key ?? '').trim();
|
|
1716
|
+
const kind = String(raw.kind ?? '').trim();
|
|
1717
|
+
const minSearchChars = Number(raw.minSearchChars);
|
|
1718
|
+
const inputFormat = String(raw.inputFormat ?? 'text').trim();
|
|
1719
|
+
if (!key ||
|
|
1720
|
+
seenKeys.has(key) ||
|
|
1721
|
+
!LOOKUP_SEARCH_STRATEGY_KINDS.has(kind) ||
|
|
1722
|
+
!['text', 'digits'].includes(inputFormat) ||
|
|
1723
|
+
!Number.isInteger(minSearchChars) ||
|
|
1724
|
+
minSearchChars < 1) {
|
|
1725
|
+
return undefined;
|
|
1726
|
+
}
|
|
1727
|
+
seenKeys.add(key);
|
|
1728
|
+
return {
|
|
1729
|
+
key,
|
|
1730
|
+
kind,
|
|
1731
|
+
minSearchChars,
|
|
1732
|
+
...(inputFormat === 'digits' ? { inputFormat } : {}),
|
|
1733
|
+
};
|
|
1734
|
+
})
|
|
1735
|
+
.filter((item) => !!item);
|
|
1736
|
+
if (searchStrategies.length) {
|
|
1737
|
+
filtering.searchStrategies = searchStrategies;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
return Object.keys(filtering).length ? filtering : undefined;
|
|
1741
|
+
}
|
|
1742
|
+
/** Parse option arrays into `{ key, value }` objects with label preservation. */
|
|
1743
|
+
parseOptions(value) {
|
|
1744
|
+
if (!Array.isArray(value)) {
|
|
1745
|
+
return [];
|
|
1746
|
+
}
|
|
1747
|
+
return value.map((opt) => {
|
|
1748
|
+
if (typeof opt === 'object' && opt !== null) {
|
|
1749
|
+
const k = opt.key ?? opt.value ?? opt.id ?? opt.label ?? '';
|
|
1750
|
+
const display = opt.label ??
|
|
1751
|
+
opt.text ??
|
|
1752
|
+
opt.title ??
|
|
1753
|
+
(opt.key !== undefined && opt.value !== undefined && opt.key !== opt.value
|
|
1754
|
+
? opt.value
|
|
1755
|
+
: undefined);
|
|
1756
|
+
const v = display ?? opt.value ?? opt.key ?? '';
|
|
1757
|
+
const result = {
|
|
1758
|
+
...opt,
|
|
1759
|
+
key: String(k),
|
|
1760
|
+
value: String(v),
|
|
1761
|
+
};
|
|
1762
|
+
if (display !== undefined && display !== null) {
|
|
1763
|
+
result.label = String(display);
|
|
1764
|
+
}
|
|
1765
|
+
return result;
|
|
1766
|
+
}
|
|
1767
|
+
return { key: String(opt), value: String(opt) };
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
parseOptionSource(value) {
|
|
1771
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1772
|
+
return undefined;
|
|
1773
|
+
}
|
|
1774
|
+
const optionSource = {
|
|
1775
|
+
key: String(value.key ?? '').trim(),
|
|
1776
|
+
};
|
|
1777
|
+
if (!optionSource.key) {
|
|
1778
|
+
return undefined;
|
|
1779
|
+
}
|
|
1780
|
+
if (value.type !== undefined) {
|
|
1781
|
+
const optionSourceType = this.parseOptionSourceType(value.type);
|
|
1782
|
+
if (optionSourceType) {
|
|
1783
|
+
optionSource.type = optionSourceType;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
if (value.resourcePath !== undefined) {
|
|
1787
|
+
optionSource.resourcePath = String(value.resourcePath);
|
|
1788
|
+
}
|
|
1789
|
+
if (value.filterField !== undefined) {
|
|
1790
|
+
optionSource.filterField = String(value.filterField);
|
|
1791
|
+
}
|
|
1792
|
+
if (value.propertyPath !== undefined) {
|
|
1793
|
+
optionSource.propertyPath = String(value.propertyPath);
|
|
1794
|
+
}
|
|
1795
|
+
if (value.labelPropertyPath !== undefined) {
|
|
1796
|
+
optionSource.labelPropertyPath = String(value.labelPropertyPath);
|
|
1797
|
+
}
|
|
1798
|
+
if (value.valuePropertyPath !== undefined) {
|
|
1799
|
+
optionSource.valuePropertyPath = String(value.valuePropertyPath);
|
|
1800
|
+
}
|
|
1801
|
+
if (value.entityKey !== undefined) {
|
|
1802
|
+
optionSource.entityKey = String(value.entityKey);
|
|
1803
|
+
}
|
|
1804
|
+
if (value.codePropertyPath !== undefined) {
|
|
1805
|
+
optionSource.codePropertyPath = String(value.codePropertyPath);
|
|
1806
|
+
}
|
|
1807
|
+
if (value.descriptionPropertyPaths !== undefined) {
|
|
1808
|
+
optionSource.descriptionPropertyPaths = this.parseStringArray(value.descriptionPropertyPaths);
|
|
1809
|
+
}
|
|
1810
|
+
if (value.statusPropertyPath !== undefined) {
|
|
1811
|
+
optionSource.statusPropertyPath = String(value.statusPropertyPath);
|
|
1812
|
+
}
|
|
1813
|
+
if (value.disabledPropertyPath !== undefined) {
|
|
1814
|
+
optionSource.disabledPropertyPath = String(value.disabledPropertyPath);
|
|
1815
|
+
}
|
|
1816
|
+
if (value.disabledReasonPropertyPath !== undefined) {
|
|
1817
|
+
optionSource.disabledReasonPropertyPath = String(value.disabledReasonPropertyPath);
|
|
1818
|
+
}
|
|
1819
|
+
if (value.searchPropertyPaths !== undefined) {
|
|
1820
|
+
optionSource.searchPropertyPaths = this.parseStringArray(value.searchPropertyPaths);
|
|
1821
|
+
}
|
|
1822
|
+
if (value.dependsOn !== undefined) {
|
|
1823
|
+
const dependsOn = this.parseNonBlankStringArray(value.dependsOn);
|
|
1824
|
+
if (dependsOn.length) {
|
|
1825
|
+
optionSource.dependsOn = dependsOn;
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
const dependencyFilterMap = this.parseStringRecord(value.dependencyFilterMap);
|
|
1829
|
+
if (dependencyFilterMap) {
|
|
1830
|
+
optionSource.dependencyFilterMap = dependencyFilterMap;
|
|
1831
|
+
}
|
|
1832
|
+
const selectionPolicy = this.parseSelectionPolicy(value.selectionPolicy);
|
|
1833
|
+
if (selectionPolicy) {
|
|
1834
|
+
optionSource.selectionPolicy = selectionPolicy;
|
|
1835
|
+
}
|
|
1836
|
+
const capabilities = this.parseLookupCapabilities(value.capabilities);
|
|
1837
|
+
if (capabilities) {
|
|
1838
|
+
optionSource.capabilities = capabilities;
|
|
1839
|
+
}
|
|
1840
|
+
const detail = this.parseLookupDetail(value.detail);
|
|
1841
|
+
if (detail) {
|
|
1842
|
+
optionSource.detail = detail;
|
|
1843
|
+
}
|
|
1844
|
+
const display = this.parseLookupDisplay(value.display);
|
|
1845
|
+
if (display) {
|
|
1846
|
+
optionSource.display = display;
|
|
1847
|
+
}
|
|
1848
|
+
const create = this.parseLookupCreate(value.create);
|
|
1849
|
+
if (create) {
|
|
1850
|
+
optionSource.create = create;
|
|
1851
|
+
}
|
|
1852
|
+
const filtering = this.parseLookupFiltering(value.filtering);
|
|
1853
|
+
if (filtering) {
|
|
1854
|
+
optionSource.filtering = filtering;
|
|
1855
|
+
}
|
|
1856
|
+
if (value.excludeSelfField !== undefined) {
|
|
1857
|
+
optionSource.excludeSelfField = this.parseBoolean(value.excludeSelfField);
|
|
1858
|
+
}
|
|
1859
|
+
if (value.searchMode !== undefined) {
|
|
1860
|
+
const searchMode = this.parseOptionSourceSearchMode(value.searchMode);
|
|
1861
|
+
if (searchMode) {
|
|
1862
|
+
optionSource.searchMode = searchMode;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
if (value.pageSize !== undefined && !Number.isNaN(Number(value.pageSize))) {
|
|
1866
|
+
optionSource.pageSize = Number(value.pageSize);
|
|
1867
|
+
}
|
|
1868
|
+
if (value.includeIds !== undefined) {
|
|
1869
|
+
optionSource.includeIds = this.parseBoolean(value.includeIds);
|
|
1870
|
+
}
|
|
1871
|
+
if (value.selectedReloadPolicy !== undefined) {
|
|
1872
|
+
const selectedReloadPolicy = this.parseOptionSourceSelectedReloadPolicy(value.selectedReloadPolicy);
|
|
1873
|
+
if (selectedReloadPolicy) {
|
|
1874
|
+
optionSource.selectedReloadPolicy = selectedReloadPolicy;
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
if (value.invalidSortPolicy !== undefined) {
|
|
1878
|
+
const invalidSortPolicy = this.parseOptionSourceInvalidSortPolicy(value.invalidSortPolicy);
|
|
1879
|
+
if (invalidSortPolicy) {
|
|
1880
|
+
optionSource.invalidSortPolicy = invalidSortPolicy;
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
return optionSource;
|
|
1884
|
+
}
|
|
1885
|
+
parseOptionSourceType(value) {
|
|
1886
|
+
const normalized = String(value ?? '').trim().toUpperCase();
|
|
1887
|
+
return OPTION_SOURCE_TYPES.has(normalized) ? normalized : undefined;
|
|
1888
|
+
}
|
|
1889
|
+
parseOptionSourceSearchMode(value) {
|
|
1890
|
+
const normalized = String(value ?? '').trim();
|
|
1891
|
+
return OPTION_SOURCE_SEARCH_MODES.has(normalized) ? normalized : undefined;
|
|
1892
|
+
}
|
|
1893
|
+
parseOptionSourceSelectedReloadPolicy(value) {
|
|
1894
|
+
const normalized = String(value ?? '').trim();
|
|
1895
|
+
return OPTION_SOURCE_SELECTED_RELOAD_POLICIES.has(normalized) ? normalized : undefined;
|
|
1896
|
+
}
|
|
1897
|
+
parseOptionSourceInvalidSortPolicy(value) {
|
|
1898
|
+
const normalized = String(value ?? '').trim();
|
|
1899
|
+
return OPTION_SOURCE_INVALID_SORT_POLICIES.has(normalized) ? normalized : undefined;
|
|
1900
|
+
}
|
|
1901
|
+
parseLookupOpenDetailMode(value) {
|
|
1902
|
+
const normalized = String(value ?? '').trim();
|
|
1903
|
+
return LOOKUP_OPEN_DETAIL_MODES.has(normalized) ? normalized : undefined;
|
|
1904
|
+
}
|
|
1905
|
+
parseValuePresentation(value) {
|
|
1906
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1907
|
+
return undefined;
|
|
1908
|
+
}
|
|
1909
|
+
const typeToken = String(value.type ?? '').trim();
|
|
1910
|
+
if (!typeToken || !VALUE_PRESENTATION_TYPES.has(typeToken)) {
|
|
1911
|
+
if (typeToken) {
|
|
1912
|
+
console.warn(`[SchemaNormalizer] Ignoring invalid x-ui.valuePresentation.type: ${typeToken}`);
|
|
1913
|
+
}
|
|
1914
|
+
return undefined;
|
|
1915
|
+
}
|
|
1916
|
+
const presentation = {
|
|
1917
|
+
type: typeToken,
|
|
1918
|
+
};
|
|
1919
|
+
const styleToken = String(value.style ?? '').trim();
|
|
1920
|
+
if (styleToken) {
|
|
1921
|
+
if (VALUE_PRESENTATION_STYLES.has(styleToken)) {
|
|
1922
|
+
presentation.style = styleToken;
|
|
1923
|
+
}
|
|
1924
|
+
else {
|
|
1925
|
+
console.warn(`[SchemaNormalizer] Ignoring invalid x-ui.valuePresentation.style: ${styleToken}`);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
const formatToken = String(value.format ?? '').trim();
|
|
1929
|
+
if (formatToken) {
|
|
1930
|
+
presentation.format = formatToken;
|
|
1931
|
+
}
|
|
1932
|
+
if (value.currency && typeof value.currency === 'object') {
|
|
1933
|
+
presentation.currency = { ...value.currency };
|
|
1934
|
+
}
|
|
1935
|
+
if (value.number && typeof value.number === 'object') {
|
|
1936
|
+
presentation.number = { ...value.number };
|
|
1937
|
+
}
|
|
1938
|
+
return presentation;
|
|
1939
|
+
}
|
|
1940
|
+
/**
|
|
1941
|
+
* Converte string/Function em função real.
|
|
1942
|
+
* Atenção: usa `new Function` para avaliar strings – requer backend confiável.
|
|
1943
|
+
*/
|
|
1944
|
+
parseFunction(fn) {
|
|
1945
|
+
if (typeof fn === 'function') {
|
|
1946
|
+
return fn;
|
|
1947
|
+
}
|
|
1948
|
+
if (typeof fn === 'string' && fn.trim()) {
|
|
1949
|
+
try {
|
|
1950
|
+
// eslint-disable-next-line no-new-func
|
|
1951
|
+
return new Function(`return (${fn});`)();
|
|
1952
|
+
}
|
|
1953
|
+
catch {
|
|
1954
|
+
return undefined;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
return undefined;
|
|
1958
|
+
}
|
|
1959
|
+
/** Parse a transform function: (val: any) => any */
|
|
1960
|
+
parseTransformFunction(fn) {
|
|
1961
|
+
const parsed = this.parseFunction(fn);
|
|
1962
|
+
if (!parsed)
|
|
1963
|
+
return undefined;
|
|
1964
|
+
return parsed;
|
|
1965
|
+
}
|
|
1966
|
+
parseConditionalValidationRules(value, fieldName) {
|
|
1967
|
+
if (value === undefined) {
|
|
1968
|
+
return undefined;
|
|
1969
|
+
}
|
|
1970
|
+
if (!Array.isArray(value)) {
|
|
1971
|
+
console.warn(`[SchemaNormalizer] Ignoring non-array x-ui.conditionalValidation for field "${fieldName}". Expected an array of declarative rules.`);
|
|
1972
|
+
return undefined;
|
|
1973
|
+
}
|
|
1974
|
+
const rules = [];
|
|
1975
|
+
value.forEach((candidate, index) => {
|
|
1976
|
+
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
|
|
1977
|
+
console.warn(`[SchemaNormalizer] Ignoring invalid x-ui.conditionalValidation[${index}] for field "${fieldName}". Expected an object rule.`);
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
const condition = this.parseJsonLogicExpression(candidate['condition'], fieldName, `x-ui.conditionalValidation[${index}].condition`);
|
|
1981
|
+
const validatorsSource = candidate['validators'];
|
|
1982
|
+
if (!validatorsSource || typeof validatorsSource !== 'object' || Array.isArray(validatorsSource)) {
|
|
1983
|
+
console.warn(`[SchemaNormalizer] Ignoring x-ui.conditionalValidation[${index}] for field "${fieldName}" because validators is missing or invalid.`);
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
rules.push({
|
|
1987
|
+
condition: condition ?? null,
|
|
1988
|
+
validators: this.parseValidators(validatorsSource),
|
|
1989
|
+
});
|
|
1990
|
+
});
|
|
1991
|
+
return rules.length ? rules : undefined;
|
|
1992
|
+
}
|
|
1993
|
+
/**
|
|
1994
|
+
* Extrai e normaliza propriedades de validação a partir do objeto x-ui.
|
|
1995
|
+
* Suporta: required, min/max, minLength/maxLength, pattern/regex, email, url,
|
|
1996
|
+
* matchField, uniqueValidator (+message), conditionalValidation, requiredChecked,
|
|
1997
|
+
* arquivos (allowedFileTypes, fileTypeMessage, maxFileSize), custom/async validators,
|
|
1998
|
+
* minWords e comportamento (validationTrigger(s), validationDebounce, showInlineErrors, errorPosition).
|
|
1999
|
+
*/
|
|
2000
|
+
parseValidators(ui, fieldName = 'unknown') {
|
|
2001
|
+
if (!ui || typeof ui !== 'object') {
|
|
2002
|
+
return {};
|
|
2003
|
+
}
|
|
2004
|
+
const out = {};
|
|
2005
|
+
// Basic validators and messages
|
|
2006
|
+
if (ui.required !== undefined)
|
|
2007
|
+
out.required = this.parseBoolean(ui.required);
|
|
2008
|
+
if (ui.requiredMessage !== undefined)
|
|
2009
|
+
out.requiredMessage = ui.requiredMessage;
|
|
2010
|
+
if (ui.minLength !== undefined)
|
|
2011
|
+
out.minLength = Number(ui.minLength);
|
|
2012
|
+
if (ui.minLengthMessage !== undefined)
|
|
2013
|
+
out.minLengthMessage = ui.minLengthMessage;
|
|
2014
|
+
if (ui.maxLength !== undefined)
|
|
2015
|
+
out.maxLength = Number(ui.maxLength);
|
|
2016
|
+
if (ui.maxLengthMessage !== undefined)
|
|
2017
|
+
out.maxLengthMessage = ui.maxLengthMessage;
|
|
2018
|
+
if (ui.min !== undefined)
|
|
2019
|
+
out.min = Number(ui.min);
|
|
2020
|
+
if (ui.max !== undefined)
|
|
2021
|
+
out.max = Number(ui.max);
|
|
2022
|
+
if (ui.minMessage !== undefined)
|
|
2023
|
+
out.minMessage = ui.minMessage;
|
|
2024
|
+
if (ui.maxMessage !== undefined)
|
|
2025
|
+
out.maxMessage = ui.maxMessage;
|
|
2026
|
+
// Range can define both min and max at once. Accept [min, max] or { min, max }
|
|
2027
|
+
if (ui.range !== undefined) {
|
|
2028
|
+
const r = ui.range;
|
|
2029
|
+
if (Array.isArray(r) && r.length >= 2) {
|
|
2030
|
+
if (out.min === undefined && r[0] != null)
|
|
2031
|
+
out.min = Number(r[0]);
|
|
2032
|
+
if (out.max === undefined && r[1] != null)
|
|
2033
|
+
out.max = Number(r[1]);
|
|
2034
|
+
}
|
|
2035
|
+
else if (typeof r === 'object' && r) {
|
|
2036
|
+
if (out.min === undefined && r.min != null)
|
|
2037
|
+
out.min = Number(r.min);
|
|
2038
|
+
if (out.max === undefined && r.max != null)
|
|
2039
|
+
out.max = Number(r.max);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
if (ui.rangeMessage !== undefined)
|
|
2043
|
+
out.rangeMessage = ui.rangeMessage;
|
|
2044
|
+
// Pattern / Regex
|
|
2045
|
+
if (ui.pattern !== undefined)
|
|
2046
|
+
out.pattern = String(ui.pattern);
|
|
2047
|
+
if (ui.patternMessage !== undefined)
|
|
2048
|
+
out.patternMessage = ui.patternMessage;
|
|
2049
|
+
// Support alias keys: regex / regexMessage
|
|
2050
|
+
if (ui.regex !== undefined && out.pattern === undefined)
|
|
2051
|
+
out.pattern = String(ui.regex);
|
|
2052
|
+
if (ui.regexMessage !== undefined && out.patternMessage === undefined)
|
|
2053
|
+
out.patternMessage = ui.regexMessage;
|
|
2054
|
+
// Email and URL pattern helpers
|
|
2055
|
+
if (ui.email !== undefined)
|
|
2056
|
+
out.email = this.parseBoolean(ui.email);
|
|
2057
|
+
if (ui.emailMessage !== undefined)
|
|
2058
|
+
out.emailMessage = ui.emailMessage;
|
|
2059
|
+
if (ui.url !== undefined)
|
|
2060
|
+
out.url = this.parseBoolean(ui.url);
|
|
2061
|
+
if (ui.urlMessage !== undefined)
|
|
2062
|
+
out.urlMessage = ui.urlMessage;
|
|
2063
|
+
// Matching and unique validators
|
|
2064
|
+
if (ui.matchField !== undefined)
|
|
2065
|
+
out.matchField = String(ui.matchField);
|
|
2066
|
+
if (ui.matchFieldMessage !== undefined)
|
|
2067
|
+
out.matchFieldMessage = ui.matchFieldMessage;
|
|
2068
|
+
if (ui.uniqueMessage !== undefined)
|
|
2069
|
+
out.uniqueMessage = ui.uniqueMessage;
|
|
2070
|
+
if (ui.uniqueValidator !== undefined) {
|
|
2071
|
+
const uniq = this.parseFunction(ui.uniqueValidator);
|
|
2072
|
+
if (uniq)
|
|
2073
|
+
out.uniqueValidator = uniq;
|
|
2074
|
+
}
|
|
2075
|
+
// Conditional validation rules
|
|
2076
|
+
if (ui.conditionalValidation !== undefined) {
|
|
2077
|
+
const cond = this.parseConditionalValidationRules(ui.conditionalValidation, fieldName);
|
|
2078
|
+
if (cond)
|
|
2079
|
+
out.conditionalValidation = cond;
|
|
2080
|
+
}
|
|
2081
|
+
// Checkbox: required to be checked
|
|
2082
|
+
if (ui.requiredChecked !== undefined)
|
|
2083
|
+
out.requiredChecked = this.parseBoolean(ui.requiredChecked);
|
|
2084
|
+
// File validation
|
|
2085
|
+
if (ui.allowedFileTypes !== undefined)
|
|
2086
|
+
out.allowedFileTypes = this.parseStringArray(ui.allowedFileTypes);
|
|
2087
|
+
if (ui.fileTypeMessage !== undefined)
|
|
2088
|
+
out.fileTypeMessage = ui.fileTypeMessage;
|
|
2089
|
+
if (ui.maxFileSize !== undefined)
|
|
2090
|
+
out.maxFileSize = Number(ui.maxFileSize);
|
|
2091
|
+
// Custom validators (sync/async)
|
|
2092
|
+
if (ui.customValidator !== undefined) {
|
|
2093
|
+
const fn = this.parseFunction(ui.customValidator);
|
|
2094
|
+
if (fn)
|
|
2095
|
+
out.customValidator = fn;
|
|
2096
|
+
}
|
|
2097
|
+
if (ui.asyncValidator !== undefined) {
|
|
2098
|
+
const fn = this.parseFunction(ui.asyncValidator);
|
|
2099
|
+
if (fn)
|
|
2100
|
+
out.asyncValidator = fn;
|
|
2101
|
+
}
|
|
2102
|
+
// Word count
|
|
2103
|
+
if (ui.minWords !== undefined)
|
|
2104
|
+
out.minWords = Number(ui.minWords);
|
|
2105
|
+
// Validation behavior
|
|
2106
|
+
if (ui.validationTrigger !== undefined) {
|
|
2107
|
+
out.validationTriggers = Array.isArray(ui.validationTrigger)
|
|
2108
|
+
? ui.validationTrigger.map((t) => String(t))
|
|
2109
|
+
: [String(ui.validationTrigger)];
|
|
2110
|
+
}
|
|
2111
|
+
if (ui.validationTriggers !== undefined && out.validationTriggers === undefined) {
|
|
2112
|
+
out.validationTriggers = Array.isArray(ui.validationTriggers)
|
|
2113
|
+
? ui.validationTriggers.map((t) => String(t))
|
|
2114
|
+
: [String(ui.validationTriggers)];
|
|
2115
|
+
}
|
|
2116
|
+
if (ui.validationDebounce !== undefined)
|
|
2117
|
+
out.validationDebounce = Number(ui.validationDebounce);
|
|
2118
|
+
if (ui.showInlineErrors !== undefined)
|
|
2119
|
+
out.showInlineErrors = this.parseBoolean(ui.showInlineErrors);
|
|
2120
|
+
if (ui.errorPosition !== undefined)
|
|
2121
|
+
out.errorPosition = String(ui.errorPosition);
|
|
2122
|
+
return out;
|
|
2123
|
+
}
|
|
2124
|
+
/** Parse array of button definitions. */
|
|
2125
|
+
parseButtons(value) {
|
|
2126
|
+
if (!Array.isArray(value)) {
|
|
2127
|
+
return [];
|
|
2128
|
+
}
|
|
2129
|
+
return value.map((v) => ({ ...v }));
|
|
2130
|
+
}
|
|
2131
|
+
parseJsonLogicExpression(value, fieldName, sourceKey) {
|
|
2132
|
+
if (value === undefined) {
|
|
2133
|
+
return undefined;
|
|
2134
|
+
}
|
|
2135
|
+
if (value === null) {
|
|
2136
|
+
return null;
|
|
2137
|
+
}
|
|
2138
|
+
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
2139
|
+
return JSON.parse(JSON.stringify(value));
|
|
2140
|
+
}
|
|
2141
|
+
console.warn(`[SchemaNormalizer] Ignoring non-Json-Logic ${sourceKey} for field "${fieldName}". Expected object/null, received ${typeof value}.`);
|
|
2142
|
+
return undefined;
|
|
2143
|
+
}
|
|
2144
|
+
resolveNonEmptyStringToken(...values) {
|
|
2145
|
+
for (const value of values) {
|
|
2146
|
+
const text = String(value ?? '').trim();
|
|
2147
|
+
if (text.length) {
|
|
2148
|
+
return text;
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
return undefined;
|
|
2152
|
+
}
|
|
2153
|
+
isNumericSchemaType(type) {
|
|
2154
|
+
const token = String(type ?? '').trim().toLowerCase();
|
|
2155
|
+
return ['number', 'integer', 'int', 'float', 'double', 'decimal'].includes(token);
|
|
2156
|
+
}
|
|
2157
|
+
resolveNumericFormatToken(prop, ui, resolvedType) {
|
|
2158
|
+
const typeToken = resolvedType ?? prop?.type;
|
|
2159
|
+
if (!this.isNumericSchemaType(typeToken)) {
|
|
2160
|
+
return undefined;
|
|
2161
|
+
}
|
|
2162
|
+
if (ui?.numberFormat?.percentage === true || ui?.['numberFormat.percentage'] === true) {
|
|
2163
|
+
return 'percent';
|
|
2164
|
+
}
|
|
2165
|
+
return this.resolveNonEmptyStringToken(ui?.numericFormat, ui?.format, ui?.numberFormat?.style, ui?.['numberFormat.style'], prop?.format);
|
|
2166
|
+
}
|
|
2167
|
+
/**
|
|
2168
|
+
* Converte o schema bruto do backend (OpenAPI + x-ui) em FieldDefinition[] tipado.
|
|
2169
|
+
* Regras:
|
|
2170
|
+
* - Ignora propriedades sem x-ui.
|
|
2171
|
+
* - Garante ordenação por 'order' quando presente.
|
|
2172
|
+
* - Funções condicionais/transformações sempre viram funções reais.
|
|
2173
|
+
*
|
|
2174
|
+
* @param schema Objeto OpenAPI/JSON Schema com properties e x-ui por propriedade.
|
|
2175
|
+
* @returns FieldDefinition[] prontos para mapeamento/uso no restante do sistema.
|
|
2176
|
+
*/
|
|
2177
|
+
normalizeSchema(schema) {
|
|
2178
|
+
return this.normalizeSchemaWithRoot(schema, schema);
|
|
2179
|
+
}
|
|
2180
|
+
normalizeSchemaWithRoot(schema, rootSchema) {
|
|
2181
|
+
if (!schema || typeof schema !== 'object') {
|
|
2182
|
+
return [];
|
|
2183
|
+
}
|
|
2184
|
+
const properties = schema.properties ?? {};
|
|
2185
|
+
const requiredNames = new Set(Array.isArray(schema.required)
|
|
2186
|
+
? schema.required.filter((name) => typeof name === 'string')
|
|
2187
|
+
: []);
|
|
2188
|
+
const fields = [];
|
|
2189
|
+
for (const [name, prop] of Object.entries(properties)) {
|
|
2190
|
+
const ui = prop &&
|
|
2191
|
+
typeof prop === 'object' &&
|
|
2192
|
+
!Array.isArray(prop) &&
|
|
2193
|
+
prop['x-ui'] &&
|
|
2194
|
+
typeof prop['x-ui'] === 'object' &&
|
|
2195
|
+
!Array.isArray(prop['x-ui'])
|
|
2196
|
+
? prop['x-ui']
|
|
2197
|
+
: undefined;
|
|
2198
|
+
// Skip properties without x-ui metadata
|
|
2199
|
+
if (!ui || Object.keys(ui).length === 0) {
|
|
2200
|
+
continue;
|
|
2201
|
+
}
|
|
2202
|
+
const field = { name };
|
|
2203
|
+
// -------------------------------------------------------------------
|
|
2204
|
+
// Basic information
|
|
2205
|
+
// -------------------------------------------------------------------
|
|
2206
|
+
field.label = ui.label ?? prop.title ?? name;
|
|
2207
|
+
field.type = this.resolveFieldType(prop, ui);
|
|
2208
|
+
const numericFormat = this.resolveNumericFormatToken(prop, ui, field.type);
|
|
2209
|
+
if (numericFormat) {
|
|
2210
|
+
field.numericFormat = numericFormat;
|
|
2211
|
+
field.format = numericFormat;
|
|
2212
|
+
}
|
|
2213
|
+
const valuePresentation = this.parseValuePresentation(ui.valuePresentation);
|
|
2214
|
+
if (valuePresentation) {
|
|
2215
|
+
field.valuePresentation = valuePresentation;
|
|
2216
|
+
}
|
|
2217
|
+
const presentation = normalizeFieldPresentation(ui.presentation);
|
|
2218
|
+
if (Object.keys(presentation).length > 0) {
|
|
2219
|
+
field.presentation = presentation;
|
|
2220
|
+
}
|
|
2221
|
+
if (ui.controlType) {
|
|
2222
|
+
field.controlType = ui.controlType;
|
|
2223
|
+
}
|
|
2224
|
+
if (field.controlType === 'array' || ui.array !== undefined) {
|
|
2225
|
+
const arrayConfig = this.normalizeArrayConfig(ui, prop, rootSchema);
|
|
2226
|
+
if (arrayConfig) {
|
|
2227
|
+
field.array = arrayConfig;
|
|
2228
|
+
if (arrayConfig.collectionValidation) {
|
|
2229
|
+
field.collectionValidation = arrayConfig.collectionValidation;
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
if (ui.placeholder !== undefined) {
|
|
2234
|
+
field.placeholder = String(ui.placeholder);
|
|
2235
|
+
}
|
|
2236
|
+
if (ui.mode !== undefined) {
|
|
2237
|
+
field.mode = ui.mode;
|
|
2238
|
+
}
|
|
2239
|
+
if (ui.description || prop.description) {
|
|
2240
|
+
field.description = ui.description ?? prop.description;
|
|
2241
|
+
}
|
|
2242
|
+
if (ui.order !== undefined) {
|
|
2243
|
+
field.order = Number(ui.order);
|
|
2244
|
+
}
|
|
2245
|
+
if (ui.group !== undefined) {
|
|
2246
|
+
field.group = ui.group;
|
|
2247
|
+
}
|
|
2248
|
+
if (ui.selectionMode !== undefined) {
|
|
2249
|
+
field.selectionMode = String(ui.selectionMode);
|
|
2250
|
+
}
|
|
2251
|
+
if (ui.variant !== undefined) {
|
|
2252
|
+
field.variant = String(ui.variant);
|
|
2253
|
+
}
|
|
2254
|
+
if (ui.density !== undefined) {
|
|
2255
|
+
field.density = String(ui.density);
|
|
2256
|
+
}
|
|
2257
|
+
if (ui.layout !== undefined) {
|
|
2258
|
+
field.layout = String(ui.layout);
|
|
2259
|
+
}
|
|
2260
|
+
if (ui.isFlex !== undefined) {
|
|
2261
|
+
field.isFlex = this.parseBoolean(ui.isFlex);
|
|
2262
|
+
}
|
|
2263
|
+
if (ui.displayOrientation !== undefined) {
|
|
2264
|
+
field.displayOrientation = String(ui.displayOrientation);
|
|
2265
|
+
}
|
|
2266
|
+
// -------------------------------------------------------------------
|
|
2267
|
+
// Behaviour and validation
|
|
2268
|
+
// -------------------------------------------------------------------
|
|
2269
|
+
if (ui.readOnly !== undefined) {
|
|
2270
|
+
field.readOnly = this.parseBoolean(ui.readOnly);
|
|
2271
|
+
}
|
|
2272
|
+
if (ui.disabled !== undefined) {
|
|
2273
|
+
field.disabled = this.parseBoolean(ui.disabled);
|
|
2274
|
+
}
|
|
2275
|
+
if (ui.editable !== undefined) {
|
|
2276
|
+
field.editable = this.parseBoolean(ui.editable);
|
|
2277
|
+
}
|
|
2278
|
+
const fieldAccess = normalizeFieldAccessMetadata(ui.fieldAccess);
|
|
2279
|
+
if (fieldAccess) {
|
|
2280
|
+
field.fieldAccess = fieldAccess;
|
|
2281
|
+
}
|
|
2282
|
+
if (ui.inlineEditing !== undefined) {
|
|
2283
|
+
field.inlineEditing = this.parseBoolean(ui.inlineEditing);
|
|
2284
|
+
}
|
|
2285
|
+
if (ui.defaultValue !== undefined) {
|
|
2286
|
+
field.defaultValue = ui.defaultValue;
|
|
2287
|
+
}
|
|
2288
|
+
if (ui.validationMode !== undefined) {
|
|
2289
|
+
field.validationMode = String(ui.validationMode);
|
|
2290
|
+
}
|
|
2291
|
+
if (ui.unique !== undefined) {
|
|
2292
|
+
field.unique = this.parseBoolean(ui.unique);
|
|
2293
|
+
}
|
|
2294
|
+
if (ui.mask !== undefined) {
|
|
2295
|
+
field.mask = String(ui.mask);
|
|
2296
|
+
}
|
|
2297
|
+
if (ui.sortable !== undefined) {
|
|
2298
|
+
field.sortable = this.parseBoolean(ui.sortable);
|
|
2299
|
+
}
|
|
2300
|
+
if (ui.viewOnlyStyle !== undefined) {
|
|
2301
|
+
field.viewOnlyStyle = String(ui.viewOnlyStyle);
|
|
2302
|
+
}
|
|
2303
|
+
Object.assign(field, this.parseValidators(ui, name));
|
|
2304
|
+
// Required membership belongs to the canonical object schema. UI hints
|
|
2305
|
+
// can add validation but cannot make a required request field optional.
|
|
2306
|
+
if (requiredNames.has(name)) {
|
|
2307
|
+
field.required = true;
|
|
2308
|
+
}
|
|
2309
|
+
const condDisplay = this.parseJsonLogicExpression(ui.conditionalDisplay, name, 'x-ui.conditionalDisplay');
|
|
2310
|
+
if (condDisplay !== undefined) {
|
|
2311
|
+
field.conditionalDisplay = condDisplay;
|
|
2312
|
+
}
|
|
2313
|
+
const condRequired = this.parseJsonLogicExpression(ui.conditionalRequired, name, 'x-ui.conditionalRequired');
|
|
2314
|
+
if (condRequired !== undefined) {
|
|
2315
|
+
field.conditionalRequired = condRequired;
|
|
2316
|
+
}
|
|
2317
|
+
const transformFn = this.parseTransformFunction(ui.transformValueFunction);
|
|
2318
|
+
if (transformFn) {
|
|
2319
|
+
field.transformValueFunction = transformFn;
|
|
2320
|
+
}
|
|
2321
|
+
// -------------------------------------------------------------------
|
|
2322
|
+
// Layout and presentation
|
|
2323
|
+
// -------------------------------------------------------------------
|
|
2324
|
+
// Visibility flags (x-ui)
|
|
2325
|
+
if (ui.hidden !== undefined) {
|
|
2326
|
+
field.hidden = this.parseBoolean(ui.hidden);
|
|
2327
|
+
}
|
|
2328
|
+
if (ui.tableHidden !== undefined) {
|
|
2329
|
+
field.tableHidden = this.parseBoolean(ui.tableHidden);
|
|
2330
|
+
}
|
|
2331
|
+
if (ui.formHidden !== undefined) {
|
|
2332
|
+
field.formHidden = this.parseBoolean(ui.formHidden);
|
|
2333
|
+
}
|
|
2334
|
+
if (ui.width !== undefined) {
|
|
2335
|
+
field.width = ui.width;
|
|
2336
|
+
}
|
|
2337
|
+
if (ui.helpText !== undefined) {
|
|
2338
|
+
field.helpText = String(ui.helpText);
|
|
2339
|
+
}
|
|
2340
|
+
if (ui.tooltip !== undefined) {
|
|
2341
|
+
field.tooltip = String(ui.tooltip);
|
|
2342
|
+
}
|
|
2343
|
+
if (ui.hint !== undefined) {
|
|
2344
|
+
field.hint = ui.hint;
|
|
2345
|
+
}
|
|
2346
|
+
if (ui.hiddenCondition !== undefined) {
|
|
2347
|
+
const hiddenCondition = this.parseJsonLogicExpression(ui.hiddenCondition, name, 'x-ui.hiddenCondition');
|
|
2348
|
+
if (hiddenCondition !== undefined) {
|
|
2349
|
+
field.hiddenCondition = hiddenCondition;
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
if (ui.icon !== undefined) {
|
|
2353
|
+
field.icon = ui.icon;
|
|
2354
|
+
}
|
|
2355
|
+
if (ui.iconPosition !== undefined) {
|
|
2356
|
+
field.iconPosition = ui.iconPosition;
|
|
2357
|
+
}
|
|
2358
|
+
if (ui.iconSize !== undefined) {
|
|
2359
|
+
field.iconSize = ui.iconSize;
|
|
2360
|
+
}
|
|
2361
|
+
if (ui.iconColor !== undefined) {
|
|
2362
|
+
field.iconColor = String(ui.iconColor);
|
|
2363
|
+
}
|
|
2364
|
+
if (ui.iconClass !== undefined) {
|
|
2365
|
+
field.iconClass = String(ui.iconClass);
|
|
2366
|
+
}
|
|
2367
|
+
if (ui.iconStyle !== undefined) {
|
|
2368
|
+
field.iconStyle = String(ui.iconStyle);
|
|
2369
|
+
}
|
|
2370
|
+
if (ui.iconFontSize !== undefined) {
|
|
2371
|
+
field.iconFontSize = ui.iconFontSize;
|
|
2372
|
+
}
|
|
2373
|
+
if (ui.tooltipOnHover !== undefined) {
|
|
2374
|
+
field.tooltipOnHover = ui.tooltipOnHover;
|
|
2375
|
+
}
|
|
2376
|
+
if (ui.clearButton !== undefined) {
|
|
2377
|
+
if (typeof ui.clearButton === 'boolean') {
|
|
2378
|
+
field.clearButton = { enabled: this.parseBoolean(ui.clearButton) };
|
|
2379
|
+
}
|
|
2380
|
+
else if (typeof ui.clearButton === 'object') {
|
|
2381
|
+
field.clearButton = { ...ui.clearButton };
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
if (ui['clearButton.enabled'] !== undefined ||
|
|
2385
|
+
ui['clearButton.icon'] !== undefined ||
|
|
2386
|
+
ui['clearButton.iconColor'] !== undefined ||
|
|
2387
|
+
ui['clearButton.tooltip'] !== undefined ||
|
|
2388
|
+
ui['clearButton.ariaLabel'] !== undefined ||
|
|
2389
|
+
ui['clearButton.showOnlyWhenFilled'] !== undefined) {
|
|
2390
|
+
const clearButton = field.clearButton ?? {};
|
|
2391
|
+
if (ui['clearButton.enabled'] !== undefined) {
|
|
2392
|
+
clearButton.enabled = this.parseBoolean(ui['clearButton.enabled']);
|
|
2393
|
+
}
|
|
2394
|
+
if (ui['clearButton.icon'] !== undefined) {
|
|
2395
|
+
clearButton.icon = String(ui['clearButton.icon']);
|
|
2396
|
+
}
|
|
2397
|
+
if (ui['clearButton.iconColor'] !== undefined) {
|
|
2398
|
+
clearButton.iconColor = String(ui['clearButton.iconColor']);
|
|
2399
|
+
}
|
|
2400
|
+
if (ui['clearButton.tooltip'] !== undefined) {
|
|
2401
|
+
clearButton.tooltip = String(ui['clearButton.tooltip']);
|
|
2402
|
+
}
|
|
2403
|
+
if (ui['clearButton.ariaLabel'] !== undefined) {
|
|
2404
|
+
clearButton.ariaLabel = String(ui['clearButton.ariaLabel']);
|
|
2405
|
+
}
|
|
2406
|
+
if (ui['clearButton.showOnlyWhenFilled'] !== undefined) {
|
|
2407
|
+
clearButton.showOnlyWhenFilled = this.parseBoolean(ui['clearButton.showOnlyWhenFilled']);
|
|
2408
|
+
}
|
|
2409
|
+
field.clearButton = clearButton;
|
|
2410
|
+
}
|
|
2411
|
+
// -------------------------------------------------------------------
|
|
2412
|
+
// Options and dependencies
|
|
2413
|
+
// -------------------------------------------------------------------
|
|
2414
|
+
if (ui.options !== undefined) {
|
|
2415
|
+
field.options = this.parseOptions(ui.options);
|
|
2416
|
+
}
|
|
2417
|
+
if (ui.optionGroups !== undefined) {
|
|
2418
|
+
field.optionGroups = this.parseOptions(ui.optionGroups);
|
|
2419
|
+
}
|
|
2420
|
+
if (ui.disabledOptions !== undefined) {
|
|
2421
|
+
field.disabledOptions = this.parseStringArray(ui.disabledOptions);
|
|
2422
|
+
}
|
|
2423
|
+
if (ui.endpoint !== undefined) {
|
|
2424
|
+
field.endpoint = ui.endpoint;
|
|
2425
|
+
}
|
|
2426
|
+
if (ui.resourcePath !== undefined) {
|
|
2427
|
+
field.resourcePath = ui.resourcePath;
|
|
2428
|
+
}
|
|
2429
|
+
if (ui.optionSource !== undefined) {
|
|
2430
|
+
const optionSource = this.parseOptionSource(ui.optionSource);
|
|
2431
|
+
if (optionSource) {
|
|
2432
|
+
// Java metadata publishes cascade context beside optionSource. Keep the
|
|
2433
|
+
// runtime contract canonical by materializing it inside optionSource.
|
|
2434
|
+
const dependsOn = this.parseNonBlankStringArray(ui.dependsOn);
|
|
2435
|
+
if (!optionSource.dependsOn?.length && dependsOn.length) {
|
|
2436
|
+
optionSource.dependsOn = dependsOn;
|
|
2437
|
+
}
|
|
2438
|
+
const dependencyFilterMap = this.parseStringRecord(ui.dependencyFilterMap);
|
|
2439
|
+
if (!optionSource.dependencyFilterMap &&
|
|
2440
|
+
dependencyFilterMap) {
|
|
2441
|
+
optionSource.dependencyFilterMap = dependencyFilterMap;
|
|
2442
|
+
}
|
|
2443
|
+
field.optionSource = optionSource;
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
if (ui.valueField !== undefined) {
|
|
2447
|
+
field.valueField = ui.valueField;
|
|
2448
|
+
}
|
|
2449
|
+
if (ui.displayField !== undefined) {
|
|
2450
|
+
field.displayField = ui.displayField;
|
|
2451
|
+
}
|
|
2452
|
+
if (ui.filterField !== undefined) {
|
|
2453
|
+
field.filterField = ui.filterField;
|
|
2454
|
+
}
|
|
2455
|
+
if (ui.multiple !== undefined) {
|
|
2456
|
+
field.multiple = this.parseBoolean(ui.multiple);
|
|
2457
|
+
}
|
|
2458
|
+
if (ui.filterable !== undefined) {
|
|
2459
|
+
field.filterable = this.parseBoolean(ui.filterable);
|
|
2460
|
+
}
|
|
2461
|
+
if (ui.emptyOptionText !== undefined) {
|
|
2462
|
+
field.emptyOptionText = ui.emptyOptionText;
|
|
2463
|
+
}
|
|
2464
|
+
if (ui.filter !== undefined) {
|
|
2465
|
+
field.filter = ui.filter;
|
|
2466
|
+
}
|
|
2467
|
+
if (ui.dialog !== undefined) {
|
|
2468
|
+
const dialog = this.parseLookupDialog(ui.dialog);
|
|
2469
|
+
if (dialog) {
|
|
2470
|
+
field.dialog = dialog;
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
if (ui.detailActionLabel !== undefined) {
|
|
2474
|
+
const detailActionLabel = String(ui.detailActionLabel).trim();
|
|
2475
|
+
if (detailActionLabel) {
|
|
2476
|
+
field.detailActionLabel = detailActionLabel;
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
if (ui.copyCodeActionLabel !== undefined) {
|
|
2480
|
+
const copyCodeActionLabel = String(ui.copyCodeActionLabel).trim();
|
|
2481
|
+
if (copyCodeActionLabel) {
|
|
2482
|
+
field.copyCodeActionLabel = copyCodeActionLabel;
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
if (ui.copyIdActionLabel !== undefined) {
|
|
2486
|
+
const copyIdActionLabel = String(ui.copyIdActionLabel).trim();
|
|
2487
|
+
if (copyIdActionLabel) {
|
|
2488
|
+
field.copyIdActionLabel = copyIdActionLabel;
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
if (ui.createActionLabel !== undefined) {
|
|
2492
|
+
const createActionLabel = String(ui.createActionLabel).trim();
|
|
2493
|
+
if (createActionLabel) {
|
|
2494
|
+
field.createActionLabel = createActionLabel;
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
if (ui.actions && typeof ui.actions === 'object' && !Array.isArray(ui.actions)) {
|
|
2498
|
+
const actions = {};
|
|
2499
|
+
if (ui.actions.showDetail !== undefined) {
|
|
2500
|
+
actions.showDetail = this.parseBoolean(ui.actions.showDetail);
|
|
2501
|
+
}
|
|
2502
|
+
if (ui.actions.showCopyCode !== undefined) {
|
|
2503
|
+
actions.showCopyCode = this.parseBoolean(ui.actions.showCopyCode);
|
|
2504
|
+
}
|
|
2505
|
+
if (ui.actions.showCopyId !== undefined) {
|
|
2506
|
+
actions.showCopyId = this.parseBoolean(ui.actions.showCopyId);
|
|
2507
|
+
}
|
|
2508
|
+
if (ui.actions.showCreate !== undefined) {
|
|
2509
|
+
actions.showCreate = this.parseBoolean(ui.actions.showCreate);
|
|
2510
|
+
}
|
|
2511
|
+
if (Object.keys(actions).length) {
|
|
2512
|
+
field.actions = actions;
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
if (ui.filterOptions !== undefined) {
|
|
2516
|
+
field.filterOptions = Array.isArray(ui.filterOptions)
|
|
2517
|
+
? [...ui.filterOptions]
|
|
2518
|
+
: this.parseStringArray(ui.filterOptions);
|
|
2519
|
+
}
|
|
2520
|
+
if (ui.dependentField !== undefined) {
|
|
2521
|
+
field.dependentField = ui.dependentField;
|
|
2522
|
+
}
|
|
2523
|
+
if (ui.resetOnDependentChange !== undefined) {
|
|
2524
|
+
field.resetOnDependentChange = this.parseBoolean(ui.resetOnDependentChange);
|
|
2525
|
+
}
|
|
2526
|
+
if (ui.debounceTime !== undefined) {
|
|
2527
|
+
field.debounceTime = Number(ui.debounceTime);
|
|
2528
|
+
}
|
|
2529
|
+
if (ui.numericStep !== undefined) {
|
|
2530
|
+
field.numericStep = Number(ui.numericStep);
|
|
2531
|
+
}
|
|
2532
|
+
if (ui.numericMin !== undefined) {
|
|
2533
|
+
field.numericMin = Number(ui.numericMin);
|
|
2534
|
+
}
|
|
2535
|
+
if (ui.numericMax !== undefined) {
|
|
2536
|
+
field.numericMax = Number(ui.numericMax);
|
|
2537
|
+
}
|
|
2538
|
+
if (ui.numericMaxLength !== undefined) {
|
|
2539
|
+
field.numericMaxLength = Number(ui.numericMaxLength);
|
|
2540
|
+
}
|
|
2541
|
+
if (ui.buttons !== undefined) {
|
|
2542
|
+
field.buttons = this.parseButtons(ui.buttons);
|
|
2543
|
+
}
|
|
2544
|
+
fields.push(field);
|
|
2545
|
+
}
|
|
2546
|
+
// Order by the optional 'order' property when present
|
|
2547
|
+
fields.sort((a, b) => {
|
|
2548
|
+
const orderA = a.order ?? Number.MAX_SAFE_INTEGER;
|
|
2549
|
+
const orderB = b.order ?? Number.MAX_SAFE_INTEGER;
|
|
2550
|
+
return orderA - orderB;
|
|
2551
|
+
});
|
|
2552
|
+
return fields;
|
|
2553
|
+
}
|
|
2554
|
+
resolveFieldType(prop, ui) {
|
|
2555
|
+
if (ui?.type !== undefined && ui?.type !== null && String(ui.type).trim() !== '') {
|
|
2556
|
+
return String(ui.type);
|
|
2557
|
+
}
|
|
2558
|
+
if (String(ui?.controlType ?? '').trim() === 'array') {
|
|
2559
|
+
return 'array';
|
|
2560
|
+
}
|
|
2561
|
+
if (prop?.type) {
|
|
2562
|
+
return String(prop.type);
|
|
2563
|
+
}
|
|
2564
|
+
const variants = [
|
|
2565
|
+
...(Array.isArray(prop?.oneOf) ? prop.oneOf : []),
|
|
2566
|
+
...(Array.isArray(prop?.anyOf) ? prop.anyOf : []),
|
|
2567
|
+
].filter((variant) => variant && typeof variant === 'object');
|
|
2568
|
+
const nonNullTypes = Array.from(new Set(variants
|
|
2569
|
+
.map((variant) => (typeof variant.type === 'string' ? variant.type.trim() : ''))
|
|
2570
|
+
.filter((type) => !!type && type !== 'null')));
|
|
2571
|
+
if (nonNullTypes.length === 1) {
|
|
2572
|
+
return nonNullTypes[0];
|
|
2573
|
+
}
|
|
2574
|
+
// Compatibilidade para schemas de range publicados como oneOf(array|object).
|
|
2575
|
+
const controlToken = String(ui?.controlType || '').trim().toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
2576
|
+
const isRangeControl = ['daterange', 'datetimerange', 'rangeslider', 'pricerange'].includes(controlToken);
|
|
2577
|
+
if (isRangeControl && nonNullTypes.includes('array')) {
|
|
2578
|
+
return 'array';
|
|
2579
|
+
}
|
|
2580
|
+
return undefined;
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
/** Normalizes the exact schema/root object using the canonical Core calculation.
|
|
2585
|
+
* The caller must admit source, content and execution before invoking this function:
|
|
2586
|
+
* declared function expressions may execute during field construction.
|
|
2587
|
+
* This performs neither strict JSON parsing nor provenance/schema admission.
|
|
2588
|
+
*/
|
|
2589
|
+
function normalizeSchemaFields(schema) {
|
|
2590
|
+
return new SchemaNormalizationKernel().normalizeSchema(schema);
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
function isObject(value) {
|
|
2594
|
+
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
2595
|
+
}
|
|
2596
|
+
/**
|
|
2597
|
+
* Deep merge that prefers values from `right` when defined.
|
|
2598
|
+
* - Objects are merged recursively
|
|
2599
|
+
* - Arrays are replaced by `right` when defined
|
|
2600
|
+
* - Primitive `undefined` on right keeps left's value
|
|
2601
|
+
*/
|
|
2602
|
+
function deepMerge(left, right) {
|
|
2603
|
+
if (right === undefined || right === null)
|
|
2604
|
+
return clone(left);
|
|
2605
|
+
if (Array.isArray(left)) {
|
|
2606
|
+
return (Array.isArray(right) ? right : clone(left));
|
|
2607
|
+
}
|
|
2608
|
+
if (!isObject(left) || !isObject(right)) {
|
|
2609
|
+
// Right wins when defined, else left
|
|
2610
|
+
return (right !== undefined ? right : left);
|
|
2611
|
+
}
|
|
2612
|
+
const out = { ...left };
|
|
2613
|
+
for (const key of Object.keys(right)) {
|
|
2614
|
+
const r = right[key];
|
|
2615
|
+
const l = left[key];
|
|
2616
|
+
if (r === undefined)
|
|
2617
|
+
continue; // keep left as-is
|
|
2618
|
+
if (Array.isArray(r)) {
|
|
2619
|
+
out[key] = r.map((v) => clone(v));
|
|
2620
|
+
}
|
|
2621
|
+
else if (isObject(r) && isObject(l)) {
|
|
2622
|
+
out[key] = deepMerge(l, r);
|
|
2623
|
+
}
|
|
2624
|
+
else {
|
|
2625
|
+
out[key] = clone(r);
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
return out;
|
|
2629
|
+
}
|
|
2630
|
+
/**
|
|
2631
|
+
* Fill only undefined properties in `target` with values from `defaults`.
|
|
2632
|
+
* - Objects are traversed recursively
|
|
2633
|
+
* - Arrays defaulting replaces when target is undefined/null
|
|
2634
|
+
*/
|
|
2635
|
+
function fillUndefined(target, defaults) {
|
|
2636
|
+
if (defaults === undefined || defaults === null)
|
|
2637
|
+
return clone(target);
|
|
2638
|
+
if (!isObject(target) || !isObject(defaults)) {
|
|
2639
|
+
return (target === undefined || target === null ? defaults : target);
|
|
2640
|
+
}
|
|
2641
|
+
const out = { ...target };
|
|
2642
|
+
for (const key of Object.keys(defaults)) {
|
|
2643
|
+
const d = defaults[key];
|
|
2644
|
+
const t = target[key];
|
|
2645
|
+
if (t === undefined || t === null) {
|
|
2646
|
+
out[key] = clone(d);
|
|
2647
|
+
}
|
|
2648
|
+
else if (isObject(t) && isObject(d)) {
|
|
2649
|
+
out[key] = fillUndefined(t, d);
|
|
2650
|
+
}
|
|
2651
|
+
else {
|
|
2652
|
+
out[key] = t;
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
return out;
|
|
2656
|
+
}
|
|
2657
|
+
function clone(v) {
|
|
2658
|
+
if (v == null || typeof v !== 'object')
|
|
2659
|
+
return v;
|
|
2660
|
+
try {
|
|
2661
|
+
return JSON.parse(JSON.stringify(v));
|
|
2662
|
+
}
|
|
2663
|
+
catch {
|
|
2664
|
+
return v;
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
function buildBaseColumnFromDef(def) {
|
|
2669
|
+
const type = resolveColumnTypeFromFieldDefinition(def);
|
|
2670
|
+
const format = type === 'string' ? resolveTextMaskFormatFromFieldDefinition(def) : undefined;
|
|
2671
|
+
const renderer = resolveRendererFromFieldPresentation(def);
|
|
2672
|
+
return {
|
|
2673
|
+
field: def.name,
|
|
2674
|
+
header: def.label || def.name,
|
|
2675
|
+
type,
|
|
2676
|
+
visible: def.tableHidden === true ? false : true,
|
|
2677
|
+
sortable: def.sortable !== false,
|
|
2678
|
+
filterable: def.filterable === true,
|
|
2679
|
+
resizable: true,
|
|
2680
|
+
order: typeof def.order === 'number' ? def.order : undefined,
|
|
2681
|
+
...(format ? { format } : {}),
|
|
2682
|
+
...(renderer ? { renderer } : {}),
|
|
2683
|
+
};
|
|
2684
|
+
}
|
|
2685
|
+
function applyLocalCustomizations(baseCol, localCol) {
|
|
2686
|
+
// Server-authoritative keys
|
|
2687
|
+
const result = {
|
|
2688
|
+
...baseCol,
|
|
2689
|
+
field: baseCol.field,
|
|
2690
|
+
header: baseCol.header, // label comes from server
|
|
2691
|
+
type: baseCol.type, // type comes from server (authoritative)
|
|
2692
|
+
sortable: baseCol.sortable,
|
|
2693
|
+
};
|
|
2694
|
+
// Merge presentation/customization from local, keeping user's preferences
|
|
2695
|
+
// and only cleaning conflicting ones if server type changed.
|
|
2696
|
+
const typeChanged = localCol.type && baseCol.type && localCol.type !== baseCol.type;
|
|
2697
|
+
result.width = localCol.width ?? baseCol.width;
|
|
2698
|
+
result.visible = localCol.visible ?? baseCol.visible;
|
|
2699
|
+
result.filterable = localCol.filterable ?? baseCol.filterable;
|
|
2700
|
+
result.resizable = localCol.resizable ?? baseCol.resizable;
|
|
2701
|
+
result.sticky = localCol.sticky ?? baseCol.sticky;
|
|
2702
|
+
result.align = localCol.align ?? baseCol.align;
|
|
2703
|
+
result.order = localCol.order ?? baseCol.order;
|
|
2704
|
+
result.sortField = localCol.sortField ?? baseCol.sortField;
|
|
2705
|
+
result.style = localCol.style ?? baseCol.style;
|
|
2706
|
+
result.headerStyle = localCol.headerStyle ?? baseCol.headerStyle;
|
|
2707
|
+
result.cssClass = localCol.cssClass ?? baseCol.cssClass;
|
|
2708
|
+
result.headerCssClass = localCol.headerCssClass ?? baseCol.headerCssClass;
|
|
2709
|
+
// If server type changed, drop potentially incompatible formatting/renderers.
|
|
2710
|
+
if (!typeChanged) {
|
|
2711
|
+
result.format = localCol.format ?? baseCol.format;
|
|
2712
|
+
result.valueMapping = localCol.valueMapping ?? baseCol.valueMapping;
|
|
2713
|
+
result.renderer = localCol.renderer ?? baseCol.renderer;
|
|
2714
|
+
}
|
|
2715
|
+
else {
|
|
2716
|
+
// Drop local presentation that may be incompatible with the new server type,
|
|
2717
|
+
// but keep canonical server presentation for the resolved type.
|
|
2718
|
+
result.format = baseCol.format;
|
|
2719
|
+
result.valueMapping = baseCol.valueMapping;
|
|
2720
|
+
result.renderer = undefined;
|
|
2721
|
+
}
|
|
2722
|
+
return result;
|
|
2723
|
+
}
|
|
2724
|
+
function reconcileTableConfig(layout, serverDefs) {
|
|
2725
|
+
const local = layout || { columns: [] };
|
|
2726
|
+
const localByField = new Map((local.columns || []).map((c) => [c.field, c]));
|
|
2727
|
+
const baseColumns = serverDefs.map(buildBaseColumnFromDef);
|
|
2728
|
+
const mergedColumns = baseColumns.map((base) => {
|
|
2729
|
+
const localCol = localByField.get(base.field);
|
|
2730
|
+
return localCol ? applyLocalCustomizations(base, localCol) : base;
|
|
2731
|
+
});
|
|
2732
|
+
const serverFields = new Set(serverDefs.map((d) => d.name));
|
|
2733
|
+
const orphans = (local.columns || []).filter((c) => !serverFields.has(c.field));
|
|
2734
|
+
const hiddenOrphans = orphans.map((c) => ({ ...c, visible: false }));
|
|
2735
|
+
const next = {
|
|
2736
|
+
...local,
|
|
2737
|
+
columns: [...mergedColumns, ...hiddenOrphans],
|
|
2738
|
+
meta: {
|
|
2739
|
+
...local.meta,
|
|
2740
|
+
updatedAt: new Date().toISOString(),
|
|
2741
|
+
},
|
|
2742
|
+
};
|
|
2743
|
+
return next;
|
|
2744
|
+
}
|
|
2745
|
+
function resolveColumnTypeFromFieldDefinition(def, fallback) {
|
|
2746
|
+
const semanticType = resolveSemanticColumnTypeFromFieldDefinition(def);
|
|
2747
|
+
if (resolveTextMaskFormatFromFieldDefinition(def) &&
|
|
2748
|
+
shouldUseTextMaskForColumn(def, semanticType)) {
|
|
2749
|
+
return 'string';
|
|
2750
|
+
}
|
|
2751
|
+
if (semanticType)
|
|
2752
|
+
return semanticType;
|
|
2753
|
+
if (fallback)
|
|
2754
|
+
return fallback;
|
|
2755
|
+
return 'string';
|
|
2756
|
+
}
|
|
2757
|
+
function resolveSemanticColumnTypeFromFieldDefinition(def) {
|
|
2758
|
+
const controlType = normalizePresentationToken(def.controlType);
|
|
2759
|
+
const numericFormat = normalizePresentationToken(def.numericFormat ?? def.format);
|
|
2760
|
+
const valuePresentationType = normalizePresentationToken(def.valuePresentation?.type);
|
|
2761
|
+
const dataType = normalizePresentationToken(def.type);
|
|
2762
|
+
if (valuePresentationType === 'currency')
|
|
2763
|
+
return 'currency';
|
|
2764
|
+
if (valuePresentationType === 'percent' || valuePresentationType === 'percentage')
|
|
2765
|
+
return 'percentage';
|
|
2766
|
+
if (valuePresentationType === 'date' || valuePresentationType === 'datetime' || valuePresentationType === 'time')
|
|
2767
|
+
return 'date';
|
|
2768
|
+
if (valuePresentationType === 'boolean')
|
|
2769
|
+
return 'boolean';
|
|
2770
|
+
if (valuePresentationType === 'number')
|
|
2771
|
+
return 'number';
|
|
2772
|
+
if (valuePresentationType === 'string')
|
|
2773
|
+
return 'string';
|
|
2774
|
+
if (numericFormat === 'currency' || controlType === 'currency')
|
|
2775
|
+
return 'currency';
|
|
2776
|
+
if (numericFormat === 'percent' || numericFormat === 'percentage')
|
|
2777
|
+
return 'percentage';
|
|
2778
|
+
if (numericFormat === 'date' || numericFormat === 'datetime')
|
|
2779
|
+
return 'date';
|
|
2780
|
+
if (controlType === 'numerictextbox' || controlType === 'inlinenumber')
|
|
2781
|
+
return 'number';
|
|
2782
|
+
if (controlType === 'checkbox' || controlType === 'toggle' || controlType === 'inlinetoggle')
|
|
2783
|
+
return 'boolean';
|
|
2784
|
+
if (controlType.includes('date') || controlType === 'time' || controlType === 'timepicker')
|
|
2785
|
+
return 'date';
|
|
2786
|
+
if (isColumnType(dataType))
|
|
2787
|
+
return dataType;
|
|
2788
|
+
if (dataType === 'text' || dataType === 'email' || dataType === 'url' || dataType === 'password')
|
|
2789
|
+
return 'string';
|
|
2790
|
+
if (dataType.includes('date') || dataType === 'time')
|
|
2791
|
+
return 'date';
|
|
2792
|
+
if (dataType.includes('bool'))
|
|
2793
|
+
return 'boolean';
|
|
2794
|
+
if (dataType.includes('number') ||
|
|
2795
|
+
dataType.includes('int') ||
|
|
2796
|
+
dataType.includes('decimal') ||
|
|
2797
|
+
dataType.includes('double') ||
|
|
2798
|
+
dataType.includes('float')) {
|
|
2799
|
+
return 'number';
|
|
2800
|
+
}
|
|
2801
|
+
if (isTextualControlType(controlType))
|
|
2802
|
+
return 'string';
|
|
2803
|
+
return undefined;
|
|
2804
|
+
}
|
|
2805
|
+
function shouldUseTextMaskForColumn(def, semanticType) {
|
|
2806
|
+
if (!semanticType || semanticType === 'string')
|
|
2807
|
+
return true;
|
|
2808
|
+
const controlType = normalizePresentationToken(def.controlType);
|
|
2809
|
+
if (isTextualControlType(controlType))
|
|
2810
|
+
return true;
|
|
2811
|
+
if (['date', 'boolean', 'currency', 'percentage', 'custom'].includes(semanticType)) {
|
|
2812
|
+
return false;
|
|
2813
|
+
}
|
|
2814
|
+
const valuePresentationType = normalizePresentationToken(def.valuePresentation?.type);
|
|
2815
|
+
const numericFormat = normalizePresentationToken(def.numericFormat ?? def.format);
|
|
2816
|
+
return !valuePresentationType && !numericFormat;
|
|
2817
|
+
}
|
|
2818
|
+
function resolveTextMaskFormatFromFieldDefinition(def) {
|
|
2819
|
+
return resolveTextMaskFormat(def.mask ??
|
|
2820
|
+
def.displayMask ??
|
|
2821
|
+
def.inputMask ??
|
|
2822
|
+
def.extraProperties?.mask);
|
|
2823
|
+
}
|
|
2824
|
+
function resolveTextMaskFormat(value) {
|
|
2825
|
+
if (typeof value !== 'string')
|
|
2826
|
+
return undefined;
|
|
2827
|
+
const format = value.trim();
|
|
2828
|
+
if (!format)
|
|
2829
|
+
return undefined;
|
|
2830
|
+
const hasMaskTokens = /[0#9Xx]/.test(format);
|
|
2831
|
+
const hasInvalidMaskLetters = /[A-WYZa-wyz]/.test(format);
|
|
2832
|
+
return hasMaskTokens && !hasInvalidMaskLetters ? format : undefined;
|
|
2833
|
+
}
|
|
2834
|
+
function resolveRendererFromFieldPresentation(def) {
|
|
2835
|
+
const presentation = def.presentation;
|
|
2836
|
+
if (!presentation || typeof presentation !== 'object')
|
|
2837
|
+
return undefined;
|
|
2838
|
+
const presenter = normalizePresentationToken(presentation.presenter ?? presentation.variant);
|
|
2839
|
+
const color = resolveSemanticPresentationColor(presentation.tone);
|
|
2840
|
+
const variant = resolveSemanticPresentationVariant(presentation.appearance);
|
|
2841
|
+
const icon = firstNonEmptyString(presentation.icon);
|
|
2842
|
+
const tooltipText = firstNonEmptyString(presentation.tooltip);
|
|
2843
|
+
const tooltip = tooltipText ? { text: tooltipText } : undefined;
|
|
2844
|
+
if (presenter === 'chip') {
|
|
2845
|
+
return {
|
|
2846
|
+
type: 'chip',
|
|
2847
|
+
chip: {
|
|
2848
|
+
textField: def.name,
|
|
2849
|
+
...(color ? { color } : {}),
|
|
2850
|
+
...(variant ? { variant } : {}),
|
|
2851
|
+
...(icon ? { icon } : {}),
|
|
2852
|
+
...(tooltip ? { tooltip } : {}),
|
|
2853
|
+
},
|
|
2854
|
+
};
|
|
2855
|
+
}
|
|
2856
|
+
if (presenter === 'badge' || presenter === 'status') {
|
|
2857
|
+
return {
|
|
2858
|
+
type: 'badge',
|
|
2859
|
+
badge: {
|
|
2860
|
+
textField: def.name,
|
|
2861
|
+
...(color ? { color } : {}),
|
|
2862
|
+
...(variant ? { variant } : {}),
|
|
2863
|
+
...(icon ? { icon } : {}),
|
|
2864
|
+
...(tooltip ? { tooltip } : {}),
|
|
2865
|
+
},
|
|
2866
|
+
};
|
|
2867
|
+
}
|
|
2868
|
+
if (presenter === 'iconvalue') {
|
|
2869
|
+
const rendererIcon = icon || firstNonEmptyString(def.icon);
|
|
2870
|
+
const affixes = resolveSemanticPresentationAffixes(presentation);
|
|
2871
|
+
const hasAffixes = !!(affixes.prefix || affixes.suffix);
|
|
2872
|
+
if (!rendererIcon && !hasAffixes)
|
|
2873
|
+
return undefined;
|
|
2874
|
+
return {
|
|
2875
|
+
type: 'icon',
|
|
2876
|
+
icon: {
|
|
2877
|
+
...(rendererIcon && !hasAffixes ? { name: rendererIcon } : {}),
|
|
2878
|
+
textField: def.name,
|
|
2879
|
+
...(tooltipText ? { ariaLabel: tooltipText } : {}),
|
|
2880
|
+
...affixes,
|
|
2881
|
+
},
|
|
2882
|
+
};
|
|
2883
|
+
}
|
|
2884
|
+
return undefined;
|
|
2885
|
+
}
|
|
2886
|
+
function resolveSemanticPresentationColor(tone) {
|
|
2887
|
+
const normalized = normalizePresentationToken(tone);
|
|
2888
|
+
if (!normalized || normalized === 'neutral')
|
|
2889
|
+
return undefined;
|
|
2890
|
+
return normalized === 'danger' ? 'warn' : normalized;
|
|
2891
|
+
}
|
|
2892
|
+
function resolveSemanticPresentationVariant(appearance) {
|
|
2893
|
+
const normalized = normalizePresentationToken(appearance);
|
|
2894
|
+
return normalized === 'filled' || normalized === 'outlined' || normalized === 'soft'
|
|
2895
|
+
? normalized
|
|
2896
|
+
: undefined;
|
|
2897
|
+
}
|
|
2898
|
+
function resolveSemanticPresentationAffixes(presentation) {
|
|
2899
|
+
const prefix = firstNonEmptyString(presentation.prefix);
|
|
2900
|
+
const suffix = firstNonEmptyString(presentation.suffix);
|
|
2901
|
+
return {
|
|
2902
|
+
...(prefix ? { prefix } : {}),
|
|
2903
|
+
...(suffix ? { suffix } : {}),
|
|
2904
|
+
};
|
|
2905
|
+
}
|
|
2906
|
+
function firstNonEmptyString(value) {
|
|
2907
|
+
if (typeof value !== 'string')
|
|
2908
|
+
return undefined;
|
|
2909
|
+
const trimmed = value.trim();
|
|
2910
|
+
return trimmed ? trimmed : undefined;
|
|
2911
|
+
}
|
|
2912
|
+
function normalizePresentationToken(value) {
|
|
2913
|
+
return String(value ?? '').trim().toLowerCase().replace(/[\s_-]+/g, '');
|
|
2914
|
+
}
|
|
2915
|
+
function isColumnType(value) {
|
|
2916
|
+
return [
|
|
2917
|
+
'string',
|
|
2918
|
+
'number',
|
|
2919
|
+
'date',
|
|
2920
|
+
'boolean',
|
|
2921
|
+
'currency',
|
|
2922
|
+
'percentage',
|
|
2923
|
+
'custom',
|
|
2924
|
+
].includes(value);
|
|
2925
|
+
}
|
|
2926
|
+
function isTextualControlType(value) {
|
|
2927
|
+
return [
|
|
2928
|
+
'input',
|
|
2929
|
+
'inlineinput',
|
|
2930
|
+
'textarea',
|
|
2931
|
+
'search',
|
|
2932
|
+
'email',
|
|
2933
|
+
'url',
|
|
2934
|
+
'phone',
|
|
2935
|
+
'cpfcnpjinput',
|
|
2936
|
+
'password',
|
|
2937
|
+
].includes(value);
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
/** Owner-only schema calculation surface; admission must precede executable metadata. */
|
|
2941
|
+
|
|
2942
|
+
/**
|
|
2943
|
+
* Generated bundle index. Do not edit.
|
|
2944
|
+
*/
|
|
2945
|
+
|
|
2946
|
+
export { deepMerge, isPraxisPresentationVisualizationTableSafe, normalizeSchemaFields, resolveColumnTypeFromFieldDefinition, resolveTextMaskFormatFromFieldDefinition };
|