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

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,41 @@ 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
+ }
3768
+ async onToolbarAction(event) {
3769
+ const action = String(event?.action || '').trim();
3770
+ if (!action)
3771
+ return;
3772
+ const row = this.extractSelectedRowFromToolbarAction(event);
3773
+ if (row)
3774
+ this.selectedRow = row;
3775
+ await this.onAction(action, row ?? undefined, event);
3776
+ }
3561
3777
  ngOnChanges(changes) {
3562
3778
  if (!changes['metadata'] && !changes['context']) {
3563
3779
  return;
3564
3780
  }
3781
+ this.materializedRefreshSequence += 1;
3565
3782
  try {
3566
3783
  const parsed = typeof this.metadata === 'string'
3567
3784
  ? JSON.parse(this.metadata)
@@ -3595,33 +3812,46 @@ class PraxisCrudComponent {
3595
3812
  async onAction(action, row, runtimeEvent) {
3596
3813
  try {
3597
3814
  document.activeElement?.blur();
3598
- let actionMeta = this.resolvedMetadata.actions?.find((candidate) => candidate.action === action);
3815
+ const normalizedAction = this.normalizeCrudActionName(action);
3816
+ const contextualRow = row ?? this.resolveSelectedRowForAction(normalizedAction);
3817
+ let actionMeta = this.resolvedMetadata.actions?.find((candidate) => this.normalizeCrudActionName(candidate.action) === normalizedAction);
3818
+ if (!actionMeta && normalizedAction !== action) {
3819
+ actionMeta = this.resolvedMetadata.actions?.find((candidate) => candidate.action === action);
3820
+ }
3599
3821
  if (!actionMeta) {
3600
- const ctxAction = this.tableCrudContext?.actions?.find((candidate) => candidate.action === action);
3822
+ const ctxAction = this.tableCrudContext?.actions?.find((candidate) => this.normalizeCrudActionName(candidate.action) === normalizedAction);
3601
3823
  if (ctxAction) {
3602
3824
  actionMeta = {
3603
- action: ctxAction.action,
3825
+ action: this.normalizeCrudActionName(ctxAction.action),
3604
3826
  openMode: ctxAction.openMode,
3605
3827
  formId: ctxAction.formId,
3606
3828
  route: ctxAction.route,
3607
3829
  };
3608
3830
  }
3609
3831
  else {
3610
- actionMeta = { action };
3832
+ actionMeta = { action: normalizedAction };
3611
3833
  }
3612
3834
  }
3613
- const effectiveAction = (actionMeta || { action });
3835
+ const effectiveAction = {
3836
+ ...(actionMeta || { action: normalizedAction }),
3837
+ action: this.normalizeCrudActionName(actionMeta?.action ?? normalizedAction),
3838
+ };
3839
+ const resourceIdentity = runtimeEvent?.resourceIdentity ?? null;
3840
+ const handledByDuplicateDraft = await this.tryHandleCanonicalDuplicateDraftAction(effectiveAction, contextualRow);
3841
+ if (handledByDuplicateDraft) {
3842
+ return;
3843
+ }
3614
3844
  if (!this.hasExplicitOpenBinding(effectiveAction)) {
3615
- const openedByDiscovery = await this.tryOpenDiscoveredCrudSurface(action, row, runtimeEvent);
3845
+ const openedByDiscovery = await this.tryOpenDiscoveredCrudSurface(effectiveAction.action, contextualRow, runtimeEvent);
3616
3846
  if (openedByDiscovery) {
3617
3847
  return;
3618
3848
  }
3619
- const handledByWorkflowAction = await this.tryOpenDiscoveredWorkflowAction(action, row, runtimeEvent);
3849
+ const handledByWorkflowAction = await this.tryOpenDiscoveredWorkflowAction(effectiveAction.action, contextualRow, runtimeEvent);
3620
3850
  if (handledByWorkflowAction) {
3621
3851
  return;
3622
3852
  }
3623
3853
  }
3624
- const handledByDelete = await this.tryHandleCanonicalDeleteAction(effectiveAction, row);
3854
+ const handledByDelete = await this.tryHandleCanonicalDeleteAction(effectiveAction, contextualRow);
3625
3855
  if (handledByDelete) {
3626
3856
  return;
3627
3857
  }
@@ -3632,7 +3862,7 @@ class PraxisCrudComponent {
3632
3862
  drawerCloseEmitted = true;
3633
3863
  this.afterClose.emit();
3634
3864
  };
3635
- const { mode, ref } = await this.launcher.launch(effectiveAction, row, this.resolvedMetadata, this.componentKeyId(), {
3865
+ const { mode, ref } = await this.launcher.launch(effectiveAction, contextualRow, this.resolvedMetadata, this.componentKeyId(), {
3636
3866
  onClose: () => emitDrawerClose(),
3637
3867
  onResult: (result) => {
3638
3868
  emitDrawerClose();
@@ -3653,9 +3883,13 @@ class PraxisCrudComponent {
3653
3883
  capabilities: effectiveAction.action === 'create'
3654
3884
  ? this.collectionCapabilities
3655
3885
  : this.collectionCapabilities,
3656
- links: this.resolveCrudRuntimeLinks(effectiveAction.action, row),
3886
+ links: this.resolveCrudRuntimeLinks(effectiveAction.action, contextualRow),
3887
+ resourceIdentity,
3657
3888
  });
3658
3889
  this.afterOpen.emit({ mode, action: effectiveAction.action });
3890
+ if (mode === 'drawer') {
3891
+ return;
3892
+ }
3659
3893
  if (!ref) {
3660
3894
  return;
3661
3895
  }
@@ -3791,6 +4025,9 @@ class PraxisCrudComponent {
3791
4025
  return this.tx('crud.emptyState.primaryAction', 'Configure metadata');
3792
4026
  }
3793
4027
  onConfigureRequested() {
4028
+ if (!this.isCustomizationAvailable()) {
4029
+ return;
4030
+ }
3794
4031
  this.configureRequested.emit();
3795
4032
  this.openCrudAuthoringFromTable();
3796
4033
  }
@@ -3841,9 +4078,140 @@ class PraxisCrudComponent {
3841
4078
  this.refreshTable();
3842
4079
  return true;
3843
4080
  }
4081
+ async tryHandleCanonicalDuplicateDraftAction(action, row) {
4082
+ const normalizedAction = this.normalizeCrudActionName(action.action);
4083
+ if (normalizedAction !== 'duplicate-draft' || !row) {
4084
+ return false;
4085
+ }
4086
+ const duplicateDraftUrl = this.getResourceDiscovery().resolveLinkHref(row['_links'], 'duplicate-draft', this.buildDiscoveryOptions());
4087
+ if (!duplicateDraftUrl) {
4088
+ return false;
4089
+ }
4090
+ const response = await firstValueFrom(this.http.post(duplicateDraftUrl, {}));
4091
+ const draft = this.unwrapRestData(response);
4092
+ if (!this.isRecord(draft)) {
4093
+ throw new Error('Duplicate draft action returned an invalid create draft.');
4094
+ }
4095
+ const configuredCreateAction = this.resolvedMetadata.actions?.find((candidate) => this.normalizeCrudActionName(candidate.action) === 'create') ?? { action: 'create' };
4096
+ const createAction = {
4097
+ ...configuredCreateAction,
4098
+ action: 'create',
4099
+ openMode: configuredCreateAction.openMode ??
4100
+ action.openMode ??
4101
+ this.resolvedMetadata.defaults?.openMode ??
4102
+ 'drawer',
4103
+ form: {
4104
+ ...(configuredCreateAction.form ?? {}),
4105
+ initialValue: { ...draft },
4106
+ },
4107
+ };
4108
+ let drawerCloseEmitted = false;
4109
+ const emitDrawerClose = () => {
4110
+ if (drawerCloseEmitted)
4111
+ return;
4112
+ drawerCloseEmitted = true;
4113
+ this.afterClose.emit();
4114
+ };
4115
+ const handleResult = (result) => {
4116
+ if (result?.type !== 'save') {
4117
+ return;
4118
+ }
4119
+ const data = this.isRecord(result.data) ? result.data : {};
4120
+ const id = data[this.getIdField()];
4121
+ this.afterSave.emit({ id, data });
4122
+ this.refreshTable();
4123
+ };
4124
+ const { mode, ref } = await this.launcher.launch(createAction, undefined, this.resolvedMetadata, this.componentKeyId(), {
4125
+ onClose: () => emitDrawerClose(),
4126
+ onResult: (result) => handleResult(result),
4127
+ }, {
4128
+ capabilities: this.collectionCapabilities,
4129
+ links: this.tableCollectionLinks,
4130
+ });
4131
+ this.afterOpen.emit({ mode, action: normalizedAction });
4132
+ if (mode !== 'drawer' && ref) {
4133
+ ref
4134
+ .afterClosed()
4135
+ .pipe(takeUntilDestroyed(this.destroyRef))
4136
+ .subscribe((result) => {
4137
+ this.afterClose.emit();
4138
+ handleResult(result);
4139
+ });
4140
+ }
4141
+ return true;
4142
+ }
3844
4143
  refreshTable() {
4144
+ if (this.tryRefreshMaterializedLocalData()) {
4145
+ return;
4146
+ }
3845
4147
  this.table.refetch();
3846
4148
  }
4149
+ tryRefreshMaterializedLocalData() {
4150
+ const readUrl = this.resolveMaterializedReadUrl();
4151
+ if (!readUrl || this.resolveResourcePath(this.resolvedMetadata)) {
4152
+ return false;
4153
+ }
4154
+ const refreshSequence = ++this.materializedRefreshSequence;
4155
+ void firstValueFrom(this.getResourceDiscovery().fetchJson(readUrl, {
4156
+ endpointKey: this.resolvedMetadata.resource?.endpointKey,
4157
+ apiUrlEntry: this.resolvedMetadata.resource?.apiUrlEntry,
4158
+ }))
4159
+ .then((response) => {
4160
+ if (!this.isCurrentMaterializedRefresh(refreshSequence, readUrl)) {
4161
+ return;
4162
+ }
4163
+ const data = this.extractCollectionData(this.unwrapRestData(response));
4164
+ if (!data) {
4165
+ this.error.emit({
4166
+ code: 'CRUD_MATERIALIZED_REFRESH_INVALID_COLLECTION',
4167
+ readUrl,
4168
+ });
4169
+ return;
4170
+ }
4171
+ this.resolvedMetadata = {
4172
+ ...this.resolvedMetadata,
4173
+ data,
4174
+ };
4175
+ this.applyResolvedCrudState(this.resolvedMetadata);
4176
+ this.cdr.markForCheck();
4177
+ })
4178
+ .catch((error) => this.error.emit(error));
4179
+ return true;
4180
+ }
4181
+ isCurrentMaterializedRefresh(refreshSequence, readUrl) {
4182
+ return (refreshSequence === this.materializedRefreshSequence &&
4183
+ this.resolveMaterializedReadUrl() === readUrl &&
4184
+ this.resolveResourcePath(this.resolvedMetadata).length === 0);
4185
+ }
4186
+ resolveMaterializedReadUrl() {
4187
+ const materialization = this.context?.['materialization'];
4188
+ return String(materialization?.['readUrl'] || '').trim();
4189
+ }
4190
+ unwrapRestData(value) {
4191
+ if (value && typeof value === 'object' && !Array.isArray(value) && 'data' in value) {
4192
+ return value.data;
4193
+ }
4194
+ return value;
4195
+ }
4196
+ extractCollectionData(data) {
4197
+ if (Array.isArray(data)) {
4198
+ return data;
4199
+ }
4200
+ if (!data || typeof data !== 'object') {
4201
+ return null;
4202
+ }
4203
+ const record = data;
4204
+ if (Array.isArray(record['content'])) {
4205
+ return record['content'];
4206
+ }
4207
+ if (Array.isArray(record['items'])) {
4208
+ return record['items'];
4209
+ }
4210
+ if (Array.isArray(record['data'])) {
4211
+ return record['data'];
4212
+ }
4213
+ return null;
4214
+ }
3847
4215
  emitTableRuntimeConfigSnapshot() {
3848
4216
  const snapshot = this.getCurrentTableConfigSnapshot();
3849
4217
  if (!snapshot)
@@ -3901,17 +4269,67 @@ class PraxisCrudComponent {
3901
4269
  }
3902
4270
  }
3903
4271
  resolveQueryContext(meta) {
4272
+ if (Array.isArray(meta?.data) && this.resolveResourcePath(meta).length === 0) {
4273
+ return null;
4274
+ }
3904
4275
  return this.isRecord(meta?.queryContext) ? meta.queryContext : null;
3905
4276
  }
3906
4277
  resolveFilterCriteria(meta) {
3907
4278
  return this.isRecord(meta?.filterCriteria) ? { ...meta.filterCriteria } : {};
3908
4279
  }
4280
+ extractRowFromEvent(event) {
4281
+ if (!this.isRecord(event)) {
4282
+ return null;
4283
+ }
4284
+ const row = event['row'];
4285
+ return this.isRecord(row) ? row : null;
4286
+ }
4287
+ extractSelectedRowFromEvent(event) {
4288
+ if (!this.isRecord(event)) {
4289
+ return null;
4290
+ }
4291
+ const selection = event['selection'];
4292
+ if (Array.isArray(selection)) {
4293
+ const first = selection[0];
4294
+ return this.isRecord(first) ? first : null;
4295
+ }
4296
+ const row = event['row'];
4297
+ if (this.isRecord(row)) {
4298
+ return row;
4299
+ }
4300
+ return null;
4301
+ }
4302
+ extractSelectedRowFromBulkAction(event) {
4303
+ const first = Array.isArray(event?.rows) ? event.rows[0] : null;
4304
+ return this.isRecord(first) ? first : null;
4305
+ }
4306
+ extractSelectedRowFromToolbarAction(event) {
4307
+ if (this.isRecord(event?.selectedRow))
4308
+ return event.selectedRow;
4309
+ if (this.isRecord(event?.row))
4310
+ return event.row;
4311
+ const first = Array.isArray(event?.selectedRows) ? event.selectedRows[0] : null;
4312
+ return this.isRecord(first) ? first : null;
4313
+ }
4314
+ extractDiscoveryActionConfig(event) {
4315
+ const config = event?.actionConfig;
4316
+ return this.isRecord(config) && typeof config['resourceKey'] === 'string'
4317
+ ? config
4318
+ : null;
4319
+ }
4320
+ resolveSelectedRowForAction(action) {
4321
+ const normalized = String(action || '').trim().toLowerCase();
4322
+ if (!normalized || normalized === 'create' || normalized === 'add' || normalized === 'new') {
4323
+ return undefined;
4324
+ }
4325
+ return this.selectedRow ?? undefined;
4326
+ }
3909
4327
  async tryOpenDiscoveredCrudSurface(action, row, runtimeEvent) {
3910
4328
  const normalizedAction = String(action || '').trim().toLowerCase();
3911
4329
  if (!this.surfaceService) {
3912
4330
  return false;
3913
4331
  }
3914
- const providedSurface = this.resolveProvidedSurface(normalizedAction, runtimeEvent?.actionConfig);
4332
+ const providedSurface = this.resolveProvidedSurface(normalizedAction, this.extractDiscoveryActionConfig(runtimeEvent));
3915
4333
  if (!providedSurface && !this.isDiscoveryManagedCrudAction(normalizedAction)) {
3916
4334
  return false;
3917
4335
  }
@@ -3942,7 +4360,7 @@ class PraxisCrudComponent {
3942
4360
  }
3943
4361
  let openPromise;
3944
4362
  try {
3945
- openPromise = Promise.resolve(this.surfaceService.open(payload, {
4363
+ const surfaceContext = {
3946
4364
  sourceId: this.componentKeyId() || undefined,
3947
4365
  payload: { action: normalizedAction, row: row ?? null },
3948
4366
  meta: {
@@ -3959,7 +4377,9 @@ class PraxisCrudComponent {
3959
4377
  resourcePath,
3960
4378
  },
3961
4379
  },
3962
- }));
4380
+ };
4381
+ const handledInline = await this.surfaceOutlets.tryActivate(payload, surfaceContext);
4382
+ openPromise = handledInline ? Promise.resolve(undefined) : Promise.resolve(this.surfaceService.open(payload, surfaceContext));
3963
4383
  }
3964
4384
  catch {
3965
4385
  return false;
@@ -3996,10 +4416,10 @@ class PraxisCrudComponent {
3996
4416
  if (!normalizedAction || this.isDiscoveryManagedCrudAction(normalizedAction) || !this.surfaceService) {
3997
4417
  return false;
3998
4418
  }
3999
- const providedAction = this.resolveProvidedWorkflowAction(normalizedAction, runtimeEvent?.actionConfig);
4419
+ const providedAction = this.resolveProvidedWorkflowAction(normalizedAction, this.extractDiscoveryActionConfig(runtimeEvent));
4000
4420
  const catalog = providedAction ? null : await this.resolveDiscoveredActionCatalog(row);
4001
4421
  const discoveredAction = providedAction || this.selectDiscoveredWorkflowAction(normalizedAction, catalog?.actions || []);
4002
- const resourcePath = String(this.resolveResourcePath(this.resolvedMetadata) || catalog?.resourcePath || '').trim();
4422
+ const resourcePath = String(catalog?.resourcePath || this.resolveResourcePath(this.resolvedMetadata) || '').trim();
4003
4423
  if (!discoveredAction || !resourcePath) {
4004
4424
  return false;
4005
4425
  }
@@ -4222,12 +4642,19 @@ class PraxisCrudComponent {
4222
4642
  }
4223
4643
  return cfg.toolbar;
4224
4644
  };
4225
- const hasToolbarAdd = (cfg.toolbar?.actions || []).some((action) => this.isAddLike(action));
4645
+ const hasToolbarAdd = (cfg.toolbar?.actions || []).some((action) => this.shouldCanonicalizeToolbarCreateAction(action));
4226
4646
  const addAction = this.resolveCreateToolbarAction(meta, capabilities);
4227
4647
  if (addAction) {
4228
4648
  if (hasToolbarAdd) {
4229
4649
  const toolbar = ensureToolbar();
4230
4650
  toolbar.visible = true;
4651
+ toolbar.actions = (toolbar.actions || []).map((action) => this.shouldCanonicalizeToolbarCreateAction(action)
4652
+ ? {
4653
+ ...action,
4654
+ action: 'create',
4655
+ }
4656
+ : action);
4657
+ changed = true;
4231
4658
  }
4232
4659
  else {
4233
4660
  const toolbar = ensureToolbar();
@@ -4246,6 +4673,7 @@ class PraxisCrudComponent {
4246
4673
  });
4247
4674
  }
4248
4675
  changed = true;
4676
+ changed = this.ensureCanonicalCollectionEmptyState(cfg, meta, addAction) || changed;
4249
4677
  }
4250
4678
  const discoveredToolbarActions = this.resolveCollectionWorkflowToolbarActions(capabilities);
4251
4679
  if (discoveredToolbarActions.length) {
@@ -4362,6 +4790,28 @@ class PraxisCrudComponent {
4362
4790
  }
4363
4791
  return false;
4364
4792
  }
4793
+ normalizeCrudActionName(action) {
4794
+ const raw = String(action || '').trim();
4795
+ const normalized = raw.toLowerCase();
4796
+ return this.isCreateActionAliasName(normalized) ? 'create' : raw;
4797
+ }
4798
+ shouldCanonicalizeToolbarCreateAction(action) {
4799
+ if (!action)
4800
+ return false;
4801
+ const normalize = (value) => String(value || '').trim().toLowerCase();
4802
+ const explicitId = normalize(action.action || action.id || action.code || action.key || action.name || action.type);
4803
+ if (this.isCreateActionAliasName(explicitId))
4804
+ return true;
4805
+ if (explicitId)
4806
+ return false;
4807
+ const icon = normalize(action.icon);
4808
+ const label = normalize(action.label);
4809
+ return ((icon === 'add' || icon === 'add_circle' || icon === 'add_box') &&
4810
+ (label === 'adicionar' || label === 'novo' || label === 'criar' || label === 'incluir'));
4811
+ }
4812
+ isCreateActionAliasName(actionName) {
4813
+ return ['create', 'add', 'novo', 'new', 'incluir', 'inserir'].includes(actionName);
4814
+ }
4365
4815
  buildTableCrudContext(meta, capabilities) {
4366
4816
  if (!meta)
4367
4817
  return undefined;
@@ -4384,6 +4834,9 @@ class PraxisCrudComponent {
4384
4834
  };
4385
4835
  }
4386
4836
  openCrudAuthoringFromTable = () => {
4837
+ if (!this.isCustomizationAvailable()) {
4838
+ return;
4839
+ }
4387
4840
  const seed = this.currentAuthoringDocument
4388
4841
  || createCrudAuthoringDocument({ metadata: this.resolvedMetadata });
4389
4842
  const ref = openCrudMetadataEditor(this.settingsPanel, {
@@ -4392,13 +4845,33 @@ class PraxisCrudComponent {
4392
4845
  title: this.tx('crud.authoring.title', 'Configurações do CRUD'),
4393
4846
  titleIcon: 'table_chart',
4394
4847
  });
4848
+ this.activeAuthoringPanelRef = ref;
4395
4849
  ref.applied$
4396
4850
  .pipe(takeUntilDestroyed(this.destroyRef))
4397
4851
  .subscribe((payload) => this.applyCrudAuthoringPayload(payload, 'applied'));
4398
4852
  ref.saved$
4399
4853
  .pipe(takeUntilDestroyed(this.destroyRef))
4400
4854
  .subscribe((payload) => this.applyCrudAuthoringPayload(payload, 'saved'));
4855
+ ref.closed$
4856
+ .pipe(takeUntilDestroyed(this.destroyRef))
4857
+ .subscribe(() => {
4858
+ if (this.activeAuthoringPanelRef === ref) {
4859
+ this.activeAuthoringPanelRef = undefined;
4860
+ }
4861
+ });
4401
4862
  };
4863
+ isCustomizationAvailable() {
4864
+ if (!this.enableCustomization) {
4865
+ return false;
4866
+ }
4867
+ const requiredCapability = String(this.authoringCapability || '').trim();
4868
+ if (!requiredCapability) {
4869
+ return true;
4870
+ }
4871
+ const capabilities = this.enterpriseRuntimeContext?.snapshot?.capabilities;
4872
+ return Array.isArray(capabilities)
4873
+ && capabilities.some((capability) => capability === requiredCapability);
4874
+ }
4402
4875
  applyCrudAuthoringPayload(payload, eventName) {
4403
4876
  const next = parseLegacyOrCrudDocument(payload);
4404
4877
  this.currentAuthoringDocument = next;
@@ -4615,6 +5088,48 @@ class PraxisCrudComponent {
4615
5088
  return action;
4616
5089
  }
4617
5090
  }
5091
+ ensureCanonicalCollectionEmptyState(config, metadata, createAction) {
5092
+ const behavior = config.behavior || {};
5093
+ if (behavior.emptyState) {
5094
+ return false;
5095
+ }
5096
+ const resourceLabel = String(metadata.resource?.label
5097
+ || metadata.resource?.title
5098
+ || metadata.resource?.path
5099
+ || '').trim();
5100
+ const title = resourceLabel
5101
+ ? this.txWithParams('crud.table.emptyState.initial.title', 'Sem registros em {label}', { label: resourceLabel })
5102
+ : this.tx('crud.table.emptyState.initial.titleFallback', 'Nenhum registro disponível.');
5103
+ const actionId = String(createAction.action || createAction.id || 'create').trim() || 'create';
5104
+ const actionLabel = String(createAction.label || this.getCrudActionLabel('create')).trim();
5105
+ const actionIcon = String(createAction.icon || 'add').trim();
5106
+ config.behavior = {
5107
+ ...behavior,
5108
+ emptyState: {
5109
+ message: '',
5110
+ contexts: {
5111
+ initial: {
5112
+ title,
5113
+ description: this.tx('crud.table.emptyState.initial.descriptionWithAction', 'Use a ação principal para adicionar o primeiro registro quando houver informações para cadastrar.'),
5114
+ actions: [
5115
+ {
5116
+ label: actionLabel,
5117
+ action: actionId,
5118
+ icon: actionIcon,
5119
+ primary: true,
5120
+ },
5121
+ ],
5122
+ },
5123
+ filtered: {
5124
+ title: this.tx('crud.table.emptyState.filtered.title', 'Nenhum resultado encontrado.'),
5125
+ description: this.tx('crud.table.emptyState.filtered.description', 'Revise os filtros ou ajuste o termo de busca.'),
5126
+ actions: [],
5127
+ },
5128
+ },
5129
+ },
5130
+ };
5131
+ return true;
5132
+ }
4618
5133
  tx(key, fallback) {
4619
5134
  try {
4620
5135
  return translateCrudRuntimeText(this.i18n, key, fallback);
@@ -4623,8 +5138,19 @@ class PraxisCrudComponent {
4623
5138
  return fallback;
4624
5139
  }
4625
5140
  }
5141
+ txWithParams(key, fallback, params) {
5142
+ try {
5143
+ return this.interpolateTranslationParams(translateCrudRuntimeText(this.i18n, key, fallback, params), params);
5144
+ }
5145
+ catch {
5146
+ return this.interpolateTranslationParams(fallback, params);
5147
+ }
5148
+ }
5149
+ interpolateTranslationParams(text, params) {
5150
+ return Object.entries(params).reduce((current, [name, value]) => current.split(`{${name}}`).join(value), text);
5151
+ }
4626
5152
  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: [
5153
+ 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
5154
  providePraxisI18nConfig(RESOURCE_DISCOVERY_I18N_CONFIG),
4629
5155
  providePraxisI18nConfig(PRAXIS_CRUD_RUNTIME_I18N_CONFIG),
4630
5156
  ], viewQueries: [{ propertyName: "table", first: true, predicate: PraxisTable, descendants: true }], usesOnChanges: true, ngImport: i0, template: `
@@ -4638,17 +5164,19 @@ class PraxisCrudComponent {
4638
5164
  [tableId]="crudId || 'default'"
4639
5165
  [crudContext]="tableCrudContext"
4640
5166
  [enableCustomization]="enableCustomization"
5167
+ [authoringCapability]="authoringCapability"
4641
5168
  (rowClick)="onTableRowClick($event)"
4642
5169
  (selectionChange)="onTableSelectionChange($event)"
4643
5170
  (rowAction)="onAction($event.action, $event.row, $event)"
4644
- (toolbarAction)="onAction($event.action)"
5171
+ (toolbarAction)="onToolbarAction($event)"
5172
+ (bulkAction)="onBulkAction($event)"
4645
5173
  (collectionLinksChange)="onCollectionLinksChange($event)"
4646
5174
  (reset)="onResetPreferences()"
4647
5175
  (metadataChange)="onTableMetadataChange()"
4648
5176
  (loadingStateChange)="onTableLoadingStateChange($event)"
4649
5177
  ></praxis-table>
4650
5178
  } @else {
4651
- @if (enableCustomization) {
5179
+ @if (isCustomizationAvailable()) {
4652
5180
  <praxis-empty-state-card
4653
5181
  icon="table_rows"
4654
5182
  [title]="getEmptyStateTitle()"
@@ -4657,7 +5185,7 @@ class PraxisCrudComponent {
4657
5185
  />
4658
5186
  }
4659
5187
  }
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"] }] });
5188
+ `, 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
5189
  }
4662
5190
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisCrudComponent, decorators: [{
4663
5191
  type: Component,
@@ -4675,17 +5203,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
4675
5203
  [tableId]="crudId || 'default'"
4676
5204
  [crudContext]="tableCrudContext"
4677
5205
  [enableCustomization]="enableCustomization"
5206
+ [authoringCapability]="authoringCapability"
4678
5207
  (rowClick)="onTableRowClick($event)"
4679
5208
  (selectionChange)="onTableSelectionChange($event)"
4680
5209
  (rowAction)="onAction($event.action, $event.row, $event)"
4681
- (toolbarAction)="onAction($event.action)"
5210
+ (toolbarAction)="onToolbarAction($event)"
5211
+ (bulkAction)="onBulkAction($event)"
4682
5212
  (collectionLinksChange)="onCollectionLinksChange($event)"
4683
5213
  (reset)="onResetPreferences()"
4684
5214
  (metadataChange)="onTableMetadataChange()"
4685
5215
  (loadingStateChange)="onTableLoadingStateChange($event)"
4686
5216
  ></praxis-table>
4687
5217
  } @else {
4688
- @if (enableCustomization) {
5218
+ @if (isCustomizationAvailable()) {
4689
5219
  <praxis-empty-state-card
4690
5220
  icon="table_rows"
4691
5221
  [title]="getEmptyStateTitle()"
@@ -4695,7 +5225,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
4695
5225
  }
4696
5226
  }
4697
5227
  `, styles: [":host{display:block;width:100%;min-width:0;max-width:100%}\n"] }]
4698
- }], propDecorators: { metadata: [{
5228
+ }], ctorParameters: () => [], propDecorators: { metadata: [{
4699
5229
  type: Input,
4700
5230
  args: [{ required: true }]
4701
5231
  }], crudId: [{
@@ -4707,6 +5237,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
4707
5237
  type: Input
4708
5238
  }], enableCustomization: [{
4709
5239
  type: Input
5240
+ }], authoringCapability: [{
5241
+ type: Input
4710
5242
  }], configureRequested: [{
4711
5243
  type: Output
4712
5244
  }], afterOpen: [{
@@ -4734,6 +5266,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
4734
5266
  args: [PraxisTable]
4735
5267
  }] } });
4736
5268
 
5269
+ function isCrudDebugEnabled() {
5270
+ try {
5271
+ if (globalThis.__PRAXIS_DEBUG_CRUD__) {
5272
+ return true;
5273
+ }
5274
+ if (typeof localStorage !== 'undefined') {
5275
+ return localStorage.getItem('praxis.debug.crud') === 'true';
5276
+ }
5277
+ }
5278
+ catch { }
5279
+ return false;
5280
+ }
5281
+ function debugCrudHost(message, data) {
5282
+ if (!isCrudDebugEnabled()) {
5283
+ return;
5284
+ }
5285
+ try {
5286
+ console.debug(message, data);
5287
+ }
5288
+ catch { }
5289
+ }
4737
5290
  class DynamicFormDialogHostComponent {
4738
5291
  dialogRef;
4739
5292
  data;
@@ -4742,8 +5295,10 @@ class DynamicFormDialogHostComponent {
4742
5295
  configStorage;
4743
5296
  formComp;
4744
5297
  modal = {};
5298
+ presentation = 'modal';
4745
5299
  maximized = false;
4746
5300
  initialSize = {};
5301
+ initialPosition;
4747
5302
  rememberState = false;
4748
5303
  stateKey;
4749
5304
  backDefaults = {};
@@ -4751,12 +5306,16 @@ class DynamicFormDialogHostComponent {
4751
5306
  resourcePath;
4752
5307
  resourceId;
4753
5308
  initialValue;
5309
+ resourceIdentity = null;
4754
5310
  schemaUrl;
4755
5311
  submitUrl;
4756
5312
  submitMethod;
4757
5313
  apiEndpointKey;
4758
5314
  apiUrlEntry;
5315
+ layoutPolicy;
5316
+ formConfig = {};
4759
5317
  formActions;
5318
+ formConfigPersistenceStrategy = 'input-first';
4760
5319
  mode = 'create';
4761
5320
  backConfig;
4762
5321
  idField = 'id';
@@ -4778,6 +5337,7 @@ class DynamicFormDialogHostComponent {
4778
5337
  this.crud = crud;
4779
5338
  this.configStorage = configStorage;
4780
5339
  this.dialogRef.disableClose = true;
5340
+ this.presentation = this.data.presentation === 'drawer' ? 'drawer' : 'modal';
4781
5341
  // i18n
4782
5342
  this.texts = {
4783
5343
  ...this.texts,
@@ -4788,6 +5348,9 @@ class DynamicFormDialogHostComponent {
4788
5348
  canMaximize: true,
4789
5349
  ...(this.data.metadata?.defaults?.modal || {}),
4790
5350
  };
5351
+ this.initialPosition = this.presentation === 'drawer'
5352
+ ? (this.data.dialogPosition ?? this.modal.position ?? { right: '0', top: '0' })
5353
+ : undefined;
4791
5354
  this.rememberState = !!this.modal.rememberLastState;
4792
5355
  this.stateKey = this.data.action?.formId
4793
5356
  ? `crud-dialog-state:${this.data.action.formId}`
@@ -4812,12 +5375,17 @@ class DynamicFormDialogHostComponent {
4812
5375
  this.apiUrlEntry = this.data.inputs?.['apiUrlEntry'] ?? null;
4813
5376
  const act = this.data.action?.action;
4814
5377
  this.mode = act === 'edit' ? 'edit' : act === 'view' ? 'view' : 'create';
5378
+ this.resourceIdentity = this.presentation === 'drawer' && this.mode === 'edit' && this.data.resourceIdentity
5379
+ ? this.data.resourceIdentity
5380
+ : null;
5381
+ this.formConfig = this.resolveFormConfig();
5382
+ this.layoutPolicy = this.resolveLayoutPolicy();
4815
5383
  this.formActions = this.resolveFormActions();
4816
5384
  // Back config: defaults from metadata/action, overridden by saved per-form config
4817
5385
  const defaults = (this.data.action?.back || this.data.metadata?.defaults?.back) || {};
4818
5386
  this.backDefaults = defaults;
4819
5387
  this.backConfig = { ...defaults };
4820
- console.debug('[CRUD:Host] constructed', {
5388
+ debugCrudHost('[CRUD:Host] constructed', {
4821
5389
  action: this.data?.action,
4822
5390
  resourcePath: this.resourcePath,
4823
5391
  resourceId: this.resourceId,
@@ -4857,6 +5425,7 @@ class DynamicFormDialogHostComponent {
4857
5425
  'apiEndpointKey',
4858
5426
  'apiUrlEntry',
4859
5427
  'initialValue',
5428
+ 'layoutPolicy',
4860
5429
  ]);
4861
5430
  const explicit = inputs['initialValue'] && typeof inputs['initialValue'] === 'object'
4862
5431
  ? { ...inputs['initialValue'] }
@@ -4869,10 +5438,50 @@ class DynamicFormDialogHostComponent {
4869
5438
  }
4870
5439
  return Object.keys(explicit).length ? explicit : null;
4871
5440
  }
5441
+ resolveFormConfig() {
5442
+ const config = this.data.metadata?.form;
5443
+ return config && typeof config === 'object' ? config : {};
5444
+ }
5445
+ get dialogTitle() {
5446
+ const actionLabel = this.resolveActionDialogTitle();
5447
+ return actionLabel ||
5448
+ stringOrUndefined(this.formConfig['title']) ||
5449
+ deriveModeTitle(this.mode, this.resolveResourceTitle()) ||
5450
+ this.texts.title;
5451
+ }
5452
+ resolveActionDialogTitle() {
5453
+ const actionLabel = stringOrUndefined(this.data.action?.label);
5454
+ if (!actionLabel) {
5455
+ return undefined;
5456
+ }
5457
+ const resource = this.data.metadata?.resource ?? {};
5458
+ const table = this.data.metadata?.table ?? {};
5459
+ const businessTitle = stringOrUndefined(resource.formTitle ??
5460
+ resource.title ??
5461
+ resource.label ??
5462
+ table.title ??
5463
+ table.label);
5464
+ const routeFallbackTitle = deriveModeTitle(this.mode, titleFromResourcePath(this.resourcePath));
5465
+ return businessTitle && routeFallbackTitle && sameTitle(actionLabel, routeFallbackTitle)
5466
+ ? undefined
5467
+ : actionLabel;
5468
+ }
5469
+ resolveResourceTitle() {
5470
+ const resource = this.data.metadata?.resource ?? {};
5471
+ const table = this.data.metadata?.table ?? {};
5472
+ return stringOrUndefined(resource.formTitle ??
5473
+ resource.title ??
5474
+ resource.label ??
5475
+ table.title ??
5476
+ table.label) || titleFromResourcePath(this.resourcePath);
5477
+ }
4872
5478
  resolveFormActions() {
4873
5479
  if (this.mode === 'view') {
4874
5480
  return undefined;
4875
5481
  }
5482
+ if (this.formConfig?.actions) {
5483
+ return undefined;
5484
+ }
4876
5485
  const submitLabel = this.resolveSubmitLabel();
4877
5486
  if (!submitLabel) {
4878
5487
  return undefined;
@@ -4890,6 +5499,15 @@ class DynamicFormDialogHostComponent {
4890
5499
  },
4891
5500
  };
4892
5501
  }
5502
+ resolveLayoutPolicy() {
5503
+ const explicit = this.data.inputs?.['layoutPolicy'] ??
5504
+ this.data.action?.form?.layoutPolicy ??
5505
+ this.data.action?.layoutPolicy;
5506
+ if (explicit && typeof explicit === 'object') {
5507
+ return explicit;
5508
+ }
5509
+ return null;
5510
+ }
4893
5511
  resolveSubmitLabel() {
4894
5512
  const action = this.data.action ?? {};
4895
5513
  const explicit = stringOrUndefined(action.form?.submitLabel ??
@@ -4962,10 +5580,10 @@ class DynamicFormDialogHostComponent {
4962
5580
  ref
4963
5581
  .afterClosed()
4964
5582
  .pipe(filter((confirmed) => !!confirmed), takeUntilDestroyed(this.destroyRef))
4965
- .subscribe(() => this.dialogRef.close());
5583
+ .subscribe(() => this.dialogRef.close({ type: 'close' }));
4966
5584
  }
4967
5585
  else {
4968
- this.dialogRef.close();
5586
+ this.dialogRef.close({ type: 'close' });
4969
5587
  }
4970
5588
  }
4971
5589
  toggleMaximize(initial = false) {
@@ -4978,11 +5596,22 @@ class DynamicFormDialogHostComponent {
4978
5596
  ? `calc(100dvh - ${2 * (gap ?? 0)}px)`
4979
5597
  : this.initialSize.height;
4980
5598
  this.dialogRef.updateSize(width, height);
4981
- this.dialogRef.updatePosition();
5599
+ this.updateHostPosition();
4982
5600
  const pane = this.dialogRef
4983
5601
  ?._containerInstance?._elementRef?.nativeElement?.parentElement;
4984
5602
  if (pane && pane.classList.contains('pfx-dialog-pane')) {
4985
- pane.style.margin = this.maximized ? `${gap}px` : '';
5603
+ if (this.presentation === 'drawer') {
5604
+ pane.classList.toggle('pfx-drawer-maximized', this.maximized);
5605
+ if (this.maximized) {
5606
+ pane.style.setProperty('--pfx-drawer-edge-gap', `${gap ?? 0}px`);
5607
+ }
5608
+ else {
5609
+ pane.style.removeProperty('--pfx-drawer-edge-gap');
5610
+ }
5611
+ }
5612
+ else {
5613
+ pane.style.margin = this.maximized ? `${gap}px` : '';
5614
+ }
4986
5615
  }
4987
5616
  this.saveState();
4988
5617
  }
@@ -5008,7 +5637,7 @@ class DynamicFormDialogHostComponent {
5008
5637
  width: saved?.width ?? this.modal.width,
5009
5638
  height: saved?.height ?? this.modal.height,
5010
5639
  };
5011
- console.debug('[CRUD:Host] ngOnInit', {
5640
+ debugCrudHost('[CRUD:Host] ngOnInit', {
5012
5641
  initialSize: this.initialSize,
5013
5642
  startMaximized: this.modal.startMaximized,
5014
5643
  fullscreenBreakpoint: this.modal.fullscreenBreakpoint,
@@ -5028,14 +5657,34 @@ class DynamicFormDialogHostComponent {
5028
5657
  }
5029
5658
  else if (this.initialSize.width || this.initialSize.height) {
5030
5659
  this.dialogRef.updateSize(this.initialSize.width, this.initialSize.height);
5660
+ this.updateHostPosition();
5661
+ }
5662
+ }
5663
+ updateHostPosition() {
5664
+ if (this.presentation !== 'drawer') {
5031
5665
  this.dialogRef.updatePosition();
5666
+ return;
5032
5667
  }
5668
+ this.dialogRef.updatePosition(this.maximized
5669
+ ? this.resolveMaximizedDrawerPosition()
5670
+ : this.initialPosition);
5671
+ }
5672
+ resolveMaximizedDrawerPosition() {
5673
+ const position = this.initialPosition ?? { right: '0', top: '0' };
5674
+ const offset = `${this.modal.edgeGap ?? 8}px`;
5675
+ const horizontal = position.left != null
5676
+ ? { left: offset }
5677
+ : { right: offset };
5678
+ const vertical = position.bottom != null && position.top == null
5679
+ ? { bottom: offset }
5680
+ : { top: offset };
5681
+ return { ...horizontal, ...vertical };
5033
5682
  }
5034
5683
  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: `
5684
+ 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
5685
  <div mat-dialog-title class="dialog-header">
5037
5686
  <h2 id="crudDialogTitle" class="dialog-title">
5038
- {{ data.action?.label || texts.title }}
5687
+ {{ dialogTitle }}
5039
5688
  </h2>
5040
5689
  <span class="spacer"></span>
5041
5690
  @if (modal.canMaximize) {
@@ -5063,17 +5712,25 @@ class DynamicFormDialogHostComponent {
5063
5712
  class="dialog-content"
5064
5713
  aria-labelledby="crudDialogTitle"
5065
5714
  >
5715
+ @if (resourceIdentity) {
5716
+ <section class="crud-drawer-resource-identity" data-testid="crud-drawer-resource-identity">
5717
+ <praxis-resource-identity [identity]="resourceIdentity"></praxis-resource-identity>
5718
+ </section>
5719
+ }
5066
5720
  <praxis-dynamic-form
5067
5721
  [formId]="data.action?.formId"
5068
5722
  [resourcePath]="resourcePath"
5069
5723
  [resourceId]="resourceId"
5070
5724
  [initialValue]="initialValue"
5071
5725
  [mode]="mode"
5726
+ [config]="formConfig"
5072
5727
  [schemaUrl]="schemaUrl"
5073
5728
  [submitUrl]="submitUrl"
5074
5729
  [submitMethod]="submitMethod"
5075
5730
  [apiEndpointKey]="apiEndpointKey"
5076
5731
  [apiUrlEntry]="apiUrlEntry"
5732
+ [configPersistenceStrategy]="formConfigPersistenceStrategy"
5733
+ [layoutPolicy]="layoutPolicy"
5077
5734
  [presentationModeGlobal]="mode === 'view' ? true : null"
5078
5735
  [backConfig]="backConfig"
5079
5736
  [actions]="formActions"
@@ -5081,7 +5738,7 @@ class DynamicFormDialogHostComponent {
5081
5738
  (formCancel)="onCancel()"
5082
5739
  ></praxis-dynamic-form>
5083
5740
  </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"] }] });
5741
+ `, 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
5742
  }
5086
5743
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicFormDialogHostComponent, decorators: [{
5087
5744
  type: Component,
@@ -5090,14 +5747,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5090
5747
  MatButtonModule,
5091
5748
  MatIconModule,
5092
5749
  PraxisIconDirective,
5750
+ PraxisResourceIdentityComponent,
5093
5751
  PraxisDynamicForm
5094
- ], providers: [GenericCrudService], host: {
5752
+ ], encapsulation: ViewEncapsulation.None, providers: [GenericCrudService], host: {
5095
5753
  class: 'praxis-dialog',
5096
5754
  '[attr.data-density]': 'modal.density || "default"',
5755
+ '[attr.data-presentation]': 'presentation',
5756
+ '[class.praxis-drawer]': 'presentation === "drawer"',
5097
5757
  }, template: `
5098
5758
  <div mat-dialog-title class="dialog-header">
5099
5759
  <h2 id="crudDialogTitle" class="dialog-title">
5100
- {{ data.action?.label || texts.title }}
5760
+ {{ dialogTitle }}
5101
5761
  </h2>
5102
5762
  <span class="spacer"></span>
5103
5763
  @if (modal.canMaximize) {
@@ -5125,17 +5785,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5125
5785
  class="dialog-content"
5126
5786
  aria-labelledby="crudDialogTitle"
5127
5787
  >
5788
+ @if (resourceIdentity) {
5789
+ <section class="crud-drawer-resource-identity" data-testid="crud-drawer-resource-identity">
5790
+ <praxis-resource-identity [identity]="resourceIdentity"></praxis-resource-identity>
5791
+ </section>
5792
+ }
5128
5793
  <praxis-dynamic-form
5129
5794
  [formId]="data.action?.formId"
5130
5795
  [resourcePath]="resourcePath"
5131
5796
  [resourceId]="resourceId"
5132
5797
  [initialValue]="initialValue"
5133
5798
  [mode]="mode"
5799
+ [config]="formConfig"
5134
5800
  [schemaUrl]="schemaUrl"
5135
5801
  [submitUrl]="submitUrl"
5136
5802
  [submitMethod]="submitMethod"
5137
5803
  [apiEndpointKey]="apiEndpointKey"
5138
5804
  [apiUrlEntry]="apiUrlEntry"
5805
+ [configPersistenceStrategy]="formConfigPersistenceStrategy"
5806
+ [layoutPolicy]="layoutPolicy"
5139
5807
  [presentationModeGlobal]="mode === 'view' ? true : null"
5140
5808
  [backConfig]="backConfig"
5141
5809
  [actions]="formActions"
@@ -5143,7 +5811,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5143
5811
  (formCancel)="onCancel()"
5144
5812
  ></praxis-dynamic-form>
5145
5813
  </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"] }]
5814
+ `, 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
5815
  }], ctorParameters: () => [{ type: undefined, decorators: [{
5148
5816
  type: Inject,
5149
5817
  args: [MatDialogRef]
@@ -5170,6 +5838,17 @@ function stringOrUndefined(value) {
5170
5838
  const text = String(value ?? '').trim();
5171
5839
  return text || undefined;
5172
5840
  }
5841
+ function sameTitle(left, right) {
5842
+ return normalizeTitle(left) === normalizeTitle(right);
5843
+ }
5844
+ function normalizeTitle(value) {
5845
+ return value
5846
+ .normalize('NFD')
5847
+ .replace(/[\u0300-\u036f]/g, '')
5848
+ .replace(/\s+/g, ' ')
5849
+ .trim()
5850
+ .toLocaleLowerCase('pt-BR');
5851
+ }
5173
5852
  function deriveCreateSubmitLabel(actionLabel) {
5174
5853
  if (!actionLabel) {
5175
5854
  return 'Criar';
@@ -5180,6 +5859,61 @@ function deriveCreateSubmitLabel(actionLabel) {
5180
5859
  }
5181
5860
  return actionLabel;
5182
5861
  }
5862
+ function deriveModeTitle(mode, resourceTitle) {
5863
+ if (!resourceTitle) {
5864
+ return undefined;
5865
+ }
5866
+ const singular = lowerFirstLetter(singularizePtBrResourceTitle(resourceTitle));
5867
+ if (mode === 'create') {
5868
+ return `Adicionar ${singular}`;
5869
+ }
5870
+ if (mode === 'edit') {
5871
+ return `Editar ${singular}`;
5872
+ }
5873
+ return `Visualizar ${singular}`;
5874
+ }
5875
+ function titleFromResourcePath(resourcePath) {
5876
+ const lastSegment = resourcePath?.split('/').filter(Boolean).pop();
5877
+ if (!lastSegment) {
5878
+ return undefined;
5879
+ }
5880
+ return lastSegment
5881
+ .replace(/[-_]+/g, ' ')
5882
+ .replace(/\s+/g, ' ')
5883
+ .trim()
5884
+ .toLocaleLowerCase('pt-BR');
5885
+ }
5886
+ function singularizePtBrResourceTitle(title) {
5887
+ const normalized = title.trim();
5888
+ const lower = normalized.toLocaleLowerCase('pt-BR');
5889
+ if (lower === 'códigos de frequência' || lower === 'codigos de frequencia') {
5890
+ return 'código de frequência';
5891
+ }
5892
+ const [first, ...rest] = normalized.split(/\s+/);
5893
+ const singularFirst = singularizePtBrWord(first);
5894
+ return [singularFirst, ...rest].join(' ');
5895
+ }
5896
+ function singularizePtBrWord(word) {
5897
+ if (/ões$/i.test(word)) {
5898
+ return word.replace(/ões$/i, 'ão');
5899
+ }
5900
+ if (/ais$/i.test(word)) {
5901
+ return word.replace(/ais$/i, 'al');
5902
+ }
5903
+ if (/eis$/i.test(word)) {
5904
+ return word.replace(/eis$/i, 'el');
5905
+ }
5906
+ if (/res$/i.test(word)) {
5907
+ return word.replace(/es$/i, '');
5908
+ }
5909
+ if (/s$/i.test(word) && !/ss$/i.test(word)) {
5910
+ return word.replace(/s$/i, '');
5911
+ }
5912
+ return word;
5913
+ }
5914
+ function lowerFirstLetter(value) {
5915
+ return value.replace(/\p{L}/u, (letter) => letter.toLocaleLowerCase('pt-BR'));
5916
+ }
5183
5917
 
5184
5918
  var dynamicFormDialogHost_component = /*#__PURE__*/Object.freeze({
5185
5919
  __proto__: null,
@@ -5229,6 +5963,7 @@ class PraxisCrudWidgetConfigEditor {
5229
5963
  componentInstanceId: this.inputs?.componentInstanceId ?? this.widgetKey,
5230
5964
  context: this.inputs?.context,
5231
5965
  enableCustomization: this.inputs?.enableCustomization ?? false,
5966
+ authoringCapability: this.inputs?.authoringCapability ?? null,
5232
5967
  };
5233
5968
  return {
5234
5969
  inputs,
@@ -5325,11 +6060,31 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
5325
6060
  type: 'PraxisDataQueryContext | null',
5326
6061
  description: 'Contexto semântico de consulta encaminhado para a tabela interna do CRUD. Preferir este contrato para novo authoring remoto.',
5327
6062
  },
6063
+ {
6064
+ name: 'metadata.resource.schemaPath',
6065
+ type: 'string | null',
6066
+ description: 'Template canonico opcional usado apenas para resolver /schemas/filtered quando resource.path precisa ser um alvo operacional concreto.',
6067
+ },
6068
+ {
6069
+ name: 'metadata.resource.title',
6070
+ type: 'string | null',
6071
+ description: 'Titulo documental do recurso usado pelo shell CRUD para derivar titulos contextuais de criacao, edicao e visualizacao quando a action nao informa label.',
6072
+ },
6073
+ {
6074
+ name: 'metadata.resource.formTitle',
6075
+ type: 'string | null',
6076
+ description: 'Titulo documental especifico para formularios do recurso, usado antes de metadata.resource.title no shell modal/drawer.',
6077
+ },
5328
6078
  {
5329
6079
  name: 'metadata.filterCriteria',
5330
6080
  type: 'Record<string, unknown> | null',
5331
6081
  description: 'Bridge declarativa de filtros encaminhada para a tabela interna. Para novo authoring, prefira metadata.queryContext.',
5332
6082
  },
6083
+ {
6084
+ name: 'metadata.form',
6085
+ type: 'FormConfig',
6086
+ 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.',
6087
+ },
5333
6088
  {
5334
6089
  name: 'metadata.actions[].form',
5335
6090
  type: 'CrudActionFormContract',
@@ -5341,6 +6096,12 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
5341
6096
  default: false,
5342
6097
  description: 'Habilita modo de customização do layout.',
5343
6098
  },
6099
+ {
6100
+ name: 'authoringCapability',
6101
+ type: 'string | null',
6102
+ default: null,
6103
+ description: 'Capability publica do contexto corporativo exigida para disponibilizar authoring governado no CRUD e na tabela interna.',
6104
+ },
5344
6105
  ],
5345
6106
  outputs: [
5346
6107
  {
@@ -5699,6 +6460,7 @@ const CRUD_AI_CAPABILITIES = {
5699
6460
  { path: 'actions[].params[].to', category: 'actions', valueKind: 'enum', allowedValues: ENUMS.paramTarget, description: 'Destino do parametro.' },
5700
6461
  { path: 'actions[].params[].name', category: 'actions', valueKind: 'string', description: 'Nome do parametro no destino.' },
5701
6462
  { path: 'actions[].form.initialValue', category: 'actions', valueKind: 'object', description: 'Seed fixo do formulario injetado em inputs.initialValue antes da abertura.' },
6463
+ { 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
6464
  { path: 'actions[].back', category: 'navigation', valueKind: 'object', description: 'BackConfig por acao.' },
5703
6465
  { path: 'actions[].back.strategy', category: 'navigation', valueKind: 'enum', allowedValues: ENUMS.backStrategy, description: 'Estrategia de retorno da acao (auto, close, navigate).' },
5704
6466
  { path: 'actions[].back.returnTo', category: 'navigation', valueKind: 'string', description: 'Rota ou destino usado quando a estrategia de retorno for navigate.' },
@@ -5736,6 +6498,7 @@ const surfaceConfigureSchema = {
5736
6498
  submitMethod: { enum: ['post', 'put', 'patch', 'delete'] },
5737
6499
  apiEndpointKey: { type: 'string' },
5738
6500
  initialValue: { type: 'object' },
6501
+ layoutPolicy: { type: 'object' },
5739
6502
  },
5740
6503
  },
5741
6504
  params: {
@@ -5789,14 +6552,28 @@ const dialogHostSchema = {
5789
6552
  width: { type: 'string' },
5790
6553
  height: { type: 'string' },
5791
6554
  minWidth: { type: 'string' },
6555
+ minHeight: { type: 'string' },
5792
6556
  maxWidth: { type: 'string' },
6557
+ maxHeight: { type: 'string' },
5793
6558
  density: { type: 'string' },
6559
+ backdropStyle: { enum: ['blur', 'dim', 'transparent'] },
5794
6560
  canMaximize: { type: 'boolean' },
5795
6561
  rememberLastState: { type: 'boolean' },
6562
+ closeOnSave: { type: 'boolean' },
5796
6563
  startMaximized: { type: 'boolean' },
5797
6564
  disableCloseOnEsc: { type: 'boolean' },
5798
6565
  disableCloseOnBackdrop: { type: 'boolean' },
5799
- fullscreenBreakpoint: { type: 'string' },
6566
+ fullscreenBreakpoint: { type: 'number' },
6567
+ position: {
6568
+ type: 'object',
6569
+ properties: {
6570
+ top: { type: 'string' },
6571
+ bottom: { type: 'string' },
6572
+ left: { type: 'string' },
6573
+ right: { type: 'string' },
6574
+ },
6575
+ },
6576
+ edgeGap: { type: 'number' },
5800
6577
  },
5801
6578
  },
5802
6579
  back: { type: 'object' },
@@ -5849,19 +6626,21 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
5849
6626
  { name: 'crudId', type: 'string', description: 'Stable CRUD instance id used for table/form identity and persistence.' },
5850
6627
  { name: 'componentInstanceId', type: 'string', description: 'Optional stable host instance id for multiple CRUD widgets on the same route.' },
5851
6628
  { name: 'context', type: 'Record<string, unknown>', description: 'Opaque host context used for authoring seeds and launcher inputs.' },
6629
+ { name: 'enableCustomization', type: 'boolean', description: 'Explicit host opt-in for CRUD authoring surfaces.' },
6630
+ { name: 'authoringCapability', type: 'string | null', description: 'Public runtime capability required before CRUD and delegated table authoring can open.' },
5852
6631
  { name: 'afterOpen', type: '{ mode: FormOpenMode; action: string }', description: 'Emitted after a CRUD action opens.' },
5853
6632
  { name: 'afterSave', type: '{ id: string | number; data: unknown }', description: 'Emitted after save; CRUD refetches the list.' },
5854
6633
  { name: 'afterDelete', type: '{ id: string | number }', description: 'Emitted after delete; CRUD refetches the list.' },
5855
6634
  ],
5856
6635
  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.' },
6636
+ { 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
6637
  { kind: 'listSurface', resolver: 'crud-list-surface', description: 'CRUD-hosted list surface and table delegation boundary.' },
5859
6638
  { kind: 'createSurface', resolver: 'crud-action-by-id:create', description: 'Create action open mode, route/form binding and launcher inputs.' },
5860
6639
  { kind: 'editSurface', resolver: 'crud-action-by-id:edit', description: 'Edit action open mode, route/form binding and launcher inputs.' },
5861
6640
  { kind: 'viewSurface', resolver: 'crud-action-by-id:view', description: 'View action open mode, route/form binding and launcher inputs.' },
5862
6641
  { 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.' },
6642
+ { 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.' },
6643
+ { 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
6644
  { kind: 'permissions', resolver: 'crud-resource-capabilities', description: 'CRUD action availability derived from resource capabilities and action permissions.' },
5866
6645
  { 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
6646
  { kind: 'childOperation', resolver: 'child-authoring-manifest-operation', description: 'Delegated form/table/dialog/settings-panel operation owned by the child component manifest.' },
@@ -5877,13 +6656,13 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
5877
6656
  effects: [{ kind: 'compile-domain-patch', handler: 'crud-resource-bind', handlerContract: {
5878
6657
  reads: ['CrudMetadata.resource', 'api_metadata', 'ResourceDiscoveryService', 'GET /{resource}/capabilities'],
5879
6658
  writes: ['CrudMetadata.resource', 'CrudMetadata.queryContext', 'PraxisCrudComponent.tableCrudContext'],
5880
- identityKeys: ['resourcePath', 'resourceKey'],
6659
+ identityKeys: ['resourcePath', 'resourceKey', 'schemaPath'],
5881
6660
  inputSchema: resourceBindSchema,
5882
6661
  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.',
6662
+ 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
6663
  } }],
5885
6664
  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'],
6665
+ affectedPaths: ['resource.path', 'resource.schemaPath', 'resource.idField', 'resource.endpointKey', 'resource.apiUrlEntry', 'queryContext', 'filterCriteria'],
5887
6666
  submissionImpact: 'affects-remote-binding',
5888
6667
  preconditions: ['crud-metadata-loaded', 'api-metadata-available'],
5889
6668
  },
@@ -6017,14 +6796,14 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
6017
6796
  target: { kind: 'dialogHost', resolver: 'crud-dialog-host-defaults', ambiguityPolicy: 'fail', required: true },
6018
6797
  inputSchema: dialogHostSchema,
6019
6798
  effects: [{ kind: 'compile-domain-patch', handler: 'crud-dialog-host-set', handlerContract: {
6020
- reads: ['CrudMetadata.defaults', 'CrudLauncherService.resolveOpenMode', 'DialogService', 'CRUD_DRAWER_ADAPTER'],
6799
+ reads: ['CrudMetadata.defaults', 'CrudLauncherService.resolveOpenMode', 'DialogService', 'DynamicFormDialogHostComponent', 'CRUD_DRAWER_ADAPTER'],
6021
6800
  writes: ['CrudMetadata.defaults.openMode', 'CrudMetadata.defaults.modal', 'CrudMetadata.defaults.back'],
6022
6801
  identityKeys: ['crudId'],
6023
6802
  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.',
6803
+ failureModes: ['open-mode-unsupported', 'drawer-runtime-unavailable', 'modal-size-invalid', 'drawer-position-lifecycle-unstable', 'back-policy-invalid'],
6804
+ description: 'Configures CRUD-owned route/modal/drawer defaults consumed by the launcher, default dialog-backed drawer runtime and optional host drawer adapter.',
6026
6805
  } }],
6027
- validators: ['open-mode-supported', 'modal-size-valid', 'drawer-adapter-available-when-needed', 'back-policy-valid', 'settings-panel-shell-compatible'],
6806
+ validators: ['open-mode-supported', 'modal-size-valid', 'drawer-runtime-available', 'drawer-position-lifecycle-stable', 'back-policy-valid', 'settings-panel-shell-compatible'],
6028
6807
  affectedPaths: ['defaults.openMode', 'defaults.modal', 'defaults.back'],
6029
6808
  submissionImpact: 'config-only',
6030
6809
  preconditions: ['crud-metadata-loaded'],
@@ -6100,7 +6879,8 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
6100
6879
  { validatorId: 'permissions-delete-valid', level: 'error', code: 'CRUD_DELETE_PERMISSION_VALID', description: 'Delete permission cannot bypass resource capabilities or confirmation policy.' },
6101
6880
  { validatorId: 'open-mode-supported', level: 'error', code: 'CRUD_OPEN_MODE_SUPPORTED', description: 'Open mode must be route, modal or drawer.' },
6102
6881
  { 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.' },
6882
+ { 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.' },
6883
+ { validatorId: 'drawer-position-lifecycle-stable', level: 'error', code: 'CRUD_DRAWER_POSITION_LIFECYCLE_STABLE', description: 'A drawer must preserve its configured viewport edge through open, remembered-size restoration, maximize and restore.' },
6104
6884
  { validatorId: 'back-policy-valid', level: 'error', code: 'CRUD_BACK_POLICY_VALID', description: 'Back policy must be valid for route/modal/drawer behavior.' },
6105
6885
  { validatorId: 'settings-panel-shell-compatible', level: 'warning', code: 'CRUD_SETTINGS_PANEL_COMPATIBLE', description: 'Authoring shell must preserve apply/save/reset semantics.' },
6106
6886
  { validatorId: 'action-permission-supported', level: 'error', code: 'CRUD_ACTION_PERMISSION_SUPPORTED', description: 'Action permissions must map to supported resource capabilities.' },
@@ -6121,6 +6901,7 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
6121
6901
  'FormConfig and FieldMetadata edits must delegate to dynamic-form or metadata-editor manifests.',
6122
6902
  'TableConfig edits must delegate to praxis-table; CRUD owns only shell orchestration and list surface binding.',
6123
6903
  'Delete behavior is destructive and requires explicit confirmation plus backend capability support.',
6904
+ 'Drawer position and edgeGap must round-trip without changing the configured edge during open, remembered-size restoration, maximize or restore.',
6124
6905
  'crudContext must remain reference-stable across change detection cycles.',
6125
6906
  ],
6126
6907
  examples: [
@@ -6132,6 +6913,7 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
6132
6913
  { id: 'crud-view-drawer-readonly', request: 'Open view in a drawer and keep the form readonly.', operationId: 'surface.view.configure', target: 'action:view', params: { actionId: 'view', openMode: 'drawer', formId: 'employee-view' }, isPositive: true },
6133
6914
  { id: 'crud-delete-enable-confirmed', request: 'Enable delete but require confirmation.', operationId: 'delete.enabled.set', target: 'action:delete', params: { enabled: true, actionId: 'delete', requiresConfirmation: true, autoDelete: true }, isPositive: true },
6134
6915
  { id: 'crud-dialog-size-medium', request: 'Make CRUD modals 900px wide and remember their last state.', operationId: 'dialog.size.set', target: 'dialogHost', params: { defaultOpenMode: 'modal', modal: { width: '900px', rememberLastState: true } }, isPositive: true },
6916
+ { id: 'crud-drawer-left-edge', request: 'Open CRUD forms in a left drawer with an 8px maximized inset.', operationId: 'dialog.size.set', target: 'dialogHost', params: { defaultOpenMode: 'drawer', modal: { position: { left: '0', top: '0' }, edgeGap: 8 } }, isPositive: true },
6135
6917
  { id: 'crud-permissions-no-delete', request: 'Hide delete when the resource capabilities do not allow it.', operationId: 'permissions.set', target: 'permissions:delete', params: { requiredCapabilities: ['delete'], denyWhenMissingCapability: true }, isPositive: true },
6136
6918
  { id: 'crud-delegate-form-layout', request: 'Add a new field to the create form.', operationId: 'form.childOperation.delegate', target: 'praxis-dynamic-form:field.add', params: { childComponentId: 'praxis-dynamic-form', childOperationId: 'field.add', reason: 'Form field semantics belong to dynamic-form' }, isPositive: true },
6137
6919
  { id: 'crud-delegate-table-column', request: 'Add a salary column to the list table.', operationId: 'form.childOperation.delegate', target: 'praxis-table:column.add', params: { childComponentId: 'praxis-table', childOperationId: 'column.add', reason: 'Table column semantics belong to praxis-table' }, isPositive: true },