@praxisui/crud 3.0.0-beta.8 → 4.0.0-beta.0

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,15 +1,15 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, InjectionToken, inject, EventEmitter, DestroyRef, ViewChild, Output, Input, Component, Inject, ENVIRONMENT_INITIALIZER } from '@angular/core';
2
+ import { Injectable, InjectionToken, inject, EventEmitter, DestroyRef, ChangeDetectorRef, ViewChild, Output, Input, Component, Inject, ENVIRONMENT_INITIALIZER } from '@angular/core';
3
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
3
4
  import { Router, ActivatedRoute, RouterLink } from '@angular/router';
4
- import { PraxisTable } from '@praxisui/table';
5
+ import { MatSnackBar } from '@angular/material/snack-bar';
5
6
  import { firstValueFrom } from 'rxjs';
7
+ import * as i2 from '@praxisui/core';
8
+ import { ASYNC_CONFIG_STORAGE, GlobalConfigService, fillUndefined, createDefaultTableConfig, PraxisI18nService, ResourceDiscoveryService, ResourceActionOpenAdapterService, ResourceSurfaceOpenAdapterService, GLOBAL_SURFACE_SERVICE, ComponentKeyService, translateUnavailableWorkflowMessage, EmptyStateCardComponent, providePraxisI18nConfig, RESOURCE_DISCOVERY_I18N_CONFIG, PraxisIconDirective, GenericCrudService, ComponentMetadataRegistry } from '@praxisui/core';
9
+ import { PraxisTable } from '@praxisui/table';
6
10
  import * as i1 from '@angular/material/dialog';
7
11
  import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
8
12
  export { MAT_DIALOG_DATA as DIALOG_DATA } from '@angular/material/dialog';
9
- import * as i2 from '@praxisui/core';
10
- import { ASYNC_CONFIG_STORAGE, GlobalConfigService, fillUndefined, createDefaultTableConfig, ComponentKeyService, EmptyStateCardComponent, PraxisIconDirective, GenericCrudService, ComponentMetadataRegistry } from '@praxisui/core';
11
- import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
12
- import { MatSnackBar } from '@angular/material/snack-bar';
13
13
  import * as i1$1 from '@angular/common';
14
14
  import { CommonModule } from '@angular/common';
15
15
  import * as i4 from '@angular/material/button';
@@ -290,61 +290,125 @@ function assertCrudMetadata(meta, options = {}) {
290
290
  });
291
291
  }
292
292
 
