@praxisui/core 10.0.0-rc.15 → 10.0.0-rc.17

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 CHANGED
@@ -93,6 +93,15 @@ backend B7 acceptance is complete.
93
93
 
94
94
  `GovernedColorPaletteService` is the canonical Angular client for color-palette projections published by Praxis Config. Published palettes carry a stable `familyKey`, an explicit `variant` identity and token-level `displayName`/`aliases`; incomplete legacy projections are rejected before caching. The service supports list/read/capabilities/preview, exact `list(familyKey)` queries, tenant/environment-isolated caching, conditional ETag revalidation and deterministic `version`/`etag` pins. Consumers may use names and aliases for presentation and discovery only: `paletteKey` and `tokenId` remain the persisted identities, and local metadata must not become a second source of governance. `GovernedColorTokenSelection` carries the exact palette/version/ETag/token observation beside the existing CSS-string field value; applications persist it only through an explicit domain field or mapping.
95
95
 
96
+ ## Declarative Page DataSources (`page.dataSources`)
97
+
98
+ `WidgetPageDefinition.dataSources` and `WidgetPageDataSourceRuntimeService` allow dynamic pages and dashboards to fetch backend KPIs, group-by aggregations, and time-series metrics without imperative host `HttpClient` orchestration:
99
+
100
+ - **Endpoint resolution (`endpointKey` + `resourcePath`)**: resolves URLs through the `API_URL` token using `endpointKey` (falling back to `'default'`) so multi-service enterprise hosts route each DataSource to its target microservice.
101
+ - **Transform presets & JsonLogic**: supports `{ kind: 'path', path }`, `{ kind: 'group-by-map', mappings, totalField }` (initializing declared keys with `0` for sparse buckets), `{ kind: 'timeseries-last', mappings }`, or arbitrary JsonLogic expressions.
102
+ - **Transient state overlay (`targetStatePath`)**: merges resolved payloads into a non-persisted runtime overlay over `page.state.values[targetStatePath]` and coalesces concurrent emissions via microtask batching before re-evaluating `page.state.derived` and `composition.links`. Live runtime payloads never dirty `authoredPageDefinition` or trigger `autoPersist` writes.
103
+ - **Resilience & visibility-aware polling**: `errorPolicy` (`'fallback' | 'silent' | 'propagate'`) applies `fallbackValue` only before the first successful response so transient network errors during polling (`refreshIntervalMs`) never overwrite live data with static fallbacks. Subsequent poll ticks pause while `document.visibilityState === 'hidden'` and resume immediately on `visibilitychange`.
104
+
96
105
  ## LLM Recommendation Guide
97
106
 
98
107
  - `BUILTIN_WIDGET_SHELL_PRESETS` is the canonical catalog for reusable shell
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-10-14T16:04:24.084Z",
3
+ "generatedAt": "2026-10-14T22:05:37.957Z",
4
4
  "packageName": "@praxisui/core",
5
- "packageVersion": "10.0.0-rc.15",
5
+ "packageVersion": "10.0.0-rc.17",
6
6
  "sourceRegistry": "praxis-component-registry-ingestion",
7
7
  "sourceRegistryVersion": "1.0.0",
8
8
  "componentCount": 4,
@@ -5532,8 +5532,28 @@ class GenericCrudService {
5532
5532
  .post(url, entity, {
5533
5533
  headers: composeHeadersWithVersion(entry),
5534
5534
  context: options?.httpContext,
5535
+ observe: 'response',
5535
5536
  })
5536
- .pipe(catchError(this.handleError));
5537
+ .pipe(map((response) => {
5538
+ const isHttpResponse = response &&
5539
+ typeof response === 'object' &&
5540
+ 'body' in response &&
5541
+ 'headers' in response;
5542
+ if (!isHttpResponse) {
5543
+ return response;
5544
+ }
5545
+ const idField = this._lastResourceMeta.idField || 'id';
5546
+ const createdId = response.body?.data?.[idField] ??
5547
+ response.body?.data?.id;
5548
+ if (createdId !== undefined && createdId !== null) {
5549
+ const versionKey = this.resourceVersionKey(createdId, options);
5550
+ return this.captureResourceVersion(versionKey, response);
5551
+ }
5552
+ if (!response.body) {
5553
+ throw new Error('Resource response body is empty.');
5554
+ }
5555
+ return response.body;
5556
+ }), catchError(this.handleError));
5537
5557
  }
