@praxisui/crud 9.0.0-beta.8 → 9.0.0-beta.80

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,16 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, InjectionToken, inject, input, signal, computed, effect, ChangeDetectionStrategy, Component, EventEmitter, DestroyRef, ChangeDetectorRef, Injector, ViewChild, Output, Input, Inject, ENVIRONMENT_INITIALIZER } from '@angular/core';
2
+ import { Injectable, InjectionToken, inject, input, signal, computed, effect, ChangeDetectionStrategy, Component, EventEmitter, DestroyRef, ChangeDetectorRef, Injector, ViewChild, Output, Input, Inject, ViewEncapsulation, ENVIRONMENT_INITIALIZER } from '@angular/core';
3
3
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
4
  import { HttpClient } from '@angular/common/http';
5
5
  import { Router, ActivatedRoute, RouterLink } from '@angular/router';
6
6
  import { MatSnackBar } from '@angular/material/snack-bar';
7
7
  import { firstValueFrom, BehaviorSubject, Subscription } from 'rxjs';
8
8
  import * as i2$1 from '@praxisui/core';
9
- import { ASYNC_CONFIG_STORAGE, GlobalConfigService, CrudOperationResolutionService, fillUndefined, SETTINGS_PANEL_DATA, PraxisI18nService, PraxisIconDirective, providePraxisI18nConfig, createDefaultTableConfig, GLOBAL_SURFACE_SERVICE, ComponentKeyService, ResourceDiscoveryService, ResourceActionOpenAdapterService, ResourceSurfaceOpenAdapterService, translateUnavailableWorkflowMessage, EmptyStateCardComponent, RESOURCE_DISCOVERY_I18N_CONFIG, GenericCrudService, ComponentMetadataRegistry } from '@praxisui/core';
9
+ import { ASYNC_CONFIG_STORAGE, GlobalConfigService, CrudOperationResolutionService, fillUndefined, SETTINGS_PANEL_DATA, PraxisI18nService, PraxisIconDirective, providePraxisI18nConfig, createDefaultTableConfig, EnterpriseRuntimeContextService, GLOBAL_SURFACE_SERVICE, SurfaceOutletRegistryService, ComponentKeyService, ResourceDiscoveryService, ResourceActionOpenAdapterService, ResourceSurfaceOpenAdapterService, translateUnavailableWorkflowMessage, EmptyStateCardComponent, RESOURCE_DISCOVERY_I18N_CONFIG, PraxisResourceIdentityComponent, GenericCrudService, ComponentMetadataRegistry } from '@praxisui/core';
10
10
  import { SettingsPanelService } from '@praxisui/settings-panel';
11
11
  import { PraxisTableInlineAuthoringEditorComponent, PraxisTable } from '@praxisui/table';
12
12
  import { ConfirmDialogComponent } from '@praxisui/dynamic-fields';
13
+ import { take, filter } from 'rxjs/operators';
13
14
  import * as i1 from '@angular/material/dialog';
14
15
  import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
15
16
  export { MAT_DIALOG_DATA as DIALOG_DATA } from '@angular/material/dialog';
@@ -34,7 +35,6 @@ import { MatSlideToggleModule } from '@angular/material/slide-toggle';
34
35
  import * as i10 from '@angular/material/tabs';
35
36
  import { MatTabsModule } from '@angular/material/tabs';
36
37
  import { PraxisDynamicForm } from '@praxisui/dynamic-form';
37
- import { filter, take } from 'rxjs/operators';
38
38
 
