@praxisui/crud 9.0.0-beta.7 → 9.0.0-beta.71

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, GLOBAL_SURFACE_SERVICE, SurfaceOutletRegistryService, ComponentKeyService, ResourceDiscoveryService, ResourceActionOpenAdapterService, ResourceSurfaceOpenAdapterService, translateUnavailableWorkflowMessage, EmptyStateCardComponent, RESOURCE_DISCOVERY_I18N_CONFIG, 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,26 @@ 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 panelClasses = mergeClassList(['pfx-dialog-pane', drawerMode ? 'pfx-drawer-pane' : 'pfx-dialog-frosted'], modalCfg.panelClass);
138
160
  // Backdrop style presets
139
161
  const backdropClasses = [];
140
162
  const style = modalCfg.backdropStyle;
@@ -147,20 +169,56 @@ class CrudLauncherService {
147
169
  else if (style === 'transparent') {
148
170
  backdropClasses.push('pfx-transparent-backdrop');
149
171
  }
150
- if (modalCfg.backdropClass) {
151
- backdropClasses.push(modalCfg.backdropClass);
152
- }
172
+ const mergedBackdropClasses = mergeClassList(backdropClasses, modalCfg.backdropClass);
153
173
  const ref = await this.dialog.openAsync(() => Promise.resolve().then(function () { return dynamicFormDialogHost_component; }).then((m) => m.DynamicFormDialogHostComponent), {
154
174
  ...modalCfg,
155
175
  panelClass: panelClasses,
156
- backdropClass: backdropClasses,
176
+ backdropClass: mergedBackdropClasses,
157
177
  autoFocus: modalCfg.autoFocus ?? true,
158
178
  restoreFocus: modalCfg.restoreFocus ?? true,
159
- minWidth: '360px',
160
- maxWidth: '95vw',
179
+ minWidth: drawerMode
180
+ ? (modalCfg.minWidth ?? DEFAULT_CRUD_DRAWER_MODAL_CONFIG.minWidth)
181
+ : '360px',
182
+ width: drawerMode
183
+ ? (modalCfg.width ?? DEFAULT_CRUD_DRAWER_MODAL_CONFIG.width)
184
+ : modalCfg.width,
185
+ height: drawerMode
186
+ ? (modalCfg.height ?? DEFAULT_CRUD_DRAWER_MODAL_CONFIG.height)
187
+ : modalCfg.height,
188
+ maxWidth: drawerMode
189
+ ? (modalCfg.maxWidth ?? DEFAULT_CRUD_DRAWER_MODAL_CONFIG.maxWidth)
190
+ : '95vw',
191
+ maxHeight: drawerMode
192
+ ? (modalCfg.maxHeight ?? DEFAULT_CRUD_DRAWER_MODAL_CONFIG.maxHeight)
193
+ : modalCfg.maxHeight,
194
+ position: drawerMode
195
+ ? (modalCfg.position ?? DEFAULT_CRUD_DRAWER_MODAL_CONFIG.position)
196
+ : modalCfg.position,
161
197
  ariaLabelledBy: 'crudDialogTitle',
162
- data: { action: actionForLaunch, row, metadata: merged.metadata, inputs },
198
+ data: {
199
+ action: actionForLaunch,
200
+ row,
201
+ metadata: merged.metadata,
202
+ inputs,
203
+ presentation: drawerMode ? 'drawer' : 'modal',
204
+ },
163
205
  });
206
+ if (drawerMode) {
207
+ ref
208
+ .afterClosed()
209
+ .pipe(take(1))
210
+ .subscribe((closedValue) => {
211
+ const result = toCrudDrawerResult(closedValue);
212
+ drawerCallbacks?.onClose?.();
213
+ if (result?.type) {
214
+ drawerCallbacks?.onResult?.(result);
215
+ }
216
+ else {
217
+ drawerCallbacks?.onResult?.({ type: 'close' });
218
+ }
219
+ });
220
+ return { mode, ref };
221
+ }
164
222
  return { mode, ref };
165
223
  }