5538
5558
  /**
5539
5559
  * Atualiza um registro existente.
@@ -5558,6 +5578,26 @@ class GenericCrudService {
5558
5578
  update(id, entity, options) {
5559
5579
  return this.updateResponse(id, entity, options).pipe(map((r) => r.data));
5560
5580
  }
5581
+ getResourceVersion(id, options) {
5582
+ if (id === undefined || id === null || !this.configured) {
5583
+ return null;
5584
+ }
5585
+ const versionKey = this.resourceVersionKey(id, options);
5586
+ return this._resourceEtags.get(versionKey) ?? null;
5587
+ }
5588
+ setResourceVersion(id, etag, options) {
5589
+ if (id === undefined || id === null || !this.configured) {
5590
+ return;
5591
+ }
5592
+ const versionKey = this.resourceVersionKey(id, options);
5593
+ const formatted = this.normalizeEtagToken(etag);
5594
+ if (formatted) {
5595
+ this._resourceEtags.set(versionKey, formatted);
5596
+ }
5597
+ else {
5598
+ this._resourceEtags.delete(versionKey);
5599
+ }
5600
+ }
5561
5601
  updateResponse(id, entity, options) {
5562
5602
  if (id === undefined || id === null)
5563
5603
  throw new Error(this.errorMessages.emptyId);
@@ -5568,10 +5608,12 @@ class GenericCrudService {
5568
5608
  const url = this.getEndpointUrl('update', id, options?.parentPath, options?.endpointKey);
5569
5609
  const versionKey = this.resourceVersionKey(id, options);
5570
5610
  let headers = composeHeadersWithVersion(entry);
5571
- const etag = this._resourceEtags.get(versionKey);
5611
+ const etag = this._resourceEtags.get(versionKey) ||
5612
+ this.normalizeEtagToken(entity?.resourceVersion) ||
5613
+ null;
5572
5614
  if (etag) {
5573
5615
  headers = (headers instanceof HttpHeaders ? headers : new HttpHeaders(headers))
5574
- .set('If-Match', etag);
5616
+ .set('If-Match', this.ensureEtagFormat(etag));
5575
5617
  }
5576
5618
  return this.http
5577
5619
  .put(url, entity, {
@@ -5594,7 +5636,9 @@ class GenericCrudService {
5594
5636
  return [buildApiUrl(entry), tenant, environment, this.resourcePath, options?.parentPath ?? '', String(id)].join('|');
5595
5637
  }
5596
5638
  captureResourceVersion(key, response) {
5597
- const etag = response.headers.get('ETag');
5639
+ const etag = this.normalizeEtagToken(response.headers?.get('ETag')) ||
5640
+ this.normalizeEtagToken(response.body?.data?.resourceVersion) ||
5641
+ null;
5598
5642
  if (etag) {
5599
5643
  this._resourceEtags.set(key, etag);
5600
5644
  }
@@ -5603,6 +5647,26 @@ class GenericCrudService {
5603
5647
  }
5604
5648
  return response.body;
5605
5649
  }
5650
+ normalizeEtagToken(etag) {
5651
+ if (typeof etag !== 'string' && typeof etag !== 'number') {
5652
+ return null;
5653
+ }
5654
+ const trimmed = String(etag).trim();
5655
+ if (!trimmed) {
5656
+ return null;
5657
+ }
5658
+ const inner = trimmed.replace(/^(?:W\/)?"|"$/g, '').trim();
5659
+ if (!inner) {
5660
+ return null;
5661
+ }
5662
+ return this.ensureEtagFormat(trimmed);
5663
+ }
5664
+ ensureEtagFormat(etag) {
5665
+ if (/^(?:W\/)?".+"$/.test(etag)) {
5666
+ return etag;
5667
+ }
5668
+ return `"${etag.replace(/^"|"$/g, '')}"`;
5669
+ }
5606
5670
  /**
5607
5671
  * Remove um registro pelo ID.
5608
5672
  *
@@ -5631,6 +5695,7 @@ class GenericCrudService {
5631
5695
  this.ensureConfigured();
5632
5696
  const entry = this.resolveEndpointEntry(options?.endpointKey);
5633
5697
  const url = this.getEndpointUrl('delete', id, options?.parentPath, options?.endpointKey);
5698
+ const versionKey = this.resourceVersionKey(id, options);
5634
5699
  debugCrudService('[CRUD:Service] delete:request', {
5635
5700
  url,
5636
5701
  id,
@@ -5642,7 +5707,7 @@ class GenericCrudService {
5642
5707
  headers: composeHeadersWithVersion(entry),
5643
5708
  context: options?.httpContext,
5644
5709
  })
5645
- .pipe(catchError(this.handleError));
5710
+ .pipe(tap(() => this._resourceEtags.delete(versionKey)), catchError(this.handleError));
5646
5711
  }
5647
5712
  /**
5648
5713
  * Realiza exclusão de múltiplos registros sequencialmente.
@@ -13237,6 +13302,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
13237
13302
  type: Optional
13238
13303
  }] }] });
13239
13304
 
13305
+ const DEFAULT_FRAMEWORK_VERSION_MESSAGES = new Set([
13306
+ 'resource version is stale.',
13307
+ 'resource version is required.',
13308
+ 'the resource changed after it was read. reload it before deciding whether to retry.',
13309
+ 'the resource changed after it was read. reload and try again.',
13310
+ 'this operation requires a current resource version in if-match.',
13311
+ ]);
13240
13312
  /**
13241
13313
  * Maps HTTP or network errors to user-friendly messages.
13242
13314
  * This service centralizes error messaging so UI components don't
@@ -13258,14 +13330,19 @@ class ErrorMessageService {
13258
13330
  ? this.normalizeDetails(payload?.['errors'])
13259
13331
  : [];
13260
13332
  if (details.length) {
13333
+ const topMessage = this.textValue(payload?.['message']);
13334
+ const resolvedTopMessage = this.resolveVersionPreconditionCopy(details[0].code, topMessage ?? details[0].message, status);
13261
13335
  return {
13262
- message: this.textValue(payload?.['message']) ?? details[0].message,
13336
+ message: resolvedTopMessage,
13263
13337
  details,
13264
13338
  };
13265
13339
  }
13266
13340
  const publicMessage = this.textValue(payload?.['message']);
13267
13341
  if (publicMessage && isPublicClientError) {
13268
- return { message: publicMessage, details: [] };
13342
+ return {
13343
+ message: this.resolveVersionPreconditionCopy(undefined, publicMessage, status),
13344
+ details: [],
13345
+ };
13269
13346
  }
13270
13347
  return { message: this.fallbackMessage(status), details: [] };
13271
13348
  }
@@ -13283,6 +13360,12 @@ class ErrorMessageService {
13283
13360
  if (status !== undefined && status >= 500) {
13284
13361
  return this.tx('global.submitError.server', 'Erro interno no servidor. Tente novamente mais tarde.');
13285
13362
  }
13363
+ if (status === 412) {
13364
+ return this.staleResourceVersionMessage();
13365
+ }
13366
+ if (status === 428) {
13367
+ return this.resourceVersionRequiredMessage();
13368
+ }
13286
13369
  if (status === 400 || status === 422) {
13287
13370
  return this.tx('global.submitError.invalidData', 'Dados inválidos ou inconsistentes. Verifique os campos e tente novamente.');
13288
13371
  }
@@ -13293,17 +13376,40 @@ class ErrorMessageService {
13293
13376
  return [];
13294
13377
  return value.flatMap((item) => {
13295
13378
  const detail = this.objectValue(item);
13296
- const message = this.textValue(detail?.['message']) ?? this.textValue(detail?.['detail']);
13297
- if (!message)
13379
+ const rawMessage = this.textValue(detail?.['message']) ?? this.textValue(detail?.['detail']);
13380
+ if (!rawMessage)
13298
13381
  return [];
13382
+ const code = this.textValue(detail?.['code']);
13383
+ const message = this.resolveVersionPreconditionCopy(code, rawMessage, undefined);
13299
13384
  return [{
13300
13385
  message,
13301
- code: this.textValue(detail?.['code']),
13386
+ code,
13302
13387
  target: this.textValue(detail?.['target']),
13303
13388
  category: this.textValue(detail?.['category']),
13304
13389
  }];
13305
13390
  });
13306
13391
  }
13392
+ resolveVersionPreconditionCopy(code, candidateMessage, status) {
13393
+ const normalizedCode = String(code ?? '').trim().toUpperCase();
13394
+ const isDefaultFrameworkCopy = DEFAULT_FRAMEWORK_VERSION_MESSAGES.has(candidateMessage.trim().toLowerCase());
13395
+ if ((normalizedCode === 'STALE_RESOURCE_VERSION' || status === 412) &&
13396
+ isDefaultFrameworkCopy) {
13397
+ return this.staleResourceVersionMessage();
13398
+ }
13399
+ if ((normalizedCode === 'RESOURCE_VERSION_REQUIRED' ||
13400
+ normalizedCode === 'MISSING_IF_MATCH' ||
13401
+ status === 428) &&
13402
+ isDefaultFrameworkCopy) {
13403
+ return this.resourceVersionRequiredMessage();
13404
+ }
13405
+ return candidateMessage;
13406
+ }
13407
+ staleResourceVersionMessage() {
13408
+ return this.tx('global.submitError.staleResourceVersion', 'Este registro foi alterado após a leitura. Recarregue os dados e revise antes de tentar salvar novamente.');
13409
+ }
13410
+ resourceVersionRequiredMessage() {
13411
+ return this.tx('global.submitError.resourceVersionRequired', 'A versão atual do registro é obrigatória para confirmar esta alteração. Recarregue o registro e tente novamente.');
13412
+ }
13307
13413
  objectValue(value) {
13308
13414
  return value !== null && typeof value === 'object' && !Array.isArray(value)
13309
13415
  ? value
@@ -39980,7 +40086,7 @@ class WidgetPageDataSourceRuntimeService {
39980
40086
  /**
39981
40087
  * Constrói a URL canônica completa para o resourcePath declarado no dataSource.
39982
40088
  */
