@praxisui/crud 10.0.0-rc.24 → 10.0.0-rc.26

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
@@ -370,6 +370,21 @@ and materializes them in the contextual bulk selection toolbar:
370
370
  `persistOnDataUpdate: true` table refreshes, and re-fetches capabilities
371
371
  when `refresh.capabilities === true`.
372
372
 
373
+ `CANCEL_REQUESTED` stays pending: requesting cancellation does not imply that the
374
+ server finished. The dialogs keep one execution-tracking subscription, prevent
375
+ duplicate cancellation and accept final results only from canonical terminal
376
+ snapshots. Once terminal, late polling or cancellation callbacks cannot downgrade
377
+ the result. Terminal status is not proof of full success; partial totals and
378
+ reconciliation diagnostics remain visible.
379
+
380
+ If tracking fails or expires, the dialogs show an unknown outcome rather than a
381
+ completed result: server effects may already exist. The explicit resume action
382
+ only reads the same execution with the original proposal/key; it never repeats
383
+ confirmation or evaluation. The person can close this state with `success:false`
384
+ and the last snapshot, without claiming that no effects committed. Tracking ends
385
+ when the dialog closes or is destroyed. Retaining intention/key custody across
386
+ reopening or reload remains a separate requirement, not a guarantee of this flow.
387
+
373
388
  Unavailable bulk operations (`availability.allowed === false`) remain visible
374
389
  but disabled in the bulk toolbar with their canonical translated reason tooltip.
375
390
 
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-10-15T13:55:17.858Z",
3
+ "generatedAt": "2026-10-15T19:07:41.629Z",
4
4
  "packageName": "@praxisui/crud",
5
- "packageVersion": "10.0.0-rc.24",
5
+ "packageVersion": "10.0.0-rc.26",
6
6
  "sourceRegistry": "praxis-component-registry-ingestion",
7
7
  "sourceRegistryVersion": "1.0.0",
8
8
  "componentCount": 2,
@@ -4,10 +4,10 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
4
  import { HttpClient } from '@angular/common/http';
5
5
  import { Router, ActivatedRoute, RouterLink } from '@angular/router';
6
6
  import { MatSnackBar } from '@angular/material/snack-bar';
7
- import { firstValueFrom, BehaviorSubject, Subscription } from 'rxjs';
7
+ import { Subscription, firstValueFrom, BehaviorSubject } 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, GenericCrudService, SETTINGS_PANEL_DATA, PraxisI18nService, buildBaseColumnFromDef, PraxisIconDirective, providePraxisI18nConfig, SchemaNormalizerService, 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, ComponentMetadataRegistry, PraxisCustomizationModeService } from '@praxisui/core';
10
+ import { PraxisBulkClientService, PraxisI18nService, generateBulkIdempotencyKey, isBulkExecutionTerminal, API_URL, SafeExpressionEvaluator, buildApiUrl, ASYNC_CONFIG_STORAGE, GlobalConfigService, CrudOperationResolutionService, LoggerService, SurfaceNavigationError, fillUndefined, GenericCrudService, SETTINGS_PANEL_DATA, buildBaseColumnFromDef, PraxisIconDirective, providePraxisI18nConfig, SchemaNormalizerService, 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, ComponentMetadataRegistry, PraxisCustomizationModeService } 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';
@@ -42,11 +42,121 @@ import { MatSlideToggleModule } from '@angular/material/slide-toggle';
42
42
  import * as i9 from '@angular/material/tabs';
43
43
  import { MatTabsModule } from '@angular/material/tabs';
44
44
 
