@praxisui/crud 10.0.0-rc.15 → 10.0.0-rc.16

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.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-10-14T16:04:24.084Z",
3
+ "generatedAt": "2026-10-14T20:20:10.826Z",
4
4
  "packageName": "@praxisui/crud",
5
- "packageVersion": "10.0.0-rc.15",
5
+ "packageVersion": "10.0.0-rc.16",
6
6
  "sourceRegistry": "praxis-component-registry-ingestion",
7
7
  "sourceRegistryVersion": "1.0.0",
8
8
  "componentCount": 2,
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, DestroyRef, signal, HostListener, Inject, ViewEncapsulation, Component, EventEmitter, ChangeDetectorRef, Output, Input, ChangeDetectionStrategy, Injectable, InjectionToken, input, computed, effect, output, Injector, booleanAttribute, ViewChild, Optional, ENVIRONMENT_INITIALIZER } from '@angular/core';
2
+ import { inject, DestroyRef, signal, HostListener, ViewChild, Inject, ViewEncapsulation, Component, EventEmitter, ChangeDetectorRef, Output, Input, ChangeDetectionStrategy, Injectable, InjectionToken, input, computed, effect, output, Injector, booleanAttribute, Optional, ENVIRONMENT_INITIALIZER } from '@angular/core';
3
3
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
4
  import { HttpClient } from '@angular/common/http';
5
5
  import { Router, ActivatedRoute, RouterLink } from '@angular/router';
@@ -7,7 +7,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
7
7
  import { firstValueFrom, BehaviorSubject, Subscription } from 'rxjs';
8
8
  import { take, filter } from 'rxjs/operators';
9
9
  import * as i2$1 from '@praxisui/core';
10
- import { PraxisBulkClientService, generateBulkIdempotencyKey, API_URL, SafeExpressionEvaluator, buildApiUrl, ASYNC_CONFIG_STORAGE, GlobalConfigService, CrudOperationResolutionService, LoggerService, SurfaceNavigationError, fillUndefined, SETTINGS_PANEL_DATA, PraxisI18nService, PraxisIconDirective, providePraxisI18nConfig, createDefaultTableConfig, PraxisWidgetEventBus, EnterpriseRuntimeContextService, GLOBAL_SURFACE_SERVICE, SurfaceOutletRegistryService, ComponentKeyService, ResourceDiscoveryService, ResourceActionOpenAdapterService, ResourceSurfaceOpenAdapterService, normalizeSurfaceOperationContext, translateUnavailableWorkflowMessage, isSurfaceNavigationError, translateSurfaceNavigationRejected, normalizeUnknownError, EmptyStateCardComponent, RESOURCE_DISCOVERY_I18N_CONFIG, SURFACE_DRAWER_REF, SURFACE_DRAWER_CONTENT_DATA, PraxisIconButtonComponent, PraxisResourceIdentityComponent, GenericCrudService, ComponentMetadataRegistry } from '@praxisui/core';
10
+ import { PraxisBulkClientService, generateBulkIdempotencyKey, API_URL, SafeExpressionEvaluator, buildApiUrl, ASYNC_CONFIG_STORAGE, GlobalConfigService, CrudOperationResolutionService, LoggerService, SurfaceNavigationError, fillUndefined, SETTINGS_PANEL_DATA, PraxisI18nService, PraxisIconDirective, providePraxisI18nConfig, createDefaultTableConfig, PraxisWidgetEventBus, EnterpriseRuntimeContextService, GLOBAL_SURFACE_SERVICE, SurfaceOutletRegistryService, ComponentKeyService, ResourceDiscoveryService, ResourceActionOpenAdapterService, ResourceSurfaceOpenAdapterService, composeHeadersWithVersion, normalizeSurfaceOperationContext, translateUnavailableWorkflowMessage, isSurfaceNavigationError, translateSurfaceNavigationRejected, normalizeUnknownError, EmptyStateCardComponent, RESOURCE_DISCOVERY_I18N_CONFIG, SURFACE_DRAWER_REF, SURFACE_DRAWER_CONTENT_DATA, PraxisIconButtonComponent, PraxisResourceIdentityComponent, GenericCrudService, ComponentMetadataRegistry } from '@praxisui/core';
11
11
  export { createDynamicRecord, extractRecordId, isDynamicDataRecord, resolveDataField } from '@praxisui/core';
12
12
  import * as i1 from '@angular/common';
13
13
  import { CommonModule } from '@angular/common';
@@ -47,11 +47,13 @@ class PraxisBulkOperationDialogComponent {
47
47
  dialogRef = inject((MatDialogRef));
48
48
  bulkClient = inject(PraxisBulkClientService);
49
49
  destroyRef = inject(DestroyRef);
50
+ parametersForm;
50
51
  step = signal('PARAMETERS', ...(ngDevMode ? [{ debugName: "step" }] : /* istanbul ignore next */ []));
51
52
  proposal = signal(null, ...(ngDevMode ? [{ debugName: "proposal" }] : /* istanbul ignore next */ []));
52
53
  execution = signal(null, ...(ngDevMode ? [{ debugName: "execution" }] : /* istanbul ignore next */ []));
53
54
  errorMessage = signal(null, ...(ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []));
54
55
  parametersValue = signal({}, ...(ngDevMode ? [{ debugName: "parametersValue" }] : /* istanbul ignore next */ []));
56
+ parametersFormValid = signal(true, ...(ngDevMode ? [{ debugName: "parametersFormValid" }] : /* istanbul ignore next */ []));
55
57
  proposalItems = signal([], ...(ngDevMode ? [{ debugName: "proposalItems" }] : /* istanbul ignore next */ []));
56
58
  proposalNextCursor = signal(null, ...(ngDevMode ? [{ debugName: "proposalNextCursor" }] : /* istanbul ignore next */ []));
57
59
  loadingProposalResults = signal(false, ...(ngDevMode ? [{ debugName: "loadingProposalResults" }] : /* istanbul ignore next */ []));
@@ -113,7 +115,18 @@ class PraxisBulkOperationDialogComponent {
113
115
  .replace(/[^a-zA-Z0-9_-]+/g, '-');
114
116
  return `bulk-params-${resourcePart}-${actionPart}`;
115
117
  }
118
+ onParametersFormReady(event) {
119
+ if (typeof event?.isValid === 'boolean') {
120
+ this.parametersFormValid.set(event.isValid);
121
+ }
122
+ if (event?.formData && typeof event.formData === 'object' && !Array.isArray(event.formData)) {
123
+ this.onParametersChange(event);
124
+ }
125
+ }
116
126
  onParametersChange(event) {
127
+ if (typeof event?.isValid === 'boolean') {
128
+ this.parametersFormValid.set(event.isValid);
129
+ }
117
130
  const raw = event?.formData ?? event?.value ?? event;
118
131
  if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
119
132
  const unwrapped = raw.parameters &&
@@ -123,7 +136,52 @@ class PraxisBulkOperationDialogComponent {
123
136
  ? raw.parameters
124
137
  : raw;
125
138
  this.parametersValue.set(unwrapped);
139
+ if (this.parametersFormValid()) {
140
+ this.errorMessage.set(null);
141
+ }
142
+ }
143
+ }
144
+ getMissingRequiredParameterNames() {
145
+ const bulkContract = this.action.execution?.bulk;
146
+ const descriptors = this.action.commandParameterDescriptors ||
147
+ bulkContract?.commandParameterDescriptors;
148
+ if (!Array.isArray(descriptors)) {
149
+ return [];
150
+ }
151
+ const currentValues = this.parametersValue();
152
+ const missing = [];
153
+ for (const desc of descriptors) {
154
+ if (!desc?.required)
155
+ continue;
156
+ const key = String(desc.name || desc.field || desc.property || '').trim();
157
+ if (!key)
158
+ continue;
159
+ const val = currentValues[key];
160
+ const isEmpty = val === undefined ||
161
+ val === null ||
162
+ (typeof val === 'string' && val.trim() === '') ||
163
+ (Array.isArray(val) && val.length === 0);
164
+ if (isEmpty) {
165
+ missing.push(String(desc.label || desc.title || key));
166
+ }
167
+ }
168
+ return missing;
169
+ }
170
+ canEvaluateParameters() {
171
+ if (!this.hasParametersSchema()) {
172
+ return true;
126
173
  }
174
+ if (!this.parametersFormValid()) {
175
+ return false;
176
+ }
177
+ if (this.getMissingRequiredParameterNames().length > 0) {
178
+ return false;
179
+ }
180
+ const dynamicFormGroup = this.parametersForm?.form;
181
+ if (dynamicFormGroup && Object.keys(dynamicFormGroup.controls || {}).length > 0) {
182
+ return this.parametersForm.effectiveFormIsValid;
183
+ }
184
+ return true;
127
185
  }
128
186
  isBlockedByAtomicity() {
129
187
  const prop = this.proposal();
@@ -228,6 +286,27 @@ class PraxisBulkOperationDialogComponent {
228
286
  this.errorMessage.set(`A seleção atual (${this.targets.length} itens) excede o limite máximo permitido pelo contrato (${maxTargets} itens por lote).`);
229
287
  return;
230
288
  }
289
+ if (this.hasParametersSchema()) {
290
+ const dynamicFormGroup = this.parametersForm?.form;
291
+ if (dynamicFormGroup && Object.keys(dynamicFormGroup.controls || {}).length > 0) {
292
+ dynamicFormGroup.markAllAsTouched();
293
+ if (!this.parametersForm.effectiveFormIsValid) {
294
+ this.parametersFormValid.set(false);
295
+ const missingLabels = this.parametersForm?.invalidRequiredFieldLabels || [];
296
+ this.errorMessage.set(missingLabels.length > 0
297
+ ? `Preencha os campos obrigatórios antes de avaliar: ${missingLabels.join(', ')}.`
298
+ : 'Corrija os campos inválidos dos parâmetros antes de avaliar a proposta.');
299
+ return;
300
+ }
301
+ }
302
+ const missingDescriptors = this.getMissingRequiredParameterNames();
303
+ if (missingDescriptors.length > 0 || !this.parametersFormValid()) {
304
+ this.errorMessage.set(missingDescriptors.length > 0
305
+ ? `Preencha os parâmetros obrigatórios antes de avaliar: ${missingDescriptors.join(', ')}.`
306
+ : 'Corrija os campos inválidos dos parâmetros antes de avaliar a proposta.');
307
+ return;
308
+ }
309
+ }
231
310
  this.confirmationIdempotencyKey = undefined;
232
311
  this.step.set('EVALUATING');
233
312
  this.errorMessage.set(null);
@@ -295,8 +374,9 @@ class PraxisBulkOperationDialogComponent {
295
374
  this.proposalNextCursor.set(page?.nextCursor ?? page?.next ?? null);
296
375
  this.loadingProposalResults.set(false);
297
376
  },
298
- error: () => {
377
+ error: (err) => {
299
378
  this.loadingProposalResults.set(false);
379
+ this.errorMessage.set(err?.error?.message || err?.message || 'Falha ao carregar os resultados detalhados da proposta.');
300
380
  },
301
381
  });
302
382
  }
@@ -386,8 +466,9 @@ class PraxisBulkOperationDialogComponent {
386
466
  this.executionNextCursor.set(page?.nextCursor ?? page?.next ?? null);
387
467
  this.loadingExecutionResults.set(false);
388
468
  },
389
- error: () => {
469
+ error: (err) => {
390
470
  this.loadingExecutionResults.set(false);
471
+ this.errorMessage.set(err?.error?.message || err?.message || 'Falha ao carregar os resultados detalhados da execução.');
391
472
  },
392
473
  });
393
474
  }
@@ -431,9 +512,16 @@ class PraxisBulkOperationDialogComponent {
431
512
  });
432
513
  }
433
514
  onCloseSuccess() {
515
+ const exec = this.execution();
516
+ const status = exec?.status;
517
+ const confirmed = exec?.totals?.confirmed ?? 0;
518
+ const isSuccess = !exec ||
519
+ status === 'COMPLETED' ||
520
+ status === 'COMPLETED_WITH_ERRORS' ||
521
+ confirmed > 0;
434
522
  this.dialogRef.close({
435
- success: true,
436
- execution: this.execution(),
523
+ success: isSuccess,
524
+ execution: exec,
437
525
  });
438
526
  }
