@praxisui/crud 9.0.0-beta.6 → 9.0.0-beta.61

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,5 +1,5 @@
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';
@@ -10,6 +10,7 @@ import { ASYNC_CONFIG_STORAGE, GlobalConfigService, CrudOperationResolutionServi
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,27 @@ function getCrudDrawerAdapterToken() {
68
68
  }
69
69
  const CRUD_DRAWER_ADAPTER = getCrudDrawerAdapterToken();
70
70
 
71
+ function isCrudDebugEnabled$1() {
72
+ try {
73
+ if (globalThis.__PRAXIS_DEBUG_CRUD__) {
74
+ return true;
75
+ }
76
+ if (typeof localStorage !== 'undefined') {
77
+ return localStorage.getItem('praxis.debug.crud') === 'true';
78
+ }
79
+ }
80
+ catch { }
81
+ return false;
82
+ }
83
+ function debugCrudLauncher(message, ...data) {
84
+ if (!isCrudDebugEnabled$1()) {
85
+ return;
86
+ }
87
+ try {
88
+ console.debug(message, ...data);
89
+ }
90
+ catch { }
91
+ }
71
92
  class CrudLauncherService {
72
93
  router = inject(Router);
73
94
  dialog = inject(DialogService);
@@ -85,8 +106,9 @@ class CrudLauncherService {
85
106
  async launch(action, row, metadata, componentKeyId, drawerCallbacks, runtime) {
86
107
  // Carregar overrides de CRUD (se houver) e mesclar em uma cópia local
87
108
  const merged = await this.mergeCrudOverrides(metadata, action, componentKeyId || undefined);
109
+ merged.action = this.normalizeActionForLaunch(merged.action);
88
110
  const mode = this.resolveOpenMode(merged.action, merged.metadata);
89
- console.debug('[CRUD:Launcher] mode=', mode, 'action=', action);
111
+ debugCrudLauncher('[CRUD:Launcher] mode=', mode, 'action=', action);
90
112
  if (mode === 'route') {
91
113
  if (!merged.action.route) {
92
114
  throw new Error(`Route not provided for action ${merged.action.action}`);
@@ -95,22 +117,6 @@ class CrudLauncherService {
95
117
  await this.router.navigateByUrl(url);
96
118
  return { mode };
97
119
  }
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
120
  const actionForLaunch = this.resolveActionForLaunch(merged.action, merged.metadata);
115
121
  if (!actionForLaunch.formId) {
116
122
  throw new Error(`formId not provided for action ${actionForLaunch.action}`);
@@ -123,18 +129,26 @@ class CrudLauncherService {
123
129
  row[idField] !== undefined) {
124
130
  inputs[idField] = row[idField];
125
131
  }
132
+ if (mode === 'drawer' && this.drawerAdapter) {
133
+ await Promise.resolve(this.drawerAdapter.open({
134
+ action: actionForLaunch,
135
+ metadata: merged.metadata,
136
+ inputs,
137
+ onClose: drawerCallbacks?.onClose,
138
+ onResult: drawerCallbacks?.onResult,
139
+ }));
140
+ return { mode };
141
+ }
126
142
  const modalCfg = { ...(merged.metadata.defaults?.modal || {}) };
127
- console.debug('[CRUD:Launcher] opening dialog with:', {
143
+ debugCrudLauncher('[CRUD:Launcher] opening dialog with:', {
128
144
  action: merged.action.action,
129
145
  formId: actionForLaunch.formId,
130
146
  inputs,
131
147
  modalCfg,
132
148
  resourcePath: merged.metadata.resource?.path ?? merged.metadata.table?.resourcePath,
133
149
  });
134
- const panelClasses = ['pfx-dialog-pane', 'pfx-dialog-frosted'];
135
- if (modalCfg.panelClass) {
136
- panelClasses.push(modalCfg.panelClass);
137
- }
150
+ const drawerMode = mode === 'drawer';
151
+ const panelClasses = mergeClassList(['pfx-dialog-pane', drawerMode ? 'pfx-drawer-pane' : 'pfx-dialog-frosted'], modalCfg.panelClass);
138
152
  // Backdrop style presets
139
153
  const backdropClasses = [];
140
154
  const style = modalCfg.backdropStyle;
@@ -147,20 +161,44 @@ class CrudLauncherService {
147
161
  else if (style === 'transparent') {
148
162
  backdropClasses.push('pfx-transparent-backdrop');
149
163
  }
150
- if (modalCfg.backdropClass) {
151
- backdropClasses.push(modalCfg.backdropClass);
152
- }
164
+ const mergedBackdropClasses = mergeClassList(backdropClasses, modalCfg.backdropClass);
153
165
  const ref = await this.dialog.openAsync(() => Promise.resolve().then(function () { return dynamicFormDialogHost_component; }).then((m) => m.DynamicFormDialogHostComponent), {
154
166
  ...modalCfg,
155
167
  panelClass: panelClasses,
156
- backdropClass: backdropClasses,
168
+ backdropClass: mergedBackdropClasses,
157
169
  autoFocus: modalCfg.autoFocus ?? true,
158
170
  restoreFocus: modalCfg.restoreFocus ?? true,
159
- minWidth: '360px',
160
- maxWidth: '95vw',
171
+ minWidth: drawerMode ? (modalCfg.minWidth ?? '0') : '360px',
172
+ width: drawerMode ? (modalCfg.width ?? 'min(560px, 100vw)') : modalCfg.width,
173
+ height: drawerMode ? (modalCfg.height ?? '100dvh') : modalCfg.height,
174
+ maxWidth: drawerMode ? (modalCfg.maxWidth ?? '100vw') : '95vw',
175
+ maxHeight: drawerMode ? (modalCfg.maxHeight ?? '100dvh') : modalCfg.maxHeight,
176
+ position: drawerMode ? (modalCfg.position ?? { right: '0', top: '0' }) : modalCfg.position,
161
177
  ariaLabelledBy: 'crudDialogTitle',
162
- data: { action: actionForLaunch, row, metadata: merged.metadata, inputs },
178
+ data: {
179
+ action: actionForLaunch,
180
+ row,
181
+ metadata: merged.metadata,
182
+ inputs,
183
+ presentation: drawerMode ? 'drawer' : 'modal',
184
+ },
163
185
  });
186
+ if (drawerMode) {
187
+ ref
188
+ .afterClosed()
189
+ .pipe(take(1))
190
+ .subscribe((closedValue) => {
191
+ const result = toCrudDrawerResult(closedValue);
192
+ drawerCallbacks?.onClose?.();
193
+ if (result?.type) {
194
+ drawerCallbacks?.onResult?.(result);
195
+ }
196
+ else {
197
+ drawerCallbacks?.onResult?.({ type: 'close' });
198
+ }
199
+ });
200
+ return { mode, ref };
201
+ }
164
202
  return { mode, ref };
165
203
  }
166
204
  resolveOpenMode(action, metadata) {
@@ -248,6 +286,9 @@ class CrudLauncherService {
248
286
  if (resolved.apiUrlEntry != null) {
249
287
  inputs['apiUrlEntry'] = resolved.apiUrlEntry;
250
288
  }
289
+ if (resolved.layoutPolicy != null) {
290
+ inputs['layoutPolicy'] = resolved.layoutPolicy;
291
+ }
251
292
  if (action.form?.initialValue != null) {
252
293
  inputs['initialValue'] = action.form.initialValue;
253
294
  }
@@ -257,6 +298,7 @@ class CrudLauncherService {
257
298
  return inputs;
258
299
  }
259
300
  resolveActionForLaunch(action, metadata) {
301
+ action = this.normalizeActionForLaunch(action);
260
302
  if (action.formId) {
261
303
  return action;
262
304
  }
@@ -264,29 +306,61 @@ class CrudLauncherService {
264
306
  return action;
265
307
  }
266
308
  const formId = this.buildInferredFormId(action, metadata);
267
- return formId ? { ...action, formId } : action;
309
+ return formId
310
+ ? {
311
+ ...action,
312
+ formId,
313
+ form: {
314
+ ...(action.form || {}),
315
+ layoutPolicy: action.form?.layoutPolicy ?? this.defaultInferredLayoutPolicy(action),
316
+ },
317
+ }
318
+ : action;
319
+ }
320
+ resolveSubmitUrlTemplate(submitUrl, row, metadata) {
321
+ const template = String(submitUrl || '').trim();
322
+ if (!template || !template.includes('{')) {
323
+ return template;
324
+ }
325
+ const idField = String(metadata.resource?.idField ?? 'id');
326
+ const rowId = row?.[idField];
327
+ if (rowId == null) {
328
+ return template;
329
+ }
330
+ const encodedId = encodeURIComponent(String(rowId));
331
+ const idFieldPattern = new RegExp(`\\{${escapeRegExp(idField)}\\}`, 'g');
332
+ return template
333
+ .replace(idFieldPattern, encodedId)
334
+ .replace(/\{id\}/g, encodedId)
335
+ .replace(/\{resourceId\}/g, encodedId);
268
336
  }
269
337
  resolveActionFormContract(action, row, metadata, runtime) {
270
338
  if (this.isExplicitCrudAction(action)) {
271
339
  return {
272
340
  schemaUrl: action.form?.schemaUrl ?? null,
273
- submitUrl: action.form?.submitUrl ?? null,
341
+ submitUrl: action.form?.submitUrl
342
+ ? this.resolveSubmitUrlTemplate(action.form.submitUrl, row, metadata)
343
+ : null,
274
344
  submitMethod: action.form?.submitMethod ?? null,
275
345
  apiEndpointKey: action.form?.apiEndpointKey ?? null,
276
346
  apiUrlEntry: action.form?.apiUrlEntry ?? null,
347
+ layoutPolicy: action.form?.layoutPolicy ?? null,
277
348
  };
278
349
  }
279
350
  if (!this.isCanonicalCrudAction(action.action)) {
280
351
  return {
281
352
  apiEndpointKey: action.form?.apiEndpointKey ?? null,
282
353
  apiUrlEntry: action.form?.apiUrlEntry ?? null,
354
+ layoutPolicy: action.form?.layoutPolicy ?? null,
283
355
  };
284
356
  }
285
357
  const resourcePath = String(metadata.resource?.path ?? metadata.table?.resourcePath ?? '').trim();
358
+ const schemaResourcePath = String(metadata.resource?.schemaPath || '').trim() || null;
286
359
  if (!resourcePath) {
287
360
  return {
288
361
  apiEndpointKey: action.form?.apiEndpointKey ?? null,
289
362
  apiUrlEntry: action.form?.apiUrlEntry ?? null,
363
+ layoutPolicy: action.form?.layoutPolicy ?? null,
290
364
  };
291
365
  }
292
366
  const idField = String(metadata.resource?.idField ?? 'id');
@@ -294,6 +368,7 @@ class CrudLauncherService {
294
368
  const resolved = this.operationResolver.resolve({
295
369
  operation: action.action,
296
370
  resourcePath,
371
+ schemaResourcePath,
297
372
  resourceId: resourceId ?? null,
298
373
  capabilities: runtime?.capabilities ?? null,
299
374
  links: runtime?.links ?? null,
@@ -301,14 +376,15 @@ class CrudLauncherService {
301
376
  endpointKey: action.form?.apiEndpointKey ??
302
377
  metadata.resource?.endpointKey ??
303
378
  undefined,
304
- apiUrlEntry: action.form?.apiUrlEntry ?? null,
379
+ apiUrlEntry: action.form?.apiUrlEntry ?? metadata.resource?.apiUrlEntry ?? null,
305
380
  });
306
381
  return {
307
382
  schemaUrl: resolved?.schemaUrl ?? null,
308
383
  submitUrl: resolved?.submitUrl ?? null,
309
384
  submitMethod: resolved?.submitMethod ?? null,
310
385
  apiEndpointKey: action.form?.apiEndpointKey ?? metadata.resource?.endpointKey ?? null,
311
- apiUrlEntry: action.form?.apiUrlEntry ?? null,
386
+ apiUrlEntry: action.form?.apiUrlEntry ?? metadata.resource?.apiUrlEntry ?? null,
387
+ layoutPolicy: action.form?.layoutPolicy ?? null,
312
388
  };
313
389
  }
314
390
  resolveRuntimeContract(action, row, metadata, runtime) {
@@ -327,9 +403,20 @@ class CrudLauncherService {
327
403
  return `${sanitizedResource || 'crud'}-${String(action.action || '').trim().toLowerCase()}`;
328
404
  }
329
405
  isCanonicalCrudAction(actionName) {
330
- const normalized = String(actionName || '').trim().toLowerCase();
406
+ const normalized = this.normalizeCrudOperationName(actionName);
331
407
  return normalized === 'create' || normalized === 'view' || normalized === 'edit' || normalized === 'delete';
332
408
  }
409
+ normalizeActionForLaunch(action) {
410
+ const normalized = this.normalizeCrudOperationName(action.action);
411
+ return normalized === action.action ? action : { ...action, action: normalized };
412
+ }
413
+ normalizeCrudOperationName(actionName) {
414
+ const normalized = String(actionName || '').trim().toLowerCase();
415
+ if (['add', 'novo', 'new', 'incluir', 'inserir'].includes(normalized)) {
416
+ return 'create';
417
+ }
418
+ return normalized;
419
+ }
333
420
  isExplicitCrudAction(action) {
334
421
  if (action.mode === 'explicit') {
335
422
  return true;
@@ -338,6 +425,19 @@ class CrudLauncherService {
338
425
  String(action.form?.submitUrl || '').trim() ||
339
426
  String(action.form?.submitMethod || '').trim());
340
427
  }
428
+ defaultInferredLayoutPolicy(action) {
429
+ const operation = this.normalizeCrudOperationName(action.action);
430
+ if (operation !== 'create' && operation !== 'edit') {
431
+ return null;
432
+ }
433
+ return {
434
+ source: 'schema',
435
+ intent: 'command',
436
+ preset: 'groupedCommand',
437
+ persistence: 'transient',
438
+ schemaType: 'request',
439
+ };
440
+ }
341
441
  async mergeCrudOverrides(metadata, action, componentKeyId) {
342
442
  try {
343
443
  if (!componentKeyId)
@@ -392,6 +492,33 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
392
492
  type: Injectable,
393
493
  args: [{ providedIn: 'root' }]
394
494
  }] });
495
+ function mergeClassList(base, extra) {
496
+ if (!extra) {
497
+ return base;
498
+ }
499
+ if (typeof extra === 'string') {
500
+ return [...base, extra];
501
+ }
502
+ if (Array.isArray(extra)) {
503
+ return [...base, ...extra];
504
+ }
505
+ return [
506
+ ...base,
507
+ ...Object.keys(extra).filter((className) => !!extra[className]),
508
+ ];
509
+ }
510
+ function escapeRegExp(value) {
511
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
512
+ }
513
+ function toCrudDrawerResult(value) {
514
+ if (!value || typeof value !== 'object') {
515
+ return undefined;
516
+ }
517
+ const result = value;
518
+ return result.type === 'save' || result.type === 'delete' || result.type === 'close'
519
+ ? result
520
+ : undefined;
521
+ }
395
522
 
396
523
  const DOCUMENT_KIND = 'praxis.crud.editor';
397
524
  const DOCUMENT_VERSION = 1;
@@ -510,10 +637,15 @@ function normalizeCrudMetadata(metadata) {
510
637
  if (base.resource && typeof base.resource === 'object') {
511
638
  normalized.resource = stripUndefinedShallow({
512
639
  path: trimString(base.resource.path) || undefined,
640
+ schemaPath: trimString(base.resource.schemaPath) || undefined,
513
641
  idField: typeof base.resource.idField === 'string'
514
642
  ? trimString(base.resource.idField) || undefined
515
643
  : base.resource.idField,
644
+ title: trimString(base.resource.title) || undefined,
645
+ label: trimString(base.resource.label) || undefined,
646
+ formTitle: trimString(base.resource.formTitle) || undefined,
516
647
  endpointKey: trimString(base.resource.endpointKey) || undefined,
648
+ apiUrlEntry: base.resource.apiUrlEntry || undefined,
517
649
  });
518
650
  }
519
651
  if (base.defaults && typeof base.defaults === 'object') {
@@ -564,6 +696,7 @@ function normalizeCrudAction(action) {
564
696
  apiEndpointKey: trimString(action.form.apiEndpointKey) || undefined,
565
697
  apiUrlEntry: action.form.apiUrlEntry || undefined,
566
698
  initialValue: normalizeInitialValue(action.form.initialValue),
699
+ layoutPolicy: action.form.layoutPolicy ?? undefined,
567
700
  }
568
701
  : undefined,
569
702
  });
@@ -3498,6 +3631,7 @@ class PraxisCrudComponent {
3498
3631
  resourceDiscoveryInstance;
3499
3632
  actionOpenAdapterInstance;
3500
3633
  surfaceOpenAdapterInstance;
3634
+ materializedRefreshSequence = 0;
3501
3635
  global = (() => {
3502
3636
  try {
3503
3637
  return inject(GlobalConfigService);
@@ -3527,6 +3661,7 @@ class PraxisCrudComponent {
3527
3661
  collectionCapabilitiesResolvedHref = null;
3528
3662
  collectionCapabilitiesRequestSeq = 0;
3529
3663
  currentAuthoringDocument;
3664
+ selectedRow = null;
3530
3665
  getResourceDiscovery() {
3531
3666
  const assigned = this.resourceDiscovery;
3532
3667
  return assigned ?? (this.resourceDiscoveryInstance ??= this.injector.get(ResourceDiscoveryService));
@@ -3553,15 +3688,32 @@ class PraxisCrudComponent {
3553
3688
  catch { }
3554
3689
  }
3555
3690
  onTableRowClick(event) {
3691
+ const row = this.extractRowFromEvent(event);
3692
+ if (row) {
3693
+ this.selectedRow = row;
3694
+ }
3556
3695
  this.rowClick.emit(event);
3557
3696
  }
3558
3697
  onTableSelectionChange(event) {
3698
+ this.selectedRow = this.extractSelectedRowFromEvent(event);
3559
3699
  this.selectionChange.emit(event);
3560
3700
  }
3701
+ async onBulkAction(event) {
3702
+ const action = String(event?.action || '').trim();
3703
+ if (!action) {
3704
+ return;
3705
+ }
3706
+ const row = this.extractSelectedRowFromBulkAction(event);
3707
+ if (row) {
3708
+ this.selectedRow = row;
3709
+ }
3710
+ await this.onAction(action, row ?? undefined, event);
3711
+ }
3561
3712
  ngOnChanges(changes) {
3562
3713
  if (!changes['metadata'] && !changes['context']) {
3563
3714
  return;
3564
3715
  }
3716
+ this.materializedRefreshSequence += 1;
3565
3717
  try {
3566
3718
  const parsed = typeof this.metadata === 'string'
3567
3719
  ? JSON.parse(this.metadata)
@@ -3595,33 +3747,41 @@ class PraxisCrudComponent {
3595
3747
  async onAction(action, row, runtimeEvent) {
3596
3748
  try {
3597
3749
  document.activeElement?.blur();
3598
- let actionMeta = this.resolvedMetadata.actions?.find((candidate) => candidate.action === action);
3750
+ const normalizedAction = this.normalizeCrudActionName(action);
3751
+ const contextualRow = row ?? this.resolveSelectedRowForAction(normalizedAction);
3752
+ let actionMeta = this.resolvedMetadata.actions?.find((candidate) => this.normalizeCrudActionName(candidate.action) === normalizedAction);
3753
+ if (!actionMeta && normalizedAction !== action) {
3754
+ actionMeta = this.resolvedMetadata.actions?.find((candidate) => candidate.action === action);
3755
+ }
3599
3756
  if (!actionMeta) {
3600
- const ctxAction = this.tableCrudContext?.actions?.find((candidate) => candidate.action === action);
3757
+ const ctxAction = this.tableCrudContext?.actions?.find((candidate) => this.normalizeCrudActionName(candidate.action) === normalizedAction);
3601
3758
  if (ctxAction) {
3602
3759
  actionMeta = {
3603
- action: ctxAction.action,
3760
+ action: this.normalizeCrudActionName(ctxAction.action),
3604
3761
  openMode: ctxAction.openMode,
3605
3762
  formId: ctxAction.formId,
3606
3763
  route: ctxAction.route,
3607
3764
  };
3608
3765
  }
3609
3766
  else {
3610
- actionMeta = { action };
3767
+ actionMeta = { action: normalizedAction };
3611
3768
  }
3612
3769
  }
3613
- const effectiveAction = (actionMeta || { action });
3770
+ const effectiveAction = {
3771
+ ...(actionMeta || { action: normalizedAction }),
3772
+ action: this.normalizeCrudActionName(actionMeta?.action ?? normalizedAction),
3773
+ };
3614
3774
  if (!this.hasExplicitOpenBinding(effectiveAction)) {
3615
- const openedByDiscovery = await this.tryOpenDiscoveredCrudSurface(action, row, runtimeEvent);
3775
+ const openedByDiscovery = await this.tryOpenDiscoveredCrudSurface(effectiveAction.action, contextualRow, runtimeEvent);
3616
3776
  if (openedByDiscovery) {
3617
3777
  return;
3618
3778
  }
3619
- const handledByWorkflowAction = await this.tryOpenDiscoveredWorkflowAction(action, row, runtimeEvent);
3779
+ const handledByWorkflowAction = await this.tryOpenDiscoveredWorkflowAction(effectiveAction.action, contextualRow, runtimeEvent);
3620
3780
  if (handledByWorkflowAction) {
3621
3781
  return;
3622
3782
  }
3623
3783
  }
3624
- const handledByDelete = await this.tryHandleCanonicalDeleteAction(effectiveAction, row);
3784
+ const handledByDelete = await this.tryHandleCanonicalDeleteAction(effectiveAction, contextualRow);
3625
3785
  if (handledByDelete) {
3626
3786
  return;
3627
3787
  }
@@ -3632,7 +3792,7 @@ class PraxisCrudComponent {
3632
3792
  drawerCloseEmitted = true;
3633
3793
  this.afterClose.emit();
3634
3794
  };
3635
- const { mode, ref } = await this.launcher.launch(effectiveAction, row, this.resolvedMetadata, this.componentKeyId(), {
3795
+ const { mode, ref } = await this.launcher.launch(effectiveAction, contextualRow, this.resolvedMetadata, this.componentKeyId(), {
3636
3796
  onClose: () => emitDrawerClose(),
3637
3797
  onResult: (result) => {
3638
3798
  emitDrawerClose();
@@ -3653,9 +3813,12 @@ class PraxisCrudComponent {
3653
3813
  capabilities: effectiveAction.action === 'create'
3654
3814
  ? this.collectionCapabilities
3655
3815
  : this.collectionCapabilities,
3656
- links: this.resolveCrudRuntimeLinks(effectiveAction.action, row),
3816
+ links: this.resolveCrudRuntimeLinks(effectiveAction.action, contextualRow),
3657
3817
  });
3658
3818
  this.afterOpen.emit({ mode, action: effectiveAction.action });
3819
+ if (mode === 'drawer') {
3820
+ return;
3821
+ }
3659
3822
  if (!ref) {
3660
3823
  return;
3661
3824
  }
@@ -3842,8 +4005,77 @@ class PraxisCrudComponent {
3842
4005
  return true;
3843
4006
  }
3844
4007
  refreshTable() {
4008
+ if (this.tryRefreshMaterializedLocalData()) {
4009
+ return;
4010
+ }
3845
4011
  this.table.refetch();
3846
4012
  }
4013
+ tryRefreshMaterializedLocalData() {
4014
+ const readUrl = this.resolveMaterializedReadUrl();
4015
+ if (!readUrl || this.resolveResourcePath(this.resolvedMetadata)) {
4016
+ return false;
4017
+ }
4018
+ const refreshSequence = ++this.materializedRefreshSequence;
4019
+ void firstValueFrom(this.getResourceDiscovery().fetchJson(readUrl, {
4020
+ endpointKey: this.resolvedMetadata.resource?.endpointKey,
4021
+ apiUrlEntry: this.resolvedMetadata.resource?.apiUrlEntry,
4022
+ }))
4023
+ .then((response) => {
4024
+ if (!this.isCurrentMaterializedRefresh(refreshSequence, readUrl)) {
4025
+ return;
4026
+ }
4027
+ const data = this.extractCollectionData(this.unwrapRestData(response));
4028
+ if (!data) {
4029
+ this.error.emit({
4030
+ code: 'CRUD_MATERIALIZED_REFRESH_INVALID_COLLECTION',
4031
+ readUrl,
4032
+ });
4033
+ return;
4034
+ }
4035
+ this.resolvedMetadata = {
4036
+ ...this.resolvedMetadata,
4037
+ data,
4038
+ };
4039
+ this.applyResolvedCrudState(this.resolvedMetadata);
4040
+ this.cdr.markForCheck();
4041
+ })
4042
+ .catch((error) => this.error.emit(error));
4043
+ return true;
4044
+ }
4045
+ isCurrentMaterializedRefresh(refreshSequence, readUrl) {
4046
+ return (refreshSequence === this.materializedRefreshSequence &&
4047
+ this.resolveMaterializedReadUrl() === readUrl &&
4048
+ this.resolveResourcePath(this.resolvedMetadata).length === 0);
4049
+ }
4050
+ resolveMaterializedReadUrl() {
4051
+ const materialization = this.context?.['materialization'];
4052
+ return String(materialization?.['readUrl'] || '').trim();
4053
+ }
4054
+ unwrapRestData(value) {
4055
+ if (value && typeof value === 'object' && !Array.isArray(value) && 'data' in value) {
4056
+ return value.data;
4057
+ }
4058
+ return value;
4059
+ }
4060
+ extractCollectionData(data) {
4061
+ if (Array.isArray(data)) {
4062
+ return data;
4063
+ }
4064
+ if (!data || typeof data !== 'object') {
4065
+ return null;
4066
+ }
4067
+ const record = data;
4068
+ if (Array.isArray(record['content'])) {
4069
+ return record['content'];
4070
+ }
4071
+ if (Array.isArray(record['items'])) {
4072
+ return record['items'];
4073
+ }
4074
+ if (Array.isArray(record['data'])) {
4075
+ return record['data'];
4076
+ }
4077
+ return null;
4078
+ }
3847
4079
  emitTableRuntimeConfigSnapshot() {
3848
4080
  const snapshot = this.getCurrentTableConfigSnapshot();
3849
4081
  if (!snapshot)
@@ -3901,11 +4133,47 @@ class PraxisCrudComponent {
3901
4133
  }
3902
4134
  }
3903
4135
  resolveQueryContext(meta) {
4136
+ if (Array.isArray(meta?.data) && this.resolveResourcePath(meta).length === 0) {
4137
+ return null;
4138
+ }
3904
4139
  return this.isRecord(meta?.queryContext) ? meta.queryContext : null;
3905
4140
  }
3906
4141
  resolveFilterCriteria(meta) {
3907
4142
  return this.isRecord(meta?.filterCriteria) ? { ...meta.filterCriteria } : {};
3908
4143
  }
4144
+ extractRowFromEvent(event) {
4145
+ if (!this.isRecord(event)) {
4146
+ return null;
4147
+ }
4148
+ const row = event['row'];
4149
+ return this.isRecord(row) ? row : null;
4150
+ }
4151
+ extractSelectedRowFromEvent(event) {
4152
+ if (!this.isRecord(event)) {
4153
+ return null;
4154
+ }
4155
+ const selection = event['selection'];
4156
+ if (Array.isArray(selection)) {
4157
+ const first = selection[0];
4158
+ return this.isRecord(first) ? first : null;
4159
+ }
4160
+ const row = event['row'];
4161
+ if (this.isRecord(row)) {
4162
+ return row;
4163
+ }
4164
+ return null;
4165
+ }
4166
+ extractSelectedRowFromBulkAction(event) {
4167
+ const first = Array.isArray(event?.rows) ? event.rows[0] : null;
4168
+ return this.isRecord(first) ? first : null;
4169
+ }
4170
+ resolveSelectedRowForAction(action) {
4171
+ const normalized = String(action || '').trim().toLowerCase();
4172
+ if (!normalized || normalized === 'create' || normalized === 'add' || normalized === 'new') {
4173
+ return undefined;
4174
+ }
4175
+ return this.selectedRow ?? undefined;
4176
+ }
3909
4177
  async tryOpenDiscoveredCrudSurface(action, row, runtimeEvent) {
3910
4178
  const normalizedAction = String(action || '').trim().toLowerCase();
3911
4179
  if (!this.surfaceService) {
@@ -3999,7 +4267,7 @@ class PraxisCrudComponent {
3999
4267
  const providedAction = this.resolveProvidedWorkflowAction(normalizedAction, runtimeEvent?.actionConfig);
4000
4268
  const catalog = providedAction ? null : await this.resolveDiscoveredActionCatalog(row);
4001
4269
  const discoveredAction = providedAction || this.selectDiscoveredWorkflowAction(normalizedAction, catalog?.actions || []);
4002
- const resourcePath = String(this.resolveResourcePath(this.resolvedMetadata) || catalog?.resourcePath || '').trim();
4270
+ const resourcePath = String(catalog?.resourcePath || this.resolveResourcePath(this.resolvedMetadata) || '').trim();
4003
4271
  if (!discoveredAction || !resourcePath) {
4004
4272
  return false;
4005
4273
  }
@@ -4222,12 +4490,19 @@ class PraxisCrudComponent {
4222
4490
  }
4223
4491
  return cfg.toolbar;
4224
4492
  };
4225
- const hasToolbarAdd = (cfg.toolbar?.actions || []).some((action) => this.isAddLike(action));
4493
+ const hasToolbarAdd = (cfg.toolbar?.actions || []).some((action) => this.shouldCanonicalizeToolbarCreateAction(action));
4226
4494
  const addAction = this.resolveCreateToolbarAction(meta, capabilities);
4227
4495
  if (addAction) {
4228
4496
  if (hasToolbarAdd) {
4229
4497
  const toolbar = ensureToolbar();
4230
4498
  toolbar.visible = true;
4499
+ toolbar.actions = (toolbar.actions || []).map((action) => this.shouldCanonicalizeToolbarCreateAction(action)
4500
+ ? {
4501
+ ...action,
4502
+ action: 'create',
4503
+ }
4504
+ : action);
4505
+ changed = true;
4231
4506
  }
4232
4507
  else {
4233
4508
  const toolbar = ensureToolbar();
@@ -4362,6 +4637,28 @@ class PraxisCrudComponent {
4362
4637
  }
4363
4638
  return false;
4364
4639
  }
4640
+ normalizeCrudActionName(action) {
4641
+ const raw = String(action || '').trim();
4642
+ const normalized = raw.toLowerCase();
4643
+ return this.isCreateActionAliasName(normalized) ? 'create' : raw;
4644
+ }
4645
+ shouldCanonicalizeToolbarCreateAction(action) {
4646
+ if (!action)
4647
+ return false;
4648
+ const normalize = (value) => String(value || '').trim().toLowerCase();
4649
+ const explicitId = normalize(action.action || action.id || action.code || action.key || action.name || action.type);
4650
+ if (this.isCreateActionAliasName(explicitId))
4651
+ return true;
4652
+ if (explicitId)
4653
+ return false;
4654
+ const icon = normalize(action.icon);
4655
+ const label = normalize(action.label);
4656
+ return ((icon === 'add' || icon === 'add_circle' || icon === 'add_box') &&
4657
+ (label === 'adicionar' || label === 'novo' || label === 'criar' || label === 'incluir'));
4658
+ }
4659
+ isCreateActionAliasName(actionName) {
4660
+ return ['create', 'add', 'novo', 'new', 'incluir', 'inserir'].includes(actionName);
4661
+ }
4365
4662
  buildTableCrudContext(meta, capabilities) {
4366
4663
  if (!meta)
4367
4664
  return undefined;
@@ -4642,6 +4939,7 @@ class PraxisCrudComponent {
4642
4939
  (selectionChange)="onTableSelectionChange($event)"
4643
4940
  (rowAction)="onAction($event.action, $event.row, $event)"
4644
4941
  (toolbarAction)="onAction($event.action)"
4942
+ (bulkAction)="onBulkAction($event)"
4645
4943
  (collectionLinksChange)="onCollectionLinksChange($event)"
4646
4944
  (reset)="onResetPreferences()"
4647
4945
  (metadataChange)="onTableMetadataChange()"
@@ -4657,7 +4955,7 @@ class PraxisCrudComponent {
4657
4955
  />
4658
4956
  }
4659
4957
  }
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"] }] });
4958
+ `, 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
4959
  }
4662
4960
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisCrudComponent, decorators: [{
4663
4961
  type: Component,
@@ -4679,6 +4977,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
4679
4977
  (selectionChange)="onTableSelectionChange($event)"
4680
4978
  (rowAction)="onAction($event.action, $event.row, $event)"
4681
4979
  (toolbarAction)="onAction($event.action)"
4980
+ (bulkAction)="onBulkAction($event)"
4682
4981
  (collectionLinksChange)="onCollectionLinksChange($event)"
4683
4982
  (reset)="onResetPreferences()"
4684
4983
  (metadataChange)="onTableMetadataChange()"
@@ -4734,6 +5033,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
4734
5033
  args: [PraxisTable]
4735
5034
  }] } });
4736
5035
 
5036
+ function isCrudDebugEnabled() {
5037
+ try {
5038
+ if (globalThis.__PRAXIS_DEBUG_CRUD__) {
5039
+ return true;
5040
+ }
5041
+ if (typeof localStorage !== 'undefined') {
5042
+ return localStorage.getItem('praxis.debug.crud') === 'true';
5043
+ }
5044
+ }
5045
+ catch { }
5046
+ return false;
5047
+ }
5048
+ function debugCrudHost(message, data) {
5049
+ if (!isCrudDebugEnabled()) {
5050
+ return;
5051
+ }
5052
+ try {
5053
+ console.debug(message, data);
5054
+ }
5055
+ catch { }
5056
+ }
4737
5057
  class DynamicFormDialogHostComponent {
4738
5058
  dialogRef;
4739
5059
  data;
@@ -4742,6 +5062,7 @@ class DynamicFormDialogHostComponent {
4742
5062
  configStorage;
4743
5063
  formComp;
4744
5064
  modal = {};
5065
+ presentation = 'modal';
4745
5066
  maximized = false;
4746
5067
  initialSize = {};
4747
5068
  rememberState = false;
@@ -4756,7 +5077,10 @@ class DynamicFormDialogHostComponent {
4756
5077
  submitMethod;
4757
5078
  apiEndpointKey;
4758
5079
  apiUrlEntry;
5080
+ layoutPolicy;
5081
+ formConfig = {};
4759
5082
  formActions;
5083
+ formConfigPersistenceStrategy = 'input-first';
4760
5084
  mode = 'create';
4761
5085
  backConfig;
4762
5086
  idField = 'id';
@@ -4778,6 +5102,7 @@ class DynamicFormDialogHostComponent {
4778
5102
  this.crud = crud;
4779
5103
  this.configStorage = configStorage;
4780
5104
  this.dialogRef.disableClose = true;
5105
+ this.presentation = this.data.presentation === 'drawer' ? 'drawer' : 'modal';
4781
5106
  // i18n
4782
5107
  this.texts = {
4783
5108
  ...this.texts,
@@ -4812,12 +5137,14 @@ class DynamicFormDialogHostComponent {
4812
5137
  this.apiUrlEntry = this.data.inputs?.['apiUrlEntry'] ?? null;
4813
5138
  const act = this.data.action?.action;
4814
5139
  this.mode = act === 'edit' ? 'edit' : act === 'view' ? 'view' : 'create';
5140
+ this.formConfig = this.resolveFormConfig();
5141
+ this.layoutPolicy = this.resolveLayoutPolicy();
4815
5142
  this.formActions = this.resolveFormActions();
4816
5143
  // Back config: defaults from metadata/action, overridden by saved per-form config
4817
5144
  const defaults = (this.data.action?.back || this.data.metadata?.defaults?.back) || {};
4818
5145
  this.backDefaults = defaults;
4819
5146
  this.backConfig = { ...defaults };
4820
- console.debug('[CRUD:Host] constructed', {
5147
+ debugCrudHost('[CRUD:Host] constructed', {
4821
5148
  action: this.data?.action,
4822
5149
  resourcePath: this.resourcePath,
4823
5150
  resourceId: this.resourceId,
@@ -4857,6 +5184,7 @@ class DynamicFormDialogHostComponent {
4857
5184
  'apiEndpointKey',
4858
5185
  'apiUrlEntry',
4859
5186
  'initialValue',
5187
+ 'layoutPolicy',
4860
5188
  ]);
4861
5189
  const explicit = inputs['initialValue'] && typeof inputs['initialValue'] === 'object'
4862
5190
  ? { ...inputs['initialValue'] }
@@ -4869,10 +5197,50 @@ class DynamicFormDialogHostComponent {
4869
5197
  }
4870
5198
  return Object.keys(explicit).length ? explicit : null;
4871
5199
  }
5200
+ resolveFormConfig() {
5201
+ const config = this.data.metadata?.form;
5202
+ return config && typeof config === 'object' ? config : {};
5203
+ }
5204
+ get dialogTitle() {
5205
+ const actionLabel = this.resolveActionDialogTitle();
5206
+ return actionLabel ||
5207
+ stringOrUndefined(this.formConfig['title']) ||
5208
+ deriveModeTitle(this.mode, this.resolveResourceTitle()) ||
5209
+ this.texts.title;
5210
+ }
5211
+ resolveActionDialogTitle() {
5212
+ const actionLabel = stringOrUndefined(this.data.action?.label);
5213
+ if (!actionLabel) {
5214
+ return undefined;
5215
+ }
5216
+ const resource = this.data.metadata?.resource ?? {};
5217
+ const table = this.data.metadata?.table ?? {};
5218
+ const businessTitle = stringOrUndefined(resource.formTitle ??
5219
+ resource.title ??
5220
+ resource.label ??
5221
+ table.title ??
5222
+ table.label);
5223
+ const routeFallbackTitle = deriveModeTitle(this.mode, titleFromResourcePath(this.resourcePath));
5224
+ return businessTitle && routeFallbackTitle && sameTitle(actionLabel, routeFallbackTitle)
5225
+ ? undefined
5226
+ : actionLabel;
5227
+ }
5228
+ resolveResourceTitle() {
5229
+ const resource = this.data.metadata?.resource ?? {};
5230
+ const table = this.data.metadata?.table ?? {};
5231
+ return stringOrUndefined(resource.formTitle ??
5232
+ resource.title ??
5233
+ resource.label ??
5234
+ table.title ??
5235
+ table.label) || titleFromResourcePath(this.resourcePath);
5236
+ }
4872
5237
  resolveFormActions() {
4873
5238
  if (this.mode === 'view') {
4874
5239
  return undefined;
4875
5240
  }
5241
+ if (this.formConfig?.actions) {
5242
+ return undefined;
5243
+ }
4876
5244
  const submitLabel = this.resolveSubmitLabel();
4877
5245
  if (!submitLabel) {
4878
5246
  return undefined;
@@ -4890,6 +5258,15 @@ class DynamicFormDialogHostComponent {
4890
5258
  },
4891
5259
  };
4892
5260
  }
5261
+ resolveLayoutPolicy() {
5262
+ const explicit = this.data.inputs?.['layoutPolicy'] ??
5263
+ this.data.action?.form?.layoutPolicy ??
5264
+ this.data.action?.layoutPolicy;
5265
+ if (explicit && typeof explicit === 'object') {
5266
+ return explicit;
5267
+ }
5268
+ return null;
5269
+ }
4893
5270
  resolveSubmitLabel() {
4894
5271
  const action = this.data.action ?? {};
4895
5272
  const explicit = stringOrUndefined(action.form?.submitLabel ??
@@ -4962,10 +5339,10 @@ class DynamicFormDialogHostComponent {
4962
5339
  ref
4963
5340
  .afterClosed()
4964
5341
  .pipe(filter((confirmed) => !!confirmed), takeUntilDestroyed(this.destroyRef))
4965
- .subscribe(() => this.dialogRef.close());
5342
+ .subscribe(() => this.dialogRef.close({ type: 'close' }));
4966
5343
  }
4967
5344
  else {
4968
- this.dialogRef.close();
5345
+ this.dialogRef.close({ type: 'close' });
4969
5346
  }
4970
5347
  }
4971
5348
  toggleMaximize(initial = false) {
@@ -5008,7 +5385,7 @@ class DynamicFormDialogHostComponent {
5008
5385
  width: saved?.width ?? this.modal.width,
5009
5386
  height: saved?.height ?? this.modal.height,
5010
5387
  };
5011
- console.debug('[CRUD:Host] ngOnInit', {
5388
+ debugCrudHost('[CRUD:Host] ngOnInit', {
5012
5389
  initialSize: this.initialSize,
5013
5390
  startMaximized: this.modal.startMaximized,
5014
5391
  fullscreenBreakpoint: this.modal.fullscreenBreakpoint,
@@ -5032,10 +5409,10 @@ class DynamicFormDialogHostComponent {
5032
5409
  }
5033
5410
  }
5034
5411
  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: `
5412
+ 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
5413
  <div mat-dialog-title class="dialog-header">
5037
5414
  <h2 id="crudDialogTitle" class="dialog-title">
5038
- {{ data.action?.label || texts.title }}
5415
+ {{ dialogTitle }}
5039
5416
  </h2>
5040
5417
  <span class="spacer"></span>
5041
5418
  @if (modal.canMaximize) {
@@ -5069,11 +5446,14 @@ class DynamicFormDialogHostComponent {
5069
5446
  [resourceId]="resourceId"
5070
5447
  [initialValue]="initialValue"
5071
5448
  [mode]="mode"
5449
+ [config]="formConfig"
5072
5450
  [schemaUrl]="schemaUrl"
5073
5451
  [submitUrl]="submitUrl"
5074
5452
  [submitMethod]="submitMethod"
5075
5453
  [apiEndpointKey]="apiEndpointKey"
5076
5454
  [apiUrlEntry]="apiUrlEntry"
5455
+ [configPersistenceStrategy]="formConfigPersistenceStrategy"
5456
+ [layoutPolicy]="layoutPolicy"
5077
5457
  [presentationModeGlobal]="mode === 'view' ? true : null"
5078
5458
  [backConfig]="backConfig"
5079
5459
  [actions]="formActions"
@@ -5081,7 +5461,7 @@ class DynamicFormDialogHostComponent {
5081
5461
  (formCancel)="onCancel()"
5082
5462
  ></praxis-dynamic-form>
5083
5463
  </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"] }] });
5464
+ `, 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
5465
  }
5086
5466
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicFormDialogHostComponent, decorators: [{
5087
5467
  type: Component,
@@ -5091,13 +5471,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5091
5471
  MatIconModule,
5092
5472
  PraxisIconDirective,
5093
5473
  PraxisDynamicForm
5094
- ], providers: [GenericCrudService], host: {
5474
+ ], encapsulation: ViewEncapsulation.None, providers: [GenericCrudService], host: {
5095
5475
  class: 'praxis-dialog',
5096
5476
  '[attr.data-density]': 'modal.density || "default"',
5477
+ '[attr.data-presentation]': 'presentation',
5478
+ '[class.praxis-drawer]': 'presentation === "drawer"',
5097
5479
  }, template: `
5098
5480
  <div mat-dialog-title class="dialog-header">
5099
5481
  <h2 id="crudDialogTitle" class="dialog-title">
5100
- {{ data.action?.label || texts.title }}
5482
+ {{ dialogTitle }}
5101
5483
  </h2>
5102
5484
  <span class="spacer"></span>
5103
5485
  @if (modal.canMaximize) {
@@ -5131,11 +5513,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5131
5513
  [resourceId]="resourceId"
5132
5514
  [initialValue]="initialValue"
5133
5515
  [mode]="mode"
5516
+ [config]="formConfig"
5134
5517
  [schemaUrl]="schemaUrl"
5135
5518
  [submitUrl]="submitUrl"
5136
5519
  [submitMethod]="submitMethod"
5137
5520
  [apiEndpointKey]="apiEndpointKey"
5138
5521
  [apiUrlEntry]="apiUrlEntry"
5522
+ [configPersistenceStrategy]="formConfigPersistenceStrategy"
5523
+ [layoutPolicy]="layoutPolicy"
5139
5524
  [presentationModeGlobal]="mode === 'view' ? true : null"
5140
5525
  [backConfig]="backConfig"
5141
5526
  [actions]="formActions"
@@ -5143,7 +5528,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5143
5528
  (formCancel)="onCancel()"
5144
5529
  ></praxis-dynamic-form>
5145
5530
  </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"] }]
5531
+ `, 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
5532
  }], ctorParameters: () => [{ type: undefined, decorators: [{
5148
5533
  type: Inject,
5149
5534
  args: [MatDialogRef]
@@ -5170,6 +5555,17 @@ function stringOrUndefined(value) {
5170
5555
  const text = String(value ?? '').trim();
5171
5556
  return text || undefined;
5172
5557
  }
5558
+ function sameTitle(left, right) {
5559
+ return normalizeTitle(left) === normalizeTitle(right);
5560
+ }
5561
+ function normalizeTitle(value) {
5562
+ return value
5563
+ .normalize('NFD')
5564
+ .replace(/[\u0300-\u036f]/g, '')
5565
+ .replace(/\s+/g, ' ')
5566
+ .trim()
5567
+ .toLocaleLowerCase('pt-BR');
5568
+ }
5173
5569
  function deriveCreateSubmitLabel(actionLabel) {
5174
5570
  if (!actionLabel) {
5175
5571
  return 'Criar';
@@ -5180,6 +5576,61 @@ function deriveCreateSubmitLabel(actionLabel) {
5180
5576
  }
5181
5577
  return actionLabel;
5182
5578
  }
5579
+ function deriveModeTitle(mode, resourceTitle) {
5580
+ if (!resourceTitle) {
5581
+ return undefined;
5582
+ }
5583
+ const singular = lowerFirstLetter(singularizePtBrResourceTitle(resourceTitle));
5584
+ if (mode === 'create') {
5585
+ return `Adicionar ${singular}`;
5586
+ }
5587
+ if (mode === 'edit') {
5588
+ return `Editar ${singular}`;
5589
+ }
5590
+ return `Visualizar ${singular}`;
5591
+ }
5592
+ function titleFromResourcePath(resourcePath) {
5593
+ const lastSegment = resourcePath?.split('/').filter(Boolean).pop();
5594
+ if (!lastSegment) {
5595
+ return undefined;
5596
+ }
5597
+ return lastSegment
5598
+ .replace(/[-_]+/g, ' ')
5599
+ .replace(/\s+/g, ' ')
5600
+ .trim()
5601
+ .toLocaleLowerCase('pt-BR');
5602
+ }
5603
+ function singularizePtBrResourceTitle(title) {
5604
+ const normalized = title.trim();
5605
+ const lower = normalized.toLocaleLowerCase('pt-BR');
5606
+ if (lower === 'códigos de frequência' || lower === 'codigos de frequencia') {
5607
+ return 'código de frequência';
5608
+ }
5609
+ const [first, ...rest] = normalized.split(/\s+/);
5610
+ const singularFirst = singularizePtBrWord(first);
5611
+ return [singularFirst, ...rest].join(' ');
5612
+ }
5613
+ function singularizePtBrWord(word) {
5614
+ if (/ões$/i.test(word)) {
5615
+ return word.replace(/ões$/i, 'ão');
5616
+ }
5617
+ if (/ais$/i.test(word)) {
5618
+ return word.replace(/ais$/i, 'al');
5619
+ }
5620
+ if (/eis$/i.test(word)) {
5621
+ return word.replace(/eis$/i, 'el');
5622
+ }
5623
+ if (/res$/i.test(word)) {
5624
+ return word.replace(/es$/i, '');
5625
+ }
5626
+ if (/s$/i.test(word) && !/ss$/i.test(word)) {
5627
+ return word.replace(/s$/i, '');
5628
+ }
5629
+ return word;
5630
+ }
5631
+ function lowerFirstLetter(value) {
5632
+ return value.replace(/\p{L}/u, (letter) => letter.toLocaleLowerCase('pt-BR'));
5633
+ }
5183
5634
 
5184
5635
  var dynamicFormDialogHost_component = /*#__PURE__*/Object.freeze({
5185
5636
  __proto__: null,
@@ -5325,11 +5776,31 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
5325
5776
  type: 'PraxisDataQueryContext | null',
5326
5777
  description: 'Contexto semântico de consulta encaminhado para a tabela interna do CRUD. Preferir este contrato para novo authoring remoto.',
5327
5778
  },
5779
+ {
5780
+ name: 'metadata.resource.schemaPath',
5781
+ type: 'string | null',
5782
+ description: 'Template canonico opcional usado apenas para resolver /schemas/filtered quando resource.path precisa ser um alvo operacional concreto.',
5783
+ },
5784
+ {
5785
+ name: 'metadata.resource.title',
5786
+ type: 'string | null',
5787
+ description: 'Titulo documental do recurso usado pelo shell CRUD para derivar titulos contextuais de criacao, edicao e visualizacao quando a action nao informa label.',
5788
+ },
5789
+ {
5790
+ name: 'metadata.resource.formTitle',
5791
+ type: 'string | null',
5792
+ description: 'Titulo documental especifico para formularios do recurso, usado antes de metadata.resource.title no shell modal/drawer.',
5793
+ },
5328
5794
  {
5329
5795
  name: 'metadata.filterCriteria',
5330
5796
  type: 'Record<string, unknown> | null',
5331
5797
  description: 'Bridge declarativa de filtros encaminhada para a tabela interna. Para novo authoring, prefira metadata.queryContext.',
5332
5798
  },
5799
+ {
5800
+ name: 'metadata.form',
5801
+ type: 'FormConfig',
5802
+ 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.',
5803
+ },
5333
5804
  {
5334
5805
  name: 'metadata.actions[].form',
5335
5806
  type: 'CrudActionFormContract',
@@ -5699,6 +6170,7 @@ const CRUD_AI_CAPABILITIES = {
5699
6170
  { path: 'actions[].params[].to', category: 'actions', valueKind: 'enum', allowedValues: ENUMS.paramTarget, description: 'Destino do parametro.' },
5700
6171
  { path: 'actions[].params[].name', category: 'actions', valueKind: 'string', description: 'Nome do parametro no destino.' },
5701
6172
  { path: 'actions[].form.initialValue', category: 'actions', valueKind: 'object', description: 'Seed fixo do formulario injetado em inputs.initialValue antes da abertura.' },
6173
+ { 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
6174
  { path: 'actions[].back', category: 'navigation', valueKind: 'object', description: 'BackConfig por acao.' },
5703
6175
  { path: 'actions[].back.strategy', category: 'navigation', valueKind: 'enum', allowedValues: ENUMS.backStrategy, description: 'Estrategia de retorno da acao (auto, close, navigate).' },
5704
6176
  { path: 'actions[].back.returnTo', category: 'navigation', valueKind: 'string', description: 'Rota ou destino usado quando a estrategia de retorno for navigate.' },
@@ -5736,6 +6208,7 @@ const surfaceConfigureSchema = {
5736
6208
  submitMethod: { enum: ['post', 'put', 'patch', 'delete'] },
5737
6209
  apiEndpointKey: { type: 'string' },
5738
6210
  initialValue: { type: 'object' },
6211
+ layoutPolicy: { type: 'object' },
5739
6212
  },
5740
6213
  },
5741
6214
  params: {
@@ -5854,14 +6327,14 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
5854
6327
  { name: 'afterDelete', type: '{ id: string | number }', description: 'Emitted after delete; CRUD refetches the list.' },
5855
6328
  ],
5856
6329
  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.' },
6330
+ { 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
6331
  { kind: 'listSurface', resolver: 'crud-list-surface', description: 'CRUD-hosted list surface and table delegation boundary.' },
5859
6332
  { kind: 'createSurface', resolver: 'crud-action-by-id:create', description: 'Create action open mode, route/form binding and launcher inputs.' },
5860
6333
  { kind: 'editSurface', resolver: 'crud-action-by-id:edit', description: 'Edit action open mode, route/form binding and launcher inputs.' },
5861
6334
  { kind: 'viewSurface', resolver: 'crud-action-by-id:view', description: 'View action open mode, route/form binding and launcher inputs.' },
5862
6335
  { 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.' },
6336
+ { 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.' },
6337
+ { 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
6338
  { kind: 'permissions', resolver: 'crud-resource-capabilities', description: 'CRUD action availability derived from resource capabilities and action permissions.' },
5866
6339
  { 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
6340
  { kind: 'childOperation', resolver: 'child-authoring-manifest-operation', description: 'Delegated form/table/dialog/settings-panel operation owned by the child component manifest.' },
@@ -5877,13 +6350,13 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
5877
6350
  effects: [{ kind: 'compile-domain-patch', handler: 'crud-resource-bind', handlerContract: {
5878
6351
  reads: ['CrudMetadata.resource', 'api_metadata', 'ResourceDiscoveryService', 'GET /{resource}/capabilities'],
5879
6352
  writes: ['CrudMetadata.resource', 'CrudMetadata.queryContext', 'PraxisCrudComponent.tableCrudContext'],
5880
- identityKeys: ['resourcePath', 'resourceKey'],
6353
+ identityKeys: ['resourcePath', 'resourceKey', 'schemaPath'],
5881
6354
  inputSchema: resourceBindSchema,
5882
6355
  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.',
6356
+ 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
6357
  } }],
5885
6358
  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'],
6359
+ affectedPaths: ['resource.path', 'resource.schemaPath', 'resource.idField', 'resource.endpointKey', 'resource.apiUrlEntry', 'queryContext', 'filterCriteria'],
5887
6360
  submissionImpact: 'affects-remote-binding',
5888
6361
  preconditions: ['crud-metadata-loaded', 'api-metadata-available'],
5889
6362
  },
@@ -6017,14 +6490,14 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
6017
6490
  target: { kind: 'dialogHost', resolver: 'crud-dialog-host-defaults', ambiguityPolicy: 'fail', required: true },
6018
6491
  inputSchema: dialogHostSchema,
6019
6492
  effects: [{ kind: 'compile-domain-patch', handler: 'crud-dialog-host-set', handlerContract: {
6020
- reads: ['CrudMetadata.defaults', 'CrudLauncherService.resolveOpenMode', 'DialogService', 'CRUD_DRAWER_ADAPTER'],
6493
+ reads: ['CrudMetadata.defaults', 'CrudLauncherService.resolveOpenMode', 'DialogService', 'DynamicFormDialogHostComponent', 'CRUD_DRAWER_ADAPTER'],
6021
6494
  writes: ['CrudMetadata.defaults.openMode', 'CrudMetadata.defaults.modal', 'CrudMetadata.defaults.back'],
6022
6495
  identityKeys: ['crudId'],
6023
6496
  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.',
6497
+ failureModes: ['open-mode-unsupported', 'drawer-runtime-unavailable', 'modal-size-invalid', 'back-policy-invalid'],
6498
+ description: 'Configures CRUD-owned route/modal/drawer defaults consumed by the launcher, default dialog-backed drawer runtime and optional host drawer adapter.',
6026
6499
  } }],
6027
- validators: ['open-mode-supported', 'modal-size-valid', 'drawer-adapter-available-when-needed', 'back-policy-valid', 'settings-panel-shell-compatible'],
6500
+ validators: ['open-mode-supported', 'modal-size-valid', 'drawer-runtime-available', 'back-policy-valid', 'settings-panel-shell-compatible'],
6028
6501
  affectedPaths: ['defaults.openMode', 'defaults.modal', 'defaults.back'],
6029
6502
  submissionImpact: 'config-only',
6030
6503
  preconditions: ['crud-metadata-loaded'],
@@ -6100,7 +6573,7 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
6100
6573
  { validatorId: 'permissions-delete-valid', level: 'error', code: 'CRUD_DELETE_PERMISSION_VALID', description: 'Delete permission cannot bypass resource capabilities or confirmation policy.' },
6101
6574
  { validatorId: 'open-mode-supported', level: 'error', code: 'CRUD_OPEN_MODE_SUPPORTED', description: 'Open mode must be route, modal or drawer.' },
6102
6575
  { 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.' },
6576
+ { 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
6577
  { validatorId: 'back-policy-valid', level: 'error', code: 'CRUD_BACK_POLICY_VALID', description: 'Back policy must be valid for route/modal/drawer behavior.' },
6105
6578
  { validatorId: 'settings-panel-shell-compatible', level: 'warning', code: 'CRUD_SETTINGS_PANEL_COMPATIBLE', description: 'Authoring shell must preserve apply/save/reset semantics.' },
6106
6579
  { validatorId: 'action-permission-supported', level: 'error', code: 'CRUD_ACTION_PERMISSION_SUPPORTED', description: 'Action permissions must map to supported resource capabilities.' },