39983
- resolveUrl(resourcePath) {
40089
+ resolveUrl(resourcePath, endpointKey) {
39984
40090
  const trimmed = (resourcePath || '').trim();
39985
40091
  if (!trimmed)
39986
40092
  return '';
@@ -39989,7 +40095,10 @@ class WidgetPageDataSourceRuntimeService {
39989
40095
  }
39990
40096
  let base = '';
39991
40097
  if (this.apiUrlConfig) {
39992
- const entry = this.apiUrlConfig['default'] || Object.values(this.apiUrlConfig)[0];
40098
+ const key = (endpointKey || 'default').trim() || 'default';
40099
+ const entry = this.apiUrlConfig[key] ||
40100
+ this.apiUrlConfig['default'] ||
40101
+ Object.values(this.apiUrlConfig)[0];
39993
40102
  if (entry) {
39994
40103
  base = buildApiUrl(entry);
39995
40104
  }
@@ -40008,7 +40117,7 @@ class WidgetPageDataSourceRuntimeService {
40008
40117
  if (!this.http) {
40009
40118
  return of(dataSource.fallbackValue ?? null);
40010
40119
  }
40011
- const url = this.resolveUrl(dataSource.resourcePath);
40120
+ const url = this.resolveUrl(dataSource.resourcePath, dataSource.endpointKey);
40012
40121
  if (!url) {
40013
40122
  return of(dataSource.fallbackValue ?? null);
40014
40123
  }
@@ -40032,7 +40141,8 @@ class WidgetPageDataSourceRuntimeService {
40032
40141
  if (!this.http) {
40033
40142
  return of(dataSource.fallbackValue ?? null);
40034
40143
  }
40035
- const url = resolvedUrl ?? this.resolveUrl(dataSource.resourcePath);
40144
+ const url = resolvedUrl ??
40145
+ this.resolveUrl(dataSource.resourcePath, dataSource.endpointKey);
40036
40146
  if (!url) {
40037
40147
  return of(dataSource.fallbackValue ?? null);
40038
40148
  }
@@ -40064,28 +40174,65 @@ class WidgetPageDataSourceRuntimeService {
40064
40174
  }
40065
40175
  return request$.pipe(map((res) => this.transformPayload(res, dataSource)));
40066
40176
  }
40177
+ /**
40178
+ * Normaliza `page.dataSources` aceitando tanto o formato canônico em Array quanto Object Map indexado por id.
40179
+ */
40180
+ normalizeDataSources(dataSources) {
40181
+ if (!dataSources || typeof dataSources !== 'object') {
40182
+ return [];
40183
+ }
40184
+ if (Array.isArray(dataSources)) {
40185
+ return dataSources.filter((item) => !!item &&
40186
+ typeof item === 'object' &&
40187
+ !Array.isArray(item) &&
40188
+ typeof item.id === 'string' &&
40189
+ item.id.trim().length > 0 &&
40190
+ typeof item.resourcePath === 'string' &&
40191
+ item.resourcePath.trim().length > 0);
40192
+ }
40193
+ return Object.entries(dataSources)
40194
+ .filter(([key, item]) => key.trim().length > 0 &&
40195
+ !!item &&
40196
+ typeof item === 'object' &&
40197
+ !Array.isArray(item) &&
40198
+ typeof item.resourcePath === 'string' &&
40199
+ item.resourcePath.trim().length > 0)
40200
+ .map(([key, item]) => {
40201
+ const candidateId = item.id;
40202
+ return {
40203
+ ...item,
40204
+ id: typeof candidateId === 'string' && candidateId.trim().length > 0
40205
+ ? candidateId.trim()
40206
+ : key.trim(),
40207
+ };
40208
+ });
40209
+ }
40067
40210
  /**
40068
40211
  * Inicia a orquestração reativa de todos os DataSources declarados em uma página dinâmica.
40069
40212
  * Retorna uma função de teardown que cancela todas as requisições em voo e timers de polling.
40070
40213
  */
40071
40214
  startDataSources(dataSources, onUpdate, onDiagnostic) {
40072
- if (!dataSources || !dataSources.length) {
40215
+ const normalizedDataSources = this.normalizeDataSources(dataSources);
40216
+ if (!normalizedDataSources.length) {
40073
40217
  return () => { };
40074
40218
  }
40075
40219
  const destroy$ = new Subject();
40076
- for (const ds of dataSources) {
40220
+ const visibilityCallbacks = [];
40221
+ for (const ds of normalizedDataSources) {
40077
40222
  if (ds.autoFetch === false) {
40078
40223
  continue;
40079
40224
  }
40080
40225
  const targetPath = ds.targetStatePath || ds.id;
40081
40226
  const intervalMs = ds.refreshIntervalMs ?? 0;
40082
40227
  let hasSucceededOnce = false;
40228
+ let skippedWhileHidden = false;
40083
40229
  onDiagnostic?.({
40084
40230
  dataSourceId: ds.id,
40085
40231
  status: 'loading',
40086
40232
  });
40087
40233
  const runSingleFetch$ = () => this.executeRawRequest(ds).pipe(map((value) => {
40088
40234
  hasSucceededOnce = true;
40235
+ skippedWhileHidden = false;
40089
40236
  const now = new Date().toISOString();
40090
40237
  onDiagnostic?.({
40091
40238
  dataSourceId: ds.id,
@@ -40120,16 +40267,43 @@ class WidgetPageDataSourceRuntimeService {
40120
40267
  }));
40121
40268
  const stream$ = intervalMs > 0
40122
40269
  ? timer(0, intervalMs).pipe(takeUntil(destroy$), switchMap(() => {
40123
- if (typeof document !== 'undefined' &&
40270
+ if (hasSucceededOnce &&
40271
+ typeof document !== 'undefined' &&
40124
40272
  document.visibilityState === 'hidden') {
40273
+ skippedWhileHidden = true;
40125
40274
  return EMPTY;
40126
40275
  }
40127
40276
  return runSingleFetch$();
40128
40277
  }))
40129
40278
  : runSingleFetch$().pipe(takeUntil(destroy$));
40130
40279
  stream$.subscribe();
40280
+ if (intervalMs > 0) {
40281
+ visibilityCallbacks.push(() => {
40282
+ if (skippedWhileHidden) {
40283
+ skippedWhileHidden = false;
40284
+ runSingleFetch$().pipe(takeUntil(destroy$)).subscribe();
40285
+ }
40286
+ });
40287
+ }
40288
+ }
40289
+ const onVisibilityChange = () => {
40290
+ if (typeof document !== 'undefined' && document.visibilityState === 'visible') {
40291
+ for (const cb of visibilityCallbacks) {
40292
+ cb();
40293
+ }
40294
+ }
40295
+ };
40296
+ if (visibilityCallbacks.length > 0 &&
40297
+ typeof document !== 'undefined' &&
40298
+ typeof document.addEventListener === 'function') {
40299
+ document.addEventListener('visibilitychange', onVisibilityChange);
40131
40300
  }
40132
40301
  return () => {
40302
+ if (visibilityCallbacks.length > 0 &&
40303
+ typeof document !== 'undefined' &&
40304
+ typeof document.removeEventListener === 'function') {
40305
+ document.removeEventListener('visibilitychange', onVisibilityChange);
40306
+ }
40133
40307
  destroy$.next();
40134
40308
  destroy$.complete();
40135
40309
  };
@@ -45716,6 +45890,7 @@ class DynamicWidgetPageComponent {
45716
45890
  pendingDataSourceUpdates = [];
45717
45891
  isCoalescingDataSourceUpdates = false;
45718
45892
  dataSourceRuntimeOverlay = new Map();
45893
+ dataSourceDiagnostics = new Map();
45719
45894
  nestedWidgetAccessor = new NestedWidgetConfigAccessor();
45720
45895
  settingsPanel = inject(SETTINGS_PANEL_BRIDGE, {
45721
45896
  optional: true,
@@ -45807,6 +45982,7 @@ class DynamicWidgetPageComponent {
45807
45982
  this.pendingDataSourceUpdates = [];
45808
45983
  this.isCoalescingDataSourceUpdates = false;
45809
45984
  this.dataSourceRuntimeOverlay.clear();
45985
+ this.dataSourceDiagnostics.clear();
45810
45986
  this.persistenceGeneration++;
45811
45987
  this.persistenceLoad?.unsubscribe();
45812
45988
  this.persistenceMutation?.unsubscribe();
@@ -45891,6 +46067,9 @@ class DynamicWidgetPageComponent {
45891
46067
  this.getDataSourcesSignature(this.pageDefinition?.dataSources) !==
45892
46068
  this.currentDataSourcesSignature) {
45893
46069
  this.dataSourceRuntimeOverlay.clear();
46070
+ this.dataSourceDiagnostics.clear();
46071
+ this.pendingDataSourceUpdates = [];
46072
+ this.currentDataSourcesSignature = null;
45894
46073
  }
45895
46074
  this.pageRuntime = this.buildStateRuntime(this.pageDefinition?.state, this.pageDefinition?.context);
45896
46075
  this.pageState = this.pageRuntime.state;
@@ -45992,13 +46171,21 @@ class DynamicWidgetPageComponent {
45992
46171
  const state = this.stateFromCompositionSnapshot(pageWithPatchedInputs.state, cycle.snapshot.state.primaryValues);
45993
46172
  const runtimeStatePaths = Array.from(new Set((cycle.snapshot.state.changedPaths || []).filter((path) => typeof path === 'string')));
45994
46173
  const updatedPrimaryStatePaths = runtimeStatePaths.filter((path) => !this.areStateValuesEqual(this.conn.extractByPath(this.stateRuntime.normalizeState(page.state).values || {}, path), this.conn.extractByPath(state.values || {}, path)));
46174
+ for (const changedPath of updatedPrimaryStatePaths) {
46175
+ this.dataSourceRuntimeOverlay.delete(changedPath);
46176
+ }
46177
+ const nextRuntime = this.buildStateRuntime(state, pageWithPatchedInputs.context);
45995
46178
  let widgets = this.cloneWidgets(pageWithPatchedInputs.widgets || []);
45996
46179
  const directDelivery = this.applyCompositionWidgetDeliveries(widgets, cycle);
45997
46180
  widgets = directDelivery.widgets;
45998
- const projectedWidgets = this.projectPersistentCompositionStateInputs(widgets, cycle.snapshot.state, cycle.snapshot.generatedAt);
46181
+ const effectiveCompositionState = {
46182
+ ...cycle.snapshot.state,
46183
+ primaryValues: this.cloneStateValues(nextRuntime.primaryValues),
46184
+ derivedValues: this.cloneStateValues(nextRuntime.derivedValues),
46185
+ };
46186
+ const projectedWidgets = this.projectPersistentCompositionStateInputs(widgets, effectiveCompositionState, cycle.snapshot.generatedAt);
45999
46187
  const stateProjectionChanged = !this.areStateValuesEqual(widgets, projectedWidgets);
46000
46188
  widgets = projectedWidgets;
46001
- const nextRuntime = this.buildStateRuntime(state, pageWithPatchedInputs.context);
46002
46189
  if (this.isTransientOnlyCompositionCycle(cycle) &&
46003
46190
  !updatedPrimaryStatePaths.length &&
46004
46191
  !directDelivery.changed &&
@@ -46214,9 +46401,8 @@ class DynamicWidgetPageComponent {
46214
46401
  };
46215
46402
  }
46216
46403
  getDataSourcesSignature(dataSources) {
46217
- return dataSources && dataSources.length > 0
46218
- ? JSON.stringify(dataSources)
46219
- : null;
46404
+ const normalized = this.dataSourceRuntime.normalizeDataSources(dataSources);
46405
+ return normalized.length > 0 ? JSON.stringify(normalized) : null;
46220
46406
  }
46221
46407
  bootstrapCompositionAdapter(page) {
46222
46408
  if (!page) {
@@ -46826,12 +47012,16 @@ class DynamicWidgetPageComponent {
46826
47012
  return { page };
46827
47013
  }
46828
47014
  const state = this.stateFromCompositionSnapshot(page.state, cycle.snapshot.state.primaryValues);
47015
+ const runtime = this.buildStateRuntime(state, page.context);
46829
47016
  let widgets = this.cloneWidgets(page.widgets || []);
46830
47017
  widgets = this.applyCompositionWidgetDeliveries(widgets, cycle).widgets;
46831
47018
  if (this.compositionDefinition) {
46832
- widgets = this.projectPersistentCompositionStateInputs(widgets, cycle.snapshot.state, cycle.snapshot.generatedAt);
47019
+ widgets = this.projectPersistentCompositionStateInputs(widgets, {
47020
+ ...cycle.snapshot.state,
47021
+ primaryValues: this.cloneStateValues(runtime.primaryValues),
47022
+ derivedValues: this.cloneStateValues(runtime.derivedValues),
47023
+ }, cycle.snapshot.generatedAt);
46833
47024
  }
46834
- const runtime = this.buildStateRuntime(state, page.context);
46835
47025
  return {
46836
47026
  page: { ...page, widgets, state },
46837
47027
  runtime,
@@ -46948,10 +47138,14 @@ class DynamicWidgetPageComponent {
46948
47138
  return;
46949
47139
  }
46950
47140
  const state = this.stateFromCompositionSnapshot(page.state, cycle.snapshot.state.primaryValues);
47141
+ const runtime = this.buildStateRuntime(state, page.context);
46951
47142
  let widgets = this.cloneWidgets(page.widgets || []);
46952
47143
  widgets = this.applyCompositionWidgetDeliveries(widgets, cycle).widgets;
46953
- widgets = this.projectPersistentCompositionStateInputs(widgets, cycle.snapshot.state, cycle.snapshot.generatedAt);
46954
- const runtime = this.buildStateRuntime(state, page.context);
47144
+ widgets = this.projectPersistentCompositionStateInputs(widgets, {
47145
+ ...cycle.snapshot.state,
47146
+ primaryValues: this.cloneStateValues(runtime.primaryValues),
47147
+ derivedValues: this.cloneStateValues(runtime.derivedValues),
47148
+ }, cycle.snapshot.generatedAt);
46955
47149
  this.applyPageUpdate({ ...page, widgets, state }, true, runtime, false, true, false);
46956
47150
  }
46957
47151
  matchesRuntimeSourceRef(endpoint, sourceRef) {
@@ -48623,15 +48817,22 @@ class DynamicWidgetPageComponent {
48623
48817
  themePreset: themePreset || undefined,
48624
48818
  }, persist);
48625
48819
  }
48626
- applyAuthoringPageUpdate(next, persist = true, runtime = this.buildStateRuntime(next.state, next.context), syncCompositionAdapter = true, skipStateWidgetProjection = false, emitUnchangedCanonicalPage = true) {
48820
+ applyAuthoringPageUpdate(next, persist = true, runtime, syncCompositionAdapter = true, skipStateWidgetProjection = false, emitUnchangedCanonicalPage = true) {
48627
48821
  this.applyPageUpdate(next, persist, runtime, syncCompositionAdapter, skipStateWidgetProjection, emitUnchangedCanonicalPage);
48628
48822
  if (this.authoredPageDefinition) {
48629
48823
  this.pageAuthoringChange.emit(this.clonePageDefinition(this.authoredPageDefinition));
48630
48824
  }
48631
48825
  }
48632
- applyPageUpdate(next, persist = true, runtime = this.buildStateRuntime(next.state, next.context), syncCompositionAdapter = true, skipStateWidgetProjection = false, emitUnchangedCanonicalPage = true) {
48826
+ applyPageUpdate(next, persist = true, runtime, syncCompositionAdapter = true, skipStateWidgetProjection = false, emitUnchangedCanonicalPage = true) {
48633
48827
  this.assertNoLegacyConnections(next);
48634
- const normalized = { ...next, state: runtime.state };
48828
+ if (this.getDataSourcesSignature(next.dataSources) !==
48829
+ this.currentDataSourcesSignature) {
48830
+ this.dataSourceRuntimeOverlay.clear();
48831
+ this.dataSourceDiagnostics.clear();
48832
+ this.pendingDataSourceUpdates = [];
48833
+ }
48834
+ const effectiveRuntime = runtime ?? this.buildStateRuntime(next.state, next.context);
48835
+ const normalized = { ...next, state: effectiveRuntime.state };
48635
48836
  const normalizedWidgets = this.applyEditShellActions(normalized.widgets || []);
48636
48837
  const bootstrapSnapshot = syncCompositionAdapter
48637
48838
  ? this.bootstrapCompositionAdapter(normalized)
@@ -48639,14 +48840,14 @@ class DynamicWidgetPageComponent {
48639
48840
  const stateBoundWidgets = skipStateWidgetProjection
48640
48841
  ? normalizedWidgets
48641
48842
  : bootstrapSnapshot
48642
- ? this.applyBootstrapCompositionHydration(normalizedWidgets, bootstrapSnapshot, runtime)
48843
+ ? this.applyBootstrapCompositionHydration(normalizedWidgets, bootstrapSnapshot, effectiveRuntime)
48643
48844
  : this.compositionDefinition
48644
48845
  ? this.compositionRuntime.projectStateWidgetInputs(this.compositionDefinition, {
48645
48846
  widgets: normalizedWidgets,
48646
48847
  state: {
48647
- primaryValues: this.cloneStateValues(runtime.primaryValues),
48648
- derivedValues: this.cloneStateValues(runtime.derivedValues),
48649
- transientValues: {},
48848
+ primaryValues: this.cloneStateValues(effectiveRuntime.primaryValues),
48849
+ derivedValues: this.cloneStateValues(effectiveRuntime.derivedValues),
48850
+ transientValues: this.cloneStateValues(this.compositionRuntime.getSnapshot().state.transientValues),
48650
48851
  changedPaths: [],
48651
48852
  diagnostics: [],
48652
48853
  },
@@ -48658,9 +48859,9 @@ class DynamicWidgetPageComponent {
48658
48859
  });
48659
48860
  this.pageDefinition = hydrated;
48660
48861
  this.page = hydrated;
48661
- this.pageState = runtime.state;
48662
- this.pageRuntime = runtime;
48663
- this.applyResponsivePresentation(hydrated, hydrated?.widgets || stateBoundWidgets, runtime);
48862
+ this.pageState = effectiveRuntime.state;
48863
+ this.pageRuntime = effectiveRuntime;
48864
+ this.applyResponsivePresentation(hydrated, hydrated?.widgets || stateBoundWidgets, effectiveRuntime);
48664
48865
  this.restartDataSources(hydrated);
48665
48866
  if (this.widgetEventBus) {
48666
48867
  this.wiringSubscription?.unsubscribe();
@@ -48676,7 +48877,7 @@ class DynamicWidgetPageComponent {
48676
48877
  if (authoredPageChanged || emitUnchangedCanonicalPage) {
48677
48878
  this.pageChange.emit(canonicalPage);
48678
48879
  }
48679
- this.reportStateDiagnostics(runtime.diagnostics);
48880
+ this.reportStateDiagnostics(effectiveRuntime.diagnostics);
48680
48881
  if (authoredPageChanged &&
48681
48882
  persist &&
48682
48883
  this.autoPersist &&
@@ -48730,7 +48931,7 @@ class DynamicWidgetPageComponent {
48730
48931
  state: {
48731
48932
  primaryValues: this.cloneStateValues(nextRuntime.primaryValues),
48732
48933
  derivedValues: this.cloneStateValues(nextRuntime.derivedValues),
48733
- transientValues: {},
48934
+ transientValues: this.cloneStateValues(this.compositionRuntime.getSnapshot().state.transientValues),
48734
48935
  changedPaths: updates.map((u) => u.targetStatePath),
48735
48936
  diagnostics: [],
48736
48937
  },
@@ -48747,6 +48948,17 @@ class DynamicWidgetPageComponent {
48747
48948
  this.reportStateDiagnostics(nextRuntime.diagnostics);
48748
48949
  this.changeDetector.markForCheck();
48749
48950
  }
48951
+ handleDataSourceDiagnostic(diagnostic) {
48952
+ if (this.destroyed) {
48953
+ return;
48954
+ }
48955
+ this.dataSourceDiagnostics.set(diagnostic.dataSourceId, diagnostic);
48956
+ if (diagnostic.status === 'error') {
48957
+ this.reportStateDiagnostics([
48958
+ `DataSource "${diagnostic.dataSourceId}" failed: ${diagnostic.error || 'unknown error'}`,
48959
+ ]);
48960
+ }
48961
+ }
48750
48962
  restartDataSources(page) {
48751
48963
  const dataSources = page?.dataSources;
48752
48964
  const signature = this.getDataSourcesSignature(dataSources);
@@ -48755,12 +48967,15 @@ class DynamicWidgetPageComponent {
48755
48967
  }
48756
48968
  this.currentDataSourcesSignature = signature;
48757
48969
  this.dataSourceRuntimeOverlay.clear();
48970
+ this.dataSourceDiagnostics.clear();
48971
+ this.pendingDataSourceUpdates = [];
48758
48972
  this.dataSourceTeardown?.();
48759
48973
  this.dataSourceTeardown = undefined;
48760
- if (!dataSources || dataSources.length === 0) {
48974
+ const normalizedDataSources = this.dataSourceRuntime.normalizeDataSources(dataSources);
48975
+ if (normalizedDataSources.length === 0) {
48761
48976
  return;
48762
48977
  }
48763
- this.dataSourceTeardown = this.dataSourceRuntime.startDataSources(dataSources, (update) => this.handleDataSourceUpdate(update));
48978
+ this.dataSourceTeardown = this.dataSourceRuntime.startDataSources(normalizedDataSources, (update) => this.handleDataSourceUpdate(update), (diagnostic) => this.handleDataSourceDiagnostic(diagnostic));
48764
48979
  }
48765
48980
  applyLayout(layout, grouping, deviceLayoutColumnsSelected = false) {
48766
48981
  this.layout = layout;
@@ -49102,7 +49317,11 @@ class DynamicWidgetPageComponent {
49102
49317
  }
49103
49318
  return this.compositionRuntime.projectStateWidgetInputs(this.compositionDefinition, {
49104
49319
  widgets,
49105
- state: snapshot.state,
49320
+ state: {
49321
+ ...snapshot.state,
49322
+ primaryValues: this.cloneStateValues(this.pageRuntime.primaryValues),
49323
+ derivedValues: this.cloneStateValues(this.pageRuntime.derivedValues),
49324
+ },
49106
49325
  now: snapshot.generatedAt,
49107
49326
  }).widgets;
49108
49327
  }
@@ -51240,7 +51459,13 @@ class DynamicWidgetPageComponent {
51240
51459
  cloned.layout = { ...page.layout };
51241
51460
  }
51242
51461
  if (page.grouping) {
51243
- cloned.grouping = [...page.grouping];
51462
+ cloned.grouping = this.cloneStateValues(page.grouping);
51463
+ }
51464
+ if (page.dataSources) {
51465
+ cloned.dataSources = this.cloneStateValues(page.dataSources);
51466
+ }
51467
+ if (page.composition) {
51468
+ cloned.composition = this.cloneStateValues(page.composition);
51244
51469
  }
51245
51470
  if (page.canvas) {
51246
51471
  cloned.canvas = {
@@ -51251,16 +51476,20 @@ class DynamicWidgetPageComponent {
51251
51476
  };
51252
51477
  }
51253
51478
  if (page.context) {
51254
- cloned.context = { ...page.context };
51479
+ cloned.context = this.cloneStateValues(page.context);
51255
51480
  }
51256
51481
  if (page.state) {
51257
51482
  const stateObj = {
51258
- values: page.state.values ? { ...page.state.values } : {},
51483
+ values: page.state.values
51484
+ ? this.cloneStateValues(page.state.values)
51485
+ : {},
51259
51486
  };
51260
- if (page.state.schema !== undefined)
51261
- stateObj.schema = page.state.schema;
51262
- if (page.state.derived !== undefined)
51263
- stateObj.derived = page.state.derived;
51487
+ if (page.state.schema !== undefined) {
51488
+ stateObj.schema = this.cloneStateValues(page.state.schema);
51489
+ }
51490
+ if (page.state.derived !== undefined) {
51491
+ stateObj.derived = this.cloneStateValues(page.state.derived);
51492
+ }
51264
51493
  cloned.state = stateObj;
51265
51494
  }
51266
51495
  return cloned;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxisui/core",
3
- "version": "10.0.0-rc.15",
3
+ "version": "10.0.0-rc.17",
4
4
  "description": "Core library for Praxis UI Workspace: types, tokens, services and utilities shared across @praxisui/* packages.",
5
5
  "peerDependencies": {
6
6
  "@angular/cdk": "^21.0.0",
@@ -6363,9 +6363,13 @@ declare class GenericCrudService<T, ID extends string | number = string | number
6363
6363
  * @returns Observable com a entidade atualizada.
6364
6364
  */
6365
6365
  update(id: ID, entity: T, options?: CrudOperationOptions): Observable<T>;
6366
+ getResourceVersion(id: ID, options?: CrudOperationOptions): string | null;
6367
+ setResourceVersion(id: ID, etag: string | null | undefined, options?: CrudOperationOptions): void;
6366
6368
  updateResponse(id: ID, entity: T, options?: CrudOperationOptions): Observable<RestApiResponse<T>>;
6367
6369
  private resourceVersionKey;
6368
6370
  private captureResourceVersion;
6371
+ private normalizeEtagToken;
6372
+ private ensureEtagFormat;
6369
6373
  /**
6370
6374
  * Remove um registro pelo ID.
6371
6375
  *
@@ -10840,6 +10844,9 @@ declare class ErrorMessageService {
10840
10844
  getSubmitErrorMessage(error: unknown): string;
10841
10845
  private fallbackMessage;
10842
10846
  private normalizeDetails;
10847
+ private resolveVersionPreconditionCopy;
10848
+ private staleResourceVersionMessage;
10849
+ private resourceVersionRequiredMessage;
10843
10850
  private objectValue;
10844
10851
  private textValue;
10845
10852
  private numberValue;
@@ -16232,6 +16239,8 @@ interface WidgetPageDataSourceDefinition {
16232
16239
  kind?: WidgetPageDataSourceKind;
16233
16240
  /** Caminho do recurso canônico no backend Praxis (ex: 'human-resources/funcionarios/stats/group-by'). */
16234
16241
  resourcePath: string;
16242
+ /** Chave opcional do endpoint configurado em API_URL (padrão: 'default'). */
16243
+ endpointKey?: string;
16235
16244
  /** Método HTTP utilizado (padrão: 'POST' para endpoints /stats, 'GET' para recursos padrão). */
16236
16245
  method?: WidgetPageDataSourceMethod;
16237
16246
  /** Parâmetros de consulta (query params para GET ou corpo da requisição para POST de analytics). */
@@ -18246,17 +18255,25 @@ declare class WidgetPageDataSourceRuntimeService {
18246
18255
  /**
18247
18256
  * Constrói a URL canônica completa para o resourcePath declarado no dataSource.
18248
18257
  */
18249
- resolveUrl(resourcePath: string): string;
18258
+ resolveUrl(resourcePath: string, endpointKey?: string): string;
18250
18259
  /**
18251
18260
  * Executa uma única consulta para um DataSource e retorna o resultado transformado.
18252
18261
  */
18253
18262
  fetchDataSource(dataSource: WidgetPageDataSourceDefinition): Observable<unknown>;
18254
18263
  private executeRawRequest;
18264
+ /**
18265
+ * Normaliza `page.dataSources` aceitando tanto o formato canônico em Array quanto Object Map indexado por id.
18266
+ */
18267
+ normalizeDataSources(dataSources: WidgetPageDataSourceDefinition[] | Record<string, Omit<WidgetPageDataSourceDefinition, 'id'> & {
18268
+ id?: string;
18269
+ }> | undefined | null): WidgetPageDataSourceDefinition[];
18255
18270
  /**
18256
18271
  * Inicia a orquestração reativa de todos os DataSources declarados em uma página dinâmica.
18257
18272
  * Retorna uma função de teardown que cancela todas as requisições em voo e timers de polling.
18258
18273
  */
18259
- startDataSources(dataSources: WidgetPageDataSourceDefinition[] | undefined, onUpdate: (event: WidgetPageDataSourceUpdateEvent) => void, onDiagnostic?: (diagnostic: WidgetPageDataSourceDiagnostic) => void): () => void;
18274
+ startDataSources(dataSources: WidgetPageDataSourceDefinition[] | Record<string, Omit<WidgetPageDataSourceDefinition, 'id'> & {
18275
+ id?: string;
18276
+ }> | undefined, onUpdate: (event: WidgetPageDataSourceUpdateEvent) => void, onDiagnostic?: (diagnostic: WidgetPageDataSourceDiagnostic) => void): () => void;
18260
18277
  /**
18261
18278
  * Transforma a resposta bruta do backend aplicando envelope canônico ou JsonLogic.
18262
18279
  */
@@ -18776,6 +18793,7 @@ declare class DynamicWidgetPageComponent implements OnChanges, OnDestroy {
18776
18793
  private pendingDataSourceUpdates;
18777
18794
  private isCoalescingDataSourceUpdates;
18778
18795
  private readonly dataSourceRuntimeOverlay;
18796
+ private readonly dataSourceDiagnostics;
18779
18797
  private readonly nestedWidgetAccessor;
18780
18798
  private readonly settingsPanel;
18781
18799
  private readonly defaultShellEditor;
@@ -19007,6 +19025,7 @@ declare class DynamicWidgetPageComponent implements OnChanges, OnDestroy {
19007
19025
  private applyPageUpdate;
19008
19026
  private handleDataSourceUpdate;
19009
19027
  private flushPendingDataSourceUpdates;
19028
+ private handleDataSourceDiagnostic;
19010
19029
  private restartDataSources;
19011
19030
  private applyLayout;
19012
19031
  private applyCanvasLayout;