45
+ const PRAXIS_CRUD_RUNTIME_I18N_NAMESPACE = 'praxisCrudRuntime';
46
+ const PRAXIS_CRUD_RUNTIME_I18N_CONFIG = {
47
+ namespaces: {
48
+ [PRAXIS_CRUD_RUNTIME_I18N_NAMESPACE]: {
49
+ 'pt-BR': {
50
+ 'crud.bulk.tracking': 'Acompanhando execução em lote...',
51
+ 'crud.bulk.trackingUnavailable': 'Não foi possível acompanhar a execução.',
52
+ 'crud.bulk.effectsMayExist': 'A execução pode continuar e já ter produzido efeitos no servidor. Retome a consulta desta execução antes de iniciar outra intenção.',
53
+ 'crud.bulk.resumeTracking': 'Retomar acompanhamento',
54
+ 'crud.bulk.closeUnknown': 'Fechar sem confirmar conclusão',
55
+ 'crud.bulk.cancelRequest': 'Solicitar cancelamento',
56
+ 'crud.bulk.requestingCancel': 'Solicitando cancelamento...',
57
+ 'crud.bulk.cancelRequested': 'Cancelamento solicitado. Aguardando confirmação do servidor.',
58
+ 'crud.emptyState.title': 'Conecte o CRUD a um recurso',
59
+ 'crud.emptyState.description': 'Informe os metadados (resourcePath / schema) ou forneça metadata.data para habilitar a tabela e as ações.',
60
+ 'crud.emptyState.primaryAction': 'Configurar metadados',
61
+ 'crud.table.emptyState.initial.title': 'Sem registros em {label}',
62
+ 'crud.table.emptyState.initial.titleFallback': 'Nenhum registro disponível.',
63
+ 'crud.table.emptyState.initial.descriptionWithToolbarAction': 'Use "{action}" na barra da tabela para adicionar o primeiro registro quando houver informações para cadastrar.',
64
+ 'crud.table.emptyState.filtered.title': 'Nenhum resultado encontrado.',
65
+ 'crud.table.emptyState.filtered.description': 'Revise os filtros ou ajuste o termo de busca.',
66
+ 'crud.preferences.resetSuccess': 'Overrides de CRUD redefinidos',
67
+ 'crud.actions.create': 'Adicionar',
68
+ 'crud.actions.view': 'Ver',
69
+ 'crud.actions.edit': 'Editar',
70
+ 'crud.actions.delete': 'Excluir',
71
+ 'crud.actions.default': 'ação',
72
+ 'crud.actions.opening': 'Abrindo {action}…',
73
+ 'crud.surface.openFailed': 'Não foi possível abrir {title}. Tente novamente ou confirme se esse recurso continua disponível.',
74
+ 'crud.dialog.recordContextLabel': 'Registro em edição',
75
+ 'crud.dialog.viewRecordContextLabel': 'Registro em consulta',
76
+ 'crud.dialog.relatedContextLabel': 'Vinculado a',
77
+ 'crud.delete.confirmMessage': 'Esta ação não pode ser desfeita. Deseja continuar?',
78
+ 'crud.delete.cancel': 'Cancelar',
79
+ },
80
+ 'en-US': {
81
+ 'crud.bulk.tracking': 'Tracking bulk execution...',
82
+ 'crud.bulk.trackingUnavailable': 'Execution tracking is unavailable.',
83
+ 'crud.bulk.effectsMayExist': 'The execution may continue and may already have produced server effects. Resume this execution before starting another intent.',
84
+ 'crud.bulk.resumeTracking': 'Resume tracking',
85
+ 'crud.bulk.closeUnknown': 'Close without confirming completion',
86
+ 'crud.bulk.cancelRequest': 'Request cancellation',
87
+ 'crud.bulk.requestingCancel': 'Requesting cancellation...',
88
+ 'crud.bulk.cancelRequested': 'Cancellation requested. Waiting for server confirmation.',
89
+ 'crud.emptyState.title': 'Connect CRUD to a resource',
90
+ 'crud.emptyState.description': 'Provide metadata (resourcePath / schema) or metadata.data to enable the table and actions.',
91
+ 'crud.emptyState.primaryAction': 'Configure metadata',
92
+ 'crud.table.emptyState.initial.title': 'No records in {label}',
93
+ 'crud.table.emptyState.initial.titleFallback': 'No records available.',
94
+ 'crud.table.emptyState.initial.descriptionWithToolbarAction': 'Use "{action}" in the table toolbar to add the first record when there is information to register.',
95
+ 'crud.table.emptyState.filtered.title': 'No results found.',
96
+ 'crud.table.emptyState.filtered.description': 'Review the filters or adjust the search term.',
97
+ 'crud.preferences.resetSuccess': 'CRUD overrides reset',
98
+ 'crud.actions.create': 'Add',
99
+ 'crud.actions.view': 'View',
100
+ 'crud.actions.edit': 'Edit',
101
+ 'crud.actions.delete': 'Delete',
102
+ 'crud.actions.default': 'action',
103
+ 'crud.actions.opening': 'Opening {action}…',
104
+ 'crud.surface.openFailed': 'Could not open {title}. Try again or confirm that this resource is still available.',
105
+ 'crud.dialog.recordContextLabel': 'Record being edited',
106
+ 'crud.dialog.viewRecordContextLabel': 'Record being viewed',
107
+ 'crud.dialog.relatedContextLabel': 'Related to',
108
+ 'crud.delete.confirmMessage': 'This action cannot be undone. Do you want to continue?',
109
+ 'crud.delete.cancel': 'Cancel',
110
+ },
111
+ },
112
+ },
113
+ };
114
+ function getCrudRuntimeDictionary(locale) {
115
+ const dictionaries = PRAXIS_CRUD_RUNTIME_I18N_CONFIG.namespaces?.[PRAXIS_CRUD_RUNTIME_I18N_NAMESPACE];
116
+ if (!dictionaries) {
117
+ return undefined;
118
+ }
119
+ const normalized = String(locale || '').trim();
120
+ const candidates = [
121
+ normalized,
122
+ normalized.toLowerCase(),
123
+ normalized.includes('-') ? normalized.split('-')[0] : '',
124
+ normalized.includes('_') ? normalized.split('_')[0] : '',
125
+ 'pt-BR',
126
+ 'pt-br',
127
+ 'en-US',
128
+ 'en-us',
129
+ ].filter(Boolean);
130
+ for (const candidate of candidates) {
131
+ const dictionary = dictionaries[candidate];
132
+ if (dictionary && typeof dictionary === 'object') {
133
+ return dictionary;
134
+ }
135
+ }
136
+ return undefined;
137
+ }
138
+ function resolveCrudRuntimeFallback(key, locale) {
139
+ return getCrudRuntimeDictionary(locale || 'pt-BR')?.[key];
140
+ }
141
+ function translateCrudRuntimeText(i18n, key, fallback, params, locale) {
142
+ const effectiveLocale = locale || i18n.getLocale?.() || i18n.getFallbackLocale?.() || 'pt-BR';
143
+ const runtimeFallback = resolveCrudRuntimeFallback(key, effectiveLocale)
144
+ || resolveCrudRuntimeFallback(key, i18n.getFallbackLocale?.())
145
+ || fallback
146
+ || key;
147
+ return i18n.t(key, params, runtimeFallback, PRAXIS_CRUD_RUNTIME_I18N_NAMESPACE);
148
+ }
149
+
45
150
  class PraxisBulkOperationDialogComponent {
46
151
  data;
47
152
  dialogRef = inject((MatDialogRef));
48
153
  bulkClient = inject(PraxisBulkClientService);
49
154
  destroyRef = inject(DestroyRef);
155
+ i18n = inject(PraxisI18nService);
156
+ trackingUnavailable = signal(false, ...(ngDevMode ? [{ debugName: "trackingUnavailable" }] : /* istanbul ignore next */ []));
157
+ trackingSubscription = null;
158
+ executionGeneration = 0;
159
+ trackingClosed = false;
50
160
  parametersForm;
51
161
  step = signal('PARAMETERS', ...(ngDevMode ? [{ debugName: "step" }] : /* istanbul ignore next */ []));
52
162
  proposal = signal(null, ...(ngDevMode ? [{ debugName: "proposal" }] : /* istanbul ignore next */ []));
@@ -74,6 +184,10 @@ class PraxisBulkOperationDialogComponent {
74
184
  };
75
185
  constructor(data) {
76
186
  this.data = data;
187
+ this.destroyRef.onDestroy(() => {
188
+ this.executionGeneration++;
189
+ this.stopExecutionTracking();
190
+ });
77
191
  this.action = data.action;
78
192
  this.targets = data.targets || [];
79
193
  }
@@ -383,51 +497,103 @@ class PraxisBulkOperationDialogComponent {
383
497
  onConfirm() {
384
498
  const prop = this.proposal();
385
499
  const bulkContract = this.action.execution?.bulk;
386
- if (!prop || !bulkContract)
500
+ if (!prop || !bulkContract || this.trackingClosed || this.step() === 'CONFIRMING')
387
501
  return;
502
+ const generation = ++this.executionGeneration;
503
+ this.stopExecutionTracking();
504
+ this.execution.set(null);
505
+ this.executionItems.set([]);
506
+ this.trackingUnavailable.set(false);
507
+ this.cancellingExecution.set(false);
388
508
  this.step.set('CONFIRMING');
389
509
  this.errorMessage.set(null);
390
- if (!this.confirmationIdempotencyKey) {
510
+ if (!this.confirmationIdempotencyKey)
391
511
  this.confirmationIdempotencyKey = generateBulkIdempotencyKey();
392
- }
393
- const requestOptions = {
394
- endpointKey: this.data.endpointKey,
395
- apiUrlEntry: this.data.apiUrlEntry,
396
- idempotencyKey: this.confirmationIdempotencyKey,
397
- };
398
- this.bulkClient
399
- .confirm(bulkContract.confirmationOperation, prop.proposalId, requestOptions)
512
+ const requestOptions = this.executionRequestOptions();
513
+ this.bulkClient.confirm(bulkContract.confirmationOperation, prop.proposalId, requestOptions)
400
514
  .pipe(takeUntilDestroyed(this.destroyRef))
401
515
  .subscribe({
402
- next: (execution) => {
403
- this.execution.set(execution);
404
- const isTerminal = typeof this.bulkClient.isTerminalStatus === 'function'
405
- ? this.bulkClient.isTerminalStatus(execution.status)
406
- : execution.status !== 'QUEUED' && execution.status !== 'RUNNING';
407
- if (!isTerminal && bulkContract.executionOperation && typeof this.bulkClient.waitForExecution === 'function') {
408
- this.bulkClient
409
- .waitForExecution(bulkContract.executionOperation, execution.executionId, 1000, 60000, requestOptions)
410
- .pipe(takeUntilDestroyed(this.destroyRef))
411
- .subscribe({
412
- next: (finalExec) => {
413
- this.finalizeExecution(finalExec);
414
- },
415
- error: (err) => {
416
- this.errorMessage.set(err?.error?.message || err?.message || 'Falha ao acompanhar execução em lote.');
417
- this.step.set('RESULT');
418
- },
419
- });
516
+ next: exec => {
517
+ if (!this.isCurrentExecution(exec, generation))
420
518
  return;
519
+ if (isBulkExecutionTerminal(exec.status))
520
+ this.finalizeExecution(exec);
521
+ else {
522
+ this.execution.set(exec);
523
+ this.startExecutionTracking(exec, requestOptions);
421
524
  }
422
- this.finalizeExecution(execution);
423
525
  },
424
- error: (err) => {
526
+ error: err => {
527
+ if (generation !== this.executionGeneration || this.destroyRef.destroyed)
528
+ return;
425
529
  this.errorMessage.set(err?.error?.message || err?.message || 'Falha ao confirmar execução em lote.');
426
530
  this.step.set('PREVIEW');
427
531
  },
428
532
  });
429
533
  }
534
+ executionRequestOptions() {
535
+ return { endpointKey: this.data.endpointKey, apiUrlEntry: this.data.apiUrlEntry,
536
+ idempotencyKey: this.confirmationIdempotencyKey };
537
+ }
538
+ isCurrentExecution(exec, generation) {
539
+ const current = this.execution();
540
+ return !this.trackingClosed && !this.destroyRef.destroyed && generation === this.executionGeneration
541
+ && this.step() === 'CONFIRMING'
542
+ && (!current || (current.executionId === exec.executionId && !isBulkExecutionTerminal(current.status)));
543
+ }
544
+ stopExecutionTracking() {
545
+ this.trackingSubscription?.unsubscribe();
546
+ this.trackingSubscription = null;
547
+ }
548
+ markTrackingUnavailable() {
549
+ this.stopExecutionTracking();
550
+ this.trackingUnavailable.set(true);
551
+ this.errorMessage.set(this.tx('crud.bulk.trackingUnavailable'));
552
+ }
553
+ startExecutionTracking(exec, options) {
554
+ const contract = this.action.execution?.bulk;
555
+ if (!this.isCurrentExecution(exec, this.executionGeneration) || this.trackingSubscription)
556
+ return;
557
+ if (!contract?.executionOperation || typeof this.bulkClient.waitForExecution !== 'function') {
558
+ this.markTrackingUnavailable();
559
+ return;
560
+ }
561
+ this.trackingUnavailable.set(false);
562
+ this.errorMessage.set(null);
563
+ const generation = this.executionGeneration;
564
+ // Install the owner BEFORE subscribing: a synchronous terminal/error can
565
+ // close it before the inner subscription is returned and added.
566
+ const owner = new Subscription();
567
+ this.trackingSubscription = owner;
568
+ owner.add(this.bulkClient.waitForExecution(contract.executionOperation, exec.executionId, 1000, 60000, options)
569
+ .pipe(takeUntilDestroyed(this.destroyRef))
570
+ .subscribe({
571
+ next: result => {
572
+ if (this.isCurrentExecution(result, generation))
573
+ this.finalizeExecution(result);
574
+ },
575
+ error: () => {
576
+ if (this.isCurrentExecution(exec, generation))
577
+ this.markTrackingUnavailable();
578
+ },
579
+ }));
580
+ }
581
+ resumeTracking() {
582
+ const exec = this.execution();
583
+ if (!this.trackingUnavailable() || !exec || isBulkExecutionTerminal(exec.status))
584
+ return;
585
+ this.startExecutionTracking(exec, this.executionRequestOptions());
586
+ }
587
+ tx(key) {
588
+ return translateCrudRuntimeText(this.i18n, key);
589
+ }
430
590
  finalizeExecution(exec) {
591
+ if (!isBulkExecutionTerminal(exec.status) || !this.isCurrentExecution(exec, this.executionGeneration))
592
+ return;
593
+ this.stopExecutionTracking();
594
+ this.trackingUnavailable.set(false);
595
+ this.cancellingExecution.set(false);
596
+ this.errorMessage.set(null);
431
597
  this.execution.set(exec);
432
598
  const inlineItems = Array.isArray(exec.items)
433
599
  ? exec.items
@@ -442,7 +608,7 @@ class PraxisBulkOperationDialogComponent {
442
608
  loadExecutionResults(cursor) {
443
609
  const exec = this.execution();
444
610
  const bulkContract = this.action.execution?.bulk;
445
- if (!exec ||
611
+ if (!exec || !isBulkExecutionTerminal(exec.status) ||
446
612
  !bulkContract?.resultsOperation ||
447
613
  typeof this.bulkClient.getExecutionResults !== 'function') {
448
614
  return;
@@ -474,38 +640,33 @@ class PraxisBulkOperationDialogComponent {
474
640
  }
475
641
  canCancelExecution() {
476
642
  const exec = this.execution();
477
- const bulkContract = this.action.execution?.bulk;
478
- if (this.step() !== 'CONFIRMING' || !exec || !bulkContract?.cancelOperation) {
479
- return false;
480
- }
481
- const isTerminal = typeof this.bulkClient.isTerminalStatus === 'function'
482
- ? this.bulkClient.isTerminalStatus(exec.status)
483
- : exec.status !== 'QUEUED' && exec.status !== 'RUNNING';
484
- return !isTerminal;
643
+ return !this.trackingClosed && !this.destroyRef.destroyed
644
+ && this.step() === 'CONFIRMING' && !!exec && !!this.action.execution?.bulk?.cancelOperation
645
+ && !this.cancellingExecution() && exec.status !== 'CANCEL_REQUESTED'
646
+ && !isBulkExecutionTerminal(exec.status);
485
647
  }
486
648
  onCancelActiveExecution() {
487
649
  const exec = this.execution();
488
- const bulkContract = this.action.execution?.bulk;
489
- if (!this.canCancelExecution() ||
490
- !exec ||
491
- !bulkContract?.cancelOperation ||
492
- typeof this.bulkClient.cancel !== 'function') {
650
+ const contract = this.action.execution?.bulk;
651
+ if (!this.canCancelExecution() || !exec || !contract?.cancelOperation)
493
652
  return;
494
- }
495
653
  this.cancellingExecution.set(true);
496
- const requestOptions = {
497
- endpointKey: this.data.endpointKey,
498
- apiUrlEntry: this.data.apiUrlEntry,
499
- };
500
- this.bulkClient
501
- .cancel(bulkContract.cancelOperation, exec.executionId, requestOptions)
502
- .pipe(takeUntilDestroyed(this.destroyRef))
503
- .subscribe({
504
- next: (cancelledExec) => {
654
+ const generation = this.executionGeneration;
655
+ this.bulkClient.cancel(contract.cancelOperation, exec.executionId, {
656
+ endpointKey: this.data.endpointKey, apiUrlEntry: this.data.apiUrlEntry,
657
+ }).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
658
+ next: result => {
659
+ if (!this.isCurrentExecution(result, generation))
660
+ return;
505
661
  this.cancellingExecution.set(false);
506
- this.finalizeExecution(cancelledExec);
662
+ if (isBulkExecutionTerminal(result.status))
663
+ this.finalizeExecution(result);
664
+ else
665
+ this.execution.set(result);
507
666
  },
508
- error: (err) => {
667
+ error: err => {
668
+ if (!this.isCurrentExecution(exec, generation))
669
+ return;
509
670
  this.cancellingExecution.set(false);
510
671
  this.errorMessage.set(err?.error?.message || err?.message || 'Falha ao solicitar cancelamento da execução em lote.');
511
672
  },
@@ -513,10 +674,11 @@ class PraxisBulkOperationDialogComponent {
513
674
  }
514
675
  onCloseSuccess() {
515
676
  const exec = this.execution();
516
- const status = exec?.status;
517
- const confirmed = exec?.totals?.confirmed ?? 0;
518
- const isSuccess = !exec ||
519
- status === 'COMPLETED' ||
677
+ if (!exec || !isBulkExecutionTerminal(exec.status))
678
+ return;
679
+ const status = exec.status;
680
+ const confirmed = exec.totals.confirmed;
681
+ const isSuccess = status === 'COMPLETED' ||
520
682
  status === 'COMPLETED_WITH_ERRORS' ||
521
683
  confirmed > 0;
522
684
  this.dialogRef.close({
@@ -526,14 +688,25 @@ class PraxisBulkOperationDialogComponent {
526
688
  });
527
689
  }
528
690
  onCancel() {
691
+ if (this.trackingClosed)
692
+ return;
529
693
  if (this.step() === 'RESULT') {
530
694
  this.onCloseSuccess();
531
695
  return;
532
696
  }
697
+ if (this.step() === 'CONFIRMING') {
698
+ if (!this.trackingUnavailable())
699
+ return;
700
+ this.trackingClosed = true;
701
+ this.executionGeneration++;
702
+ this.stopExecutionTracking();
703
+ this.dialogRef.close({ success: false, execution: this.execution(), results: this.executionItems() });
704
+ return;
705
+ }
533
706
  this.dialogRef.close({ success: false });
534
707
  }
535
708
  onEscape(event) {
536
- if (this.step() !== 'CONFIRMING' && this.step() !== 'EVALUATING') {
709
+ if ((this.step() !== 'CONFIRMING' || this.trackingUnavailable()) && this.step() !== 'EVALUATING') {
537
710
  event?.preventDefault();
538
711
  this.onCancel();
539
712
  }
@@ -551,7 +724,7 @@ class PraxisBulkOperationDialogComponent {
551
724
  </p>
552
725
  </div>
553
726
  </div>
554
- <button mat-icon-button (click)="onCancel()" [disabled]="step() === 'CONFIRMING'" aria-label="Fechar diálogo">
727
+ <button mat-icon-button (click)="onCancel()" [disabled]="step() === 'CONFIRMING' && !trackingUnavailable()" aria-label="Fechar diálogo">
555
728
  <mat-icon>close</mat-icon>
556
729
  </button>
557
730
  </div>
@@ -712,17 +885,23 @@ class PraxisBulkOperationDialogComponent {
712
885
  <!-- Passo de Confirmação em Progresso -->
713
886
  @if (step() === 'CONFIRMING') {
714
887
  <div class="praxis-bulk-dialog__loading">
715
- <mat-spinner diameter="40" />
716
- <p>Executando alterações em lote com garantia de idempotência...</p>
717
- @if (canCancelExecution()) {
718
- <button
719
- mat-stroked-button
720
- color="warn"
721
- type="button"
722
- (click)="onCancelActiveExecution()"
723
- [disabled]="cancellingExecution()"
724
- >
725
- {{ cancellingExecution() ? 'Cancelando...' : 'Cancelar Execução' }}
888
+ @if (trackingUnavailable()) {
889
+ <div role="alert">
890
+ <strong>{{ tx('crud.bulk.trackingUnavailable') }}</strong>
891
+ <p>{{ tx('crud.bulk.effectsMayExist') }}</p>
892
+ </div>
893
+ <button mat-stroked-button type="button" data-testid="bulk-execution-resume" (click)="resumeTracking()">
894
+ {{ tx('crud.bulk.resumeTracking') }}
895
+ </button>
896
+ <button mat-button type="button" (click)="onCancel()">{{ tx('crud.bulk.closeUnknown') }}</button>
897
+ } @else {
898
+ <mat-spinner diameter="40" />
899
+ <p role="status">{{ execution()?.status === 'CANCEL_REQUESTED' ? tx('crud.bulk.cancelRequested') : tx('crud.bulk.tracking') }}</p>
900
+ }
901
+ @if (canCancelExecution() || cancellingExecution()) {
902
+ <button mat-stroked-button color="warn" type="button"
903
+ (click)="onCancelActiveExecution()" [disabled]="cancellingExecution()">
904
+ {{ cancellingExecution() ? tx('crud.bulk.requestingCancel') : tx('crud.bulk.cancelRequest') }}
726
905
  </button>
727
906
  }
728
907
  </div>
@@ -803,7 +982,7 @@ class PraxisBulkOperationDialogComponent {
803
982
  }
804
983
 
805
984
  <!-- Erro Geral -->
806
- @if (errorMessage()) {
985
+ @if (errorMessage() && !trackingUnavailable()) {
807
986
  <div class="praxis-bulk-dialog__alert praxis-bulk-dialog__alert--error">
808
987
  <mat-icon>error</mat-icon>
809
988
  <div class="alert-content">
@@ -869,7 +1048,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
869
1048
  </p>
870
1049
  </div>
871
1050
  </div>
872
- <button mat-icon-button (click)="onCancel()" [disabled]="step() === 'CONFIRMING'" aria-label="Fechar diálogo">
1051
+ <button mat-icon-button (click)="onCancel()" [disabled]="step() === 'CONFIRMING' && !trackingUnavailable()" aria-label="Fechar diálogo">
873
1052
  <mat-icon>close</mat-icon>
874
1053
  </button>
875
1054
  </div>
@@ -1030,17 +1209,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
1030
1209
  <!-- Passo de Confirmação em Progresso -->
1031
1210
  @if (step() === 'CONFIRMING') {
1032
1211
  <div class="praxis-bulk-dialog__loading">
1033
- <mat-spinner diameter="40" />
1034
- <p>Executando alterações em lote com garantia de idempotência...</p>
1035
- @if (canCancelExecution()) {
1036
- <button
1037
- mat-stroked-button
1038
- color="warn"
1039
- type="button"
1040
- (click)="onCancelActiveExecution()"
1041
- [disabled]="cancellingExecution()"
1042
- >
1043
- {{ cancellingExecution() ? 'Cancelando...' : 'Cancelar Execução' }}
1212
+ @if (trackingUnavailable()) {
1213
+ <div role="alert">
1214
+ <strong>{{ tx('crud.bulk.trackingUnavailable') }}</strong>
1215
+ <p>{{ tx('crud.bulk.effectsMayExist') }}</p>
1216
+ </div>
1217
+ <button mat-stroked-button type="button" data-testid="bulk-execution-resume" (click)="resumeTracking()">
1218
+ {{ tx('crud.bulk.resumeTracking') }}
1219
+ </button>
1220
+ <button mat-button type="button" (click)="onCancel()">{{ tx('crud.bulk.closeUnknown') }}</button>
1221
+ } @else {
1222
+ <mat-spinner diameter="40" />
1223
+ <p role="status">{{ execution()?.status === 'CANCEL_REQUESTED' ? tx('crud.bulk.cancelRequested') : tx('crud.bulk.tracking') }}</p>
1224
+ }
1225
+ @if (canCancelExecution() || cancellingExecution()) {
1226
+ <button mat-stroked-button color="warn" type="button"
1227
+ (click)="onCancelActiveExecution()" [disabled]="cancellingExecution()">
1228
+ {{ cancellingExecution() ? tx('crud.bulk.requestingCancel') : tx('crud.bulk.cancelRequest') }}
1044
1229
  </button>
1045
1230
  }
1046
1231
  </div>
@@ -1121,7 +1306,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
1121
1306
  }
1122
1307
 
1123
1308
  <!-- Erro Geral -->
1124
- @if (errorMessage()) {
1309
+ @if (errorMessage() && !trackingUnavailable()) {
1125
1310
  <div class="praxis-bulk-dialog__alert praxis-bulk-dialog__alert--error">
1126
1311
  <mat-icon>error</mat-icon>
1127
1312
  <div class="alert-content">
@@ -1180,6 +1365,11 @@ class PraxisBulkEditDialogComponent {
1180
1365
  dialogRef = inject((MatDialogRef));
1181
1366
  bulkClient = inject(PraxisBulkClientService);
1182
1367
  destroyRef = inject(DestroyRef);
1368
+ i18n = inject(PraxisI18nService);
1369
+ trackingUnavailable = signal(false, ...(ngDevMode ? [{ debugName: "trackingUnavailable" }] : /* istanbul ignore next */ []));
1370
+ trackingSubscription = null;
1371
+ executionGeneration = 0;
1372
+ trackingClosed = false;
1183
1373
  step = signal('FIELDS', ...(ngDevMode ? [{ debugName: "step" }] : /* istanbul ignore next */ []));
1184
1374
  fieldRows = signal([], ...(ngDevMode ? [{ debugName: "fieldRows" }] : /* istanbul ignore next */ []));
1185
1375
  proposal = signal(null, ...(ngDevMode ? [{ debugName: "proposal" }] : /* istanbul ignore next */ []));
@@ -1207,6 +1397,10 @@ class PraxisBulkEditDialogComponent {
1207
1397
  }
1208
1398
  constructor(data) {
1209
1399
  this.data = data;
1400
+ this.destroyRef.onDestroy(() => {
1401
+ this.executionGeneration++;
1402
+ this.stopExecutionTracking();
1403
+ });
1210
1404
  this.contract = data.bulkContract;
1211
1405
  this.targets = data.targets || [];
1212
1406
  this.writableFields = data.bulkContract.editableFields?.writableFields || [];
@@ -1636,51 +1830,104 @@ class PraxisBulkEditDialogComponent {
1636
1830
  }
1637
1831
  onConfirm() {
1638
1832
  const prop = this.proposal();
1639
- if (!prop)
1833
+ const bulkContract = this.contract;
1834
+ if (!prop || !bulkContract || this.trackingClosed || this.step() === 'CONFIRMING')
1640
1835
  return;
1836
+ const generation = ++this.executionGeneration;
1837
+ this.stopExecutionTracking();
1838
+ this.execution.set(null);
1839
+ this.executionItems.set([]);
1840
+ this.trackingUnavailable.set(false);
1841
+ this.cancellingExecution.set(false);
1641
1842
  this.step.set('CONFIRMING');
1642
1843
  this.errorMessage.set(null);
1643
- if (!this.confirmationIdempotencyKey) {
1844
+ if (!this.confirmationIdempotencyKey)
1644
1845
  this.confirmationIdempotencyKey = generateBulkIdempotencyKey();
1645
- }
1646
- const requestOptions = {
1647
- endpointKey: this.data.endpointKey,
1648
- apiUrlEntry: this.data.apiUrlEntry,
1649
- idempotencyKey: this.confirmationIdempotencyKey,
1650
- };
1651
- this.bulkClient
1652
- .confirm(this.contract.confirmationOperation, prop.proposalId, requestOptions)
1846
+ const requestOptions = this.executionRequestOptions();
1847
+ this.bulkClient.confirm(bulkContract.confirmationOperation, prop.proposalId, requestOptions)
1653
1848
  .pipe(takeUntilDestroyed(this.destroyRef))
1654
1849
  .subscribe({
1655
- next: (exec) => {
1656
- this.execution.set(exec);
1657
- const isTerminal = typeof this.bulkClient.isTerminalStatus === 'function'
1658
- ? this.bulkClient.isTerminalStatus(exec.status)
1659
- : exec.status !== 'QUEUED' && exec.status !== 'RUNNING';
1660
- if (!isTerminal && this.contract.executionOperation && typeof this.bulkClient.waitForExecution === 'function') {
1661
- this.bulkClient
1662
- .waitForExecution(this.contract.executionOperation, exec.executionId, 1000, 60000, requestOptions)
1663
- .pipe(takeUntilDestroyed(this.destroyRef))
1664
- .subscribe({
1665
- next: (finalExec) => {
1666
- this.finalizeExecution(finalExec);
1667
- },
1668
- error: (err) => {
1669
- this.errorMessage.set(err?.error?.message || err?.message || 'Falha ao acompanhar execução em lote.');
1670
- this.step.set('RESULT');
1671
- },
1672
- });
1850
+ next: exec => {
1851
+ if (!this.isCurrentExecution(exec, generation))
1673
1852
  return;
1853
+ if (isBulkExecutionTerminal(exec.status))
1854
+ this.finalizeExecution(exec);
1855
+ else {
1856
+ this.execution.set(exec);
1857
+ this.startExecutionTracking(exec, requestOptions);
1674
1858
  }
1675
- this.finalizeExecution(exec);
1676
1859
  },
1677
- error: (err) => {
1860
+ error: err => {
1861
+ if (generation !== this.executionGeneration || this.destroyRef.destroyed)
1862
+ return;
1678
1863
  this.errorMessage.set(err?.error?.message || err?.message || 'Falha ao confirmar alterações em lote.');
1679
1864
  this.step.set('PREVIEW');
1680
1865
  },
1681
1866
  });
1682
1867
  }
1868
+ executionRequestOptions() {
1869
+ return { endpointKey: this.data.endpointKey, apiUrlEntry: this.data.apiUrlEntry,
1870
+ idempotencyKey: this.confirmationIdempotencyKey };
1871
+ }
1872
+ isCurrentExecution(exec, generation) {
1873
+ const current = this.execution();
1874
+ return !this.trackingClosed && !this.destroyRef.destroyed && generation === this.executionGeneration
1875
+ && this.step() === 'CONFIRMING'
1876
+ && (!current || (current.executionId === exec.executionId && !isBulkExecutionTerminal(current.status)));
1877
+ }
1878
+ stopExecutionTracking() {
1879
+ this.trackingSubscription?.unsubscribe();
1880
+ this.trackingSubscription = null;
1881
+ }
1882
+ markTrackingUnavailable() {
1883
+ this.stopExecutionTracking();
1884
+ this.trackingUnavailable.set(true);
1885
+ this.errorMessage.set(this.tx('crud.bulk.trackingUnavailable'));
1886
+ }
1887
+ startExecutionTracking(exec, options) {
1888
+ const contract = this.contract;
1889
+ if (!this.isCurrentExecution(exec, this.executionGeneration) || this.trackingSubscription)
1890
+ return;
1891
+ if (!contract?.executionOperation || typeof this.bulkClient.waitForExecution !== 'function') {
1892
+ this.markTrackingUnavailable();
1893
+ return;
1894
+ }
1895
+ this.trackingUnavailable.set(false);
1896
+ this.errorMessage.set(null);
1897
+ const generation = this.executionGeneration;
1898
+ // Install the owner BEFORE subscribing: a synchronous terminal/error can
1899
+ // close it before the inner subscription is returned and added.
1900
+ const owner = new Subscription();
1901
+ this.trackingSubscription = owner;
1902
+ owner.add(this.bulkClient.waitForExecution(contract.executionOperation, exec.executionId, 1000, 60000, options)
1903
+ .pipe(takeUntilDestroyed(this.destroyRef))
1904
+ .subscribe({
1905
+ next: result => {
1906
+ if (this.isCurrentExecution(result, generation))
1907
+ this.finalizeExecution(result);
1908
+ },
1909
+ error: () => {
1910
+ if (this.isCurrentExecution(exec, generation))
1911
+ this.markTrackingUnavailable();
1912
+ },
1913
+ }));
1914
+ }
1915
+ resumeTracking() {
1916
+ const exec = this.execution();
1917
+ if (!this.trackingUnavailable() || !exec || isBulkExecutionTerminal(exec.status))
1918
+ return;
1919
+ this.startExecutionTracking(exec, this.executionRequestOptions());
1920
+ }
1921
+ tx(key) {
1922
+ return translateCrudRuntimeText(this.i18n, key);
1923
+ }
1683
1924
  finalizeExecution(exec) {
1925
+ if (!isBulkExecutionTerminal(exec.status) || !this.isCurrentExecution(exec, this.executionGeneration))
1926
+ return;
1927
+ this.stopExecutionTracking();
1928
+ this.trackingUnavailable.set(false);
1929
+ this.cancellingExecution.set(false);
1930
+ this.errorMessage.set(null);
1684
1931
  this.execution.set(exec);
1685
1932
  const inlineItems = Array.isArray(exec.items)
1686
1933
  ? exec.items
@@ -1694,7 +1941,7 @@ class PraxisBulkEditDialogComponent {
1694
1941
  }
1695
1942
  loadExecutionResults(cursor) {
1696
1943
  const exec = this.execution();
1697
- if (!exec ||
1944
+ if (!exec || !isBulkExecutionTerminal(exec.status) ||
1698
1945
  !this.contract?.resultsOperation ||
1699
1946
  typeof this.bulkClient.getExecutionResults !== 'function') {
1700
1947
  return;
@@ -1726,36 +1973,33 @@ class PraxisBulkEditDialogComponent {
1726
1973
  }
1727
1974
  canCancelExecution() {
1728
1975
  const exec = this.execution();
1729
- if (this.step() !== 'CONFIRMING' || !exec || !this.contract?.cancelOperation) {
1730
- return false;
1731
- }
1732
- const isTerminal = typeof this.bulkClient.isTerminalStatus === 'function'
1733
- ? this.bulkClient.isTerminalStatus(exec.status)
1734
- : exec.status !== 'QUEUED' && exec.status !== 'RUNNING';
1735
- return !isTerminal;
1976
+ return !this.trackingClosed && !this.destroyRef.destroyed
1977
+ && this.step() === 'CONFIRMING' && !!exec && !!this.contract?.cancelOperation
1978
+ && !this.cancellingExecution() && exec.status !== 'CANCEL_REQUESTED'
1979
+ && !isBulkExecutionTerminal(exec.status);
1736
1980
  }
1737
1981
  onCancelActiveExecution() {
1738
1982
  const exec = this.execution();
1739
- if (!this.canCancelExecution() ||
1740
- !exec ||
1741
- !this.contract?.cancelOperation ||
1742
- typeof this.bulkClient.cancel !== 'function') {
1983
+ const contract = this.contract;
1984
+ if (!this.canCancelExecution() || !exec || !contract?.cancelOperation)
1743
1985
  return;
1744
- }
1745
1986
  this.cancellingExecution.set(true);
1746
- const requestOptions = {
1747
- endpointKey: this.data.endpointKey,
1748
- apiUrlEntry: this.data.apiUrlEntry,
1749
- };
1750
- this.bulkClient
1751
- .cancel(this.contract.cancelOperation, exec.executionId, requestOptions)
1752
- .pipe(takeUntilDestroyed(this.destroyRef))
1753
- .subscribe({
1754
- next: (cancelledExec) => {
1987
+ const generation = this.executionGeneration;
1988
+ this.bulkClient.cancel(contract.cancelOperation, exec.executionId, {
1989
+ endpointKey: this.data.endpointKey, apiUrlEntry: this.data.apiUrlEntry,
1990
+ }).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
1991
+ next: result => {
1992
+ if (!this.isCurrentExecution(result, generation))
1993
+ return;
1755
1994
  this.cancellingExecution.set(false);
1756
- this.finalizeExecution(cancelledExec);
1995
+ if (isBulkExecutionTerminal(result.status))
1996
+ this.finalizeExecution(result);
1997
+ else
1998
+ this.execution.set(result);
1757
1999
  },
1758
- error: (err) => {
2000
+ error: err => {
2001
+ if (!this.isCurrentExecution(exec, generation))
2002
+ return;
1759
2003
  this.cancellingExecution.set(false);
1760
2004
  this.errorMessage.set(err?.error?.message || err?.message || 'Falha ao solicitar cancelamento da execução em lote.');
1761
2005
  },
@@ -1763,10 +2007,11 @@ class PraxisBulkEditDialogComponent {
1763
2007
  }
1764
2008
  onCloseSuccess() {
1765
2009
  const exec = this.execution();
1766
- const status = exec?.status;
1767
- const confirmed = exec?.totals?.confirmed ?? 0;
1768
- const isSuccess = !exec ||
1769
- status === 'COMPLETED' ||
2010
+ if (!exec || !isBulkExecutionTerminal(exec.status))
2011
+ return;
2012
+ const status = exec.status;
2013
+ const confirmed = exec.totals.confirmed;
2014
+ const isSuccess = status === 'COMPLETED' ||
1770
2015
  status === 'COMPLETED_WITH_ERRORS' ||
1771
2016
  confirmed > 0;
1772
2017
  this.dialogRef.close({
@@ -1776,14 +2021,25 @@ class PraxisBulkEditDialogComponent {
1776
2021
  });
1777
2022
  }
1778
2023
  onCancel() {
2024
+ if (this.trackingClosed)
2025
+ return;
1779
2026
  if (this.step() === 'RESULT') {
1780
2027
  this.onCloseSuccess();
1781
2028
  return;
1782
2029
  }
2030
+ if (this.step() === 'CONFIRMING') {
2031
+ if (!this.trackingUnavailable())
2032
+ return;
2033
+ this.trackingClosed = true;
2034
+ this.executionGeneration++;
2035
+ this.stopExecutionTracking();
2036
+ this.dialogRef.close({ success: false, execution: this.execution(), results: this.executionItems() });
2037
+ return;
2038
+ }
1783
2039
  this.dialogRef.close({ success: false });
1784
2040
  }
1785
2041
  onEscape(event) {
1786
- if (this.step() !== 'CONFIRMING' && this.step() !== 'EVALUATING') {
2042
+ if ((this.step() !== 'CONFIRMING' || this.trackingUnavailable()) && this.step() !== 'EVALUATING') {
1787
2043
  event?.preventDefault();
1788
2044
  this.onCancel();
1789
2045
  }
@@ -1821,7 +2077,7 @@ class PraxisBulkEditDialogComponent {
1821
2077
  <p id="praxis-bulk-edit-dialog-subtitle" class="praxis-bulk-edit-dialog__subtitle">{{ dialogSubtitle }}</p>
1822
2078
  </div>
1823
2079
  </div>
1824
- <button mat-icon-button (click)="onCancel()" [disabled]="step() === 'CONFIRMING'" aria-label="Fechar diálogo">
2080
+ <button mat-icon-button (click)="onCancel()" [disabled]="step() === 'CONFIRMING' && !trackingUnavailable()" aria-label="Fechar diálogo">
1825
2081
  <mat-icon>close</mat-icon>
1826
2082
  </button>
1827
2083
  </div>
@@ -2058,17 +2314,23 @@ class PraxisBulkEditDialogComponent {
2058
2314
  <!-- Confirmação em Andamento -->
2059
2315
  @if (step() === 'CONFIRMING') {
2060
2316
  <div class="praxis-bulk-edit-dialog__loading">
2061
- <mat-spinner diameter="40" />
2062
- <p>Gravando atualizações em lote com garantia de idempotência...</p>
2063
- @if (canCancelExecution()) {
2064
- <button
2065
- mat-stroked-button
2066
- color="warn"
2067
- type="button"
2068
- (click)="onCancelActiveExecution()"
2069
- [disabled]="cancellingExecution()"
2070
- >
2071
- {{ cancellingExecution() ? 'Cancelando...' : 'Cancelar Execução' }}
2317
+ @if (trackingUnavailable()) {
2318
+ <div role="alert">
2319
+ <strong>{{ tx('crud.bulk.trackingUnavailable') }}</strong>
2320
+ <p>{{ tx('crud.bulk.effectsMayExist') }}</p>
2321
+ </div>
2322
+ <button mat-stroked-button type="button" data-testid="bulk-execution-resume" (click)="resumeTracking()">
2323
+ {{ tx('crud.bulk.resumeTracking') }}
2324
+ </button>
2325
+ <button mat-button type="button" (click)="onCancel()">{{ tx('crud.bulk.closeUnknown') }}</button>
2326
+ } @else {
2327
+ <mat-spinner diameter="40" />
2328
+ <p role="status">{{ execution()?.status === 'CANCEL_REQUESTED' ? tx('crud.bulk.cancelRequested') : tx('crud.bulk.tracking') }}</p>
2329
+ }
2330
+ @if (canCancelExecution() || cancellingExecution()) {
2331
+ <button mat-stroked-button color="warn" type="button"
2332
+ (click)="onCancelActiveExecution()" [disabled]="cancellingExecution()">
2333
+ {{ cancellingExecution() ? tx('crud.bulk.requestingCancel') : tx('crud.bulk.cancelRequest') }}
2072
2334
  </button>
2073
2335
  }
2074
2336
  </div>
@@ -2144,7 +2406,7 @@ class PraxisBulkEditDialogComponent {
2144
2406
  </div>
2145
2407
  }
2146
2408
 
2147
- @if (errorMessage()) {
2409
+ @if (errorMessage() && !trackingUnavailable()) {
2148
2410
  <div class="praxis-bulk-edit-dialog__alert praxis-bulk-edit-dialog__alert--error">
2149
2411
  <mat-icon>error</mat-icon>
2150
2412
  <div class="alert-content">
@@ -2211,7 +2473,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
2211
2473
  <p id="praxis-bulk-edit-dialog-subtitle" class="praxis-bulk-edit-dialog__subtitle">{{ dialogSubtitle }}</p>
2212
2474
  </div>
2213
2475
  </div>
2214
- <button mat-icon-button (click)="onCancel()" [disabled]="step() === 'CONFIRMING'" aria-label="Fechar diálogo">
2476
+ <button mat-icon-button (click)="onCancel()" [disabled]="step() === 'CONFIRMING' && !trackingUnavailable()" aria-label="Fechar diálogo">
2215
2477
  <mat-icon>close</mat-icon>
2216
2478
  </button>
2217
2479
  </div>
@@ -2448,17 +2710,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
2448
2710
  <!-- Confirmação em Andamento -->
2449
2711
  @if (step() === 'CONFIRMING') {
2450
2712
  <div class="praxis-bulk-edit-dialog__loading">
2451
- <mat-spinner diameter="40" />
2452
- <p>Gravando atualizações em lote com garantia de idempotência...</p>
2453
- @if (canCancelExecution()) {
2454
- <button
2455
- mat-stroked-button
2456
- color="warn"
2457
- type="button"
2458
- (click)="onCancelActiveExecution()"
2459
- [disabled]="cancellingExecution()"
2460
- >
2461
- {{ cancellingExecution() ? 'Cancelando...' : 'Cancelar Execução' }}
2713
+ @if (trackingUnavailable()) {
2714
+ <div role="alert">
2715
+ <strong>{{ tx('crud.bulk.trackingUnavailable') }}</strong>
2716
+ <p>{{ tx('crud.bulk.effectsMayExist') }}</p>
2717
+ </div>
2718
+ <button mat-stroked-button type="button" data-testid="bulk-execution-resume" (click)="resumeTracking()">
2719
+ {{ tx('crud.bulk.resumeTracking') }}
2720
+ </button>
2721
+ <button mat-button type="button" (click)="onCancel()">{{ tx('crud.bulk.closeUnknown') }}</button>
2722
+ } @else {
2723
+ <mat-spinner diameter="40" />
2724
+ <p role="status">{{ execution()?.status === 'CANCEL_REQUESTED' ? tx('crud.bulk.cancelRequested') : tx('crud.bulk.tracking') }}</p>
2725
+ }
2726
+ @if (canCancelExecution() || cancellingExecution()) {
2727
+ <button mat-stroked-button color="warn" type="button"
2728
+ (click)="onCancelActiveExecution()" [disabled]="cancellingExecution()">
2729
+ {{ cancellingExecution() ? tx('crud.bulk.requestingCancel') : tx('crud.bulk.cancelRequest') }}
2462
2730
  </button>
2463
2731
  }
2464
2732
  </div>
@@ -2534,7 +2802,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
2534
2802
  </div>
2535
2803
  }
2536
2804
 
2537
- @if (errorMessage()) {
2805
+ @if (errorMessage() && !trackingUnavailable()) {
2538
2806
  <div class="praxis-bulk-edit-dialog__alert praxis-bulk-edit-dialog__alert--error">
2539
2807
  <mat-icon>error</mat-icon>
2540
2808
  <div class="alert-content">
@@ -4170,95 +4438,6 @@ function assertCrudMetadata(meta, options = {}) {
4170
4438
  });
4171
4439
  }
4172
4440
 
4173
- const PRAXIS_CRUD_RUNTIME_I18N_NAMESPACE = 'praxisCrudRuntime';
4174
- const PRAXIS_CRUD_RUNTIME_I18N_CONFIG = {
4175
- namespaces: {
4176
- [PRAXIS_CRUD_RUNTIME_I18N_NAMESPACE]: {
4177
- 'pt-BR': {
4178
- 'crud.emptyState.title': 'Conecte o CRUD a um recurso',
4179
- 'crud.emptyState.description': 'Informe os metadados (resourcePath / schema) ou forneça metadata.data para habilitar a tabela e as ações.',
4180
- 'crud.emptyState.primaryAction': 'Configurar metadados',
4181
- 'crud.table.emptyState.initial.title': 'Sem registros em {label}',
4182
- 'crud.table.emptyState.initial.titleFallback': 'Nenhum registro disponível.',
4183
- 'crud.table.emptyState.initial.descriptionWithToolbarAction': 'Use "{action}" na barra da tabela para adicionar o primeiro registro quando houver informações para cadastrar.',
4184
- 'crud.table.emptyState.filtered.title': 'Nenhum resultado encontrado.',
4185
- 'crud.table.emptyState.filtered.description': 'Revise os filtros ou ajuste o termo de busca.',
4186
- 'crud.preferences.resetSuccess': 'Overrides de CRUD redefinidos',
4187
- 'crud.actions.create': 'Adicionar',
4188
- 'crud.actions.view': 'Ver',
4189
- 'crud.actions.edit': 'Editar',
4190
- 'crud.actions.delete': 'Excluir',
4191
- 'crud.actions.default': 'ação',
4192
- 'crud.actions.opening': 'Abrindo {action}…',
4193
- 'crud.surface.openFailed': 'Não foi possível abrir {title}. Tente novamente ou confirme se esse recurso continua disponível.',
4194
- 'crud.dialog.recordContextLabel': 'Registro em edição',
4195
- 'crud.dialog.viewRecordContextLabel': 'Registro em consulta',
4196
- 'crud.dialog.relatedContextLabel': 'Vinculado a',
4197
- 'crud.delete.confirmMessage': 'Esta ação não pode ser desfeita. Deseja continuar?',
4198
- 'crud.delete.cancel': 'Cancelar',
4199
- },
4200
- 'en-US': {
4201
- 'crud.emptyState.title': 'Connect CRUD to a resource',
4202
- 'crud.emptyState.description': 'Provide metadata (resourcePath / schema) or metadata.data to enable the table and actions.',
4203
- 'crud.emptyState.primaryAction': 'Configure metadata',
4204
- 'crud.table.emptyState.initial.title': 'No records in {label}',
4205
- 'crud.table.emptyState.initial.titleFallback': 'No records available.',
4206
- 'crud.table.emptyState.initial.descriptionWithToolbarAction': 'Use "{action}" in the table toolbar to add the first record when there is information to register.',
4207
- 'crud.table.emptyState.filtered.title': 'No results found.',
4208
- 'crud.table.emptyState.filtered.description': 'Review the filters or adjust the search term.',
4209
- 'crud.preferences.resetSuccess': 'CRUD overrides reset',
4210
- 'crud.actions.create': 'Add',
4211
- 'crud.actions.view': 'View',
4212
- 'crud.actions.edit': 'Edit',
4213
- 'crud.actions.delete': 'Delete',
4214
- 'crud.actions.default': 'action',
4215
- 'crud.actions.opening': 'Opening {action}…',
4216
- 'crud.surface.openFailed': 'Could not open {title}. Try again or confirm that this resource is still available.',
4217
- 'crud.dialog.recordContextLabel': 'Record being edited',
4218
- 'crud.dialog.viewRecordContextLabel': 'Record being viewed',
4219
- 'crud.dialog.relatedContextLabel': 'Related to',
4220
- 'crud.delete.confirmMessage': 'This action cannot be undone. Do you want to continue?',
4221
- 'crud.delete.cancel': 'Cancel',
4222
- },
4223
- },
4224
- },
4225
- };
4226
- function getCrudRuntimeDictionary(locale) {
4227
- const dictionaries = PRAXIS_CRUD_RUNTIME_I18N_CONFIG.namespaces?.[PRAXIS_CRUD_RUNTIME_I18N_NAMESPACE];
4228
- if (!dictionaries) {
4229
- return undefined;
4230
- }
4231
- const normalized = String(locale || '').trim();
4232
- const candidates = [
4233
- normalized,
4234
- normalized.toLowerCase(),
4235
- normalized.includes('-') ? normalized.split('-')[0] : '',
4236
- normalized.includes('_') ? normalized.split('_')[0] : '',
4237
- 'pt-BR',
4238
- 'pt-br',
4239
- 'en-US',
4240
- 'en-us',
4241
- ].filter(Boolean);
4242
- for (const candidate of candidates) {
4243
- const dictionary = dictionaries[candidate];
4244
- if (dictionary && typeof dictionary === 'object') {
4245
- return dictionary;
4246
- }
4247
- }
4248
- return undefined;
4249
- }
4250
- function resolveCrudRuntimeFallback(key, locale) {
4251
- return getCrudRuntimeDictionary(locale || 'pt-BR')?.[key];
4252
- }
4253
- function translateCrudRuntimeText(i18n, key, fallback, params, locale) {
4254
- const effectiveLocale = locale || i18n.getLocale?.() || i18n.getFallbackLocale?.() || 'pt-BR';
4255
- const runtimeFallback = resolveCrudRuntimeFallback(key, effectiveLocale)
4256
- || resolveCrudRuntimeFallback(key, i18n.getFallbackLocale?.())
4257
- || fallback
4258
- || key;
4259
- return i18n.t(key, params, runtimeFallback, PRAXIS_CRUD_RUNTIME_I18N_NAMESPACE);
4260
- }
4261
-
4262
4441
  const PRAXIS_CRUD_AUTHORING_I18N_NAMESPACE = 'praxisCrudAuthoring';
4263
4442
  const PRAXIS_CRUD_AUTHORING_I18N_CONFIG = {
4264
4443
  namespaces: {
@@ -13351,8 +13530,8 @@ function lowerFirstLetter(value) {
13351
13530
  }
13352
13531
 
13353
13532
  var dynamicFormDialogHost_component = /*#__PURE__*/Object.freeze({
13354
- __proto__: null,
13355
- DynamicFormDialogHostComponent: DynamicFormDialogHostComponent
13533
+ __proto__: null,
13534
+ DynamicFormDialogHostComponent: DynamicFormDialogHostComponent
13356
13535
  });
13357
13536
 
13358
13537
  class PraxisCrudWidgetConfigEditor {
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@praxisui/crud",
3
- "version": "10.0.0-rc.24",
3
+ "version": "10.0.0-rc.26",
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.24",
9
- "@praxisui/table": "^10.0.0-rc.24",
10
- "@praxisui/core": "^10.0.0-rc.24",
11
- "@praxisui/dynamic-fields": "^10.0.0-rc.24",
12
- "@praxisui/settings-panel": "^10.0.0-rc.24",
8
+ "@praxisui/dynamic-form": "^10.0.0-rc.26",
9
+ "@praxisui/table": "^10.0.0-rc.26",
10
+ "@praxisui/core": "^10.0.0-rc.26",
11
+ "@praxisui/dynamic-fields": "^10.0.0-rc.26",
12
+ "@praxisui/settings-panel": "^10.0.0-rc.26",
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.24",
17
+ "@praxisui/ai": "^10.0.0-rc.26",
18
18
  "rxjs": "~7.8.0"
19
19
  },
20
20
  "dependencies": {
@@ -1291,6 +1291,11 @@ declare class PraxisBulkOperationDialogComponent implements OnInit {
1291
1291
  private readonly dialogRef;
1292
1292
  private readonly bulkClient;
1293
1293
  private readonly destroyRef;
1294
+ private readonly i18n;
1295
+ protected readonly trackingUnavailable: _angular_core.WritableSignal<boolean>;
1296
+ private trackingSubscription;
1297
+ private executionGeneration;
1298
+ private trackingClosed;
1294
1299
  parametersForm?: PraxisDynamicForm;
1295
1300
  readonly step: _angular_core.WritableSignal<BulkDialogStep>;
1296
1301
  readonly proposal: _angular_core.WritableSignal<_praxisui_core.BulkProposal | null>;
@@ -1330,6 +1335,13 @@ declare class PraxisBulkOperationDialogComponent implements OnInit {
1330
1335
  onEvaluate(): void;
1331
1336
  loadProposalResults(cursor?: string): void;
1332
1337
  onConfirm(): void;
1338
+ private executionRequestOptions;
1339
+ private isCurrentExecution;
1340
+ private stopExecutionTracking;
1341
+ private markTrackingUnavailable;
1342
+ private startExecutionTracking;
1343
+ protected resumeTracking(): void;
1344
+ protected tx(key: string): string;
1333
1345
  private finalizeExecution;
1334
1346
  loadExecutionResults(cursor?: string): void;
1335
1347
  canCancelExecution(): boolean;
@@ -1365,6 +1377,11 @@ declare class PraxisBulkEditDialogComponent {
1365
1377
  private readonly dialogRef;
1366
1378
  private readonly bulkClient;
1367
1379
  private readonly destroyRef;
1380
+ private readonly i18n;
1381
+ protected readonly trackingUnavailable: _angular_core.WritableSignal<boolean>;
1382
+ private trackingSubscription;
1383
+ private executionGeneration;
1384
+ private trackingClosed;
1368
1385
  readonly step: _angular_core.WritableSignal<BulkEditStep>;
1369
1386
  readonly fieldRows: _angular_core.WritableSignal<EditableFieldRow[]>;
1370
1387
  readonly proposal: _angular_core.WritableSignal<_praxisui_core.BulkProposal | null>;
@@ -1414,6 +1431,13 @@ declare class PraxisBulkEditDialogComponent {
1414
1431
  onEvaluate(): void;
1415
1432
  loadProposalResults(cursor?: string): void;
1416
1433
  onConfirm(): void;
1434
+ private executionRequestOptions;
1435
+ private isCurrentExecution;
1436
+ private stopExecutionTracking;
1437
+ private markTrackingUnavailable;
1438
+ private startExecutionTracking;
1439
+ protected resumeTracking(): void;
1440
+ protected tx(key: string): string;
1417
1441
  private finalizeExecution;
1418
1442
  loadExecutionResults(cursor?: string): void;
1419
1443
  canCancelExecution(): boolean;