439
527
  onCancel() {
@@ -450,7 +538,7 @@ class PraxisBulkOperationDialogComponent {
450
538
  }
451
539
  }
452
540
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisBulkOperationDialogComponent, deps: [{ token: MAT_DIALOG_DATA }], target: i0.ɵɵFactoryTarget.Component });
453
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisBulkOperationDialogComponent, isStandalone: true, selector: "praxis-bulk-operation-dialog", host: { listeners: { "keydown.escape": "onEscape($event)" } }, ngImport: i0, template: `
541
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisBulkOperationDialogComponent, isStandalone: true, selector: "praxis-bulk-operation-dialog", host: { listeners: { "keydown.escape": "onEscape($event)" } }, viewQueries: [{ propertyName: "parametersForm", first: true, predicate: PraxisDynamicForm, descendants: true }], ngImport: i0, template: `
454
542
  <div class="praxis-bulk-dialog" role="dialog" aria-labelledby="praxis-bulk-dialog-title" aria-describedby="praxis-bulk-dialog-subtitle">
455
543
  <div class="praxis-bulk-dialog__header">
456
544
  <div class="praxis-bulk-dialog__title-group">
@@ -490,6 +578,7 @@ class PraxisBulkOperationDialogComponent {
490
578
  [submitMethod]="'post'"
491
579
  [submitUrl]="''"
492
580
  [actions]="formActions"
581
+ (formReady)="onParametersFormReady($event)"
493
582
  (valueChange)="onParametersChange($event)"
494
583
  />
495
584
  </div>
@@ -727,7 +816,7 @@ class PraxisBulkOperationDialogComponent {
727
816
  <div class="praxis-bulk-dialog__actions">
728
817
  @if (step() === 'PARAMETERS') {
729
818
  <button mat-button (click)="onCancel()">Cancelar</button>
730
- <button mat-flat-button color="primary" (click)="onEvaluate()">
819
+ <button mat-flat-button color="primary" (click)="onEvaluate()" [disabled]="!canEvaluateParameters()">
731
820
  Avaliar Proposta
732
821
  </button>
733
822
  }
@@ -807,6 +896,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
807
896
  [submitMethod]="'post'"
808
897
  [submitUrl]="''"
809
898
  [actions]="formActions"
899
+ (formReady)="onParametersFormReady($event)"
810
900
  (valueChange)="onParametersChange($event)"
811
901
  />
812
902
  </div>
@@ -1044,7 +1134,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
1044
1134
  <div class="praxis-bulk-dialog__actions">
1045
1135
  @if (step() === 'PARAMETERS') {
1046
1136
  <button mat-button (click)="onCancel()">Cancelar</button>
1047
- <button mat-flat-button color="primary" (click)="onEvaluate()">
1137
+ <button mat-flat-button color="primary" (click)="onEvaluate()" [disabled]="!canEvaluateParameters()">
1048
1138
  Avaliar Proposta
1049
1139
  </button>
1050
1140
  }
@@ -1076,7 +1166,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
1076
1166
  }], ctorParameters: () => [{ type: undefined, decorators: [{
1077
1167
  type: Inject,
1078
1168
  args: [MAT_DIALOG_DATA]
1079
- }] }], propDecorators: { onEscape: [{
1169
+ }] }], propDecorators: { parametersForm: [{
1170
+ type: ViewChild,
1171
+ args: [PraxisDynamicForm]
1172
+ }], onEscape: [{
1080
1173
  type: HostListener,
1081
1174
  args: ['keydown.escape', ['$event']]
1082
1175
  }] } });
@@ -1180,6 +1273,7 @@ class PraxisBulkEditDialogComponent {
1180
1273
  const selectedFields = this.fieldRows().map((r) => r.field);
1181
1274
  return this.writableFields.some((f) => !selectedFields.includes(f));
1182
1275
  }
1276
+ nextRowId = 1;
1183
1277
  isFieldSelectedInOtherRow(field, currentRow) {
1184
1278
  return this.fieldRows().some((r) => r !== currentRow && r.field === field);
1185
1279
  }
@@ -1189,6 +1283,7 @@ class PraxisBulkEditDialogComponent {
1189
1283
  this.fieldRows.update((rows) => [
1190
1284
  ...rows,
1191
1285
  {
1286
+ id: `field-row-${this.nextRowId++}`,
1192
1287
  field: nextField,
1193
1288
  operator: 'SET',
1194
1289
  value: '',
@@ -1198,6 +1293,21 @@ class PraxisBulkEditDialogComponent {
1198
1293
  removeFieldRow(index) {
1199
1294
  this.fieldRows.update((rows) => rows.filter((_, i) => i !== index));
1200
1295
  }
1296
+ getCurrencyFieldLabel(fieldName) {
1297
+ const descriptor = this.getFieldDescriptor(fieldName);
1298
+ const prop = this.getEffectiveSchemaProperty(fieldName);
1299
+ const col = this.data.columns?.find((c) => c.field === fieldName);
1300
+ const currencySymbol = descriptor?.['currencySymbol'] ||
1301
+ descriptor?.['currencyCode'] ||
1302
+ prop?.['x-ui']?.currencySymbol ||
1303
+ prop?.['x-ui']?.currencyCode ||
1304
+ col?.['currencySymbol'] ||
1305
+ col?.['currencyCode'];
1306
+ if (typeof currencySymbol === 'string' && currencySymbol.trim()) {
1307
+ return `Novo Valor (${currencySymbol.trim()})`;
1308
+ }
1309
+ return 'Novo Valor Monetário';
1310
+ }
1201
1311
  getFieldLabel(fieldName) {
1202
1312
  if (!fieldName)
1203
1313
  return '';
@@ -1517,8 +1627,9 @@ class PraxisBulkEditDialogComponent {
1517
1627
  this.proposalNextCursor.set(page?.nextCursor ?? page?.next ?? null);
1518
1628
  this.loadingProposalResults.set(false);
1519
1629
  },
1520
- error: () => {
1630
+ error: (err) => {
1521
1631
  this.loadingProposalResults.set(false);
1632
+ this.errorMessage.set(err?.error?.message || err?.message || 'Falha ao carregar os resultados detalhados da proposta.');
1522
1633
  },
1523
1634
  });
1524
1635
  }
@@ -1606,8 +1717,9 @@ class PraxisBulkEditDialogComponent {
1606
1717
  this.executionNextCursor.set(page?.nextCursor ?? page?.next ?? null);
1607
1718
  this.loadingExecutionResults.set(false);
1608
1719
  },
1609
- error: () => {
1720
+ error: (err) => {
1610
1721
  this.loadingExecutionResults.set(false);
1722
+ this.errorMessage.set(err?.error?.message || err?.message || 'Falha ao carregar os resultados detalhados da execução.');
1611
1723
  },
1612
1724
  });
1613
1725
  }
@@ -1649,9 +1761,16 @@ class PraxisBulkEditDialogComponent {
1649
1761
  });
1650
1762
  }
1651
1763
  onCloseSuccess() {
1764
+ const exec = this.execution();
1765
+ const status = exec?.status;
1766
+ const confirmed = exec?.totals?.confirmed ?? 0;
1767
+ const isSuccess = !exec ||
1768
+ status === 'COMPLETED' ||
1769
+ status === 'COMPLETED_WITH_ERRORS' ||
1770
+ confirmed > 0;
1652
1771
  this.dialogRef.close({
1653
- success: true,
1654
- execution: this.execution(),
1772
+ success: isSuccess,
1773
+ execution: exec,
1655
1774
  });
1656
1775
  }
