@praxisui/table 10.0.0-rc.36 → 10.0.0-rc.38
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 +4 -0
- package/ai/component-registry.json +304 -119
- package/docs/governed-page-widget-adapter.md +55 -0
- package/fesm2022/praxisui-table-authoring.mjs +409 -27
- package/fesm2022/{praxisui-table-filter-form-dialog-host.component-CN4pADPh.mjs → praxisui-table-filter-form-dialog-host.component-DlGomgO3.mjs} +2 -2
- package/fesm2022/{praxisui-table-praxisui-table-WJvWsf3B.mjs → praxisui-table-praxisui-table-OCwUTNLH.mjs} +2494 -543
- package/fesm2022/{praxisui-table-table-agentic-authoring-turn-flow-CNFpSoP9.mjs → praxisui-table-table-agentic-authoring-turn-flow-a_Au4ZVa.mjs} +1 -1
- package/fesm2022/{praxisui-table-table-ai.adapter-CGb2n3wx.mjs → praxisui-table-table-ai.adapter-DGZMgfsM.mjs} +1 -1
- package/fesm2022/praxisui-table.mjs +1 -1
- package/package.json +10 -10
- package/types/praxisui-table-authoring.d.ts +27 -2
- package/types/praxisui-table.d.ts +110 -8
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Table em páginas governadas
|
|
2
|
+
|
|
3
|
+
O adapter do owner mantém `definition.inputs.config` como a única fonte de configuração do filho.
|
|
4
|
+
A página guarda autoria; o documento nativo é reconstruído a partir desses inputs. A projeção de
|
|
5
|
+
execução é transitória e nunca deve ser gravada de volta no documento da página.
|
|
6
|
+
|
|
7
|
+
O perfil inicial vem do contrato P0 `page-publication-p0` de `praxis-config-starter` (PR #506).
|
|
8
|
+
Core valida estrutura, referências, limites e exige validators dos owners. Este adapter valida o
|
|
9
|
+
perfil nativo local; não autoriza uma fonte, registra uma seed ou substitui a admissão no servidor.
|
|
10
|
+
|
|
11
|
+
`decodeTablePageInputs` reconstrói `praxis.table.editor` v1 com config e somente os bindings
|
|
12
|
+
presentes (`resourcePath`, `horizontalScroll`). `resourcePath: null` é desconexão explícita;
|
|
13
|
+
ausência não é desconexão. Config precisa ser autoria compacta: `columns: []` e
|
|
14
|
+
`columnProjection.source: 'schema'`. Null em qualquer nível de config é negado.
|
|
15
|
+
|
|
16
|
+
`encodeTablePageInputs` é a projeção inversa sem defaults. Campos fora do perfil,
|
|
17
|
+
blocos vazios sem representação nos inputs, identidade/storage e dados de execução recebem
|
|
18
|
+
diagnóstico. O adapter não filtra silenciosamente autoria incompatível.
|
|
19
|
+
|
|
20
|
+
`projectTablePageEditorChanges(authoredInputs, editorBaseline, editorValue)` aplica apenas
|
|
21
|
+
deltas explícitos sobre a autoria original usando o reconciliador nativo do owner. Capture o
|
|
22
|
+
baseline após a normalização/montagem do editor e preserve-o durante aquela sessão. Alterar
|
|
23
|
+
bindings ou contexto do mesmo widget retorna `seed.replacement-required`; a página precisa
|
|
24
|
+
substituir o widget com nova key e nova fonte admitida. A API de projeção altera somente o draft
|
|
25
|
+
que o chamador recebe; ela não salva preferências nem persiste a página.
|
|
26
|
+
|
|
27
|
+
`materializeTablePageRuntimeInputs(inputs, pageId, widgetKey)` usa o helper canônico Core para
|
|
28
|
+
identidades `page:<pageId>:widget:<widgetKey>`, projeta `configPersistenceStrategy: 'volatile'`
|
|
29
|
+
e desabilita customização/copiloto independentes no filho. Submit e dados continuam nos contratos
|
|
30
|
+
de domínio existentes. Não existe input runtime `authoringDocument`.
|
|
31
|
+
|
|
32
|
+
A estratégia `volatile` impede leitura, gravação e limpeza de preferências próprias e conexão
|
|
33
|
+
persistida. Table também repassa o `disablePersistence` existente ao filtro embutido e impede
|
|
34
|
+
reset por chave do filtro; largura, densidade e outros ajustes não escrevem preferências.
|
|
35
|
+
Defaults visuais/materializações transitórias permanecem internos ao runtime e ao editor.
|
|
36
|
+
|
|
37
|
+
Limites: JSON lossless, 256 KiB UTF-8 e profundidade 64 pelo helper Core compartilhado; o documento
|
|
38
|
+
integral da página também tem o mesmo orçamento. Não há host publicado, endpoint de publicação,
|
|
39
|
+
fonte admitida, banco ou release novos neste incremento. A integração Page Builder/host governado
|
|
40
|
+
é trabalho posterior; os wrappers comuns continuam atendendo os fluxos não governados existentes.
|
|
41
|
+
|
|
42
|
+
`volatile` também exclui overlays pessoais de configuração global nos defaults/estado de autoria
|
|
43
|
+
que o owner consome. A instância compartilhada `GlobalConfigService` pode ler sua chave global;
|
|
44
|
+
isso não é prova de zero leitura na aplicação. Tenant, cache, locale e CSP continuam infraestrutura
|
|
45
|
+
do host. O gate comprova ausência de I/O nas chaves do filho e ausência de influência da autoria
|
|
46
|
+
pessoal sobre a configuração publicada. Em Table, `TableDefaultsProvider.getDefaults(id,
|
|
47
|
+
{ includeGlobal: false })` preserva defaults nativos sem duplicá-los; mudanças globais e reset
|
|
48
|
+
seguem a mesma política, inclusive o fallback de timezone usado em regras JSON Logic.
|
|
49
|
+
|
|
50
|
+
Native Table diagnostics historically use dot paths without escaping dynamic override field names.
|
|
51
|
+
For those diagnostics, the page adapter deliberately points to the exact `columnProjection.overrides`
|
|
52
|
+
ancestor rather than inventing a JSON Pointer by splitting a field such as `a.b` or `a[0]`.
|
|
53
|
+
The diagnostic code still names the rejected native constraint.
|
|
54
|
+
|
|
55
|
+
Respostas e filas de preferências capturam a geração, a identidade e a estratégia do filho. Trocar identidade, documento ou estratégia (inclusive A → volatile → A), ou destruir a instância, invalida a continuação antiga antes do próximo I/O ou da materialização. Uma requisição já enviada não é cancelada retroativamente. Respostas tardias de erro também passam pelo mesmo fence.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { validateRichContentDocument, isSafeRichContentClassName } from '@praxisui/rich-content';
|
|
2
2
|
import { resolveColumnTypeFromFieldDefinition, deepMerge, isPraxisPresentationVisualizationTableSafe, resolveTextMaskFormatFromFieldDefinition } from '@praxisui/core/schema';
|
|
3
|
+
import { uiLayoutJsonIdentity, cloneBoundedUiLayoutJsonDocument, derivePageChildRuntimeId } from '@praxisui/core';
|
|
3
4
|
|
|
4
5
|
const ORIGIN = '__praxisAuthoredDisabled';
|
|
5
6
|
const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
@@ -32,18 +33,28 @@ function identity(action) {
|
|
|
32
33
|
}
|
|
33
34
|
/** Projects only canonical action slots, never arbitrary business payloads or callback values. */
|
|
34
35
|
function projectTableActionAuthoring(config, authored, report) {
|
|
36
|
+
const hasAuthoredBaseline = authored !== undefined && authored !== null;
|
|
35
37
|
function actions(value, original, path) {
|
|
36
38
|
if (!Array.isArray(value))
|
|
37
39
|
return value;
|
|
38
40
|
const originals = Array.isArray(original) ? original.filter(isAction) : [];
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
const projected = [];
|
|
42
|
+
value.forEach((item, index) => {
|
|
43
|
+
if (!isAction(item)) {
|
|
44
|
+
projected.push(item);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
42
47
|
const key = identity(item);
|
|
43
48
|
const matches = key ? originals.filter(candidate => identity(candidate) === key) : [];
|
|
44
49
|
// No label matching, positional matching or guessing across duplicate identities.
|
|
45
50
|
const source = matches.length === 1 && value.filter(candidate => isAction(candidate) && identity(candidate) === key).length === 1
|
|
46
51
|
? matches[0] : undefined;
|
|
52
|
+
const isRuntimeSynthesizedOnly = Boolean(item['__praxisDiscoveredAction']) ||
|
|
53
|
+
Boolean(item['__praxisBulkSyncAction']) ||
|
|
54
|
+
(hasAuthoredBaseline && Boolean(item['__praxisCollectionCreateAction']));
|
|
55
|
+
if (isRuntimeSynthesizedOnly && !source) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
47
58
|
const result = { ...item };
|
|
48
59
|
if (hasRuntimeState(item)) {
|
|
49
60
|
const origin = source && !hasRuntimeState(source) ? authoredDisabled(source) : authoredDisabled(item);
|
|
@@ -63,8 +74,9 @@ function projectTableActionAuthoring(config, authored, report) {
|
|
|
63
74
|
}
|
|
64
75
|
if (hasOwn(item, 'children'))
|
|
65
76
|
result['children'] = actions(item['children'], source?.['children'], `${path}[${index}].children`);
|
|
66
|
-
|
|
77
|
+
projected.push(result);
|
|
67
78
|
});
|
|
79
|
+
return projected;
|
|
68
80
|
}
|
|
69
81
|
const result = { ...config };
|
|
70
82
|
if (config?.toolbar)
|
|
@@ -1241,69 +1253,113 @@ class TableSchemaColumnProjector {
|
|
|
1241
1253
|
}
|
|
1242
1254
|
inferFieldTypeFromFieldName(fieldName) {
|
|
1243
1255
|
const lowercaseName = fieldName.toLowerCase();
|
|
1244
|
-
// Date/time patterns - more specific patterns first
|
|
1256
|
+
// Date/time patterns - more specific patterns first (EN + PT-BR)
|
|
1245
1257
|
if (lowercaseName.endsWith('date') ||
|
|
1246
1258
|
lowercaseName.endsWith('time') ||
|
|
1247
1259
|
lowercaseName.endsWith('at') ||
|
|
1248
1260
|
lowercaseName.startsWith('date') ||
|
|
1261
|
+
lowercaseName.startsWith('data') ||
|
|
1262
|
+
lowercaseName.startsWith('dt_') ||
|
|
1263
|
+
lowercaseName.startsWith('dt') ||
|
|
1264
|
+
lowercaseName.endsWith('_em') ||
|
|
1249
1265
|
lowercaseName === 'created' ||
|
|
1250
1266
|
lowercaseName === 'updated' ||
|
|
1251
1267
|
lowercaseName === 'modified' ||
|
|
1268
|
+
lowercaseName === 'nascimento' ||
|
|
1269
|
+
lowercaseName === 'admissao' ||
|
|
1270
|
+
lowercaseName === 'demissao' ||
|
|
1271
|
+
lowercaseName === 'vencimento' ||
|
|
1252
1272
|
lowercaseName.includes('timestamp')) {
|
|
1253
1273
|
return 'date';
|
|
1254
1274
|
}
|
|
1255
|
-
// Boolean patterns - most specific first to avoid conflicts
|
|
1275
|
+
// Boolean patterns - most specific first to avoid conflicts (EN + PT-BR)
|
|
1256
1276
|
if (lowercaseName.startsWith('is_') ||
|
|
1257
1277
|
lowercaseName.startsWith('has_') ||
|
|
1258
1278
|
lowercaseName.startsWith('can_') ||
|
|
1259
1279
|
lowercaseName.startsWith('should_') ||
|
|
1280
|
+
lowercaseName.startsWith('fl_') ||
|
|
1281
|
+
lowercaseName.startsWith('ind_') ||
|
|
1260
1282
|
lowercaseName === 'active' ||
|
|
1283
|
+
lowercaseName === 'ativo' ||
|
|
1284
|
+
lowercaseName === 'inativo' ||
|
|
1261
1285
|
lowercaseName === 'enabled' ||
|
|
1286
|
+
lowercaseName === 'habilitado' ||
|
|
1262
1287
|
lowercaseName === 'visible' ||
|
|
1288
|
+
lowercaseName === 'visivel' ||
|
|
1263
1289
|
lowercaseName === 'deleted' ||
|
|
1290
|
+
lowercaseName === 'excluido' ||
|
|
1264
1291
|
lowercaseName === 'archived' ||
|
|
1292
|
+
lowercaseName === 'arquivado' ||
|
|
1265
1293
|
lowercaseName.endsWith('_flag') ||
|
|
1266
1294
|
lowercaseName.endsWith('_enabled') ||
|
|
1267
|
-
lowercaseName.endsWith('_active')
|
|
1295
|
+
lowercaseName.endsWith('_active') ||
|
|
1296
|
+
lowercaseName.endsWith('_ativo')) {
|
|
1268
1297
|
return 'boolean';
|
|
1269
1298
|
}
|
|
1270
|
-
// Currency/money patterns - exclude common false positives
|
|
1299
|
+
// Currency/money patterns - exclude common false positives (EN + PT-BR)
|
|
1271
1300
|
if ((lowercaseName.includes('price') ||
|
|
1301
|
+
lowercaseName.includes('preco') ||
|
|
1272
1302
|
lowercaseName.includes('amount') ||
|
|
1273
1303
|
lowercaseName.includes('cost') ||
|
|
1304
|
+
lowercaseName.includes('custo') ||
|
|
1274
1305
|
lowercaseName.includes('salary') ||
|
|
1306
|
+
lowercaseName.includes('salario') ||
|
|
1307
|
+
lowercaseName.includes('remuneracao') ||
|
|
1308
|
+
lowercaseName.includes('orcamento') ||
|
|
1309
|
+
lowercaseName.includes('indenizacao') ||
|
|
1310
|
+
lowercaseName.includes('multa') ||
|
|
1311
|
+
lowercaseName.includes('desconto') ||
|
|
1312
|
+
lowercaseName.includes('liquido') ||
|
|
1313
|
+
lowercaseName.includes('bruto') ||
|
|
1275
1314
|
lowercaseName.includes('wage') ||
|
|
1276
1315
|
lowercaseName.includes('fee') ||
|
|
1277
|
-
(lowercaseName.includes('value') &&
|
|
1316
|
+
((lowercaseName.includes('value') || lowercaseName.includes('valor') || lowercaseName.startsWith('vl_')) &&
|
|
1278
1317
|
!lowercaseName.includes('id') &&
|
|
1279
1318
|
!lowercaseName.includes('key'))) &&
|
|
1280
1319
|
!lowercaseName.includes('count') &&
|
|
1281
|
-
!lowercaseName.includes('type')
|
|
1320
|
+
!lowercaseName.includes('type') &&
|
|
1321
|
+
!lowercaseName.includes('tipo')) {
|
|
1282
1322
|
return 'currency';
|
|
1283
1323
|
}
|
|
1284
|
-
// Percentage patterns - be more specific
|
|
1324
|
+
// Percentage patterns - be more specific (EN + PT-BR)
|
|
1285
1325
|
if (lowercaseName.includes('percent') ||
|
|
1326
|
+
lowercaseName.includes('percentual') ||
|
|
1327
|
+
lowercaseName.includes('aliquota') ||
|
|
1328
|
+
lowercaseName.includes('margem') ||
|
|
1329
|
+
lowercaseName.startsWith('pct_') ||
|
|
1286
1330
|
lowercaseName.endsWith('_rate') ||
|
|
1287
1331
|
lowercaseName.endsWith('_ratio') ||
|
|
1288
1332
|
lowercaseName.endsWith('_pct') ||
|
|
1289
|
-
|
|
1333
|
+
lowercaseName.endsWith('_taxa') ||
|
|
1334
|
+
((lowercaseName.includes('rate') || lowercaseName.includes('taxa')) &&
|
|
1290
1335
|
!lowercaseName.includes('created') &&
|
|
1291
1336
|
!lowercaseName.includes('updated')) ||
|
|
1292
1337
|
lowercaseName.endsWith('_score')) {
|
|
1293
1338
|
return 'percentage';
|
|
1294
1339
|
}
|
|
1295
|
-
// Number patterns - exclude common false positives like string IDs
|
|
1340
|
+
// Number patterns - exclude common false positives like string IDs (EN + PT-BR)
|
|
1296
1341
|
if ((lowercaseName.endsWith('_id') && lowercaseName !== 'id') || // composite IDs might be strings
|
|
1297
1342
|
lowercaseName === 'id' ||
|
|
1298
1343
|
lowercaseName.includes('count') ||
|
|
1299
1344
|
lowercaseName.includes('quantity') ||
|
|
1345
|
+
lowercaseName.includes('quantidade') ||
|
|
1346
|
+
lowercaseName.includes('qtd') ||
|
|
1300
1347
|
lowercaseName.includes('number') ||
|
|
1348
|
+
lowercaseName.includes('numero') ||
|
|
1301
1349
|
lowercaseName.includes('total') ||
|
|
1302
1350
|
lowercaseName.includes('sum') ||
|
|
1351
|
+
lowercaseName.includes('soma') ||
|
|
1303
1352
|
lowercaseName.includes('age') ||
|
|
1353
|
+
lowercaseName.includes('idade') ||
|
|
1304
1354
|
lowercaseName.includes('weight') ||
|
|
1355
|
+
lowercaseName.includes('peso') ||
|
|
1305
1356
|
lowercaseName.includes('height') ||
|
|
1357
|
+
lowercaseName.includes('altura') ||
|
|
1306
1358
|
lowercaseName.includes('size') ||
|
|
1359
|
+
lowercaseName.includes('tamanho') ||
|
|
1360
|
+
lowercaseName.includes('pontuacao') ||
|
|
1361
|
+
lowercaseName.includes('score') ||
|
|
1362
|
+
lowercaseName.includes('nota') ||
|
|
1307
1363
|
lowercaseName.endsWith('_num') ||
|
|
1308
1364
|
lowercaseName.endsWith('_number')) {
|
|
1309
1365
|
return 'number';
|
|
@@ -1319,12 +1375,13 @@ class TableSchemaColumnProjector {
|
|
|
1319
1375
|
const valuePresentationType = this.normalizePresentationToken(field.valuePresentation?.type);
|
|
1320
1376
|
const displayFormat = this.firstNonEmptyString(field.displayFormat, field.extraProperties?.displayFormat);
|
|
1321
1377
|
const valueMapping = this.resolveSchemaValueMapping(field);
|
|
1322
|
-
if (valueMapping) {
|
|
1378
|
+
if (valueMapping && (!col.valueMapping || Object.keys(col.valueMapping).length === 0)) {
|
|
1323
1379
|
col.valueMapping = valueMapping;
|
|
1324
1380
|
}
|
|
1325
1381
|
const presentation = field.presentation;
|
|
1326
1382
|
if (presentation?.presenter === 'microVisualization' &&
|
|
1327
|
-
isPraxisPresentationVisualizationTableSafe(presentation?.visualization)
|
|
1383
|
+
isPraxisPresentationVisualizationTableSafe(presentation?.visualization) &&
|
|
1384
|
+
!col.renderer?.type) {
|
|
1328
1385
|
col.renderer = {
|
|
1329
1386
|
...(col.renderer || {}),
|
|
1330
1387
|
type: 'microVisualization',
|
|
@@ -1335,8 +1392,11 @@ class TableSchemaColumnProjector {
|
|
|
1335
1392
|
}
|
|
1336
1393
|
this.applySchemaSemanticCellPresentation(field, col, presentation);
|
|
1337
1394
|
if (numericFormat === 'currency' || controlType === 'currency' || col.type === 'currency') {
|
|
1395
|
+
const wasCurrency = col.type === 'currency';
|
|
1338
1396
|
col.type = 'currency';
|
|
1339
|
-
col.format
|
|
1397
|
+
if (!col.format || (!wasCurrency && !String(col.format).includes('|'))) {
|
|
1398
|
+
col.format = this.resolveSchemaCurrencyFormat(field);
|
|
1399
|
+
}
|
|
1340
1400
|
return;
|
|
1341
1401
|
}
|
|
1342
1402
|
if (numericFormat === 'date' ||
|
|
@@ -1349,13 +1409,15 @@ class TableSchemaColumnProjector {
|
|
|
1349
1409
|
controlType === 'date' ||
|
|
1350
1410
|
controlType === 'datetime') {
|
|
1351
1411
|
col.type = 'date';
|
|
1352
|
-
col.format
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1412
|
+
if (!col.format) {
|
|
1413
|
+
col.format = displayFormat || (numericFormat === 'datetime' ||
|
|
1414
|
+
valuePresentationType === 'datetime' ||
|
|
1415
|
+
controlType === 'datetimepicker' ||
|
|
1416
|
+
controlType === 'datetimelocal' ||
|
|
1417
|
+
controlType === 'datetime'
|
|
1418
|
+
? 'dd/MM/yyyy HH:mm'
|
|
1419
|
+
: 'dd/MM/yyyy');
|
|
1420
|
+
}
|
|
1359
1421
|
return;
|
|
1360
1422
|
}
|
|
1361
1423
|
if (col.type === 'boolean' && !col.format) {
|
|
@@ -1364,7 +1426,10 @@ class TableSchemaColumnProjector {
|
|
|
1364
1426
|
const schemaDataType = this.normalizePresentationToken(field.type);
|
|
1365
1427
|
const schemaFormat = this.normalizePresentationToken(field.format);
|
|
1366
1428
|
if (col.type === 'number' && !col.format) {
|
|
1367
|
-
if (
|
|
1429
|
+
if (displayFormat) {
|
|
1430
|
+
col.format = displayFormat;
|
|
1431
|
+
}
|
|
1432
|
+
else if (schemaDataType === 'integer' ||
|
|
1368
1433
|
schemaFormat === 'integer' ||
|
|
1369
1434
|
schemaFormat === 'int32' ||
|
|
1370
1435
|
schemaFormat === 'int64' ||
|
|
@@ -1408,10 +1473,12 @@ class TableSchemaColumnProjector {
|
|
|
1408
1473
|
size: 32,
|
|
1409
1474
|
},
|
|
1410
1475
|
};
|
|
1411
|
-
// Sensible defaults for avatar column
|
|
1476
|
+
// Sensible defaults for avatar column (84px prevents header truncation when centered with sort/resize gutters; not sortable by default)
|
|
1412
1477
|
col.align = col.align || 'center';
|
|
1478
|
+
if (col.sortable === undefined)
|
|
1479
|
+
col.sortable = false;
|
|
1413
1480
|
if (!col.width)
|
|
1414
|
-
col.width = '
|
|
1481
|
+
col.width = '84px';
|
|
1415
1482
|
return;
|
|
1416
1483
|
}
|
|
1417
1484
|
}
|
|
@@ -1726,8 +1793,323 @@ function deriveTableEditorDocument(input) {
|
|
|
1726
1793
|
});
|
|
1727
1794
|
}
|
|
1728
1795
|
|
|
1796
|
+
/** Shared presentation ordering; hidden columns must not shift visible fallback positions. */
|
|
1797
|
+
function orderTableColumns(columns) {
|
|
1798
|
+
if (!columns.some(column => Number.isFinite(column.order)))
|
|
1799
|
+
return [...columns];
|
|
1800
|
+
let visibleIndex = 0;
|
|
1801
|
+
return columns.map((column, originalIndex) => {
|
|
1802
|
+
const fallback = column.visible === false ? originalIndex : visibleIndex++;
|
|
1803
|
+
return { column, originalIndex, explicit: Number.isFinite(column.order), order: Number.isFinite(column.order) ? Number(column.order) : fallback };
|
|
1804
|
+
}).sort((left, right) => left.order - right.order
|
|
1805
|
+
|| Number(right.explicit) - Number(left.explicit)
|
|
1806
|
+
|| left.originalIndex - right.originalIndex).map(entry => entry.column);
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
/** Preserve authored values; runtime/schema defaults remain transient unless explicitly edited. */
|
|
1810
|
+
function applyTableAuthoringChanges(authored, baseline, edited) {
|
|
1811
|
+
return projectChanges(authored, baseline, edited, true);
|
|
1812
|
+
}
|
|
1813
|
+
function projectChanges(authored, baseline, edited, tableConfigRoot) {
|
|
1814
|
+
if (sameAuthoringValue(baseline, edited))
|
|
1815
|
+
return authored;
|
|
1816
|
+
if (!edited || typeof edited !== 'object' || Array.isArray(edited))
|
|
1817
|
+
return edited;
|
|
1818
|
+
const original = authored && typeof authored === 'object' ? authored : {};
|
|
1819
|
+
const before = baseline && typeof baseline === 'object' ? baseline : {};
|
|
1820
|
+
const after = edited;
|
|
1821
|
+
const result = { ...original };
|
|
1822
|
+
for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) {
|
|
1823
|
+
if (sameAuthoringValue(before[key], after[key]))
|
|
1824
|
+
continue;
|
|
1825
|
+
if (!Object.hasOwn(after, key))
|
|
1826
|
+
delete result[key];
|
|
1827
|
+
else
|
|
1828
|
+
Object.defineProperty(result, key, {
|
|
1829
|
+
value: projectChanges(Object.hasOwn(original, key) ? original[key] : undefined, Object.hasOwn(before, key) ? before[key] : undefined, after[key], false),
|
|
1830
|
+
enumerable: true, configurable: true, writable: true,
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
if (tableConfigRoot && isRecord(original['columnProjection']) && original['columnProjection']['source'] === 'schema'
|
|
1834
|
+
&& isRecord(result['columnProjection']) && result['columnProjection']['source'] === 'schema'
|
|
1835
|
+
&& Array.isArray(after['columns']) && JSON.stringify(before['columns']) !== JSON.stringify(after['columns'])) {
|
|
1836
|
+
result['columns'] = [];
|
|
1837
|
+
result['columnProjection'] = projectSchemaColumnChanges(result['columnProjection'], before['columns'], after['columns'], isRecord(before['columnProjection']) ? before['columnProjection'] : original['columnProjection'], after['columnProjection']);
|
|
1838
|
+
}
|
|
1839
|
+
return result;
|
|
1840
|
+
}
|
|
1841
|
+
/** Write the existing compact schema projection, never a partial columns snapshot. */
|
|
1842
|
+
function projectSchemaColumnChanges(authored, baseline, edited, baselineProjection, editedProjection) {
|
|
1843
|
+
if (!isRecord(authored) || authored['source'] !== 'schema') {
|
|
1844
|
+
throw new Error('Table schema authorship requires its canonical columnProjection.');
|
|
1845
|
+
}
|
|
1846
|
+
const projection = structuredClone(authored);
|
|
1847
|
+
const overrides = Object.assign(Object.create(null), isRecord(projection['overrides']) ? projection['overrides'] : {});
|
|
1848
|
+
const additions = columnsByField(Array.isArray(projection['additions']) ? projection['additions'] : []);
|
|
1849
|
+
const before = columnsByField(Array.isArray(baseline) ? baseline : []);
|
|
1850
|
+
const after = columnsByField(edited);
|
|
1851
|
+
const nextAdditions = [...additions].filter(([field]) => !before.has(field) && !after.has(field))
|
|
1852
|
+
.map(([, column]) => structuredClone(column));
|
|
1853
|
+
for (const [field, column] of after) {
|
|
1854
|
+
if (additions.has(field) || !before.has(field)) {
|
|
1855
|
+
nextAdditions.push(structuredClone(column));
|
|
1856
|
+
continue;
|
|
1857
|
+
}
|
|
1858
|
+
const previousOverride = isRecord(overrides[field]) ? overrides[field] : {};
|
|
1859
|
+
if (Object.hasOwn(previousOverride, 'field')) {
|
|
1860
|
+
throw new Error('Table schema overrides cannot replace canonical field identity.');
|
|
1861
|
+
}
|
|
1862
|
+
const projected = projectChanges({ field, ...previousOverride }, before.get(field), column, false);
|
|
1863
|
+
const override = isRecord(projected) ? { ...projected } : {};
|
|
1864
|
+
// A direct edit of native intent is stronger evidence than a rematerialized snapshot.
|
|
1865
|
+
const baselineOverrides = isRecord(baselineProjection) && isRecord(baselineProjection['overrides'])
|
|
1866
|
+
? baselineProjection['overrides'] : {};
|
|
1867
|
+
const editedOverrides = isRecord(editedProjection) && isRecord(editedProjection['overrides'])
|
|
1868
|
+
? editedProjection['overrides'] : {};
|
|
1869
|
+
const oldIntent = isRecord(baselineOverrides[field]) ? baselineOverrides[field] : {};
|
|
1870
|
+
const newIntent = isRecord(editedOverrides[field]) ? editedOverrides[field] : {};
|
|
1871
|
+
if (isRecord(editedProjection)) {
|
|
1872
|
+
for (const key of new Set([...Object.keys(oldIntent), ...Object.keys(newIntent)])) {
|
|
1873
|
+
if (!changedNativeValue(oldIntent, newIntent, key))
|
|
1874
|
+
continue;
|
|
1875
|
+
if (Object.hasOwn(previousOverride, key))
|
|
1876
|
+
override[key] = structuredClone(previousOverride[key]);
|
|
1877
|
+
else
|
|
1878
|
+
delete override[key];
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
delete override['field'];
|
|
1882
|
+
delete override['order']; // Ordering has a dedicated projection field.
|
|
1883
|
+
if (Object.keys(override).length)
|
|
1884
|
+
overrides[field] = override;
|
|
1885
|
+
else
|
|
1886
|
+
delete overrides[field];
|
|
1887
|
+
}
|
|
1888
|
+
// Missing fields do not erase their overrides; membership and intent are separate.
|
|
1889
|
+
const previousOrder = orderedFields(before, additions);
|
|
1890
|
+
const nextOrder = orderedFields(after, additions).filter(field => before.has(field));
|
|
1891
|
+
if (!changedNativeValue(baselineProjection, editedProjection, 'include')
|
|
1892
|
+
&& previousOrder.some(field => !nextOrder.includes(field))) {
|
|
1893
|
+
const include = Array.isArray(projection['include']) ? projection['include'] : [];
|
|
1894
|
+
projection['include'] = [...nextOrder, ...include.filter(field => !before.has(field))];
|
|
1895
|
+
}
|
|
1896
|
+
if (!changedNativeValue(baselineProjection, editedProjection, 'order')
|
|
1897
|
+
&& JSON.stringify(previousOrder) !== JSON.stringify(nextOrder)) {
|
|
1898
|
+
const order = Array.isArray(projection['order']) ? projection['order'] : [];
|
|
1899
|
+
projection['order'] = [...nextOrder, ...order.filter(field => !before.has(field) && !nextOrder.includes(field))];
|
|
1900
|
+
}
|
|
1901
|
+
if (Object.keys(overrides).length)
|
|
1902
|
+
projection['overrides'] = overrides;
|
|
1903
|
+
else
|
|
1904
|
+
delete projection['overrides'];
|
|
1905
|
+
if (nextAdditions.length)
|
|
1906
|
+
projection['additions'] = nextAdditions;
|
|
1907
|
+
else
|
|
1908
|
+
delete projection['additions'];
|
|
1909
|
+
return projection;
|
|
1910
|
+
}
|
|
1911
|
+
function changedNativeValue(before, after, key) {
|
|
1912
|
+
if (!isRecord(after))
|
|
1913
|
+
return false;
|
|
1914
|
+
const previous = isRecord(before) ? before : {};
|
|
1915
|
+
return Object.hasOwn(previous, key) !== Object.hasOwn(after, key)
|
|
1916
|
+
|| !sameAuthoringValue(previous[key], after[key]);
|
|
1917
|
+
}
|
|
1918
|
+
function orderedFields(columns, additions) {
|
|
1919
|
+
return orderTableColumns([...columns].map(([field, column]) => ({
|
|
1920
|
+
field, order: typeof column['order'] === 'number' ? column['order'] : undefined,
|
|
1921
|
+
visible: typeof column['visible'] === 'boolean' ? column['visible'] : undefined,
|
|
1922
|
+
}))).map(column => column.field).filter(field => !additions.has(field));
|
|
1923
|
+
}
|
|
1924
|
+
function columnsByField(columns) {
|
|
1925
|
+
const result = new Map();
|
|
1926
|
+
for (const column of columns) {
|
|
1927
|
+
if (!isRecord(column) || typeof column['field'] !== 'string' || !column['field'].trim()) {
|
|
1928
|
+
throw new Error('Table authoring requires a non-empty column field identity.');
|
|
1929
|
+
}
|
|
1930
|
+
const identity = column['field'].trim();
|
|
1931
|
+
if (result.has(identity))
|
|
1932
|
+
throw new Error('Table authoring requires unique column field identities.');
|
|
1933
|
+
result.set(identity, column);
|
|
1934
|
+
}
|
|
1935
|
+
return result;
|
|
1936
|
+
}
|
|
1937
|
+
function isRecord(value) {
|
|
1938
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
1939
|
+
}
|
|
1940
|
+
/** Object insertion order is not authored intent; undefined is an internal absence sentinel. */
|
|
1941
|
+
function sameAuthoringValue(left, right) {
|
|
1942
|
+
if (left === undefined || right === undefined)
|
|
1943
|
+
return left === right;
|
|
1944
|
+
try {
|
|
1945
|
+
return uiLayoutJsonIdentity(left) === uiLayoutJsonIdentity(right);
|
|
1946
|
+
}
|
|
1947
|
+
catch {
|
|
1948
|
+
if (Array.isArray(left) || Array.isArray(right))
|
|
1949
|
+
return Array.isArray(left) && Array.isArray(right)
|
|
1950
|
+
&& left.length === right.length && left.every((value, index) => sameAuthoringValue(value, right[index]));
|
|
1951
|
+
if (!isRecord(left) || !isRecord(right))
|
|
1952
|
+
return left === right;
|
|
1953
|
+
const keys = Object.keys(left).filter(key => left[key] !== undefined);
|
|
1954
|
+
const otherKeys = Object.keys(right).filter(key => right[key] !== undefined);
|
|
1955
|
+
return keys.length === otherKeys.length && keys.every(key => Object.prototype.hasOwnProperty.call(right, key)
|
|
1956
|
+
&& sameAuthoringValue(left[key], right[key]));
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
const BINDINGS = ['resourcePath', 'horizontalScroll'];
|
|
1961
|
+
const INPUTS = ['config', ...BINDINGS];
|
|
1962
|
+
/** Raw compact schema authorship only; native normalization remains an editor/runtime projection. */
|
|
1963
|
+
function decodeTablePageInputs(value) {
|
|
1964
|
+
let inputs;
|
|
1965
|
+
try {
|
|
1966
|
+
inputs = cloneBoundedUiLayoutJsonDocument(value);
|
|
1967
|
+
}
|
|
1968
|
+
catch {
|
|
1969
|
+
return { document: null, diagnostics: [problem('table.page.json.invalid', '/definition/inputs')] };
|
|
1970
|
+
}
|
|
1971
|
+
const diagnostics = [];
|
|
1972
|
+
extras(inputs, INPUTS, '/definition/inputs', diagnostics);
|
|
1973
|
+
const config = inputs['config'];
|
|
1974
|
+
if (!record(config) || !Array.isArray(config['columns']) || config['columns'].length !== 0
|
|
1975
|
+
|| !record(config['columnProjection']) || config['columnProjection']['source'] !== 'schema')
|
|
1976
|
+
diagnostics.push(problem('table.page.config.schema-required', '/definition/inputs/config'));
|
|
1977
|
+
if (record(config))
|
|
1978
|
+
nulls(config, '/definition/inputs/config', diagnostics);
|
|
1979
|
+
if (Object.prototype.hasOwnProperty.call(inputs, 'resourcePath') && inputs['resourcePath'] !== null && (typeof inputs['resourcePath'] !== 'string' || [...inputs['resourcePath']].length > 1024))
|
|
1980
|
+
diagnostics.push(problem('table.page.resourcePath.invalid', '/definition/inputs/resourcePath'));
|
|
1981
|
+
if (Object.prototype.hasOwnProperty.call(inputs, 'horizontalScroll') && !['auto', 'wrap', 'none'].includes(inputs['horizontalScroll']))
|
|
1982
|
+
diagnostics.push(problem('table.page.horizontalScroll.invalid', '/definition/inputs/horizontalScroll'));
|
|
1983
|
+
const bindings = Object.fromEntries(BINDINGS.filter(key => Object.prototype.hasOwnProperty.call(inputs, key)).map(key => [key, inputs[key]]));
|
|
1984
|
+
const document = { kind: 'praxis.table.editor', version: 1, config, ...(Object.keys(bindings).length ? { bindings } : {}) };
|
|
1985
|
+
if (!diagnostics.length) {
|
|
1986
|
+
try {
|
|
1987
|
+
diagnostics.push(...validateTableAuthoringDocument(document).filter(d => d.level === 'error').map(d => ({ ...d, path: nativePath(d.path) })));
|
|
1988
|
+
}
|
|
1989
|
+
catch {
|
|
1990
|
+
diagnostics.push(problem('table.page.native.invalid', '/definition/inputs/config'));
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
if (!diagnostics.length) {
|
|
1994
|
+
try {
|
|
1995
|
+
cloneBoundedUiLayoutJsonDocument(document);
|
|
1996
|
+
}
|
|
1997
|
+
catch {
|
|
1998
|
+
diagnostics.push(problem('table.page.json.invalid', '/definition/inputs'));
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
return { document: diagnostics.length ? null : document, diagnostics };
|
|
2002
|
+
}
|
|
2003
|
+
/** Core prefixes these widget-relative JSON Pointers; this callback never resolves a source/latest. */
|
|
2004
|
+
function validateTablePageWidget(widget) {
|
|
2005
|
+
if (widget.definition.id !== 'praxis-table')
|
|
2006
|
+
return [{ code: 'table.page.component.invalid', path: '/definition/id' }];
|
|
2007
|
+
return decodeTablePageInputs(widget.definition.inputs).diagnostics.map(d => ({ code: d.code, path: d.path ?? '/definition/inputs' }));
|
|
2008
|
+
}
|
|
2009
|
+
function encodeTablePageInputs(value) {
|
|
2010
|
+
let document;
|
|
2011
|
+
try {
|
|
2012
|
+
document = cloneBoundedUiLayoutJsonDocument(value);
|
|
2013
|
+
}
|
|
2014
|
+
catch {
|
|
2015
|
+
return { inputs: null, diagnostics: [problem('table.page.json.invalid', '/')] };
|
|
2016
|
+
}
|
|
2017
|
+
const diagnostics = [];
|
|
2018
|
+
extras(document, ['kind', 'version', 'config', 'bindings'], '', diagnostics);
|
|
2019
|
+
if (document['kind'] !== 'praxis.table.editor' || document['version'] !== 1)
|
|
2020
|
+
diagnostics.push(problem('table.page.native.invalid', '/'));
|
|
2021
|
+
const inputs = { config: document['config'] };
|
|
2022
|
+
if (Object.prototype.hasOwnProperty.call(document, 'bindings')) {
|
|
2023
|
+
const bindings = document['bindings'];
|
|
2024
|
+
if (!record(bindings) || !Object.keys(bindings).length)
|
|
2025
|
+
diagnostics.push(problem('table.page.envelope.unrepresentable', '/bindings'));
|
|
2026
|
+
else {
|
|
2027
|
+
extras(bindings, BINDINGS, '/bindings', diagnostics);
|
|
2028
|
+
Object.assign(inputs, bindings);
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
const decoded = diagnostics.length ? null : decodeTablePageInputs(inputs);
|
|
2032
|
+
return { inputs: diagnostics.length || decoded?.diagnostics.length ? null : inputs, diagnostics: [...diagnostics, ...(decoded?.diagnostics ?? [])] };
|
|
2033
|
+
}
|
|
2034
|
+
/** No-change values in a materialized native editor cannot author defaults or erase absent keys. */
|
|
2035
|
+
function projectTablePageEditorChanges(authoredInputs, editorBaseline, editorValue) {
|
|
2036
|
+
const original = decodeTablePageInputs(authoredInputs);
|
|
2037
|
+
if (!original.document)
|
|
2038
|
+
return { inputs: null, diagnostics: original.diagnostics };
|
|
2039
|
+
let baseline;
|
|
2040
|
+
let edited;
|
|
2041
|
+
try {
|
|
2042
|
+
baseline = cloneBoundedUiLayoutJsonDocument(editorBaseline);
|
|
2043
|
+
edited = cloneBoundedUiLayoutJsonDocument(editorValue);
|
|
2044
|
+
}
|
|
2045
|
+
catch {
|
|
2046
|
+
return { inputs: null, diagnostics: [problem('table.page.json.invalid', '/')] };
|
|
2047
|
+
}
|
|
2048
|
+
const authored = original.document;
|
|
2049
|
+
// Native config delta has schema-column projection semantics; envelope delta is plain JSON.
|
|
2050
|
+
let candidate;
|
|
2051
|
+
try {
|
|
2052
|
+
candidate = applyTableAuthoringChanges(authored, baseline, edited);
|
|
2053
|
+
candidate['config'] = applyTableAuthoringChanges(authored['config'], baseline['config'], edited['config']);
|
|
2054
|
+
}
|
|
2055
|
+
catch {
|
|
2056
|
+
return { inputs: null, diagnostics: [problem('table.page.editor.invalid', '/definition/inputs/config')] };
|
|
2057
|
+
}
|
|
2058
|
+
const before = { ...authored };
|
|
2059
|
+
const after = { ...candidate };
|
|
2060
|
+
delete before['config'];
|
|
2061
|
+
delete after['config'];
|
|
2062
|
+
if (uiLayoutJsonIdentity(before) !== uiLayoutJsonIdentity(after))
|
|
2063
|
+
return { inputs: null, diagnostics: [problem('table.page.seed.replacement-required', '/definition/inputs')] };
|
|
2064
|
+
return encodeTablePageInputs(candidate);
|
|
2065
|
+
}
|
|
2066
|
+
function extras(source, allowed, path, diagnostics) {
|
|
2067
|
+
for (const key of Object.keys(source))
|
|
2068
|
+
if (!allowed.includes(key))
|
|
2069
|
+
diagnostics.push(problem('table.page.profile.unsupported', path + '/' + pointer(key)));
|
|
2070
|
+
}
|
|
2071
|
+
function nulls(value, path, diagnostics) {
|
|
2072
|
+
if (value === null)
|
|
2073
|
+
diagnostics.push(problem('table.page.config.null', path));
|
|
2074
|
+
else if (Array.isArray(value))
|
|
2075
|
+
value.forEach((item, index) => nulls(item, path + '/' + index, diagnostics));
|
|
2076
|
+
else if (record(value))
|
|
2077
|
+
Object.entries(value).forEach(([key, item]) => nulls(item, path + '/' + pointer(key), diagnostics));
|
|
2078
|
+
}
|
|
2079
|
+
function pointer(key) { return key.replace(/~/g, '~0').replace(/\//g, '~1'); }
|
|
2080
|
+
function record(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); }
|
|
2081
|
+
function problem(code, path) { return { level: 'error', code, path, message: code }; }
|
|
2082
|
+
/** Derive execution-only identity and disable independent preferences. Never persist this projection. */
|
|
2083
|
+
function materializeTablePageRuntimeInputs(value, pageId, widgetKey) {
|
|
2084
|
+
const decoded = decodeTablePageInputs(value);
|
|
2085
|
+
if (!decoded.document)
|
|
2086
|
+
return { inputs: null, diagnostics: decoded.diagnostics };
|
|
2087
|
+
let id;
|
|
2088
|
+
try {
|
|
2089
|
+
id = derivePageChildRuntimeId(pageId, widgetKey);
|
|
2090
|
+
}
|
|
2091
|
+
catch {
|
|
2092
|
+
return { inputs: null, diagnostics: [problem('table.page.identity.invalid', '/key')] };
|
|
2093
|
+
}
|
|
2094
|
+
return {
|
|
2095
|
+
inputs: { ...cloneBoundedUiLayoutJsonDocument(value), tableId: id, componentInstanceId: id,
|
|
2096
|
+
configPersistenceStrategy: 'volatile', enableCustomization: false },
|
|
2097
|
+
diagnostics: [],
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
function nativePath(value) {
|
|
2101
|
+
// The native dot-path protocol does not escape dynamic field identities. Its
|
|
2102
|
+
// overrides ancestor is precise; splitting that field would invent a pointer.
|
|
2103
|
+
if (value?.startsWith('config.columnProjection.overrides.'))
|
|
2104
|
+
return '/definition/inputs/config/columnProjection/overrides';
|
|
2105
|
+
const parts = (value ?? 'config').replace(/\[(\d+)\]/g, '.$1').split('.');
|
|
2106
|
+
if (parts[0] === 'bindings')
|
|
2107
|
+
parts.shift();
|
|
2108
|
+
return '/definition/inputs/' + parts.map(pointer).join('/');
|
|
2109
|
+
}
|
|
2110
|
+
|
|
1729
2111
|
/**
|
|
1730
2112
|
* Generated bundle index. Do not edit.
|
|
1731
2113
|
*/
|
|
1732
2114
|
|
|
1733
|
-
export { buildTableApplyPlan, createTableAuthoringDocument, deriveTableEditorDocument, normalizeTableAuthoringDocument, parseLegacyOrTableDocument, serializeTableAuthoringDocument, toCanonicalTableConfig, validateTableAuthoringDocument };
|
|
2115
|
+
export { buildTableApplyPlan, createTableAuthoringDocument, decodeTablePageInputs, deriveTableEditorDocument, encodeTablePageInputs, materializeTablePageRuntimeInputs, normalizeTableAuthoringDocument, parseLegacyOrTableDocument, projectTablePageEditorChanges, serializeTableAuthoringDocument, toCanonicalTableConfig, validateTableAuthoringDocument, validateTablePageWidget };
|