166
224
  resolveOpenMode(action, metadata) {
@@ -248,6 +306,9 @@ class CrudLauncherService {
248
306
  if (resolved.apiUrlEntry != null) {
249
307
  inputs['apiUrlEntry'] = resolved.apiUrlEntry;
250
308
  }
309
+ if (resolved.layoutPolicy != null) {
310
+ inputs['layoutPolicy'] = resolved.layoutPolicy;
311
+ }
251
312
  if (action.form?.initialValue != null) {
252
313
  inputs['initialValue'] = action.form.initialValue;
253
314
  }
@@ -257,6 +318,7 @@ class CrudLauncherService {
257
318
  return inputs;
258
319
  }
259
320
  resolveActionForLaunch(action, metadata) {
321
+ action = this.normalizeActionForLaunch(action);
260
322
  if (action.formId) {
261
323
  return action;
262
324
  }
@@ -264,29 +326,61 @@ class CrudLauncherService {
264
326
  return action;
265
327
  }
266
328
  const formId = this.buildInferredFormId(action, metadata);
267
- return formId ? { ...action, formId } : action;
329
+ return formId
330
+ ? {
331
+ ...action,
332
+ formId,
333
+ form: {
334
+ ...(action.form || {}),
335
+ layoutPolicy: action.form?.layoutPolicy ?? this.defaultInferredLayoutPolicy(action),
336
+ },
337
+ }
338
+ : action;
339
+ }
340
+ resolveSubmitUrlTemplate(submitUrl, row, metadata) {
341
+ const template = String(submitUrl || '').trim();
342
+ if (!template || !template.includes('{')) {
343
+ return template;
344
+ }
345
+ const idField = String(metadata.resource?.idField ?? 'id');
346
+ const rowId = row?.[idField];
347
+ if (rowId == null) {
348
+ return template;
349
+ }
350
+ const encodedId = encodeURIComponent(String(rowId));
351
+ const idFieldPattern = new RegExp(`\\{${escapeRegExp(idField)}\\}`, 'g');
352
+ return template
353
+ .replace(idFieldPattern, encodedId)
354
+ .replace(/\{id\}/g, encodedId)
355
+ .replace(/\{resourceId\}/g, encodedId);
268
356
  }
269
357
  resolveActionFormContract(action, row, metadata, runtime) {
270
358
  if (this.isExplicitCrudAction(action)) {
271
359
  return {
272
360
  schemaUrl: action.form?.schemaUrl ?? null,
273
- submitUrl: action.form?.submitUrl ?? null,
361
+ submitUrl: action.form?.submitUrl
362
+ ? this.resolveSubmitUrlTemplate(action.form.submitUrl, row, metadata)
363
+ : null,
274
364
  submitMethod: action.form?.submitMethod ?? null,
275
365
  apiEndpointKey: action.form?.apiEndpointKey ?? null,
276
366
  apiUrlEntry: action.form?.apiUrlEntry ?? null,
367
+ layoutPolicy: action.form?.layoutPolicy ?? null,
277
368
  };
278
369
  }
279
370
  if (!this.isCanonicalCrudAction(action.action)) {
280
371
  return {
281
372
  apiEndpointKey: action.form?.apiEndpointKey ?? null,
282
373
  apiUrlEntry: action.form?.apiUrlEntry ?? null,
374
+ layoutPolicy: action.form?.layoutPolicy ?? null,
283
375
  };
284
376
  }
285
377
  const resourcePath = String(metadata.resource?.path ?? metadata.table?.resourcePath ?? '').trim();
378
+ const schemaResourcePath = String(metadata.resource?.schemaPath || '').trim() || null;
286
379
  if (!resourcePath) {
287
380
  return {
288
381
  apiEndpointKey: action.form?.apiEndpointKey ?? null,
289
382
  apiUrlEntry: action.form?.apiUrlEntry ?? null,
383
+ layoutPolicy: action.form?.layoutPolicy ?? null,
290
384
  };
291
385
  }
292
386
  const idField = String(metadata.resource?.idField ?? 'id');
@@ -294,6 +388,7 @@ class CrudLauncherService {
294
388
  const resolved = this.operationResolver.resolve({
295
389
  operation: action.action,
296
390
  resourcePath,
391
+ schemaResourcePath,
297
392
  resourceId: resourceId ?? null,
298
393
  capabilities: runtime?.capabilities ?? null,
299
394
  links: runtime?.links ?? null,
@@ -301,14 +396,15 @@ class CrudLauncherService {
301
396
  endpointKey: action.form?.apiEndpointKey ??
302
397
  metadata.resource?.endpointKey ??
303
398
  undefined,
304
- apiUrlEntry: action.form?.apiUrlEntry ?? null,
399
+ apiUrlEntry: action.form?.apiUrlEntry ?? metadata.resource?.apiUrlEntry ?? null,
305
400
  });
306
401
  return {
307
402
  schemaUrl: resolved?.schemaUrl ?? null,
308
403
  submitUrl: resolved?.submitUrl ?? null,
309
404
  submitMethod: resolved?.submitMethod ?? null,
310
405
  apiEndpointKey: action.form?.apiEndpointKey ?? metadata.resource?.endpointKey ?? null,
311
- apiUrlEntry: action.form?.apiUrlEntry ?? null,
406
+ apiUrlEntry: action.form?.apiUrlEntry ?? metadata.resource?.apiUrlEntry ?? null,
407
+ layoutPolicy: action.form?.layoutPolicy ?? null,
312
408
  };
313
409
  }
314
410
  resolveRuntimeContract(action, row, metadata, runtime) {
@@ -327,9 +423,20 @@ class CrudLauncherService {
327
423
  return `${sanitizedResource || 'crud'}-${String(action.action || '').trim().toLowerCase()}`;
328
424
  }
329
425
  isCanonicalCrudAction(actionName) {
330
- const normalized = String(actionName || '').trim().toLowerCase();
426
+ const normalized = this.normalizeCrudOperationName(actionName);
331
427
  return normalized === 'create' || normalized === 'view' || normalized === 'edit' || normalized === 'delete';
332
428
  }
429
+ normalizeActionForLaunch(action) {
430
+ const normalized = this.normalizeCrudOperationName(action.action);
431
+ return normalized === action.action ? action : { ...action, action: normalized };
432
+ }
433
+ normalizeCrudOperationName(actionName) {
434
+ const normalized = String(actionName || '').trim().toLowerCase();
435
+ if (['add', 'novo', 'new', 'incluir', 'inserir'].includes(normalized)) {
436
+ return 'create';
437
+ }
438
+ return normalized;
439
+ }
333
440
  isExplicitCrudAction(action) {
334
441
  if (action.mode === 'explicit') {
335
442
  return true;
@@ -338,6 +445,19 @@ class CrudLauncherService {
338
445
  String(action.form?.submitUrl || '').trim() ||
339
446
  String(action.form?.submitMethod || '').trim());
340
447
  }
448
+ defaultInferredLayoutPolicy(action) {
449
+ const operation = this.normalizeCrudOperationName(action.action);
450
+ if (operation !== 'create' && operation !== 'edit') {
451
+ return null;
452
+ }
453
+ return {
454
+ source: 'schema',
455
+ intent: 'command',
456
+ preset: 'groupedCommand',
457
+ persistence: 'transient',
458
+ schemaType: 'request',
459
+ };
460
+ }
341
461
  async mergeCrudOverrides(metadata, action, componentKeyId) {
342
462
  try {
343
463
  if (!componentKeyId)
@@ -392,6 +512,33 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
392
512
  type: Injectable,
393
513
  args: [{ providedIn: 'root' }]
394
514
  }] });
515
+ function mergeClassList(base, extra) {
516
+ if (!extra) {
517
+ return base;
518
+ }
519
+ if (typeof extra === 'string') {
520
+ return [...base, extra];
521
+ }
522
+ if (Array.isArray(extra)) {
523
+ return [...base, ...extra];
524
+ }
525
+ return [
526
+ ...base,
527
+ ...Object.keys(extra).filter((className) => !!extra[className]),
528
+ ];
529
+ }
530
+ function escapeRegExp(value) {
531
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
532
+ }
533
+ function toCrudDrawerResult(value) {
534
+ if (!value || typeof value !== 'object') {
535
+ return undefined;
536
+ }
537
+ const result = value;
538
+ return result.type === 'save' || result.type === 'delete' || result.type === 'close'
539
+ ? result
540
+ : undefined;
541
+ }
395
542
 
396
543
  const DOCUMENT_KIND = 'praxis.crud.editor';
397
544
  const DOCUMENT_VERSION = 1;
@@ -510,10 +657,15 @@ function normalizeCrudMetadata(metadata) {
510
657
  if (base.resource && typeof base.resource === 'object') {
511
658
  normalized.resource = stripUndefinedShallow({
512
659
  path: trimString(base.resource.path) || undefined,
660
+ schemaPath: trimString(base.resource.schemaPath) || undefined,
513
661
  idField: typeof base.resource.idField === 'string'
514
662
  ? trimString(base.resource.idField) || undefined
515
663
  : base.resource.idField,
664
+ title: trimString(base.resource.title) || undefined,
665
+ label: trimString(base.resource.label) || undefined,
666
+ formTitle: trimString(base.resource.formTitle) || undefined,
516
667
  endpointKey: trimString(base.resource.endpointKey) || undefined,
668
+ apiUrlEntry: base.resource.apiUrlEntry || undefined,
517
669
  });
518
670
  }
519
671
  if (base.defaults && typeof base.defaults === 'object') {
@@ -564,6 +716,7 @@ function normalizeCrudAction(action) {
564
716
  apiEndpointKey: trimString(action.form.apiEndpointKey) || undefined,
565
717
  apiUrlEntry: action.form.apiUrlEntry || undefined,
566
718
  initialValue: normalizeInitialValue(action.form.initialValue),
719
+ layoutPolicy: action.form.layoutPolicy ?? undefined,
567
720
  }
568
721
  : undefined,
569
722
  });
@@ -3498,6 +3651,7 @@ class PraxisCrudComponent {
3498
3651
  resourceDiscoveryInstance;
3499
3652
  actionOpenAdapterInstance;
3500
3653
  surfaceOpenAdapterInstance;
3654
+ materializedRefreshSequence = 0;
3501
3655
  global = (() => {
3502
3656
  try {
3503
3657
  return inject(GlobalConfigService);
@@ -3507,6 +3661,7 @@ class PraxisCrudComponent {
3507
3661
  }
3508
3662
  })();
3509
3663
  surfaceService = inject(GLOBAL_SURFACE_SERVICE, { optional: true });
3664
+ surfaceOutlets = inject(SurfaceOutletRegistryService);
3510
3665
  componentKeys = inject(ComponentKeyService);
3511
3666
  route = (() => {
3512
3667
  try {
@@ -3527,6 +3682,7 @@ class PraxisCrudComponent {
3527
3682
  collectionCapabilitiesResolvedHref = null;
3528
3683
  collectionCapabilitiesRequestSeq = 0;
3529
3684
  currentAuthoringDocument;
3685
+ selectedRow = null;
3530
3686
  getResourceDiscovery() {
3531
3687
  const assigned = this.resourceDiscovery;
3532
3688
  return assigned ?? (this.resourceDiscoveryInstance ??= this.injector.get(ResourceDiscoveryService));
@@ -3553,15 +3709,32 @@ class PraxisCrudComponent {
3553
3709
  catch { }
3554
3710
  }
3555
3711
  onTableRowClick(event) {
3712
+ const row = this.extractRowFromEvent(event);
3713
+ if (row) {
3714
+ this.selectedRow = row;
3715
+ }
3556
3716
  this.rowClick.emit(event);
3557
3717
  }
3558
3718
  onTableSelectionChange(event) {
3719
+ this.selectedRow = this.extractSelectedRowFromEvent(event);
3559
3720
  this.selectionChange.emit(event);
3560
3721
  }
3722
+ async onBulkAction(event) {
3723
+ const action = String(event?.action || '').trim();
3724
+ if (!action) {
3725
+ return;
3726
+ }
3727
+ const row = this.extractSelectedRowFromBulkAction(event);
3728
+ if (row) {
3729
+ this.selectedRow = row;
3730
+ }
3731
+ await this.onAction(action, row ?? undefined, event);
3732
+ }
3561
3733
  ngOnChanges(changes) {
3562
3734
  if (!changes['metadata'] && !changes['context']) {
3563
3735
  return;
3564
3736
  }
3737
+ this.materializedRefreshSequence += 1;
3565
3738
  try {
3566
3739
  const parsed = typeof this.metadata === 'string'
3567
3740
  ? JSON.parse(this.metadata)
@@ -3595,33 +3768,41 @@ class PraxisCrudComponent {
3595
3768
  async onAction(action, row, runtimeEvent) {
3596
3769
  try {
3597
3770
  document.activeElement?.blur();
3598
- let actionMeta = this.resolvedMetadata.actions?.find((candidate) => candidate.action === action);
3771
+ const normalizedAction = this.normalizeCrudActionName(action);
3772
+ const contextualRow = row ?? this.resolveSelectedRowForAction(normalizedAction);
3773
+ let actionMeta = this.resolvedMetadata.actions?.find((candidate) => this.normalizeCrudActionName(candidate.action) === normalizedAction);
3774
+ if (!actionMeta && normalizedAction !== action) {
3775
+ actionMeta = this.resolvedMetadata.actions?.find((candidate) => candidate.action === action);
3776
+ }
3599
3777
  if (!actionMeta) {
3600
- const ctxAction = this.tableCrudContext?.actions?.find((candidate) => candidate.action === action);
3778
+ const ctxAction = this.tableCrudContext?.actions?.find((candidate) => this.normalizeCrudActionName(candidate.action) === normalizedAction);
3601
3779
  if (ctxAction) {
3602
3780
  actionMeta = {
3603
- action: ctxAction.action,
3781
+ action: this.normalizeCrudActionName(ctxAction.action),
3604
3782
  openMode: ctxAction.openMode,
3605
3783
  formId: ctxAction.formId,
3606
3784
  route: ctxAction.route,
3607
3785
  };
3608
3786
  }
3609
3787
  else {
3610
- actionMeta = { action };
3788
+ actionMeta = { action: normalizedAction };
3611
3789
  }
3612
3790
  }
3613
- const effectiveAction = (actionMeta || { action });
3791
+ const effectiveAction = {
3792
+ ...(actionMeta || { action: normalizedAction }),
3793
+ action: this.normalizeCrudActionName(actionMeta?.action ?? normalizedAction),
3794
+ };
3614
3795
  if (!this.hasExplicitOpenBinding(effectiveAction)) {
3615
- const openedByDiscovery = await this.tryOpenDiscoveredCrudSurface(action, row, runtimeEvent);
3796
+ const openedByDiscovery = await this.tryOpenDiscoveredCrudSurface(effectiveAction.action, contextualRow, runtimeEvent);
3616
3797
  if (openedByDiscovery) {
3617
3798
  return;
3618
3799
  }
3619
- const handledByWorkflowAction = await this.tryOpenDiscoveredWorkflowAction(action, row, runtimeEvent);
3800
+ const handledByWorkflowAction = await this.tryOpenDiscoveredWorkflowAction(effectiveAction.action, contextualRow, runtimeEvent);
3620
3801
  if (handledByWorkflowAction) {
3621
3802
  return;
3622
3803
  }
3623
3804
  }
3624
- const handledByDelete = await this.tryHandleCanonicalDeleteAction(effectiveAction, row);
3805
+ const handledByDelete = await this.tryHandleCanonicalDeleteAction(effectiveAction, contextualRow);
3625
3806
  if (handledByDelete) {
3626
3807
  return;
3627
3808
  }
@@ -3632,7 +3813,7 @@ class PraxisCrudComponent {
3632
3813
  drawerCloseEmitted = true;
3633
3814
  this.afterClose.emit();
3634
3815
  };
3635
- const { mode, ref } = await this.launcher.launch(effectiveAction, row, this.resolvedMetadata, this.componentKeyId(), {
3816
+ const { mode, ref } = await this.launcher.launch(effectiveAction, contextualRow, this.resolvedMetadata, this.componentKeyId(), {
3636
3817
  onClose: () => emitDrawerClose(),
3637
3818
  onResult: (result) => {
3638
3819
  emitDrawerClose();
@@ -3653,9 +3834,12 @@ class PraxisCrudComponent {
3653
3834
  capabilities: effectiveAction.action === 'create'
3654
3835
  ? this.collectionCapabilities
3655
3836
  : this.collectionCapabilities,
3656
- links: this.resolveCrudRuntimeLinks(effectiveAction.action, row),
3837
+ links: this.resolveCrudRuntimeLinks(effectiveAction.action, contextualRow),
3657
3838
  });
3658
3839
  this.afterOpen.emit({ mode, action: effectiveAction.action });
3840
+ if (mode === 'drawer') {
3841
+ return;
3842
+ }
3659
3843
  if (!ref) {
3660
3844
  return;
3661
3845
  }
@@ -3842,8 +4026,77 @@ class PraxisCrudComponent {
3842
4026
  return true;
3843
4027
  }
3844
4028
  refreshTable() {
4029
+ if (this.tryRefreshMaterializedLocalData()) {
4030
+ return;
4031
+ }
3845
4032
  this.table.refetch();
3846
4033
  }
4034
+ tryRefreshMaterializedLocalData() {
4035
+ const readUrl = this.resolveMaterializedReadUrl();
4036
+ if (!readUrl || this.resolveResourcePath(this.resolvedMetadata)) {
4037
+ return false;
4038
+ }
4039
+ const refreshSequence = ++this.materializedRefreshSequence;
4040
+ void firstValueFrom(this.getResourceDiscovery().fetchJson(readUrl, {
4041
+ endpointKey: this.resolvedMetadata.resource?.endpointKey,
4042
+ apiUrlEntry: this.resolvedMetadata.resource?.apiUrlEntry,
4043
+ }))
4044
+ .then((response) => {
4045
+ if (!this.isCurrentMaterializedRefresh(refreshSequence, readUrl)) {
4046
+ return;
4047
+ }
4048
+ const data = this.extractCollectionData(this.unwrapRestData(response));
4049
+ if (!data) {
4050
+ this.error.emit({
4051
+ code: 'CRUD_MATERIALIZED_REFRESH_INVALID_COLLECTION',
4052
+ readUrl,
4053
+ });
4054
+ return;
4055
+ }
4056
+ this.resolvedMetadata = {
4057
+ ...this.resolvedMetadata,
4058
+ data,
4059
+ };
4060
+ this.applyResolvedCrudState(this.resolvedMetadata);
4061
+ this.cdr.markForCheck();
4062
+ })
4063
+ .catch((error) => this.error.emit(error));
4064
+ return true;
4065
+ }
4066
+ isCurrentMaterializedRefresh(refreshSequence, readUrl) {
4067
+ return (refreshSequence === this.materializedRefreshSequence &&
4068
+ this.resolveMaterializedReadUrl() === readUrl &&
4069
+ this.resolveResourcePath(this.resolvedMetadata).length === 0);
4070
+ }
4071
+ resolveMaterializedReadUrl() {
4072
+ const materialization = this.context?.['materialization'];
4073
+ return String(materialization?.['readUrl'] || '').trim();
4074
+ }
4075
+ unwrapRestData(value) {
4076
+ if (value && typeof value === 'object' && !Array.isArray(value) && 'data' in value) {
4077
+ return value.data;
4078
+ }
4079
+ return value;
4080
+ }
4081
+ extractCollectionData(data) {
4082
+ if (Array.isArray(data)) {
4083
+ return data;
4084
+ }
4085
+ if (!data || typeof data !== 'object') {
4086
+ return null;
4087
+ }
4088
+ const record = data;
4089
+ if (Array.isArray(record['content'])) {
4090
+ return record['content'];
4091
+ }
4092
+ if (Array.isArray(record['items'])) {
4093
+ return record['items'];
4094
+ }
4095
+ if (Array.isArray(record['data'])) {
4096
+ return record['data'];
4097
+ }
4098
+ return null;
4099
+ }
3847
4100
  emitTableRuntimeConfigSnapshot() {
3848
4101
  const snapshot = this.getCurrentTableConfigSnapshot();
3849
4102
  if (!snapshot)
@@ -3901,11 +4154,47 @@ class PraxisCrudComponent {
3901
4154
  }
3902
4155
  }
3903
4156
  resolveQueryContext(meta) {
4157
+ if (Array.isArray(meta?.data) && this.resolveResourcePath(meta).length === 0) {
4158
+ return null;
4159
+ }
3904
4160
  return this.isRecord(meta?.queryContext) ? meta.queryContext : null;
3905
4161
  }
3906
4162
  resolveFilterCriteria(meta) {
3907
4163
  return this.isRecord(meta?.filterCriteria) ? { ...meta.filterCriteria } : {};
3908
4164
  }
4165
+ extractRowFromEvent(event) {
4166
+ if (!this.isRecord(event)) {
4167
+ return null;
4168
+ }
4169
+ const row = event['row'];
4170
+ return this.isRecord(row) ? row : null;
4171
+ }
4172
+ extractSelectedRowFromEvent(event) {
4173
+ if (!this.isRecord(event)) {
4174
+ return null;
4175
+ }
4176
+ const selection = event['selection'];
4177
+ if (Array.isArray(selection)) {
4178
+ const first = selection[0];
4179
+ return this.isRecord(first) ? first : null;
4180
+ }
4181
+ const row = event['row'];
4182
+ if (this.isRecord(row)) {
4183
+ return row;
4184
+ }
4185
+ return null;
4186
+ }
4187
+ extractSelectedRowFromBulkAction(event) {
4188
+ const first = Array.isArray(event?.rows) ? event.rows[0] : null;
4189
+ return this.isRecord(first) ? first : null;
4190
+ }
4191
+ resolveSelectedRowForAction(action) {
4192
+ const normalized = String(action || '').trim().toLowerCase();
4193
+ if (!normalized || normalized === 'create' || normalized === 'add' || normalized === 'new') {
4194
+ return undefined;
4195
+ }
4196
+ return this.selectedRow ?? undefined;
4197
+ }
3909
4198
  async tryOpenDiscoveredCrudSurface(action, row, runtimeEvent) {
3910
4199
  const normalizedAction = String(action || '').trim().toLowerCase();
3911
4200
  if (!this.surfaceService) {
@@ -3942,7 +4231,7 @@ class PraxisCrudComponent {
3942
4231
  }
3943
4232
  let openPromise;
3944
4233
  try {
3945
- openPromise = Promise.resolve(this.surfaceService.open(payload, {
4234
+ const surfaceContext = {
3946
4235
  sourceId: this.componentKeyId() || undefined,
3947
4236
  payload: { action: normalizedAction, row: row ?? null },
3948
4237
  meta: {
@@ -3959,7 +4248,9 @@ class PraxisCrudComponent {
3959
4248
  resourcePath,
3960
4249
  },
3961
4250
  },
3962
- }));
4251
+ };
4252
+ const handledInline = await this.surfaceOutlets.tryActivate(payload, surfaceContext);
4253
+ openPromise = handledInline ? Promise.resolve(undefined) : Promise.resolve(this.surfaceService.open(payload, surfaceContext));
3963
4254
  }
3964
4255
  catch {
3965
4256
  return false;
@@ -3999,7 +4290,7 @@ class PraxisCrudComponent {
3999
4290
  const providedAction = this.resolveProvidedWorkflowAction(normalizedAction, runtimeEvent?.actionConfig);
4000
4291
  const catalog = providedAction ? null : await this.resolveDiscoveredActionCatalog(row);
4001
4292
  const discoveredAction = providedAction || this.selectDiscoveredWorkflowAction(normalizedAction, catalog?.actions || []);
4002
- const resourcePath = String(this.resolveResourcePath(this.resolvedMetadata) || catalog?.resourcePath || '').trim();
4293
+ const resourcePath = String(catalog?.resourcePath || this.resolveResourcePath(this.resolvedMetadata) || '').trim();
4003
4294
  if (!discoveredAction || !resourcePath) {
4004
4295
  return false;
4005
4296
  }
@@ -4222,12 +4513,19 @@ class PraxisCrudComponent {
4222
4513
  }
4223
4514
  return cfg.toolbar;
4224
4515
  };
4225
- const hasToolbarAdd = (cfg.toolbar?.actions || []).some((action) => this.isAddLike(action));
4516
+ const hasToolbarAdd = (cfg.toolbar?.actions || []).some((action) => this.shouldCanonicalizeToolbarCreateAction(action));
4226
4517
  const addAction = this.resolveCreateToolbarAction(meta, capabilities);
4227
4518
  if (addAction) {
4228
4519
  if (hasToolbarAdd) {
4229
4520
  const toolbar = ensureToolbar();
4230
4521
  toolbar.visible = true;
4522
+ toolbar.actions = (toolbar.actions || []).map((action) => this.shouldCanonicalizeToolbarCreateAction(action)
4523
+ ? {
4524
+ ...action,
4525
+ action: 'create',
4526
+ }
4527
+ : action);
4528
+ changed = true;
4231
4529
  }
4232
4530
  else {
4233
4531
  const toolbar = ensureToolbar();
@@ -4362,6 +4660,28 @@ class PraxisCrudComponent {
4362
4660
  }
4363
4661
  return false;
4364
4662
  }
4663
+ normalizeCrudActionName(action) {
4664
+ const raw = String(action || '').trim();
4665
+ const normalized = raw.toLowerCase();
4666
+ return this.isCreateActionAliasName(normalized) ? 'create' : raw;
4667
+ }
4668
+ shouldCanonicalizeToolbarCreateAction(action) {
4669
+ if (!action)
4670
+ return false;
4671
+ const normalize = (value) => String(value || '').trim().toLowerCase();
4672
+ const explicitId = normalize(action.action || action.id || action.code || action.key || action.name || action.type);
4673
+ if (this.isCreateActionAliasName(explicitId))
4674
+ return true;
4675
+ if (explicitId)
4676
+ return false;
4677
+ const icon = normalize(action.icon);
4678
+ const label = normalize(action.label);
4679
+ return ((icon === 'add' || icon === 'add_circle' || icon === 'add_box') &&
4680
+ (label === 'adicionar' || label === 'novo' || label === 'criar' || label === 'incluir'));
4681
+ }
4682
+ isCreateActionAliasName(actionName) {
4683
+ return ['create', 'add', 'novo', 'new', 'incluir', 'inserir'].includes(actionName);
4684
+ }
4365
4685
  buildTableCrudContext(meta, capabilities) {
4366
4686
  if (!meta)
4367
4687
  return undefined;
@@ -4642,6 +4962,7 @@ class PraxisCrudComponent {
4642
4962
  (selectionChange)="onTableSelectionChange($event)"
4643
4963
  (rowAction)="onAction($event.action, $event.row, $event)"
4644
4964
  (toolbarAction)="onAction($event.action)"
4965
+ (bulkAction)="onBulkAction($event)"
4645
4966
  (collectionLinksChange)="onCollectionLinksChange($event)"
4646
4967
  (reset)="onResetPreferences()"
4647
4968
  (metadataChange)="onTableMetadataChange()"
@@ -4657,7 +4978,7 @@ class PraxisCrudComponent {
4657
4978
  />
4658
4979
  }
4659
4980
  }
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"] }] });
4981
+ `, 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", "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
4982
  }
4662
4983
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisCrudComponent, decorators: [{
4663
4984
  type: Component,
@@ -4679,6 +5000,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
4679
5000
  (selectionChange)="onTableSelectionChange($event)"
4680
5001
  (rowAction)="onAction($event.action, $event.row, $event)"
4681
5002
  (toolbarAction)="onAction($event.action)"
5003
+ (bulkAction)="onBulkAction($event)"
4682
5004
  (collectionLinksChange)="onCollectionLinksChange($event)"
4683
5005
  (reset)="onResetPreferences()"
4684
5006
  (metadataChange)="onTableMetadataChange()"
@@ -4734,6 +5056,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
4734
5056
  args: [PraxisTable]
4735
5057
  }] } });
4736
5058
 
5059
+ function isCrudDebugEnabled() {
5060
+ try {
5061
+ if (globalThis.__PRAXIS_DEBUG_CRUD__) {
5062
+ return true;
5063
+ }
5064
+ if (typeof localStorage !== 'undefined') {
5065
+ return localStorage.getItem('praxis.debug.crud') === 'true';
5066
+ }
5067
+ }
5068
+ catch { }
5069
+ return false;
5070
+ }
5071
+ function debugCrudHost(message, data) {
5072
+ if (!isCrudDebugEnabled()) {
5073
+ return;
5074
+ }
5075
+ try {
5076
+ console.debug(message, data);
5077
+ }
5078
+ catch { }
5079
+ }
4737
5080
  class DynamicFormDialogHostComponent {
4738
5081
  dialogRef;
4739
5082
  data;
@@ -4742,6 +5085,7 @@ class DynamicFormDialogHostComponent {
4742
5085
  configStorage;
4743
5086
  formComp;
4744
5087
  modal = {};
5088
+ presentation = 'modal';
4745
5089
  maximized = false;
4746
5090
  initialSize = {};
4747
5091
  rememberState = false;
@@ -4756,7 +5100,10 @@ class DynamicFormDialogHostComponent {
4756
5100
  submitMethod;
4757
5101
  apiEndpointKey;
4758
5102
  apiUrlEntry;
5103
+ layoutPolicy;
5104
+ formConfig = {};
4759
5105
  formActions;
5106
+ formConfigPersistenceStrategy = 'input-first';
4760
5107
  mode = 'create';
4761
5108
  backConfig;
4762
5109
  idField = 'id';
@@ -4778,6 +5125,7 @@ class DynamicFormDialogHostComponent {
4778
5125
  this.crud = crud;
4779
5126
  this.configStorage = configStorage;
4780
5127
  this.dialogRef.disableClose = true;
5128
+ this.presentation = this.data.presentation === 'drawer' ? 'drawer' : 'modal';
4781
5129
  // i18n
4782
5130
  this.texts = {
4783
5131
  ...this.texts,
@@ -4812,12 +5160,14 @@ class DynamicFormDialogHostComponent {
4812
5160
  this.apiUrlEntry = this.data.inputs?.['apiUrlEntry'] ?? null;
4813
5161
  const act = this.data.action?.action;
4814
5162
  this.mode = act === 'edit' ? 'edit' : act === 'view' ? 'view' : 'create';
5163
+ this.formConfig = this.resolveFormConfig();
5164
+ this.layoutPolicy = this.resolveLayoutPolicy();
4815
5165
  this.formActions = this.resolveFormActions();
4816
5166
  // Back config: defaults from metadata/action, overridden by saved per-form config
4817
5167
  const defaults = (this.data.action?.back || this.data.metadata?.defaults?.back) || {};
4818
5168
  this.backDefaults = defaults;
4819
5169
  this.backConfig = { ...defaults };
4820
- console.debug('[CRUD:Host] constructed', {
5170
+ debugCrudHost('[CRUD:Host] constructed', {
4821
5171
  action: this.data?.action,
4822
5172
  resourcePath: this.resourcePath,
4823
5173
  resourceId: this.resourceId,
@@ -4857,6 +5207,7 @@ class DynamicFormDialogHostComponent {
4857
5207
  'apiEndpointKey',
4858
5208
  'apiUrlEntry',
4859
5209
  'initialValue',
5210
+ 'layoutPolicy',
4860
5211
  ]);
4861
5212
  const explicit = inputs['initialValue'] && typeof inputs['initialValue'] === 'object'
4862
5213
  ? { ...inputs['initialValue'] }
@@ -4869,10 +5220,50 @@ class DynamicFormDialogHostComponent {
4869
5220
  }
4870
5221
  return Object.keys(explicit).length ? explicit : null;
4871
5222
  }
5223
+ resolveFormConfig() {
5224
+ const config = this.data.metadata?.form;
5225
+ return config && typeof config === 'object' ? config : {};
5226
+ }
5227
+ get dialogTitle() {
5228
+ const actionLabel = this.resolveActionDialogTitle();
5229
+ return actionLabel ||
5230
+ stringOrUndefined(this.formConfig['title']) ||
5231
+ deriveModeTitle(this.mode, this.resolveResourceTitle()) ||
5232
+ this.texts.title;
5233
+ }
5234
+ resolveActionDialogTitle() {
5235
+ const actionLabel = stringOrUndefined(this.data.action?.label);
5236
+ if (!actionLabel) {
5237
+ return undefined;
5238
+ }
5239
+ const resource = this.data.metadata?.resource ?? {};
5240
+ const table = this.data.metadata?.table ?? {};
5241
+ const businessTitle = stringOrUndefined(resource.formTitle ??
5242
+ resource.title ??
5243
+ resource.label ??
5244
+ table.title ??
5245
+ table.label);
5246
+ const routeFallbackTitle = deriveModeTitle(this.mode, titleFromResourcePath(this.resourcePath));
5247
+ return businessTitle && routeFallbackTitle && sameTitle(actionLabel, routeFallbackTitle)
5248
+ ? undefined
5249
+ : actionLabel;
5250
+ }
5251
+ resolveResourceTitle() {
5252
+ const resource = this.data.metadata?.resource ?? {};
5253
+ const table = this.data.metadata?.table ?? {};
5254
+ return stringOrUndefined(resource.formTitle ??
5255
+ resource.title ??
5256
+ resource.label ??
5257
+ table.title ??
5258
+ table.label) || titleFromResourcePath(this.resourcePath);
5259
+ }
4872
5260
  resolveFormActions() {
4873
5261
  if (this.mode === 'view') {
4874
5262
  return undefined;
4875
5263
  }
5264
+ if (this.formConfig?.actions) {
5265
+ return undefined;
5266
+ }
4876
5267
  const submitLabel = this.resolveSubmitLabel();
4877
5268
  if (!submitLabel) {
4878
5269
  return undefined;
@@ -4890,6 +5281,15 @@ class DynamicFormDialogHostComponent {
4890
5281
  },
4891
5282
  };
4892
5283
  }
5284
+ resolveLayoutPolicy() {
5285
+ const explicit = this.data.inputs?.['layoutPolicy'] ??
5286
+ this.data.action?.form?.layoutPolicy ??
5287
+ this.data.action?.layoutPolicy;
5288
+ if (explicit && typeof explicit === 'object') {
5289
+ return explicit;
5290
+ }
5291
+ return null;
5292
+ }
4893
5293
  resolveSubmitLabel() {
4894
5294
  const action = this.data.action ?? {};
4895
5295
  const explicit = stringOrUndefined(action.form?.submitLabel ??
@@ -4962,10 +5362,10 @@ class DynamicFormDialogHostComponent {
4962
5362
  ref
4963
5363
  .afterClosed()
4964
5364
  .pipe(filter((confirmed) => !!confirmed), takeUntilDestroyed(this.destroyRef))
4965
- .subscribe(() => this.dialogRef.close());
5365
+ .subscribe(() => this.dialogRef.close({ type: 'close' }));
4966
5366
  }
4967
5367
  else {
4968
- this.dialogRef.close();
5368
+ this.dialogRef.close({ type: 'close' });
4969
5369
  }
4970
5370
  }
4971
5371
  toggleMaximize(initial = false) {
@@ -5008,7 +5408,7 @@ class DynamicFormDialogHostComponent {
5008
5408
  width: saved?.width ?? this.modal.width,
5009
5409
  height: saved?.height ?? this.modal.height,
5010
5410
  };
5011
- console.debug('[CRUD:Host] ngOnInit', {
5411
+ debugCrudHost('[CRUD:Host] ngOnInit', {
5012
5412
  initialSize: this.initialSize,
5013
5413
  startMaximized: this.modal.startMaximized,
5014
5414
  fullscreenBreakpoint: this.modal.fullscreenBreakpoint,
@@ -5032,10 +5432,10 @@ class DynamicFormDialogHostComponent {
5032
5432
  }
5033
5433
  }
5034
5434
  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: `
5435
+ 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
5436
  <div mat-dialog-title class="dialog-header">
5037
5437
  <h2 id="crudDialogTitle" class="dialog-title">
5038
- {{ data.action?.label || texts.title }}
5438
+ {{ dialogTitle }}
5039
5439
  </h2>
5040
5440
  <span class="spacer"></span>
5041
5441
  @if (modal.canMaximize) {
@@ -5069,11 +5469,14 @@ class DynamicFormDialogHostComponent {
5069
5469
  [resourceId]="resourceId"
5070
5470
  [initialValue]="initialValue"
5071
5471
  [mode]="mode"
5472
+ [config]="formConfig"
5072
5473
  [schemaUrl]="schemaUrl"
5073
5474
  [submitUrl]="submitUrl"
5074
5475
  [submitMethod]="submitMethod"
5075
5476
  [apiEndpointKey]="apiEndpointKey"
5076
5477
  [apiUrlEntry]="apiUrlEntry"
5478
+ [configPersistenceStrategy]="formConfigPersistenceStrategy"
5479
+ [layoutPolicy]="layoutPolicy"
5077
5480
  [presentationModeGlobal]="mode === 'view' ? true : null"
5078
5481
  [backConfig]="backConfig"
5079
5482
  [actions]="formActions"
@@ -5081,7 +5484,7 @@ class DynamicFormDialogHostComponent {
5081
5484
  (formCancel)="onCancel()"
5082
5485
  ></praxis-dynamic-form>
5083
5486
  </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"] }] });
5487
+ `, 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 .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: 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
5488
  }
5086
5489
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicFormDialogHostComponent, decorators: [{
5087
5490
  type: Component,
@@ -5091,13 +5494,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5091
5494
  MatIconModule,
5092
5495
  PraxisIconDirective,
5093
5496
  PraxisDynamicForm
5094
- ], providers: [GenericCrudService], host: {
5497
+ ], encapsulation: ViewEncapsulation.None, providers: [GenericCrudService], host: {
5095
5498
  class: 'praxis-dialog',
5096
5499
  '[attr.data-density]': 'modal.density || "default"',
5500
+ '[attr.data-presentation]': 'presentation',
5501
+ '[class.praxis-drawer]': 'presentation === "drawer"',
5097
5502
  }, template: `
5098
5503
  <div mat-dialog-title class="dialog-header">
5099
5504
  <h2 id="crudDialogTitle" class="dialog-title">
5100
- {{ data.action?.label || texts.title }}
5505
+ {{ dialogTitle }}
5101
5506
  </h2>
5102
5507
  <span class="spacer"></span>
5103
5508
  @if (modal.canMaximize) {
@@ -5131,11 +5536,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5131
5536
  [resourceId]="resourceId"
5132
5537
  [initialValue]="initialValue"
5133
5538
  [mode]="mode"
5539
+ [config]="formConfig"
5134
5540
  [schemaUrl]="schemaUrl"
5135
5541
  [submitUrl]="submitUrl"
5136
5542
  [submitMethod]="submitMethod"
5137
5543
  [apiEndpointKey]="apiEndpointKey"
5138
5544
  [apiUrlEntry]="apiUrlEntry"
5545
+ [configPersistenceStrategy]="formConfigPersistenceStrategy"
5546
+ [layoutPolicy]="layoutPolicy"
5139
5547
  [presentationModeGlobal]="mode === 'view' ? true : null"
5140
5548
  [backConfig]="backConfig"
5141
5549
  [actions]="formActions"
@@ -5143,7 +5551,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5143
5551
  (formCancel)="onCancel()"
5144
5552
  ></praxis-dynamic-form>
5145
5553
  </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"] }]
5554
+ `, 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 .mat-mdc-dialog-surface,.cdk-overlay-pane.pfx-drawer-pane .mdc-dialog__surface{border-radius:0!important}\n"] }]
5147
5555
  }], ctorParameters: () => [{ type: undefined, decorators: [{
5148
5556
  type: Inject,
5149
5557
  args: [MatDialogRef]
@@ -5170,6 +5578,17 @@ function stringOrUndefined(value) {
5170
5578
  const text = String(value ?? '').trim();
5171
5579
  return text || undefined;
5172
5580
  }
5581
+ function sameTitle(left, right) {
5582
+ return normalizeTitle(left) === normalizeTitle(right);
5583
+ }
5584
+ function normalizeTitle(value) {
5585
+ return value
5586
+ .normalize('NFD')
5587
+ .replace(/[\u0300-\u036f]/g, '')
5588
+ .replace(/\s+/g, ' ')
5589
+ .trim()
5590
+ .toLocaleLowerCase('pt-BR');
5591
+ }
5173
5592
  function deriveCreateSubmitLabel(actionLabel) {
5174
5593
  if (!actionLabel) {
5175
5594
  return 'Criar';
@@ -5180,6 +5599,61 @@ function deriveCreateSubmitLabel(actionLabel) {
5180
5599
  }
5181
5600
  return actionLabel;
5182
5601
  }
5602
+ function deriveModeTitle(mode, resourceTitle) {
5603
+ if (!resourceTitle) {
5604
+ return undefined;
5605
+ }
5606
+ const singular = lowerFirstLetter(singularizePtBrResourceTitle(resourceTitle));
5607
+ if (mode === 'create') {
5608
+ return `Adicionar ${singular}`;
5609
+ }
5610
+ if (mode === 'edit') {
5611
+ return `Editar ${singular}`;
5612
+ }
5613
+ return `Visualizar ${singular}`;
5614
+ }
5615
+ function titleFromResourcePath(resourcePath) {
5616
+ const lastSegment = resourcePath?.split('/').filter(Boolean).pop();
5617
+ if (!lastSegment) {
5618
+ return undefined;
5619
+ }
5620
+ return lastSegment
5621
+ .replace(/[-_]+/g, ' ')
5622
+ .replace(/\s+/g, ' ')
5623
+ .trim()
5624
+ .toLocaleLowerCase('pt-BR');
5625
+ }
5626
+ function singularizePtBrResourceTitle(title) {
5627
+ const normalized = title.trim();
5628
+ const lower = normalized.toLocaleLowerCase('pt-BR');
5629
+ if (lower === 'códigos de frequência' || lower === 'codigos de frequencia') {
5630
+ return 'código de frequência';
5631
+ }
5632
+ const [first, ...rest] = normalized.split(/\s+/);
5633
+ const singularFirst = singularizePtBrWord(first);
5634
+ return [singularFirst, ...rest].join(' ');
5635
+ }
5636
+ function singularizePtBrWord(word) {
5637
+ if (/ões$/i.test(word)) {
5638
+ return word.replace(/ões$/i, 'ão');
5639
+ }
5640
+ if (/ais$/i.test(word)) {
5641
+ return word.replace(/ais$/i, 'al');
5642
+ }
5643
+ if (/eis$/i.test(word)) {
5644
+ return word.replace(/eis$/i, 'el');
5645
+ }
5646
+ if (/res$/i.test(word)) {
5647
+ return word.replace(/es$/i, '');
5648
+ }
5649
+ if (/s$/i.test(word) && !/ss$/i.test(word)) {
5650
+ return word.replace(/s$/i, '');
5651
+ }
5652
+ return word;
5653
+ }
5654
+ function lowerFirstLetter(value) {
5655
+ return value.replace(/\p{L}/u, (letter) => letter.toLocaleLowerCase('pt-BR'));
5656
+ }
5183
5657
 
5184
5658
  var dynamicFormDialogHost_component = /*#__PURE__*/Object.freeze({
5185
5659
  __proto__: null,
@@ -5325,11 +5799,31 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
5325
5799
  type: 'PraxisDataQueryContext | null',
5326
5800
  description: 'Contexto semântico de consulta encaminhado para a tabela interna do CRUD. Preferir este contrato para novo authoring remoto.',
5327
5801
  },
5802
+ {
5803
+ name: 'metadata.resource.schemaPath',
5804
+ type: 'string | null',
5805
+ description: 'Template canonico opcional usado apenas para resolver /schemas/filtered quando resource.path precisa ser um alvo operacional concreto.',
5806
+ },
5807
+ {
5808
+ name: 'metadata.resource.title',
5809
+ type: 'string | null',
5810
+ description: 'Titulo documental do recurso usado pelo shell CRUD para derivar titulos contextuais de criacao, edicao e visualizacao quando a action nao informa label.',
5811
+ },
5812
+ {
5813
+ name: 'metadata.resource.formTitle',
5814
+ type: 'string | null',
5815
+ description: 'Titulo documental especifico para formularios do recurso, usado antes de metadata.resource.title no shell modal/drawer.',
5816
+ },
5328
5817
  {
5329
5818
  name: 'metadata.filterCriteria',
5330
5819
  type: 'Record<string, unknown> | null',
5331
5820
  description: 'Bridge declarativa de filtros encaminhada para a tabela interna. Para novo authoring, prefira metadata.queryContext.',
5332
5821
  },
5822
+ {
5823
+ name: 'metadata.form',
5824
+ type: 'FormConfig',
5825
+ 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.',
5826
+ },
5333
5827
  {
5334
5828
  name: 'metadata.actions[].form',
5335
5829
  type: 'CrudActionFormContract',
@@ -5699,6 +6193,7 @@ const CRUD_AI_CAPABILITIES = {
5699
6193
  { path: 'actions[].params[].to', category: 'actions', valueKind: 'enum', allowedValues: ENUMS.paramTarget, description: 'Destino do parametro.' },
5700
6194
  { path: 'actions[].params[].name', category: 'actions', valueKind: 'string', description: 'Nome do parametro no destino.' },
5701
6195
  { path: 'actions[].form.initialValue', category: 'actions', valueKind: 'object', description: 'Seed fixo do formulario injetado em inputs.initialValue antes da abertura.' },
6196
+ { 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
6197
  { path: 'actions[].back', category: 'navigation', valueKind: 'object', description: 'BackConfig por acao.' },
5703
6198
  { path: 'actions[].back.strategy', category: 'navigation', valueKind: 'enum', allowedValues: ENUMS.backStrategy, description: 'Estrategia de retorno da acao (auto, close, navigate).' },
5704
6199
  { path: 'actions[].back.returnTo', category: 'navigation', valueKind: 'string', description: 'Rota ou destino usado quando a estrategia de retorno for navigate.' },
@@ -5736,6 +6231,7 @@ const surfaceConfigureSchema = {
5736
6231
  submitMethod: { enum: ['post', 'put', 'patch', 'delete'] },
5737
6232
  apiEndpointKey: { type: 'string' },
5738
6233
  initialValue: { type: 'object' },
6234
+ layoutPolicy: { type: 'object' },
5739
6235
  },
5740
6236
  },
5741
6237
  params: {
@@ -5854,14 +6350,14 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
5854
6350
  { name: 'afterDelete', type: '{ id: string | number }', description: 'Emitted after delete; CRUD refetches the list.' },
5855
6351
  ],
5856
6352
  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.' },
6353
+ { 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
6354
  { kind: 'listSurface', resolver: 'crud-list-surface', description: 'CRUD-hosted list surface and table delegation boundary.' },
5859
6355
  { kind: 'createSurface', resolver: 'crud-action-by-id:create', description: 'Create action open mode, route/form binding and launcher inputs.' },
5860
6356
  { kind: 'editSurface', resolver: 'crud-action-by-id:edit', description: 'Edit action open mode, route/form binding and launcher inputs.' },
5861
6357
  { kind: 'viewSurface', resolver: 'crud-action-by-id:view', description: 'View action open mode, route/form binding and launcher inputs.' },
5862
6358
  { 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.' },
6359
+ { 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.' },
6360
+ { 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
6361
  { kind: 'permissions', resolver: 'crud-resource-capabilities', description: 'CRUD action availability derived from resource capabilities and action permissions.' },
5866
6362
  { 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
6363
  { kind: 'childOperation', resolver: 'child-authoring-manifest-operation', description: 'Delegated form/table/dialog/settings-panel operation owned by the child component manifest.' },
@@ -5877,13 +6373,13 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
5877
6373
  effects: [{ kind: 'compile-domain-patch', handler: 'crud-resource-bind', handlerContract: {
5878
6374
  reads: ['CrudMetadata.resource', 'api_metadata', 'ResourceDiscoveryService', 'GET /{resource}/capabilities'],
5879
6375
  writes: ['CrudMetadata.resource', 'CrudMetadata.queryContext', 'PraxisCrudComponent.tableCrudContext'],
5880
- identityKeys: ['resourcePath', 'resourceKey'],
6376
+ identityKeys: ['resourcePath', 'resourceKey', 'schemaPath'],
5881
6377
  inputSchema: resourceBindSchema,
5882
6378
  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.',
6379
+ 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
6380
  } }],
5885
6381
  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'],
6382
+ affectedPaths: ['resource.path', 'resource.schemaPath', 'resource.idField', 'resource.endpointKey', 'resource.apiUrlEntry', 'queryContext', 'filterCriteria'],
5887
6383
  submissionImpact: 'affects-remote-binding',
5888
6384
  preconditions: ['crud-metadata-loaded', 'api-metadata-available'],
5889
6385
  },
@@ -6017,14 +6513,14 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
6017
6513
  target: { kind: 'dialogHost', resolver: 'crud-dialog-host-defaults', ambiguityPolicy: 'fail', required: true },
6018
6514
  inputSchema: dialogHostSchema,
6019
6515
  effects: [{ kind: 'compile-domain-patch', handler: 'crud-dialog-host-set', handlerContract: {
6020
- reads: ['CrudMetadata.defaults', 'CrudLauncherService.resolveOpenMode', 'DialogService', 'CRUD_DRAWER_ADAPTER'],
6516
+ reads: ['CrudMetadata.defaults', 'CrudLauncherService.resolveOpenMode', 'DialogService', 'DynamicFormDialogHostComponent', 'CRUD_DRAWER_ADAPTER'],
6021
6517
  writes: ['CrudMetadata.defaults.openMode', 'CrudMetadata.defaults.modal', 'CrudMetadata.defaults.back'],
6022
6518
  identityKeys: ['crudId'],
6023
6519
  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.',
6520
+ failureModes: ['open-mode-unsupported', 'drawer-runtime-unavailable', 'modal-size-invalid', 'back-policy-invalid'],
6521
+ description: 'Configures CRUD-owned route/modal/drawer defaults consumed by the launcher, default dialog-backed drawer runtime and optional host drawer adapter.',
6026
6522
  } }],
6027
- validators: ['open-mode-supported', 'modal-size-valid', 'drawer-adapter-available-when-needed', 'back-policy-valid', 'settings-panel-shell-compatible'],
6523
+ validators: ['open-mode-supported', 'modal-size-valid', 'drawer-runtime-available', 'back-policy-valid', 'settings-panel-shell-compatible'],
6028
6524
  affectedPaths: ['defaults.openMode', 'defaults.modal', 'defaults.back'],
6029
6525
  submissionImpact: 'config-only',
6030
6526
  preconditions: ['crud-metadata-loaded'],
@@ -6100,7 +6596,7 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
6100
6596
  { validatorId: 'permissions-delete-valid', level: 'error', code: 'CRUD_DELETE_PERMISSION_VALID', description: 'Delete permission cannot bypass resource capabilities or confirmation policy.' },
6101
6597
  { validatorId: 'open-mode-supported', level: 'error', code: 'CRUD_OPEN_MODE_SUPPORTED', description: 'Open mode must be route, modal or drawer.' },
6102
6598
  { 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.' },
6599
+ { validatorId: 'drawer-runtime-available', level: 'error', code: 'CRUD_DRAWER_RUNTIME_AVAILABLE', description: 'Drawer open mode uses the default dialog-backed drawer runtime with CRUD-owned chrome, responsive right-panel sizing, Escape/backdrop handling and semantic close/save results; a host drawer adapter is optional only for shell-specific presentation.' },
6104
6600
  { validatorId: 'back-policy-valid', level: 'error', code: 'CRUD_BACK_POLICY_VALID', description: 'Back policy must be valid for route/modal/drawer behavior.' },
6105
6601
  { validatorId: 'settings-panel-shell-compatible', level: 'warning', code: 'CRUD_SETTINGS_PANEL_COMPATIBLE', description: 'Authoring shell must preserve apply/save/reset semantics.' },
6106
6602
  { validatorId: 'action-permission-supported', level: 'error', code: 'CRUD_ACTION_PERMISSION_SUPPORTED', description: 'Action permissions must map to supported resource capabilities.' },