@praxisui/table 10.0.0-rc.4 → 10.0.0-rc.6
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/ai/component-registry.json +35 -4
- package/fesm2022/praxisui-table-authoring.mjs +1463 -0
- package/fesm2022/{praxisui-table-praxisui-table-DKvwb6v1.mjs → praxisui-table-praxisui-table-BIRFv2S8.mjs} +3916 -969
- package/fesm2022/{praxisui-table-table-agentic-authoring-turn-flow-BzjYuDvo.mjs → praxisui-table-table-agentic-authoring-turn-flow-DtOwXgFW.mjs} +1 -1
- package/fesm2022/{praxisui-table-table-ai.adapter-CPgpIGoi.mjs → praxisui-table-table-ai.adapter-25xtoiWq.mjs} +1 -1
- package/fesm2022/praxisui-table.mjs +2 -1
- package/package.json +14 -10
- package/types/praxisui-table-authoring.d.ts +104 -0
- package/types/praxisui-table.d.ts +386 -11
|
@@ -0,0 +1,1463 @@
|
|
|
1
|
+
import { validateRichContentDocument, isSafeRichContentClassName } from '@praxisui/rich-content';
|
|
2
|
+
import { resolveColumnTypeFromFieldDefinition, deepMerge, isPraxisPresentationVisualizationTableSafe, resolveTextMaskFormatFromFieldDefinition } from '@praxisui/core/schema';
|
|
3
|
+
|
|
4
|
+
const ORIGIN = '__praxisAuthoredDisabled';
|
|
5
|
+
const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
6
|
+
const isAction = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
7
|
+
const hasRuntimeState = (action) => Object.keys(action).some(key => key.startsWith('__praxis'));
|
|
8
|
+
function authoredDisabled(action) {
|
|
9
|
+
const origin = action[ORIGIN];
|
|
10
|
+
if (isAction(origin) && typeof origin['present'] === 'boolean'
|
|
11
|
+
&& (!origin['present'] || typeof origin['value'] === 'boolean')) {
|
|
12
|
+
return origin;
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
present: typeof action['disabled'] === 'boolean',
|
|
16
|
+
value: typeof action['disabled'] === 'boolean' ? action['disabled'] : undefined,
|
|
17
|
+
// Old materializations do not carry enough evidence to remove a denial safely.
|
|
18
|
+
...(hasRuntimeState(action) && action['disabled'] === true ? { ambiguous: true } : {}),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Internal runtime bookkeeping. It survives clones but is never part of an authored action. */
|
|
22
|
+
function withActionAvailability(action, disabled, source = action) {
|
|
23
|
+
const origin = authoredDisabled(source);
|
|
24
|
+
return { ...action, disabled: origin.value === true || disabled, [ORIGIN]: origin };
|
|
25
|
+
}
|
|
26
|
+
function identity(action) {
|
|
27
|
+
if (typeof action['id'] === 'string' && action['id'].trim())
|
|
28
|
+
return `id:${action['id']}`;
|
|
29
|
+
if (typeof action['action'] === 'string' && action['action'].trim())
|
|
30
|
+
return `action:${action['action']}`;
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
/** Projects only canonical action slots, never arbitrary business payloads or callback values. */
|
|
34
|
+
function projectTableActionAuthoring(config, authored, report) {
|
|
35
|
+
function actions(value, original, path) {
|
|
36
|
+
if (!Array.isArray(value))
|
|
37
|
+
return value;
|
|
38
|
+
const originals = Array.isArray(original) ? original.filter(isAction) : [];
|
|
39
|
+
return value.map((item, index) => {
|
|
40
|
+
if (!isAction(item))
|
|
41
|
+
return item;
|
|
42
|
+
const key = identity(item);
|
|
43
|
+
const matches = key ? originals.filter(candidate => identity(candidate) === key) : [];
|
|
44
|
+
// No label matching, positional matching or guessing across duplicate identities.
|
|
45
|
+
const source = matches.length === 1 && value.filter(candidate => isAction(candidate) && identity(candidate) === key).length === 1
|
|
46
|
+
? matches[0] : undefined;
|
|
47
|
+
const result = { ...item };
|
|
48
|
+
if (hasRuntimeState(item)) {
|
|
49
|
+
const origin = source && !hasRuntimeState(source) ? authoredDisabled(source) : authoredDisabled(item);
|
|
50
|
+
if (origin.present)
|
|
51
|
+
result['disabled'] = origin.value;
|
|
52
|
+
else
|
|
53
|
+
delete result['disabled'];
|
|
54
|
+
if (origin.ambiguous)
|
|
55
|
+
report?.({
|
|
56
|
+
level: 'warning', code: 'table.action.disabled.origin.unknown',
|
|
57
|
+
message: 'Review the disabled setting: this legacy action contains runtime availability without its authored origin. The existing denial was preserved.',
|
|
58
|
+
path: `${path}[${index}].disabled`,
|
|
59
|
+
});
|
|
60
|
+
for (const name of Object.keys(result))
|
|
61
|
+
if (name.startsWith('__praxis'))
|
|
62
|
+
delete result[name];
|
|
63
|
+
}
|
|
64
|
+
if (hasOwn(item, 'children'))
|
|
65
|
+
result['children'] = actions(item['children'], source?.['children'], `${path}[${index}].children`);
|
|
66
|
+
return result;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const result = { ...config };
|
|
70
|
+
if (config?.toolbar)
|
|
71
|
+
result.toolbar = {
|
|
72
|
+
...config.toolbar,
|
|
73
|
+
...(hasOwn(config.toolbar, 'actions') ? { actions: actions(config.toolbar.actions, authored?.toolbar?.actions, 'config.toolbar.actions') } : {}),
|
|
74
|
+
};
|
|
75
|
+
if (config?.actions) {
|
|
76
|
+
result.actions = { ...config.actions };
|
|
77
|
+
for (const slot of ['row', 'bulk']) {
|
|
78
|
+
const group = config.actions[slot];
|
|
79
|
+
if (group)
|
|
80
|
+
result.actions[slot] = {
|
|
81
|
+
...group,
|
|
82
|
+
...(hasOwn(group, 'actions') ? { actions: actions(group.actions, authored?.actions?.[slot]?.actions, `config.actions.${slot}.actions`) } : {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
/** Internal adapter: project before canonical normalization discards runtime evidence. */
|
|
89
|
+
function projectTableEditorActionInput(raw, authored) {
|
|
90
|
+
if (!isAction(raw))
|
|
91
|
+
return raw;
|
|
92
|
+
if (raw['kind'] === 'praxis.table.editor' && isAction(raw['config'])) {
|
|
93
|
+
return { ...raw, config: projectTableActionAuthoring(raw['config'], authored) };
|
|
94
|
+
}
|
|
95
|
+
return projectTableActionAuthoring(raw, authored);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const FORBIDDEN_ENCODED_PATH_PART = /%(?:25)*(?:2e|2f|5c)/i;
|
|
99
|
+
const FORBIDDEN_RAW_PATH_PART = /\\/;
|
|
100
|
+
const FORBIDDEN_CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/;
|
|
101
|
+
const FORBIDDEN_DOT_SEGMENT = /(?:^|\/)\.{1,2}(?:\/|$)/;
|
|
102
|
+
const ABSOLUTE_OR_SCHEME = /^(?:\/\/|[a-z][a-z0-9+.-]*:|[a-z]:[\\/])/i;
|
|
103
|
+
function isTableDetailResourcePathTemplateSafe(value) {
|
|
104
|
+
const raw = String(value || '').trim();
|
|
105
|
+
const path = raw.split(/[?#]/, 1)[0];
|
|
106
|
+
return !!raw && !ABSOLUTE_OR_SCHEME.test(raw)
|
|
107
|
+
&& !FORBIDDEN_CONTROL_CHARACTER.test(raw)
|
|
108
|
+
&& !FORBIDDEN_ENCODED_PATH_PART.test(path)
|
|
109
|
+
&& !FORBIDDEN_RAW_PATH_PART.test(path)
|
|
110
|
+
&& !FORBIDDEN_DOT_SEGMENT.test(path);
|
|
111
|
+
}
|
|
112
|
+
function canonicalPath(value, baseHref, allowWildcard = false) {
|
|
113
|
+
const raw = String(value || '').trim();
|
|
114
|
+
if (!isTableDetailResourcePathTemplateSafe(raw))
|
|
115
|
+
return null;
|
|
116
|
+
let wildcardSentinel = '__PRAXIS_DETAIL_WILDCARD__';
|
|
117
|
+
while (raw.includes(wildcardSentinel)) {
|
|
118
|
+
wildcardSentinel += '_';
|
|
119
|
+
}
|
|
120
|
+
const prepared = allowWildcard ? raw.replace(/\*/g, wildcardSentinel) : raw;
|
|
121
|
+
try {
|
|
122
|
+
const parsed = new URL(prepared, baseHref);
|
|
123
|
+
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password)
|
|
124
|
+
return null;
|
|
125
|
+
const path = parsed.pathname
|
|
126
|
+
.replace(/^\/+/, '')
|
|
127
|
+
.replace(new RegExp(wildcardSentinel, 'g'), '*');
|
|
128
|
+
return { origin: parsed.origin, path, url: parsed.href };
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function wildcardToRegExp(pattern) {
|
|
135
|
+
let source = '^';
|
|
136
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
137
|
+
const character = pattern[index];
|
|
138
|
+
if (character === '*') {
|
|
139
|
+
if (pattern[index + 1] === '*') {
|
|
140
|
+
source += '.*';
|
|
141
|
+
index += 1;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
source += '[^/]+';
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
source += character.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return new RegExp(`${source}$`);
|
|
152
|
+
}
|
|
153
|
+
/** Resolves the URL exactly as the browser will, then applies same-origin and path policy. */
|
|
154
|
+
function resolveTableDetailResourcePath(value, allowList, baseHref, trustedOrigin) {
|
|
155
|
+
let base;
|
|
156
|
+
let trusted;
|
|
157
|
+
try {
|
|
158
|
+
base = new URL(baseHref);
|
|
159
|
+
trusted = new URL(trustedOrigin);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return { ok: false, reason: 'invalid' };
|
|
163
|
+
}
|
|
164
|
+
const candidate = canonicalPath(value, base.href);
|
|
165
|
+
if (!candidate)
|
|
166
|
+
return { ok: false, reason: 'invalid' };
|
|
167
|
+
if (base.origin !== trusted.origin || candidate.origin !== trusted.origin) {
|
|
168
|
+
return { ok: false, reason: 'origin' };
|
|
169
|
+
}
|
|
170
|
+
const allowed = allowList.some((entry) => {
|
|
171
|
+
const pattern = canonicalPath(entry, base.href, true);
|
|
172
|
+
return !!pattern
|
|
173
|
+
&& pattern.origin === trusted.origin
|
|
174
|
+
&& wildcardToRegExp(pattern.path).test(candidate.path);
|
|
175
|
+
});
|
|
176
|
+
return allowed
|
|
177
|
+
? { ok: true, url: candidate.url, normalizedPath: candidate.path }
|
|
178
|
+
: { ok: false, reason: 'allowlist' };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function isObject(value) {
|
|
182
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
183
|
+
}
|
|
184
|
+
function applyTableDetailAuthoringOperation(config, operationId, input) {
|
|
185
|
+
if (!isObject(input) || !Object.keys(input).length)
|
|
186
|
+
throw new Error('Detail input must be a non-empty object.');
|
|
187
|
+
const merge = (source, patch) => {
|
|
188
|
+
if (!patch || typeof patch !== 'object' || Array.isArray(patch))
|
|
189
|
+
return structuredClone(patch);
|
|
190
|
+
const next = source && typeof source === 'object' && !Array.isArray(source) ? { ...source } : {};
|
|
191
|
+
for (const key of Object.keys(patch))
|
|
192
|
+
Object.defineProperty(next, key, {
|
|
193
|
+
value: merge(Object.prototype.hasOwnProperty.call(next, key) ? next[key] : undefined, patch[key]),
|
|
194
|
+
enumerable: true, configurable: true, writable: true,
|
|
195
|
+
});
|
|
196
|
+
return next;
|
|
197
|
+
};
|
|
198
|
+
let next = { ...config, behavior: { ...config.behavior } };
|
|
199
|
+
let command = {};
|
|
200
|
+
if (operationId === 'expansion.configure') {
|
|
201
|
+
next.behavior.expansion = merge(config.behavior?.expansion, input);
|
|
202
|
+
if (input.enabled === true)
|
|
203
|
+
command = { enabled: true, placement: 'row' };
|
|
204
|
+
}
|
|
205
|
+
else if (operationId === 'detail.configure') {
|
|
206
|
+
if (input.presentation !== undefined && !isObject(input.presentation))
|
|
207
|
+
throw new Error('Invalid detail presentation input.');
|
|
208
|
+
next.behavior.detail = merge(config.behavior?.detail, input);
|
|
209
|
+
command = {
|
|
210
|
+
placement: input.presentation?.placement,
|
|
211
|
+
initiallyCollapsed: input.presentation?.initiallyCollapsed,
|
|
212
|
+
bottomStackPosition: input.presentation?.bottomStackPosition,
|
|
213
|
+
emptyBehavior: input.presentation?.emptyBehavior,
|
|
214
|
+
enabled: input.enabled,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
else if (operationId === 'detail.presentation.configure') {
|
|
218
|
+
next.behavior.detail = { ...config.behavior?.detail, presentation: merge(config.behavior?.detail?.presentation, input) };
|
|
219
|
+
command = {
|
|
220
|
+
placement: input.placement,
|
|
221
|
+
initiallyCollapsed: input.initiallyCollapsed,
|
|
222
|
+
bottomStackPosition: input.bottomStackPosition,
|
|
223
|
+
emptyBehavior: input.emptyBehavior,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
next.behavior.detail = { ...config.behavior?.detail, source: merge(config.behavior?.detail?.source, input) };
|
|
228
|
+
}
|
|
229
|
+
if (((operationId === 'detail.configure' || operationId === 'expansion.configure') && input.enabled !== undefined && typeof input.enabled !== 'boolean')
|
|
230
|
+
|| (command.enabled !== undefined && typeof command.enabled !== 'boolean')
|
|
231
|
+
|| (command.placement !== undefined && !['row', 'bottom'].includes(command.placement))
|
|
232
|
+
|| (command.initiallyCollapsed !== undefined && typeof command.initiallyCollapsed !== 'boolean')
|
|
233
|
+
|| (command.bottomStackPosition !== undefined && !['beforeControls', 'afterControls'].includes(command.bottomStackPosition))
|
|
234
|
+
|| (command.emptyBehavior !== undefined && !['message', 'hide'].includes(command.emptyBehavior))) {
|
|
235
|
+
throw new Error('Invalid detail presentation input.');
|
|
236
|
+
}
|
|
237
|
+
if (Object.keys(command).length)
|
|
238
|
+
next = configureTableDetailPresentation(next, command);
|
|
239
|
+
const issues = tableDetailAuthoringIssues(next);
|
|
240
|
+
if (issues.length)
|
|
241
|
+
throw new Error(`Invalid detail configuration: ${issues.join(', ')}`);
|
|
242
|
+
return next;
|
|
243
|
+
}
|
|
244
|
+
/** Explicit authoring commands only. Hydration never calls this function. */
|
|
245
|
+
function configureTableDetailPresentation(config, command) {
|
|
246
|
+
const behavior = config.behavior ?? {};
|
|
247
|
+
const detail = behavior.detail ?? {};
|
|
248
|
+
const presentation = { ...detail.presentation };
|
|
249
|
+
if (command.placement !== undefined)
|
|
250
|
+
presentation.placement = command.placement;
|
|
251
|
+
if (command.initiallyCollapsed !== undefined)
|
|
252
|
+
presentation.initiallyCollapsed = command.initiallyCollapsed;
|
|
253
|
+
if (command.bottomStackPosition !== undefined)
|
|
254
|
+
presentation.bottomStackPosition = command.bottomStackPosition;
|
|
255
|
+
if (command.emptyBehavior !== undefined)
|
|
256
|
+
presentation.emptyBehavior = command.emptyBehavior;
|
|
257
|
+
const next = { ...config, behavior: { ...behavior, detail: { ...detail } } };
|
|
258
|
+
const target = next.behavior;
|
|
259
|
+
if (Object.keys(presentation).length)
|
|
260
|
+
target.detail.presentation = presentation;
|
|
261
|
+
if (command.enabled !== undefined)
|
|
262
|
+
target.detail.enabled = command.enabled;
|
|
263
|
+
if (command.enabled === false) {
|
|
264
|
+
target.expansion = { ...behavior.expansion, enabled: false };
|
|
265
|
+
}
|
|
266
|
+
else if (command.placement !== undefined || command.enabled === true) {
|
|
267
|
+
target.detail.enabled = true;
|
|
268
|
+
const bottom = (command.placement ?? detail.presentation?.placement ?? 'row') === 'bottom';
|
|
269
|
+
target.expansion = { ...behavior.expansion, enabled: !bottom };
|
|
270
|
+
if (bottom && behavior.selection?.enabled !== true) {
|
|
271
|
+
target.selection = {
|
|
272
|
+
mode: 'both', allowSelectAll: false, checkboxPosition: 'start',
|
|
273
|
+
persistSelection: false, persistOnDataUpdate: false,
|
|
274
|
+
...behavior.selection, type: 'single', enabled: true,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return next;
|
|
279
|
+
}
|
|
280
|
+
function tableDetailAuthoringIssues(config) {
|
|
281
|
+
const detail = config.behavior?.detail;
|
|
282
|
+
if (detail === undefined)
|
|
283
|
+
return [];
|
|
284
|
+
if (!isObject(detail))
|
|
285
|
+
return ['structure'];
|
|
286
|
+
for (const key of ['presentation', 'source', 'schemaContract', 'rendering', 'height', 'lazyLoad']) {
|
|
287
|
+
if (detail[key] !== undefined && !isObject(detail[key]))
|
|
288
|
+
return ['structure'];
|
|
289
|
+
}
|
|
290
|
+
const sourceMode = detail.source?.mode;
|
|
291
|
+
const resource = detail.source?.resource;
|
|
292
|
+
const resourcePath = detail.source?.resourcePath;
|
|
293
|
+
const resourceAllowList = detail.source?.resourceAllowList;
|
|
294
|
+
if ((detail.enabled !== undefined && typeof detail.enabled !== 'boolean')
|
|
295
|
+
|| (detail.presentation?.initiallyCollapsed !== undefined && typeof detail.presentation.initiallyCollapsed !== 'boolean')
|
|
296
|
+
|| (detail.presentation?.bottomStackPosition !== undefined && !['beforeControls', 'afterControls'].includes(detail.presentation.bottomStackPosition))
|
|
297
|
+
|| (detail.presentation?.emptyBehavior !== undefined && !['message', 'hide'].includes(detail.presentation.emptyBehavior))
|
|
298
|
+
|| (detail.source?.mode !== undefined && !['inline', 'resource', 'resourcePath', 'hypermedia'].includes(detail.source.mode))
|
|
299
|
+
|| (detail.source?.inlineSchema !== undefined && !isObject(detail.source.inlineSchema))
|
|
300
|
+
|| (detail.source?.inlineSchema?.items !== undefined && !Array.isArray(detail.source.inlineSchema.items))
|
|
301
|
+
|| (resource !== undefined && !isObject(resource))
|
|
302
|
+
|| (resourcePath !== undefined && !isObject(resourcePath))
|
|
303
|
+
|| (detail.source?.contextMap !== undefined && !isObject(detail.source.contextMap))
|
|
304
|
+
|| (resourcePath?.paramsMap !== undefined && !isObject(resourcePath.paramsMap))
|
|
305
|
+
|| (resourceAllowList !== undefined && (!Array.isArray(resourceAllowList) || resourceAllowList.some((entry) => typeof entry !== 'string')))
|
|
306
|
+
|| (detail.source?.fallbackMode !== undefined && !['none', 'inline', 'resource'].includes(detail.source.fallbackMode))
|
|
307
|
+
|| (detail.lazyLoad?.enabled !== undefined && typeof detail.lazyLoad.enabled !== 'boolean')
|
|
308
|
+
|| (detail.lazyLoad?.actionId !== undefined && typeof detail.lazyLoad.actionId !== 'string')
|
|
309
|
+
|| (detail.lazyLoad?.cache !== undefined && !isObject(detail.lazyLoad.cache))
|
|
310
|
+
|| (detail.lazyLoad?.cache?.enabled !== undefined && typeof detail.lazyLoad.cache.enabled !== 'boolean')
|
|
311
|
+
|| (detail.lazyLoad?.cache?.ttlMs !== undefined && (!Number.isFinite(detail.lazyLoad.cache.ttlMs) || detail.lazyLoad.cache.ttlMs <= 0))
|
|
312
|
+
|| (detail.lazyLoad?.retry !== undefined && !isObject(detail.lazyLoad.retry))
|
|
313
|
+
|| (detail.lazyLoad?.retry?.maxAttempts !== undefined && (!Number.isInteger(detail.lazyLoad.retry.maxAttempts) || detail.lazyLoad.retry.maxAttempts < 0))
|
|
314
|
+
|| (detail.lazyLoad?.cancelOnCollapse !== undefined && typeof detail.lazyLoad.cancelOnCollapse !== 'boolean')
|
|
315
|
+
|| (detail.lazyLoad?.dedupeByRowKey !== undefined && typeof detail.lazyLoad.dedupeByRowKey !== 'boolean'))
|
|
316
|
+
return ['structure'];
|
|
317
|
+
const issues = [];
|
|
318
|
+
const placement = detail.presentation?.placement ?? 'row';
|
|
319
|
+
if (placement !== 'row' && placement !== 'bottom')
|
|
320
|
+
issues.push('placement');
|
|
321
|
+
if ((placement === 'bottom' || detail.enabled === false) && config.behavior?.expansion?.enabled)
|
|
322
|
+
issues.push('conflict');
|
|
323
|
+
if (detail.enabled !== false && placement === 'bottom' && !config.behavior?.selection?.enabled)
|
|
324
|
+
issues.push('selection');
|
|
325
|
+
if (detail.height?.px !== undefined && (!Number.isFinite(detail.height.px) || detail.height.px <= 0))
|
|
326
|
+
issues.push('height');
|
|
327
|
+
if (detail.height?.mode !== undefined && !['fixed', 'dynamic'].includes(detail.height.mode))
|
|
328
|
+
issues.push('height');
|
|
329
|
+
if (detail.height?.mode === 'fixed' && detail.height?.px === undefined)
|
|
330
|
+
issues.push('height');
|
|
331
|
+
const validResource = !!resource
|
|
332
|
+
&& ['ui-composition', 'form-schema', 'table-schema', 'dashboard-schema'].includes(resource.kind)
|
|
333
|
+
&& typeof resource.id === 'string' && !!resource.id.trim()
|
|
334
|
+
&& typeof resource.version === 'string' && !!resource.version.trim();
|
|
335
|
+
const validInline = detail.source?.inlineSchema !== undefined;
|
|
336
|
+
const effectiveSourceMode = sourceMode ?? 'inline';
|
|
337
|
+
if (detail.enabled !== false && effectiveSourceMode === 'inline' && !validInline)
|
|
338
|
+
issues.push('source');
|
|
339
|
+
if (sourceMode === 'resource' && !validResource)
|
|
340
|
+
issues.push('source');
|
|
341
|
+
if (detail.source?.fallbackMode !== undefined
|
|
342
|
+
&& detail.source.fallbackMode !== 'none'
|
|
343
|
+
&& detail.source.fallbackMode === effectiveSourceMode)
|
|
344
|
+
issues.push('source');
|
|
345
|
+
if (sourceMode === 'resourcePath' && (typeof resourcePath?.path !== 'string' || !resourcePath.path.trim()
|
|
346
|
+
|| !isTableDetailResourcePathTemplateSafe(resourcePath.path)
|
|
347
|
+
|| (resourcePath.method !== undefined && !['GET', 'POST'].includes(resourcePath.method))
|
|
348
|
+
|| !Array.isArray(resourceAllowList) || resourceAllowList.length === 0
|
|
349
|
+
|| resourceAllowList.some((entry) => !entry.trim() || !isTableDetailResourcePathTemplateSafe(entry))))
|
|
350
|
+
issues.push('source');
|
|
351
|
+
if (detail.source?.fallbackMode === 'inline' && !validInline)
|
|
352
|
+
issues.push('source');
|
|
353
|
+
if (detail.source?.fallbackMode === 'resource' && !validResource)
|
|
354
|
+
issues.push('source');
|
|
355
|
+
if (validInline && (effectiveSourceMode === 'inline' || detail.source?.fallbackMode === 'inline')) {
|
|
356
|
+
const inlineIssues = inspectInlineDetailSchema(detail.source.inlineSchema, config);
|
|
357
|
+
issues.push(...inlineIssues);
|
|
358
|
+
}
|
|
359
|
+
return [...new Set(issues)];
|
|
360
|
+
}
|
|
361
|
+
function inspectInlineDetailSchema(schema, config) {
|
|
362
|
+
const issues = [];
|
|
363
|
+
const declaredActionIds = new Set((config.actions?.row?.actions ?? [])
|
|
364
|
+
.map((action) => String(action.id || '').trim())
|
|
365
|
+
.filter(Boolean));
|
|
366
|
+
const seen = new WeakSet();
|
|
367
|
+
const visit = (value) => {
|
|
368
|
+
if (!value || typeof value !== 'object')
|
|
369
|
+
return;
|
|
370
|
+
if (seen.has(value))
|
|
371
|
+
return;
|
|
372
|
+
seen.add(value);
|
|
373
|
+
if (Array.isArray(value)) {
|
|
374
|
+
value.forEach(visit);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const node = value;
|
|
378
|
+
if (node['type'] === 'richContent') {
|
|
379
|
+
if (!validateRichContentDocument(node['document']).valid
|
|
380
|
+
|| !isSafeRichContentClassName(node['rootClassName'])) {
|
|
381
|
+
issues.push('content');
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (typeof node['actionId'] === 'string') {
|
|
385
|
+
const actionId = node['actionId'].trim();
|
|
386
|
+
if (!actionId || !declaredActionIds.has(actionId))
|
|
387
|
+
issues.push('action');
|
|
388
|
+
}
|
|
389
|
+
Object.values(node).forEach(visit);
|
|
390
|
+
};
|
|
391
|
+
visit(schema);
|
|
392
|
+
return issues;
|
|
393
|
+
}
|
|
394
|
+
/** Edits the original authoring tree; keys exist only in the editor's WeakMap. */
|
|
395
|
+
class TableDetailTreeDraft {
|
|
396
|
+
keys = new WeakMap();
|
|
397
|
+
sequence = 0;
|
|
398
|
+
selectedKey = null;
|
|
399
|
+
schema;
|
|
400
|
+
constructor(schema = { layout: 'stack', items: [] }) {
|
|
401
|
+
const nodeType = String(schema?.type || '').trim();
|
|
402
|
+
if (!nodeType) {
|
|
403
|
+
this.schema = schema;
|
|
404
|
+
}
|
|
405
|
+
else if (nodeType === 'tabs') {
|
|
406
|
+
this.schema = {
|
|
407
|
+
...schema,
|
|
408
|
+
layout: 'tabs',
|
|
409
|
+
items: Array.isArray(schema.items)
|
|
410
|
+
? schema.items
|
|
411
|
+
: (Array.isArray(schema.content) ? schema.content : []),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
this.schema = { layout: 'stack', items: [schema] };
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
entries() {
|
|
419
|
+
const result = [];
|
|
420
|
+
const visit = (nodes, depth) => {
|
|
421
|
+
if (!Array.isArray(nodes))
|
|
422
|
+
return;
|
|
423
|
+
for (const node of nodes) {
|
|
424
|
+
if (!node || typeof node !== 'object' || Array.isArray(node))
|
|
425
|
+
continue;
|
|
426
|
+
let key = this.keys.get(node);
|
|
427
|
+
if (!key) {
|
|
428
|
+
key = `detail-node-${++this.sequence}`;
|
|
429
|
+
this.keys.set(node, key);
|
|
430
|
+
}
|
|
431
|
+
result.push({ key, node, siblings: nodes, depth });
|
|
432
|
+
for (const child of ['items', 'content', 'cards', 'nodes'])
|
|
433
|
+
visit(node[child], depth + 1);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
visit(this.schema.items, 0);
|
|
437
|
+
return result;
|
|
438
|
+
}
|
|
439
|
+
selected() { return this.entries().find(entry => entry.key === this.selectedKey); }
|
|
440
|
+
move(direction) {
|
|
441
|
+
const entry = this.selected();
|
|
442
|
+
if (!entry)
|
|
443
|
+
return;
|
|
444
|
+
const index = entry.siblings.indexOf(entry.node);
|
|
445
|
+
const next = index + direction;
|
|
446
|
+
if (next < 0 || next >= entry.siblings.length)
|
|
447
|
+
return;
|
|
448
|
+
entry.siblings.splice(index, 1);
|
|
449
|
+
entry.siblings.splice(next, 0, entry.node);
|
|
450
|
+
}
|
|
451
|
+
add(node, nested = false) {
|
|
452
|
+
const selected = this.selected()?.node;
|
|
453
|
+
const childKey = selected?.type === 'card' || selected?.type === 'tab' ? 'content' : 'items';
|
|
454
|
+
const parent = nested && selected ? selected : this.schema;
|
|
455
|
+
const key = nested && selected ? childKey : 'items';
|
|
456
|
+
if (!nested && this.schema.layout === 'tabs' && node.type !== 'tab') {
|
|
457
|
+
node = { type: 'tab', title: node.title || node.label || node.type, content: [node] };
|
|
458
|
+
}
|
|
459
|
+
if (!Array.isArray(parent[key]))
|
|
460
|
+
parent[key] = [];
|
|
461
|
+
parent[key].push(node);
|
|
462
|
+
this.selectedKey = this.entries().find(entry => entry.node === node)?.key ?? null;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const DOCUMENT_KIND = 'praxis.table.editor';
|
|
467
|
+
const DOCUMENT_VERSION = 1;
|
|
468
|
+
const HORIZONTAL_SCROLL_VALUES = ['auto', 'wrap', 'none'];
|
|
469
|
+
function createTableAuthoringDocument(source) {
|
|
470
|
+
const doc = {
|
|
471
|
+
kind: DOCUMENT_KIND,
|
|
472
|
+
version: DOCUMENT_VERSION,
|
|
473
|
+
config: asTableConfig(source?.config),
|
|
474
|
+
};
|
|
475
|
+
if (hasOwnProperty(source, 'bindings')) {
|
|
476
|
+
doc.bindings = source?.bindings;
|
|
477
|
+
}
|
|
478
|
+
return normalizeTableAuthoringDocument(doc);
|
|
479
|
+
}
|
|
480
|
+
function parseLegacyOrTableDocument(raw) {
|
|
481
|
+
const obj = asRecord(raw);
|
|
482
|
+
if (obj.kind === DOCUMENT_KIND && obj.version === DOCUMENT_VERSION) {
|
|
483
|
+
return normalizeTableAuthoringDocument({
|
|
484
|
+
kind: DOCUMENT_KIND,
|
|
485
|
+
version: DOCUMENT_VERSION,
|
|
486
|
+
config: asTableConfig(obj.config),
|
|
487
|
+
bindings: obj.bindings,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
if (looksLikeLegacyPayload(obj)) {
|
|
491
|
+
const legacyConfig = extractLegacyTableConfig(obj);
|
|
492
|
+
const legacyIdField = normalizeIdField(obj.__idField__);
|
|
493
|
+
if (legacyIdField) {
|
|
494
|
+
legacyConfig.meta = {
|
|
495
|
+
...(legacyConfig.meta || {}),
|
|
496
|
+
idField: legacyIdField,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
return normalizeTableAuthoringDocument({
|
|
500
|
+
kind: DOCUMENT_KIND,
|
|
501
|
+
version: DOCUMENT_VERSION,
|
|
502
|
+
config: legacyConfig,
|
|
503
|
+
bindings: {
|
|
504
|
+
resourcePath: normalizeResourcePath(obj.__resourcePath__),
|
|
505
|
+
horizontalScroll: normalizeHorizontalScroll(obj.__horizontalScroll__),
|
|
506
|
+
},
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
return createTableAuthoringDocument({ config: raw });
|
|
510
|
+
}
|
|
511
|
+
function normalizeTableAuthoringDocument(doc) {
|
|
512
|
+
const config = ensureAuthoringCompleteness(normalizeTableConfig(doc?.config));
|
|
513
|
+
const legacyBindingIdField = normalizeIdField(doc?.bindings?.idField);
|
|
514
|
+
const legacyRuntimeIdField = normalizeIdField(doc?.config?.__idField__);
|
|
515
|
+
const canonicalIdField = normalizeIdField(config?.meta?.idField);
|
|
516
|
+
const migratedIdField = canonicalIdField || legacyBindingIdField || legacyRuntimeIdField;
|
|
517
|
+
if (migratedIdField && !canonicalIdField) {
|
|
518
|
+
config.meta = {
|
|
519
|
+
...(config.meta || {}),
|
|
520
|
+
idField: migratedIdField,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
const normalized = {
|
|
524
|
+
kind: DOCUMENT_KIND,
|
|
525
|
+
version: DOCUMENT_VERSION,
|
|
526
|
+
config,
|
|
527
|
+
};
|
|
528
|
+
if (hasOwnProperty(doc, 'bindings')) {
|
|
529
|
+
normalized.bindings = normalizeBindings(doc?.bindings);
|
|
530
|
+
}
|
|
531
|
+
return normalized;
|
|
532
|
+
}
|
|
533
|
+
function validateTableAuthoringDocument(doc, context) {
|
|
534
|
+
const raw = asRecord(doc);
|
|
535
|
+
const rawConfig = asRecord(raw.config);
|
|
536
|
+
const rawBindings = asRecord(raw.bindings);
|
|
537
|
+
const normalized = normalizeTableAuthoringDocument(doc);
|
|
538
|
+
const diagnostics = [];
|
|
539
|
+
projectTableActionAuthoring(rawConfig, undefined, diagnostic => diagnostics.push(diagnostic));
|
|
540
|
+
for (const issue of tableDetailAuthoringIssues(rawConfig)) {
|
|
541
|
+
diagnostics.push(errorDiagnostic(`table.config.detail.${issue}`, `Invalid detail configuration: ${issue}`, 'config.behavior.detail'));
|
|
542
|
+
}
|
|
543
|
+
if ('columns' in rawConfig && !Array.isArray(rawConfig.columns)) {
|
|
544
|
+
diagnostics.push(errorDiagnostic('table.config.columns.invalid', 'config.columns must be an array', 'config.columns'));
|
|
545
|
+
return diagnostics;
|
|
546
|
+
}
|
|
547
|
+
const rawColumns = Array.isArray(rawConfig.columns)
|
|
548
|
+
? rawConfig.columns
|
|
549
|
+
: normalized.config.columns;
|
|
550
|
+
const columnFields = new Set();
|
|
551
|
+
rawColumns.forEach((column, index) => {
|
|
552
|
+
const field = trimString(column?.field);
|
|
553
|
+
if (!field) {
|
|
554
|
+
diagnostics.push(errorDiagnostic('table.config.columns.field.required', 'Column field is required', `config.columns[${index}].field`));
|
|
555
|
+
}
|
|
556
|
+
else if (columnFields.has(field)) {
|
|
557
|
+
diagnostics.push(errorDiagnostic('table.config.columns.field.duplicate', 'Column field must be unique within the table', `config.columns[${index}].field`));
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
columnFields.add(field);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
if ('columnProjection' in rawConfig) {
|
|
564
|
+
const projection = asRecord(rawConfig.columnProjection);
|
|
565
|
+
if (!rawConfig.columnProjection
|
|
566
|
+
|| typeof rawConfig.columnProjection !== 'object'
|
|
567
|
+
|| Array.isArray(rawConfig.columnProjection)) {
|
|
568
|
+
diagnostics.push(errorDiagnostic('table.config.columnProjection.invalid', 'config.columnProjection must be an object', 'config.columnProjection'));
|
|
569
|
+
}
|
|
570
|
+
else if (projection.source !== 'schema') {
|
|
571
|
+
diagnostics.push(errorDiagnostic('table.config.columnProjection.source.invalid', 'config.columnProjection.source must be schema', 'config.columnProjection.source'));
|
|
572
|
+
}
|
|
573
|
+
if ('include' in projection) {
|
|
574
|
+
if (!Array.isArray(projection.include)
|
|
575
|
+
|| projection.include.some((field) => typeof field !== 'string' || !field.trim())
|
|
576
|
+
|| new Set(projection.include).size !== projection.include.length) {
|
|
577
|
+
diagnostics.push(errorDiagnostic('table.config.columnProjection.include.invalid', 'config.columnProjection.include must be an ordered array of unique canonical field names', 'config.columnProjection.include'));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if ('order' in projection && (!Array.isArray(projection.order)
|
|
581
|
+
|| projection.order.some((field) => typeof field !== 'string' || !field.trim() || field !== field.trim())
|
|
582
|
+
|| new Set(projection.order).size !== projection.order.length)) {
|
|
583
|
+
diagnostics.push(errorDiagnostic('table.config.columnProjection.order.invalid', 'config.columnProjection.order must contain unique canonical field names without selecting membership', 'config.columnProjection.order'));
|
|
584
|
+
}
|
|
585
|
+
if ('overrides' in projection) {
|
|
586
|
+
const overrides = asRecord(projection.overrides);
|
|
587
|
+
if (!projection.overrides
|
|
588
|
+
|| typeof projection.overrides !== 'object'
|
|
589
|
+
|| Array.isArray(projection.overrides)) {
|
|
590
|
+
diagnostics.push(errorDiagnostic('table.config.columnProjection.overrides.invalid', 'config.columnProjection.overrides must be an object keyed by canonical field name', 'config.columnProjection.overrides'));
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
Object.entries(overrides).forEach(([field, override]) => {
|
|
594
|
+
if (!field.trim() || !override || typeof override !== 'object' || Array.isArray(override)) {
|
|
595
|
+
diagnostics.push(errorDiagnostic('table.config.columnProjection.override.invalid', 'Each column projection override must be an object keyed by canonical field name', `config.columnProjection.overrides.${field}`));
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
if (hasOwnProperty(override, 'field')) {
|
|
599
|
+
diagnostics.push(errorDiagnostic('table.config.columnProjection.override.field.forbidden', 'A schema column projection override cannot change the canonical field identity', `config.columnProjection.overrides.${field}.field`));
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if ('additions' in projection) {
|
|
605
|
+
if (!Array.isArray(projection.additions)) {
|
|
606
|
+
diagnostics.push(errorDiagnostic('table.config.columnProjection.additions.invalid', 'config.columnProjection.additions must be an array of editorial columns', 'config.columnProjection.additions'));
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
const fields = projection.additions
|
|
610
|
+
.map((column) => trimString(asRecord(column).field));
|
|
611
|
+
if (fields.some((field) => !field)
|
|
612
|
+
|| new Set(fields).size !== fields.length) {
|
|
613
|
+
diagnostics.push(errorDiagnostic('table.config.columnProjection.additions.field.invalid', 'Each editorial addition must have a unique field', 'config.columnProjection.additions'));
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const bindings = normalized.bindings || {};
|
|
619
|
+
const metaIdField = trimString(normalized.config?.meta?.idField);
|
|
620
|
+
if ('horizontalScroll' in rawBindings &&
|
|
621
|
+
rawBindings.horizontalScroll !== undefined &&
|
|
622
|
+
normalizeHorizontalScroll(rawBindings.horizontalScroll) === undefined) {
|
|
623
|
+
diagnostics.push(errorDiagnostic('table.bindings.horizontalScroll.invalid', 'horizontalScroll must be auto, wrap or none', 'bindings.horizontalScroll'));
|
|
624
|
+
}
|
|
625
|
+
if (bindings.horizontalScroll &&
|
|
626
|
+
!HORIZONTAL_SCROLL_VALUES.includes(bindings.horizontalScroll)) {
|
|
627
|
+
diagnostics.push(errorDiagnostic('table.bindings.horizontalScroll.invalid', 'horizontalScroll must be auto, wrap or none', 'bindings.horizontalScroll'));
|
|
628
|
+
}
|
|
629
|
+
const effectiveMode = resolveEffectiveDataMode(normalized, context);
|
|
630
|
+
if (rawConfig.columnProjection && effectiveMode !== 'remote') {
|
|
631
|
+
diagnostics.push(warnDiagnostic('table.config.columnProjection.remote-schema.required', 'Schema column projection requires a remote resourcePath at runtime', 'config.columnProjection'));
|
|
632
|
+
}
|
|
633
|
+
if (effectiveMode === 'local') {
|
|
634
|
+
if (normalized.config.behavior?.pagination?.strategy === 'server') {
|
|
635
|
+
diagnostics.push(warnDiagnostic('table.behavior.pagination.strategy.invalid-for-local', 'Server pagination will be projected to client in local mode', 'config.behavior.pagination.strategy'));
|
|
636
|
+
}
|
|
637
|
+
if (normalized.config.behavior?.sorting?.strategy === 'server') {
|
|
638
|
+
diagnostics.push(warnDiagnostic('table.behavior.sorting.strategy.invalid-for-local', 'Server sorting will be projected to client in local mode', 'config.behavior.sorting.strategy'));
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
if (context?.server?.idField &&
|
|
642
|
+
metaIdField &&
|
|
643
|
+
context.server.idField !== metaIdField) {
|
|
644
|
+
diagnostics.push(warnDiagnostic('table.meta.idField.diverges-from-server', 'config.meta.idField diverges from current server idField', 'config.meta.idField'));
|
|
645
|
+
}
|
|
646
|
+
const configHash = trimString(normalized.config?.meta?.serverHash);
|
|
647
|
+
if (context?.server?.schemaHash && !configHash) {
|
|
648
|
+
diagnostics.push(infoDiagnostic('table.meta.serverHash.snapshot-missing', 'No persisted schema snapshot is available for reconciliation with the current server schema hash', 'config.meta.serverHash'));
|
|
649
|
+
}
|
|
650
|
+
if (context?.server?.schemaHash && configHash && context.server.schemaHash !== configHash) {
|
|
651
|
+
diagnostics.push(warnDiagnostic('table.meta.serverHash.diverges-from-server', 'config.meta.serverHash snapshot diverges from current server schema hash', 'config.meta.serverHash'));
|
|
652
|
+
}
|
|
653
|
+
return diagnostics;
|
|
654
|
+
}
|
|
655
|
+
function toCanonicalTableConfig(doc, context) {
|
|
656
|
+
const normalized = normalizeTableAuthoringDocument(doc);
|
|
657
|
+
const cfg = cloneJson(normalized.config);
|
|
658
|
+
const effectiveMode = resolveEffectiveDataMode(normalized, context);
|
|
659
|
+
if (effectiveMode === 'local') {
|
|
660
|
+
if (cfg.behavior?.pagination?.strategy === 'server') {
|
|
661
|
+
cfg.behavior = {
|
|
662
|
+
...(cfg.behavior || {}),
|
|
663
|
+
pagination: {
|
|
664
|
+
...(cfg.behavior?.pagination || {}),
|
|
665
|
+
strategy: 'client',
|
|
666
|
+
},
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
if (cfg.behavior?.sorting?.strategy === 'server') {
|
|
670
|
+
cfg.behavior = {
|
|
671
|
+
...(cfg.behavior || {}),
|
|
672
|
+
sorting: {
|
|
673
|
+
...(cfg.behavior?.sorting || {}),
|
|
674
|
+
strategy: 'client',
|
|
675
|
+
},
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return stripLegacyRuntimeMarkers(cfg);
|
|
680
|
+
}
|
|
681
|
+
function buildTableApplyPlan(doc, runtime, options) {
|
|
682
|
+
const normalized = normalizeTableAuthoringDocument(doc);
|
|
683
|
+
const hasBindings = hasOwnProperty(doc, 'bindings');
|
|
684
|
+
const diagnostics = validateTableAuthoringDocument(doc, {
|
|
685
|
+
hasLocalDataInput: runtime?.hasLocalDataInput,
|
|
686
|
+
localDataModeDefaultEnabled: runtime?.localDataModeDefaultEnabled,
|
|
687
|
+
server: {
|
|
688
|
+
idField: runtime?.server?.idField,
|
|
689
|
+
schemaHash: runtime?.server?.schemaHash,
|
|
690
|
+
},
|
|
691
|
+
});
|
|
692
|
+
const bindingsPatch = hasBindings
|
|
693
|
+
? normalizeBindings(normalized.bindings) || {}
|
|
694
|
+
: undefined;
|
|
695
|
+
const diff = deriveBindingsDiff(bindingsPatch, runtime?.currentBindings);
|
|
696
|
+
return {
|
|
697
|
+
canonicalConfig: toCanonicalTableConfig(normalized, {
|
|
698
|
+
hasLocalDataInput: runtime?.hasLocalDataInput,
|
|
699
|
+
localDataModeDefaultEnabled: runtime?.localDataModeDefaultEnabled,
|
|
700
|
+
}),
|
|
701
|
+
bindingsPatch,
|
|
702
|
+
persistence: {
|
|
703
|
+
saveConfig: options?.saveConfig === true,
|
|
704
|
+
saveBindings: options?.saveBindings === true,
|
|
705
|
+
},
|
|
706
|
+
runtime: deriveRuntimePlan(bindingsPatch, diff),
|
|
707
|
+
metadata: {
|
|
708
|
+
attachServerMeta: options?.attachServerMeta === true,
|
|
709
|
+
},
|
|
710
|
+
diff,
|
|
711
|
+
diagnostics,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
function serializeTableAuthoringDocument(doc) {
|
|
715
|
+
const normalized = normalizeTableAuthoringDocument(doc);
|
|
716
|
+
return stripUndefinedDeep({
|
|
717
|
+
kind: DOCUMENT_KIND,
|
|
718
|
+
version: DOCUMENT_VERSION,
|
|
719
|
+
config: normalized.config,
|
|
720
|
+
bindings: normalized.bindings,
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
function normalizeTableConfig(config) {
|
|
724
|
+
const normalized = cloneJson(projectTableActionAuthoring(config || { columns: [] }));
|
|
725
|
+
if (!Array.isArray(normalized.columns)) {
|
|
726
|
+
normalized.columns = [];
|
|
727
|
+
}
|
|
728
|
+
if (normalized.behavior && typeof normalized.behavior === 'object') {
|
|
729
|
+
Object.keys(normalized.behavior).forEach((key) => {
|
|
730
|
+
const value = normalized.behavior[key];
|
|
731
|
+
if (value &&
|
|
732
|
+
typeof value === 'object' &&
|
|
733
|
+
!Array.isArray(value) &&
|
|
734
|
+
Object.keys(value).length === 0) {
|
|
735
|
+
delete normalized.behavior[key];
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
if (Object.keys(normalized.behavior).length === 0) {
|
|
739
|
+
delete normalized.behavior;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
const settings = normalized.behavior?.filtering?.advancedFilters?.settings;
|
|
743
|
+
if (settings && typeof settings === 'object' && !Array.isArray(settings)) {
|
|
744
|
+
Object.keys(settings).forEach((key) => {
|
|
745
|
+
if (settings[key] === undefined || settings[key] === null || settings[key] === '') {
|
|
746
|
+
delete settings[key];
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
return normalized;
|
|
751
|
+
}
|
|
752
|
+
function ensureAuthoringCompleteness(config) {
|
|
753
|
+
const next = cloneJson(config);
|
|
754
|
+
const advanced = next.behavior?.filtering?.advancedFilters;
|
|
755
|
+
if (advanced &&
|
|
756
|
+
typeof advanced === 'object' &&
|
|
757
|
+
!Object.prototype.hasOwnProperty.call(advanced, 'settings')) {
|
|
758
|
+
advanced.settings = {};
|
|
759
|
+
}
|
|
760
|
+
return next;
|
|
761
|
+
}
|
|
762
|
+
function normalizeBindings(bindings) {
|
|
763
|
+
if (!bindings || typeof bindings !== 'object')
|
|
764
|
+
return undefined;
|
|
765
|
+
const normalized = {
|
|
766
|
+
resourcePath: normalizeResourcePath(bindings.resourcePath),
|
|
767
|
+
horizontalScroll: normalizeHorizontalScroll(bindings.horizontalScroll),
|
|
768
|
+
};
|
|
769
|
+
return stripUndefinedShallow(normalized);
|
|
770
|
+
}
|
|
771
|
+
function resolveEffectiveDataMode(doc, context) {
|
|
772
|
+
const resourcePath = trimString(doc.bindings?.resourcePath);
|
|
773
|
+
if (resourcePath)
|
|
774
|
+
return 'remote';
|
|
775
|
+
const localModeFlag = readBoolean(doc.config?.behavior?.localDataMode?.enabled);
|
|
776
|
+
const effectiveLocalFlag = localModeFlag ?? (context?.localDataModeDefaultEnabled === true);
|
|
777
|
+
if (context?.hasLocalDataInput && effectiveLocalFlag) {
|
|
778
|
+
return 'local';
|
|
779
|
+
}
|
|
780
|
+
return 'empty';
|
|
781
|
+
}
|
|
782
|
+
function deriveBindingsDiff(next, current) {
|
|
783
|
+
if (next === undefined) {
|
|
784
|
+
return {
|
|
785
|
+
resourcePathChanged: false,
|
|
786
|
+
horizontalScrollChanged: false,
|
|
787
|
+
requiresDataRefresh: false,
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
const normalizedNext = normalizeBindings(next) || {};
|
|
791
|
+
const normalizedCurrent = normalizeBindings(current) || {};
|
|
792
|
+
const resourcePathChanged = normalizeResourcePath(normalizedNext.resourcePath) !==
|
|
793
|
+
normalizeResourcePath(normalizedCurrent.resourcePath);
|
|
794
|
+
const horizontalScrollChanged = normalizeHorizontalScroll(normalizedNext.horizontalScroll) !==
|
|
795
|
+
normalizeHorizontalScroll(normalizedCurrent.horizontalScroll);
|
|
796
|
+
return {
|
|
797
|
+
resourcePathChanged,
|
|
798
|
+
horizontalScrollChanged,
|
|
799
|
+
requiresDataRefresh: resourcePathChanged,
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
function deriveRuntimePlan(bindings, diff) {
|
|
803
|
+
if (bindings === undefined) {
|
|
804
|
+
return {
|
|
805
|
+
reconnectDataSource: false,
|
|
806
|
+
reloadSchema: false,
|
|
807
|
+
fetchData: false,
|
|
808
|
+
refreshLocalView: false,
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
const resourcePath = trimString(bindings?.resourcePath);
|
|
812
|
+
if (resourcePath) {
|
|
813
|
+
return {
|
|
814
|
+
reconnectDataSource: !!diff?.resourcePathChanged,
|
|
815
|
+
reloadSchema: !!diff?.resourcePathChanged,
|
|
816
|
+
fetchData: !!diff?.requiresDataRefresh,
|
|
817
|
+
refreshLocalView: false,
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
return {
|
|
821
|
+
reconnectDataSource: !!diff?.resourcePathChanged,
|
|
822
|
+
reloadSchema: false,
|
|
823
|
+
fetchData: false,
|
|
824
|
+
refreshLocalView: true,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
function extractLegacyTableConfig(obj) {
|
|
828
|
+
const clone = { ...obj };
|
|
829
|
+
delete clone.__resourcePath__;
|
|
830
|
+
delete clone.__resourcePathIntent__;
|
|
831
|
+
delete clone.__idField__;
|
|
832
|
+
delete clone.__horizontalScroll__;
|
|
833
|
+
return asTableConfig(clone);
|
|
834
|
+
}
|
|
835
|
+
function stripLegacyRuntimeMarkers(config) {
|
|
836
|
+
const clone = cloneJson(config);
|
|
837
|
+
delete clone.__resourcePath__;
|
|
838
|
+
delete clone.__resourcePathIntent__;
|
|
839
|
+
delete clone.__idField__;
|
|
840
|
+
delete clone.__horizontalScroll__;
|
|
841
|
+
return clone;
|
|
842
|
+
}
|
|
843
|
+
function looksLikeLegacyPayload(obj) {
|
|
844
|
+
return ('__resourcePath__' in obj ||
|
|
845
|
+
'__resourcePathIntent__' in obj ||
|
|
846
|
+
'__idField__' in obj ||
|
|
847
|
+
'__horizontalScroll__' in obj);
|
|
848
|
+
}
|
|
849
|
+
function asTableConfig(value) {
|
|
850
|
+
if (value && typeof value === 'object') {
|
|
851
|
+
return value;
|
|
852
|
+
}
|
|
853
|
+
return { columns: [] };
|
|
854
|
+
}
|
|
855
|
+
function asRecord(value) {
|
|
856
|
+
return value && typeof value === 'object'
|
|
857
|
+
? value
|
|
858
|
+
: {};
|
|
859
|
+
}
|
|
860
|
+
function hasOwnProperty(value, key) {
|
|
861
|
+
return !!value && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, key);
|
|
862
|
+
}
|
|
863
|
+
function normalizeResourcePath(value) {
|
|
864
|
+
if (value === undefined)
|
|
865
|
+
return undefined;
|
|
866
|
+
const trimmed = trimString(value);
|
|
867
|
+
return trimmed || null;
|
|
868
|
+
}
|
|
869
|
+
function normalizeIdField(value) {
|
|
870
|
+
const trimmed = trimString(value);
|
|
871
|
+
return trimmed || undefined;
|
|
872
|
+
}
|
|
873
|
+
function normalizeHorizontalScroll(value) {
|
|
874
|
+
const candidate = trimString(value);
|
|
875
|
+
if (!candidate)
|
|
876
|
+
return undefined;
|
|
877
|
+
return HORIZONTAL_SCROLL_VALUES.includes(candidate) ? candidate : undefined;
|
|
878
|
+
}
|
|
879
|
+
function trimString(value) {
|
|
880
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
881
|
+
}
|
|
882
|
+
function readBoolean(value) {
|
|
883
|
+
if (value === true || value === false)
|
|
884
|
+
return value;
|
|
885
|
+
return undefined;
|
|
886
|
+
}
|
|
887
|
+
function cloneJson(value) {
|
|
888
|
+
return JSON.parse(JSON.stringify(value ?? null));
|
|
889
|
+
}
|
|
890
|
+
function stripUndefinedShallow(value) {
|
|
891
|
+
const entries = Object.entries(value).filter(([, v]) => v !== undefined);
|
|
892
|
+
if (!entries.length)
|
|
893
|
+
return undefined;
|
|
894
|
+
return Object.fromEntries(entries);
|
|
895
|
+
}
|
|
896
|
+
function stripUndefinedDeep(value) {
|
|
897
|
+
return JSON.parse(JSON.stringify(value));
|
|
898
|
+
}
|
|
899
|
+
function errorDiagnostic(code, message, path) {
|
|
900
|
+
return { level: 'error', code, message, path };
|
|
901
|
+
}
|
|
902
|
+
function warnDiagnostic(code, message, path) {
|
|
903
|
+
return { level: 'warning', code, message, path };
|
|
904
|
+
}
|
|
905
|
+
function infoDiagnostic(code, message, path) {
|
|
906
|
+
return { level: 'info', code, message, path };
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Internal canonical schema-to-column projection shared by runtime and authoring.
|
|
911
|
+
* Receives already-admitted fields and intent; does not normalize untrusted schema,
|
|
912
|
+
* recover historical evidence, execute user actions or attest provenance.
|
|
913
|
+
* No component instance, dependency injection or network request is required.
|
|
914
|
+
* Not part of the root public API.
|
|
915
|
+
*/
|
|
916
|
+
class TableSchemaColumnProjector {
|
|
917
|
+
convertFieldToColumn(field) {
|
|
918
|
+
const apiType = resolveColumnTypeFromFieldDefinition(field, this.inferFieldTypeFromFieldName(field.name));
|
|
919
|
+
const col = {
|
|
920
|
+
field: field.name,
|
|
921
|
+
header: field.label ?? field.name,
|
|
922
|
+
order: field.order,
|
|
923
|
+
width: field.width ?? undefined,
|
|
924
|
+
sortable: field.sortable,
|
|
925
|
+
visible: field.tableHidden || field.hidden ? false : true,
|
|
926
|
+
type: apiType,
|
|
927
|
+
_originalApiType: apiType,
|
|
928
|
+
_isApiField: true,
|
|
929
|
+
};
|
|
930
|
+
this.applySchemaFieldPresentation(field, col);
|
|
931
|
+
// Auto-renderers: map known UI controls to richer cell renderers on first bootstrap
|
|
932
|
+
// - Avatar: when backend marks controlType 'avatar' (x-ui) or field name matches avatar-like patterns
|
|
933
|
+
try {
|
|
934
|
+
this.applyAutoRenderer(field, col);
|
|
935
|
+
}
|
|
936
|
+
catch { }
|
|
937
|
+
return col;
|
|
938
|
+
}
|
|
939
|
+
projectSchemaColumns(fields, projection) {
|
|
940
|
+
const overrides = projection?.overrides ?? {};
|
|
941
|
+
const include = Array.isArray(projection?.include)
|
|
942
|
+
? projection.include
|
|
943
|
+
.map((field) => String(field || '').trim())
|
|
944
|
+
.filter((field, index, values) => !!field && values.indexOf(field) === index)
|
|
945
|
+
: [];
|
|
946
|
+
const includeOrder = new Map(include.map((field, index) => [field, index]));
|
|
947
|
+
const order = Array.isArray(projection?.order) ? projection.order : include;
|
|
948
|
+
const presentationOrder = new Map(order.map((field, index) => [field, index]));
|
|
949
|
+
const schemaColumns = (Array.isArray(fields) ? fields : [])
|
|
950
|
+
.filter((field) => !field.tableHidden && !field.hidden)
|
|
951
|
+
.filter((field) => include.length === 0 || includeOrder.has(field.name))
|
|
952
|
+
.sort((left, right) => {
|
|
953
|
+
return (presentationOrder.get(left.name) ?? Number.MAX_SAFE_INTEGER)
|
|
954
|
+
- (presentationOrder.get(right.name) ?? Number.MAX_SAFE_INTEGER);
|
|
955
|
+
})
|
|
956
|
+
.map((field) => {
|
|
957
|
+
const base = this.convertFieldToColumn(field);
|
|
958
|
+
const override = overrides[field.name];
|
|
959
|
+
if (!override || typeof override !== 'object' || Array.isArray(override)) {
|
|
960
|
+
return base;
|
|
961
|
+
}
|
|
962
|
+
const merged = deepMerge(base, override);
|
|
963
|
+
return {
|
|
964
|
+
...merged,
|
|
965
|
+
field: base.field,
|
|
966
|
+
_isApiField: base._isApiField,
|
|
967
|
+
_originalApiType: base._originalApiType,
|
|
968
|
+
};
|
|
969
|
+
});
|
|
970
|
+
const canonicalFields = new Set((Array.isArray(fields) ? fields : [])
|
|
971
|
+
.map((field) => String(field?.name || '').trim())
|
|
972
|
+
.filter(Boolean));
|
|
973
|
+
const additions = Array.isArray(projection?.additions)
|
|
974
|
+
? projection.additions
|
|
975
|
+
.filter((column) => {
|
|
976
|
+
const field = String(column?.field || '').trim();
|
|
977
|
+
return !!field && !canonicalFields.has(field);
|
|
978
|
+
})
|
|
979
|
+
.map((column) => this.toPortableColumnDefinition(column))
|
|
980
|
+
: [];
|
|
981
|
+
// Presentation priority must also survive setupColumns' numeric ordering.
|
|
982
|
+
// It never admits hidden fields, overrides field identity, or selects membership.
|
|
983
|
+
const combined = [...schemaColumns, ...additions];
|
|
984
|
+
if (Array.isArray(projection?.order) && projection.order.length > 0) {
|
|
985
|
+
combined.sort((left, right) => {
|
|
986
|
+
return (presentationOrder.get(left.field) ?? Number.MAX_SAFE_INTEGER)
|
|
987
|
+
- (presentationOrder.get(right.field) ?? Number.MAX_SAFE_INTEGER);
|
|
988
|
+
});
|
|
989
|
+
return combined.map((column) => {
|
|
990
|
+
const explicitOrder = presentationOrder.get(column.field);
|
|
991
|
+
return explicitOrder !== undefined ? { ...column, order: explicitOrder } : column;
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
return combined;
|
|
995
|
+
}
|
|
996
|
+
inferFieldTypeFromFieldName(fieldName) {
|
|
997
|
+
const lowercaseName = fieldName.toLowerCase();
|
|
998
|
+
// Date/time patterns - more specific patterns first
|
|
999
|
+
if (lowercaseName.endsWith('date') ||
|
|
1000
|
+
lowercaseName.endsWith('time') ||
|
|
1001
|
+
lowercaseName.endsWith('at') ||
|
|
1002
|
+
lowercaseName.startsWith('date') ||
|
|
1003
|
+
lowercaseName === 'created' ||
|
|
1004
|
+
lowercaseName === 'updated' ||
|
|
1005
|
+
lowercaseName === 'modified' ||
|
|
1006
|
+
lowercaseName.includes('timestamp')) {
|
|
1007
|
+
return 'date';
|
|
1008
|
+
}
|
|
1009
|
+
// Boolean patterns - most specific first to avoid conflicts
|
|
1010
|
+
if (lowercaseName.startsWith('is_') ||
|
|
1011
|
+
lowercaseName.startsWith('has_') ||
|
|
1012
|
+
lowercaseName.startsWith('can_') ||
|
|
1013
|
+
lowercaseName.startsWith('should_') ||
|
|
1014
|
+
lowercaseName === 'active' ||
|
|
1015
|
+
lowercaseName === 'enabled' ||
|
|
1016
|
+
lowercaseName === 'visible' ||
|
|
1017
|
+
lowercaseName === 'deleted' ||
|
|
1018
|
+
lowercaseName === 'archived' ||
|
|
1019
|
+
lowercaseName.endsWith('_flag') ||
|
|
1020
|
+
lowercaseName.endsWith('_enabled') ||
|
|
1021
|
+
lowercaseName.endsWith('_active')) {
|
|
1022
|
+
return 'boolean';
|
|
1023
|
+
}
|
|
1024
|
+
// Currency/money patterns - exclude common false positives
|
|
1025
|
+
if ((lowercaseName.includes('price') ||
|
|
1026
|
+
lowercaseName.includes('amount') ||
|
|
1027
|
+
lowercaseName.includes('cost') ||
|
|
1028
|
+
lowercaseName.includes('salary') ||
|
|
1029
|
+
lowercaseName.includes('wage') ||
|
|
1030
|
+
lowercaseName.includes('fee') ||
|
|
1031
|
+
(lowercaseName.includes('value') &&
|
|
1032
|
+
!lowercaseName.includes('id') &&
|
|
1033
|
+
!lowercaseName.includes('key'))) &&
|
|
1034
|
+
!lowercaseName.includes('count') &&
|
|
1035
|
+
!lowercaseName.includes('type')) {
|
|
1036
|
+
return 'currency';
|
|
1037
|
+
}
|
|
1038
|
+
// Percentage patterns - be more specific
|
|
1039
|
+
if (lowercaseName.includes('percent') ||
|
|
1040
|
+
lowercaseName.endsWith('_rate') ||
|
|
1041
|
+
lowercaseName.endsWith('_ratio') ||
|
|
1042
|
+
lowercaseName.endsWith('_pct') ||
|
|
1043
|
+
(lowercaseName.includes('rate') &&
|
|
1044
|
+
!lowercaseName.includes('created') &&
|
|
1045
|
+
!lowercaseName.includes('updated')) ||
|
|
1046
|
+
lowercaseName.endsWith('_score')) {
|
|
1047
|
+
return 'percentage';
|
|
1048
|
+
}
|
|
1049
|
+
// Number patterns - exclude common false positives like string IDs
|
|
1050
|
+
if ((lowercaseName.endsWith('_id') && lowercaseName !== 'id') || // composite IDs might be strings
|
|
1051
|
+
lowercaseName === 'id' ||
|
|
1052
|
+
lowercaseName.includes('count') ||
|
|
1053
|
+
lowercaseName.includes('quantity') ||
|
|
1054
|
+
lowercaseName.includes('number') ||
|
|
1055
|
+
lowercaseName.includes('total') ||
|
|
1056
|
+
lowercaseName.includes('sum') ||
|
|
1057
|
+
lowercaseName.includes('age') ||
|
|
1058
|
+
lowercaseName.includes('weight') ||
|
|
1059
|
+
lowercaseName.includes('height') ||
|
|
1060
|
+
lowercaseName.includes('size') ||
|
|
1061
|
+
lowercaseName.endsWith('_num') ||
|
|
1062
|
+
lowercaseName.endsWith('_number')) {
|
|
1063
|
+
return 'number';
|
|
1064
|
+
}
|
|
1065
|
+
// Default to string for ambiguous cases
|
|
1066
|
+
return 'string';
|
|
1067
|
+
}
|
|
1068
|
+
applySchemaFieldPresentation(field, col) {
|
|
1069
|
+
if (!field || !col)
|
|
1070
|
+
return;
|
|
1071
|
+
const controlType = this.normalizePresentationToken(field.controlType);
|
|
1072
|
+
const numericFormat = this.normalizePresentationToken(field.numericFormat ?? field.format);
|
|
1073
|
+
const valuePresentationType = this.normalizePresentationToken(field.valuePresentation?.type);
|
|
1074
|
+
const displayFormat = this.firstNonEmptyString(field.displayFormat, field.extraProperties?.displayFormat);
|
|
1075
|
+
const valueMapping = this.resolveSchemaValueMapping(field);
|
|
1076
|
+
if (valueMapping) {
|
|
1077
|
+
col.valueMapping = valueMapping;
|
|
1078
|
+
}
|
|
1079
|
+
const presentation = field.presentation;
|
|
1080
|
+
if (presentation?.presenter === 'microVisualization' &&
|
|
1081
|
+
isPraxisPresentationVisualizationTableSafe(presentation?.visualization)) {
|
|
1082
|
+
col.renderer = {
|
|
1083
|
+
...(col.renderer || {}),
|
|
1084
|
+
type: 'microVisualization',
|
|
1085
|
+
microVisualization: {
|
|
1086
|
+
visualization: presentation.visualization,
|
|
1087
|
+
},
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
this.applySchemaSemanticCellPresentation(field, col, presentation);
|
|
1091
|
+
if (numericFormat === 'currency' || controlType === 'currency' || col.type === 'currency') {
|
|
1092
|
+
col.type = 'currency';
|
|
1093
|
+
col.format = this.resolveSchemaCurrencyFormat(field);
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
if (numericFormat === 'date' ||
|
|
1097
|
+
numericFormat === 'datetime' ||
|
|
1098
|
+
valuePresentationType === 'date' ||
|
|
1099
|
+
valuePresentationType === 'datetime' ||
|
|
1100
|
+
controlType === 'datepicker' ||
|
|
1101
|
+
controlType === 'datetimepicker' ||
|
|
1102
|
+
controlType === 'datetimelocal' ||
|
|
1103
|
+
controlType === 'date' ||
|
|
1104
|
+
controlType === 'datetime') {
|
|
1105
|
+
col.type = 'date';
|
|
1106
|
+
col.format = displayFormat || (numericFormat === 'datetime' ||
|
|
1107
|
+
valuePresentationType === 'datetime' ||
|
|
1108
|
+
controlType === 'datetimepicker' ||
|
|
1109
|
+
controlType === 'datetimelocal' ||
|
|
1110
|
+
controlType === 'datetime'
|
|
1111
|
+
? 'dd/MM/yyyy HH:mm'
|
|
1112
|
+
: 'dd/MM/yyyy');
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
if (col.type === 'boolean' && !col.format) {
|
|
1116
|
+
col.format = this.resolveSchemaBooleanFormat(field);
|
|
1117
|
+
}
|
|
1118
|
+
const schemaDataType = this.normalizePresentationToken(field.type);
|
|
1119
|
+
const schemaFormat = this.normalizePresentationToken(field.format);
|
|
1120
|
+
if (col.type === 'number' &&
|
|
1121
|
+
!col.format &&
|
|
1122
|
+
(schemaDataType === 'integer' || schemaFormat === 'int32' || schemaFormat === 'int64')) {
|
|
1123
|
+
col.format = '1.0-0';
|
|
1124
|
+
}
|
|
1125
|
+
const textMaskFormat = resolveTextMaskFormatFromFieldDefinition(field);
|
|
1126
|
+
const maskResolvedType = resolveColumnTypeFromFieldDefinition(field, col.type);
|
|
1127
|
+
if (textMaskFormat && maskResolvedType === 'string') {
|
|
1128
|
+
col.type = 'string';
|
|
1129
|
+
col.format = textMaskFormat;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
applyAutoRenderer(field, col) {
|
|
1133
|
+
if (!field || !col)
|
|
1134
|
+
return;
|
|
1135
|
+
// Do not override explicit renderer if any (defensive; initial columns have none)
|
|
1136
|
+
if (col.renderer && col.renderer.type)
|
|
1137
|
+
return;
|
|
1138
|
+
const ctl = (field.controlType || '').toString().trim().toLowerCase();
|
|
1139
|
+
const fname = (field.name || '').toString();
|
|
1140
|
+
const lname = fname.toLowerCase();
|
|
1141
|
+
const isAvatarByControl = ctl === 'avatar';
|
|
1142
|
+
if (isAvatarByControl) {
|
|
1143
|
+
col.renderer = {
|
|
1144
|
+
type: 'avatar',
|
|
1145
|
+
avatar: {
|
|
1146
|
+
srcField: fname,
|
|
1147
|
+
initialsField: 'nomeCompleto',
|
|
1148
|
+
shape: 'circle',
|
|
1149
|
+
size: 32,
|
|
1150
|
+
},
|
|
1151
|
+
};
|
|
1152
|
+
// Sensible defaults for avatar column
|
|
1153
|
+
col.align = col.align || 'center';
|
|
1154
|
+
if (!col.width)
|
|
1155
|
+
col.width = '56px';
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
toPortableColumnDefinition(column) {
|
|
1160
|
+
const cloned = this.clonePlainObject(column);
|
|
1161
|
+
delete cloned['_isApiField'];
|
|
1162
|
+
delete cloned['_originalApiType'];
|
|
1163
|
+
return cloned;
|
|
1164
|
+
}
|
|
1165
|
+
normalizePresentationToken(value) {
|
|
1166
|
+
return String(value ?? '')
|
|
1167
|
+
.trim()
|
|
1168
|
+
.toLowerCase()
|
|
1169
|
+
.replace(/[_\-\s]/g, '');
|
|
1170
|
+
}
|
|
1171
|
+
firstNonEmptyString(...values) {
|
|
1172
|
+
for (const value of values) {
|
|
1173
|
+
if (typeof value !== 'string')
|
|
1174
|
+
continue;
|
|
1175
|
+
const trimmed = value.trim();
|
|
1176
|
+
if (trimmed)
|
|
1177
|
+
return trimmed;
|
|
1178
|
+
}
|
|
1179
|
+
return undefined;
|
|
1180
|
+
}
|
|
1181
|
+
resolveSchemaValueMapping(field) {
|
|
1182
|
+
const explicit = field.valueMapping;
|
|
1183
|
+
if (explicit && typeof explicit === 'object' && !Array.isArray(explicit)) {
|
|
1184
|
+
const normalized = Object.entries(explicit).reduce((acc, [key, value]) => {
|
|
1185
|
+
if (key && typeof value === 'string' && value.trim()) {
|
|
1186
|
+
acc[key] = value.trim();
|
|
1187
|
+
}
|
|
1188
|
+
return acc;
|
|
1189
|
+
}, {});
|
|
1190
|
+
if (Object.keys(normalized).length)
|
|
1191
|
+
return normalized;
|
|
1192
|
+
}
|
|
1193
|
+
const optionMapping = this.resolveSchemaOptionsValueMapping(field);
|
|
1194
|
+
if (optionMapping)
|
|
1195
|
+
return optionMapping;
|
|
1196
|
+
const enumValues = this.normalizeEnumValues(field.enumValues) || [];
|
|
1197
|
+
if (enumValues.length) {
|
|
1198
|
+
return enumValues.reduce((acc, value) => {
|
|
1199
|
+
acc[value] = this.humanizeSchemaEnumValue(value);
|
|
1200
|
+
return acc;
|
|
1201
|
+
}, {});
|
|
1202
|
+
}
|
|
1203
|
+
return undefined;
|
|
1204
|
+
}
|
|
1205
|
+
applySchemaSemanticCellPresentation(field, col, presentation) {
|
|
1206
|
+
if (!presentation || typeof presentation !== 'object')
|
|
1207
|
+
return;
|
|
1208
|
+
if (col.renderer?.type)
|
|
1209
|
+
return;
|
|
1210
|
+
const presenter = this.normalizePresentationToken(presentation.presenter ?? presentation.variant);
|
|
1211
|
+
const textField = field.name;
|
|
1212
|
+
const color = this.resolveSemanticPresentationColor(presentation.tone);
|
|
1213
|
+
const variant = this.resolveSemanticPresentationVariant(presentation.appearance);
|
|
1214
|
+
const icon = this.firstNonEmptyString(presentation.icon);
|
|
1215
|
+
const tooltipText = this.firstNonEmptyString(presentation.tooltip);
|
|
1216
|
+
const tooltip = tooltipText ? { text: tooltipText } : undefined;
|
|
1217
|
+
const fixedText = this.firstNonEmptyString(presentation.label);
|
|
1218
|
+
if (presenter === 'chip') {
|
|
1219
|
+
col.renderer = {
|
|
1220
|
+
type: 'chip',
|
|
1221
|
+
chip: {
|
|
1222
|
+
text: fixedText,
|
|
1223
|
+
textField: fixedText ? undefined : textField,
|
|
1224
|
+
color,
|
|
1225
|
+
icon,
|
|
1226
|
+
variant,
|
|
1227
|
+
tooltip,
|
|
1228
|
+
},
|
|
1229
|
+
};
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
if (presenter === 'badge' || presenter === 'status') {
|
|
1233
|
+
col.renderer = {
|
|
1234
|
+
type: 'badge',
|
|
1235
|
+
badge: {
|
|
1236
|
+
text: fixedText,
|
|
1237
|
+
textField: fixedText ? undefined : textField,
|
|
1238
|
+
color,
|
|
1239
|
+
icon,
|
|
1240
|
+
variant,
|
|
1241
|
+
tooltip,
|
|
1242
|
+
},
|
|
1243
|
+
};
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
if (presenter === 'iconvalue') {
|
|
1247
|
+
const rendererIcon = icon || this.firstNonEmptyString(field.icon);
|
|
1248
|
+
const affixes = this.resolveSemanticPresentationAffixes(presentation);
|
|
1249
|
+
const hasAffixes = !!(affixes.prefix || affixes.suffix);
|
|
1250
|
+
if (!rendererIcon && !hasAffixes)
|
|
1251
|
+
return;
|
|
1252
|
+
if (affixes.prefix || affixes.suffix || tooltipText) {
|
|
1253
|
+
col.renderer = {
|
|
1254
|
+
type: 'icon',
|
|
1255
|
+
icon: {
|
|
1256
|
+
...(!hasAffixes && rendererIcon ? { name: rendererIcon } : {}),
|
|
1257
|
+
textField,
|
|
1258
|
+
...(tooltipText ? { ariaLabel: tooltipText } : {}),
|
|
1259
|
+
...affixes,
|
|
1260
|
+
},
|
|
1261
|
+
};
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
if (!rendererIcon)
|
|
1265
|
+
return;
|
|
1266
|
+
col.renderer = {
|
|
1267
|
+
type: 'compose',
|
|
1268
|
+
compose: {
|
|
1269
|
+
items: [
|
|
1270
|
+
{
|
|
1271
|
+
type: 'icon',
|
|
1272
|
+
icon: {
|
|
1273
|
+
name: rendererIcon,
|
|
1274
|
+
...(color ? { color } : {}),
|
|
1275
|
+
...(field.label ? { ariaLabel: field.label } : {}),
|
|
1276
|
+
},
|
|
1277
|
+
},
|
|
1278
|
+
{
|
|
1279
|
+
type: 'value',
|
|
1280
|
+
field: textField,
|
|
1281
|
+
},
|
|
1282
|
+
],
|
|
1283
|
+
layout: {
|
|
1284
|
+
direction: 'row',
|
|
1285
|
+
gap: 6,
|
|
1286
|
+
align: 'center',
|
|
1287
|
+
},
|
|
1288
|
+
},
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
resolveSchemaCurrencyFormat(field) {
|
|
1293
|
+
const currency = this.firstNonEmptyString(field.currency, field.extraProperties?.currency) || 'BRL';
|
|
1294
|
+
const decimalPlaces = this.firstFiniteNumber(field.decimalPlaces, field.extraProperties?.decimalPlaces) ?? 2;
|
|
1295
|
+
return `${currency}|symbol|${decimalPlaces}`;
|
|
1296
|
+
}
|
|
1297
|
+
resolveSchemaBooleanFormat(field) {
|
|
1298
|
+
const explicit = this.firstNonEmptyString(field.booleanFormat, field.extraProperties?.booleanFormat);
|
|
1299
|
+
if (explicit)
|
|
1300
|
+
return explicit;
|
|
1301
|
+
const name = this.normalizePresentationToken(field.name);
|
|
1302
|
+
if (name === 'ativo' || name === 'active' || name.endsWith('ativo')) {
|
|
1303
|
+
return 'active-inactive';
|
|
1304
|
+
}
|
|
1305
|
+
return 'true-false';
|
|
1306
|
+
}
|
|
1307
|
+
clonePlainObject(value) {
|
|
1308
|
+
try {
|
|
1309
|
+
return JSON.parse(JSON.stringify(value));
|
|
1310
|
+
}
|
|
1311
|
+
catch {
|
|
1312
|
+
return value;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
resolveSchemaOptionsValueMapping(field) {
|
|
1316
|
+
const options = this.normalizeSchemaOptions(field.options) || [];
|
|
1317
|
+
if (!options.length)
|
|
1318
|
+
return undefined;
|
|
1319
|
+
const valueKey = this.normalizeSchemaKey(field.optionValueKey) || 'key';
|
|
1320
|
+
const labelKey = this.normalizeSchemaKey(field.optionLabelKey) || 'value';
|
|
1321
|
+
const mapping = options.reduce((acc, option) => {
|
|
1322
|
+
if (!option || typeof option !== 'object')
|
|
1323
|
+
return acc;
|
|
1324
|
+
const value = option[valueKey]
|
|
1325
|
+
?? option.key
|
|
1326
|
+
?? option.id
|
|
1327
|
+
?? option.value;
|
|
1328
|
+
const label = option[labelKey]
|
|
1329
|
+
?? option.label
|
|
1330
|
+
?? option.value
|
|
1331
|
+
?? option.name;
|
|
1332
|
+
if (value !== undefined && value !== null && label !== undefined && label !== null) {
|
|
1333
|
+
acc[String(value)] = String(label);
|
|
1334
|
+
}
|
|
1335
|
+
return acc;
|
|
1336
|
+
}, {});
|
|
1337
|
+
return Object.keys(mapping).length ? mapping : undefined;
|
|
1338
|
+
}
|
|
1339
|
+
normalizeEnumValues(values) {
|
|
1340
|
+
if (!Array.isArray(values) || values.length === 0)
|
|
1341
|
+
return undefined;
|
|
1342
|
+
const normalized = values
|
|
1343
|
+
.map((value) => (value != null ? String(value).trim() : ''))
|
|
1344
|
+
.filter((value) => value.length > 0);
|
|
1345
|
+
return normalized.length ? Array.from(new Set(normalized)) : undefined;
|
|
1346
|
+
}
|
|
1347
|
+
humanizeSchemaEnumValue(value) {
|
|
1348
|
+
return value
|
|
1349
|
+
.toLowerCase()
|
|
1350
|
+
.split(/[_\-\s]+/)
|
|
1351
|
+
.filter(Boolean)
|
|
1352
|
+
.map((part, index) => index === 0 ? this.capitalizeSchemaEnumPart(part) : part)
|
|
1353
|
+
.join(' ');
|
|
1354
|
+
}
|
|
1355
|
+
resolveSemanticPresentationColor(tone) {
|
|
1356
|
+
const normalized = this.normalizePresentationToken(tone);
|
|
1357
|
+
if (normalized === 'neutral')
|
|
1358
|
+
return 'basic';
|
|
1359
|
+
if (!normalized)
|
|
1360
|
+
return undefined;
|
|
1361
|
+
if (normalized === 'danger')
|
|
1362
|
+
return 'warn';
|
|
1363
|
+
if (normalized === 'warning')
|
|
1364
|
+
return 'warn';
|
|
1365
|
+
return normalized;
|
|
1366
|
+
}
|
|
1367
|
+
resolveSemanticPresentationVariant(appearance) {
|
|
1368
|
+
const normalized = this.normalizePresentationToken(appearance);
|
|
1369
|
+
if (normalized === 'filled' || normalized === 'outlined' || normalized === 'soft' || normalized === 'plain') {
|
|
1370
|
+
return normalized;
|
|
1371
|
+
}
|
|
1372
|
+
return undefined;
|
|
1373
|
+
}
|
|
1374
|
+
resolveSemanticPresentationAffixes(presentation) {
|
|
1375
|
+
const prefix = this.firstNonEmptyString(presentation.prefix);
|
|
1376
|
+
const suffix = this.firstNonEmptyString(presentation.suffix);
|
|
1377
|
+
return {
|
|
1378
|
+
...(prefix ? { prefix } : {}),
|
|
1379
|
+
...(suffix ? { suffix } : {}),
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
firstFiniteNumber(...values) {
|
|
1383
|
+
for (const value of values) {
|
|
1384
|
+
const numeric = typeof value === 'number' ? value : Number(value);
|
|
1385
|
+
if (Number.isFinite(numeric))
|
|
1386
|
+
return numeric;
|
|
1387
|
+
}
|
|
1388
|
+
return undefined;
|
|
1389
|
+
}
|
|
1390
|
+
normalizeSchemaOptions(options) {
|
|
1391
|
+
if (!Array.isArray(options) || options.length === 0)
|
|
1392
|
+
return undefined;
|
|
1393
|
+
const normalized = options
|
|
1394
|
+
.map((option) => {
|
|
1395
|
+
if (option == null)
|
|
1396
|
+
return null;
|
|
1397
|
+
if (typeof option === 'string') {
|
|
1398
|
+
const text = option.trim();
|
|
1399
|
+
if (!text)
|
|
1400
|
+
return null;
|
|
1401
|
+
return text;
|
|
1402
|
+
}
|
|
1403
|
+
if (typeof option === 'number') {
|
|
1404
|
+
return Number.isFinite(option) ? option : null;
|
|
1405
|
+
}
|
|
1406
|
+
if (typeof option === 'boolean') {
|
|
1407
|
+
return option;
|
|
1408
|
+
}
|
|
1409
|
+
if (typeof option === 'object') {
|
|
1410
|
+
const candidate = option;
|
|
1411
|
+
const hasKey = Object.prototype.hasOwnProperty.call(candidate, 'key');
|
|
1412
|
+
const hasValue = Object.prototype.hasOwnProperty.call(candidate, 'value');
|
|
1413
|
+
if (hasKey || hasValue) {
|
|
1414
|
+
const rawKey = candidate['key'] ?? candidate['value'];
|
|
1415
|
+
const rawValue = candidate['value'] ?? candidate['key'];
|
|
1416
|
+
const key = rawKey != null ? String(rawKey).trim() : '';
|
|
1417
|
+
const value = rawValue != null ? String(rawValue).trim() : '';
|
|
1418
|
+
if (!key && !value)
|
|
1419
|
+
return null;
|
|
1420
|
+
return { ...candidate, key: key || value, value: value || key };
|
|
1421
|
+
}
|
|
1422
|
+
return candidate;
|
|
1423
|
+
}
|
|
1424
|
+
return null;
|
|
1425
|
+
})
|
|
1426
|
+
.filter((option) => option !== null);
|
|
1427
|
+
return normalized.length ? normalized : undefined;
|
|
1428
|
+
}
|
|
1429
|
+
normalizeSchemaKey(value) {
|
|
1430
|
+
const text = value != null ? String(value).trim() : '';
|
|
1431
|
+
return text.length > 0 ? text : undefined;
|
|
1432
|
+
}
|
|
1433
|
+
capitalizeSchemaEnumPart(value) {
|
|
1434
|
+
return value ? `${value.charAt(0).toUpperCase()}${value.slice(1)}` : value;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
const tableSchemaColumnProjector = new TableSchemaColumnProjector();
|
|
1438
|
+
|
|
1439
|
+
/** Same JSON boundary and projection used when opening the native Table editor.
|
|
1440
|
+
* Empty fields preserve native columns, including the disconnected editor case.
|
|
1441
|
+
*/
|
|
1442
|
+
function materializeTableEditorConfig(config, fields) {
|
|
1443
|
+
const materialized = { ...config };
|
|
1444
|
+
if (config.columnProjection?.source === 'schema' && fields.length > 0) {
|
|
1445
|
+
materialized.columns = tableSchemaColumnProjector.projectSchemaColumns(fields, config.columnProjection);
|
|
1446
|
+
}
|
|
1447
|
+
return JSON.parse(JSON.stringify(materialized));
|
|
1448
|
+
}
|
|
1449
|
+
/** Full native document: canonical defaults/action normalization are owned by
|
|
1450
|
+
* createTableAuthoringDocument. This is not a public host executor or attestation.
|
|
1451
|
+
*/
|
|
1452
|
+
function deriveTableEditorDocument(input) {
|
|
1453
|
+
return createTableAuthoringDocument({
|
|
1454
|
+
config: materializeTableEditorConfig(input.config, input.fields),
|
|
1455
|
+
bindings: input.bindings,
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
/**
|
|
1460
|
+
* Generated bundle index. Do not edit.
|
|
1461
|
+
*/
|
|
1462
|
+
|
|
1463
|
+
export { buildTableApplyPlan, createTableAuthoringDocument, deriveTableEditorDocument, normalizeTableAuthoringDocument, parseLegacyOrTableDocument, serializeTableAuthoringDocument, toCanonicalTableConfig, validateTableAuthoringDocument };
|