293
+ const PRAXIS_CRUD_RUNTIME_I18N_NAMESPACE = 'praxisCrudRuntime';
294
+ const PRAXIS_CRUD_RUNTIME_I18N_CONFIG = {
295
+ namespaces: {
296
+ [PRAXIS_CRUD_RUNTIME_I18N_NAMESPACE]: {
297
+ 'pt-BR': {
298
+ 'crud.emptyState.title': 'Conecte o CRUD a um recurso',
299
+ 'crud.emptyState.description': 'Informe os metadados (resourcePath / schema) ou forneça metadata.data para habilitar a tabela e as ações.',
300
+ 'crud.emptyState.primaryAction': 'Configurar metadados',
301
+ 'crud.preferences.resetSuccess': 'Overrides de CRUD redefinidos',
302
+ 'crud.actions.create': 'Adicionar',
303
+ 'crud.actions.view': 'Ver',
304
+ 'crud.actions.edit': 'Editar',
305
+ 'crud.actions.delete': 'Excluir',
306
+ },
307
+ 'en-US': {
308
+ 'crud.emptyState.title': 'Connect CRUD to a resource',
309
+ 'crud.emptyState.description': 'Provide metadata (resourcePath / schema) or metadata.data to enable the table and actions.',
310
+ 'crud.emptyState.primaryAction': 'Configure metadata',
311
+ 'crud.preferences.resetSuccess': 'CRUD overrides reset',
312
+ 'crud.actions.create': 'Add',
313
+ 'crud.actions.view': 'View',
314
+ 'crud.actions.edit': 'Edit',
315
+ 'crud.actions.delete': 'Delete',
316
+ },
317
+ },
318
+ },
319
+ };
320
+ function getCrudRuntimeDictionary(locale) {
321
+ const dictionaries = PRAXIS_CRUD_RUNTIME_I18N_CONFIG.namespaces?.[PRAXIS_CRUD_RUNTIME_I18N_NAMESPACE];
322
+ if (!dictionaries) {
323
+ return undefined;
324
+ }
325
+ const normalized = String(locale || '').trim();
326
+ const candidates = [
327
+ normalized,
328
+ normalized.toLowerCase(),
329
+ normalized.includes('-') ? normalized.split('-')[0] : '',
330
+ normalized.includes('_') ? normalized.split('_')[0] : '',
331
+ 'pt-BR',
332
+ 'pt-br',
333
+ 'en-US',
334
+ 'en-us',
335
+ ].filter(Boolean);
336
+ for (const candidate of candidates) {
337
+ const dictionary = dictionaries[candidate];
338
+ if (dictionary && typeof dictionary === 'object') {
339
+ return dictionary;
340
+ }
341
+ }
342
+ return undefined;
343
+ }
344
+ function resolveCrudRuntimeFallback(key, locale) {
345
+ return getCrudRuntimeDictionary(locale || 'pt-BR')?.[key];
346
+ }
347
+ function translateCrudRuntimeText(i18n, key, fallback, params, locale) {
348
+ const runtimeFallback = resolveCrudRuntimeFallback(key, locale)
349
+ || resolveCrudRuntimeFallback(key, locale || i18n.getLocale?.() || i18n.getFallbackLocale?.() || 'pt-BR')
350
+ || fallback
351
+ || key;
352
+ return i18n.t(key, params, runtimeFallback, PRAXIS_CRUD_RUNTIME_I18N_NAMESPACE);
353
+ }
354
+
293
355
  class PraxisCrudComponent {
294
- /** JSON inline ou chave/URL resolvida pelo MetadataResolver */
295
356
  metadata;
296
- /** Identificador obrigatório do CRUD (base para tabela/formulário) */
297
357
  crudId;
298
- /** Identificador opcional para instâncias múltiplas */
299
358
  componentInstanceId;
300
359
  context;
301
- /** Encaminha o modo de edição de layout para a tabela interna */
302
360
  enableCustomization = false;
303
- /** CTA: usado pelo Builder para abrir configuração de metadados quando vazio */
304
361
  configureRequested = new EventEmitter();
305
362
  afterOpen = new EventEmitter();
306
363
  afterClose = new EventEmitter();
307
364
  afterSave = new EventEmitter();
308
365
  afterDelete = new EventEmitter();
309
366
  error = new EventEmitter();
310
- /**
311
- * Emits the live PraxisTable configuration snapshot after runtime hydration or
312
- * metadata refreshes. This reflects the effective table state in memory,
313
- * deduplicated by structural JSON equality, and is intended for external hosts
314
- * that need to mirror the table editor/runtime state.
315
- */
316
367
  tableRuntimeConfigChange = new EventEmitter();
317
368
  resolvedMetadata;
318
- /** Configuração efetiva da tabela com melhorias automáticas (ex.: botão Adicionar). */
319
369
  effectiveTableConfig;
320
- /** Config passado ao PraxisTable — sempre definido (fallback seguro). */
321
370
  tableConfigForBinding = createDefaultTableConfig();
371
+ tableQueryContext = null;
372
+ tableFilterCriteria = {};
373
+ tableCrudContext;
322
374
  launcher = inject(CrudLauncherService);
323
375
  destroyRef = inject(DestroyRef);
376
+ cdr = inject(ChangeDetectorRef);
324
377
  table;
325
378
  storage = inject(ASYNC_CONFIG_STORAGE);
326
379
  snack = inject(MatSnackBar);
327
- global = (() => { try {
328
- return inject(GlobalConfigService);
329
- }
330
- catch {
331
- return undefined;
332
- } })();
380
+ i18n = inject(PraxisI18nService);
381
+ resourceDiscovery = inject(ResourceDiscoveryService);
382
+ actionOpenAdapter = inject(ResourceActionOpenAdapterService);
383
+ surfaceOpenAdapter = inject(ResourceSurfaceOpenAdapterService);
384
+ global = (() => {
385
+ try {
386
+ return inject(GlobalConfigService);
387
+ }
388
+ catch {
389
+ return undefined;
390
+ }
391
+ })();
392
+ surfaceService = inject(GLOBAL_SURFACE_SERVICE, { optional: true });
333
393
  componentKeys = inject(ComponentKeyService);
334
- route = (() => { try {
335
- return inject(ActivatedRoute);
336
- }
337
- catch {
338
- return undefined;
339
- } })();
394
+ route = (() => {
395
+ try {
396
+ return inject(ActivatedRoute);
397
+ }
398
+ catch {
399
+ return undefined;
400
+ }
401
+ })();
340
402
  warnedMissingId = false;
341
403
  lastEmittedTableRuntimeConfigJson = '';
342
- /**
343
- * Stable CRUD context passed to PraxisTable.
344
- * Previously this was created via a getter, producing a new object each CD tick
345
- * and causing excessive re-renders. Now we compute it only when metadata changes.
346
- */
347
- tableCrudContext;
404
+ lastAssignedTableConfigJson = '';
405
+ lastAssignedTableCrudContextJson = '';
406
+ lastAppliedResourceIdentity = null;
407
+ tableCollectionLinks = null;
408
+ collectionCapabilities = null;
409
+ collectionCapabilitiesRequestHref = null;
410
+ collectionCapabilitiesResolvedHref = null;
411
+ collectionCapabilitiesRequestSeq = 0;
348
412
  onResetPreferences() {
349
413
  try {
350
414
  const keyId = this.componentKeyId();
@@ -352,44 +416,51 @@ class PraxisCrudComponent {
352
416
  return;
353
417
  const key = `crud-overrides:${keyId}`;
354
418
  void firstValueFrom(this.storage.clearConfig(key)).catch(() => { });
355
- this.snack.open('Overrides de CRUD redefinidos', undefined, { duration: 2000 });
419
+ this.snack.open(this.tx('crud.preferences.resetSuccess', 'CRUD overrides reset'), undefined, {
420
+ duration: 2000,
421
+ });
356
422
  }
357
423
  catch { }
358
424
  }
359
425
  ngOnChanges(changes) {
360
- if (changes['metadata']) {
361
- try {
362
- const parsed = typeof this.metadata === 'string'
363
- ? JSON.parse(this.metadata)
364
- : this.metadata;
365
- this.resolvedMetadata = parsed;
366
- // Runtime metadata must stay structurally valid, but route/formId can be
367
- // completed later by launcher overrides, defaults or host policy.
368
- assertCrudMetadata(this.resolvedMetadata, {
369
- allowDeferredActionBindings: true,
370
- });
371
- this.effectiveTableConfig = this.buildEffectiveTableConfig(this.resolvedMetadata);
372
- // Evitar passar undefined para [config], o que sobrescreve o default do PraxisTable
373
- this.tableConfigForBinding =
374
- this.effectiveTableConfig ||
375
- (this.resolvedMetadata.table ??
376
- createDefaultTableConfig());
377
- // Build a stable table context when metadata changes
378
- this.tableCrudContext = this.buildTableCrudContext(this.resolvedMetadata);
379
- }
380
- catch (err) {
381
- this.error.emit(err);
382
- }
426
+ if (!changes['metadata']) {
427
+ return;
428
+ }
429
+ try {
430
+ const parsed = typeof this.metadata === 'string'
431
+ ? JSON.parse(this.metadata)
432
+ : this.metadata;
433
+ this.resolvedMetadata = parsed;
434
+ assertCrudMetadata(this.resolvedMetadata, {
435
+ allowDeferredActionBindings: true,
436
+ });
437
+ this.tableQueryContext = this.resolveQueryContext(this.resolvedMetadata);
438
+ this.tableFilterCriteria = this.resolveFilterCriteria(this.resolvedMetadata);
439
+ this.tableCollectionLinks = null;
440
+ this.collectionCapabilities = null;
441
+ this.collectionCapabilitiesRequestHref = null;
442
+ this.collectionCapabilitiesResolvedHref = null;
443
+ this.collectionCapabilitiesRequestSeq += 1;
444
+ this.applyResolvedCrudState(this.resolvedMetadata);
445
+ }
446
+ catch (err) {
447
+ this.error.emit(err);
383
448
  }
384
449
  }
385
- async onAction(action, row) {
450
+ async onAction(action, row, runtimeEvent) {
386
451
  try {
387
452
  document.activeElement?.blur();
388
- let actionMeta = this.resolvedMetadata.actions?.find((a) => a.action === action);
389
- // Fallback: if metadata.actions is missing or doesn't include the action,
390
- // synthesize from table CRUD context or let overrides drive behavior.
453
+ const openedByDiscovery = await this.tryOpenDiscoveredCrudSurface(action, row);
454
+ if (openedByDiscovery) {
455
+ return;
456
+ }
457
+ const handledByWorkflowAction = await this.tryOpenDiscoveredWorkflowAction(action, row, runtimeEvent);
458
+ if (handledByWorkflowAction) {
459
+ return;
460
+ }
461
+ let actionMeta = this.resolvedMetadata.actions?.find((candidate) => candidate.action === action);
391
462
  if (!actionMeta) {
392
- const ctxAction = this.tableCrudContext?.actions?.find((a) => a.action === action);
463
+ const ctxAction = this.tableCrudContext?.actions?.find((candidate) => candidate.action === action);
393
464
  if (ctxAction) {
394
465
  actionMeta = {
395
466
  action: ctxAction.action,
@@ -399,7 +470,6 @@ class PraxisCrudComponent {
399
470
  };
400
471
  }
401
472
  else {
402
- // Minimal stub – CrudLauncherService will merge overrides/defaults
403
473
  actionMeta = { action };
404
474
  }
405
475
  }
@@ -412,9 +482,7 @@ class PraxisCrudComponent {
412
482
  this.afterClose.emit();
413
483
  };
414
484
  const { mode, ref } = await this.launcher.launch(effectiveAction, row, this.resolvedMetadata, this.componentKeyId(), {
415
- onClose: () => {
416
- emitDrawerClose();
417
- },
485
+ onClose: () => emitDrawerClose(),
418
486
  onResult: (result) => {
419
487
  emitDrawerClose();
420
488
  const data = (result?.data || {});
@@ -432,35 +500,33 @@ class PraxisCrudComponent {
432
500
  },
433
501
  });
434
502
  this.afterOpen.emit({ mode, action: effectiveAction.action });
435
- if (ref) {
436
- const idField = this.getIdField();
437
- ref
438
- .afterClosed()
439
- .pipe(takeUntilDestroyed(this.destroyRef))
440
- .subscribe((result) => {
441
- this.afterClose.emit();
442
- if (result?.type === 'save') {
443
- const data = result.data;
444
- const id = data?.[idField];
445
- this.afterSave.emit({ id, data: result.data });
446
- this.refreshTable();
447
- }
448
- if (result?.type === 'delete') {
449
- const data = result.data;
450
- const id = data?.[idField];
451
- this.afterDelete.emit({ id });
452
- this.refreshTable();
453
- }
454
- });
503
+ if (!ref) {
504
+ return;
455
505
  }
506
+ const idField = this.getIdField();
507
+ ref
508
+ .afterClosed()
509
+ .pipe(takeUntilDestroyed(this.destroyRef))
510
+ .subscribe((result) => {
511
+ this.afterClose.emit();
512
+ if (result?.type === 'save') {
513
+ const data = result.data;
514
+ const id = data?.[idField];
515
+ this.afterSave.emit({ id, data: result.data });
516
+ this.refreshTable();
517
+ }
518
+ if (result?.type === 'delete') {
519
+ const data = result.data;
520
+ const id = data?.[idField];
521
+ this.afterDelete.emit({ id });
522
+ this.refreshTable();
523
+ }
524
+ });
456
525
  }
457
526
  catch (err) {
458
527
  this.error.emit(err);
459
528
  }
460
529
  }
461
- refreshTable() {
462
- this.table.refetch();
463
- }
464
530
  getCurrentTableConfigSnapshot() {
465
531
  const current = this.table?.config || this.tableConfigForBinding || this.effectiveTableConfig;
466
532
  if (!current)
@@ -483,20 +549,50 @@ class PraxisCrudComponent {
483
549
  }
484
550
  this.emitTableRuntimeConfigSnapshot();
485
551
  }
486
- emitTableRuntimeConfigSnapshot() {
487
- const snapshot = this.getCurrentTableConfigSnapshot();
488
- if (!snapshot)
552
+ onCollectionLinksChange(links) {
553
+ this.tableCollectionLinks = links || null;
554
+ const capabilitiesHref = this.resolveCollectionCapabilitiesHref(links);
555
+ if (!capabilitiesHref) {
556
+ this.collectionCapabilitiesRequestSeq += 1;
557
+ this.collectionCapabilitiesRequestHref = null;
558
+ this.collectionCapabilitiesResolvedHref = null;
559
+ if (this.collectionCapabilities) {
560
+ this.collectionCapabilities = null;
561
+ this.applyResolvedCrudState(this.resolvedMetadata);
562
+ this.syncRuntimeBindings();
563
+ }
489
564
  return;
490
- try {
491
- const nextJson = JSON.stringify(snapshot);
492
- if (nextJson === this.lastEmittedTableRuntimeConfigJson)
493
- return;
494
- this.lastEmittedTableRuntimeConfigJson = nextJson;
495
565
  }
496
- catch {
497
- // Fallback to emitting when serialization is not possible.
566
+ if (capabilitiesHref === this.collectionCapabilitiesRequestHref ||
567
+ (capabilitiesHref === this.collectionCapabilitiesResolvedHref && !!this.collectionCapabilities)) {
568
+ return;
498
569
  }
499
- this.tableRuntimeConfigChange.emit(snapshot);
570
+ const requestSeq = ++this.collectionCapabilitiesRequestSeq;
571
+ this.collectionCapabilitiesRequestHref = capabilitiesHref;
572
+ void firstValueFrom(this.resourceDiscovery.getCapabilities(links || {}, this.buildDiscoveryOptions()))
573
+ .then((snapshot) => {
574
+ if (requestSeq !== this.collectionCapabilitiesRequestSeq) {
575
+ return;
576
+ }
577
+ this.collectionCapabilitiesRequestHref = null;
578
+ this.collectionCapabilitiesResolvedHref = capabilitiesHref;
579
+ this.collectionCapabilities = snapshot;
580
+ this.applyResolvedCrudState(this.resolvedMetadata);
581
+ this.syncRuntimeBindings();
582
+ })
583
+ .catch(() => {
584
+ if (requestSeq !== this.collectionCapabilitiesRequestSeq) {
585
+ return;
586
+ }
587
+ const hadCapabilities = !!this.collectionCapabilities;
588
+ this.collectionCapabilities = null;
589
+ this.collectionCapabilitiesRequestHref = null;
590
+ this.collectionCapabilitiesResolvedHref = null;
591
+ if (hadCapabilities) {
592
+ this.applyResolvedCrudState(this.resolvedMetadata);
593
+ this.syncRuntimeBindings();
594
+ }
595
+ });
500
596
  }
501
597
  resolveResourcePath(meta) {
502
598
  return (meta?.resource?.path ||
@@ -516,73 +612,462 @@ class PraxisCrudComponent {
516
612
  shouldRenderTable(meta) {
517
613
  return this.resolveResourcePath(meta).length > 0 || Array.isArray(meta?.data);
518
614
  }
519
- getIdField() {
520
- return this.resolvedMetadata?.resource?.idField || 'id';
615
+ getEmptyStateTitle() {
616
+ return this.tx('crud.emptyState.title', 'Connect CRUD to a resource');
617
+ }
618
+ getEmptyStateDescription() {
619
+ return this.tx('crud.emptyState.description', 'Provide metadata (resourcePath / schema) or metadata.data to enable the table and actions.');
620
+ }
621
+ getEmptyStatePrimaryAction() {
622
+ return this.tx('crud.emptyState.primaryAction', 'Configure metadata');
521
623
  }
522
624
  onConfigureRequested() {
523
625
  this.configureRequested.emit();
524
626
  }
525
- /**
526
- * Constrói uma configuração de tabela efetiva a partir dos metadados,
527
- * adicionando automaticamente a ação de "Adicionar" na toolbar quando aplicável.
528
- * - Evita duplicidade se a toolbar já tiver ação equivalente.
529
- * - Garante visibilidade da toolbar quando a ação é injetada.
530
- */
531
- buildEffectiveTableConfig(meta) {
627
+ refreshTable() {
628
+ this.table.refetch();
629
+ }
630
+ emitTableRuntimeConfigSnapshot() {
631
+ const snapshot = this.getCurrentTableConfigSnapshot();
632
+ if (!snapshot)
633
+ return;
634
+ try {
635
+ const nextJson = JSON.stringify(snapshot);
636
+ if (nextJson === this.lastEmittedTableRuntimeConfigJson)
637
+ return;
638
+ this.lastEmittedTableRuntimeConfigJson = nextJson;
639
+ }
640
+ catch { }
641
+ this.tableRuntimeConfigChange.emit(snapshot);
642
+ }
643
+ applyResolvedCrudState(meta) {
644
+ this.effectiveTableConfig = this.buildEffectiveTableConfig(meta, this.collectionCapabilities);
645
+ const nextTableConfig = this.effectiveTableConfig ||
646
+ (meta.table ?? createDefaultTableConfig());
647
+ this.assignTableConfigForBinding(nextTableConfig);
648
+ this.assignTableCrudContext(this.buildTableCrudContext(meta, this.collectionCapabilities));
649
+ this.lastAppliedResourceIdentity = this.resolveCrudResourceIdentity(meta);
650
+ }
651
+ assignTableConfigForBinding(next) {
652
+ const nextJson = this.safeSerialize(next);
653
+ if (nextJson !== null && nextJson === this.lastAssignedTableConfigJson) {
654
+ return;
655
+ }
656
+ this.lastAssignedTableConfigJson = nextJson ?? '';
657
+ this.tableConfigForBinding = next;
658
+ }
659
+ assignTableCrudContext(next) {
660
+ const nextJson = this.safeSerialize(next);
661
+ if (nextJson !== null && nextJson === this.lastAssignedTableCrudContextJson) {
662
+ return;
663
+ }
664
+ this.lastAssignedTableCrudContextJson = nextJson ?? '';
665
+ this.tableCrudContext = next;
666
+ }
667
+ safeSerialize(value) {
668
+ try {
669
+ return JSON.stringify(value);
670
+ }
671
+ catch {
672
+ return null;
673
+ }
674
+ }
675
+ syncRuntimeBindings() {
676
+ try {
677
+ this.cdr.detectChanges();
678
+ }
679
+ catch {
680
+ try {
681
+ this.cdr.markForCheck();
682
+ }
683
+ catch { }
684
+ }
685
+ }
686
+ resolveQueryContext(meta) {
687
+ return this.isRecord(meta?.queryContext) ? meta.queryContext : null;
688
+ }
689
+ resolveFilterCriteria(meta) {
690
+ return this.isRecord(meta?.filterCriteria) ? { ...meta.filterCriteria } : {};
691
+ }
692
+ async tryOpenDiscoveredCrudSurface(action, row) {
693
+ const normalizedAction = String(action || '').trim().toLowerCase();
694
+ if (!this.isDiscoveryManagedCrudAction(normalizedAction) || !this.surfaceService) {
695
+ return false;
696
+ }
697
+ const catalog = await this.resolveDiscoveredSurfaceCatalog(normalizedAction, row);
698
+ const surface = this.selectSurfaceForCrudAction(normalizedAction, catalog?.surfaces || []);
699
+ const resourcePath = String(catalog?.resourcePath || this.resolveResourcePath(this.resolvedMetadata) || '').trim();
700
+ if (!catalog || !surface || !resourcePath) {
701
+ return false;
702
+ }
703
+ let payload;
704
+ try {
705
+ payload = this.surfaceOpenAdapter.toPayload(surface, {
706
+ resourcePath,
707
+ resourceId: this.resolveRowResourceId(row),
708
+ endpointKey: this.resolvedMetadata?.resource?.endpointKey,
709
+ apiUrlEntry: this.resolveDiscoveryApiEntry(),
710
+ group: catalog.group ?? null,
711
+ });
712
+ }
713
+ catch {
714
+ return false;
715
+ }
716
+ let openPromise;
717
+ try {
718
+ openPromise = Promise.resolve(this.surfaceService.open(payload, {
719
+ sourceId: this.componentKeyId() || undefined,
720
+ payload: { action: normalizedAction, row: row ?? null },
721
+ meta: {
722
+ crudId: this.crudId,
723
+ resourcePath,
724
+ surfaceId: surface.id,
725
+ operationId: surface.operationId,
726
+ },
727
+ runtime: {
728
+ row: row ?? null,
729
+ item: row ?? null,
730
+ state: {
731
+ action: normalizedAction,
732
+ resourcePath,
733
+ },
734
+ },
735
+ }));
736
+ }
737
+ catch {
738
+ return false;
739
+ }
740
+ this.afterOpen.emit({
741
+ mode: this.mapSurfacePresentationToCrudMode(payload?.presentation),
742
+ action,
743
+ });
744
+ void openPromise
745
+ .then((result) => this.handleDiscoveredSurfaceResult(result))
746
+ .catch((err) => this.error.emit(err));
747
+ return true;
748
+ }
749
+ async resolveDiscoveredSurfaceCatalog(action, row) {
750
+ try {
751
+ if (action === 'create') {
752
+ if (!this.tableCollectionLinks) {
753
+ return null;
754
+ }
755
+ return await firstValueFrom(this.resourceDiscovery.getSurfaces(this.tableCollectionLinks, this.buildDiscoveryOptions()));
756
+ }
757
+ const rowLinks = row?._links;
758
+ if (!rowLinks) {
759
+ return null;
760
+ }
761
+ return await firstValueFrom(this.resourceDiscovery.getSurfaces(rowLinks, this.buildDiscoveryOptions()));
762
+ }
763
+ catch {
764
+ return null;
765
+ }
766
+ }
767
+ async tryOpenDiscoveredWorkflowAction(action, row, runtimeEvent) {
768
+ const normalizedAction = String(action || '').trim().toLowerCase();
769
+ if (!normalizedAction || this.isDiscoveryManagedCrudAction(normalizedAction) || !this.surfaceService) {
770
+ return false;
771
+ }
772
+ const providedAction = this.resolveProvidedWorkflowAction(normalizedAction, runtimeEvent?.actionConfig);
773
+ const catalog = providedAction ? null : await this.resolveDiscoveredActionCatalog(row);
774
+ const discoveredAction = providedAction || this.selectDiscoveredWorkflowAction(normalizedAction, catalog?.actions || []);
775
+ const resourcePath = String(catalog?.resourcePath || this.resolveResourcePath(this.resolvedMetadata) || '').trim();
776
+ if (!discoveredAction || !resourcePath) {
777
+ return false;
778
+ }
779
+ if (discoveredAction.availability?.allowed === false) {
780
+ this.snack.open(translateUnavailableWorkflowMessage(this.i18n, discoveredAction.availability), undefined, { duration: 2500 });
781
+ return true;
782
+ }
783
+ let payload;
784
+ try {
785
+ payload = this.actionOpenAdapter.toPayload(discoveredAction, {
786
+ resourcePath,
787
+ resourceId: this.resolveRowResourceId(row),
788
+ endpointKey: this.resolvedMetadata?.resource?.endpointKey,
789
+ apiUrlEntry: this.resolveDiscoveryApiEntry(),
790
+ group: catalog?.group ?? null,
791
+ });
792
+ }
793
+ catch {
794
+ return false;
795
+ }
796
+ let openPromise;
797
+ try {
798
+ openPromise = Promise.resolve(this.surfaceService.open(payload, {
799
+ sourceId: this.componentKeyId() || undefined,
800
+ payload: { action: normalizedAction, row: row ?? null },
801
+ meta: {
802
+ crudId: this.crudId,
803
+ resourcePath,
804
+ actionId: discoveredAction.id,
805
+ operationId: discoveredAction.operationId,
806
+ },
807
+ runtime: {
808
+ row: row ?? null,
809
+ item: row ?? null,
810
+ state: {
811
+ action: normalizedAction,
812
+ resourcePath,
813
+ },
814
+ },
815
+ }));
816
+ }
817
+ catch {
818
+ return false;
819
+ }
820
+ this.afterOpen.emit({
821
+ mode: this.mapSurfacePresentationToCrudMode(payload?.presentation),
822
+ action,
823
+ });
824
+ void openPromise
825
+ .then((result) => this.handleDiscoveredSurfaceResult(result))
826
+ .catch((err) => this.error.emit(err));
827
+ return true;
828
+ }
829
+ async resolveDiscoveredActionCatalog(row) {
830
+ try {
831
+ if (row) {
832
+ const rowLinks = row?._links;
833
+ if (!rowLinks) {
834
+ return null;
835
+ }
836
+ return await firstValueFrom(this.resourceDiscovery.getActions(rowLinks, this.buildDiscoveryOptions()));
837
+ }
838
+ if (!this.tableCollectionLinks) {
839
+ return null;
840
+ }
841
+ return await firstValueFrom(this.resourceDiscovery.getActions(this.tableCollectionLinks, this.buildDiscoveryOptions()));
842
+ }
843
+ catch {
844
+ return null;
845
+ }
846
+ }
847
+ selectDiscoveredWorkflowAction(action, actions) {
848
+ const normalizedAction = String(action || '').trim().toLowerCase();
849
+ return (actions
850
+ .slice()
851
+ .sort((left, right) => (left.order ?? 0) - (right.order ?? 0))
852
+ .find((candidate) => String(candidate.id || '').trim().toLowerCase() === normalizedAction) ||
853
+ null);
854
+ }
855
+ resolveProvidedWorkflowAction(action, candidate) {
856
+ if (!candidate || typeof candidate !== 'object') {
857
+ return null;
858
+ }
859
+ const normalizedId = String(candidate.id || '').trim().toLowerCase();
860
+ if (!normalizedId || normalizedId !== action) {
861
+ return null;
862
+ }
863
+ if (!candidate.operationId || !candidate.path || !candidate.method) {
864
+ return null;
865
+ }
866
+ return candidate;
867
+ }
868
+ selectSurfaceForCrudAction(action, surfaces) {
869
+ const candidates = surfaces
870
+ .filter((surface) => surface.availability?.allowed !== false)
871
+ .sort((left, right) => (left.order ?? 0) - (right.order ?? 0));
872
+ const preferredIds = this.getPreferredSurfaceIdsForCrudAction(action);
873
+ const preferred = candidates.find((surface) => preferredIds.includes(String(surface.id || '').trim().toLowerCase()));
874
+ if (preferred) {
875
+ return preferred;
876
+ }
877
+ if (action === 'create') {
878
+ return (candidates.find((surface) => surface.scope === 'COLLECTION' &&
879
+ this.isWritableCrudSurface(surface) &&
880
+ String(surface.method || '').toUpperCase() === 'POST') ||
881
+ candidates.find((surface) => surface.scope === 'COLLECTION' && this.isWritableCrudSurface(surface)) ||
882
+ null);
883
+ }
884
+ if (action === 'view') {
885
+ return (candidates.find((surface) => surface.scope === 'ITEM' &&
886
+ this.isReadableCrudSurface(surface) &&
887
+ String(surface.method || '').toUpperCase() === 'GET') ||
888
+ candidates.find((surface) => surface.scope === 'ITEM' && this.isReadableCrudSurface(surface)) ||
889
+ null);
890
+ }
891
+ if (action === 'edit') {
892
+ return (candidates.find((surface) => surface.scope === 'ITEM' &&
893
+ this.isWritableCrudSurface(surface) &&
894
+ ['PUT', 'PATCH'].includes(String(surface.method || '').toUpperCase())) ||
895
+ candidates.find((surface) => surface.scope === 'ITEM' && this.isWritableCrudSurface(surface)) ||
896
+ null);
897
+ }
898
+ return null;
899
+ }
900
+ getPreferredSurfaceIdsForCrudAction(action) {
901
+ switch (action) {
902
+ case 'create':
903
+ return ['create'];
904
+ case 'view':
905
+ return ['detail', 'view'];
906
+ case 'edit':
907
+ return ['edit', 'update'];
908
+ default:
909
+ return [];
910
+ }
911
+ }
912
+ isDiscoveryManagedCrudAction(action) {
913
+ return action === 'create' || action === 'view' || action === 'edit';
914
+ }
915
+ isWritableCrudSurface(surface) {
916
+ return surface.kind === 'FORM' || surface.kind === 'PARTIAL_FORM';
917
+ }
918
+ isReadableCrudSurface(surface) {
919
+ return surface.kind === 'VIEW' || surface.kind === 'READ_PROJECTION';
920
+ }
921
+ resolveRowResourceId(row) {
922
+ if (!row) {
923
+ return null;
924
+ }
925
+ const value = row[this.getIdField()];
926
+ return typeof value === 'string' || typeof value === 'number' ? value : null;
927
+ }
928
+ mapSurfacePresentationToCrudMode(presentation) {
929
+ return presentation === 'modal' ? 'modal' : 'drawer';
930
+ }
931
+ handleDiscoveredSurfaceResult(result) {
932
+ this.afterClose.emit();
933
+ const data = (result?.data || {});
934
+ const idField = this.getIdField();
935
+ if (result?.type === 'save') {
936
+ const id = data?.[idField];
937
+ this.afterSave.emit({ id, data });
938
+ this.refreshTable();
939
+ }
940
+ if (result?.type === 'delete') {
941
+ const id = data?.[idField];
942
+ this.afterDelete.emit({ id });
943
+ this.refreshTable();
944
+ }
945
+ }
946
+ buildEffectiveTableConfig(meta, capabilities) {
532
947
  const base = meta.table;
533
- // Clonar base ou criar default para permitir injeção de melhorias
948
+ const current = this.getCurrentTableConfigSnapshot();
949
+ const nextResourceIdentity = this.resolveCrudResourceIdentity(meta);
534
950
  const cfg = base
535
951
  ? JSON.parse(JSON.stringify(base))
536
952
  : createDefaultTableConfig();
537
953
  let changed = false;
954
+ if (nextResourceIdentity !== null &&
955
+ nextResourceIdentity === this.lastAppliedResourceIdentity &&
956
+ (cfg.columns?.length ?? 0) === 0 &&
957
+ (current?.columns?.length ?? 0) > 0) {
958
+ cfg.columns = JSON.parse(JSON.stringify(current.columns));
959
+ changed = true;
960
+ }
961
+ if (Array.isArray(meta?.data) && this.resolveResourcePath(meta).length === 0) {
962
+ const behavior = cfg.behavior || {};
963
+ const localDataMode = behavior.localDataMode || {};
964
+ if (localDataMode.enabled !== true) {
965
+ cfg.behavior = {
966
+ ...behavior,
967
+ localDataMode: {
968
+ ...localDataMode,
969
+ enabled: true,
970
+ },
971
+ };
972
+ changed = true;
973
+ }
974
+ }
538
975
  const ensureToolbar = () => {
539
976
  if (!cfg.toolbar) {
540
977
  cfg.toolbar = { visible: true, position: 'top' };
541
978
  }
542
979
  return cfg.toolbar;
543
980
  };
544
- // 1) Toolbar: injetar ação "Adicionar" se metadata declara create/add e toolbar não tiver
545
- const hasToolbarAdd = (cfg.toolbar?.actions || []).some((a) => this.isAddLike(a));
546
- const addAction = (meta.actions || []).find((a) => this.isAddLike(a));
981
+ const hasToolbarAdd = (cfg.toolbar?.actions || []).some((action) => this.isAddLike(action));
982
+ const addAction = this.resolveCreateToolbarAction(meta, capabilities);
547
983
  if (addAction) {
548
984
  if (hasToolbarAdd) {
549
- const tb = ensureToolbar();
550
- tb.visible = true; // garantir visibilidade
985
+ const toolbar = ensureToolbar();
986
+ toolbar.visible = true;
551
987
  }
552
988
  else {
553
- const tb = ensureToolbar();
554
- tb.visible = true;
555
- if (!tb.actions)
556
- tb.actions = [];
557
- const injected = {
989
+ const toolbar = ensureToolbar();
990
+ toolbar.visible = true;
991
+ if (!toolbar.actions) {
992
+ toolbar.actions = [];
993
+ }
994
+ toolbar.actions.push({
558
995
  id: addAction.id || 'add',
559
- label: addAction.label || 'Adicionar',
996
+ label: addAction.label || this.getCrudActionLabel('create'),
560
997
  icon: addAction.icon || 'add',
561
998
  type: 'button',
562
999
  color: 'primary',
563
1000
  position: 'end',
564
1001
  action: addAction.action || 'add',
565
- };
566
- tb.actions.push(injected);
1002
+ });
567
1003
  }
568
1004
  changed = true;
569
1005
  }
570
- // 2) Row actions automáticas (apenas quando host não definiu explicitamente)
1006
+ const discoveredToolbarActions = this.resolveCollectionWorkflowToolbarActions(capabilities);
1007
+ if (discoveredToolbarActions.length) {
1008
+ const toolbar = ensureToolbar();
1009
+ toolbar.visible = true;
1010
+ if (!toolbar.actions) {
1011
+ toolbar.actions = [];
1012
+ }
1013
+ for (const action of discoveredToolbarActions) {
1014
+ const actionId = String(action.action || action.id || '')
1015
+ .trim()
1016
+ .toLowerCase();
1017
+ const alreadyExists = toolbar.actions.some((candidate) => String(candidate?.action || candidate?.id || candidate?.code || candidate?.key || '')
1018
+ .trim()
1019
+ .toLowerCase() === actionId);
1020
+ if (!alreadyExists) {
1021
+ toolbar.actions.push(action);
1022
+ changed = true;
1023
+ }
1024
+ }
1025
+ }
571
1026
  const hostDefinedRow = !!(base && base.actions && base.actions.row);
572
- const crudDefaults = this.global.get('crud.defaults') || {};
573
- const autoRow = crudDefaults.autoRowActions !== false; // default true
1027
+ const crudDefaults = this.global?.get('crud.defaults') || {};
1028
+ const autoRow = crudDefaults.autoRowActions !== false;
574
1029
  if (!hostDefinedRow && autoRow) {
575
- const acts = (meta.actions || []);
576
- const hasView = acts.some((a) => String(a.action).toLowerCase() === 'view');
577
- const hasEdit = acts.some((a) => String(a.action).toLowerCase() === 'edit');
578
- const includeDelete = !!crudDefaults.includeDeleteInRow && acts.some((a) => String(a.action).toLowerCase() === 'delete');
1030
+ const acts = this.resolveContextCrudActions(meta, capabilities);
1031
+ const hasView = capabilities
1032
+ ? this.supportsViewCapability(capabilities)
1033
+ : acts.some((action) => String(action.action).toLowerCase() === 'view');
1034
+ const hasEdit = capabilities
1035
+ ? this.supportsEditCapability(capabilities)
1036
+ : acts.some((action) => String(action.action).toLowerCase() === 'edit');
1037
+ const includeDelete = capabilities
1038
+ ? !!crudDefaults.includeDeleteInRow &&
1039
+ this.supportsDeleteCapability(capabilities) &&
1040
+ acts.some((action) => String(action.action).toLowerCase() === 'delete')
1041
+ : !!crudDefaults.includeDeleteInRow &&
1042
+ acts.some((action) => String(action.action).toLowerCase() === 'delete');
579
1043
  const rowActions = [];
580
- if (hasView)
581
- rowActions.push({ id: 'view', label: 'Ver', icon: 'visibility', action: 'view', tooltip: 'Ver' });
582
- if (hasEdit)
583
- rowActions.push({ id: 'edit', label: 'Editar', icon: 'edit', action: 'edit', tooltip: 'Editar' });
584
- if (includeDelete)
585
- rowActions.push({ id: 'delete', label: 'Excluir', icon: 'delete', action: 'delete', tooltip: 'Excluir' });
1044
+ if (hasView) {
1045
+ rowActions.push({
1046
+ id: 'view',
1047
+ label: this.getCrudActionLabel('view'),
1048
+ icon: 'visibility',
1049
+ action: 'view',
1050
+ tooltip: this.getCrudActionLabel('view'),
1051
+ });
1052
+ }
1053
+ if (hasEdit) {
1054
+ rowActions.push({
1055
+ id: 'edit',
1056
+ label: this.getCrudActionLabel('edit'),
1057
+ icon: 'edit',
1058
+ action: 'edit',
1059
+ tooltip: this.getCrudActionLabel('edit'),
1060
+ });
1061
+ }
1062
+ if (includeDelete) {
1063
+ rowActions.push({
1064
+ id: 'delete',
1065
+ label: this.getCrudActionLabel('delete'),
1066
+ icon: 'delete',
1067
+ action: 'delete',
1068
+ tooltip: this.getCrudActionLabel('delete'),
1069
+ });
1070
+ }
586
1071
  if (rowActions.length) {
587
1072
  cfg.actions = cfg.actions || {};
588
1073
  cfg.actions.row = {
@@ -597,53 +1082,197 @@ class PraxisCrudComponent {
597
1082
  changed = true;
598
1083
  }
599
1084
  }
600
- // Se nada foi alterado e havia base, não retornar para preservar semântica anterior
601
1085
  if (!changed) {
602
1086
  return base ? undefined : cfg;
603
1087
  }
604
1088
  return cfg;
605
1089
  }
606
- /** Heurística leve para identificar ações do tipo "adicionar/criar" */
607
- isAddLike(a) {
608
- if (!a)
1090
+ isAddLike(action) {
1091
+ if (!action)
609
1092
  return false;
610
- const normalize = (s) => String(s || '').trim().toLowerCase();
611
- const id = normalize(a.action || a.id || a.code || a.key || a.name || a.type);
612
- const icon = normalize(a.icon);
613
- const label = normalize(a.label);
1093
+ const normalize = (value) => String(value || '').trim().toLowerCase();
1094
+ const id = normalize(action.action || action.id || action.code || action.key || action.name || action.type);
1095
+ const icon = normalize(action.icon);
1096
+ const label = normalize(action.label);
614
1097
  const addIds = new Set(['add', 'create', 'novo', 'new', 'incluir', 'inserir']);
615
1098
  if (addIds.has(id))
616
1099
  return true;
617
1100
  if (icon === 'add' || icon === 'add_circle' || icon === 'add_box')
618
1101
  return true;
619
- if (label === 'adicionar' || label === 'novo' || label === 'criar' || label === 'incluir')
1102
+ if (label === 'adicionar' || label === 'novo' || label === 'criar' || label === 'incluir') {
620
1103
  return true;
1104
+ }
621
1105
  return false;
622
1106
  }
623
- /** Builds a stable CRUD context object for PraxisTable based on metadata. */
624
- buildTableCrudContext(meta) {
1107
+ buildTableCrudContext(meta, capabilities) {
625
1108
  if (!meta)
626
1109
  return undefined;
627
- // Provide a minimal default action set when metadata.actions is empty,
628
- // so the editor can expose per-action overrides entirely via UI.
629
- const baseActions = meta.actions && meta.actions.length
630
- ? meta.actions
631
- : [{ action: 'create' }, { action: 'view' }, { action: 'edit' }];
1110
+ const baseActions = this.resolveContextCrudActions(meta, capabilities);
632
1111
  return {
633
1112
  tableId: this.crudId || 'default',
634
1113
  componentKeyId: this.componentKeyId() || undefined,
635
1114
  resourcePath: this.resolveResourcePath(meta) || undefined,
1115
+ endpointKey: meta.resource?.endpointKey,
636
1116
  defaults: meta.defaults,
637
- actions: baseActions.map((a) => ({
638
- action: a.action,
639
- label: a.label,
640
- formId: a.formId,
641
- route: a.route,
642
- openMode: a.openMode,
1117
+ actions: baseActions.map((action) => ({
1118
+ action: action.action,
1119
+ label: action.label,
1120
+ formId: action.formId,
1121
+ route: action.route,
1122
+ openMode: action.openMode,
643
1123
  })),
644
1124
  idField: meta.resource?.idField || 'id',
645
1125
  };
646
1126
  }
1127
+ resolveCreateToolbarAction(meta, capabilities) {
1128
+ if (capabilities && !this.supportsCreateCapability(capabilities)) {
1129
+ return null;
1130
+ }
1131
+ const declared = (meta.actions || []).find((action) => this.isAddLike(action));
1132
+ if (declared) {
1133
+ return declared;
1134
+ }
1135
+ return capabilities ? { action: 'create', label: this.getCrudActionLabel('create') } : null;
1136
+ }
1137
+ resolveContextCrudActions(meta, capabilities) {
1138
+ const declaredActions = (meta.actions || []).filter(Boolean);
1139
+ if (!capabilities) {
1140
+ return declaredActions.length
1141
+ ? declaredActions
1142
+ : [
1143
+ { action: 'create', label: this.getCrudActionLabel('create') },
1144
+ { action: 'view', label: this.getCrudActionLabel('view') },
1145
+ { action: 'edit', label: this.getCrudActionLabel('edit') },
1146
+ ];
1147
+ }
1148
+ const actionMap = new Map(declaredActions.map((action) => [String(action.action || '').trim().toLowerCase(), action]));
1149
+ const runtimeActions = [];
1150
+ if (this.supportsCreateCapability(capabilities)) {
1151
+ runtimeActions.push(actionMap.get('create') || {
1152
+ action: 'create',
1153
+ label: this.getCrudActionLabel('create'),
1154
+ });
1155
+ }
1156
+ if (this.supportsViewCapability(capabilities)) {
1157
+ runtimeActions.push(actionMap.get('view') || {
1158
+ action: 'view',
1159
+ label: this.getCrudActionLabel('view'),
1160
+ });
1161
+ }
1162
+ if (this.supportsEditCapability(capabilities)) {
1163
+ runtimeActions.push(actionMap.get('edit') || {
1164
+ action: 'edit',
1165
+ label: this.getCrudActionLabel('edit'),
1166
+ });
1167
+ }
1168
+ if (this.supportsDeleteCapability(capabilities) && actionMap.has('delete')) {
1169
+ runtimeActions.push(actionMap.get('delete'));
1170
+ }
1171
+ for (const action of this.resolveCollectionWorkflowCrudActions(capabilities)) {
1172
+ const normalizedAction = String(action.action || '').trim().toLowerCase();
1173
+ if (!normalizedAction ||
1174
+ runtimeActions.some((candidate) => String(candidate.action || '').trim().toLowerCase() === normalizedAction)) {
1175
+ continue;
1176
+ }
1177
+ runtimeActions.push(action);
1178
+ }
1179
+ for (const action of declaredActions) {
1180
+ const normalizedAction = String(action.action || '').trim().toLowerCase();
1181
+ if (normalizedAction === 'create' ||
1182
+ normalizedAction === 'view' ||
1183
+ normalizedAction === 'edit' ||
1184
+ normalizedAction === 'delete') {
1185
+ continue;
1186
+ }
1187
+ runtimeActions.push(action);
1188
+ }
1189
+ return runtimeActions;
1190
+ }
1191
+ resolveCollectionWorkflowCrudActions(capabilities) {
1192
+ if (!capabilities) {
1193
+ return [];
1194
+ }
1195
+ return (capabilities.actions || [])
1196
+ .filter((action) => action.scope === 'COLLECTION')
1197
+ .sort((left, right) => (left.order ?? 0) - (right.order ?? 0))
1198
+ .map((action) => ({
1199
+ action: action.id,
1200
+ label: action.title,
1201
+ }));
1202
+ }
1203
+ resolveCollectionWorkflowToolbarActions(capabilities) {
1204
+ if (!capabilities) {
1205
+ return [];
1206
+ }
1207
+ return (capabilities.actions || [])
1208
+ .filter((action) => action.scope === 'COLLECTION')
1209
+ .sort((left, right) => (left.order ?? 0) - (right.order ?? 0))
1210
+ .map((action) => ({
1211
+ id: action.id,
1212
+ label: action.title || action.id,
1213
+ type: 'button',
1214
+ appearance: 'outlined',
1215
+ position: 'end',
1216
+ action: action.id,
1217
+ disabled: action.availability?.allowed === false,
1218
+ tooltip: action.description ||
1219
+ (action.availability?.allowed === false
1220
+ ? translateUnavailableWorkflowMessage(this.i18n, action.availability)
1221
+ : undefined),
1222
+ }));
1223
+ }
1224
+ supportsCreateCapability(snapshot) {
1225
+ return (this.hasCanonicalOperation(snapshot, 'create') ||
1226
+ snapshot.surfaces.some((surface) => surface.scope === 'COLLECTION' &&
1227
+ this.isWritableCrudSurface(surface) &&
1228
+ surface.availability?.allowed !== false));
1229
+ }
1230
+ supportsViewCapability(snapshot) {
1231
+ return this.hasCanonicalOperation(snapshot, 'byId');
1232
+ }
1233
+ supportsEditCapability(snapshot) {
1234
+ return this.hasCanonicalOperation(snapshot, 'update');
1235
+ }
1236
+ supportsDeleteCapability(snapshot) {
1237
+ return this.hasCanonicalOperation(snapshot, 'delete');
1238
+ }
1239
+ hasCanonicalOperation(snapshot, operation) {
1240
+ return snapshot.canonicalOperations?.[operation] === true;
1241
+ }
1242
+ resolveCollectionCapabilitiesHref(links) {
1243
+ try {
1244
+ return this.resourceDiscovery.resolveLinkHref(links || undefined, 'capabilities', this.buildDiscoveryOptions());
1245
+ }
1246
+ catch {
1247
+ return null;
1248
+ }
1249
+ }
1250
+ resolveCrudResourceIdentity(meta) {
1251
+ const resourcePath = meta ? this.resolveResourcePath(meta) : '';
1252
+ if (!resourcePath) {
1253
+ return null;
1254
+ }
1255
+ const endpointKey = String(meta?.resource?.endpointKey || '').trim().toLowerCase();
1256
+ return `${resourcePath.toLowerCase()}|${endpointKey}`;
1257
+ }
1258
+ buildDiscoveryOptions() {
1259
+ const endpointKey = this.resolvedMetadata?.resource?.endpointKey;
1260
+ return endpointKey ? { endpointKey: endpointKey } : undefined;
1261
+ }
1262
+ resolveDiscoveryApiEntry() {
1263
+ try {
1264
+ return this.resourceDiscovery.resolveApiEntry(this.buildDiscoveryOptions());
1265
+ }
1266
+ catch {
1267
+ return null;
1268
+ }
1269
+ }
1270
+ isRecord(value) {
1271
+ return !!value && typeof value === 'object' && !Array.isArray(value);
1272
+ }
1273
+ getIdField() {
1274
+ return this.resolvedMetadata?.resource?.idField || 'id';
1275
+ }
647
1276
  componentKeyId() {
648
1277
  const key = this.componentKeys.buildComponentId({
649
1278
  componentType: 'praxis-crud',
@@ -663,18 +1292,46 @@ class PraxisCrudComponent {
663
1292
  this.warnedMissingId = true;
664
1293
  console.warn('[PraxisCrud] crudId is required for config persistence.');
665
1294
  }
1295
+ getCrudActionLabel(action) {
1296
+ switch (action) {
1297
+ case 'create':
1298
+ return this.tx('crud.actions.create', 'Add');
1299
+ case 'view':
1300
+ return this.tx('crud.actions.view', 'View');
1301
+ case 'edit':
1302
+ return this.tx('crud.actions.edit', 'Edit');
1303
+ case 'delete':
1304
+ return this.tx('crud.actions.delete', 'Delete');
1305
+ default:
1306
+ return action;
1307
+ }
1308
+ }
1309
+ tx(key, fallback) {
1310
+ try {
1311
+ return translateCrudRuntimeText(this.i18n, key, fallback);
1312
+ }
1313
+ catch {
1314
+ return fallback;
1315
+ }
1316
+ }
666
1317
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisCrudComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
667
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisCrudComponent, isStandalone: true, selector: "praxis-crud", inputs: { metadata: "metadata", crudId: "crudId", componentInstanceId: "componentInstanceId", context: "context", enableCustomization: "enableCustomization" }, outputs: { configureRequested: "configureRequested", afterOpen: "afterOpen", afterClose: "afterClose", afterSave: "afterSave", afterDelete: "afterDelete", error: "error", tableRuntimeConfigChange: "tableRuntimeConfigChange" }, viewQueries: [{ propertyName: "table", first: true, predicate: PraxisTable, descendants: true }], usesOnChanges: true, ngImport: i0, template: `
1318
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisCrudComponent, isStandalone: true, selector: "praxis-crud", inputs: { metadata: "metadata", crudId: "crudId", componentInstanceId: "componentInstanceId", context: "context", enableCustomization: "enableCustomization" }, outputs: { configureRequested: "configureRequested", afterOpen: "afterOpen", afterClose: "afterClose", afterSave: "afterSave", afterDelete: "afterDelete", error: "error", tableRuntimeConfigChange: "tableRuntimeConfigChange" }, providers: [
1319
+ providePraxisI18nConfig(RESOURCE_DISCOVERY_I18N_CONFIG),
1320
+ providePraxisI18nConfig(PRAXIS_CRUD_RUNTIME_I18N_CONFIG),
1321
+ ], viewQueries: [{ propertyName: "table", first: true, predicate: PraxisTable, descendants: true }], usesOnChanges: true, ngImport: i0, template: `
668
1322
  @if (shouldRenderTable(resolvedMetadata)) {
669
1323
  <praxis-table
670
1324
  [config]="tableConfigForBinding"
671
1325
  [resourcePath]="resolveResourcePath(resolvedMetadata)"
672
1326
  [data]="resolveTableData(resolvedMetadata)"
1327
+ [queryContext]="tableQueryContext"
1328
+ [filterCriteria]="tableFilterCriteria"
673
1329
  [tableId]="crudId || 'default'"
674
1330
  [crudContext]="tableCrudContext"
675
1331
  [enableCustomization]="enableCustomization"
676
- (rowAction)="onAction($event.action, $event.row)"
1332
+ (rowAction)="onAction($event.action, $event.row, $event)"
677
1333
  (toolbarAction)="onAction($event.action)"
1334
+ (collectionLinksChange)="onCollectionLinksChange($event)"
678
1335
  (reset)="onResetPreferences()"
679
1336
  (metadataChange)="onTableMetadataChange()"
680
1337
  (loadingStateChange)="onTableLoadingStateChange($event)"
@@ -683,31 +1340,33 @@ class PraxisCrudComponent {
683
1340
  @if (enableCustomization) {
684
1341
  <praxis-empty-state-card
685
1342
  icon="table_rows"
686
- [title]="'Conecte o CRUD a um recurso'"
687
- [description]="'Informe os metadados (resourcePath / schema) ou forneça metadata.data para habilitar a tabela e ações.'"
688
- [primaryAction]="{ label: 'Configurar metadados', icon: 'bolt', action: onConfigureRequested.bind(this) }"
1343
+ [title]="getEmptyStateTitle()"
1344
+ [description]="getEmptyStateDescription()"
1345
+ [primaryAction]="{ label: getEmptyStatePrimaryAction(), icon: 'bolt', action: onConfigureRequested.bind(this) }"
689
1346
  />
690
1347
  }
691
1348
  }
692
- `, isInline: true, dependencies: [{ kind: "component", type: PraxisTable, selector: "praxis-table", inputs: ["config", "resourcePath", "data", "tableId", "componentInstanceId", "title", "subtitle", "icon", "autoDelete", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "crudContext", "dslFunctionRegistry", "filterCriteria", "queryContext", "enableCustomization", "dense"], outputs: ["rowClick", "rowDoubleClick", "rowExpansionChange", "rowAction", "toolbarAction", "bulkAction", "columnReorder", "columnReorderAttempt", "beforeDelete", "afterDelete", "deleteError", "beforeBulkDelete", "afterBulkDelete", "bulkDeleteError", "schemaStatusChange", "metadataChange", "loadingStateChange"] }, { kind: "component", type: EmptyStateCardComponent, selector: "praxis-empty-state-card", inputs: ["icon", "title", "description", "primaryAction", "secondaryActions", "inline", "tone"] }] });
1349
+ `, isInline: true, styles: [":host{display:block;width:100%;min-width:0;max-width:100%}\n"], dependencies: [{ kind: "component", type: PraxisTable, selector: "praxis-table", inputs: ["config", "resourcePath", "data", "tableId", "componentInstanceId", "title", "subtitle", "icon", "autoDelete", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "crudContext", "dslFunctionRegistry", "filterCriteria", "queryContext", "enableCustomization", "dense"], outputs: ["rowClick", "rowDoubleClick", "rowExpansionChange", "rowAction", "toolbarAction", "bulkAction", "columnReorder", "columnReorderAttempt", "beforeDelete", "afterDelete", "deleteError", "beforeBulkDelete", "afterBulkDelete", "bulkDeleteError", "schemaStatusChange", "metadataChange", "loadingStateChange", "collectionLinksChange"] }, { kind: "component", type: EmptyStateCardComponent, selector: "praxis-empty-state-card", inputs: ["icon", "title", "description", "primaryAction", "secondaryActions", "inline", "tone"] }] });
693
1350
  }
694
1351
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisCrudComponent, decorators: [{
695
1352
  type: Component,
696
- args: [{
697
- selector: 'praxis-crud',
698
- standalone: true,
699
- imports: [PraxisTable, EmptyStateCardComponent],
700
- template: `
1353
+ args: [{ selector: 'praxis-crud', standalone: true, imports: [PraxisTable, EmptyStateCardComponent], providers: [
1354
+ providePraxisI18nConfig(RESOURCE_DISCOVERY_I18N_CONFIG),
1355
+ providePraxisI18nConfig(PRAXIS_CRUD_RUNTIME_I18N_CONFIG),
1356
+ ], template: `
701
1357
  @if (shouldRenderTable(resolvedMetadata)) {
702
1358
  <praxis-table
703
1359
  [config]="tableConfigForBinding"
704
1360
  [resourcePath]="resolveResourcePath(resolvedMetadata)"
705
1361
  [data]="resolveTableData(resolvedMetadata)"
1362
+ [queryContext]="tableQueryContext"
1363
+ [filterCriteria]="tableFilterCriteria"
706
1364
  [tableId]="crudId || 'default'"
707
1365
  [crudContext]="tableCrudContext"
708
1366
  [enableCustomization]="enableCustomization"
709
- (rowAction)="onAction($event.action, $event.row)"
1367
+ (rowAction)="onAction($event.action, $event.row, $event)"
710
1368
  (toolbarAction)="onAction($event.action)"
1369
+ (collectionLinksChange)="onCollectionLinksChange($event)"
711
1370
  (reset)="onResetPreferences()"
712
1371
  (metadataChange)="onTableMetadataChange()"
713
1372
  (loadingStateChange)="onTableLoadingStateChange($event)"
@@ -716,14 +1375,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
716
1375
  @if (enableCustomization) {
717
1376
  <praxis-empty-state-card
718
1377
  icon="table_rows"
719
- [title]="'Conecte o CRUD a um recurso'"
720
- [description]="'Informe os metadados (resourcePath / schema) ou forneça metadata.data para habilitar a tabela e ações.'"
721
- [primaryAction]="{ label: 'Configurar metadados', icon: 'bolt', action: onConfigureRequested.bind(this) }"
1378
+ [title]="getEmptyStateTitle()"
1379
+ [description]="getEmptyStateDescription()"
1380
+ [primaryAction]="{ label: getEmptyStatePrimaryAction(), icon: 'bolt', action: onConfigureRequested.bind(this) }"
722
1381
  />
723
1382
  }
724
1383
  }
725
- `,
726
- }]
1384
+ `, styles: [":host{display:block;width:100%;min-width:0;max-width:100%}\n"] }]
727
1385
  }], propDecorators: { metadata: [{
728
1386
  type: Input,
729
1387
  args: [{ required: true }]
@@ -771,6 +1429,11 @@ class DynamicFormDialogHostComponent {
771
1429
  destroyRef = inject(DestroyRef);
772
1430
  resourcePath;
773
1431
  resourceId;
1432
+ schemaUrl;
1433
+ submitUrl;
1434
+ submitMethod;
1435
+ apiEndpointKey;
1436
+ apiUrlEntry;
774
1437
  mode = 'create';
775
1438
  backConfig;
776
1439
  idField = 'id';
@@ -815,6 +1478,14 @@ class DynamicFormDialogHostComponent {
815
1478
  }
816
1479
  this.idField = this.data.metadata?.resource?.idField ?? 'id';
817
1480
  this.resourceId = this.data.inputs?.[this.idField];
1481
+ this.schemaUrl = this.data.inputs?.['schemaUrl'] ?? null;
1482
+ this.submitUrl = this.data.inputs?.['submitUrl'] ?? null;
1483
+ this.submitMethod = this.data.inputs?.['submitMethod'] ?? null;
1484
+ this.apiEndpointKey =
1485
+ this.data.inputs?.['apiEndpointKey'] ??
1486
+ this.data.metadata?.resource?.endpointKey ??
1487
+ null;
1488
+ this.apiUrlEntry = this.data.inputs?.['apiUrlEntry'] ?? null;
818
1489
  const act = this.data.action?.action;
819
1490
  this.mode = act === 'edit' ? 'edit' : act === 'view' ? 'view' : 'create';
820
1491
  // Back config: defaults from metadata/action, overridden by saved per-form config
@@ -1004,13 +1675,18 @@ class DynamicFormDialogHostComponent {
1004
1675
  [resourcePath]="resourcePath"
1005
1676
  [resourceId]="resourceId"
1006
1677
  [mode]="mode"
1678
+ [schemaUrl]="schemaUrl"
1679
+ [submitUrl]="submitUrl"
1680
+ [submitMethod]="submitMethod"
1681
+ [apiEndpointKey]="apiEndpointKey"
1682
+ [apiUrlEntry]="apiUrlEntry"
1007
1683
  [presentationModeGlobal]="mode === 'view' ? true : null"
1008
1684
  [backConfig]="backConfig"
1009
1685
  (formSubmit)="onSave($event)"
1010
1686
  (formCancel)="onCancel()"
1011
1687
  ></praxis-dynamic-form>
1012
1688
  </mat-dialog-content>
1013
- `, isInline: true, styles: [":host{--dlg-header-h: 56px;--dlg-footer-h: 56px;--dlg-pad: 16px;display:flex;flex-direction:column;height:100%;overflow:hidden}:host([data-density=\"compact\"]){--dlg-header-h: 44px;--dlg-footer-h: 44px;--dlg-pad: 12px}.dialog-header{position:sticky;top:0;z-index:1;display:flex;align-items:center;gap:var(--dlg-pad);padding:0 var(--dlg-pad);height:var(--dlg-header-h);margin:0;background:var(--md-sys-color-surface-container-high);border-bottom:1px solid var(--md-sys-color-outline-variant);color:var(--md-sys-color-on-surface)}.dialog-title{margin:0;font:inherit;font-weight:600;color:var(--md-sys-color-on-surface)}.spacer{flex:1}.dialog-content{overflow:auto;padding:var(--dlg-pad);max-height:calc(100svh - var(--dlg-header-h) - 32px)}.dialog-header button.mat-icon-button{color:var(--md-sys-color-on-surface-variant)}.dialog-header button.mat-icon-button:hover{color:var(--md-sys-color-primary);background:var(--md-sys-color-primary-container)}.dialog-footer{position:sticky;bottom:0;z-index:1;padding:var(--dlg-pad)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: MatDialogModule }, { kind: "directive", type: i1.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i1.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "component", type: PraxisDynamicForm, selector: "praxis-dynamic-form", inputs: ["resourcePath", "resourceId", "editorialContext", "mode", "config", "schemaSource", "enableCustomization", "formId", "componentInstanceId", "layout", "backConfig", "hooks", "removeEmptyContainersOnSave", "reactiveValidation", "reactiveValidationDebounceMs", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "readonlyModeGlobal", "disabledModeGlobal", "presentationModeGlobal", "visibleGlobal", "customEndpoints"], outputs: ["formSubmit", "formCancel", "formReset", "configChange", "formReady", "valueChange", "syncCompleted", "initializationError", "loadingStateChange", "enableCustomizationChange", "customAction", "actionConfirmation", "schemaStatusChange", "fieldRenderError", "widgetEvent"] }] });
1689
+ `, isInline: true, styles: [":host{--dlg-header-h: 56px;--dlg-footer-h: 56px;--dlg-pad: 16px;display:flex;flex-direction:column;height:100%;overflow:hidden}:host([data-density=\"compact\"]){--dlg-header-h: 44px;--dlg-footer-h: 44px;--dlg-pad: 12px}.dialog-header{position:sticky;top:0;z-index:1;display:flex;align-items:center;gap:var(--dlg-pad);padding:0 var(--dlg-pad);height:var(--dlg-header-h);margin:0;background:var(--md-sys-color-surface-container-high);border-bottom:1px solid var(--md-sys-color-outline-variant);color:var(--md-sys-color-on-surface)}.dialog-title{margin:0;font:inherit;font-weight:600;color:var(--md-sys-color-on-surface)}.spacer{flex:1}.dialog-content{overflow:auto;padding:var(--dlg-pad);max-height:calc(100svh - var(--dlg-header-h) - 32px)}.dialog-header button.mat-icon-button{color:var(--md-sys-color-on-surface-variant)}.dialog-header button.mat-icon-button:hover{color:var(--md-sys-color-primary);background:var(--md-sys-color-primary-container)}.dialog-footer{position:sticky;bottom:0;z-index:1;padding:var(--dlg-pad)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: MatDialogModule }, { kind: "directive", type: i1.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i1.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "component", type: PraxisDynamicForm, selector: "praxis-dynamic-form", inputs: ["resourcePath", "resourceId", "editorialContext", "mode", "config", "schemaSource", "schemaUrl", "submitUrl", "submitMethod", "responseSchemaUrl", "apiEndpointKey", "apiUrlEntry", "enableCustomization", "formId", "componentInstanceId", "layout", "backConfig", "hooks", "removeEmptyContainersOnSave", "reactiveValidation", "reactiveValidationDebounceMs", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "readonlyModeGlobal", "disabledModeGlobal", "presentationModeGlobal", "visibleGlobal", "customEndpoints"], outputs: ["formSubmit", "formCancel", "formReset", "configChange", "formReady", "valueChange", "syncCompleted", "initializationError", "loadingStateChange", "enableCustomizationChange", "customAction", "actionConfirmation", "schemaStatusChange", "fieldRenderError", "widgetEvent"] }] });
1014
1690
  }
1015
1691
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: DynamicFormDialogHostComponent, decorators: [{
1016
1692
  type: Component,
@@ -1060,6 +1736,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1060
1736
  [resourcePath]="resourcePath"
1061
1737
  [resourceId]="resourceId"
1062
1738
  [mode]="mode"
1739
+ [schemaUrl]="schemaUrl"
1740
+ [submitUrl]="submitUrl"
1741
+ [submitMethod]="submitMethod"
1742
+ [apiEndpointKey]="apiEndpointKey"
1743
+ [apiUrlEntry]="apiUrlEntry"
1063
1744
  [presentationModeGlobal]="mode === 'view' ? true : null"
1064
1745
  [backConfig]="backConfig"
1065
1746
  (formSubmit)="onSave($event)"
@@ -1115,6 +1796,16 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
1115
1796
  type: 'Record<string, unknown>',
1116
1797
  description: 'Contexto opaco do host. A implementacao atual o expõe como Input, mas nao o consome no launcher nem no tableCrudContext.',
1117
1798
  },
1799
+ {
1800
+ name: 'metadata.queryContext',
1801
+ type: 'PraxisDataQueryContext | null',
1802
+ description: 'Contexto semantico de consulta encaminhado para a tabela interna do CRUD. Preferir este contrato para novo authoring remoto.',
1803
+ },
1804
+ {
1805
+ name: 'metadata.filterCriteria',
1806
+ type: 'Record<string, unknown> | null',
1807
+ description: 'Bridge declarativa de filtros encaminhada para a tabela interna. Para novo authoring, prefira metadata.queryContext.',
1808
+ },
1118
1809
  {
1119
1810
  name: 'enableCustomization',
1120
1811
  type: 'boolean',
@@ -1448,4 +2139,3 @@ const CRUD_AI_CAPABILITIES = {
1448
2139
  */
1449
2140
 
1450
2141
  export { CRUD_AI_CAPABILITIES, CrudLauncherService, CrudPageHeaderComponent, DialogService, DynamicFormDialogHostComponent, PRAXIS_CRUD_COMPONENT_METADATA, PraxisCrudComponent, assertCrudMetadata, providePraxisCrudMetadata };
1451
- //# sourceMappingURL=praxisui-crud.mjs.map