1657
1776
  onCancel() {
@@ -1738,7 +1857,7 @@ class PraxisBulkEditDialogComponent {
1738
1857
  </div>
1739
1858
  } @else {
1740
1859
  <div class="field-rows-container">
1741
- @for (row of fieldRows(); track $index) {
1860
+ @for (row of fieldRows(); track row.id; let idx = $index) {
1742
1861
  <div class="field-row">
1743
1862
  <!-- Seleção do Campo -->
1744
1863
  <mat-form-field appearance="outline" class="field-select">
@@ -1769,7 +1888,7 @@ class PraxisBulkEditDialogComponent {
1769
1888
  @switch (getFieldType(row.field)) {
1770
1889
  @case ('currency') {
1771
1890
  <mat-form-field appearance="outline" class="value-input">
1772
- <mat-label>Novo Valor (R$)</mat-label>
1891
+ <mat-label>{{ getCurrencyFieldLabel(row.field) }}</mat-label>
1773
1892
  <input matInput type="number" step="0.01" [(ngModel)]="row.value" (ngModelChange)="errorMessage.set(null)" placeholder="0.00" />
1774
1893
  </mat-form-field>
1775
1894
  }
@@ -1819,7 +1938,7 @@ class PraxisBulkEditDialogComponent {
1819
1938
  }
1820
1939
 
1821
1940
  <!-- Remover Linha -->
1822
- <button mat-icon-button color="warn" (click)="removeFieldRow($index)" title="Remover">
1941
+ <button mat-icon-button color="warn" (click)="removeFieldRow(idx)" title="Remover">
1823
1942
  <mat-icon>delete_outline</mat-icon>
1824
1943
  </button>
1825
1944
  </div>
@@ -2128,7 +2247,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
2128
2247
  </div>
2129
2248
  } @else {
2130
2249
  <div class="field-rows-container">
2131
- @for (row of fieldRows(); track $index) {
2250
+ @for (row of fieldRows(); track row.id; let idx = $index) {
2132
2251
  <div class="field-row">
2133
2252
  <!-- Seleção do Campo -->
2134
2253
  <mat-form-field appearance="outline" class="field-select">
@@ -2159,7 +2278,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
2159
2278
  @switch (getFieldType(row.field)) {
2160
2279
  @case ('currency') {
2161
2280
  <mat-form-field appearance="outline" class="value-input">
2162
- <mat-label>Novo Valor (R$)</mat-label>
2281
+ <mat-label>{{ getCurrencyFieldLabel(row.field) }}</mat-label>
2163
2282
  <input matInput type="number" step="0.01" [(ngModel)]="row.value" (ngModelChange)="errorMessage.set(null)" placeholder="0.00" />
2164
2283
  </mat-form-field>
2165
2284
  }
@@ -2209,7 +2328,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
2209
2328
  }
2210
2329
 
2211
2330
  <!-- Remover Linha -->
2212
- <button mat-icon-button color="warn" (click)="removeFieldRow($index)" title="Remover">
2331
+ <button mat-icon-button color="warn" (click)="removeFieldRow(idx)" title="Remover">
2213
2332
  <mat-icon>delete_outline</mat-icon>
2214
2333
  </button>
2215
2334
  </div>
@@ -2528,6 +2647,11 @@ class PraxisKpiBandComponent {
2528
2647
  * para cliques em cards. Quando explicitamente falso, evita a falsa affordance de botões.
2529
2648
  */
2530
2649
  isInteractiveHost;
2650
+ /**
2651
+ * Filtro ativo sincronizado pelo host (ex.: PraxisCrudComponent / PraxisResourcePage)
2652
+ * para destacar visualmente o card de KPI correspondente.
2653
+ */
2654
+ activeFilter;
2531
2655
  cardClick = new EventEmitter();
2532
2656
  statsLoaded = new EventEmitter();
2533
2657
  statsError = new EventEmitter();
@@ -2578,7 +2702,7 @@ class PraxisKpiBandComponent {
2578
2702
  if (!this.initialized) {
2579
2703
  return;
2580
2704
  }
2581
- if (changes['config'] || changes['dataContext']) {
2705
+ if (changes['config'] || changes['dataContext'] || changes['activeFilter'] || changes['isInteractiveHost']) {
2582
2706
  if (changes['config'] && this.config?.source !== changes['config'].previousValue?.source) {
2583
2707
  this.refresh();
2584
2708
  }
@@ -2677,6 +2801,7 @@ class PraxisKpiBandComponent {
2677
2801
  const interactive = card.interactive !== undefined
2678
2802
  ? card.interactive
2679
2803
  : !!(card.action || card.filter || card.route || isObserved);
2804
+ const active = this.isCardFilterActive(card.filter, this.activeFilter);
2680
2805
  return {
2681
2806
  raw: card,
2682
2807
  id: card.id,
@@ -2687,12 +2812,35 @@ class PraxisKpiBandComponent {
2687
2812
  icon: card.icon,
2688
2813
  tone: card.tone || 'neutral',
2689
2814
  interactive,
2815
+ active,
2690
2816
  className: card.className,
2691
2817
  tooltip: card.tooltip,
2692
2818
  };
2693
2819
  });
2694
2820
  this.cdr.markForCheck();
2695
2821
  }
2822
+ isCardFilterActive(cardFilter, activeFilter) {
2823
+ if (cardFilter === undefined ||
2824
+ cardFilter === null ||
2825
+ !activeFilter ||
2826
+ typeof activeFilter !== 'object') {
2827
+ return false;
2828
+ }
2829
+ if (typeof cardFilter !== 'object' || Array.isArray(cardFilter)) {
2830
+ return false;
2831
+ }
2832
+ const cardRecord = cardFilter;
2833
+ const cardKeys = Object.keys(cardRecord).sort();
2834
+ const activeKeys = Object.keys(activeFilter).sort();
2835
+ if (cardKeys.length !== activeKeys.length) {
2836
+ return false;
2837
+ }
2838
+ if (cardKeys.length === 0) {
2839
+ return true;
2840
+ }
2841
+ return cardKeys.every((key, idx) => key === activeKeys[idx] &&
2842
+ JSON.stringify(cardRecord[key]) === JSON.stringify(activeFilter[key]));
2843
+ }
2696
2844
  onCardClick(card, index) {
2697
2845
  if (!card.interactive)
2698
2846
  return;
@@ -2703,7 +2851,7 @@ class PraxisKpiBandComponent {
2703
2851
  });
2704
2852
  }
2705
2853
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisKpiBandComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2706
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.14", type: PraxisKpiBandComponent, isStandalone: true, selector: "praxis-kpi-band", inputs: { config: "config", dataContext: "dataContext", isInteractiveHost: "isInteractiveHost" }, outputs: { cardClick: "cardClick", statsLoaded: "statsLoaded", statsError: "statsError", refreshClick: "refreshClick" }, usesOnChanges: true, ngImport: i0, template: `
2854
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.14", type: PraxisKpiBandComponent, isStandalone: true, selector: "praxis-kpi-band", inputs: { config: "config", dataContext: "dataContext", isInteractiveHost: "isInteractiveHost", activeFilter: "activeFilter" }, outputs: { cardClick: "cardClick", statsLoaded: "statsLoaded", statsError: "statsError", refreshClick: "refreshClick" }, usesOnChanges: true, ngImport: i0, template: `
2707
2855
  <div
2708
2856
  *ngIf="isVisible"
2709
2857
  class="praxis-kpi-band"
@@ -2746,9 +2894,11 @@ class PraxisKpiBandComponent {
2746
2894
  *ngFor="let card of evaluatedCards; let i = index"
2747
2895
  class="praxis-kpi-card"
2748
2896
  [class.interactive]="card.interactive"
2897
+ [class.active]="card.active"
2749
2898
  [ngClass]="[card.tone ? 'tone-' + card.tone : 'tone-neutral', card.className || '']"
2750
2899
  [attr.role]="card.interactive ? 'button' : null"
2751
2900
  [attr.tabindex]="card.interactive ? 0 : null"
2901
+ [attr.aria-pressed]="card.interactive && card.raw.filter !== undefined ? (card.active ? 'true' : 'false') : null"
2752
2902
  [matTooltip]="card.tooltip || ''"
2753
2903
  (click)="onCardClick(card, i)"
2754
2904
  (keydown.enter)="onCardClick(card, i)"
@@ -2774,7 +2924,7 @@ class PraxisKpiBandComponent {
2774
2924
  </ng-template>
2775
2925
  </div>
2776
2926
  </div>
2777
- `, isInline: true, styles: [".praxis-kpi-band{display:flex;flex-direction:column;gap:.75rem;margin-bottom:1.25rem;width:100%;box-sizing:border-box}.praxis-kpi-band-header{display:flex;align-items:center;justify-content:space-between;gap:1rem}.praxis-kpi-band-title{margin:0;font-size:1rem;font-weight:600;color:var(--praxis-text-primary, #0f172a)}.praxis-kpi-band-subtitle{margin:.125rem 0 0;font-size:.8125rem;color:var(--praxis-text-muted, #64748b)}.praxis-kpi-band-grid{display:grid;grid-template-columns:repeat(var(--praxis-kpi-columns, auto-fit),minmax(200px,1fr));gap:var(--praxis-kpi-gap, 1rem);width:100%}.praxis-kpi-card{display:flex;flex-direction:column;justify-content:space-between;padding:1rem 1.25rem;border-radius:.75rem;background:var(--praxis-surface-card, #ffffff);border:1px solid var(--praxis-border-subtle, #e2e8f0);box-shadow:0 1px 3px #0000000a;transition:transform .15s ease,box-shadow .15s ease,border-color .15s ease;position:relative;overflow:hidden;min-height:5.5rem;box-sizing:border-box}.praxis-kpi-card.interactive{cursor:pointer}.praxis-kpi-card.interactive:hover{transform:translateY(-2px);box-shadow:0 4px 12px #00000014}.praxis-kpi-card.interactive:focus-visible{outline:2px solid var(--praxis-primary, #0284c7);outline-offset:2px}.praxis-kpi-card-top{display:flex;align-items:center;justify-content:space-between;margin-bottom:.5rem}.praxis-kpi-card-label{font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--praxis-text-muted, #64748b)}.praxis-kpi-card-icon{display:flex;align-items:center;justify-content:center;width:2rem;height:2rem;border-radius:.5rem}.praxis-kpi-card-icon mat-icon{font-size:1.25rem;width:1.25rem;height:1.25rem}.praxis-kpi-card-value{font-size:1.625rem;font-weight:700;line-height:1.2;letter-spacing:-.02em;font-feature-settings:\"tnum\" on,\"lnum\" on;color:var(--praxis-text-primary, #0f172a)}.praxis-kpi-card-caption{font-size:.75rem;color:var(--praxis-text-muted, #64748b);margin-top:.375rem}.praxis-kpi-card-footnote{font-size:.7rem;color:var(--praxis-text-secondary, #94a3b8);margin-top:.25rem}.tone-info{border-left:4px solid var(--praxis-info, #0284c7)}.tone-info .praxis-kpi-card-icon{background:color-mix(in srgb,var(--praxis-info, #0284c7) 14%,transparent);color:var(--praxis-info, #0284c7)}.tone-success{border-left:4px solid var(--praxis-success, #059669)}.tone-success .praxis-kpi-card-icon{background:color-mix(in srgb,var(--praxis-success, #059669) 14%,transparent);color:var(--praxis-success, #059669)}.tone-warning{border-left:4px solid var(--praxis-warning, #d97706)}.tone-warning .praxis-kpi-card-icon{background:color-mix(in srgb,var(--praxis-warning, #d97706) 14%,transparent);color:var(--praxis-warning, #d97706)}.tone-error,.tone-danger{border-left:4px solid var(--praxis-error, #dc2626)}.tone-error .praxis-kpi-card-icon,.tone-danger .praxis-kpi-card-icon{background:color-mix(in srgb,var(--praxis-error, #dc2626) 14%,transparent);color:var(--praxis-error, #dc2626)}.tone-neutral{border-left:4px solid var(--praxis-neutral, #94a3b8)}.tone-neutral .praxis-kpi-card-icon{background:color-mix(in srgb,var(--praxis-neutral, #94a3b8) 14%,transparent);color:var(--praxis-text-secondary, #64748b)}.praxis-kpi-card-skeleton{pointer-events:none}.praxis-kpi-skeleton-line{background:linear-gradient(90deg,color-mix(in srgb,var(--praxis-text-secondary, #94a3b8) 12%,transparent) 25%,color-mix(in srgb,var(--praxis-text-secondary, #94a3b8) 22%,transparent),color-mix(in srgb,var(--praxis-text-secondary, #94a3b8) 12%,transparent) 75%);background-size:200% 100%;animation:praxis-shimmer 1.5s infinite;border-radius:.25rem}.praxis-kpi-skeleton-line.label{width:40%;height:.75rem;margin-bottom:.75rem}.praxis-kpi-skeleton-line.value{width:65%;height:1.5rem;margin-bottom:.5rem}.praxis-kpi-skeleton-line.caption{width:50%;height:.65rem}@keyframes praxis-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2927
+ `, isInline: true, styles: [".praxis-kpi-band{display:flex;flex-direction:column;gap:.75rem;margin-bottom:1.25rem;width:100%;box-sizing:border-box}.praxis-kpi-band-header{display:flex;align-items:center;justify-content:space-between;gap:1rem}.praxis-kpi-band-title{margin:0;font-size:1rem;font-weight:600;color:var(--praxis-text-primary, #0f172a)}.praxis-kpi-band-subtitle{margin:.125rem 0 0;font-size:.8125rem;color:var(--praxis-text-muted, #64748b)}.praxis-kpi-band-grid{display:grid;grid-template-columns:repeat(var(--praxis-kpi-columns, auto-fit),minmax(200px,1fr));gap:var(--praxis-kpi-gap, 1rem);width:100%}.praxis-kpi-card{display:flex;flex-direction:column;justify-content:space-between;padding:1rem 1.25rem;border-radius:.75rem;background:var(--praxis-surface-card, #ffffff);border:1px solid var(--praxis-border-subtle, #e2e8f0);box-shadow:0 1px 3px #0000000a;transition:transform .15s ease,box-shadow .15s ease,border-color .15s ease;position:relative;overflow:hidden;min-height:5.5rem;box-sizing:border-box}.praxis-kpi-card.interactive{cursor:pointer}.praxis-kpi-card.interactive:hover{transform:translateY(-2px);box-shadow:0 4px 12px #00000014}.praxis-kpi-card.active{border-color:var(--praxis-primary, #0284c7);box-shadow:0 0 0 1px color-mix(in srgb,var(--praxis-primary, #0284c7) 45%,transparent),0 4px 12px #00000014}.praxis-kpi-card.interactive:focus-visible{outline:2px solid var(--praxis-primary, #0284c7);outline-offset:2px}.praxis-kpi-card-top{display:flex;align-items:center;justify-content:space-between;margin-bottom:.5rem}.praxis-kpi-card-label{font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--praxis-text-muted, #64748b)}.praxis-kpi-card-icon{display:flex;align-items:center;justify-content:center;width:2rem;height:2rem;border-radius:.5rem}.praxis-kpi-card-icon mat-icon{font-size:1.25rem;width:1.25rem;height:1.25rem}.praxis-kpi-card-value{font-size:1.625rem;font-weight:700;line-height:1.2;letter-spacing:-.02em;font-feature-settings:\"tnum\" on,\"lnum\" on;color:var(--praxis-text-primary, #0f172a)}.praxis-kpi-card-caption{font-size:.75rem;color:var(--praxis-text-muted, #64748b);margin-top:.375rem}.praxis-kpi-card-footnote{font-size:.7rem;color:var(--praxis-text-secondary, #94a3b8);margin-top:.25rem}.tone-info{border-left:4px solid var(--praxis-info, #0284c7)}.tone-info .praxis-kpi-card-icon{background:color-mix(in srgb,var(--praxis-info, #0284c7) 14%,transparent);color:var(--praxis-info, #0284c7)}.tone-success{border-left:4px solid var(--praxis-success, #059669)}.tone-success .praxis-kpi-card-icon{background:color-mix(in srgb,var(--praxis-success, #059669) 14%,transparent);color:var(--praxis-success, #059669)}.tone-warning{border-left:4px solid var(--praxis-warning, #d97706)}.tone-warning .praxis-kpi-card-icon{background:color-mix(in srgb,var(--praxis-warning, #d97706) 14%,transparent);color:var(--praxis-warning, #d97706)}.tone-error,.tone-danger{border-left:4px solid var(--praxis-error, #dc2626)}.tone-error .praxis-kpi-card-icon,.tone-danger .praxis-kpi-card-icon{background:color-mix(in srgb,var(--praxis-error, #dc2626) 14%,transparent);color:var(--praxis-error, #dc2626)}.tone-neutral{border-left:4px solid var(--praxis-neutral, #94a3b8)}.tone-neutral .praxis-kpi-card-icon{background:color-mix(in srgb,var(--praxis-neutral, #94a3b8) 14%,transparent);color:var(--praxis-text-secondary, #64748b)}.praxis-kpi-card-skeleton{pointer-events:none}.praxis-kpi-skeleton-line{background:linear-gradient(90deg,color-mix(in srgb,var(--praxis-text-secondary, #94a3b8) 12%,transparent) 25%,color-mix(in srgb,var(--praxis-text-secondary, #94a3b8) 22%,transparent),color-mix(in srgb,var(--praxis-text-secondary, #94a3b8) 12%,transparent) 75%);background-size:200% 100%;animation:praxis-shimmer 1.5s infinite;border-radius:.25rem}.praxis-kpi-skeleton-line.label{width:40%;height:.75rem;margin-bottom:.75rem}.praxis-kpi-skeleton-line.value{width:65%;height:1.5rem;margin-bottom:.5rem}.praxis-kpi-skeleton-line.caption{width:50%;height:.65rem}@keyframes praxis-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2778
2928
  }
2779
2929
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisKpiBandComponent, decorators: [{
2780
2930
  type: Component,
@@ -2821,9 +2971,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
2821
2971
  *ngFor="let card of evaluatedCards; let i = index"
2822
2972
  class="praxis-kpi-card"
2823
2973
  [class.interactive]="card.interactive"
2974
+ [class.active]="card.active"
2824
2975
  [ngClass]="[card.tone ? 'tone-' + card.tone : 'tone-neutral', card.className || '']"
2825
2976
  [attr.role]="card.interactive ? 'button' : null"
2826
2977
  [attr.tabindex]="card.interactive ? 0 : null"
2978
+ [attr.aria-pressed]="card.interactive && card.raw.filter !== undefined ? (card.active ? 'true' : 'false') : null"
2827
2979
  [matTooltip]="card.tooltip || ''"
2828
2980
  (click)="onCardClick(card, i)"
2829
2981
  (keydown.enter)="onCardClick(card, i)"
@@ -2849,13 +3001,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
2849
3001
  </ng-template>
2850
3002
  </div>
2851
3003
  </div>
2852
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".praxis-kpi-band{display:flex;flex-direction:column;gap:.75rem;margin-bottom:1.25rem;width:100%;box-sizing:border-box}.praxis-kpi-band-header{display:flex;align-items:center;justify-content:space-between;gap:1rem}.praxis-kpi-band-title{margin:0;font-size:1rem;font-weight:600;color:var(--praxis-text-primary, #0f172a)}.praxis-kpi-band-subtitle{margin:.125rem 0 0;font-size:.8125rem;color:var(--praxis-text-muted, #64748b)}.praxis-kpi-band-grid{display:grid;grid-template-columns:repeat(var(--praxis-kpi-columns, auto-fit),minmax(200px,1fr));gap:var(--praxis-kpi-gap, 1rem);width:100%}.praxis-kpi-card{display:flex;flex-direction:column;justify-content:space-between;padding:1rem 1.25rem;border-radius:.75rem;background:var(--praxis-surface-card, #ffffff);border:1px solid var(--praxis-border-subtle, #e2e8f0);box-shadow:0 1px 3px #0000000a;transition:transform .15s ease,box-shadow .15s ease,border-color .15s ease;position:relative;overflow:hidden;min-height:5.5rem;box-sizing:border-box}.praxis-kpi-card.interactive{cursor:pointer}.praxis-kpi-card.interactive:hover{transform:translateY(-2px);box-shadow:0 4px 12px #00000014}.praxis-kpi-card.interactive:focus-visible{outline:2px solid var(--praxis-primary, #0284c7);outline-offset:2px}.praxis-kpi-card-top{display:flex;align-items:center;justify-content:space-between;margin-bottom:.5rem}.praxis-kpi-card-label{font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--praxis-text-muted, #64748b)}.praxis-kpi-card-icon{display:flex;align-items:center;justify-content:center;width:2rem;height:2rem;border-radius:.5rem}.praxis-kpi-card-icon mat-icon{font-size:1.25rem;width:1.25rem;height:1.25rem}.praxis-kpi-card-value{font-size:1.625rem;font-weight:700;line-height:1.2;letter-spacing:-.02em;font-feature-settings:\"tnum\" on,\"lnum\" on;color:var(--praxis-text-primary, #0f172a)}.praxis-kpi-card-caption{font-size:.75rem;color:var(--praxis-text-muted, #64748b);margin-top:.375rem}.praxis-kpi-card-footnote{font-size:.7rem;color:var(--praxis-text-secondary, #94a3b8);margin-top:.25rem}.tone-info{border-left:4px solid var(--praxis-info, #0284c7)}.tone-info .praxis-kpi-card-icon{background:color-mix(in srgb,var(--praxis-info, #0284c7) 14%,transparent);color:var(--praxis-info, #0284c7)}.tone-success{border-left:4px solid var(--praxis-success, #059669)}.tone-success .praxis-kpi-card-icon{background:color-mix(in srgb,var(--praxis-success, #059669) 14%,transparent);color:var(--praxis-success, #059669)}.tone-warning{border-left:4px solid var(--praxis-warning, #d97706)}.tone-warning .praxis-kpi-card-icon{background:color-mix(in srgb,var(--praxis-warning, #d97706) 14%,transparent);color:var(--praxis-warning, #d97706)}.tone-error,.tone-danger{border-left:4px solid var(--praxis-error, #dc2626)}.tone-error .praxis-kpi-card-icon,.tone-danger .praxis-kpi-card-icon{background:color-mix(in srgb,var(--praxis-error, #dc2626) 14%,transparent);color:var(--praxis-error, #dc2626)}.tone-neutral{border-left:4px solid var(--praxis-neutral, #94a3b8)}.tone-neutral .praxis-kpi-card-icon{background:color-mix(in srgb,var(--praxis-neutral, #94a3b8) 14%,transparent);color:var(--praxis-text-secondary, #64748b)}.praxis-kpi-card-skeleton{pointer-events:none}.praxis-kpi-skeleton-line{background:linear-gradient(90deg,color-mix(in srgb,var(--praxis-text-secondary, #94a3b8) 12%,transparent) 25%,color-mix(in srgb,var(--praxis-text-secondary, #94a3b8) 22%,transparent),color-mix(in srgb,var(--praxis-text-secondary, #94a3b8) 12%,transparent) 75%);background-size:200% 100%;animation:praxis-shimmer 1.5s infinite;border-radius:.25rem}.praxis-kpi-skeleton-line.label{width:40%;height:.75rem;margin-bottom:.75rem}.praxis-kpi-skeleton-line.value{width:65%;height:1.5rem;margin-bottom:.5rem}.praxis-kpi-skeleton-line.caption{width:50%;height:.65rem}@keyframes praxis-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}\n"] }]
3004
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".praxis-kpi-band{display:flex;flex-direction:column;gap:.75rem;margin-bottom:1.25rem;width:100%;box-sizing:border-box}.praxis-kpi-band-header{display:flex;align-items:center;justify-content:space-between;gap:1rem}.praxis-kpi-band-title{margin:0;font-size:1rem;font-weight:600;color:var(--praxis-text-primary, #0f172a)}.praxis-kpi-band-subtitle{margin:.125rem 0 0;font-size:.8125rem;color:var(--praxis-text-muted, #64748b)}.praxis-kpi-band-grid{display:grid;grid-template-columns:repeat(var(--praxis-kpi-columns, auto-fit),minmax(200px,1fr));gap:var(--praxis-kpi-gap, 1rem);width:100%}.praxis-kpi-card{display:flex;flex-direction:column;justify-content:space-between;padding:1rem 1.25rem;border-radius:.75rem;background:var(--praxis-surface-card, #ffffff);border:1px solid var(--praxis-border-subtle, #e2e8f0);box-shadow:0 1px 3px #0000000a;transition:transform .15s ease,box-shadow .15s ease,border-color .15s ease;position:relative;overflow:hidden;min-height:5.5rem;box-sizing:border-box}.praxis-kpi-card.interactive{cursor:pointer}.praxis-kpi-card.interactive:hover{transform:translateY(-2px);box-shadow:0 4px 12px #00000014}.praxis-kpi-card.active{border-color:var(--praxis-primary, #0284c7);box-shadow:0 0 0 1px color-mix(in srgb,var(--praxis-primary, #0284c7) 45%,transparent),0 4px 12px #00000014}.praxis-kpi-card.interactive:focus-visible{outline:2px solid var(--praxis-primary, #0284c7);outline-offset:2px}.praxis-kpi-card-top{display:flex;align-items:center;justify-content:space-between;margin-bottom:.5rem}.praxis-kpi-card-label{font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--praxis-text-muted, #64748b)}.praxis-kpi-card-icon{display:flex;align-items:center;justify-content:center;width:2rem;height:2rem;border-radius:.5rem}.praxis-kpi-card-icon mat-icon{font-size:1.25rem;width:1.25rem;height:1.25rem}.praxis-kpi-card-value{font-size:1.625rem;font-weight:700;line-height:1.2;letter-spacing:-.02em;font-feature-settings:\"tnum\" on,\"lnum\" on;color:var(--praxis-text-primary, #0f172a)}.praxis-kpi-card-caption{font-size:.75rem;color:var(--praxis-text-muted, #64748b);margin-top:.375rem}.praxis-kpi-card-footnote{font-size:.7rem;color:var(--praxis-text-secondary, #94a3b8);margin-top:.25rem}.tone-info{border-left:4px solid var(--praxis-info, #0284c7)}.tone-info .praxis-kpi-card-icon{background:color-mix(in srgb,var(--praxis-info, #0284c7) 14%,transparent);color:var(--praxis-info, #0284c7)}.tone-success{border-left:4px solid var(--praxis-success, #059669)}.tone-success .praxis-kpi-card-icon{background:color-mix(in srgb,var(--praxis-success, #059669) 14%,transparent);color:var(--praxis-success, #059669)}.tone-warning{border-left:4px solid var(--praxis-warning, #d97706)}.tone-warning .praxis-kpi-card-icon{background:color-mix(in srgb,var(--praxis-warning, #d97706) 14%,transparent);color:var(--praxis-warning, #d97706)}.tone-error,.tone-danger{border-left:4px solid var(--praxis-error, #dc2626)}.tone-error .praxis-kpi-card-icon,.tone-danger .praxis-kpi-card-icon{background:color-mix(in srgb,var(--praxis-error, #dc2626) 14%,transparent);color:var(--praxis-error, #dc2626)}.tone-neutral{border-left:4px solid var(--praxis-neutral, #94a3b8)}.tone-neutral .praxis-kpi-card-icon{background:color-mix(in srgb,var(--praxis-neutral, #94a3b8) 14%,transparent);color:var(--praxis-text-secondary, #64748b)}.praxis-kpi-card-skeleton{pointer-events:none}.praxis-kpi-skeleton-line{background:linear-gradient(90deg,color-mix(in srgb,var(--praxis-text-secondary, #94a3b8) 12%,transparent) 25%,color-mix(in srgb,var(--praxis-text-secondary, #94a3b8) 22%,transparent),color-mix(in srgb,var(--praxis-text-secondary, #94a3b8) 12%,transparent) 75%);background-size:200% 100%;animation:praxis-shimmer 1.5s infinite;border-radius:.25rem}.praxis-kpi-skeleton-line.label{width:40%;height:.75rem;margin-bottom:.75rem}.praxis-kpi-skeleton-line.value{width:65%;height:1.5rem;margin-bottom:.5rem}.praxis-kpi-skeleton-line.caption{width:50%;height:.65rem}@keyframes praxis-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}\n"] }]
2853
3005
  }], propDecorators: { config: [{
2854
3006
  type: Input
2855
3007
  }], dataContext: [{
2856
3008
  type: Input
2857
3009
  }], isInteractiveHost: [{
2858
3010
  type: Input
3011
+ }], activeFilter: [{
3012
+ type: Input
2859
3013
  }], cardClick: [{
2860
3014
  type: Output
2861
3015
  }], statsLoaded: [{
@@ -6869,11 +7023,14 @@ class PraxisCrudComponent {
6869
7023
  }
6870
7024
  onKpiCardClick(event) {
6871
7025
  this.kpiCardClick.emit(event);
6872
- if (event.card.filter && typeof event.card.filter === 'object') {
6873
- this.tableQueryContext = {
6874
- ...(this.tableQueryContext || {}),
7026
+ if (!this.kpiCardClick.observed && event.card.filter && typeof event.card.filter === 'object') {
7027
+ this.tableFilterCriteria = {
7028
+ ...this.resolveFilterCriteria(this.resolvedMetadata),
6875
7029
  ...event.card.filter,
6876
7030
  };
7031
+ if (this.table) {
7032
+ this.table.applyAdvancedFilterCriteria(this.tableFilterCriteria);
7033
+ }
6877
7034
  this.cdr.markForCheck();
6878
7035
  }
6879
7036
  }
@@ -7054,6 +7211,9 @@ class PraxisCrudComponent {
7054
7211
  this.snack.open('Nenhum registro selecionado para a operação em lote.', undefined, { duration: 3000 });
7055
7212
  return;
7056
7213
  }
7214
+ if (this.openingActionLabel()) {
7215
+ return;
7216
+ }
7057
7217
  const resourcePath = String(this.resolveResourcePath(this.resolvedMetadata) || '').trim();
7058
7218
  const resourceKey = String(this.resolvedMetadata?.resource?.resourceKey ||
7059
7219
  this.tableCrudContext?.tableId ||
@@ -7064,118 +7224,164 @@ class PraxisCrudComponent {
7064
7224
  (a.id || a.action) === action);
7065
7225
  const tableBulkAction = (this.effectiveTableConfig?.actions?.bulk?.actions || []).find((a) => this.normalizeCrudActionName(a.id || a.action) === normalizedAction ||
7066
7226
  (a.id || a.action) === action);
7067
- let resolvedBulk = this.resolveSyncBulkExecutionContractForAction(action, normalizedAction, configAction, crudAction, tableBulkAction);
7068
- if (!resolvedBulk.contract && this.tableCollectionLinks && (!crudAction || !this.hasExplicitOpenBinding(crudAction))) {
7069
- resolvedBulk = await this.resolveAsyncDiscoveredBulkContractForAction(action, normalizedAction);
7227
+ this.openingActionLabel.set(this.resolveOpeningActionLabel(action, event));
7228
+ try {
7229
+ let resolvedBulk = this.resolveSyncBulkExecutionContractForAction(action, normalizedAction, configAction, crudAction, tableBulkAction);
7230
+ if (!resolvedBulk.contract && this.tableCollectionLinks && (!crudAction || !this.hasExplicitOpenBinding(crudAction))) {
7231
+ resolvedBulk = await this.resolveAsyncDiscoveredBulkContractForAction(action, normalizedAction);
7232
+ }
7233
+ const bulkContract = resolvedBulk.contract;
7234
+ const discoveredCatalogAction = resolvedBulk.catalogAction;
7235
+ if (bulkContract) {
7236
+ const selectedCount = Array.isArray(rawTargets) && rawTargets.length > 0 ? rawTargets.length : rows.length;
7237
+ const maxTargets = bulkContract.limits?.maxTargets;
7238
+ if (typeof maxTargets === 'number' && maxTargets > 0 && selectedCount > maxTargets) {
7239
+ this.snack.open(`A seleção atual (${selectedCount} itens) excede o limite máximo permitido (${maxTargets} itens por lote).`, undefined, { duration: 4000 });
7240
+ return;
7241
+ }
7242
+ const requiresResourceVersion = discoveredCatalogAction?.execution?.preconditions?.resourceVersion !== 'NONE';
7243
+ const targets = Array.isArray(rawTargets) && rawTargets.length > 0
7244
+ ? rawTargets
7245
+ : await this.resolveBulkTargetsFromRows(rows, requiresResourceVersion);
7246
+ if (!targets.length) {
7247
+ this.snack.open('Nenhum registro selecionado para a operação em lote.', undefined, { duration: 3000 });
7248
+ return;
7249
+ }
7250
+ const columns = this.table?.config?.columns?.length
7251
+ ? this.table.config.columns
7252
+ : this.effectiveTableConfig?.columns;
7253
+ const schema = this.resolvedMetadata?.schema || this.resolvedMetadata?.resource?.schema;
7254
+ if (bulkContract.mode === 'UNIFORM_UPDATE') {
7255
+ const actionTitle = configAction?.title ||
7256
+ tableBulkAction?.title ||
7257
+ discoveredCatalogAction?.title;
7258
+ const dialogRef = this.dialog.open(PraxisBulkEditDialogComponent, {
7259
+ width: '680px',
7260
+ maxWidth: '92vw',
7261
+ data: {
7262
+ bulkContract,
7263
+ targets,
7264
+ resourcePath,
7265
+ resourceKey,
7266
+ endpointKey: this.resolvedMetadata?.resource?.endpointKey,
7267
+ apiUrlEntry: this.resolveDiscoveryApiEntry(),
7268
+ columns,
7269
+ schema,
7270
+ title: actionTitle || 'Atualização de Registros em Massa',
7271
+ },
7272
+ panelClass: ['praxis-bulk-dialog-panel'],
7273
+ backdropClass: ['praxis-bulk-dialog-backdrop'],
7274
+ disableClose: true,
7275
+ });
7276
+ dialogRef
7277
+ .afterClosed()
7278
+ .pipe(takeUntilDestroyed(this.destroyRef))
7279
+ .subscribe((res) => {
7280
+ this.handleBulkDialogResult(res, discoveredCatalogAction?.execution?.refresh, 'Atualização em lote concluída com sucesso.');
7281
+ });
7282
+ return;
7283
+ }
7284
+ if (bulkContract.mode === 'DOMAIN_COMMAND') {
7285
+ const actionRef = discoveredCatalogAction || crudAction || configAction || {
7286
+ id: action,
7287
+ action,
7288
+ label: configAction?.label || tableBulkAction?.label || action,
7289
+ title: configAction?.title || configAction?.label || tableBulkAction?.label || action,
7290
+ execution: { bulk: bulkContract },
7291
+ };
7292
+ if (!actionRef.execution?.bulk) {
7293
+ actionRef.execution = { ...(actionRef.execution || {}), bulk: bulkContract };
7294
+ }
7295
+ actionRef.title = actionRef.title || actionRef.label || action;
7296
+ const dialogRef = this.dialog.open(PraxisBulkOperationDialogComponent, {
7297
+ width: '680px',
7298
+ maxWidth: '92vw',
7299
+ data: {
7300
+ action: actionRef,
7301
+ targets,
7302
+ resourcePath,
7303
+ resourceKey,
7304
+ endpointKey: this.resolvedMetadata?.resource?.endpointKey,
7305
+ apiUrlEntry: this.resolveDiscoveryApiEntry(),
7306
+ columns,
7307
+ schema,
7308
+ },
7309
+ panelClass: ['praxis-bulk-dialog-panel'],
7310
+ backdropClass: ['praxis-bulk-dialog-backdrop'],
7311
+ disableClose: true,
7312
+ });
7313
+ dialogRef
7314
+ .afterClosed()
7315
+ .pipe(takeUntilDestroyed(this.destroyRef))
7316
+ .subscribe((res) => {
7317
+ this.handleBulkDialogResult(res, discoveredCatalogAction?.execution?.refresh, 'Operação em lote executada com sucesso.');
7318
+ });
7319
+ return;
7320
+ }
7321
+ }
7322
+ }
7323
+ finally {
7324
+ this.openingActionLabel.set(null);
7325
+ }
7326
+ const row = this.extractSelectedRowFromBulkAction(event);
7327
+ if (row) {
7328
+ this.selectedRow = row;
7070
7329
  }
7071
- const bulkContract = resolvedBulk.contract;
7072
- const discoveredCatalogAction = resolvedBulk.catalogAction;
7073
- if (bulkContract) {
7074
- const selectedCount = Array.isArray(rawTargets) && rawTargets.length > 0 ? rawTargets.length : rows.length;
7075
- const maxTargets = bulkContract.limits?.maxTargets;
7076
- if (typeof maxTargets === 'number' && maxTargets > 0 && selectedCount > maxTargets) {
7077
- this.snack.open(`A seleção atual (${selectedCount} itens) excede o limite máximo permitido (${maxTargets} itens por lote).`, undefined, { duration: 4000 });
7330
+ await this.onAction(action, row ?? undefined, event);
7331
+ }
7332
+ handleBulkDialogResult(res, refreshPolicy, defaultSuccessMessage) {
7333
+ const exec = res?.execution;
7334
+ if (exec && exec.status) {
7335
+ const status = exec.status;
7336
+ const confirmed = exec.totals?.['confirmed'] ?? 0;
7337
+ const rejected = (exec.totals?.['denied'] ?? 0) +
7338
+ (exec.totals?.['invalid'] ?? 0) +
7339
+ (exec.totals?.['conflict'] ?? 0) +
7340
+ (exec.totals?.['notProcessed'] ?? 0) +
7341
+ (exec.totals?.['unknown'] ?? 0);
7342
+ if (status === 'COMPLETED' && rejected === 0) {
7343
+ this.handleBulkExecutionCompleted(refreshPolicy, true);
7344
+ this.snack.open(defaultSuccessMessage, undefined, { duration: 3000 });
7078
7345
  return;
7079
7346
  }
7080
- const requiresResourceVersion = discoveredCatalogAction?.execution?.preconditions?.resourceVersion !== 'NONE';
7081
- const targets = Array.isArray(rawTargets) && rawTargets.length > 0
7082
- ? rawTargets
7083
- : await this.resolveBulkTargetsFromRows(rows, requiresResourceVersion);
7084
- if (!targets.length) {
7085
- this.snack.open('Nenhum registro selecionado para a operação em lote.', undefined, { duration: 3000 });
7347
+ if (status === 'COMPLETED_WITH_ERRORS' || (status === 'COMPLETED' && rejected > 0)) {
7348
+ this.handleBulkExecutionCompleted(refreshPolicy, false);
7349
+ this.snack.open(`Operação em lote concluída parcialmente (${confirmed} efetivado(s), ${rejected} com restrição).`, undefined, { duration: 4000 });
7086
7350
  return;
7087
7351
  }
7088
- const columns = this.table?.config?.columns?.length
7089
- ? this.table.config.columns
7090
- : this.effectiveTableConfig?.columns;
7091
- const schema = this.resolvedMetadata?.schema || this.resolvedMetadata?.resource?.schema;
7092
- if (bulkContract.mode === 'UNIFORM_UPDATE') {
7093
- const actionTitle = configAction?.title ||
7094
- tableBulkAction?.title ||
7095
- discoveredCatalogAction?.title;
7096
- const dialogRef = this.dialog.open(PraxisBulkEditDialogComponent, {
7097
- width: '680px',
7098
- maxWidth: '92vw',
7099
- data: {
7100
- bulkContract,
7101
- targets,
7102
- resourcePath,
7103
- resourceKey,
7104
- endpointKey: this.resolvedMetadata?.resource?.endpointKey,
7105
- apiUrlEntry: this.resolveDiscoveryApiEntry(),
7106
- columns,
7107
- schema,
7108
- title: actionTitle || 'Atualização de Registros em Massa',
7109
- },
7110
- panelClass: ['praxis-bulk-dialog-panel'],
7111
- backdropClass: ['praxis-bulk-dialog-backdrop'],
7112
- disableClose: true,
7113
- });
7114
- dialogRef
7115
- .afterClosed()
7116
- .pipe(takeUntilDestroyed(this.destroyRef))
7117
- .subscribe((res) => {
7118
- if (res?.success) {
7119
- this.handleBulkExecutionCompleted(discoveredCatalogAction?.execution?.refresh);
7120
- this.snack.open('Atualização em lote concluída com sucesso.', undefined, { duration: 3000 });
7121
- }
7122
- });
7352
+ if (status === 'CANCELLED') {
7353
+ if (confirmed > 0) {
7354
+ this.handleBulkExecutionCompleted(refreshPolicy, false);
7355
+ }
7356
+ this.snack.open(confirmed > 0
7357
+ ? `Execução em lote cancelada após efetivar ${confirmed} registro(s).`
7358
+ : 'Execução em lote cancelada pelo operador.', undefined, { duration: 4000 });
7123
7359
  return;
7124
7360
  }
7125
- if (bulkContract.mode === 'DOMAIN_COMMAND') {
7126
- const actionRef = discoveredCatalogAction || crudAction || configAction || {
7127
- id: action,
7128
- action,
7129
- label: configAction?.label || tableBulkAction?.label || action,
7130
- title: configAction?.title || configAction?.label || tableBulkAction?.label || action,
7131
- execution: { bulk: bulkContract },
7132
- };
7133
- if (!actionRef.execution?.bulk) {
7134
- actionRef.execution = { ...(actionRef.execution || {}), bulk: bulkContract };
7361
+ if (status === 'STOPPED' || status === 'RECONCILIATION_REQUIRED') {
7362
+ if (confirmed > 0 || status === 'RECONCILIATION_REQUIRED') {
7363
+ this.handleBulkExecutionCompleted(refreshPolicy, false);
7135
7364
  }
7136
- actionRef.title = actionRef.title || actionRef.label || action;
7137
- const dialogRef = this.dialog.open(PraxisBulkOperationDialogComponent, {
7138
- width: '680px',
7139
- maxWidth: '92vw',
7140
- data: {
7141
- action: actionRef,
7142
- targets,
7143
- resourcePath,
7144
- resourceKey,
7145
- endpointKey: this.resolvedMetadata?.resource?.endpointKey,
7146
- apiUrlEntry: this.resolveDiscoveryApiEntry(),
7147
- columns,
7148
- schema,
7149
- },
7150
- panelClass: ['praxis-bulk-dialog-panel'],
7151
- backdropClass: ['praxis-bulk-dialog-backdrop'],
7152
- disableClose: true,
7153
- });
7154
- dialogRef
7155
- .afterClosed()
7156
- .pipe(takeUntilDestroyed(this.destroyRef))
7157
- .subscribe((res) => {
7158
- if (res?.success) {
7159
- this.handleBulkExecutionCompleted(discoveredCatalogAction?.execution?.refresh);
7160
- this.snack.open('Operação em lote executada com sucesso.', undefined, { duration: 3000 });
7161
- }
7162
- });
7365
+ this.snack.open(status === 'RECONCILIATION_REQUIRED'
7366
+ ? 'Execução em lote exige reconciliação. Verifique os diagnósticos.'
7367
+ : 'Execução em lote interrompida por restrição. Seleção preservada.', undefined, { duration: 4000 });
7163
7368
  return;
7164
7369
  }
7165
7370
  }
7166
- const row = this.extractSelectedRowFromBulkAction(event);
7167
- if (row) {
7168
- this.selectedRow = row;
7371
+ if (res?.success) {
7372
+ this.handleBulkExecutionCompleted(refreshPolicy, true);
7373
+ this.snack.open(defaultSuccessMessage, undefined, { duration: 3000 });
7169
7374
  }
7170
- await this.onAction(action, row ?? undefined, event);
7171
7375
  }
7172
- handleBulkExecutionCompleted(refreshPolicy) {
7173
- try {
7174
- this.table?.clearSelection?.();
7175
- this.table?.selection?.clear?.();
7176
- }
7177
- catch {
7178
- // Best-effort table selection cleanup
7376
+ handleBulkExecutionCompleted(refreshPolicy, clearSelection = true) {
7377
+ if (clearSelection) {
7378
+ try {
7379
+ this.table?.clearSelection?.();
7380
+ this.table?.selection?.clear?.();
7381
+ }
7382
+ catch {
7383
+ // Best-effort table selection cleanup
7384
+ }
7179
7385
  }
7180
7386
  if (refreshPolicy?.collection !== false) {
7181
7387
  this.refreshTable();
@@ -7185,7 +7391,7 @@ class PraxisCrudComponent {
7185
7391
  this.onCollectionLinksChange(this.tableCollectionLinks);
7186
7392
  }
7187
7393
  }
7188
- async resolveBulkTargetsFromRows(rows, fetchSelfWhenVersionMissing = true) {
7394
+ async resolveBulkTargetsFromRows(rows, fetchSelfWhenVersionMissing = true, fallbackResourcePath) {
7189
7395
  const options = this.buildDiscoveryOptions();
7190
7396
  const results = new Array(rows.length);
7191
7397
  const concurrency = 4;
@@ -7199,11 +7405,23 @@ class PraxisCrudComponent {
7199
7405
  (r?.etag != null ? String(r.etag).trim() : null) ||
7200
7406
  (r?.eTag != null ? String(r.eTag).trim() : null) ||
7201
7407
  undefined;
7202
- if (fetchSelfWhenVersionMissing && !expectedVersion && r?._links?.self) {
7408
+ if (fetchSelfWhenVersionMissing && !expectedVersion) {
7203
7409
  try {
7204
- const selfHref = this.getResourceDiscovery().resolveLinkHref(r._links, 'self', options);
7410
+ const discovery = this.getResourceDiscovery();
7411
+ let selfHref = r?._links?.self
7412
+ ? discovery.resolveLinkHref(r._links, 'self', options)
7413
+ : null;
7414
+ if (!selfHref && fallbackResourcePath && id != null) {
7415
+ const candidatePath = `${fallbackResourcePath.replace(/\/+$/, '')}/${encodeURIComponent(String(id))}`;
7416
+ selfHref =
7417
+ discovery.resolveHref?.(candidatePath, options) ??
7418
+ candidatePath;
7419
+ }
7205
7420
  if (selfHref) {
7206
- const resp = await firstValueFrom(this.http.get(selfHref, { observe: 'response' }));
7421
+ const resp = await firstValueFrom(this.http.get(selfHref, {
7422
+ headers: composeHeadersWithVersion(this.resolveDiscoveryApiEntry() ?? {}),
7423
+ observe: 'response',
7424
+ }));
7207
7425
  const etagHeader = resp.headers?.get('ETag');
7208
7426
  const bodyVersion = resp.body?.data?.resourceVersion ??
7209
7427
  resp.body?.resourceVersion ??
@@ -7294,6 +7512,9 @@ class PraxisCrudComponent {
7294
7512
  }
7295
7513
  ngOnChanges(changes) {
7296
7514
  if (!changes['metadata'] && !changes['context'] && !changes['resource']) {
7515
+ if (changes['filterCriteria'] && !changes['filterCriteria'].firstChange) {
7516
+ this.applyFilterCriteria(this.filterCriteria);
7517
+ }
7297
7518
  return;
7298
7519
  }
7299
7520
  if (changes['metadata']) {
@@ -7375,11 +7596,16 @@ class PraxisCrudComponent {
7375
7596
  this.emitInspectOnlyDiagnostics(this.resolvedMetadata);
7376
7597
  this.tableQueryContext = this.resolveQueryContext(this.resolvedMetadata);
7377
7598
  this.tableFilterCriteria = this.resolveFilterCriteria(this.resolvedMetadata);
7378
- this.tableCollectionLinks = null;
7379
- this.collectionCapabilities = null;
7380
- this.collectionCapabilitiesRequestHref = null;
7381
- this.collectionCapabilitiesResolvedHref = null;
7382
- this.collectionCapabilitiesRequestSeq += 1;
7599
+ const nextResourceIdentity = this.resolveCrudResourceIdentity(this.resolvedMetadata);
7600
+ if (nextResourceIdentity !== this.lastAppliedResourceIdentity ||
7601
+ !nextResourceIdentity ||
7602
+ this.isInspectOnly(this.resolvedMetadata)) {
7603
+ this.tableCollectionLinks = null;
7604
+ this.collectionCapabilities = null;
7605
+ this.collectionCapabilitiesRequestHref = null;
7606
+ this.collectionCapabilitiesResolvedHref = null;
7607
+ this.collectionCapabilitiesRequestSeq += 1;
7608
+ }
7383
7609
  this.applyResolvedCrudState(this.resolvedMetadata);
7384
7610
  }
7385
7611
  catch (err) {
@@ -8225,12 +8451,29 @@ class PraxisCrudComponent {
8225
8451
  let payload;
8226
8452
  try {
8227
8453
  const resourceId = this.resolveRowResourceId(row);
8454
+ const preconditions = discoveredAction.execution?.preconditions;
8455
+ const needsItemResourceVersion = !!row &&
8456
+ preconditions?.resourceVersion != null &&
8457
+ preconditions.resourceVersion !== 'NONE' &&
8458
+ preconditions.resourceVersionTransport === 'IF_MATCH' &&
8459
+ !String(preconditions.resourceVersionTargetResourceKey ?? '').trim() &&
8460
+ !String(preconditions.resourceVersionTargetIdField ?? '').trim();
8461
+ let resourceVersion;
8462
+ if (row) {
8463
+ const resolvedTargets = await this.resolveBulkTargetsFromRows([row], needsItemResourceVersion, needsItemResourceVersion ? resourcePath : undefined);
8464
+ resourceVersion = resolvedTargets[0]?.expectedVersion;
8465
+ }
8228
8466
  payload = this.getActionOpenAdapter().toPayload(discoveredAction, {
8229
8467
  resourcePath,
8230
8468
  resourceId,
8231
8469
  endpointKey: this.resolvedMetadata?.resource?.endpointKey,
8232
8470
  apiUrlEntry: this.resolveDiscoveryApiEntry(),
8233
8471
  group: catalog?.group ?? null,
8472
+ ...(preconditions?.correlationId &&
8473
+ preconditions.correlationId !== 'NONE'
8474
+ ? { correlationId }
8475
+ : {}),
8476
+ ...(resourceVersion != null ? { resourceVersion } : {}),
8234
8477
  });
8235
8478
  const resourceIdentity = runtimeEvent?.resourceIdentity ?? null;
8236
8479
  if (resourceId != null && resourceIdentity) {
@@ -9319,6 +9562,7 @@ class PraxisCrudComponent {
9319
9562
  <praxis-kpi-band
9320
9563
  [config]="resolvedKpiBandConfig"
9321
9564
  [dataContext]="kpiDataContext"
9565
+ [activeFilter]="tableFilterCriteria"
9322
9566
  [isInteractiveHost]="isKpiInteractiveHost"
9323
9567
  (cardClick)="onKpiCardClick($event)"
9324
9568
  />
@@ -9361,7 +9605,7 @@ class PraxisCrudComponent {
9361
9605
  }
9362
9606
  }
9363
9607
  </div>
9364
- `, isInline: true, styles: [":host{display:block;width:100%;min-width:0;max-width:100%}.praxis-crud-runtime{position:relative;min-width:0}.praxis-crud-opening-status{position:absolute;z-index:6;top:.7rem;right:.7rem;display:inline-flex;align-items:center;gap:.55rem;min-height:2.25rem;max-width:min(24rem,calc(100% - 1.4rem));padding:.48rem .72rem;border:1px solid color-mix(in srgb,var(--md-sys-color-primary, #3f51b5) 32%,transparent);border-radius:999px;color:var(--md-sys-color-on-surface, currentColor);background:color-mix(in srgb,var(--md-sys-color-surface-container-high, #fff) 94%,transparent);box-shadow:0 10px 28px #0f172a29;font-size:.82rem;font-weight:650;line-height:1.25;pointer-events:none}.praxis-crud-opening-status__spinner{width:.95rem;height:.95rem;flex:0 0 auto;border:2px solid color-mix(in srgb,var(--md-sys-color-primary, #3f51b5) 26%,transparent);border-top-color:var(--md-sys-color-primary, #3f51b5);border-radius:50%;animation:praxis-crud-opening-spin .72s linear infinite}@keyframes praxis-crud-opening-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.praxis-crud-opening-status__spinner{animation:none;border-top-color:currentColor}}\n"], dependencies: [{ kind: "component", type: PraxisTable, selector: "praxis-table", inputs: ["config", "resourcePath", "data", "tableId", "componentInstanceId", "configPersistenceStrategy", "title", "subtitle", "icon", "autoDelete", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "crudContext", "filterCriteria", "filterTeleport", "widgetId", "queryContext", "aiContext", "aiAssistantVoiceInputMode", "aiAssistantVoiceLanguage", "horizontalScroll", "enableCustomization", "authoringCapability", "dense"], outputs: ["queryContextClear", "queryContextChange", "rowClick", "widgetEvent", "resourceEvent", "rowDoubleClick", "rowExpansionChange", "rowAction", "toolbarAction", "bulkAction", "exportAction", "columnReorder", "columnReorderAttempt", "columnResize", "beforeDelete", "afterDelete", "deleteError", "beforeBulkDelete", "afterBulkDelete", "bulkDeleteError", "schemaStatusChange", "configChange", "metadataChange", "loadingStateChange", "collectionLinksChange", "selectionChange", "drawerOpen", "drawerClose", "drawerTabChange"] }, { kind: "component", type: EmptyStateCardComponent, selector: "praxis-empty-state-card", inputs: ["icon", "title", "description", "primaryAction", "secondaryActions", "inline", "tone", "variant", "alignment", "density", "iconContainer"] }, { kind: "component", type: PraxisKpiBandComponent, selector: "praxis-kpi-band", inputs: ["config", "dataContext", "isInteractiveHost"], outputs: ["cardClick", "statsLoaded", "statsError", "refreshClick"] }] });
9608
+ `, isInline: true, styles: [":host{display:block;width:100%;min-width:0;max-width:100%}.praxis-crud-runtime{position:relative;min-width:0}.praxis-crud-opening-status{position:absolute;z-index:6;top:.7rem;right:.7rem;display:inline-flex;align-items:center;gap:.55rem;min-height:2.25rem;max-width:min(24rem,calc(100% - 1.4rem));padding:.48rem .72rem;border:1px solid color-mix(in srgb,var(--md-sys-color-primary, #3f51b5) 32%,transparent);border-radius:999px;color:var(--md-sys-color-on-surface, currentColor);background:color-mix(in srgb,var(--md-sys-color-surface-container-high, #fff) 94%,transparent);box-shadow:0 10px 28px #0f172a29;font-size:.82rem;font-weight:650;line-height:1.25;pointer-events:none}.praxis-crud-opening-status__spinner{width:.95rem;height:.95rem;flex:0 0 auto;border:2px solid color-mix(in srgb,var(--md-sys-color-primary, #3f51b5) 26%,transparent);border-top-color:var(--md-sys-color-primary, #3f51b5);border-radius:50%;animation:praxis-crud-opening-spin .72s linear infinite}@keyframes praxis-crud-opening-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.praxis-crud-opening-status__spinner{animation:none;border-top-color:currentColor}}\n"], dependencies: [{ kind: "component", type: PraxisTable, selector: "praxis-table", inputs: ["config", "resourcePath", "data", "tableId", "componentInstanceId", "configPersistenceStrategy", "title", "subtitle", "icon", "autoDelete", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "crudContext", "filterCriteria", "filterTeleport", "widgetId", "queryContext", "aiContext", "aiAssistantVoiceInputMode", "aiAssistantVoiceLanguage", "horizontalScroll", "enableCustomization", "authoringCapability", "dense"], outputs: ["queryContextClear", "queryContextChange", "rowClick", "widgetEvent", "resourceEvent", "rowDoubleClick", "rowExpansionChange", "rowAction", "toolbarAction", "bulkAction", "exportAction", "columnReorder", "columnReorderAttempt", "columnResize", "beforeDelete", "afterDelete", "deleteError", "beforeBulkDelete", "afterBulkDelete", "bulkDeleteError", "schemaStatusChange", "configChange", "metadataChange", "loadingStateChange", "collectionLinksChange", "selectionChange", "drawerOpen", "drawerClose", "drawerTabChange"] }, { kind: "component", type: EmptyStateCardComponent, selector: "praxis-empty-state-card", inputs: ["icon", "title", "description", "primaryAction", "secondaryActions", "inline", "tone", "variant", "alignment", "density", "iconContainer"] }, { kind: "component", type: PraxisKpiBandComponent, selector: "praxis-kpi-band", inputs: ["config", "dataContext", "isInteractiveHost", "activeFilter"], outputs: ["cardClick", "statsLoaded", "statsError", "refreshClick"] }] });
9365
9609
  }
9366
9610
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisCrudComponent, decorators: [{
9367
9611
  type: Component,
@@ -9380,6 +9624,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
9380
9624
  <praxis-kpi-band
9381
9625
  [config]="resolvedKpiBandConfig"
9382
9626
  [dataContext]="kpiDataContext"
9627
+ [activeFilter]="tableFilterCriteria"
9383
9628
  [isInteractiveHost]="isKpiInteractiveHost"
9384
9629
  (cardClick)="onKpiCardClick($event)"
9385
9630
  />
@@ -9540,8 +9785,14 @@ class DynamicFormDialogHostComponent {
9540
9785
  contextIdentity = null;
9541
9786
  showResourceIdentityKey = true;
9542
9787
  schemaUrl;
9788
+ readUrl;
9543
9789
  submitUrl;
9544
9790
  submitMethod;
9791
+ submitIdempotencyKey;
9792
+ submitCorrelationId;
9793
+ submitResourceVersion;
9794
+ submitResourceVersionTargetResourceKey;
9795
+ submitResourceVersionTargetIdField;
9545
9796
  apiEndpointKey;
9546
9797
  apiUrlEntry;
9547
9798
  layoutPolicy;
@@ -9615,8 +9866,19 @@ class DynamicFormDialogHostComponent {
9615
9866
  this.resourceId = this.data.inputs?.[this.idField];
9616
9867
  this.initialValue = this.extractInitialValue(this.data.inputs);
9617
9868
  this.schemaUrl = this.data.inputs?.['schemaUrl'] ?? null;
9869
+ this.readUrl = this.data.inputs?.['readUrl'] ?? null;
9618
9870
  this.submitUrl = this.data.inputs?.['submitUrl'] ?? null;
9619
9871
  this.submitMethod = this.data.inputs?.['submitMethod'] ?? null;
9872
+ this.submitIdempotencyKey =
9873
+ this.data.inputs?.['submitIdempotencyKey'] ?? null;
9874
+ this.submitCorrelationId =
9875
+ this.data.inputs?.['submitCorrelationId'] ?? null;
9876
+ this.submitResourceVersion =
9877
+ this.data.inputs?.['submitResourceVersion'] ?? null;
9878
+ this.submitResourceVersionTargetResourceKey =
9879
+ this.data.inputs?.['submitResourceVersionTargetResourceKey'] ?? null;
9880
+ this.submitResourceVersionTargetIdField =
9881
+ this.data.inputs?.['submitResourceVersionTargetIdField'] ?? null;
9620
9882
  this.apiEndpointKey =
9621
9883
  this.data.inputs?.['apiEndpointKey'] ??
9622
9884
  this.data.metadata?.resource?.endpointKey ??
@@ -9699,8 +9961,14 @@ class DynamicFormDialogHostComponent {
9699
9961
  const reserved = new Set([
9700
9962
  this.idField,
9701
9963
  'schemaUrl',
9964
+ 'readUrl',
9702
9965
  'submitUrl',
9703
9966
  'submitMethod',
9967
+ 'submitIdempotencyKey',
9968
+ 'submitCorrelationId',
9969
+ 'submitResourceVersion',
9970
+ 'submitResourceVersionTargetResourceKey',
9971
+ 'submitResourceVersionTargetIdField',
9704
9972
  'apiEndpointKey',
9705
9973
  'apiUrlEntry',
9706
9974
  'initialValue',
@@ -10189,8 +10457,14 @@ class DynamicFormDialogHostComponent {
10189
10457
  [mode]="mode"
10190
10458
  [config]="formConfig"
10191
10459
  [schemaUrl]="schemaUrl"
10460
+ [readUrl]="readUrl"
10192
10461
  [submitUrl]="submitUrl"
10193
10462
  [submitMethod]="submitMethod"
10463
+ [submitIdempotencyKey]="submitIdempotencyKey"
10464
+ [submitCorrelationId]="submitCorrelationId"
10465
+ [submitResourceVersion]="submitResourceVersion"
10466
+ [submitResourceVersionTargetResourceKey]="submitResourceVersionTargetResourceKey"
10467
+ [submitResourceVersionTargetIdField]="submitResourceVersionTargetIdField"
10194
10468
  [apiEndpointKey]="apiEndpointKey"
10195
10469
  [apiUrlEntry]="apiUrlEntry"
10196
10470
  [configPersistenceStrategy]="formConfigPersistenceStrategy"
@@ -10313,8 +10587,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
10313
10587
  [mode]="mode"
10314
10588
  [config]="formConfig"
10315
10589
  [schemaUrl]="schemaUrl"
10590
+ [readUrl]="readUrl"
10316
10591
  [submitUrl]="submitUrl"
10317
10592
  [submitMethod]="submitMethod"
10593
+ [submitIdempotencyKey]="submitIdempotencyKey"
10594
+ [submitCorrelationId]="submitCorrelationId"
10595
+ [submitResourceVersion]="submitResourceVersion"
10596
+ [submitResourceVersionTargetResourceKey]="submitResourceVersionTargetResourceKey"
10597
+ [submitResourceVersionTargetIdField]="submitResourceVersionTargetIdField"
10318
10598
  [apiEndpointKey]="apiEndpointKey"
10319
10599
  [apiUrlEntry]="apiUrlEntry"
10320
10600
  [configPersistenceStrategy]="formConfigPersistenceStrategy"
@@ -11820,7 +12100,10 @@ class PraxisResourcePage {
11820
12100
  activeScopeFilter = signal({}, ...(ngDevMode ? [{ debugName: "activeScopeFilter" }] : /* istanbul ignore next */ []));
11821
12101
  effectiveResource = computed(() => {
11822
12102
  this.inputsVersion();
11823
- return this.resource || this.resourcePath || this.routeResourceSignal();
12103
+ return (this.resource ||
12104
+ this.resourcePath ||
12105
+ this.routeResourceSignal() ||
12106
+ this.extractResourcePathFromMetadata(this.metadata || this.routeMetadataSignal()));
11824
12107
  }, ...(ngDevMode ? [{ debugName: "effectiveResource" }] : /* istanbul ignore next */ []));
11825
12108
  effectiveTitle = computed(() => {
11826
12109
  this.inputsVersion();
@@ -12031,9 +12314,8 @@ class PraxisResourcePage {
12031
12314
  }
12032
12315
  if (this.route.data) {
12033
12316
  this.routeSub.add(this.route.data.subscribe((d) => {
12034
- const nextRes = d['resource'] || d['resourcePath'];
12035
- if (nextRes) {
12036
- this.routeResourceSignal.set(nextRes);
12317
+ if ('resource' in d || 'resourcePath' in d) {
12318
+ this.routeResourceSignal.set(d['resource'] || d['resourcePath'] || null);
12037
12319
  }
12038
12320
  this.extractRouteData(d);
12039
12321
  }));
@@ -12076,10 +12358,17 @@ class PraxisResourcePage {
12076
12358
  const cardFilter = event.card.filter;
12077
12359
  if (cardFilter !== undefined && cardFilter !== null) {
12078
12360
  const sb = this.effectiveScopeBar();
12079
- if (sb && sb.items) {
12080
- const matched = sb.items.find((item) => this.areScopeFiltersEqual(item.filter, cardFilter));
12081
- if (matched) {
12082
- this.onScopeChange(matched);
12361
+ const matched = sb?.items?.find((item) => this.areScopeFiltersEqual(item.filter, cardFilter));
12362
+ if (matched) {
12363
+ this.onScopeChange(matched);
12364
+ }
12365
+ else {
12366
+ const normalized = this.normalizeScopeFilterExpression(cardFilter);
12367
+ if (Object.keys(normalized).length === 0) {
12368
+ this.onScopeClear();
12369
+ }
12370
+ else {
12371
+ this.activeScopeFilter.set(normalized);
12083
12372
  }
12084
12373
  }
12085
12374
  }
@@ -12088,6 +12377,30 @@ class PraxisResourcePage {
12088
12377
  applyItemFilter(item) {
12089
12378
  this.activeScopeFilter.set(this.normalizeScopeFilterExpression(item.filter));
12090
12379
  }
12380
+ extractResourcePathFromMetadata(metaSource) {
12381
+ if (!metaSource)
12382
+ return null;
12383
+ if (typeof metaSource === 'string') {
12384
+ const trimmed = metaSource.trim();
12385
+ if (!trimmed)
12386
+ return null;
12387
+ if (trimmed.startsWith('{')) {
12388
+ try {
12389
+ const parsed = JSON.parse(trimmed);
12390
+ return (parsed?.resource?.path ||
12391
+ parsed?.table?.resourcePath ||
12392
+ null);
12393
+ }
12394
+ catch {
12395
+ return null;
12396
+ }
12397
+ }
12398
+ return trimmed;
12399
+ }
12400
+ return (metaSource.resource?.path ||
12401
+ metaSource.table?.resourcePath ||
12402
+ null);
12403
+ }
12091
12404
  normalizeScopeFilterExpression(filter) {
12092
12405
  if (filter && typeof filter === 'object' && !Array.isArray(filter)) {
12093
12406
  return { ...filter };
@@ -12142,13 +12455,19 @@ class PraxisResourcePage {
12142
12455
  return {};
12143
12456
  }
12144
12457
  areScopeFiltersEqual(a, b) {
12458
+ if (a === undefined || a === null || b === undefined || b === null) {
12459
+ return false;
12460
+ }
12145
12461
  const normA = this.normalizeScopeFilterExpression(a);
12146
12462
  const normB = this.normalizeScopeFilterExpression(b);
12147
12463
  const keysA = Object.keys(normA).sort();
12148
12464
  const keysB = Object.keys(normB).sort();
12149
- if (keysA.length === 0 || keysA.length !== keysB.length) {
12465
+ if (keysA.length !== keysB.length) {
12150
12466
  return false;
12151
12467
  }
12468
+ if (keysA.length === 0) {
12469
+ return true;
12470
+ }
12152
12471
  return keysA.every((key, idx) => key === keysB[idx] &&
12153
12472
  JSON.stringify(normA[key]) === JSON.stringify(normB[key]));
12154
12473
  }
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@praxisui/crud",
3
- "version": "10.0.0-rc.15",
3
+ "version": "10.0.0-rc.16",
4
4
  "description": "CRUD building blocks for Praxis UI: integrates dynamic forms and tables with unified configuration and services.",
5
5
  "peerDependencies": {
6
6
  "@angular/common": "^21.0.0",
7
7
  "@angular/core": "^21.0.0",
8
- "@praxisui/dynamic-form": "^10.0.0-rc.15",
9
- "@praxisui/table": "^10.0.0-rc.15",
10
- "@praxisui/core": "^10.0.0-rc.15",
11
- "@praxisui/dynamic-fields": "^10.0.0-rc.15",
12
- "@praxisui/settings-panel": "^10.0.0-rc.15",
8
+ "@praxisui/dynamic-form": "^10.0.0-rc.16",
9
+ "@praxisui/table": "^10.0.0-rc.16",
10
+ "@praxisui/core": "^10.0.0-rc.16",
11
+ "@praxisui/dynamic-fields": "^10.0.0-rc.16",
12
+ "@praxisui/settings-panel": "^10.0.0-rc.16",
13
13
  "@angular/cdk": "^21.0.0",
14
14
  "@angular/forms": "^21.0.0",
15
15
  "@angular/material": "^21.0.0",
16
16
  "@angular/router": "^21.0.0",
17
- "@praxisui/ai": "^10.0.0-rc.15",
17
+ "@praxisui/ai": "^10.0.0-rc.16",
18
18
  "rxjs": "~7.8.0"
19
19
  },
20
20
  "dependencies": {
@@ -354,6 +354,7 @@ declare class PraxisCrudComponent implements OnChanges {
354
354
  onTableDrawerOpen(event: DrawerOpenEvent<DynamicDataRecord>): void;
355
355
  onTableDrawerClose(): void;
356
356
  onBulkAction(event: any): Promise<void>;
357
+ private handleBulkDialogResult;
357
358
  private handleBulkExecutionCompleted;
358
359
  private resolveBulkTargetsFromRows;
359
360
  private resolveSyncBulkExecutionContractForAction;
@@ -622,8 +623,14 @@ declare class DynamicFormDialogHostComponent implements OnInit, AfterViewInit {
622
623
  contextIdentity: MaterializedResourceIdentity | null;
623
624
  showResourceIdentityKey: boolean;
624
625
  schemaUrl?: string | null;
626
+ readUrl?: string | null;
625
627
  submitUrl?: string | null;
626
628
  submitMethod?: string | null;
629
+ submitIdempotencyKey?: string | null;
630
+ submitCorrelationId?: string | null;
631
+ submitResourceVersion?: string | number | null;
632
+ submitResourceVersionTargetResourceKey?: string | null;
633
+ submitResourceVersionTargetIdField?: string | null;
627
634
  apiEndpointKey?: ApiEndpoint | string | null;
628
635
  apiUrlEntry?: ApiUrlEntry | null;
629
636
  layoutPolicy?: DynamicFormLayoutPolicy | null;
@@ -961,6 +968,7 @@ interface EvaluatedKpiCard {
961
968
  icon?: string;
962
969
  tone?: string;
963
970
  interactive: boolean;
971
+ active?: boolean;
964
972
  className?: string;
965
973
  tooltip?: string;
966
974
  }
@@ -973,6 +981,11 @@ declare class PraxisKpiBandComponent implements OnInit, OnChanges {
973
981
  * para cliques em cards. Quando explicitamente falso, evita a falsa affordance de botões.
974
982
  */
975
983
  isInteractiveHost?: boolean;
984
+ /**
985
+ * Filtro ativo sincronizado pelo host (ex.: PraxisCrudComponent / PraxisResourcePage)
986
+ * para destacar visualmente o card de KPI correspondente.
987
+ */
988
+ activeFilter?: Record<string, unknown> | null;
976
989
  cardClick: EventEmitter<{
977
990
  card: PraxisKpiBandCard;
978
991
  index: number;
@@ -1002,9 +1015,10 @@ declare class PraxisKpiBandComponent implements OnInit, OnChanges {
1002
1015
  private resolveSourceUrl;
1003
1016
  private fetchFromSource;
1004
1017
  recomputeCards(): void;
1018
+ private isCardFilterActive;
1005
1019
  onCardClick(card: EvaluatedKpiCard, index: number): void;
1006
1020
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<PraxisKpiBandComponent, never>;
1007
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<PraxisKpiBandComponent, "praxis-kpi-band", never, { "config": { "alias": "config"; "required": false; }; "dataContext": { "alias": "dataContext"; "required": false; }; "isInteractiveHost": { "alias": "isInteractiveHost"; "required": false; }; }, { "cardClick": "cardClick"; "statsLoaded": "statsLoaded"; "statsError": "statsError"; "refreshClick": "refreshClick"; }, never, never, true, never>;
1021
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<PraxisKpiBandComponent, "praxis-kpi-band", never, { "config": { "alias": "config"; "required": false; }; "dataContext": { "alias": "dataContext"; "required": false; }; "isInteractiveHost": { "alias": "isInteractiveHost"; "required": false; }; "activeFilter": { "alias": "activeFilter"; "required": false; }; }, { "cardClick": "cardClick"; "statsLoaded": "statsLoaded"; "statsError": "statsError"; "refreshClick": "refreshClick"; }, never, never, true, never>;
1008
1022
  }
1009
1023
 
1010
1024
  /**
@@ -1135,6 +1149,7 @@ declare class PraxisResourcePage implements OnInit, OnChanges, OnDestroy {
1135
1149
  data: any;
1136
1150
  }): void;
1137
1151
  private applyItemFilter;
1152
+ private extractResourcePathFromMetadata;
1138
1153
  private normalizeScopeFilterExpression;
1139
1154
  private areScopeFiltersEqual;
1140
1155
  private extractRouteData;
@@ -1159,11 +1174,13 @@ declare class PraxisBulkOperationDialogComponent implements OnInit {
1159
1174
  private readonly dialogRef;
1160
1175
  private readonly bulkClient;
1161
1176
  private readonly destroyRef;
1177
+ parametersForm?: PraxisDynamicForm;
1162
1178
  readonly step: _angular_core.WritableSignal<BulkDialogStep>;
1163
1179
  readonly proposal: _angular_core.WritableSignal<_praxisui_core.BulkProposal | null>;
1164
1180
  readonly execution: _angular_core.WritableSignal<_praxisui_core.BulkExecution | null>;
1165
1181
  readonly errorMessage: _angular_core.WritableSignal<string | null>;
1166
1182
  readonly parametersValue: _angular_core.WritableSignal<Record<string, unknown>>;
1183
+ readonly parametersFormValid: _angular_core.WritableSignal<boolean>;
1167
1184
  readonly proposalItems: _angular_core.WritableSignal<BulkProposalItemResult<_praxisui_core.BulkWireId>[]>;
1168
1185
  readonly proposalNextCursor: _angular_core.WritableSignal<string | null>;
1169
1186
  readonly loadingProposalResults: _angular_core.WritableSignal<boolean>;
@@ -1181,7 +1198,10 @@ declare class PraxisBulkOperationDialogComponent implements OnInit {
1181
1198
  hasParametersSchema(): boolean;
1182
1199
  parametersSchemaUrl(): string | null;
1183
1200
  getParametersFormId(): string;
1201
+ onParametersFormReady(event: any): void;
1184
1202
  onParametersChange(event: any): void;
1203
+ private getMissingRequiredParameterNames;
1204
+ canEvaluateParameters(): boolean;
1185
1205
  isBlockedByAtomicity(): boolean;
1186
1206
  isConfirmDisabled(): boolean;
1187
1207
  isDiagnosticError(diag: any): boolean;
@@ -1218,6 +1238,7 @@ interface PraxisBulkEditDialogData {
1218
1238
  }
1219
1239
  type BulkEditStep = 'FIELDS' | 'EVALUATING' | 'PREVIEW' | 'CONFIRMING' | 'RESULT';
1220
1240
  interface EditableFieldRow {
1241
+ id: string;
1221
1242
  field: string;
1222
1243
  operator: 'SET' | 'CLEAR';
1223
1244
  value: string;
@@ -1253,9 +1274,11 @@ declare class PraxisBulkEditDialogComponent {
1253
1274
  private getFieldDescriptor;
1254
1275
  isFieldClearable(fieldName: string): boolean;
1255
1276
  hasAvailableFieldsToAdd(): boolean;
1277
+ private nextRowId;
1256
1278
  isFieldSelectedInOtherRow(field: string, currentRow: EditableFieldRow): boolean;
1257
1279
  addFieldRow(): void;
1258
1280
  removeFieldRow(index: number): void;
1281
+ getCurrencyFieldLabel(fieldName: string): string;
1259
1282
  getFieldLabel(fieldName: string): string;
1260
1283
  getFieldType(fieldName: string): 'currency' | 'number' | 'date' | 'boolean' | 'enum' | 'string';
1261
1284
  getFieldEnumOptions(fieldName: string): Array<{