39
39
  class DialogService {
40
40
  matDialog;
@@ -68,6 +68,35 @@ function getCrudDrawerAdapterToken() {
68
68
  }
69
69
  const CRUD_DRAWER_ADAPTER = getCrudDrawerAdapterToken();
70
70
 
71
+ const DEFAULT_CRUD_DRAWER_MODAL_CONFIG = {
72
+ minWidth: 'min(100vw, 520px)',
73
+ width: 'min(760px, 58vw)',
74
+ height: '100dvh',
75
+ maxWidth: '100vw',
76
+ maxHeight: '100dvh',
77
+ position: { right: '0', top: '0' },
78
+ };
79
+ function isCrudDebugEnabled$1() {
80
+ try {
81
+ if (globalThis.__PRAXIS_DEBUG_CRUD__) {
82
+ return true;
83
+ }
84
+ if (typeof localStorage !== 'undefined') {
85
+ return localStorage.getItem('praxis.debug.crud') === 'true';
86
+ }
87
+ }
88
+ catch { }
89
+ return false;
90
+ }
91
+ function debugCrudLauncher(message, ...data) {
92
+ if (!isCrudDebugEnabled$1()) {
93
+ return;
94
+ }
95
+ try {
96
+ console.debug(message, ...data);
97
+ }
98
+ catch { }
99
+ }
71
100
  class CrudLauncherService {
72
101
  router = inject(Router);
73
102
  dialog = inject(DialogService);
@@ -85,8 +114,9 @@ class CrudLauncherService {
85
114
  async launch(action, row, metadata, componentKeyId, drawerCallbacks, runtime) {
86
115
  // Carregar overrides de CRUD (se houver) e mesclar em uma cópia local
87
116
  const merged = await this.mergeCrudOverrides(metadata, action, componentKeyId || undefined);
117
+ merged.action = this.normalizeActionForLaunch(merged.action);
88
118
  const mode = this.resolveOpenMode(merged.action, merged.metadata);
89
- console.debug('[CRUD:Launcher] mode=', mode, 'action=', action);
119
+ debugCrudLauncher('[CRUD:Launcher] mode=', mode, 'action=', action);
90
120
  if (mode === 'route') {
91
121
  if (!merged.action.route) {
92
122
  throw new Error(`Route not provided for action ${merged.action.action}`);
@@ -95,22 +125,6 @@ class CrudLauncherService {
95
125
  await this.router.navigateByUrl(url);
96
126
  return { mode };
97
127
  }
98
- if (mode === 'drawer' && this.drawerAdapter) {
99
- const actionForLaunch = this.resolveActionForLaunch(merged.action, merged.metadata);
100
- const inputs = this.mapInputs(actionForLaunch, row, merged.metadata, runtime);
101
- const idField = merged.metadata.resource?.idField ?? 'id';
102
- if (row && inputs[idField] === undefined && row[idField] !== undefined) {
103
- inputs[idField] = row[idField];
104
- }
105
- await Promise.resolve(this.drawerAdapter.open({
106
- action: actionForLaunch,
107
- metadata: merged.metadata,
108
- inputs,
109
- onClose: drawerCallbacks?.onClose,
110
- onResult: drawerCallbacks?.onResult,
111
- }));
112
- return { mode };
113
- }
114
128
  const actionForLaunch = this.resolveActionForLaunch(merged.action, merged.metadata);
115
129
  if (!actionForLaunch.formId) {
116
130
  throw new Error(`formId not provided for action ${actionForLaunch.action}`);
@@ -123,18 +137,29 @@ class CrudLauncherService {
123
137
  row[idField] !== undefined) {
124
138
  inputs[idField] = row[idField];
125
139
  }
140
+ if (mode === 'drawer' && this.drawerAdapter) {
141
+ await Promise.resolve(this.drawerAdapter.open({
142
+ action: actionForLaunch,
143
+ metadata: merged.metadata,
144
+ inputs,
145
+ onClose: drawerCallbacks?.onClose,
146
+ onResult: drawerCallbacks?.onResult,
147
+ }));
148
+ return { mode };
149
+ }
126
150
  const modalCfg = { ...(merged.metadata.defaults?.modal || {}) };
127
- console.debug('[CRUD:Launcher] opening dialog with:', {
151
+ debugCrudLauncher('[CRUD:Launcher] opening dialog with:', {
128
152
  action: merged.action.action,
129
153
  formId: actionForLaunch.formId,
130
154
  inputs,
131
155
  modalCfg,
132
156
  resourcePath: merged.metadata.resource?.path ?? merged.metadata.table?.resourcePath,
133
157
  });
134
- const panelClasses = ['pfx-dialog-pane', 'pfx-dialog-frosted'];
135
- if (modalCfg.panelClass) {
136
- panelClasses.push(modalCfg.panelClass);
137
- }
158
+ const drawerMode = mode === 'drawer';
159
+ const dialogPosition = drawerMode
160
+ ? (modalCfg.position ?? DEFAULT_CRUD_DRAWER_MODAL_CONFIG.position)
161
+ : modalCfg.position;
162
+ const panelClasses = mergeClassList(['pfx-dialog-pane', drawerMode ? 'pfx-drawer-pane' : 'pfx-dialog-frosted'], modalCfg.panelClass);
138
163
  // Backdrop style presets
139
164
  const backdropClasses = [];
140
165
  const style = modalCfg.backdropStyle;
@@ -147,20 +172,58 @@ class CrudLauncherService {
147
172
  else if (style === 'transparent') {
148
173
  backdropClasses.push('pfx-transparent-backdrop');
149
174
  }
150
- if (modalCfg.backdropClass) {
151
- backdropClasses.push(modalCfg.backdropClass);
152
- }
175
+ const mergedBackdropClasses = mergeClassList(backdropClasses, modalCfg.backdropClass);
153
176
  const ref = await this.dialog.openAsync(() => Promise.resolve().then(function () { return dynamicFormDialogHost_component; }).then((m) => m.DynamicFormDialogHostComponent), {
154
177
  ...modalCfg,
155
178
  panelClass: panelClasses,
156
- backdropClass: backdropClasses,
179
+ backdropClass: mergedBackdropClasses,
157
180
  autoFocus: modalCfg.autoFocus ?? true,
158
181
  restoreFocus: modalCfg.restoreFocus ?? true,
159
- minWidth: '360px',
160
- maxWidth: '95vw',
182
+ minWidth: drawerMode
183
+ ? (modalCfg.minWidth ?? DEFAULT_CRUD_DRAWER_MODAL_CONFIG.minWidth)
184
+ : '360px',
185
+ width: drawerMode
186
+ ? (modalCfg.width ?? DEFAULT_CRUD_DRAWER_MODAL_CONFIG.width)
187
+ : modalCfg.width,
188
+ height: drawerMode
189
+ ? (modalCfg.height ?? DEFAULT_CRUD_DRAWER_MODAL_CONFIG.height)
190
+ : modalCfg.height,
191
+ maxWidth: drawerMode
192
+ ? (modalCfg.maxWidth ?? DEFAULT_CRUD_DRAWER_MODAL_CONFIG.maxWidth)
193
+ : '95vw',
194
+ maxHeight: drawerMode
195
+ ? (modalCfg.maxHeight ?? DEFAULT_CRUD_DRAWER_MODAL_CONFIG.maxHeight)
196
+ : modalCfg.maxHeight,
197
+ position: dialogPosition,
161
198
  ariaLabelledBy: 'crudDialogTitle',
162
- data: { action: actionForLaunch, row, metadata: merged.metadata, inputs },
199
+ data: {
200
+ action: actionForLaunch,
201
+ row,
202
+ metadata: merged.metadata,
203
+ inputs,
204
+ presentation: drawerMode ? 'drawer' : 'modal',
205
+ dialogPosition,
206
+ resourceIdentity: drawerMode && actionForLaunch.action === 'edit'
207
+ ? runtime?.resourceIdentity ?? null
208
+ : null,
209
+ },
163
210
  });
211
+ if (drawerMode) {
212
+ ref
213
+ .afterClosed()
214
+ .pipe(take(1))
215
+ .subscribe((closedValue) => {
216
+ const result = toCrudDrawerResult(closedValue);
217
+ drawerCallbacks?.onClose?.();
218
+ if (result?.type) {
219
+ drawerCallbacks?.onResult?.(result);
220
+ }
221
+ else {
222
+ drawerCallbacks?.onResult?.({ type: 'close' });
223
+ }
224
+ });
225
+ return { mode, ref };
226
+ }
164
227
  return { mode, ref };
165
228
  }
166
229
  resolveOpenMode(action, metadata) {
@@ -168,12 +231,17 @@ class CrudLauncherService {
168
231
  const local = action.openMode ?? metadata.defaults?.openMode;
169
232
  if (local)
170
233
  return local;
171
- // Global fallback (action-specific, then general), then hard fallback 'route'
234
+ // Global fallback (action-specific, then general). A canonical CRUD
235
+ // operation without an explicit route can still be materialized through
236
+ // the inferred form contract, so it must not fall through to a route that
237
+ // does not exist.
172
238
  try {
173
239
  const globalCrud = this.global.getCrud();
174
240
  const actionName = action.action;
175
241
  const globalMode = (actionName && globalCrud?.actionDefaults?.[actionName]?.openMode) ?? globalCrud?.defaults?.openMode;
176
- let resolved = globalMode ?? 'route';
242
+ let resolved = globalMode ?? (this.isCanonicalCrudAction(action.action) && !action.route
243
+ ? 'drawer'
244
+ : 'route');
177
245
  // Safety: if modal/drawer but there is no formId and the action cannot be inferred, degrade to route.
178
246
  if ((resolved === 'modal' || resolved === 'drawer') &&
179
247
  !action.formId &&
@@ -248,6 +316,9 @@ class CrudLauncherService {
248
316
  if (resolved.apiUrlEntry != null) {
249
317
  inputs['apiUrlEntry'] = resolved.apiUrlEntry;
250
318
  }
319
+ if (resolved.layoutPolicy != null) {
320
+ inputs['layoutPolicy'] = resolved.layoutPolicy;
321
+ }
251
322
  if (action.form?.initialValue != null) {
252
323
  inputs['initialValue'] = action.form.initialValue;
253
324
  }
@@ -257,6 +328,7 @@ class CrudLauncherService {
257
328
  return inputs;
258
329
  }
259
330
  resolveActionForLaunch(action, metadata) {
331
+ action = this.normalizeActionForLaunch(action);
260
332
  if (action.formId) {
261
333
  return action;
262
334
  }
@@ -264,29 +336,61 @@ class CrudLauncherService {
264
336
  return action;
265
337
  }
266
338
  const formId = this.buildInferredFormId(action, metadata);
267
- return formId ? { ...action, formId } : action;
339
+ return formId
340
+ ? {
341
+ ...action,
342
+ formId,
343
+ form: {
344
+ ...(action.form || {}),
345
+ layoutPolicy: action.form?.layoutPolicy ?? this.defaultInferredLayoutPolicy(action),
346
+ },
347
+ }
348
+ : action;
349
+ }
350
+ resolveSubmitUrlTemplate(submitUrl, row, metadata) {
351
+ const template = String(submitUrl || '').trim();
352
+ if (!template || !template.includes('{')) {
353
+ return template;
354
+ }
355
+ const idField = String(metadata.resource?.idField ?? 'id');
356
+ const rowId = row?.[idField];
357
+ if (rowId == null) {
358
+ return template;
359
+ }
360
+ const encodedId = encodeURIComponent(String(rowId));
361
+ const idFieldPattern = new RegExp(`\\{${escapeRegExp(idField)}\\}`, 'g');
362
+ return template
363
+ .replace(idFieldPattern, encodedId)
364
+ .replace(/\{id\}/g, encodedId)
365
+ .replace(/\{resourceId\}/g, encodedId);
268
366
  }
269
367
  resolveActionFormContract(action, row, metadata, runtime) {
270
368
  if (this.isExplicitCrudAction(action)) {
271
369
  return {
272
370
  schemaUrl: action.form?.schemaUrl ?? null,
273
- submitUrl: action.form?.submitUrl ?? null,
371
+ submitUrl: action.form?.submitUrl
372
+ ? this.resolveSubmitUrlTemplate(action.form.submitUrl, row, metadata)
373
+ : null,
274
374
  submitMethod: action.form?.submitMethod ?? null,
275
375
  apiEndpointKey: action.form?.apiEndpointKey ?? null,
276
376
  apiUrlEntry: action.form?.apiUrlEntry ?? null,
377
+ layoutPolicy: action.form?.layoutPolicy ?? null,
277
378
  };
278
379
  }
279
380
  if (!this.isCanonicalCrudAction(action.action)) {
280
381
  return {
281
382
  apiEndpointKey: action.form?.apiEndpointKey ?? null,
282
383
  apiUrlEntry: action.form?.apiUrlEntry ?? null,
384
+ layoutPolicy: action.form?.layoutPolicy ?? null,
283
385
  };
284
386
  }
285
387
  const resourcePath = String(metadata.resource?.path ?? metadata.table?.resourcePath ?? '').trim();
388
+ const schemaResourcePath = String(metadata.resource?.schemaPath || '').trim() || null;
286
389
  if (!resourcePath) {
287
390
  return {
288
391
  apiEndpointKey: action.form?.apiEndpointKey ?? null,
289
392
  apiUrlEntry: action.form?.apiUrlEntry ?? null,
393
+ layoutPolicy: action.form?.layoutPolicy ?? null,
290
394
  };
291
395
  }
292
396
  const idField = String(metadata.resource?.idField ?? 'id');
@@ -294,6 +398,7 @@ class CrudLauncherService {
294
398
  const resolved = this.operationResolver.resolve({
295
399
  operation: action.action,
296
400
  resourcePath,
401
+ schemaResourcePath,
297
402
  resourceId: resourceId ?? null,
298
403
  capabilities: runtime?.capabilities ?? null,
299
404
  links: runtime?.links ?? null,
@@ -301,14 +406,15 @@ class CrudLauncherService {
301
406
  endpointKey: action.form?.apiEndpointKey ??
302
407
  metadata.resource?.endpointKey ??
303
408
  undefined,
304
- apiUrlEntry: action.form?.apiUrlEntry ?? null,
409
+ apiUrlEntry: action.form?.apiUrlEntry ?? metadata.resource?.apiUrlEntry ?? null,
305
410
  });
306
411
  return {
307
412
  schemaUrl: resolved?.schemaUrl ?? null,
308
413
  submitUrl: resolved?.submitUrl ?? null,
309
414
  submitMethod: resolved?.submitMethod ?? null,
310
415
  apiEndpointKey: action.form?.apiEndpointKey ?? metadata.resource?.endpointKey ?? null,
311
- apiUrlEntry: action.form?.apiUrlEntry ?? null,
416
+ apiUrlEntry: action.form?.apiUrlEntry ?? metadata.resource?.apiUrlEntry ?? null,
417
+ layoutPolicy: action.form?.layoutPolicy ?? null,
312
418
  };
313
419
  }
314
420
  resolveRuntimeContract(action, row, metadata, runtime) {
@@ -327,9 +433,20 @@ class CrudLauncherService {
327
433
  return `${sanitizedResource || 'crud'}-${String(action.action || '').trim().toLowerCase()}`;
328
434
  }
329
435
  isCanonicalCrudAction(actionName) {
330
- const normalized = String(actionName || '').trim().toLowerCase();
436
+ const normalized = this.normalizeCrudOperationName(actionName);
331
437
  return normalized === 'create' || normalized === 'view' || normalized === 'edit' || normalized === 'delete';
332
438
  }
439
+ normalizeActionForLaunch(action) {
440
+ const normalized = this.normalizeCrudOperationName(action.action);
441
+ return normalized === action.action ? action : { ...action, action: normalized };
442
+ }
443
+ normalizeCrudOperationName(actionName) {
444
+ const normalized = String(actionName || '').trim().toLowerCase();
445
+ if (['add', 'novo', 'new', 'incluir', 'inserir'].includes(normalized)) {
446
+ return 'create';
447
+ }
448
+ return normalized;
449
+ }
333
450
  isExplicitCrudAction(action) {
334
451
  if (action.mode === 'explicit') {
335
452
  return true;
@@ -338,6 +455,19 @@ class CrudLauncherService {
338
455
  String(action.form?.submitUrl || '').trim() ||
339
456
  String(action.form?.submitMethod || '').trim());
340
457
  }
458
+ defaultInferredLayoutPolicy(action) {
459
+ const operation = this.normalizeCrudOperationName(action.action);
460
+ if (operation !== 'create' && operation !== 'edit') {
461
+ return null;
462
+ }
463
+ return {
464
+ source: 'schema',
465
+ intent: 'command',
466
+ preset: 'groupedCommand',
467
+ persistence: 'transient',
468
+ schemaType: 'request',
469
+ };
470
+ }
341
471
  async mergeCrudOverrides(metadata, action, componentKeyId) {
342
472
  try {
343
473
  if (!componentKeyId)
@@ -392,6 +522,33 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
392
522
  type: Injectable,
393
523
  args: [{ providedIn: 'root' }]
394
524
  }] });
525
+ function mergeClassList(base, extra) {
526
+ if (!extra) {
527
+ return base;
528
+ }
529
+ if (typeof extra === 'string') {
530
+ return [...base, extra];
531
+ }
532
+ if (Array.isArray(extra)) {
533
+ return [...base, ...extra];
534
+ }
535
+ return [
536
+ ...base,
537
+ ...Object.keys(extra).filter((className) => !!extra[className]),
538
+ ];
539
+ }
540
+ function escapeRegExp(value) {
541
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
542
+ }
543
+ function toCrudDrawerResult(value) {
544
+ if (!value || typeof value !== 'object') {
545
+ return undefined;
546
+ }
547
+ const result = value;
548
+ return result.type === 'save' || result.type === 'delete' || result.type === 'close'
549
+ ? result
550
+ : undefined;
551
+ }
395
552
 
396
553
  const DOCUMENT_KIND = 'praxis.crud.editor';
397
554
  const DOCUMENT_VERSION = 1;
@@ -510,10 +667,15 @@ function normalizeCrudMetadata(metadata) {
510
667
  if (base.resource && typeof base.resource === 'object') {
511
668
  normalized.resource = stripUndefinedShallow({
512
669
  path: trimString(base.resource.path) || undefined,
670
+ schemaPath: trimString(base.resource.schemaPath) || undefined,
513
671
  idField: typeof base.resource.idField === 'string'
514
672
  ? trimString(base.resource.idField) || undefined
515
673
  : base.resource.idField,
674
+ title: trimString(base.resource.title) || undefined,
675
+ label: trimString(base.resource.label) || undefined,
676
+ formTitle: trimString(base.resource.formTitle) || undefined,
516
677
  endpointKey: trimString(base.resource.endpointKey) || undefined,
678
+ apiUrlEntry: base.resource.apiUrlEntry || undefined,
517
679
  });
518
680
  }
519
681
  if (base.defaults && typeof base.defaults === 'object') {
@@ -564,6 +726,7 @@ function normalizeCrudAction(action) {
564
726
  apiEndpointKey: trimString(action.form.apiEndpointKey) || undefined,
565
727
  apiUrlEntry: action.form.apiUrlEntry || undefined,
566
728
  initialValue: normalizeInitialValue(action.form.initialValue),
729
+ layoutPolicy: action.form.layoutPolicy ?? undefined,
567
730
  }
568
731
  : undefined,
569
732
  });
@@ -721,6 +884,11 @@ const PRAXIS_CRUD_RUNTIME_I18N_CONFIG = {
721
884
  'crud.emptyState.title': 'Conecte o CRUD a um recurso',
722
885
  'crud.emptyState.description': 'Informe os metadados (resourcePath / schema) ou forneça metadata.data para habilitar a tabela e as ações.',
723
886
  'crud.emptyState.primaryAction': 'Configurar metadados',
887
+ 'crud.table.emptyState.initial.title': 'Sem registros em {label}',
888
+ 'crud.table.emptyState.initial.titleFallback': 'Nenhum registro disponível.',
889
+ 'crud.table.emptyState.initial.descriptionWithAction': 'Use a ação principal para adicionar o primeiro registro quando houver informações para cadastrar.',
890
+ 'crud.table.emptyState.filtered.title': 'Nenhum resultado encontrado.',
891
+ 'crud.table.emptyState.filtered.description': 'Revise os filtros ou ajuste o termo de busca.',
724
892
  'crud.preferences.resetSuccess': 'Overrides de CRUD redefinidos',
725
893
  'crud.actions.create': 'Adicionar',
726
894
  'crud.actions.view': 'Ver',
@@ -733,6 +901,11 @@ const PRAXIS_CRUD_RUNTIME_I18N_CONFIG = {
733
901
  'crud.emptyState.title': 'Connect CRUD to a resource',
734
902
  'crud.emptyState.description': 'Provide metadata (resourcePath / schema) or metadata.data to enable the table and actions.',
735
903
  'crud.emptyState.primaryAction': 'Configure metadata',
904
+ 'crud.table.emptyState.initial.title': 'No records in {label}',
905
+ 'crud.table.emptyState.initial.titleFallback': 'No records available.',
906
+ 'crud.table.emptyState.initial.descriptionWithAction': 'Use the primary action to add the first record when there is information to register.',
907
+ 'crud.table.emptyState.filtered.title': 'No results found.',
908
+ 'crud.table.emptyState.filtered.description': 'Review the filters or adjust the search term.',
736
909
  'crud.preferences.resetSuccess': 'CRUD overrides reset',
737
910
  'crud.actions.create': 'Add',
738
911
  'crud.actions.view': 'View',
@@ -3467,6 +3640,8 @@ class PraxisCrudComponent {
3467
3640
  componentInstanceId;
3468
3641
  context;
3469
3642
  enableCustomization = false;
3643
+ /** Capability publica exigida para authoring governado do CRUD e da tabela interna. */
3644
+ authoringCapability = null;
3470
3645
  configureRequested = new EventEmitter();
3471
3646
  afterOpen = new EventEmitter();
3472
3647
  afterClose = new EventEmitter();
@@ -3491,6 +3666,7 @@ class PraxisCrudComponent {
3491
3666
  table;
3492
3667
  storage = inject(ASYNC_CONFIG_STORAGE);
3493
3668
  settingsPanel = inject(SettingsPanelService);
3669
+ enterpriseRuntimeContext = inject(EnterpriseRuntimeContextService, { optional: true });
3494
3670
  snack = inject(MatSnackBar);
3495
3671
  dialog = inject(DialogService);
3496
3672
  i18n = inject(PraxisI18nService);
@@ -3498,6 +3674,7 @@ class PraxisCrudComponent {
3498
3674
  resourceDiscoveryInstance;
3499
3675
  actionOpenAdapterInstance;
3500
3676
  surfaceOpenAdapterInstance;
3677
+ materializedRefreshSequence = 0;
3501
3678
  global = (() => {
3502
3679
  try {
3503
3680
  return inject(GlobalConfigService);
@@ -3507,6 +3684,7 @@ class PraxisCrudComponent {
3507
3684
  }
3508
3685
  })();
3509
3686
  surfaceService = inject(GLOBAL_SURFACE_SERVICE, { optional: true });
3687
+ surfaceOutlets = inject(SurfaceOutletRegistryService);
3510
3688
  componentKeys = inject(ComponentKeyService);
3511
3689
  route = (() => {
3512
3690
  try {
@@ -3523,10 +3701,23 @@ class PraxisCrudComponent {
3523
3701
  lastAppliedResourceIdentity = null;
3524
3702
  tableCollectionLinks = null;
3525
3703
  collectionCapabilities = null;
3704
+ activeAuthoringPanelRef;
3705
+ constructor() {
3706
+ this.enterpriseRuntimeContext?.contextChanges$
3707
+ .pipe(takeUntilDestroyed(this.destroyRef))
3708
+ .subscribe(() => {
3709
+ if (!this.isCustomizationAvailable()) {
3710
+ this.activeAuthoringPanelRef?.close();
3711
+ this.activeAuthoringPanelRef = undefined;
3712
+ }
3713
+ this.cdr.markForCheck();
3714
+ });
3715
+ }
3526
3716
  collectionCapabilitiesRequestHref = null;
3527
3717
  collectionCapabilitiesResolvedHref = null;
3528
3718
  collectionCapabilitiesRequestSeq = 0;
3529
3719
  currentAuthoringDocument;
3720
+ selectedRow = null;
3530
3721
  getResourceDiscovery() {
3531
3722
  const assigned = this.resourceDiscovery;
3532
3723
  return assigned ?? (this.resourceDiscoveryInstance ??= this.injector.get(ResourceDiscoveryService));
@@ -3553,15 +3744,32 @@ class PraxisCrudComponent {
3553
3744
  catch { }
3554
3745
  }
3555
3746
  onTableRowClick(event) {
3747
+ const row = this.extractRowFromEvent(event);
3748
+ if (row) {
3749
+ this.selectedRow = row;
3750
+ }
3556
3751
  this.rowClick.emit(event);
3557
3752
  }
3558
3753
  onTableSelectionChange(event) {
3754
+ this.selectedRow = this.extractSelectedRowFromEvent(event);
3559
3755
  this.selectionChange.emit(event);
3560
3756
  }
3757
+ async onBulkAction(event) {
3758
+ const action = String(event?.action || '').trim();
3759
+ if (!action) {
3760
+ return;
3761
+ }
3762
+ const row = this.extractSelectedRowFromBulkAction(event);
3763
+ if (row) {
3764
+ this.selectedRow = row;
3765
+ }
3766
+ await this.onAction(action, row ?? undefined, event);
3767
+ }
3561
3768
  ngOnChanges(changes) {
3562
3769
  if (!changes['metadata'] && !changes['context']) {
3563
3770
  return;
3564
3771
  }
3772
+ this.materializedRefreshSequence += 1;
3565
3773
  try {
3566
3774
  const parsed = typeof this.metadata === 'string'
3567
3775
  ? JSON.parse(this.metadata)
@@ -3595,33 +3803,46 @@ class PraxisCrudComponent {
3595
3803
  async onAction(action, row, runtimeEvent) {
3596
3804
  try {
3597
3805
  document.activeElement?.blur();
3598
- let actionMeta = this.resolvedMetadata.actions?.find((candidate) => candidate.action === action);
3806
+ const normalizedAction = this.normalizeCrudActionName(action);
3807
+ const contextualRow = row ?? this.resolveSelectedRowForAction(normalizedAction);
3808
+ let actionMeta = this.resolvedMetadata.actions?.find((candidate) => this.normalizeCrudActionName(candidate.action) === normalizedAction);
3809
+ if (!actionMeta && normalizedAction !== action) {
3810
+ actionMeta = this.resolvedMetadata.actions?.find((candidate) => candidate.action === action);
3811
+ }
3599
3812
  if (!actionMeta) {
3600
- const ctxAction = this.tableCrudContext?.actions?.find((candidate) => candidate.action === action);
3813
+ const ctxAction = this.tableCrudContext?.actions?.find((candidate) => this.normalizeCrudActionName(candidate.action) === normalizedAction);
3601
3814
  if (ctxAction) {
3602
3815
  actionMeta = {
3603
- action: ctxAction.action,
3816
+ action: this.normalizeCrudActionName(ctxAction.action),
3604
3817
  openMode: ctxAction.openMode,
3605
3818
  formId: ctxAction.formId,
3606
3819
  route: ctxAction.route,
3607
3820
  };
3608
3821
  }
3609
3822
  else {
3610
- actionMeta = { action };
3823
+ actionMeta = { action: normalizedAction };
3611
3824
  }
3612
3825
  }
3613
- const effectiveAction = (actionMeta || { action });
3826
+ const effectiveAction = {
3827
+ ...(actionMeta || { action: normalizedAction }),
3828
+ action: this.normalizeCrudActionName(actionMeta?.action ?? normalizedAction),
3829
+ };
3830
+ const resourceIdentity = runtimeEvent?.resourceIdentity ?? null;
3831
+ const handledByDuplicateDraft = await this.tryHandleCanonicalDuplicateDraftAction(effectiveAction, contextualRow);
3832
+ if (handledByDuplicateDraft) {
3833
+ return;
3834
+ }
3614
3835
  if (!this.hasExplicitOpenBinding(effectiveAction)) {
3615
- const openedByDiscovery = await this.tryOpenDiscoveredCrudSurface(action, row, runtimeEvent);
3836
+ const openedByDiscovery = await this.tryOpenDiscoveredCrudSurface(effectiveAction.action, contextualRow, runtimeEvent);
3616
3837
  if (openedByDiscovery) {
3617
3838
  return;
3618
3839
  }
3619
- const handledByWorkflowAction = await this.tryOpenDiscoveredWorkflowAction(action, row, runtimeEvent);
3840
+ const handledByWorkflowAction = await this.tryOpenDiscoveredWorkflowAction(effectiveAction.action, contextualRow, runtimeEvent);
3620
3841
  if (handledByWorkflowAction) {
3621
3842
  return;
3622
3843
  }
3623
3844
  }
3624
- const handledByDelete = await this.tryHandleCanonicalDeleteAction(effectiveAction, row);
3845
+ const handledByDelete = await this.tryHandleCanonicalDeleteAction(effectiveAction, contextualRow);
3625
3846
  if (handledByDelete) {
3626
3847
  return;
3627
3848
  }
@@ -3632,7 +3853,7 @@ class PraxisCrudComponent {
3632
3853
  drawerCloseEmitted = true;
3633
3854
  this.afterClose.emit();
3634
3855
  };
3635
- const { mode, ref } = await this.launcher.launch(effectiveAction, row, this.resolvedMetadata, this.componentKeyId(), {
3856
+ const { mode, ref } = await this.launcher.launch(effectiveAction, contextualRow, this.resolvedMetadata, this.componentKeyId(), {
3636
3857
  onClose: () => emitDrawerClose(),
3637
3858
  onResult: (result) => {
3638
3859
  emitDrawerClose();
@@ -3653,9 +3874,13 @@ class PraxisCrudComponent {
3653
3874
  capabilities: effectiveAction.action === 'create'
3654
3875
  ? this.collectionCapabilities
3655
3876
  : this.collectionCapabilities,
3656
- links: this.resolveCrudRuntimeLinks(effectiveAction.action, row),
3877
+ links: this.resolveCrudRuntimeLinks(effectiveAction.action, contextualRow),
3878
+ resourceIdentity,
3657
3879
  });
3658
3880
  this.afterOpen.emit({ mode, action: effectiveAction.action });
3881
+ if (mode === 'drawer') {
3882
+ return;
3883
+ }
3659
3884
  if (!ref) {
3660
3885
  return;
3661
3886
  }
@@ -3791,6 +4016,9 @@ class PraxisCrudComponent {
3791
4016
  return this.tx('crud.emptyState.primaryAction', 'Configure metadata');
3792
4017
  }
3793
4018
  onConfigureRequested() {
4019
+ if (!this.isCustomizationAvailable()) {
4020
+ return;
4021
+ }
3794
4022
  this.configureRequested.emit();
3795
4023
  this.openCrudAuthoringFromTable();
3796
4024
  }
@@ -3841,9 +4069,140 @@ class PraxisCrudComponent {
3841
4069
  this.refreshTable();
3842
4070
  return true;
3843
4071
  }
4072
+ async tryHandleCanonicalDuplicateDraftAction(action, row) {
4073
+ const normalizedAction = this.normalizeCrudActionName(action.action);
4074
+ if (normalizedAction !== 'duplicate-draft' || !row) {
4075
+ return false;
4076
+ }
4077
+ const duplicateDraftUrl = this.getResourceDiscovery().resolveLinkHref(row['_links'], 'duplicate-draft', this.buildDiscoveryOptions());
4078
+ if (!duplicateDraftUrl) {
4079
+ return false;
4080
+ }
4081
+ const response = await firstValueFrom(this.http.post(duplicateDraftUrl, {}));
4082
+ const draft = this.unwrapRestData(response);
4083
+ if (!this.isRecord(draft)) {
4084
+ throw new Error('Duplicate draft action returned an invalid create draft.');
4085
+ }
4086
+ const configuredCreateAction = this.resolvedMetadata.actions?.find((candidate) => this.normalizeCrudActionName(candidate.action) === 'create') ?? { action: 'create' };
4087
+ const createAction = {
4088
+ ...configuredCreateAction,
4089
+ action: 'create',
4090
+ openMode: configuredCreateAction.openMode ??
4091
+ action.openMode ??
4092
+ this.resolvedMetadata.defaults?.openMode ??
4093
+ 'drawer',
4094
+ form: {
4095
+ ...(configuredCreateAction.form ?? {}),
4096
+ initialValue: { ...draft },
4097
+ },
4098
+ };
4099
+ let drawerCloseEmitted = false;
4100
+ const emitDrawerClose = () => {
4101
+ if (drawerCloseEmitted)
4102
+ return;
4103
+ drawerCloseEmitted = true;
4104
+ this.afterClose.emit();
4105
+ };
4106
+ const handleResult = (result) => {
4107
+ if (result?.type !== 'save') {
4108
+ return;
4109
+ }
4110
+ const data = this.isRecord(result.data) ? result.data : {};
4111
+ const id = data[this.getIdField()];
4112
+ this.afterSave.emit({ id, data });
4113
+ this.refreshTable();
4114
+ };
4115
+ const { mode, ref } = await this.launcher.launch(createAction, undefined, this.resolvedMetadata, this.componentKeyId(), {
4116
+ onClose: () => emitDrawerClose(),
4117
+ onResult: (result) => handleResult(result),
4118
+ }, {
4119
+ capabilities: this.collectionCapabilities,
4120
+ links: this.tableCollectionLinks,
4121
+ });
4122
+ this.afterOpen.emit({ mode, action: normalizedAction });
4123
+ if (mode !== 'drawer' && ref) {
4124
+ ref
4125
+ .afterClosed()
4126
+ .pipe(takeUntilDestroyed(this.destroyRef))
4127
+ .subscribe((result) => {
4128
+ this.afterClose.emit();
4129
+ handleResult(result);
4130
+ });
4131
+ }
4132
+ return true;
4133
+ }
3844
4134
  refreshTable() {
4135
+ if (this.tryRefreshMaterializedLocalData()) {
4136
+ return;
4137
+ }
3845
4138
  this.table.refetch();
3846
4139
  }
4140
+ tryRefreshMaterializedLocalData() {
4141
+ const readUrl = this.resolveMaterializedReadUrl();
4142
+ if (!readUrl || this.resolveResourcePath(this.resolvedMetadata)) {
4143
+ return false;
4144
+ }
4145
+ const refreshSequence = ++this.materializedRefreshSequence;
4146
+ void firstValueFrom(this.getResourceDiscovery().fetchJson(readUrl, {
4147
+ endpointKey: this.resolvedMetadata.resource?.endpointKey,
4148
+ apiUrlEntry: this.resolvedMetadata.resource?.apiUrlEntry,
4149
+ }))
4150
+ .then((response) => {
4151
+ if (!this.isCurrentMaterializedRefresh(refreshSequence, readUrl)) {
4152
+ return;
4153
+ }
4154
+ const data = this.extractCollectionData(this.unwrapRestData(response));
4155
+ if (!data) {
4156
+ this.error.emit({
4157
+ code: 'CRUD_MATERIALIZED_REFRESH_INVALID_COLLECTION',
4158
+ readUrl,
4159
+ });
4160
+ return;
4161
+ }
4162
+ this.resolvedMetadata = {
4163
+ ...this.resolvedMetadata,
4164
+ data,
4165
+ };
4166
+ this.applyResolvedCrudState(this.resolvedMetadata);
4167
+ this.cdr.markForCheck();
4168
+ })
4169
+ .catch((error) => this.error.emit(error));
4170
+ return true;
4171
+ }
4172
+ isCurrentMaterializedRefresh(refreshSequence, readUrl) {
4173
+ return (refreshSequence === this.materializedRefreshSequence &&
4174
+ this.resolveMaterializedReadUrl() === readUrl &&
4175
+ this.resolveResourcePath(this.resolvedMetadata).length === 0);
4176
+ }
4177
+ resolveMaterializedReadUrl() {
4178
+ const materialization = this.context?.['materialization'];
4179
+ return String(materialization?.['readUrl'] || '').trim();
4180
+ }
4181
+ unwrapRestData(value) {
4182
+ if (value && typeof value === 'object' && !Array.isArray(value) && 'data' in value) {
4183
+ return value.data;
4184
+ }
4185
+ return value;
4186
+ }
4187
+ extractCollectionData(data) {
4188
+ if (Array.isArray(data)) {
4189
+ return data;
4190
+ }
4191
+ if (!data || typeof data !== 'object') {
4192
+ return null;
4193
+ }
4194
+ const record = data;
4195
+ if (Array.isArray(record['content'])) {
4196
+ return record['content'];
4197
+ }
4198
+ if (Array.isArray(record['items'])) {
4199
+ return record['items'];
4200
+ }
4201
+ if (Array.isArray(record['data'])) {
4202
+ return record['data'];
4203
+ }
4204
+ return null;
4205
+ }
3847
4206
  emitTableRuntimeConfigSnapshot() {
3848
4207
  const snapshot = this.getCurrentTableConfigSnapshot();
3849
4208
  if (!snapshot)
@@ -3901,11 +4260,47 @@ class PraxisCrudComponent {
3901
4260
  }
3902
4261
  }
3903
4262
  resolveQueryContext(meta) {
4263
+ if (Array.isArray(meta?.data) && this.resolveResourcePath(meta).length === 0) {
4264
+ return null;
4265
+ }
3904
4266
  return this.isRecord(meta?.queryContext) ? meta.queryContext : null;
3905
4267
  }
3906
4268
  resolveFilterCriteria(meta) {
3907
4269
  return this.isRecord(meta?.filterCriteria) ? { ...meta.filterCriteria } : {};
3908
4270
  }
4271
+ extractRowFromEvent(event) {
4272
+ if (!this.isRecord(event)) {
4273
+ return null;
4274
+ }
4275
+ const row = event['row'];
4276
+ return this.isRecord(row) ? row : null;
4277
+ }
4278
+ extractSelectedRowFromEvent(event) {
4279
+ if (!this.isRecord(event)) {
4280
+ return null;
4281
+ }
4282
+ const selection = event['selection'];
4283
+ if (Array.isArray(selection)) {
4284
+ const first = selection[0];
4285
+ return this.isRecord(first) ? first : null;
4286
+ }
4287
+ const row = event['row'];
4288
+ if (this.isRecord(row)) {
4289
+ return row;
4290
+ }
4291
+ return null;
4292
+ }
4293
+ extractSelectedRowFromBulkAction(event) {
4294
+ const first = Array.isArray(event?.rows) ? event.rows[0] : null;
4295
+ return this.isRecord(first) ? first : null;
4296
+ }
4297
+ resolveSelectedRowForAction(action) {
4298
+ const normalized = String(action || '').trim().toLowerCase();
4299
+ if (!normalized || normalized === 'create' || normalized === 'add' || normalized === 'new') {
4300
+ return undefined;
4301
+ }
4302
+ return this.selectedRow ?? undefined;
4303
+ }
3909
4304
  async tryOpenDiscoveredCrudSurface(action, row, runtimeEvent) {
3910
4305
  const normalizedAction = String(action || '').trim().toLowerCase();
3911
4306
  if (!this.surfaceService) {
@@ -3942,7 +4337,7 @@ class PraxisCrudComponent {
3942
4337
  }
3943
4338
  let openPromise;
3944
4339
  try {
3945
- openPromise = Promise.resolve(this.surfaceService.open(payload, {
4340
+ const surfaceContext = {
3946
4341
  sourceId: this.componentKeyId() || undefined,
3947
4342
  payload: { action: normalizedAction, row: row ?? null },
3948
4343
  meta: {
@@ -3959,7 +4354,9 @@ class PraxisCrudComponent {
3959
4354
  resourcePath,
3960
4355
  },
3961
4356
  },
3962
- }));
4357
+ };
4358
+ const handledInline = await this.surfaceOutlets.tryActivate(payload, surfaceContext);
4359
+ openPromise = handledInline ? Promise.resolve(undefined) : Promise.resolve(this.surfaceService.open(payload, surfaceContext));
3963
4360
  }
3964
4361
  catch {
3965
4362
  return false;
@@ -3999,7 +4396,7 @@ class PraxisCrudComponent {
3999
4396
  const providedAction = this.resolveProvidedWorkflowAction(normalizedAction, runtimeEvent?.actionConfig);
4000
4397
  const catalog = providedAction ? null : await this.resolveDiscoveredActionCatalog(row);
4001
4398
  const discoveredAction = providedAction || this.selectDiscoveredWorkflowAction(normalizedAction, catalog?.actions || []);
4002
- const resourcePath = String(this.resolveResourcePath(this.resolvedMetadata) || catalog?.resourcePath || '').trim();
4399
+ const resourcePath = String(catalog?.resourcePath || this.resolveResourcePath(this.resolvedMetadata) || '').trim();
4003
4400
  if (!discoveredAction || !resourcePath) {
4004
4401
  return false;
4005
4402
  }
@@ -4222,12 +4619,19 @@ class PraxisCrudComponent {
4222
4619
  }
4223
4620
  return cfg.toolbar;
4224
4621
  };
4225
- const hasToolbarAdd = (cfg.toolbar?.actions || []).some((action) => this.isAddLike(action));
4622
+ const hasToolbarAdd = (cfg.toolbar?.actions || []).some((action) => this.shouldCanonicalizeToolbarCreateAction(action));
4226
4623
  const addAction = this.resolveCreateToolbarAction(meta, capabilities);
4227
4624
  if (addAction) {
4228
4625
  if (hasToolbarAdd) {
4229
4626
  const toolbar = ensureToolbar();
4230
4627
  toolbar.visible = true;
4628
+ toolbar.actions = (toolbar.actions || []).map((action) => this.shouldCanonicalizeToolbarCreateAction(action)
4629
+ ? {
4630
+ ...action,
4631
+ action: 'create',
4632
+ }
4633
+ : action);
4634
+ changed = true;
4231
4635
  }
4232
4636
  else {
4233
4637
  const toolbar = ensureToolbar();
@@ -4246,6 +4650,7 @@ class PraxisCrudComponent {
4246
4650
  });
4247
4651
  }
4248
4652
  changed = true;
4653
+ changed = this.ensureCanonicalCollectionEmptyState(cfg, meta, addAction) || changed;
4249
4654
  }
4250
4655
  const discoveredToolbarActions = this.resolveCollectionWorkflowToolbarActions(capabilities);
4251
4656
  if (discoveredToolbarActions.length) {
@@ -4362,6 +4767,28 @@ class PraxisCrudComponent {
4362
4767
  }
4363
4768
  return false;
4364
4769
  }
4770
+ normalizeCrudActionName(action) {
4771
+ const raw = String(action || '').trim();
4772
+ const normalized = raw.toLowerCase();
4773
+ return this.isCreateActionAliasName(normalized) ? 'create' : raw;
4774
+ }
4775
+ shouldCanonicalizeToolbarCreateAction(action) {
4776
+ if (!action)
4777
+ return false;
4778
+ const normalize = (value) => String(value || '').trim().toLowerCase();
4779
+ const explicitId = normalize(action.action || action.id || action.code || action.key || action.name || action.type);
4780
+ if (this.isCreateActionAliasName(explicitId))
4781
+ return true;
4782
+ if (explicitId)
4783
+ return false;
4784
+ const icon = normalize(action.icon);
4785
+ const label = normalize(action.label);
4786
+ return ((icon === 'add' || icon === 'add_circle' || icon === 'add_box') &&
4787
+ (label === 'adicionar' || label === 'novo' || label === 'criar' || label === 'incluir'));
4788
+ }
4789
+ isCreateActionAliasName(actionName) {
4790
+ return ['create', 'add', 'novo', 'new', 'incluir', 'inserir'].includes(actionName);
4791
+ }
4365
4792
  buildTableCrudContext(meta, capabilities) {
4366
4793
  if (!meta)
4367
4794
  return undefined;
@@ -4384,6 +4811,9 @@ class PraxisCrudComponent {
4384
4811
  };
4385
4812
  }
4386
4813
  openCrudAuthoringFromTable = () => {
4814
+ if (!this.isCustomizationAvailable()) {
4815
+ return;
4816
+ }
4387
4817
  const seed = this.currentAuthoringDocument
4388
4818
  || createCrudAuthoringDocument({ metadata: this.resolvedMetadata });
4389
4819
  const ref = openCrudMetadataEditor(this.settingsPanel, {
@@ -4392,13 +4822,33 @@ class PraxisCrudComponent {
4392
4822
  title: this.tx('crud.authoring.title', 'Configurações do CRUD'),
4393
4823
  titleIcon: 'table_chart',
4394
4824
  });
4825
+ this.activeAuthoringPanelRef = ref;
4395
4826
  ref.applied$
4396
4827
  .pipe(takeUntilDestroyed(this.destroyRef))
4397
4828
  .subscribe((payload) => this.applyCrudAuthoringPayload(payload, 'applied'));
4398
4829
  ref.saved$
4399
4830
  .pipe(takeUntilDestroyed(this.destroyRef))
4400
4831
  .subscribe((payload) => this.applyCrudAuthoringPayload(payload, 'saved'));
4832
+ ref.closed$
4833
+ .pipe(takeUntilDestroyed(this.destroyRef))
4834
+ .subscribe(() => {
4835
+ if (this.activeAuthoringPanelRef === ref) {
4836
+ this.activeAuthoringPanelRef = undefined;
4837
+ }
4838
+ });
4401
4839
  };
4840
+ isCustomizationAvailable() {
4841
+ if (!this.enableCustomization) {
4842
+ return false;
4843
+ }
4844
+ const requiredCapability = String(this.authoringCapability || '').trim();
4845
+ if (!requiredCapability) {
4846
+ return true;
4847
+ }
4848
+ const capabilities = this.enterpriseRuntimeContext?.snapshot?.capabilities;
4849
+ return Array.isArray(capabilities)
4850
+ && capabilities.some((capability) => capability === requiredCapability);
4851
+ }
4402
4852
  applyCrudAuthoringPayload(payload, eventName) {
4403
4853
  const next = parseLegacyOrCrudDocument(payload);
4404
4854
  this.currentAuthoringDocument = next;
@@ -4615,6 +5065,48 @@ class PraxisCrudComponent {
4615
5065
  return action;
4616
5066
  }
4617
5067
  }
5068
+ ensureCanonicalCollectionEmptyState(config, metadata, createAction) {
5069
+ const behavior = config.behavior || {};
5070
+ if (behavior.emptyState) {
5071
+ return false;
5072
+ }
5073
+ const resourceLabel = String(metadata.resource?.label
5074
+ || metadata.resource?.title
5075
+ || metadata.resource?.path
5076
+ || '').trim();
5077
+ const title = resourceLabel
5078
+ ? this.txWithParams('crud.table.emptyState.initial.title', 'Sem registros em {label}', { label: resourceLabel })
5079
+ : this.tx('crud.table.emptyState.initial.titleFallback', 'Nenhum registro disponível.');
5080
+ const actionId = String(createAction.action || createAction.id || 'create').trim() || 'create';
5081
+ const actionLabel = String(createAction.label || this.getCrudActionLabel('create')).trim();
5082
+ const actionIcon = String(createAction.icon || 'add').trim();
5083
+ config.behavior = {
5084
+ ...behavior,
5085
+ emptyState: {
5086
+ message: '',
5087
+ contexts: {
5088
+ initial: {
5089
+ title,
5090
+ description: this.tx('crud.table.emptyState.initial.descriptionWithAction', 'Use a ação principal para adicionar o primeiro registro quando houver informações para cadastrar.'),
5091
+ actions: [
5092
+ {
5093
+ label: actionLabel,
5094
+ action: actionId,
5095
+ icon: actionIcon,
5096
+ primary: true,
5097
+ },
5098
+ ],
5099
+ },
5100
+ filtered: {
5101
+ title: this.tx('crud.table.emptyState.filtered.title', 'Nenhum resultado encontrado.'),
5102
+ description: this.tx('crud.table.emptyState.filtered.description', 'Revise os filtros ou ajuste o termo de busca.'),
5103
+ actions: [],
5104
+ },
5105
+ },
5106
+ },
5107
+ };
5108
+ return true;
5109
+ }
4618
5110
  tx(key, fallback) {
4619
5111
  try {
4620
5112
  return translateCrudRuntimeText(this.i18n, key, fallback);
@@ -4623,8 +5115,19 @@ class PraxisCrudComponent {
4623
5115
  return fallback;
4624
5116
  }
4625
5117
  }
5118
+ txWithParams(key, fallback, params) {
5119
+ try {
5120
+ return this.interpolateTranslationParams(translateCrudRuntimeText(this.i18n, key, fallback, params), params);
5121
+ }
5122
+ catch {
5123
+ return this.interpolateTranslationParams(fallback, params);
5124
+ }
5125
+ }
5126
+ interpolateTranslationParams(text, params) {
5127
+ return Object.entries(params).reduce((current, [name, value]) => current.replaceAll(`{${name}}`, value), text);
5128
+ }
4626
5129
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisCrudComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4627
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", 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", rowClick: "rowClick", selectionChange: "selectionChange", tableRuntimeConfigChange: "tableRuntimeConfigChange", crudAuthoringDocumentApplied: "crudAuthoringDocumentApplied", crudAuthoringDocumentSaved: "crudAuthoringDocumentSaved" }, providers: [
5130
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisCrudComponent, isStandalone: true, selector: "praxis-crud", inputs: { metadata: "metadata", crudId: "crudId", componentInstanceId: "componentInstanceId", context: "context", enableCustomization: "enableCustomization", authoringCapability: "authoringCapability" }, outputs: { configureRequested: "configureRequested", afterOpen: "afterOpen", afterClose: "afterClose", afterSave: "afterSave", afterDelete: "afterDelete", error: "error", rowClick: "rowClick", selectionChange: "selectionChange", tableRuntimeConfigChange: "tableRuntimeConfigChange", crudAuthoringDocumentApplied: "crudAuthoringDocumentApplied", crudAuthoringDocumentSaved: "crudAuthoringDocumentSaved" }, providers: [
4628
5131
  providePraxisI18nConfig(RESOURCE_DISCOVERY_I18N_CONFIG),
4629
5132
  providePraxisI18nConfig(PRAXIS_CRUD_RUNTIME_I18N_CONFIG),
4630
5133
  ], viewQueries: [{ propertyName: "table", first: true, predicate: PraxisTable, descendants: true }], usesOnChanges: true, ngImport: i0, template: `
@@ -4638,17 +5141,19 @@ class PraxisCrudComponent {
4638
5141
  [tableId]="crudId || 'default'"
4639
5142
  [crudContext]="tableCrudContext"
4640
5143
  [enableCustomization]="enableCustomization"
5144
+ [authoringCapability]="authoringCapability"
4641
5145
  (rowClick)="onTableRowClick($event)"
4642
5146
  (selectionChange)="onTableSelectionChange($event)"
4643
5147
  (rowAction)="onAction($event.action, $event.row, $event)"
4644
5148
  (toolbarAction)="onAction($event.action)"
5149
+ (bulkAction)="onBulkAction($event)"
4645
5150
  (collectionLinksChange)="onCollectionLinksChange($event)"
4646
5151
  (reset)="onResetPreferences()"
4647
5152
  (metadataChange)="onTableMetadataChange()"
4648
5153
  (loadingStateChange)="onTableLoadingStateChange($event)"
4649
5154
  ></praxis-table>
4650
5155
  } @else {
4651
- @if (enableCustomization) {
5156
+ @if (isCustomizationAvailable()) {
4652
5157
  <praxis-empty-state-card
4653
5158
  icon="table_rows"
4654
5159
  [title]="getEmptyStateTitle()"
@@ -4657,7 +5162,7 @@ class PraxisCrudComponent {
4657
5162
  />
4658
5163
  }
4659
5164
  }
4660
- `, 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", "configPersistenceStrategy", "title", "subtitle", "icon", "autoDelete", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "crudContext", "filterCriteria", "queryContext", "aiContext", "aiAssistantVoiceInputMode", "aiAssistantVoiceLanguage", "horizontalScroll", "enableCustomization", "dense"], outputs: ["rowClick", "widgetEvent", "rowDoubleClick", "rowExpansionChange", "rowAction", "toolbarAction", "bulkAction", "exportAction", "columnReorder", "columnReorderAttempt", "beforeDelete", "afterDelete", "deleteError", "beforeBulkDelete", "afterBulkDelete", "bulkDeleteError", "schemaStatusChange", "configChange", "metadataChange", "loadingStateChange", "collectionLinksChange", "selectionChange"] }, { kind: "component", type: EmptyStateCardComponent, selector: "praxis-empty-state-card", inputs: ["icon", "title", "description", "primaryAction", "secondaryActions", "inline", "tone"] }] });
5165
+ `, 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", "configPersistenceStrategy", "title", "subtitle", "icon", "autoDelete", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "crudContext", "filterCriteria", "queryContext", "aiContext", "aiAssistantVoiceInputMode", "aiAssistantVoiceLanguage", "horizontalScroll", "enableCustomization", "authoringCapability", "dense"], outputs: ["rowClick", "widgetEvent", "resourceEvent", "rowDoubleClick", "rowExpansionChange", "rowAction", "toolbarAction", "bulkAction", "exportAction", "columnReorder", "columnReorderAttempt", "columnResize", "beforeDelete", "afterDelete", "deleteError", "beforeBulkDelete", "afterBulkDelete", "bulkDeleteError", "schemaStatusChange", "configChange", "metadataChange", "loadingStateChange", "collectionLinksChange", "selectionChange"] }, { kind: "component", type: EmptyStateCardComponent, selector: "praxis-empty-state-card", inputs: ["icon", "title", "description", "primaryAction", "secondaryActions", "inline", "tone", "variant", "alignment", "density", "iconContainer"] }] });
4661
5166
  }
4662
5167
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisCrudComponent, decorators: [{
4663
5168
  type: Component,
@@ -4675,17 +5180,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
4675
5180
  [tableId]="crudId || 'default'"
4676
5181
  [crudContext]="tableCrudContext"
4677
5182
  [enableCustomization]="enableCustomization"
5183
+ [authoringCapability]="authoringCapability"
4678
5184
  (rowClick)="onTableRowClick($event)"
4679
5185
  (selectionChange)="onTableSelectionChange($event)"
4680
5186
  (rowAction)="onAction($event.action, $event.row, $event)"
4681
5187
  (toolbarAction)="onAction($event.action)"
5188
+ (bulkAction)="onBulkAction($event)"
4682
5189
  (collectionLinksChange)="onCollectionLinksChange($event)"
4683
5190
  (reset)="onResetPreferences()"
4684
5191
  (metadataChange)="onTableMetadataChange()"
4685
5192
  (loadingStateChange)="onTableLoadingStateChange($event)"
4686
5193
  ></praxis-table>
4687
5194
  } @else {
4688
- @if (enableCustomization) {
5195
+ @if (isCustomizationAvailable()) {
4689
5196
  <praxis-empty-state-card
4690
5197
  icon="table_rows"
4691
5198
  [title]="getEmptyStateTitle()"
@@ -4695,7 +5202,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
4695
5202
  }
4696
5203
  }
4697
5204
  `, styles: [":host{display:block;width:100%;min-width:0;max-width:100%}\n"] }]
4698
- }], propDecorators: { metadata: [{
5205
+ }], ctorParameters: () => [], propDecorators: { metadata: [{
4699
5206
  type: Input,
4700
5207
  args: [{ required: true }]
4701
5208
  }], crudId: [{
@@ -4707,6 +5214,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
4707
5214
  type: Input
4708
5215
  }], enableCustomization: [{
4709
5216
  type: Input
5217
+ }], authoringCapability: [{
5218
+ type: Input
4710
5219
  }], configureRequested: [{
4711
5220
  type: Output
4712
5221
  }], afterOpen: [{
@@ -4734,6 +5243,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
4734
5243
  args: [PraxisTable]
4735
5244
  }] } });
4736
5245
 
5246
+ function isCrudDebugEnabled() {
5247
+ try {
5248
+ if (globalThis.__PRAXIS_DEBUG_CRUD__) {
5249
+ return true;
5250
+ }
5251
+ if (typeof localStorage !== 'undefined') {
5252
+ return localStorage.getItem('praxis.debug.crud') === 'true';
5253
+ }
5254
+ }
5255
+ catch { }
5256
+ return false;
5257
+ }
5258
+ function debugCrudHost(message, data) {
5259
+ if (!isCrudDebugEnabled()) {
5260
+ return;
5261
+ }
5262
+ try {
5263
+ console.debug(message, data);
5264
+ }
5265
+ catch { }
5266
+ }
4737
5267
  class DynamicFormDialogHostComponent {
4738
5268
  dialogRef;
4739
5269
  data;
@@ -4742,8 +5272,10 @@ class DynamicFormDialogHostComponent {
4742
5272
  configStorage;
4743
5273
  formComp;
4744
5274
  modal = {};
5275
+ presentation = 'modal';
4745
5276
  maximized = false;
4746
5277
  initialSize = {};
5278
+ initialPosition;
4747
5279
  rememberState = false;
4748
5280
  stateKey;
4749
5281
  backDefaults = {};
@@ -4751,12 +5283,16 @@ class DynamicFormDialogHostComponent {
4751
5283
  resourcePath;
4752
5284
  resourceId;
4753
5285
  initialValue;
5286
+ resourceIdentity = null;
4754
5287
  schemaUrl;
4755
5288
  submitUrl;
4756
5289
  submitMethod;
4757
5290
  apiEndpointKey;
4758
5291
  apiUrlEntry;
5292
+ layoutPolicy;
5293
+ formConfig = {};
4759
5294
  formActions;
5295
+ formConfigPersistenceStrategy = 'input-first';
4760
5296
  mode = 'create';
4761
5297
  backConfig;
4762
5298
  idField = 'id';
@@ -4778,6 +5314,7 @@ class DynamicFormDialogHostComponent {
4778
5314
  this.crud = crud;
4779
5315
  this.configStorage = configStorage;
4780
5316
  this.dialogRef.disableClose = true;
5317
+ this.presentation = this.data.presentation === 'drawer' ? 'drawer' : 'modal';
4781
5318
  // i18n
4782
5319
  this.texts = {
4783
5320
  ...this.texts,
@@ -4788,6 +5325,9 @@ class DynamicFormDialogHostComponent {
4788
5325
  canMaximize: true,
4789
5326
  ...(this.data.metadata?.defaults?.modal || {}),
4790
5327
  };
5328
+ this.initialPosition = this.presentation === 'drawer'
5329
+ ? (this.data.dialogPosition ?? this.modal.position ?? { right: '0', top: '0' })
5330
+ : undefined;
4791
5331
  this.rememberState = !!this.modal.rememberLastState;
4792
5332
  this.stateKey = this.data.action?.formId
4793
5333
  ? `crud-dialog-state:${this.data.action.formId}`
@@ -4812,12 +5352,17 @@ class DynamicFormDialogHostComponent {
4812
5352
  this.apiUrlEntry = this.data.inputs?.['apiUrlEntry'] ?? null;
4813
5353
  const act = this.data.action?.action;
4814
5354
  this.mode = act === 'edit' ? 'edit' : act === 'view' ? 'view' : 'create';
5355
+ this.resourceIdentity = this.presentation === 'drawer' && this.mode === 'edit' && this.data.resourceIdentity
5356
+ ? this.data.resourceIdentity
5357
+ : null;
5358
+ this.formConfig = this.resolveFormConfig();
5359
+ this.layoutPolicy = this.resolveLayoutPolicy();
4815
5360
  this.formActions = this.resolveFormActions();
4816
5361
  // Back config: defaults from metadata/action, overridden by saved per-form config
4817
5362
  const defaults = (this.data.action?.back || this.data.metadata?.defaults?.back) || {};
4818
5363
  this.backDefaults = defaults;
4819
5364
  this.backConfig = { ...defaults };
4820
- console.debug('[CRUD:Host] constructed', {
5365
+ debugCrudHost('[CRUD:Host] constructed', {
4821
5366
  action: this.data?.action,
4822
5367
  resourcePath: this.resourcePath,
4823
5368
  resourceId: this.resourceId,
@@ -4857,6 +5402,7 @@ class DynamicFormDialogHostComponent {
4857
5402
  'apiEndpointKey',
4858
5403
  'apiUrlEntry',
4859
5404
  'initialValue',
5405
+ 'layoutPolicy',
4860
5406
  ]);
4861
5407
  const explicit = inputs['initialValue'] && typeof inputs['initialValue'] === 'object'
4862
5408
  ? { ...inputs['initialValue'] }
@@ -4869,10 +5415,50 @@ class DynamicFormDialogHostComponent {
4869
5415
  }
4870
5416
  return Object.keys(explicit).length ? explicit : null;
4871
5417
  }
5418
+ resolveFormConfig() {
5419
+ const config = this.data.metadata?.form;
5420
+ return config && typeof config === 'object' ? config : {};
5421
+ }
5422
+ get dialogTitle() {
5423
+ const actionLabel = this.resolveActionDialogTitle();
5424
+ return actionLabel ||
5425
+ stringOrUndefined(this.formConfig['title']) ||
5426
+ deriveModeTitle(this.mode, this.resolveResourceTitle()) ||
5427
+ this.texts.title;
5428
+ }
5429
+ resolveActionDialogTitle() {
5430
+ const actionLabel = stringOrUndefined(this.data.action?.label);
5431
+ if (!actionLabel) {
5432
+ return undefined;
5433
+ }
5434
+ const resource = this.data.metadata?.resource ?? {};
5435
+ const table = this.data.metadata?.table ?? {};
5436
+ const businessTitle = stringOrUndefined(resource.formTitle ??
5437
+ resource.title ??
5438
+ resource.label ??
5439
+ table.title ??
5440
+ table.label);
5441
+ const routeFallbackTitle = deriveModeTitle(this.mode, titleFromResourcePath(this.resourcePath));
5442
+ return businessTitle && routeFallbackTitle && sameTitle(actionLabel, routeFallbackTitle)
5443
+ ? undefined
5444
+ : actionLabel;
5445
+ }
5446
+ resolveResourceTitle() {
5447
+ const resource = this.data.metadata?.resource ?? {};
5448
+ const table = this.data.metadata?.table ?? {};
5449
+ return stringOrUndefined(resource.formTitle ??
5450
+ resource.title ??
5451
+ resource.label ??
5452
+ table.title ??
5453
+ table.label) || titleFromResourcePath(this.resourcePath);
5454
+ }
4872
5455
  resolveFormActions() {
4873
5456
  if (this.mode === 'view') {
4874
5457
  return undefined;
4875
5458
  }
5459
+ if (this.formConfig?.actions) {
5460
+ return undefined;
5461
+ }
4876
5462
  const submitLabel = this.resolveSubmitLabel();
4877
5463
  if (!submitLabel) {
4878
5464
  return undefined;
@@ -4890,6 +5476,15 @@ class DynamicFormDialogHostComponent {
4890
5476
  },
4891
5477
  };
4892
5478
  }
5479
+ resolveLayoutPolicy() {
5480
+ const explicit = this.data.inputs?.['layoutPolicy'] ??
5481
+ this.data.action?.form?.layoutPolicy ??
5482
+ this.data.action?.layoutPolicy;
5483
+ if (explicit && typeof explicit === 'object') {
5484
+ return explicit;
5485
+ }
5486
+ return null;
5487
+ }
4893
5488
  resolveSubmitLabel() {
4894
5489
  const action = this.data.action ?? {};
4895
5490
  const explicit = stringOrUndefined(action.form?.submitLabel ??
@@ -4962,10 +5557,10 @@ class DynamicFormDialogHostComponent {
4962
5557
  ref
4963
5558
  .afterClosed()
4964
5559
  .pipe(filter((confirmed) => !!confirmed), takeUntilDestroyed(this.destroyRef))
4965
- .subscribe(() => this.dialogRef.close());
5560
+ .subscribe(() => this.dialogRef.close({ type: 'close' }));
4966
5561
  }
4967
5562
  else {
4968
- this.dialogRef.close();
5563
+ this.dialogRef.close({ type: 'close' });
4969
5564
  }
4970
5565
  }
4971
5566
  toggleMaximize(initial = false) {
@@ -4978,11 +5573,22 @@ class DynamicFormDialogHostComponent {
4978
5573
  ? `calc(100dvh - ${2 * (gap ?? 0)}px)`
4979
5574
  : this.initialSize.height;
4980
5575
  this.dialogRef.updateSize(width, height);
4981
- this.dialogRef.updatePosition();
5576
+ this.updateHostPosition();
4982
5577
  const pane = this.dialogRef
4983
5578
  ?._containerInstance?._elementRef?.nativeElement?.parentElement;
4984
5579
  if (pane && pane.classList.contains('pfx-dialog-pane')) {
4985
- pane.style.margin = this.maximized ? `${gap}px` : '';
5580
+ if (this.presentation === 'drawer') {
5581
+ pane.classList.toggle('pfx-drawer-maximized', this.maximized);
5582
+ if (this.maximized) {
5583
+ pane.style.setProperty('--pfx-drawer-edge-gap', `${gap ?? 0}px`);
5584
+ }
5585
+ else {
5586
+ pane.style.removeProperty('--pfx-drawer-edge-gap');
5587
+ }
5588
+ }
5589
+ else {
5590
+ pane.style.margin = this.maximized ? `${gap}px` : '';
5591
+ }
4986
5592
  }
4987
5593
  this.saveState();
4988
5594
  }
@@ -5008,7 +5614,7 @@ class DynamicFormDialogHostComponent {
5008
5614
  width: saved?.width ?? this.modal.width,
5009
5615
  height: saved?.height ?? this.modal.height,
5010
5616
  };
5011
- console.debug('[CRUD:Host] ngOnInit', {
5617
+ debugCrudHost('[CRUD:Host] ngOnInit', {
5012
5618
  initialSize: this.initialSize,
5013
5619
  startMaximized: this.modal.startMaximized,
5014
5620
  fullscreenBreakpoint: this.modal.fullscreenBreakpoint,
@@ -5028,14 +5634,34 @@ class DynamicFormDialogHostComponent {
5028
5634
  }
5029
5635
  else if (this.initialSize.width || this.initialSize.height) {
5030
5636
  this.dialogRef.updateSize(this.initialSize.width, this.initialSize.height);
5637
+ this.updateHostPosition();
5638
+ }
5639
+ }
5640
+ updateHostPosition() {
5641
+ if (this.presentation !== 'drawer') {
5031
5642
  this.dialogRef.updatePosition();
5643
+ return;
5032
5644
  }
5645
+ this.dialogRef.updatePosition(this.maximized
5646
+ ? this.resolveMaximizedDrawerPosition()
5647
+ : this.initialPosition);
5648
+ }
5649
+ resolveMaximizedDrawerPosition() {
5650
+ const position = this.initialPosition ?? { right: '0', top: '0' };
5651
+ const offset = `${this.modal.edgeGap ?? 8}px`;
5652
+ const horizontal = position.left != null
5653
+ ? { left: offset }
5654
+ : { right: offset };
5655
+ const vertical = position.bottom != null && position.top == null
5656
+ ? { bottom: offset }
5657
+ : { top: offset };
5658
+ return { ...horizontal, ...vertical };
5033
5659
  }
5034
5660
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicFormDialogHostComponent, deps: [{ token: MatDialogRef }, { token: MAT_DIALOG_DATA }, { token: DialogService }, { token: i2$1.GenericCrudService }, { token: ASYNC_CONFIG_STORAGE }], target: i0.ɵɵFactoryTarget.Component });
5035
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: DynamicFormDialogHostComponent, isStandalone: true, selector: "praxis-dynamic-form-dialog-host", host: { properties: { "attr.data-density": "modal.density || \"default\"" }, classAttribute: "praxis-dialog" }, providers: [GenericCrudService], viewQueries: [{ propertyName: "formComp", first: true, predicate: PraxisDynamicForm, descendants: true }], ngImport: i0, template: `
5661
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: DynamicFormDialogHostComponent, isStandalone: true, selector: "praxis-dynamic-form-dialog-host", host: { properties: { "attr.data-density": "modal.density || \"default\"", "attr.data-presentation": "presentation", "class.praxis-drawer": "presentation === \"drawer\"" }, classAttribute: "praxis-dialog" }, providers: [GenericCrudService], viewQueries: [{ propertyName: "formComp", first: true, predicate: PraxisDynamicForm, descendants: true }], ngImport: i0, template: `
5036
5662
  <div mat-dialog-title class="dialog-header">
5037
5663
  <h2 id="crudDialogTitle" class="dialog-title">
5038
- {{ data.action?.label || texts.title }}
5664
+ {{ dialogTitle }}
5039
5665
  </h2>
5040
5666
  <span class="spacer"></span>
5041
5667
  @if (modal.canMaximize) {
@@ -5063,17 +5689,25 @@ class DynamicFormDialogHostComponent {
5063
5689
  class="dialog-content"
5064
5690
  aria-labelledby="crudDialogTitle"
5065
5691
  >
5692
+ @if (resourceIdentity) {
5693
+ <section class="crud-drawer-resource-identity" data-testid="crud-drawer-resource-identity">
5694
+ <praxis-resource-identity [identity]="resourceIdentity"></praxis-resource-identity>
5695
+ </section>
5696
+ }
5066
5697
  <praxis-dynamic-form
5067
5698
  [formId]="data.action?.formId"
5068
5699
  [resourcePath]="resourcePath"
5069
5700
  [resourceId]="resourceId"
5070
5701
  [initialValue]="initialValue"
5071
5702
  [mode]="mode"
5703
+ [config]="formConfig"
5072
5704
  [schemaUrl]="schemaUrl"
5073
5705
  [submitUrl]="submitUrl"
5074
5706
  [submitMethod]="submitMethod"
5075
5707
  [apiEndpointKey]="apiEndpointKey"
5076
5708
  [apiUrlEntry]="apiUrlEntry"
5709
+ [configPersistenceStrategy]="formConfigPersistenceStrategy"
5710
+ [layoutPolicy]="layoutPolicy"
5077
5711
  [presentationModeGlobal]="mode === 'view' ? true : null"
5078
5712
  [backConfig]="backConfig"
5079
5713
  [actions]="formActions"
@@ -5081,7 +5715,7 @@ class DynamicFormDialogHostComponent {
5081
5715
  (formCancel)="onCancel()"
5082
5716
  ></praxis-dynamic-form>
5083
5717
  </mat-dialog-content>
5084
- `, 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: 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: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i6.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", "initialValue", "editorialContext", "mode", "config", "actions", "schemaSource", "schemaUrl", "readUrl", "submitUrl", "submitMethod", "responseSchemaUrl", "apiEndpointKey", "apiUrlEntry", "enableCustomization", "showAiAssistant", "formId", "componentInstanceId", "configPersistenceStrategy", "layout", "backConfig", "hooks", "removeEmptyContainersOnSave", "reactiveValidation", "reactiveValidationDebounceMs", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "readonlyModeGlobal", "disabledModeGlobal", "presentationModeGlobal", "visibleGlobal", "domainRules", "customEndpoints"], outputs: ["formSubmit", "formCancel", "formReset", "configChange", "configPatchChange", "formReady", "valueChange", "syncCompleted", "initializationError", "loadingStateChange", "enableCustomizationChange", "customAction", "actionConfirmation", "schemaStatusChange", "fieldRenderError", "ruleDiagnosticsChange"] }] });
5718
+ `, isInline: true, styles: ["praxis-dynamic-form-dialog-host{--dlg-header-h: 56px;--dlg-footer-h: 56px;--dlg-pad: 16px;display:flex;flex-direction:column;height:100%;overflow:hidden}praxis-dynamic-form-dialog-host[data-density=compact]{--dlg-header-h: 44px;--dlg-footer-h: 44px;--dlg-pad: 12px}praxis-dynamic-form-dialog-host .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)}praxis-dynamic-form-dialog-host .dialog-title{margin:0;font:inherit;font-weight:600;color:var(--md-sys-color-on-surface)}praxis-dynamic-form-dialog-host .spacer{flex:1}praxis-dynamic-form-dialog-host .dialog-content{flex:1 1 auto;overflow:auto;padding:var(--dlg-pad);max-height:calc(100svh - var(--dlg-header-h) - 32px)}praxis-dynamic-form-dialog-host.praxis-drawer{--dlg-header-h: 64px;width:100%;min-width:0;max-width:100vw;height:100dvh;background:var(--pfx-form-surface, var(--md-sys-color-surface));color:var(--md-sys-color-on-surface)}praxis-dynamic-form-dialog-host.praxis-drawer .dialog-header{background:color-mix(in srgb,var(--md-sys-color-surface-container-high, var(--md-sys-color-surface)),transparent 4%)}praxis-dynamic-form-dialog-host.praxis-drawer .dialog-content{max-height:none;min-height:0;padding:clamp(12px,2.4vw,24px)}praxis-dynamic-form-dialog-host .dialog-header button.mat-icon-button{color:var(--md-sys-color-on-surface-variant)}praxis-dynamic-form-dialog-host .dialog-header button.mat-icon-button:hover{color:var(--md-sys-color-primary);background:var(--md-sys-color-primary-container)}praxis-dynamic-form-dialog-host .dialog-footer{position:sticky;bottom:0;z-index:1;padding:var(--dlg-pad)}.pfx-blur-backdrop{background-color:var(--pfx-backdrop, rgba(15, 23, 42, .42))!important;backdrop-filter:blur(var(--pfx-backdrop-blur, 10px)) saturate(110%);-webkit-backdrop-filter:blur(var(--pfx-backdrop-blur, 10px)) saturate(110%)}.pfx-transparent-backdrop{background-color:transparent!important}.cdk-overlay-pane.pfx-dialog-pane{overflow:hidden;transition:width .2s ease,height .2s ease,margin .2s ease}.cdk-overlay-pane.pfx-dialog-pane .mat-mdc-dialog-surface,.cdk-overlay-pane.pfx-dialog-pane .mdc-dialog__surface{display:flex;flex-direction:column;width:100%;height:100%;max-height:inherit;overflow:hidden;background:var(--pfx-form-surface, var(--md-sys-color-surface))!important;border:1px solid var(--pfx-form-stroke, var(--md-sys-color-outline-variant))!important}.cdk-overlay-pane.pfx-dialog-pane.pfx-dialog-frosted .mat-mdc-dialog-surface,.cdk-overlay-pane.pfx-dialog-pane.pfx-dialog-frosted .mdc-dialog__surface{backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}.cdk-overlay-pane.pfx-drawer-pane{margin:0!important;border-radius:0!important;box-shadow:var(--md-sys-elevation-level3, 0 24px 80px rgba(15, 23, 42, .28)),0 0 0 1px color-mix(in srgb,var(--md-sys-color-outline, #64748b) 24%,transparent)}.cdk-overlay-pane.pfx-drawer-pane.pfx-drawer-maximized{margin:var(--pfx-drawer-edge-gap, 8px)!important}.cdk-overlay-pane.pfx-drawer-pane .mat-mdc-dialog-surface,.cdk-overlay-pane.pfx-drawer-pane .mdc-dialog__surface{border-radius:0!important}\n"], dependencies: [{ 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: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i6.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: PraxisResourceIdentityComponent, selector: "praxis-resource-identity", inputs: ["identity", "emptyTitle", "density", "showMetadataLabels"] }, { kind: "component", type: PraxisDynamicForm, selector: "praxis-dynamic-form", inputs: ["resourcePath", "resourceId", "initialValue", "editorialContext", "mode", "config", "actions", "schemaSource", "schemaUrl", "readUrl", "submitUrl", "submitMethod", "responseSchemaUrl", "apiEndpointKey", "apiUrlEntry", "enableCustomization", "showAiAssistant", "formId", "componentInstanceId", "configPersistenceStrategy", "layout", "generatedLayoutPreset", "layoutPolicy", "backConfig", "hooks", "removeEmptyContainersOnSave", "reactiveValidation", "reactiveValidationDebounceMs", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "readonlyModeGlobal", "disabledModeGlobal", "presentationModeGlobal", "visibleGlobal", "fieldIconPolicy", "domainRules", "customEndpoints"], outputs: ["formSubmit", "formCancel", "formReset", "configChange", "configPatchChange", "formReady", "valueChange", "syncCompleted", "initializationError", "loadingStateChange", "enableCustomizationChange", "customAction", "actionConfirmation", "schemaStatusChange", "fieldRenderError", "ruleDiagnosticsChange"] }], encapsulation: i0.ViewEncapsulation.None });
5085
5719
  }
5086
5720
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicFormDialogHostComponent, decorators: [{
5087
5721
  type: Component,
@@ -5090,14 +5724,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5090
5724
  MatButtonModule,
5091
5725
  MatIconModule,
5092
5726
  PraxisIconDirective,
5727
+ PraxisResourceIdentityComponent,
5093
5728
  PraxisDynamicForm
5094
- ], providers: [GenericCrudService], host: {
5729
+ ], encapsulation: ViewEncapsulation.None, providers: [GenericCrudService], host: {
5095
5730
  class: 'praxis-dialog',
5096
5731
  '[attr.data-density]': 'modal.density || "default"',
5732
+ '[attr.data-presentation]': 'presentation',
5733
+ '[class.praxis-drawer]': 'presentation === "drawer"',
5097
5734
  }, template: `
5098
5735
  <div mat-dialog-title class="dialog-header">
5099
5736
  <h2 id="crudDialogTitle" class="dialog-title">
5100
- {{ data.action?.label || texts.title }}
5737
+ {{ dialogTitle }}
5101
5738
  </h2>
5102
5739
  <span class="spacer"></span>
5103
5740
  @if (modal.canMaximize) {
@@ -5125,17 +5762,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5125
5762
  class="dialog-content"
5126
5763
  aria-labelledby="crudDialogTitle"
5127
5764
  >
5765
+ @if (resourceIdentity) {
5766
+ <section class="crud-drawer-resource-identity" data-testid="crud-drawer-resource-identity">
5767
+ <praxis-resource-identity [identity]="resourceIdentity"></praxis-resource-identity>
5768
+ </section>
5769
+ }
5128
5770
  <praxis-dynamic-form
5129
5771
  [formId]="data.action?.formId"
5130
5772
  [resourcePath]="resourcePath"
5131
5773
  [resourceId]="resourceId"
5132
5774
  [initialValue]="initialValue"
5133
5775
  [mode]="mode"
5776
+ [config]="formConfig"
5134
5777
  [schemaUrl]="schemaUrl"
5135
5778
  [submitUrl]="submitUrl"
5136
5779
  [submitMethod]="submitMethod"
5137
5780
  [apiEndpointKey]="apiEndpointKey"
5138
5781
  [apiUrlEntry]="apiUrlEntry"
5782
+ [configPersistenceStrategy]="formConfigPersistenceStrategy"
5783
+ [layoutPolicy]="layoutPolicy"
5139
5784
  [presentationModeGlobal]="mode === 'view' ? true : null"
5140
5785
  [backConfig]="backConfig"
5141
5786
  [actions]="formActions"
@@ -5143,7 +5788,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5143
5788
  (formCancel)="onCancel()"
5144
5789
  ></praxis-dynamic-form>
5145
5790
  </mat-dialog-content>
5146
- `, 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"] }]
5791
+ `, styles: ["praxis-dynamic-form-dialog-host{--dlg-header-h: 56px;--dlg-footer-h: 56px;--dlg-pad: 16px;display:flex;flex-direction:column;height:100%;overflow:hidden}praxis-dynamic-form-dialog-host[data-density=compact]{--dlg-header-h: 44px;--dlg-footer-h: 44px;--dlg-pad: 12px}praxis-dynamic-form-dialog-host .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)}praxis-dynamic-form-dialog-host .dialog-title{margin:0;font:inherit;font-weight:600;color:var(--md-sys-color-on-surface)}praxis-dynamic-form-dialog-host .spacer{flex:1}praxis-dynamic-form-dialog-host .dialog-content{flex:1 1 auto;overflow:auto;padding:var(--dlg-pad);max-height:calc(100svh - var(--dlg-header-h) - 32px)}praxis-dynamic-form-dialog-host.praxis-drawer{--dlg-header-h: 64px;width:100%;min-width:0;max-width:100vw;height:100dvh;background:var(--pfx-form-surface, var(--md-sys-color-surface));color:var(--md-sys-color-on-surface)}praxis-dynamic-form-dialog-host.praxis-drawer .dialog-header{background:color-mix(in srgb,var(--md-sys-color-surface-container-high, var(--md-sys-color-surface)),transparent 4%)}praxis-dynamic-form-dialog-host.praxis-drawer .dialog-content{max-height:none;min-height:0;padding:clamp(12px,2.4vw,24px)}praxis-dynamic-form-dialog-host .dialog-header button.mat-icon-button{color:var(--md-sys-color-on-surface-variant)}praxis-dynamic-form-dialog-host .dialog-header button.mat-icon-button:hover{color:var(--md-sys-color-primary);background:var(--md-sys-color-primary-container)}praxis-dynamic-form-dialog-host .dialog-footer{position:sticky;bottom:0;z-index:1;padding:var(--dlg-pad)}.pfx-blur-backdrop{background-color:var(--pfx-backdrop, rgba(15, 23, 42, .42))!important;backdrop-filter:blur(var(--pfx-backdrop-blur, 10px)) saturate(110%);-webkit-backdrop-filter:blur(var(--pfx-backdrop-blur, 10px)) saturate(110%)}.pfx-transparent-backdrop{background-color:transparent!important}.cdk-overlay-pane.pfx-dialog-pane{overflow:hidden;transition:width .2s ease,height .2s ease,margin .2s ease}.cdk-overlay-pane.pfx-dialog-pane .mat-mdc-dialog-surface,.cdk-overlay-pane.pfx-dialog-pane .mdc-dialog__surface{display:flex;flex-direction:column;width:100%;height:100%;max-height:inherit;overflow:hidden;background:var(--pfx-form-surface, var(--md-sys-color-surface))!important;border:1px solid var(--pfx-form-stroke, var(--md-sys-color-outline-variant))!important}.cdk-overlay-pane.pfx-dialog-pane.pfx-dialog-frosted .mat-mdc-dialog-surface,.cdk-overlay-pane.pfx-dialog-pane.pfx-dialog-frosted .mdc-dialog__surface{backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}.cdk-overlay-pane.pfx-drawer-pane{margin:0!important;border-radius:0!important;box-shadow:var(--md-sys-elevation-level3, 0 24px 80px rgba(15, 23, 42, .28)),0 0 0 1px color-mix(in srgb,var(--md-sys-color-outline, #64748b) 24%,transparent)}.cdk-overlay-pane.pfx-drawer-pane.pfx-drawer-maximized{margin:var(--pfx-drawer-edge-gap, 8px)!important}.cdk-overlay-pane.pfx-drawer-pane .mat-mdc-dialog-surface,.cdk-overlay-pane.pfx-drawer-pane .mdc-dialog__surface{border-radius:0!important}\n"] }]
5147
5792
  }], ctorParameters: () => [{ type: undefined, decorators: [{
5148
5793
  type: Inject,
5149
5794
  args: [MatDialogRef]
@@ -5170,6 +5815,17 @@ function stringOrUndefined(value) {
5170
5815
  const text = String(value ?? '').trim();
5171
5816
  return text || undefined;
5172
5817
  }
5818
+ function sameTitle(left, right) {
5819
+ return normalizeTitle(left) === normalizeTitle(right);
5820
+ }
5821
+ function normalizeTitle(value) {
5822
+ return value
5823
+ .normalize('NFD')
5824
+ .replace(/[\u0300-\u036f]/g, '')
5825
+ .replace(/\s+/g, ' ')
5826
+ .trim()
5827
+ .toLocaleLowerCase('pt-BR');
5828
+ }
5173
5829
  function deriveCreateSubmitLabel(actionLabel) {
5174
5830
  if (!actionLabel) {
5175
5831
  return 'Criar';
@@ -5180,6 +5836,61 @@ function deriveCreateSubmitLabel(actionLabel) {
5180
5836
  }
5181
5837
  return actionLabel;
5182
5838
  }
5839
+ function deriveModeTitle(mode, resourceTitle) {
5840
+ if (!resourceTitle) {
5841
+ return undefined;
5842
+ }
5843
+ const singular = lowerFirstLetter(singularizePtBrResourceTitle(resourceTitle));
5844
+ if (mode === 'create') {
5845
+ return `Adicionar ${singular}`;
5846
+ }
5847
+ if (mode === 'edit') {
5848
+ return `Editar ${singular}`;
5849
+ }
5850
+ return `Visualizar ${singular}`;
5851
+ }
5852
+ function titleFromResourcePath(resourcePath) {
5853
+ const lastSegment = resourcePath?.split('/').filter(Boolean).pop();
5854
+ if (!lastSegment) {
5855
+ return undefined;
5856
+ }
5857
+ return lastSegment
5858
+ .replace(/[-_]+/g, ' ')
5859
+ .replace(/\s+/g, ' ')
5860
+ .trim()
5861
+ .toLocaleLowerCase('pt-BR');
5862
+ }
5863
+ function singularizePtBrResourceTitle(title) {
5864
+ const normalized = title.trim();
5865
+ const lower = normalized.toLocaleLowerCase('pt-BR');
5866
+ if (lower === 'códigos de frequência' || lower === 'codigos de frequencia') {
5867
+ return 'código de frequência';
5868
+ }
5869
+ const [first, ...rest] = normalized.split(/\s+/);
5870
+ const singularFirst = singularizePtBrWord(first);
5871
+ return [singularFirst, ...rest].join(' ');
5872
+ }
5873
+ function singularizePtBrWord(word) {
5874
+ if (/ões$/i.test(word)) {
5875
+ return word.replace(/ões$/i, 'ão');
5876
+ }
5877
+ if (/ais$/i.test(word)) {
5878
+ return word.replace(/ais$/i, 'al');
5879
+ }
5880
+ if (/eis$/i.test(word)) {
5881
+ return word.replace(/eis$/i, 'el');
5882
+ }
5883
+ if (/res$/i.test(word)) {
5884
+ return word.replace(/es$/i, '');
5885
+ }
5886
+ if (/s$/i.test(word) && !/ss$/i.test(word)) {
5887
+ return word.replace(/s$/i, '');
5888
+ }
5889
+ return word;
5890
+ }
5891
+ function lowerFirstLetter(value) {
5892
+ return value.replace(/\p{L}/u, (letter) => letter.toLocaleLowerCase('pt-BR'));
5893
+ }
5183
5894
 
5184
5895
  var dynamicFormDialogHost_component = /*#__PURE__*/Object.freeze({
5185
5896
  __proto__: null,
@@ -5229,6 +5940,7 @@ class PraxisCrudWidgetConfigEditor {
5229
5940
  componentInstanceId: this.inputs?.componentInstanceId ?? this.widgetKey,
5230
5941
  context: this.inputs?.context,
5231
5942
  enableCustomization: this.inputs?.enableCustomization ?? false,
5943
+ authoringCapability: this.inputs?.authoringCapability ?? null,
5232
5944
  };
5233
5945
  return {
5234
5946
  inputs,
@@ -5325,11 +6037,31 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
5325
6037
  type: 'PraxisDataQueryContext | null',
5326
6038
  description: 'Contexto semântico de consulta encaminhado para a tabela interna do CRUD. Preferir este contrato para novo authoring remoto.',
5327
6039
  },
6040
+ {
6041
+ name: 'metadata.resource.schemaPath',
6042
+ type: 'string | null',
6043
+ description: 'Template canonico opcional usado apenas para resolver /schemas/filtered quando resource.path precisa ser um alvo operacional concreto.',
6044
+ },
6045
+ {
6046
+ name: 'metadata.resource.title',
6047
+ type: 'string | null',
6048
+ description: 'Titulo documental do recurso usado pelo shell CRUD para derivar titulos contextuais de criacao, edicao e visualizacao quando a action nao informa label.',
6049
+ },
6050
+ {
6051
+ name: 'metadata.resource.formTitle',
6052
+ type: 'string | null',
6053
+ description: 'Titulo documental especifico para formularios do recurso, usado antes de metadata.resource.title no shell modal/drawer.',
6054
+ },
5328
6055
  {
5329
6056
  name: 'metadata.filterCriteria',
5330
6057
  type: 'Record<string, unknown> | null',
5331
6058
  description: 'Bridge declarativa de filtros encaminhada para a tabela interna. Para novo authoring, prefira metadata.queryContext.',
5332
6059
  },
6060
+ {
6061
+ name: 'metadata.form',
6062
+ type: 'FormConfig',
6063
+ description: 'Configuração do formulário encaminhada para o PraxisDynamicForm aberto por ações modal/drawer; layout, fieldMetadata, actions e helpPresentation permanecem propriedade do Dynamic Form.',
6064
+ },
5333
6065
  {
5334
6066
  name: 'metadata.actions[].form',
5335
6067
  type: 'CrudActionFormContract',
@@ -5341,6 +6073,12 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
5341
6073
  default: false,
5342
6074
  description: 'Habilita modo de customização do layout.',
5343
6075
  },
6076
+ {
6077
+ name: 'authoringCapability',
6078
+ type: 'string | null',
6079
+ default: null,
6080
+ description: 'Capability publica do contexto corporativo exigida para disponibilizar authoring governado no CRUD e na tabela interna.',
6081
+ },
5344
6082
  ],
5345
6083
  outputs: [
5346
6084
  {
@@ -5699,6 +6437,7 @@ const CRUD_AI_CAPABILITIES = {
5699
6437
  { path: 'actions[].params[].to', category: 'actions', valueKind: 'enum', allowedValues: ENUMS.paramTarget, description: 'Destino do parametro.' },
5700
6438
  { path: 'actions[].params[].name', category: 'actions', valueKind: 'string', description: 'Nome do parametro no destino.' },
5701
6439
  { path: 'actions[].form.initialValue', category: 'actions', valueKind: 'object', description: 'Seed fixo do formulario injetado em inputs.initialValue antes da abertura.' },
6440
+ { path: 'actions[].form.layoutPolicy', category: 'actions', valueKind: 'object', description: 'Politica schema-driven opt-in encaminhada ao Dynamic Form para materializar detalhe ou command form sem FormConfig.sections local; deve acompanhar schemaUrl compativel, especialmente schemaType=request para comandos.' },
5702
6441
  { path: 'actions[].back', category: 'navigation', valueKind: 'object', description: 'BackConfig por acao.' },
5703
6442
  { path: 'actions[].back.strategy', category: 'navigation', valueKind: 'enum', allowedValues: ENUMS.backStrategy, description: 'Estrategia de retorno da acao (auto, close, navigate).' },
5704
6443
  { path: 'actions[].back.returnTo', category: 'navigation', valueKind: 'string', description: 'Rota ou destino usado quando a estrategia de retorno for navigate.' },
@@ -5736,6 +6475,7 @@ const surfaceConfigureSchema = {
5736
6475
  submitMethod: { enum: ['post', 'put', 'patch', 'delete'] },
5737
6476
  apiEndpointKey: { type: 'string' },
5738
6477
  initialValue: { type: 'object' },
6478
+ layoutPolicy: { type: 'object' },
5739
6479
  },
5740
6480
  },
5741
6481
  params: {
@@ -5849,19 +6589,21 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
5849
6589
  { name: 'crudId', type: 'string', description: 'Stable CRUD instance id used for table/form identity and persistence.' },
5850
6590
  { name: 'componentInstanceId', type: 'string', description: 'Optional stable host instance id for multiple CRUD widgets on the same route.' },
5851
6591
  { name: 'context', type: 'Record<string, unknown>', description: 'Opaque host context used for authoring seeds and launcher inputs.' },
6592
+ { name: 'enableCustomization', type: 'boolean', description: 'Explicit host opt-in for CRUD authoring surfaces.' },
6593
+ { name: 'authoringCapability', type: 'string | null', description: 'Public runtime capability required before CRUD and delegated table authoring can open.' },
5852
6594
  { name: 'afterOpen', type: '{ mode: FormOpenMode; action: string }', description: 'Emitted after a CRUD action opens.' },
5853
6595
  { name: 'afterSave', type: '{ id: string | number; data: unknown }', description: 'Emitted after save; CRUD refetches the list.' },
5854
6596
  { name: 'afterDelete', type: '{ id: string | number }', description: 'Emitted after delete; CRUD refetches the list.' },
5855
6597
  ],
5856
6598
  editableTargets: [
5857
- { kind: 'resourceBinding', resolver: 'crud-resource-by-path-or-key', description: 'Resource path/key, id field, endpoint key and query context owned by CRUD orchestration.' },
6599
+ { kind: 'resourceBinding', resolver: 'crud-resource-by-path-or-key', description: 'Resource path/key, optional schema path template, id field, endpoint key and query context owned by CRUD orchestration.' },
5858
6600
  { kind: 'listSurface', resolver: 'crud-list-surface', description: 'CRUD-hosted list surface and table delegation boundary.' },
5859
6601
  { kind: 'createSurface', resolver: 'crud-action-by-id:create', description: 'Create action open mode, route/form binding and launcher inputs.' },
5860
6602
  { kind: 'editSurface', resolver: 'crud-action-by-id:edit', description: 'Edit action open mode, route/form binding and launcher inputs.' },
5861
6603
  { kind: 'viewSurface', resolver: 'crud-action-by-id:view', description: 'View action open mode, route/form binding and launcher inputs.' },
5862
6604
  { kind: 'deleteBehavior', resolver: 'crud-action-by-id:delete', description: 'Delete action enablement, confirmation, endpoint and capability policy.' },
5863
- { kind: 'dialogHost', resolver: 'crud-dialog-host-defaults', description: 'Route/modal/drawer defaults consumed by CrudLauncherService and DynamicFormDialogHostComponent.' },
5864
- { kind: 'formBinding', resolver: 'crud-action-form-contract-by-action-id', description: 'CRUD-owned form binding fields: formId, schemaUrl, submitUrl, submitMethod, params and initialValue.' },
6605
+ { kind: 'dialogHost', resolver: 'crud-dialog-host-defaults', description: 'Route/modal/drawer defaults consumed by CrudLauncherService and DynamicFormDialogHostComponent, including contextual shell titles derived from action label, form title or resource metadata.' },
6606
+ { kind: 'formBinding', resolver: 'crud-action-form-contract-by-action-id', description: 'CRUD-owned form binding fields: formId, schemaUrl, submitUrl, submitMethod, params and initialValue. Shared FormConfig remains owned by Dynamic Form and is forwarded through CrudMetadata.form when present.' },
5865
6607
  { kind: 'permissions', resolver: 'crud-resource-capabilities', description: 'CRUD action availability derived from resource capabilities and action permissions.' },
5866
6608
  { kind: 'domainGovernanceContext', resolver: 'domain-catalog-context-by-resource-key', description: 'Read-only semantic/governance context resolved from Domain Catalog and referenced from CRUD queryContext.meta.' },
5867
6609
  { kind: 'childOperation', resolver: 'child-authoring-manifest-operation', description: 'Delegated form/table/dialog/settings-panel operation owned by the child component manifest.' },
@@ -5877,13 +6619,13 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
5877
6619
  effects: [{ kind: 'compile-domain-patch', handler: 'crud-resource-bind', handlerContract: {
5878
6620
  reads: ['CrudMetadata.resource', 'api_metadata', 'ResourceDiscoveryService', 'GET /{resource}/capabilities'],
5879
6621
  writes: ['CrudMetadata.resource', 'CrudMetadata.queryContext', 'PraxisCrudComponent.tableCrudContext'],
5880
- identityKeys: ['resourcePath', 'resourceKey'],
6622
+ identityKeys: ['resourcePath', 'resourceKey', 'schemaPath'],
5881
6623
  inputSchema: resourceBindSchema,
5882
6624
  failureModes: ['resource-not-found', 'schema-url-not-canonical', 'capabilities-unavailable', 'id-field-missing'],
5883
- description: 'Binds CRUD to a canonical resource and validates it against api_metadata/resource capabilities before runtime use.',
6625
+ description: 'Binds CRUD to a canonical resource and validates it against api_metadata/resource capabilities before runtime use. Use CrudMetadata.resource.schemaPath when schema discovery must use a templated path while resource.path targets a concrete nested collection.',
5884
6626
  } }],
5885
6627
  validators: ['resource-exists-in-api-metadata', 'resource-path-canonical', 'resource-key-stable', 'id-field-known', 'resource-capabilities-resolvable'],
5886
- affectedPaths: ['resource.path', 'resource.idField', 'resource.endpointKey', 'queryContext', 'filterCriteria'],
6628
+ affectedPaths: ['resource.path', 'resource.schemaPath', 'resource.idField', 'resource.endpointKey', 'resource.apiUrlEntry', 'queryContext', 'filterCriteria'],
5887
6629
  submissionImpact: 'affects-remote-binding',
5888
6630
  preconditions: ['crud-metadata-loaded', 'api-metadata-available'],
5889
6631
  },
@@ -6017,14 +6759,14 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
6017
6759
  target: { kind: 'dialogHost', resolver: 'crud-dialog-host-defaults', ambiguityPolicy: 'fail', required: true },
6018
6760
  inputSchema: dialogHostSchema,
6019
6761
  effects: [{ kind: 'compile-domain-patch', handler: 'crud-dialog-host-set', handlerContract: {
6020
- reads: ['CrudMetadata.defaults', 'CrudLauncherService.resolveOpenMode', 'DialogService', 'CRUD_DRAWER_ADAPTER'],
6762
+ reads: ['CrudMetadata.defaults', 'CrudLauncherService.resolveOpenMode', 'DialogService', 'DynamicFormDialogHostComponent', 'CRUD_DRAWER_ADAPTER'],
6021
6763
  writes: ['CrudMetadata.defaults.openMode', 'CrudMetadata.defaults.modal', 'CrudMetadata.defaults.back'],
6022
6764
  identityKeys: ['crudId'],
6023
6765
  inputSchema: dialogHostSchema,
6024
- failureModes: ['open-mode-unsupported', 'drawer-adapter-missing', 'modal-size-invalid', 'back-policy-invalid'],
6025
- description: 'Configures CRUD-owned route/modal/drawer defaults consumed by the launcher and dialog host.',
6766
+ failureModes: ['open-mode-unsupported', 'drawer-runtime-unavailable', 'modal-size-invalid', 'back-policy-invalid'],
6767
+ description: 'Configures CRUD-owned route/modal/drawer defaults consumed by the launcher, default dialog-backed drawer runtime and optional host drawer adapter.',
6026
6768
  } }],
6027
- validators: ['open-mode-supported', 'modal-size-valid', 'drawer-adapter-available-when-needed', 'back-policy-valid', 'settings-panel-shell-compatible'],
6769
+ validators: ['open-mode-supported', 'modal-size-valid', 'drawer-runtime-available', 'back-policy-valid', 'settings-panel-shell-compatible'],
6028
6770
  affectedPaths: ['defaults.openMode', 'defaults.modal', 'defaults.back'],
6029
6771
  submissionImpact: 'config-only',
6030
6772
  preconditions: ['crud-metadata-loaded'],
@@ -6100,7 +6842,7 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
6100
6842
  { validatorId: 'permissions-delete-valid', level: 'error', code: 'CRUD_DELETE_PERMISSION_VALID', description: 'Delete permission cannot bypass resource capabilities or confirmation policy.' },
6101
6843
  { validatorId: 'open-mode-supported', level: 'error', code: 'CRUD_OPEN_MODE_SUPPORTED', description: 'Open mode must be route, modal or drawer.' },
6102
6844
  { validatorId: 'modal-size-valid', level: 'error', code: 'CRUD_MODAL_SIZE_VALID', description: 'Modal sizing defaults must be valid DialogConfig values.' },
6103
- { validatorId: 'drawer-adapter-available-when-needed', level: 'error', code: 'CRUD_DRAWER_ADAPTER_AVAILABLE', description: 'Drawer open mode requires a host-provided drawer adapter.' },
6845
+ { validatorId: 'drawer-runtime-available', level: 'error', code: 'CRUD_DRAWER_RUNTIME_AVAILABLE', description: 'Drawer open mode uses the default dialog-backed drawer runtime with CRUD-owned chrome, responsive right-panel sizing, Escape/backdrop handling and semantic close/save results; a host drawer adapter is optional only for shell-specific presentation.' },
6104
6846
  { validatorId: 'back-policy-valid', level: 'error', code: 'CRUD_BACK_POLICY_VALID', description: 'Back policy must be valid for route/modal/drawer behavior.' },
6105
6847
  { validatorId: 'settings-panel-shell-compatible', level: 'warning', code: 'CRUD_SETTINGS_PANEL_COMPATIBLE', description: 'Authoring shell must preserve apply/save/reset semantics.' },
6106
6848
  { validatorId: 'action-permission-supported', level: 'error', code: 'CRUD_ACTION_PERMISSION_SUPPORTED', description: 'Action permissions must map to supported resource capabilities.' },