@praxisui/crud 9.0.0-beta.9 → 9.0.0-beta.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,16 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, InjectionToken, inject, input, signal, computed, effect, ChangeDetectionStrategy, Component, EventEmitter, DestroyRef, ChangeDetectorRef, Injector, ViewChild, Output, Input, Inject, ENVIRONMENT_INITIALIZER } from '@angular/core';
2
+ import { Injectable, InjectionToken, inject, input, signal, computed, effect, ChangeDetectionStrategy, Component, EventEmitter, DestroyRef, ChangeDetectorRef, Injector, ViewChild, Output, Input, Inject, ViewEncapsulation, ENVIRONMENT_INITIALIZER } from '@angular/core';
3
3
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
4
  import { HttpClient } from '@angular/common/http';
5
5
  import { Router, ActivatedRoute, RouterLink } from '@angular/router';
6
6
  import { MatSnackBar } from '@angular/material/snack-bar';
7
7
  import { firstValueFrom, BehaviorSubject, Subscription } from 'rxjs';
8
8
  import * as i2$1 from '@praxisui/core';
9
- import { ASYNC_CONFIG_STORAGE, GlobalConfigService, CrudOperationResolutionService, fillUndefined, SETTINGS_PANEL_DATA, PraxisI18nService, PraxisIconDirective, providePraxisI18nConfig, createDefaultTableConfig, GLOBAL_SURFACE_SERVICE, ComponentKeyService, ResourceDiscoveryService, ResourceActionOpenAdapterService, ResourceSurfaceOpenAdapterService, translateUnavailableWorkflowMessage, EmptyStateCardComponent, RESOURCE_DISCOVERY_I18N_CONFIG, GenericCrudService, ComponentMetadataRegistry } from '@praxisui/core';
9
+ import { ASYNC_CONFIG_STORAGE, GlobalConfigService, CrudOperationResolutionService, fillUndefined, SETTINGS_PANEL_DATA, PraxisI18nService, PraxisIconDirective, providePraxisI18nConfig, createDefaultTableConfig, EnterpriseRuntimeContextService, GLOBAL_SURFACE_SERVICE, SurfaceOutletRegistryService, ComponentKeyService, ResourceDiscoveryService, ResourceActionOpenAdapterService, ResourceSurfaceOpenAdapterService, translateUnavailableWorkflowMessage, EmptyStateCardComponent, RESOURCE_DISCOVERY_I18N_CONFIG, PraxisIconButtonComponent, PraxisResourceIdentityComponent, GenericCrudService, ComponentMetadataRegistry } from '@praxisui/core';
10
10
  import { SettingsPanelService } from '@praxisui/settings-panel';
11
11
  import { PraxisTableInlineAuthoringEditorComponent, PraxisTable } from '@praxisui/table';
12
12
  import { ConfirmDialogComponent } from '@praxisui/dynamic-fields';
13
+ import { take, filter } from 'rxjs/operators';
13
14
  import * as i1 from '@angular/material/dialog';
14
15
  import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
15
16
  export { MAT_DIALOG_DATA as DIALOG_DATA } from '@angular/material/dialog';
@@ -34,7 +35,6 @@ import { MatSlideToggleModule } from '@angular/material/slide-toggle';
34
35
  import * as i10 from '@angular/material/tabs';
35
36
  import { MatTabsModule } from '@angular/material/tabs';
36
37
  import { PraxisDynamicForm } from '@praxisui/dynamic-form';
37
- import { filter, take } from 'rxjs/operators';
38
38
 
39
39
  class DialogService {
40
40
  matDialog;
@@ -68,6 +68,35 @@ function getCrudDrawerAdapterToken() {
68
68
  }
69
69
  const CRUD_DRAWER_ADAPTER = getCrudDrawerAdapterToken();
70
70
 
71
+ const DEFAULT_CRUD_DRAWER_MODAL_CONFIG = {
72
+ minWidth: 'min(100vw, 520px)',
73
+ width: 'min(760px, 58vw)',
74
+ height: '100dvh',
75
+ maxWidth: '100vw',
76
+ maxHeight: '100dvh',
77
+ position: { right: '0', top: '0' },
78
+ };
79
+ function isCrudDebugEnabled$1() {
80
+ try {
81
+ if (globalThis.__PRAXIS_DEBUG_CRUD__) {
82
+ return true;
83
+ }
84
+ if (typeof localStorage !== 'undefined') {
85
+ return localStorage.getItem('praxis.debug.crud') === 'true';
86
+ }
87
+ }
88
+ catch { }
89
+ return false;
90
+ }
91
+ function debugCrudLauncher(message, ...data) {
92
+ if (!isCrudDebugEnabled$1()) {
93
+ return;
94
+ }
95
+ try {
96
+ console.debug(message, ...data);
97
+ }
98
+ catch { }
99
+ }
71
100
  class CrudLauncherService {
72
101
  router = inject(Router);
73
102
  dialog = inject(DialogService);
@@ -85,8 +114,9 @@ class CrudLauncherService {
85
114
  async launch(action, row, metadata, componentKeyId, drawerCallbacks, runtime) {
86
115
  // Carregar overrides de CRUD (se houver) e mesclar em uma cópia local
87
116
  const merged = await this.mergeCrudOverrides(metadata, action, componentKeyId || undefined);
117
+ merged.action = this.normalizeActionForLaunch(merged.action);
88
118
  const mode = this.resolveOpenMode(merged.action, merged.metadata);
89
- console.debug('[CRUD:Launcher] mode=', mode, 'action=', action);
119
+ debugCrudLauncher('[CRUD:Launcher] mode=', mode, 'action=', action);
90
120
  if (mode === 'route') {
91
121
  if (!merged.action.route) {
92
122
  throw new Error(`Route not provided for action ${merged.action.action}`);
@@ -95,22 +125,6 @@ class CrudLauncherService {
95
125
  await this.router.navigateByUrl(url);
96
126
  return { mode };
97
127
  }
98
- if (mode === 'drawer' && this.drawerAdapter) {
99
- const actionForLaunch = this.resolveActionForLaunch(merged.action, merged.metadata);
100
- const inputs = this.mapInputs(actionForLaunch, row, merged.metadata, runtime);
101
- const idField = merged.metadata.resource?.idField ?? 'id';
102
- if (row && inputs[idField] === undefined && row[idField] !== undefined) {
103
- inputs[idField] = row[idField];
104
- }
105
- await Promise.resolve(this.drawerAdapter.open({
106
- action: actionForLaunch,
107
- metadata: merged.metadata,
108
- inputs,
109
- onClose: drawerCallbacks?.onClose,
110
- onResult: drawerCallbacks?.onResult,
111
- }));
112
- return { mode };
113
- }
114
128
  const actionForLaunch = this.resolveActionForLaunch(merged.action, merged.metadata);
115
129
  if (!actionForLaunch.formId) {
116
130
  throw new Error(`formId not provided for action ${actionForLaunch.action}`);
@@ -123,18 +137,33 @@ class CrudLauncherService {
123
137
  row[idField] !== undefined) {
124
138
  inputs[idField] = row[idField];
125
139
  }
140
+ const resourceIdentity = actionForLaunch.action === 'edit'
141
+ ? runtime?.resourceIdentity ?? null
142
+ : null;
143
+ if (mode === 'drawer' && this.drawerAdapter) {
144
+ await Promise.resolve(this.drawerAdapter.open({
145
+ action: actionForLaunch,
146
+ metadata: merged.metadata,
147
+ inputs,
148
+ resourceIdentity,
149
+ onClose: drawerCallbacks?.onClose,
150
+ onResult: drawerCallbacks?.onResult,
151
+ }));
152
+ return { mode };
153
+ }
126
154
  const modalCfg = { ...(merged.metadata.defaults?.modal || {}) };
127
- console.debug('[CRUD:Launcher] opening dialog with:', {
155
+ debugCrudLauncher('[CRUD:Launcher] opening dialog with:', {
128
156
  action: merged.action.action,
129
157
  formId: actionForLaunch.formId,
130
158
  inputs,
131
159
  modalCfg,
132
160
  resourcePath: merged.metadata.resource?.path ?? merged.metadata.table?.resourcePath,
133
161
  });
134
- const panelClasses = ['pfx-dialog-pane', 'pfx-dialog-frosted'];
135
- if (modalCfg.panelClass) {
136
- panelClasses.push(modalCfg.panelClass);
137
- }
162
+ const drawerMode = mode === 'drawer';
163
+ const dialogPosition = drawerMode
164
+ ? (modalCfg.position ?? DEFAULT_CRUD_DRAWER_MODAL_CONFIG.position)
165
+ : modalCfg.position;
166
+ const panelClasses = mergeClassList(['pfx-dialog-pane', drawerMode ? 'pfx-drawer-pane' : 'pfx-dialog-frosted'], modalCfg.panelClass);
138
167
  // Backdrop style presets
139
168
  const backdropClasses = [];
140
169
  const style = modalCfg.backdropStyle;
@@ -147,20 +176,56 @@ class CrudLauncherService {
147
176
  else if (style === 'transparent') {
148
177
  backdropClasses.push('pfx-transparent-backdrop');
149
178
  }
150
- if (modalCfg.backdropClass) {
151
- backdropClasses.push(modalCfg.backdropClass);
152
- }
179
+ const mergedBackdropClasses = mergeClassList(backdropClasses, modalCfg.backdropClass);
153
180
  const ref = await this.dialog.openAsync(() => Promise.resolve().then(function () { return dynamicFormDialogHost_component; }).then((m) => m.DynamicFormDialogHostComponent), {
154
181
  ...modalCfg,
155
182
  panelClass: panelClasses,
156
- backdropClass: backdropClasses,
183
+ backdropClass: mergedBackdropClasses,
157
184
  autoFocus: modalCfg.autoFocus ?? true,
158
185
  restoreFocus: modalCfg.restoreFocus ?? true,
159
- minWidth: '360px',
160
- maxWidth: '95vw',
186
+ minWidth: drawerMode
187
+ ? (modalCfg.minWidth ?? DEFAULT_CRUD_DRAWER_MODAL_CONFIG.minWidth)
188
+ : '360px',
189
+ width: drawerMode
190
+ ? (modalCfg.width ?? DEFAULT_CRUD_DRAWER_MODAL_CONFIG.width)
191
+ : modalCfg.width,
192
+ height: drawerMode
193
+ ? (modalCfg.height ?? DEFAULT_CRUD_DRAWER_MODAL_CONFIG.height)
194
+ : modalCfg.height,
195
+ maxWidth: drawerMode
196
+ ? (modalCfg.maxWidth ?? DEFAULT_CRUD_DRAWER_MODAL_CONFIG.maxWidth)
197
+ : '95vw',
198
+ maxHeight: drawerMode
199
+ ? (modalCfg.maxHeight ?? DEFAULT_CRUD_DRAWER_MODAL_CONFIG.maxHeight)
200
+ : modalCfg.maxHeight,
201
+ position: dialogPosition,
161
202
  ariaLabelledBy: 'crudDialogTitle',
162
- data: { action: actionForLaunch, row, metadata: merged.metadata, inputs },
203
+ data: {
204
+ action: actionForLaunch,
205
+ row,
206
+ metadata: merged.metadata,
207
+ inputs,
208
+ presentation: drawerMode ? 'drawer' : 'modal',
209
+ dialogPosition,
210
+ resourceIdentity,
211
+ },
163
212
  });
213
+ if (drawerMode) {
214
+ ref
215
+ .afterClosed()
216
+ .pipe(take(1))
217
+ .subscribe((closedValue) => {
218
+ const result = toCrudDrawerResult(closedValue);
219
+ drawerCallbacks?.onClose?.();
220
+ if (result?.type) {
221
+ drawerCallbacks?.onResult?.(result);
222
+ }
223
+ else {
224
+ drawerCallbacks?.onResult?.({ type: 'close' });
225
+ }
226
+ });
227
+ return { mode, ref };
228
+ }
164
229
  return { mode, ref };
165
230
  }
166
231
  resolveOpenMode(action, metadata) {
@@ -168,12 +233,17 @@ class CrudLauncherService {
168
233
  const local = action.openMode ?? metadata.defaults?.openMode;
169
234
  if (local)
170
235
  return local;
171
- // Global fallback (action-specific, then general), then hard fallback 'route'
236
+ // Global fallback (action-specific, then general). A canonical CRUD
237
+ // operation without an explicit route can still be materialized through
238
+ // the inferred form contract, so it must not fall through to a route that
239
+ // does not exist.
172
240
  try {
173
241
  const globalCrud = this.global.getCrud();
174
242
  const actionName = action.action;
175
243
  const globalMode = (actionName && globalCrud?.actionDefaults?.[actionName]?.openMode) ?? globalCrud?.defaults?.openMode;
176
- let resolved = globalMode ?? 'route';
244
+ let resolved = globalMode ?? (this.isCanonicalCrudAction(action.action) && !action.route
245
+ ? 'drawer'
246
+ : 'route');
177
247
  // Safety: if modal/drawer but there is no formId and the action cannot be inferred, degrade to route.
178
248
  if ((resolved === 'modal' || resolved === 'drawer') &&
179
249
  !action.formId &&
@@ -248,6 +318,9 @@ class CrudLauncherService {
248
318
  if (resolved.apiUrlEntry != null) {
249
319
  inputs['apiUrlEntry'] = resolved.apiUrlEntry;
250
320
  }
321
+ if (resolved.layoutPolicy != null) {
322
+ inputs['layoutPolicy'] = resolved.layoutPolicy;
323
+ }
251
324
  if (action.form?.initialValue != null) {
252
325
  inputs['initialValue'] = action.form.initialValue;
253
326
  }
@@ -257,6 +330,7 @@ class CrudLauncherService {
257
330
  return inputs;
258
331
  }
259
332
  resolveActionForLaunch(action, metadata) {
333
+ action = this.normalizeActionForLaunch(action);
260
334
  if (action.formId) {
261
335
  return action;
262
336
  }
@@ -264,29 +338,61 @@ class CrudLauncherService {
264
338
  return action;
265
339
  }
266
340
  const formId = this.buildInferredFormId(action, metadata);
267
- return formId ? { ...action, formId } : action;
341
+ return formId
342
+ ? {
343
+ ...action,
344
+ formId,
345
+ form: {
346
+ ...(action.form || {}),
347
+ layoutPolicy: action.form?.layoutPolicy ?? this.defaultInferredLayoutPolicy(action),
348
+ },
349
+ }
350
+ : action;
351
+ }
352
+ resolveSubmitUrlTemplate(submitUrl, row, metadata) {
353
+ const template = String(submitUrl || '').trim();
354
+ if (!template || !template.includes('{')) {
355
+ return template;
356
+ }
357
+ const idField = String(metadata.resource?.idField ?? 'id');
358
+ const rowId = row?.[idField];
359
+ if (rowId == null) {
360
+ return template;
361
+ }
362
+ const encodedId = encodeURIComponent(String(rowId));
363
+ const idFieldPattern = new RegExp(`\\{${escapeRegExp(idField)}\\}`, 'g');
364
+ return template
365
+ .replace(idFieldPattern, encodedId)
366
+ .replace(/\{id\}/g, encodedId)
367
+ .replace(/\{resourceId\}/g, encodedId);
268
368
  }
269
369
  resolveActionFormContract(action, row, metadata, runtime) {
270
370
  if (this.isExplicitCrudAction(action)) {
271
371
  return {
272
372
  schemaUrl: action.form?.schemaUrl ?? null,
273
- submitUrl: action.form?.submitUrl ?? null,
373
+ submitUrl: action.form?.submitUrl
374
+ ? this.resolveSubmitUrlTemplate(action.form.submitUrl, row, metadata)
375
+ : null,
274
376
  submitMethod: action.form?.submitMethod ?? null,
275
377
  apiEndpointKey: action.form?.apiEndpointKey ?? null,
276
378
  apiUrlEntry: action.form?.apiUrlEntry ?? null,
379
+ layoutPolicy: action.form?.layoutPolicy ?? null,
277
380
  };
278
381
  }
279
382
  if (!this.isCanonicalCrudAction(action.action)) {
280
383
  return {
281
384
  apiEndpointKey: action.form?.apiEndpointKey ?? null,
282
385
  apiUrlEntry: action.form?.apiUrlEntry ?? null,
386
+ layoutPolicy: action.form?.layoutPolicy ?? null,
283
387
  };
284
388
  }
285
389
  const resourcePath = String(metadata.resource?.path ?? metadata.table?.resourcePath ?? '').trim();
390
+ const schemaResourcePath = String(metadata.resource?.schemaPath || '').trim() || null;
286
391
  if (!resourcePath) {
287
392
  return {
288
393
  apiEndpointKey: action.form?.apiEndpointKey ?? null,
289
394
  apiUrlEntry: action.form?.apiUrlEntry ?? null,
395
+ layoutPolicy: action.form?.layoutPolicy ?? null,
290
396
  };
291
397
  }
292
398
  const idField = String(metadata.resource?.idField ?? 'id');
@@ -294,6 +400,7 @@ class CrudLauncherService {
294
400
  const resolved = this.operationResolver.resolve({
295
401
  operation: action.action,
296
402
  resourcePath,
403
+ schemaResourcePath,
297
404
  resourceId: resourceId ?? null,
298
405
  capabilities: runtime?.capabilities ?? null,
299
406
  links: runtime?.links ?? null,
@@ -301,14 +408,15 @@ class CrudLauncherService {
301
408
  endpointKey: action.form?.apiEndpointKey ??
302
409
  metadata.resource?.endpointKey ??
303
410
  undefined,
304
- apiUrlEntry: action.form?.apiUrlEntry ?? null,
411
+ apiUrlEntry: action.form?.apiUrlEntry ?? metadata.resource?.apiUrlEntry ?? null,
305
412
  });
306
413
  return {
307
414
  schemaUrl: resolved?.schemaUrl ?? null,
308
415
  submitUrl: resolved?.submitUrl ?? null,
309
416
  submitMethod: resolved?.submitMethod ?? null,
310
417
  apiEndpointKey: action.form?.apiEndpointKey ?? metadata.resource?.endpointKey ?? null,
311
- apiUrlEntry: action.form?.apiUrlEntry ?? null,
418
+ apiUrlEntry: action.form?.apiUrlEntry ?? metadata.resource?.apiUrlEntry ?? null,
419
+ layoutPolicy: action.form?.layoutPolicy ?? null,
312
420
  };
313
421
  }
314
422
  resolveRuntimeContract(action, row, metadata, runtime) {
@@ -327,9 +435,20 @@ class CrudLauncherService {
327
435
  return `${sanitizedResource || 'crud'}-${String(action.action || '').trim().toLowerCase()}`;
328
436
  }
329
437
  isCanonicalCrudAction(actionName) {
330
- const normalized = String(actionName || '').trim().toLowerCase();
438
+ const normalized = this.normalizeCrudOperationName(actionName);
331
439
  return normalized === 'create' || normalized === 'view' || normalized === 'edit' || normalized === 'delete';
332
440
  }
441
+ normalizeActionForLaunch(action) {
442
+ const normalized = this.normalizeCrudOperationName(action.action);
443
+ return normalized === action.action ? action : { ...action, action: normalized };
444
+ }
445
+ normalizeCrudOperationName(actionName) {
446
+ const normalized = String(actionName || '').trim().toLowerCase();
447
+ if (['add', 'novo', 'new', 'incluir', 'inserir'].includes(normalized)) {
448
+ return 'create';
449
+ }
450
+ return normalized;
451
+ }
333
452
  isExplicitCrudAction(action) {
334
453
  if (action.mode === 'explicit') {
335
454
  return true;
@@ -338,6 +457,19 @@ class CrudLauncherService {
338
457
  String(action.form?.submitUrl || '').trim() ||
339
458
  String(action.form?.submitMethod || '').trim());
340
459
  }
460
+ defaultInferredLayoutPolicy(action) {
461
+ const operation = this.normalizeCrudOperationName(action.action);
462
+ if (operation !== 'create' && operation !== 'edit') {
463
+ return null;
464
+ }
465
+ return {
466
+ source: 'schema',
467
+ intent: 'command',
468
+ preset: 'groupedCommand',
469
+ persistence: 'transient',
470
+ schemaType: 'request',
471
+ };
472
+ }
341
473
  async mergeCrudOverrides(metadata, action, componentKeyId) {
342
474
  try {
343
475
  if (!componentKeyId)
@@ -392,6 +524,33 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
392
524
  type: Injectable,
393
525
  args: [{ providedIn: 'root' }]
394
526
  }] });
527
+ function mergeClassList(base, extra) {
528
+ if (!extra) {
529
+ return base;
530
+ }
531
+ if (typeof extra === 'string') {
532
+ return [...base, extra];
533
+ }
534
+ if (Array.isArray(extra)) {
535
+ return [...base, ...extra];
536
+ }
537
+ return [
538
+ ...base,
539
+ ...Object.keys(extra).filter((className) => !!extra[className]),
540
+ ];
541
+ }
542
+ function escapeRegExp(value) {
543
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
544
+ }
545
+ function toCrudDrawerResult(value) {
546
+ if (!value || typeof value !== 'object') {
547
+ return undefined;
548
+ }
549
+ const result = value;
550
+ return result.type === 'save' || result.type === 'delete' || result.type === 'close'
551
+ ? result
552
+ : undefined;
553
+ }
395
554
 
396
555
  const DOCUMENT_KIND = 'praxis.crud.editor';
397
556
  const DOCUMENT_VERSION = 1;
@@ -510,10 +669,15 @@ function normalizeCrudMetadata(metadata) {
510
669
  if (base.resource && typeof base.resource === 'object') {
511
670
  normalized.resource = stripUndefinedShallow({
512
671
  path: trimString(base.resource.path) || undefined,
672
+ schemaPath: trimString(base.resource.schemaPath) || undefined,
513
673
  idField: typeof base.resource.idField === 'string'
514
674
  ? trimString(base.resource.idField) || undefined
515
675
  : base.resource.idField,
676
+ title: trimString(base.resource.title) || undefined,
677
+ label: trimString(base.resource.label) || undefined,
678
+ formTitle: trimString(base.resource.formTitle) || undefined,
516
679
  endpointKey: trimString(base.resource.endpointKey) || undefined,
680
+ apiUrlEntry: base.resource.apiUrlEntry || undefined,
517
681
  });
518
682
  }
519
683
  if (base.defaults && typeof base.defaults === 'object') {
@@ -564,6 +728,7 @@ function normalizeCrudAction(action) {
564
728
  apiEndpointKey: trimString(action.form.apiEndpointKey) || undefined,
565
729
  apiUrlEntry: action.form.apiUrlEntry || undefined,
566
730
  initialValue: normalizeInitialValue(action.form.initialValue),
731
+ layoutPolicy: action.form.layoutPolicy ?? undefined,
567
732
  }
568
733
  : undefined,
569
734
  });
@@ -721,11 +886,17 @@ const PRAXIS_CRUD_RUNTIME_I18N_CONFIG = {
721
886
  'crud.emptyState.title': 'Conecte o CRUD a um recurso',
722
887
  'crud.emptyState.description': 'Informe os metadados (resourcePath / schema) ou forneça metadata.data para habilitar a tabela e as ações.',
723
888
  'crud.emptyState.primaryAction': 'Configurar metadados',
889
+ 'crud.table.emptyState.initial.title': 'Sem registros em {label}',
890
+ 'crud.table.emptyState.initial.titleFallback': 'Nenhum registro disponível.',
891
+ 'crud.table.emptyState.initial.descriptionWithAction': 'Use a ação principal para adicionar o primeiro registro quando houver informações para cadastrar.',
892
+ 'crud.table.emptyState.filtered.title': 'Nenhum resultado encontrado.',
893
+ 'crud.table.emptyState.filtered.description': 'Revise os filtros ou ajuste o termo de busca.',
724
894
  'crud.preferences.resetSuccess': 'Overrides de CRUD redefinidos',
725
895
  'crud.actions.create': 'Adicionar',
726
896
  'crud.actions.view': 'Ver',
727
897
  'crud.actions.edit': 'Editar',
728
898
  'crud.actions.delete': 'Excluir',
899
+ 'crud.dialog.recordContextLabel': 'Registro em edição',
729
900
  'crud.delete.confirmMessage': 'Esta ação não pode ser desfeita. Deseja continuar?',
730
901
  'crud.delete.cancel': 'Cancelar',
731
902
  },
@@ -733,11 +904,17 @@ const PRAXIS_CRUD_RUNTIME_I18N_CONFIG = {
733
904
  'crud.emptyState.title': 'Connect CRUD to a resource',
734
905
  'crud.emptyState.description': 'Provide metadata (resourcePath / schema) or metadata.data to enable the table and actions.',
735
906
  'crud.emptyState.primaryAction': 'Configure metadata',
907
+ 'crud.table.emptyState.initial.title': 'No records in {label}',
908
+ 'crud.table.emptyState.initial.titleFallback': 'No records available.',
909
+ 'crud.table.emptyState.initial.descriptionWithAction': 'Use the primary action to add the first record when there is information to register.',
910
+ 'crud.table.emptyState.filtered.title': 'No results found.',
911
+ 'crud.table.emptyState.filtered.description': 'Review the filters or adjust the search term.',
736
912
  'crud.preferences.resetSuccess': 'CRUD overrides reset',
737
913
  'crud.actions.create': 'Add',
738
914
  'crud.actions.view': 'View',
739
915
  'crud.actions.edit': 'Edit',
740
916
  'crud.actions.delete': 'Delete',
917
+ 'crud.dialog.recordContextLabel': 'Record being edited',
741
918
  'crud.delete.confirmMessage': 'This action cannot be undone. Do you want to continue?',
742
919
  'crud.delete.cancel': 'Cancel',
743
920
  },
@@ -772,8 +949,9 @@ function resolveCrudRuntimeFallback(key, locale) {
772
949
  return getCrudRuntimeDictionary(locale || 'pt-BR')?.[key];
773
950
  }
774
951
  function translateCrudRuntimeText(i18n, key, fallback, params, locale) {
775
- const runtimeFallback = resolveCrudRuntimeFallback(key, locale)
776
- || resolveCrudRuntimeFallback(key, locale || i18n.getLocale?.() || i18n.getFallbackLocale?.() || 'pt-BR')
952
+ const effectiveLocale = locale || i18n.getLocale?.() || i18n.getFallbackLocale?.() || 'pt-BR';
953
+ const runtimeFallback = resolveCrudRuntimeFallback(key, effectiveLocale)
954
+ || resolveCrudRuntimeFallback(key, i18n.getFallbackLocale?.())
777
955
  || fallback
778
956
  || key;
779
957
  return i18n.t(key, params, runtimeFallback, PRAXIS_CRUD_RUNTIME_I18N_NAMESPACE);
@@ -3467,6 +3645,8 @@ class PraxisCrudComponent {
3467
3645
  componentInstanceId;
3468
3646
  context;
3469
3647
  enableCustomization = false;
3648
+ /** Capability publica exigida para authoring governado do CRUD e da tabela interna. */
3649
+ authoringCapability = null;
3470
3650
  configureRequested = new EventEmitter();
3471
3651
  afterOpen = new EventEmitter();
3472
3652
  afterClose = new EventEmitter();
@@ -3491,6 +3671,7 @@ class PraxisCrudComponent {
3491
3671
  table;
3492
3672
  storage = inject(ASYNC_CONFIG_STORAGE);
3493
3673
  settingsPanel = inject(SettingsPanelService);
3674
+ enterpriseRuntimeContext = inject(EnterpriseRuntimeContextService, { optional: true });
3494
3675
  snack = inject(MatSnackBar);
3495
3676
  dialog = inject(DialogService);
3496
3677
  i18n = inject(PraxisI18nService);
@@ -3498,6 +3679,7 @@ class PraxisCrudComponent {
3498
3679
  resourceDiscoveryInstance;
3499
3680
  actionOpenAdapterInstance;
3500
3681
  surfaceOpenAdapterInstance;
3682
+ materializedRefreshSequence = 0;
3501
3683
  global = (() => {
3502
3684
  try {
3503
3685
  return inject(GlobalConfigService);
@@ -3507,6 +3689,7 @@ class PraxisCrudComponent {
3507
3689
  }
3508
3690
  })();
3509
3691
  surfaceService = inject(GLOBAL_SURFACE_SERVICE, { optional: true });
3692
+ surfaceOutlets = inject(SurfaceOutletRegistryService);
3510
3693
  componentKeys = inject(ComponentKeyService);
3511
3694
  route = (() => {
3512
3695
  try {
@@ -3523,10 +3706,23 @@ class PraxisCrudComponent {
3523
3706
  lastAppliedResourceIdentity = null;
3524
3707
  tableCollectionLinks = null;
3525
3708
  collectionCapabilities = null;
3709
+ activeAuthoringPanelRef;
3710
+ constructor() {
3711
+ this.enterpriseRuntimeContext?.contextChanges$
3712
+ .pipe(takeUntilDestroyed(this.destroyRef))
3713
+ .subscribe(() => {
3714
+ if (!this.isCustomizationAvailable()) {
3715
+ this.activeAuthoringPanelRef?.close();
3716
+ this.activeAuthoringPanelRef = undefined;
3717
+ }
3718
+ this.cdr.markForCheck();
3719
+ });
3720
+ }
3526
3721
  collectionCapabilitiesRequestHref = null;
3527
3722
  collectionCapabilitiesResolvedHref = null;
3528
3723
  collectionCapabilitiesRequestSeq = 0;
3529
3724
  currentAuthoringDocument;
3725
+ selectedRow = null;
3530
3726
  getResourceDiscovery() {
3531
3727
  const assigned = this.resourceDiscovery;
3532
3728
  return assigned ?? (this.resourceDiscoveryInstance ??= this.injector.get(ResourceDiscoveryService));
@@ -3553,15 +3749,41 @@ class PraxisCrudComponent {
3553
3749
  catch { }
3554
3750
  }
3555
3751
  onTableRowClick(event) {
3752
+ const row = this.extractRowFromEvent(event);
3753
+ if (row) {
3754
+ this.selectedRow = row;
3755
+ }
3556
3756
  this.rowClick.emit(event);
3557
3757
  }
3558
3758
  onTableSelectionChange(event) {
3759
+ this.selectedRow = this.extractSelectedRowFromEvent(event);
3559
3760
  this.selectionChange.emit(event);
3560
3761
  }
3762
+ async onBulkAction(event) {
3763
+ const action = String(event?.action || '').trim();
3764
+ if (!action) {
3765
+ return;
3766
+ }
3767
+ const row = this.extractSelectedRowFromBulkAction(event);
3768
+ if (row) {
3769
+ this.selectedRow = row;
3770
+ }
3771
+ await this.onAction(action, row ?? undefined, event);
3772
+ }
3773
+ async onToolbarAction(event) {
3774
+ const action = String(event?.action || '').trim();
3775
+ if (!action)
3776
+ return;
3777
+ const row = this.extractSelectedRowFromToolbarAction(event);
3778
+ if (row)
3779
+ this.selectedRow = row;
3780
+ await this.onAction(action, row ?? undefined, event);
3781
+ }
3561
3782
  ngOnChanges(changes) {
3562
3783
  if (!changes['metadata'] && !changes['context']) {
3563
3784
  return;
3564
3785
  }
3786
+ this.materializedRefreshSequence += 1;
3565
3787
  try {
3566
3788
  const parsed = typeof this.metadata === 'string'
3567
3789
  ? JSON.parse(this.metadata)
@@ -3595,33 +3817,46 @@ class PraxisCrudComponent {
3595
3817
  async onAction(action, row, runtimeEvent) {
3596
3818
  try {
3597
3819
  document.activeElement?.blur();
3598
- let actionMeta = this.resolvedMetadata.actions?.find((candidate) => candidate.action === action);
3820
+ const normalizedAction = this.normalizeCrudActionName(action);
3821
+ const contextualRow = row ?? this.resolveSelectedRowForAction(normalizedAction);
3822
+ let actionMeta = this.resolvedMetadata.actions?.find((candidate) => this.normalizeCrudActionName(candidate.action) === normalizedAction);
3823
+ if (!actionMeta && normalizedAction !== action) {
3824
+ actionMeta = this.resolvedMetadata.actions?.find((candidate) => candidate.action === action);
3825
+ }
3599
3826
  if (!actionMeta) {
3600
- const ctxAction = this.tableCrudContext?.actions?.find((candidate) => candidate.action === action);
3827
+ const ctxAction = this.tableCrudContext?.actions?.find((candidate) => this.normalizeCrudActionName(candidate.action) === normalizedAction);
3601
3828
  if (ctxAction) {
3602
3829
  actionMeta = {
3603
- action: ctxAction.action,
3830
+ action: this.normalizeCrudActionName(ctxAction.action),
3604
3831
  openMode: ctxAction.openMode,
3605
3832
  formId: ctxAction.formId,
3606
3833
  route: ctxAction.route,
3607
3834
  };
3608
3835
  }
3609
3836
  else {
3610
- actionMeta = { action };
3837
+ actionMeta = { action: normalizedAction };
3611
3838
  }
3612
3839
  }
3613
- const effectiveAction = (actionMeta || { action });
3840
+ const effectiveAction = {
3841
+ ...(actionMeta || { action: normalizedAction }),
3842
+ action: this.normalizeCrudActionName(actionMeta?.action ?? normalizedAction),
3843
+ };
3844
+ const resourceIdentity = runtimeEvent?.resourceIdentity ?? null;
3845
+ const handledByDuplicateDraft = await this.tryHandleCanonicalDuplicateDraftAction(effectiveAction, contextualRow);
3846
+ if (handledByDuplicateDraft) {
3847
+ return;
3848
+ }
3614
3849
  if (!this.hasExplicitOpenBinding(effectiveAction)) {
3615
- const openedByDiscovery = await this.tryOpenDiscoveredCrudSurface(action, row, runtimeEvent);
3850
+ const openedByDiscovery = await this.tryOpenDiscoveredCrudSurface(effectiveAction.action, contextualRow, runtimeEvent);
3616
3851
  if (openedByDiscovery) {
3617
3852
  return;
3618
3853
  }
3619
- const handledByWorkflowAction = await this.tryOpenDiscoveredWorkflowAction(action, row, runtimeEvent);
3854
+ const handledByWorkflowAction = await this.tryOpenDiscoveredWorkflowAction(effectiveAction.action, contextualRow, runtimeEvent);
3620
3855
  if (handledByWorkflowAction) {
3621
3856
  return;
3622
3857
  }
3623
3858
  }
3624
- const handledByDelete = await this.tryHandleCanonicalDeleteAction(effectiveAction, row);
3859
+ const handledByDelete = await this.tryHandleCanonicalDeleteAction(effectiveAction, contextualRow);
3625
3860
  if (handledByDelete) {
3626
3861
  return;
3627
3862
  }
@@ -3632,7 +3867,7 @@ class PraxisCrudComponent {
3632
3867
  drawerCloseEmitted = true;
3633
3868
  this.afterClose.emit();
3634
3869
  };
3635
- const { mode, ref } = await this.launcher.launch(effectiveAction, row, this.resolvedMetadata, this.componentKeyId(), {
3870
+ const { mode, ref } = await this.launcher.launch(effectiveAction, contextualRow, this.resolvedMetadata, this.componentKeyId(), {
3636
3871
  onClose: () => emitDrawerClose(),
3637
3872
  onResult: (result) => {
3638
3873
  emitDrawerClose();
@@ -3653,9 +3888,13 @@ class PraxisCrudComponent {
3653
3888
  capabilities: effectiveAction.action === 'create'
3654
3889
  ? this.collectionCapabilities
3655
3890
  : this.collectionCapabilities,
3656
- links: this.resolveCrudRuntimeLinks(effectiveAction.action, row),
3891
+ links: this.resolveCrudRuntimeLinks(effectiveAction.action, contextualRow),
3892
+ resourceIdentity,
3657
3893
  });
3658
3894
  this.afterOpen.emit({ mode, action: effectiveAction.action });
3895
+ if (mode === 'drawer') {
3896
+ return;
3897
+ }
3659
3898
  if (!ref) {
3660
3899
  return;
3661
3900
  }
@@ -3791,6 +4030,9 @@ class PraxisCrudComponent {
3791
4030
  return this.tx('crud.emptyState.primaryAction', 'Configure metadata');
3792
4031
  }
3793
4032
  onConfigureRequested() {
4033
+ if (!this.isCustomizationAvailable()) {
4034
+ return;
4035
+ }
3794
4036
  this.configureRequested.emit();
3795
4037
  this.openCrudAuthoringFromTable();
3796
4038
  }
@@ -3841,9 +4083,140 @@ class PraxisCrudComponent {
3841
4083
  this.refreshTable();
3842
4084
  return true;
3843
4085
  }
4086
+ async tryHandleCanonicalDuplicateDraftAction(action, row) {
4087
+ const normalizedAction = this.normalizeCrudActionName(action.action);
4088
+ if (normalizedAction !== 'duplicate-draft' || !row) {
4089
+ return false;
4090
+ }
4091
+ const duplicateDraftUrl = this.getResourceDiscovery().resolveLinkHref(row['_links'], 'duplicate-draft', this.buildDiscoveryOptions());
4092
+ if (!duplicateDraftUrl) {
4093
+ return false;
4094
+ }
4095
+ const response = await firstValueFrom(this.http.post(duplicateDraftUrl, {}));
4096
+ const draft = this.unwrapRestData(response);
4097
+ if (!this.isRecord(draft)) {
4098
+ throw new Error('Duplicate draft action returned an invalid create draft.');
4099
+ }
4100
+ const configuredCreateAction = this.resolvedMetadata.actions?.find((candidate) => this.normalizeCrudActionName(candidate.action) === 'create') ?? { action: 'create' };
4101
+ const createAction = {
4102
+ ...configuredCreateAction,
4103
+ action: 'create',
4104
+ openMode: configuredCreateAction.openMode ??
4105
+ action.openMode ??
4106
+ this.resolvedMetadata.defaults?.openMode ??
4107
+ 'drawer',
4108
+ form: {
4109
+ ...(configuredCreateAction.form ?? {}),
4110
+ initialValue: { ...draft },
4111
+ },
4112
+ };
4113
+ let drawerCloseEmitted = false;
4114
+ const emitDrawerClose = () => {
4115
+ if (drawerCloseEmitted)
4116
+ return;
4117
+ drawerCloseEmitted = true;
4118
+ this.afterClose.emit();
4119
+ };
4120
+ const handleResult = (result) => {
4121
+ if (result?.type !== 'save') {
4122
+ return;
4123
+ }
4124
+ const data = this.isRecord(result.data) ? result.data : {};
4125
+ const id = data[this.getIdField()];
4126
+ this.afterSave.emit({ id, data });
4127
+ this.refreshTable();
4128
+ };
4129
+ const { mode, ref } = await this.launcher.launch(createAction, undefined, this.resolvedMetadata, this.componentKeyId(), {
4130
+ onClose: () => emitDrawerClose(),
4131
+ onResult: (result) => handleResult(result),
4132
+ }, {
4133
+ capabilities: this.collectionCapabilities,
4134
+ links: this.tableCollectionLinks,
4135
+ });
4136
+ this.afterOpen.emit({ mode, action: normalizedAction });
4137
+ if (mode !== 'drawer' && ref) {
4138
+ ref
4139
+ .afterClosed()
4140
+ .pipe(takeUntilDestroyed(this.destroyRef))
4141
+ .subscribe((result) => {
4142
+ this.afterClose.emit();
4143
+ handleResult(result);
4144
+ });
4145
+ }
4146
+ return true;
4147
+ }
3844
4148
  refreshTable() {
4149
+ if (this.tryRefreshMaterializedLocalData()) {
4150
+ return;
4151
+ }
3845
4152
  this.table.refetch();
3846
4153
  }
4154
+ tryRefreshMaterializedLocalData() {
4155
+ const readUrl = this.resolveMaterializedReadUrl();
4156
+ if (!readUrl || this.resolveResourcePath(this.resolvedMetadata)) {
4157
+ return false;
4158
+ }
4159
+ const refreshSequence = ++this.materializedRefreshSequence;
4160
+ void firstValueFrom(this.getResourceDiscovery().fetchJson(readUrl, {
4161
+ endpointKey: this.resolvedMetadata.resource?.endpointKey,
4162
+ apiUrlEntry: this.resolvedMetadata.resource?.apiUrlEntry,
4163
+ }))
4164
+ .then((response) => {
4165
+ if (!this.isCurrentMaterializedRefresh(refreshSequence, readUrl)) {
4166
+ return;
4167
+ }
4168
+ const data = this.extractCollectionData(this.unwrapRestData(response));
4169
+ if (!data) {
4170
+ this.error.emit({
4171
+ code: 'CRUD_MATERIALIZED_REFRESH_INVALID_COLLECTION',
4172
+ readUrl,
4173
+ });
4174
+ return;
4175
+ }
4176
+ this.resolvedMetadata = {
4177
+ ...this.resolvedMetadata,
4178
+ data,
4179
+ };
4180
+ this.applyResolvedCrudState(this.resolvedMetadata);
4181
+ this.cdr.markForCheck();
4182
+ })
4183
+ .catch((error) => this.error.emit(error));
4184
+ return true;
4185
+ }
4186
+ isCurrentMaterializedRefresh(refreshSequence, readUrl) {
4187
+ return (refreshSequence === this.materializedRefreshSequence &&
4188
+ this.resolveMaterializedReadUrl() === readUrl &&
4189
+ this.resolveResourcePath(this.resolvedMetadata).length === 0);
4190
+ }
4191
+ resolveMaterializedReadUrl() {
4192
+ const materialization = this.context?.['materialization'];
4193
+ return String(materialization?.['readUrl'] || '').trim();
4194
+ }
4195
+ unwrapRestData(value) {
4196
+ if (value && typeof value === 'object' && !Array.isArray(value) && 'data' in value) {
4197
+ return value.data;
4198
+ }
4199
+ return value;
4200
+ }
4201
+ extractCollectionData(data) {
4202
+ if (Array.isArray(data)) {
4203
+ return data;
4204
+ }
4205
+ if (!data || typeof data !== 'object') {
4206
+ return null;
4207
+ }
4208
+ const record = data;
4209
+ if (Array.isArray(record['content'])) {
4210
+ return record['content'];
4211
+ }
4212
+ if (Array.isArray(record['items'])) {
4213
+ return record['items'];
4214
+ }
4215
+ if (Array.isArray(record['data'])) {
4216
+ return record['data'];
4217
+ }
4218
+ return null;
4219
+ }
3847
4220
  emitTableRuntimeConfigSnapshot() {
3848
4221
  const snapshot = this.getCurrentTableConfigSnapshot();
3849
4222
  if (!snapshot)
@@ -3901,17 +4274,67 @@ class PraxisCrudComponent {
3901
4274
  }
3902
4275
  }
3903
4276
  resolveQueryContext(meta) {
4277
+ if (Array.isArray(meta?.data) && this.resolveResourcePath(meta).length === 0) {
4278
+ return null;
4279
+ }
3904
4280
  return this.isRecord(meta?.queryContext) ? meta.queryContext : null;
3905
4281
  }
3906
4282
  resolveFilterCriteria(meta) {
3907
4283
  return this.isRecord(meta?.filterCriteria) ? { ...meta.filterCriteria } : {};
3908
4284
  }
4285
+ extractRowFromEvent(event) {
4286
+ if (!this.isRecord(event)) {
4287
+ return null;
4288
+ }
4289
+ const row = event['row'];
4290
+ return this.isRecord(row) ? row : null;
4291
+ }
4292
+ extractSelectedRowFromEvent(event) {
4293
+ if (!this.isRecord(event)) {
4294
+ return null;
4295
+ }
4296
+ const selection = event['selection'];
4297
+ if (Array.isArray(selection)) {
4298
+ const first = selection[0];
4299
+ return this.isRecord(first) ? first : null;
4300
+ }
4301
+ const row = event['row'];
4302
+ if (this.isRecord(row)) {
4303
+ return row;
4304
+ }
4305
+ return null;
4306
+ }
4307
+ extractSelectedRowFromBulkAction(event) {
4308
+ const first = Array.isArray(event?.rows) ? event.rows[0] : null;
4309
+ return this.isRecord(first) ? first : null;
4310
+ }
4311
+ extractSelectedRowFromToolbarAction(event) {
4312
+ if (this.isRecord(event?.selectedRow))
4313
+ return event.selectedRow;
4314
+ if (this.isRecord(event?.row))
4315
+ return event.row;
4316
+ const first = Array.isArray(event?.selectedRows) ? event.selectedRows[0] : null;
4317
+ return this.isRecord(first) ? first : null;
4318
+ }
4319
+ extractDiscoveryActionConfig(event) {
4320
+ const config = event?.actionConfig;
4321
+ return this.isRecord(config) && typeof config['resourceKey'] === 'string'
4322
+ ? config
4323
+ : null;
4324
+ }
4325
+ resolveSelectedRowForAction(action) {
4326
+ const normalized = String(action || '').trim().toLowerCase();
4327
+ if (!normalized || normalized === 'create' || normalized === 'add' || normalized === 'new') {
4328
+ return undefined;
4329
+ }
4330
+ return this.selectedRow ?? undefined;
4331
+ }
3909
4332
  async tryOpenDiscoveredCrudSurface(action, row, runtimeEvent) {
3910
4333
  const normalizedAction = String(action || '').trim().toLowerCase();
3911
4334
  if (!this.surfaceService) {
3912
4335
  return false;
3913
4336
  }
3914
- const providedSurface = this.resolveProvidedSurface(normalizedAction, runtimeEvent?.actionConfig);
4337
+ const providedSurface = this.resolveProvidedSurface(normalizedAction, this.extractDiscoveryActionConfig(runtimeEvent));
3915
4338
  if (!providedSurface && !this.isDiscoveryManagedCrudAction(normalizedAction)) {
3916
4339
  return false;
3917
4340
  }
@@ -3942,7 +4365,7 @@ class PraxisCrudComponent {
3942
4365
  }
3943
4366
  let openPromise;
3944
4367
  try {
3945
- openPromise = Promise.resolve(this.surfaceService.open(payload, {
4368
+ const surfaceContext = {
3946
4369
  sourceId: this.componentKeyId() || undefined,
3947
4370
  payload: { action: normalizedAction, row: row ?? null },
3948
4371
  meta: {
@@ -3959,7 +4382,9 @@ class PraxisCrudComponent {
3959
4382
  resourcePath,
3960
4383
  },
3961
4384
  },
3962
- }));
4385
+ };
4386
+ const handledInline = await this.surfaceOutlets.tryActivate(payload, surfaceContext);
4387
+ openPromise = handledInline ? Promise.resolve(undefined) : Promise.resolve(this.surfaceService.open(payload, surfaceContext));
3963
4388
  }
3964
4389
  catch {
3965
4390
  return false;
@@ -3996,10 +4421,10 @@ class PraxisCrudComponent {
3996
4421
  if (!normalizedAction || this.isDiscoveryManagedCrudAction(normalizedAction) || !this.surfaceService) {
3997
4422
  return false;
3998
4423
  }
3999
- const providedAction = this.resolveProvidedWorkflowAction(normalizedAction, runtimeEvent?.actionConfig);
4424
+ const providedAction = this.resolveProvidedWorkflowAction(normalizedAction, this.extractDiscoveryActionConfig(runtimeEvent));
4000
4425
  const catalog = providedAction ? null : await this.resolveDiscoveredActionCatalog(row);
4001
4426
  const discoveredAction = providedAction || this.selectDiscoveredWorkflowAction(normalizedAction, catalog?.actions || []);
4002
- const resourcePath = String(this.resolveResourcePath(this.resolvedMetadata) || catalog?.resourcePath || '').trim();
4427
+ const resourcePath = String(catalog?.resourcePath || this.resolveResourcePath(this.resolvedMetadata) || '').trim();
4003
4428
  if (!discoveredAction || !resourcePath) {
4004
4429
  return false;
4005
4430
  }
@@ -4083,24 +4508,26 @@ class PraxisCrudComponent {
4083
4508
  if (!candidate || typeof candidate !== 'object') {
4084
4509
  return null;
4085
4510
  }
4086
- const normalizedId = String(candidate.id || '').trim().toLowerCase();
4511
+ const catalogCandidate = candidate;
4512
+ const normalizedId = String(catalogCandidate.id || '').trim().toLowerCase();
4087
4513
  if (!normalizedId || normalizedId !== action) {
4088
4514
  return null;
4089
4515
  }
4090
- if (!candidate.operationId || !candidate.path || !candidate.method) {
4516
+ if (!catalogCandidate.operationId || !catalogCandidate.path || !catalogCandidate.method) {
4091
4517
  return null;
4092
4518
  }
4093
- return candidate;
4519
+ return catalogCandidate;
4094
4520
  }
4095
4521
  resolveProvidedSurface(action, candidate) {
4096
4522
  if (!candidate || typeof candidate !== 'object') {
4097
4523
  return null;
4098
4524
  }
4099
- const normalizedId = String(candidate.id || '').trim().toLowerCase();
4525
+ const catalogCandidate = candidate;
4526
+ const normalizedId = String(catalogCandidate.id || '').trim().toLowerCase();
4100
4527
  if (!normalizedId || normalizedId !== action) {
4101
4528
  return null;
4102
4529
  }
4103
- const surface = candidate;
4530
+ const surface = catalogCandidate;
4104
4531
  if (!surface.kind || !surface.scope || !surface.path || !surface.method) {
4105
4532
  return null;
4106
4533
  }
@@ -4222,12 +4649,19 @@ class PraxisCrudComponent {
4222
4649
  }
4223
4650
  return cfg.toolbar;
4224
4651
  };
4225
- const hasToolbarAdd = (cfg.toolbar?.actions || []).some((action) => this.isAddLike(action));
4652
+ const hasToolbarAdd = (cfg.toolbar?.actions || []).some((action) => this.shouldCanonicalizeToolbarCreateAction(action));
4226
4653
  const addAction = this.resolveCreateToolbarAction(meta, capabilities);
4227
4654
  if (addAction) {
4228
4655
  if (hasToolbarAdd) {
4229
4656
  const toolbar = ensureToolbar();
4230
4657
  toolbar.visible = true;
4658
+ toolbar.actions = (toolbar.actions || []).map((action) => this.shouldCanonicalizeToolbarCreateAction(action)
4659
+ ? {
4660
+ ...action,
4661
+ action: 'create',
4662
+ }
4663
+ : action);
4664
+ changed = true;
4231
4665
  }
4232
4666
  else {
4233
4667
  const toolbar = ensureToolbar();
@@ -4246,6 +4680,7 @@ class PraxisCrudComponent {
4246
4680
  });
4247
4681
  }
4248
4682
  changed = true;
4683
+ changed = this.ensureCanonicalCollectionEmptyState(cfg, meta, addAction) || changed;
4249
4684
  }
4250
4685
  const discoveredToolbarActions = this.resolveCollectionWorkflowToolbarActions(capabilities);
4251
4686
  if (discoveredToolbarActions.length) {
@@ -4362,6 +4797,28 @@ class PraxisCrudComponent {
4362
4797
  }
4363
4798
  return false;
4364
4799
  }
4800
+ normalizeCrudActionName(action) {
4801
+ const raw = String(action || '').trim();
4802
+ const normalized = raw.toLowerCase();
4803
+ return this.isCreateActionAliasName(normalized) ? 'create' : raw;
4804
+ }
4805
+ shouldCanonicalizeToolbarCreateAction(action) {
4806
+ if (!action)
4807
+ return false;
4808
+ const normalize = (value) => String(value || '').trim().toLowerCase();
4809
+ const explicitId = normalize(action.action || action.id || action.code || action.key || action.name || action.type);
4810
+ if (this.isCreateActionAliasName(explicitId))
4811
+ return true;
4812
+ if (explicitId)
4813
+ return false;
4814
+ const icon = normalize(action.icon);
4815
+ const label = normalize(action.label);
4816
+ return ((icon === 'add' || icon === 'add_circle' || icon === 'add_box') &&
4817
+ (label === 'adicionar' || label === 'novo' || label === 'criar' || label === 'incluir'));
4818
+ }
4819
+ isCreateActionAliasName(actionName) {
4820
+ return ['create', 'add', 'novo', 'new', 'incluir', 'inserir'].includes(actionName);
4821
+ }
4365
4822
  buildTableCrudContext(meta, capabilities) {
4366
4823
  if (!meta)
4367
4824
  return undefined;
@@ -4384,6 +4841,9 @@ class PraxisCrudComponent {
4384
4841
  };
4385
4842
  }
4386
4843
  openCrudAuthoringFromTable = () => {
4844
+ if (!this.isCustomizationAvailable()) {
4845
+ return;
4846
+ }
4387
4847
  const seed = this.currentAuthoringDocument
4388
4848
  || createCrudAuthoringDocument({ metadata: this.resolvedMetadata });
4389
4849
  const ref = openCrudMetadataEditor(this.settingsPanel, {
@@ -4392,13 +4852,33 @@ class PraxisCrudComponent {
4392
4852
  title: this.tx('crud.authoring.title', 'Configurações do CRUD'),
4393
4853
  titleIcon: 'table_chart',
4394
4854
  });
4855
+ this.activeAuthoringPanelRef = ref;
4395
4856
  ref.applied$
4396
4857
  .pipe(takeUntilDestroyed(this.destroyRef))
4397
4858
  .subscribe((payload) => this.applyCrudAuthoringPayload(payload, 'applied'));
4398
4859
  ref.saved$
4399
4860
  .pipe(takeUntilDestroyed(this.destroyRef))
4400
4861
  .subscribe((payload) => this.applyCrudAuthoringPayload(payload, 'saved'));
4862
+ ref.closed$
4863
+ .pipe(takeUntilDestroyed(this.destroyRef))
4864
+ .subscribe(() => {
4865
+ if (this.activeAuthoringPanelRef === ref) {
4866
+ this.activeAuthoringPanelRef = undefined;
4867
+ }
4868
+ });
4401
4869
  };
4870
+ isCustomizationAvailable() {
4871
+ if (!this.enableCustomization) {
4872
+ return false;
4873
+ }
4874
+ const requiredCapability = String(this.authoringCapability || '').trim();
4875
+ if (!requiredCapability) {
4876
+ return true;
4877
+ }
4878
+ const capabilities = this.enterpriseRuntimeContext?.snapshot?.capabilities;
4879
+ return Array.isArray(capabilities)
4880
+ && capabilities.some((capability) => capability === requiredCapability);
4881
+ }
4402
4882
  applyCrudAuthoringPayload(payload, eventName) {
4403
4883
  const next = parseLegacyOrCrudDocument(payload);
4404
4884
  this.currentAuthoringDocument = next;
@@ -4615,6 +5095,48 @@ class PraxisCrudComponent {
4615
5095
  return action;
4616
5096
  }
4617
5097
  }
5098
+ ensureCanonicalCollectionEmptyState(config, metadata, createAction) {
5099
+ const behavior = config.behavior || {};
5100
+ if (behavior.emptyState) {
5101
+ return false;
5102
+ }
5103
+ const resourceLabel = String(metadata.resource?.label
5104
+ || metadata.resource?.title
5105
+ || metadata.resource?.path
5106
+ || '').trim();
5107
+ const title = resourceLabel
5108
+ ? this.txWithParams('crud.table.emptyState.initial.title', 'Sem registros em {label}', { label: resourceLabel })
5109
+ : this.tx('crud.table.emptyState.initial.titleFallback', 'Nenhum registro disponível.');
5110
+ const actionId = String(createAction.action || createAction.id || 'create').trim() || 'create';
5111
+ const actionLabel = String(createAction.label || this.getCrudActionLabel('create')).trim();
5112
+ const actionIcon = String(createAction.icon || 'add').trim();
5113
+ config.behavior = {
5114
+ ...behavior,
5115
+ emptyState: {
5116
+ message: '',
5117
+ contexts: {
5118
+ initial: {
5119
+ title,
5120
+ description: this.tx('crud.table.emptyState.initial.descriptionWithAction', 'Use a ação principal para adicionar o primeiro registro quando houver informações para cadastrar.'),
5121
+ actions: [
5122
+ {
5123
+ label: actionLabel,
5124
+ action: actionId,
5125
+ icon: actionIcon,
5126
+ primary: true,
5127
+ },
5128
+ ],
5129
+ },
5130
+ filtered: {
5131
+ title: this.tx('crud.table.emptyState.filtered.title', 'Nenhum resultado encontrado.'),
5132
+ description: this.tx('crud.table.emptyState.filtered.description', 'Revise os filtros ou ajuste o termo de busca.'),
5133
+ actions: [],
5134
+ },
5135
+ },
5136
+ },
5137
+ };
5138
+ return true;
5139
+ }
4618
5140
  tx(key, fallback) {
4619
5141
  try {
4620
5142
  return translateCrudRuntimeText(this.i18n, key, fallback);
@@ -4623,8 +5145,19 @@ class PraxisCrudComponent {
4623
5145
  return fallback;
4624
5146
  }
4625
5147
  }
5148
+ txWithParams(key, fallback, params) {
5149
+ try {
5150
+ return this.interpolateTranslationParams(translateCrudRuntimeText(this.i18n, key, fallback, params), params);
5151
+ }
5152
+ catch {
5153
+ return this.interpolateTranslationParams(fallback, params);
5154
+ }
5155
+ }
5156
+ interpolateTranslationParams(text, params) {
5157
+ return Object.entries(params).reduce((current, [name, value]) => current.split(`{${name}}`).join(value), text);
5158
+ }
4626
5159
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisCrudComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4627
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisCrudComponent, isStandalone: true, selector: "praxis-crud", inputs: { metadata: "metadata", crudId: "crudId", componentInstanceId: "componentInstanceId", context: "context", enableCustomization: "enableCustomization" }, outputs: { configureRequested: "configureRequested", afterOpen: "afterOpen", afterClose: "afterClose", afterSave: "afterSave", afterDelete: "afterDelete", error: "error", rowClick: "rowClick", selectionChange: "selectionChange", tableRuntimeConfigChange: "tableRuntimeConfigChange", crudAuthoringDocumentApplied: "crudAuthoringDocumentApplied", crudAuthoringDocumentSaved: "crudAuthoringDocumentSaved" }, providers: [
5160
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisCrudComponent, isStandalone: true, selector: "praxis-crud", inputs: { metadata: "metadata", crudId: "crudId", componentInstanceId: "componentInstanceId", context: "context", enableCustomization: "enableCustomization", authoringCapability: "authoringCapability" }, outputs: { configureRequested: "configureRequested", afterOpen: "afterOpen", afterClose: "afterClose", afterSave: "afterSave", afterDelete: "afterDelete", error: "error", rowClick: "rowClick", selectionChange: "selectionChange", tableRuntimeConfigChange: "tableRuntimeConfigChange", crudAuthoringDocumentApplied: "crudAuthoringDocumentApplied", crudAuthoringDocumentSaved: "crudAuthoringDocumentSaved" }, providers: [
4628
5161
  providePraxisI18nConfig(RESOURCE_DISCOVERY_I18N_CONFIG),
4629
5162
  providePraxisI18nConfig(PRAXIS_CRUD_RUNTIME_I18N_CONFIG),
4630
5163
  ], viewQueries: [{ propertyName: "table", first: true, predicate: PraxisTable, descendants: true }], usesOnChanges: true, ngImport: i0, template: `
@@ -4638,17 +5171,19 @@ class PraxisCrudComponent {
4638
5171
  [tableId]="crudId || 'default'"
4639
5172
  [crudContext]="tableCrudContext"
4640
5173
  [enableCustomization]="enableCustomization"
5174
+ [authoringCapability]="authoringCapability"
4641
5175
  (rowClick)="onTableRowClick($event)"
4642
5176
  (selectionChange)="onTableSelectionChange($event)"
4643
5177
  (rowAction)="onAction($event.action, $event.row, $event)"
4644
- (toolbarAction)="onAction($event.action)"
5178
+ (toolbarAction)="onToolbarAction($event)"
5179
+ (bulkAction)="onBulkAction($event)"
4645
5180
  (collectionLinksChange)="onCollectionLinksChange($event)"
4646
5181
  (reset)="onResetPreferences()"
4647
5182
  (metadataChange)="onTableMetadataChange()"
4648
5183
  (loadingStateChange)="onTableLoadingStateChange($event)"
4649
5184
  ></praxis-table>
4650
5185
  } @else {
4651
- @if (enableCustomization) {
5186
+ @if (isCustomizationAvailable()) {
4652
5187
  <praxis-empty-state-card
4653
5188
  icon="table_rows"
4654
5189
  [title]="getEmptyStateTitle()"
@@ -4657,7 +5192,7 @@ class PraxisCrudComponent {
4657
5192
  />
4658
5193
  }
4659
5194
  }
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"] }] });
5195
+ `, isInline: true, styles: [":host{display:block;width:100%;min-width:0;max-width:100%}\n"], dependencies: [{ kind: "component", type: PraxisTable, selector: "praxis-table", inputs: ["config", "resourcePath", "data", "tableId", "componentInstanceId", "configPersistenceStrategy", "title", "subtitle", "icon", "autoDelete", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "crudContext", "filterCriteria", "queryContext", "aiContext", "aiAssistantVoiceInputMode", "aiAssistantVoiceLanguage", "horizontalScroll", "enableCustomization", "authoringCapability", "dense"], outputs: ["rowClick", "widgetEvent", "resourceEvent", "rowDoubleClick", "rowExpansionChange", "rowAction", "toolbarAction", "bulkAction", "exportAction", "columnReorder", "columnReorderAttempt", "columnResize", "beforeDelete", "afterDelete", "deleteError", "beforeBulkDelete", "afterBulkDelete", "bulkDeleteError", "schemaStatusChange", "configChange", "metadataChange", "loadingStateChange", "collectionLinksChange", "selectionChange"] }, { kind: "component", type: EmptyStateCardComponent, selector: "praxis-empty-state-card", inputs: ["icon", "title", "description", "primaryAction", "secondaryActions", "inline", "tone", "variant", "alignment", "density", "iconContainer"] }] });
4661
5196
  }
4662
5197
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisCrudComponent, decorators: [{
4663
5198
  type: Component,
@@ -4675,17 +5210,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
4675
5210
  [tableId]="crudId || 'default'"
4676
5211
  [crudContext]="tableCrudContext"
4677
5212
  [enableCustomization]="enableCustomization"
5213
+ [authoringCapability]="authoringCapability"
4678
5214
  (rowClick)="onTableRowClick($event)"
4679
5215
  (selectionChange)="onTableSelectionChange($event)"
4680
5216
  (rowAction)="onAction($event.action, $event.row, $event)"
4681
- (toolbarAction)="onAction($event.action)"
5217
+ (toolbarAction)="onToolbarAction($event)"
5218
+ (bulkAction)="onBulkAction($event)"
4682
5219
  (collectionLinksChange)="onCollectionLinksChange($event)"
4683
5220
  (reset)="onResetPreferences()"
4684
5221
  (metadataChange)="onTableMetadataChange()"
4685
5222
  (loadingStateChange)="onTableLoadingStateChange($event)"
4686
5223
  ></praxis-table>
4687
5224
  } @else {
4688
- @if (enableCustomization) {
5225
+ @if (isCustomizationAvailable()) {
4689
5226
  <praxis-empty-state-card
4690
5227
  icon="table_rows"
4691
5228
  [title]="getEmptyStateTitle()"
@@ -4695,7 +5232,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
4695
5232
  }
4696
5233
  }
4697
5234
  `, styles: [":host{display:block;width:100%;min-width:0;max-width:100%}\n"] }]
4698
- }], propDecorators: { metadata: [{
5235
+ }], ctorParameters: () => [], propDecorators: { metadata: [{
4699
5236
  type: Input,
4700
5237
  args: [{ required: true }]
4701
5238
  }], crudId: [{
@@ -4707,6 +5244,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
4707
5244
  type: Input
4708
5245
  }], enableCustomization: [{
4709
5246
  type: Input
5247
+ }], authoringCapability: [{
5248
+ type: Input
4710
5249
  }], configureRequested: [{
4711
5250
  type: Output
4712
5251
  }], afterOpen: [{
@@ -4734,6 +5273,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
4734
5273
  args: [PraxisTable]
4735
5274
  }] } });
4736
5275
 
5276
+ function isCrudDebugEnabled() {
5277
+ try {
5278
+ if (globalThis.__PRAXIS_DEBUG_CRUD__) {
5279
+ return true;
5280
+ }
5281
+ if (typeof localStorage !== 'undefined') {
5282
+ return localStorage.getItem('praxis.debug.crud') === 'true';
5283
+ }
5284
+ }
5285
+ catch { }
5286
+ return false;
5287
+ }
5288
+ function debugCrudHost(message, data) {
5289
+ if (!isCrudDebugEnabled()) {
5290
+ return;
5291
+ }
5292
+ try {
5293
+ console.debug(message, data);
5294
+ }
5295
+ catch { }
5296
+ }
4737
5297
  class DynamicFormDialogHostComponent {
4738
5298
  dialogRef;
4739
5299
  data;
@@ -4742,21 +5302,28 @@ class DynamicFormDialogHostComponent {
4742
5302
  configStorage;
4743
5303
  formComp;
4744
5304
  modal = {};
5305
+ presentation = 'modal';
4745
5306
  maximized = false;
4746
5307
  initialSize = {};
5308
+ initialPosition;
4747
5309
  rememberState = false;
4748
5310
  stateKey;
4749
5311
  backDefaults = {};
4750
5312
  destroyRef = inject(DestroyRef);
5313
+ i18n = inject(PraxisI18nService);
4751
5314
  resourcePath;
4752
5315
  resourceId;
4753
5316
  initialValue;
5317
+ resourceIdentity = null;
4754
5318
  schemaUrl;
4755
5319
  submitUrl;
4756
5320
  submitMethod;
4757
5321
  apiEndpointKey;
4758
5322
  apiUrlEntry;
5323
+ layoutPolicy;
5324
+ formConfig = {};
4759
5325
  formActions;
5326
+ formConfigPersistenceStrategy = 'input-first';
4760
5327
  mode = 'create';
4761
5328
  backConfig;
4762
5329
  idField = 'id';
@@ -4766,6 +5333,7 @@ class DynamicFormDialogHostComponent {
4766
5333
  closeLabel: 'Fechar diálogo',
4767
5334
  maximizeLabel: 'Maximizar',
4768
5335
  restoreLabel: 'Restaurar',
5336
+ recordContextLabel: translateCrudRuntimeText(this.i18n, 'crud.dialog.recordContextLabel', 'Registro em edição'),
4769
5337
  discardTitle: 'Descartar alterações?',
4770
5338
  discardMessage: 'Você tem alterações não salvas. Deseja fechar assim mesmo?',
4771
5339
  discardConfirm: 'Descartar',
@@ -4778,6 +5346,7 @@ class DynamicFormDialogHostComponent {
4778
5346
  this.crud = crud;
4779
5347
  this.configStorage = configStorage;
4780
5348
  this.dialogRef.disableClose = true;
5349
+ this.presentation = this.data.presentation === 'drawer' ? 'drawer' : 'modal';
4781
5350
  // i18n
4782
5351
  this.texts = {
4783
5352
  ...this.texts,
@@ -4788,6 +5357,9 @@ class DynamicFormDialogHostComponent {
4788
5357
  canMaximize: true,
4789
5358
  ...(this.data.metadata?.defaults?.modal || {}),
4790
5359
  };
5360
+ this.initialPosition = this.presentation === 'drawer'
5361
+ ? (this.data.dialogPosition ?? this.modal.position ?? { right: '0', top: '0' })
5362
+ : undefined;
4791
5363
  this.rememberState = !!this.modal.rememberLastState;
4792
5364
  this.stateKey = this.data.action?.formId
4793
5365
  ? `crud-dialog-state:${this.data.action.formId}`
@@ -4812,12 +5384,17 @@ class DynamicFormDialogHostComponent {
4812
5384
  this.apiUrlEntry = this.data.inputs?.['apiUrlEntry'] ?? null;
4813
5385
  const act = this.data.action?.action;
4814
5386
  this.mode = act === 'edit' ? 'edit' : act === 'view' ? 'view' : 'create';
5387
+ this.resourceIdentity = this.mode === 'edit' && this.data.resourceIdentity
5388
+ ? this.data.resourceIdentity
5389
+ : null;
5390
+ this.formConfig = this.resolveFormConfig();
5391
+ this.layoutPolicy = this.resolveLayoutPolicy();
4815
5392
  this.formActions = this.resolveFormActions();
4816
5393
  // Back config: defaults from metadata/action, overridden by saved per-form config
4817
5394
  const defaults = (this.data.action?.back || this.data.metadata?.defaults?.back) || {};
4818
5395
  this.backDefaults = defaults;
4819
5396
  this.backConfig = { ...defaults };
4820
- console.debug('[CRUD:Host] constructed', {
5397
+ debugCrudHost('[CRUD:Host] constructed', {
4821
5398
  action: this.data?.action,
4822
5399
  resourcePath: this.resourcePath,
4823
5400
  resourceId: this.resourceId,
@@ -4857,6 +5434,7 @@ class DynamicFormDialogHostComponent {
4857
5434
  'apiEndpointKey',
4858
5435
  'apiUrlEntry',
4859
5436
  'initialValue',
5437
+ 'layoutPolicy',
4860
5438
  ]);
4861
5439
  const explicit = inputs['initialValue'] && typeof inputs['initialValue'] === 'object'
4862
5440
  ? { ...inputs['initialValue'] }
@@ -4869,10 +5447,50 @@ class DynamicFormDialogHostComponent {
4869
5447
  }
4870
5448
  return Object.keys(explicit).length ? explicit : null;
4871
5449
  }
5450
+ resolveFormConfig() {
5451
+ const config = this.data.metadata?.form;
5452
+ return config && typeof config === 'object' ? config : {};
5453
+ }
5454
+ get dialogTitle() {
5455
+ const actionLabel = this.resolveActionDialogTitle();
5456
+ return actionLabel ||
5457
+ stringOrUndefined(this.formConfig['title']) ||
5458
+ deriveModeTitle(this.mode, this.resolveResourceTitle()) ||
5459
+ this.texts.title;
5460
+ }
5461
+ resolveActionDialogTitle() {
5462
+ const actionLabel = stringOrUndefined(this.data.action?.label);
5463
+ if (!actionLabel) {
5464
+ return undefined;
5465
+ }
5466
+ const resource = this.data.metadata?.resource ?? {};
5467
+ const table = this.data.metadata?.table ?? {};
5468
+ const businessTitle = stringOrUndefined(resource.formTitle ??
5469
+ resource.title ??
5470
+ resource.label ??
5471
+ table.title ??
5472
+ table.label);
5473
+ const routeFallbackTitle = deriveModeTitle(this.mode, titleFromResourcePath(this.resourcePath));
5474
+ return businessTitle && routeFallbackTitle && sameTitle(actionLabel, routeFallbackTitle)
5475
+ ? undefined
5476
+ : actionLabel;
5477
+ }
5478
+ resolveResourceTitle() {
5479
+ const resource = this.data.metadata?.resource ?? {};
5480
+ const table = this.data.metadata?.table ?? {};
5481
+ return stringOrUndefined(resource.formTitle ??
5482
+ resource.title ??
5483
+ resource.label ??
5484
+ table.title ??
5485
+ table.label) || titleFromResourcePath(this.resourcePath);
5486
+ }
4872
5487
  resolveFormActions() {
4873
5488
  if (this.mode === 'view') {
4874
5489
  return undefined;
4875
5490
  }
5491
+ if (this.formConfig?.actions) {
5492
+ return undefined;
5493
+ }
4876
5494
  const submitLabel = this.resolveSubmitLabel();
4877
5495
  if (!submitLabel) {
4878
5496
  return undefined;
@@ -4890,6 +5508,15 @@ class DynamicFormDialogHostComponent {
4890
5508
  },
4891
5509
  };
4892
5510
  }
5511
+ resolveLayoutPolicy() {
5512
+ const explicit = this.data.inputs?.['layoutPolicy'] ??
5513
+ this.data.action?.form?.layoutPolicy ??
5514
+ this.data.action?.layoutPolicy;
5515
+ if (explicit && typeof explicit === 'object') {
5516
+ return explicit;
5517
+ }
5518
+ return null;
5519
+ }
4893
5520
  resolveSubmitLabel() {
4894
5521
  const action = this.data.action ?? {};
4895
5522
  const explicit = stringOrUndefined(action.form?.submitLabel ??
@@ -4962,13 +5589,17 @@ class DynamicFormDialogHostComponent {
4962
5589
  ref
4963
5590
  .afterClosed()
4964
5591
  .pipe(filter((confirmed) => !!confirmed), takeUntilDestroyed(this.destroyRef))
4965
- .subscribe(() => this.dialogRef.close());
5592
+ .subscribe(() => this.dialogRef.close({ type: 'close' }));
4966
5593
  }
4967
5594
  else {
4968
- this.dialogRef.close();
5595
+ this.dialogRef.close({ type: 'close' });
4969
5596
  }
4970
5597
  }
4971
5598
  toggleMaximize(initial = false) {
5599
+ const pane = this.resolveOverlayPane();
5600
+ if (!initial && !this.maximized && pane) {
5601
+ this.captureRenderedInitialSize(pane);
5602
+ }
4972
5603
  this.maximized = initial ? true : !this.maximized;
4973
5604
  const gap = this.maximized ? this.modal.edgeGap ?? 8 : undefined;
4974
5605
  const width = this.maximized
@@ -4978,19 +5609,40 @@ class DynamicFormDialogHostComponent {
4978
5609
  ? `calc(100dvh - ${2 * (gap ?? 0)}px)`
4979
5610
  : this.initialSize.height;
4980
5611
  this.dialogRef.updateSize(width, height);
4981
- this.dialogRef.updatePosition();
4982
- const pane = this.dialogRef
4983
- ?._containerInstance?._elementRef?.nativeElement?.parentElement;
5612
+ this.updateHostPosition();
4984
5613
  if (pane && pane.classList.contains('pfx-dialog-pane')) {
4985
- pane.style.margin = this.maximized ? `${gap}px` : '';
5614
+ if (this.presentation === 'drawer') {
5615
+ pane.classList.toggle('pfx-drawer-maximized', this.maximized);
5616
+ if (this.maximized) {
5617
+ pane.style.setProperty('--pfx-drawer-edge-gap', `${gap ?? 0}px`);
5618
+ }
5619
+ else {
5620
+ pane.style.removeProperty('--pfx-drawer-edge-gap');
5621
+ }
5622
+ }
5623
+ else {
5624
+ pane.style.margin = this.maximized ? `${gap}px` : '';
5625
+ }
4986
5626
  }
4987
5627
  this.saveState();
4988
5628
  }
5629
+ resolveOverlayPane() {
5630
+ return this.dialogRef
5631
+ ?._containerInstance?._elementRef?.nativeElement?.parentElement;
5632
+ }
5633
+ captureRenderedInitialSize(pane) {
5634
+ const style = getComputedStyle(pane);
5635
+ const renderedWidth = Number.parseFloat(style.width) > 0 ? style.width : undefined;
5636
+ const renderedHeight = Number.parseFloat(style.height) > 0 ? style.height : undefined;
5637
+ this.initialSize = {
5638
+ width: this.initialSize.width ?? renderedWidth,
5639
+ height: this.initialSize.height ?? renderedHeight,
5640
+ };
5641
+ }
4989
5642
  saveState() {
4990
5643
  if (!this.rememberState || !this.stateKey)
4991
5644
  return;
4992
- const pane = this.dialogRef
4993
- ?._containerInstance?._elementRef?.nativeElement?.parentElement;
5645
+ const pane = this.resolveOverlayPane();
4994
5646
  const style = pane ? getComputedStyle(pane) : null;
4995
5647
  const currentWidth = style?.width || this.initialSize.width;
4996
5648
  const currentHeight = style?.height || this.initialSize.height;
@@ -5008,7 +5660,7 @@ class DynamicFormDialogHostComponent {
5008
5660
  width: saved?.width ?? this.modal.width,
5009
5661
  height: saved?.height ?? this.modal.height,
5010
5662
  };
5011
- console.debug('[CRUD:Host] ngOnInit', {
5663
+ debugCrudHost('[CRUD:Host] ngOnInit', {
5012
5664
  initialSize: this.initialSize,
5013
5665
  startMaximized: this.modal.startMaximized,
5014
5666
  fullscreenBreakpoint: this.modal.fullscreenBreakpoint,
@@ -5028,52 +5680,89 @@ class DynamicFormDialogHostComponent {
5028
5680
  }
5029
5681
  else if (this.initialSize.width || this.initialSize.height) {
5030
5682
  this.dialogRef.updateSize(this.initialSize.width, this.initialSize.height);
5683
+ this.updateHostPosition();
5684
+ }
5685
+ }
5686
+ updateHostPosition() {
5687
+ if (this.presentation !== 'drawer') {
5031
5688
  this.dialogRef.updatePosition();
5689
+ return;
5032
5690
  }
5691
+ this.dialogRef.updatePosition(this.maximized
5692
+ ? this.resolveMaximizedDrawerPosition()
5693
+ : this.initialPosition);
5694
+ }
5695
+ resolveMaximizedDrawerPosition() {
5696
+ const position = this.initialPosition ?? { right: '0', top: '0' };
5697
+ const offset = `${this.modal.edgeGap ?? 8}px`;
5698
+ const horizontal = position.left != null
5699
+ ? { left: offset }
5700
+ : { right: offset };
5701
+ const vertical = position.bottom != null && position.top == null
5702
+ ? { bottom: offset }
5703
+ : { top: offset };
5704
+ return { ...horizontal, ...vertical };
5033
5705
  }
5034
5706
  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: `
5707
+ 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: [
5708
+ GenericCrudService,
5709
+ providePraxisI18nConfig(PRAXIS_CRUD_RUNTIME_I18N_CONFIG),
5710
+ ], viewQueries: [{ propertyName: "formComp", first: true, predicate: PraxisDynamicForm, descendants: true }], ngImport: i0, template: `
5036
5711
  <div mat-dialog-title class="dialog-header">
5037
5712
  <h2 id="crudDialogTitle" class="dialog-title">
5038
- {{ data.action?.label || texts.title }}
5713
+ {{ dialogTitle }}
5039
5714
  </h2>
5040
5715
  <span class="spacer"></span>
5041
5716
  @if (modal.canMaximize) {
5042
5717
  <button
5043
- mat-icon-button
5718
+ [praxisIconButton]="maximized ? 'close_fullscreen' : 'open_in_full'"
5719
+ size="compact"
5044
5720
  type="button"
5045
5721
  (click)="toggleMaximize()"
5046
5722
  [attr.aria-label]="maximized ? texts.restoreLabel : texts.maximizeLabel"
5047
- >
5048
- <mat-icon [praxisIcon]="maximized ? 'close_fullscreen' : 'open_in_full'"></mat-icon>
5049
- </button>
5723
+ ></button>
5050
5724
  }
5051
5725
  <button
5052
- mat-icon-button
5726
+ praxisIconButton="close"
5727
+ size="compact"
5053
5728
  type="button"
5054
5729
  (click)="onCancel()"
5055
5730
  [attr.aria-label]="texts.closeLabel"
5056
5731
  cdkFocusInitial
5057
- >
5058
- <mat-icon [praxisIcon]="'close'"></mat-icon>
5059
- </button>
5732
+ ></button>
5060
5733
  </div>
5061
5734
 
5062
5735
  <mat-dialog-content
5063
5736
  class="dialog-content"
5064
5737
  aria-labelledby="crudDialogTitle"
5065
5738
  >
5739
+ @if (resourceIdentity) {
5740
+ <div class="crud-resource-identity" data-testid="crud-resource-identity">
5741
+ <span class="crud-resource-identity__label" aria-hidden="true">
5742
+ {{ texts.recordContextLabel }}
5743
+ </span>
5744
+ <praxis-resource-identity
5745
+ [identity]="resourceIdentity"
5746
+ density="compact"
5747
+ [showKeyLabel]="true"
5748
+ [ariaLabel]="texts.recordContextLabel"
5749
+ ></praxis-resource-identity>
5750
+ </div>
5751
+ }
5066
5752
  <praxis-dynamic-form
5067
5753
  [formId]="data.action?.formId"
5068
5754
  [resourcePath]="resourcePath"
5069
5755
  [resourceId]="resourceId"
5070
5756
  [initialValue]="initialValue"
5071
5757
  [mode]="mode"
5758
+ [config]="formConfig"
5072
5759
  [schemaUrl]="schemaUrl"
5073
5760
  [submitUrl]="submitUrl"
5074
5761
  [submitMethod]="submitMethod"
5075
5762
  [apiEndpointKey]="apiEndpointKey"
5076
5763
  [apiUrlEntry]="apiUrlEntry"
5764
+ [configPersistenceStrategy]="formConfigPersistenceStrategy"
5765
+ [layoutPolicy]="layoutPolicy"
5077
5766
  [presentationModeGlobal]="mode === 'view' ? true : null"
5078
5767
  [backConfig]="backConfig"
5079
5768
  [actions]="formActions"
@@ -5081,61 +5770,79 @@ class DynamicFormDialogHostComponent {
5081
5770
  (formCancel)="onCancel()"
5082
5771
  ></praxis-dynamic-form>
5083
5772
  </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"] }] });
5773
+ `, 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 .crud-resource-identity{position:sticky;top:0;z-index:1;display:grid;gap:.25rem;margin-block-end:var(--dlg-pad);padding-block:.5rem .75rem;background:var(--pfx-form-surface, var(--md-sys-color-surface));border-bottom:1px solid var(--md-sys-color-outline-variant)}praxis-dynamic-form-dialog-host .crud-resource-identity__label{color:var(--md-sys-color-on-surface-variant);font:var(--md-sys-typescale-label-medium, 500 .75rem/1rem inherit)}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.praxis-icon-button{color:var(--md-sys-color-on-surface-variant)}praxis-dynamic-form-dialog-host .dialog-header button.praxis-icon-button:hover{color:var(--md-sys-color-primary)}praxis-dynamic-form-dialog-host .dialog-footer{position:sticky;bottom:0;z-index:1;padding:var(--dlg-pad)}.pfx-blur-backdrop{background-color:var(--pfx-backdrop, rgba(15, 23, 42, .42))!important;backdrop-filter:blur(var(--pfx-backdrop-blur, 10px)) saturate(110%);-webkit-backdrop-filter:blur(var(--pfx-backdrop-blur, 10px)) saturate(110%)}.pfx-transparent-backdrop{background-color:transparent!important}.cdk-overlay-pane.pfx-dialog-pane{overflow:hidden;transition:width .2s ease,height .2s ease,margin .2s ease}.cdk-overlay-pane.pfx-dialog-pane .mat-mdc-dialog-surface,.cdk-overlay-pane.pfx-dialog-pane .mdc-dialog__surface{display:flex;flex-direction:column;width:100%;height:100%;max-height:inherit;overflow:hidden;background:var(--pfx-form-surface, var(--md-sys-color-surface))!important;border:1px solid var(--pfx-form-stroke, var(--md-sys-color-outline-variant))!important}.cdk-overlay-pane.pfx-dialog-pane.pfx-dialog-frosted .mat-mdc-dialog-surface,.cdk-overlay-pane.pfx-dialog-pane.pfx-dialog-frosted .mdc-dialog__surface{backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}.cdk-overlay-pane.pfx-drawer-pane{margin:0!important;border-radius:0!important;box-shadow:var(--md-sys-elevation-level3, 0 24px 80px rgba(15, 23, 42, .28)),0 0 0 1px color-mix(in srgb,var(--md-sys-color-outline, #64748b) 24%,transparent)}.cdk-overlay-pane.pfx-drawer-pane.pfx-drawer-maximized{margin:var(--pfx-drawer-edge-gap, 8px)!important}.cdk-overlay-pane.pfx-drawer-pane .mat-mdc-dialog-surface,.cdk-overlay-pane.pfx-drawer-pane .mdc-dialog__surface{border-radius:0!important}\n"], dependencies: [{ kind: "ngmodule", type: MatDialogModule }, { kind: "directive", type: i1.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i1.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "component", type: PraxisIconButtonComponent, selector: "button[praxisIconButton]", inputs: ["praxisIconButton", "size", "appearance", "presentation", "pressed", "busy"] }, { kind: "component", type: PraxisResourceIdentityComponent, selector: "praxis-resource-identity", inputs: ["identity", "emptyTitle", "density", "showMetadataLabels", "showKeyLabel", "ariaLabel"] }, { 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
5774
  }
5086
5775
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicFormDialogHostComponent, decorators: [{
5087
5776
  type: Component,
5088
5777
  args: [{ selector: 'praxis-dynamic-form-dialog-host', standalone: true, imports: [
5089
5778
  MatDialogModule,
5090
- MatButtonModule,
5091
- MatIconModule,
5092
- PraxisIconDirective,
5779
+ PraxisIconButtonComponent,
5780
+ PraxisResourceIdentityComponent,
5093
5781
  PraxisDynamicForm
5094
- ], providers: [GenericCrudService], host: {
5782
+ ], encapsulation: ViewEncapsulation.None, providers: [
5783
+ GenericCrudService,
5784
+ providePraxisI18nConfig(PRAXIS_CRUD_RUNTIME_I18N_CONFIG),
5785
+ ], host: {
5095
5786
  class: 'praxis-dialog',
5096
5787
  '[attr.data-density]': 'modal.density || "default"',
5788
+ '[attr.data-presentation]': 'presentation',
5789
+ '[class.praxis-drawer]': 'presentation === "drawer"',
5097
5790
  }, template: `
5098
5791
  <div mat-dialog-title class="dialog-header">
5099
5792
  <h2 id="crudDialogTitle" class="dialog-title">
5100
- {{ data.action?.label || texts.title }}
5793
+ {{ dialogTitle }}
5101
5794
  </h2>
5102
5795
  <span class="spacer"></span>
5103
5796
  @if (modal.canMaximize) {
5104
5797
  <button
5105
- mat-icon-button
5798
+ [praxisIconButton]="maximized ? 'close_fullscreen' : 'open_in_full'"
5799
+ size="compact"
5106
5800
  type="button"
5107
5801
  (click)="toggleMaximize()"
5108
5802
  [attr.aria-label]="maximized ? texts.restoreLabel : texts.maximizeLabel"
5109
- >
5110
- <mat-icon [praxisIcon]="maximized ? 'close_fullscreen' : 'open_in_full'"></mat-icon>
5111
- </button>
5803
+ ></button>
5112
5804
  }
5113
5805
  <button
5114
- mat-icon-button
5806
+ praxisIconButton="close"
5807
+ size="compact"
5115
5808
  type="button"
5116
5809
  (click)="onCancel()"
5117
5810
  [attr.aria-label]="texts.closeLabel"
5118
5811
  cdkFocusInitial
5119
- >
5120
- <mat-icon [praxisIcon]="'close'"></mat-icon>
5121
- </button>
5812
+ ></button>
5122
5813
  </div>
5123
5814
 
5124
5815
  <mat-dialog-content
5125
5816
  class="dialog-content"
5126
5817
  aria-labelledby="crudDialogTitle"
5127
5818
  >
5819
+ @if (resourceIdentity) {
5820
+ <div class="crud-resource-identity" data-testid="crud-resource-identity">
5821
+ <span class="crud-resource-identity__label" aria-hidden="true">
5822
+ {{ texts.recordContextLabel }}
5823
+ </span>
5824
+ <praxis-resource-identity
5825
+ [identity]="resourceIdentity"
5826
+ density="compact"
5827
+ [showKeyLabel]="true"
5828
+ [ariaLabel]="texts.recordContextLabel"
5829
+ ></praxis-resource-identity>
5830
+ </div>
5831
+ }
5128
5832
  <praxis-dynamic-form
5129
5833
  [formId]="data.action?.formId"
5130
5834
  [resourcePath]="resourcePath"
5131
5835
  [resourceId]="resourceId"
5132
5836
  [initialValue]="initialValue"
5133
5837
  [mode]="mode"
5838
+ [config]="formConfig"
5134
5839
  [schemaUrl]="schemaUrl"
5135
5840
  [submitUrl]="submitUrl"
5136
5841
  [submitMethod]="submitMethod"
5137
5842
  [apiEndpointKey]="apiEndpointKey"
5138
5843
  [apiUrlEntry]="apiUrlEntry"
5844
+ [configPersistenceStrategy]="formConfigPersistenceStrategy"
5845
+ [layoutPolicy]="layoutPolicy"
5139
5846
  [presentationModeGlobal]="mode === 'view' ? true : null"
5140
5847
  [backConfig]="backConfig"
5141
5848
  [actions]="formActions"
@@ -5143,7 +5850,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5143
5850
  (formCancel)="onCancel()"
5144
5851
  ></praxis-dynamic-form>
5145
5852
  </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"] }]
5853
+ `, 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 .crud-resource-identity{position:sticky;top:0;z-index:1;display:grid;gap:.25rem;margin-block-end:var(--dlg-pad);padding-block:.5rem .75rem;background:var(--pfx-form-surface, var(--md-sys-color-surface));border-bottom:1px solid var(--md-sys-color-outline-variant)}praxis-dynamic-form-dialog-host .crud-resource-identity__label{color:var(--md-sys-color-on-surface-variant);font:var(--md-sys-typescale-label-medium, 500 .75rem/1rem inherit)}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.praxis-icon-button{color:var(--md-sys-color-on-surface-variant)}praxis-dynamic-form-dialog-host .dialog-header button.praxis-icon-button:hover{color:var(--md-sys-color-primary)}praxis-dynamic-form-dialog-host .dialog-footer{position:sticky;bottom:0;z-index:1;padding:var(--dlg-pad)}.pfx-blur-backdrop{background-color:var(--pfx-backdrop, rgba(15, 23, 42, .42))!important;backdrop-filter:blur(var(--pfx-backdrop-blur, 10px)) saturate(110%);-webkit-backdrop-filter:blur(var(--pfx-backdrop-blur, 10px)) saturate(110%)}.pfx-transparent-backdrop{background-color:transparent!important}.cdk-overlay-pane.pfx-dialog-pane{overflow:hidden;transition:width .2s ease,height .2s ease,margin .2s ease}.cdk-overlay-pane.pfx-dialog-pane .mat-mdc-dialog-surface,.cdk-overlay-pane.pfx-dialog-pane .mdc-dialog__surface{display:flex;flex-direction:column;width:100%;height:100%;max-height:inherit;overflow:hidden;background:var(--pfx-form-surface, var(--md-sys-color-surface))!important;border:1px solid var(--pfx-form-stroke, var(--md-sys-color-outline-variant))!important}.cdk-overlay-pane.pfx-dialog-pane.pfx-dialog-frosted .mat-mdc-dialog-surface,.cdk-overlay-pane.pfx-dialog-pane.pfx-dialog-frosted .mdc-dialog__surface{backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}.cdk-overlay-pane.pfx-drawer-pane{margin:0!important;border-radius:0!important;box-shadow:var(--md-sys-elevation-level3, 0 24px 80px rgba(15, 23, 42, .28)),0 0 0 1px color-mix(in srgb,var(--md-sys-color-outline, #64748b) 24%,transparent)}.cdk-overlay-pane.pfx-drawer-pane.pfx-drawer-maximized{margin:var(--pfx-drawer-edge-gap, 8px)!important}.cdk-overlay-pane.pfx-drawer-pane .mat-mdc-dialog-surface,.cdk-overlay-pane.pfx-drawer-pane .mdc-dialog__surface{border-radius:0!important}\n"] }]
5147
5854
  }], ctorParameters: () => [{ type: undefined, decorators: [{
5148
5855
  type: Inject,
5149
5856
  args: [MatDialogRef]
@@ -5170,6 +5877,17 @@ function stringOrUndefined(value) {
5170
5877
  const text = String(value ?? '').trim();
5171
5878
  return text || undefined;
5172
5879
  }
5880
+ function sameTitle(left, right) {
5881
+ return normalizeTitle(left) === normalizeTitle(right);
5882
+ }
5883
+ function normalizeTitle(value) {
5884
+ return value
5885
+ .normalize('NFD')
5886
+ .replace(/[\u0300-\u036f]/g, '')
5887
+ .replace(/\s+/g, ' ')
5888
+ .trim()
5889
+ .toLocaleLowerCase('pt-BR');
5890
+ }
5173
5891
  function deriveCreateSubmitLabel(actionLabel) {
5174
5892
  if (!actionLabel) {
5175
5893
  return 'Criar';
@@ -5180,6 +5898,61 @@ function deriveCreateSubmitLabel(actionLabel) {
5180
5898
  }
5181
5899
  return actionLabel;
5182
5900
  }
5901
+ function deriveModeTitle(mode, resourceTitle) {
5902
+ if (!resourceTitle) {
5903
+ return undefined;
5904
+ }
5905
+ const singular = lowerFirstLetter(singularizePtBrResourceTitle(resourceTitle));
5906
+ if (mode === 'create') {
5907
+ return `Adicionar ${singular}`;
5908
+ }
5909
+ if (mode === 'edit') {
5910
+ return `Editar ${singular}`;
5911
+ }
5912
+ return `Visualizar ${singular}`;
5913
+ }
5914
+ function titleFromResourcePath(resourcePath) {
5915
+ const lastSegment = resourcePath?.split('/').filter(Boolean).pop();
5916
+ if (!lastSegment) {
5917
+ return undefined;
5918
+ }
5919
+ return lastSegment
5920
+ .replace(/[-_]+/g, ' ')
5921
+ .replace(/\s+/g, ' ')
5922
+ .trim()
5923
+ .toLocaleLowerCase('pt-BR');
5924
+ }
5925
+ function singularizePtBrResourceTitle(title) {
5926
+ const normalized = title.trim();
5927
+ const lower = normalized.toLocaleLowerCase('pt-BR');
5928
+ if (lower === 'códigos de frequência' || lower === 'codigos de frequencia') {
5929
+ return 'código de frequência';
5930
+ }
5931
+ const [first, ...rest] = normalized.split(/\s+/);
5932
+ const singularFirst = singularizePtBrWord(first);
5933
+ return [singularFirst, ...rest].join(' ');
5934
+ }
5935
+ function singularizePtBrWord(word) {
5936
+ if (/ões$/i.test(word)) {
5937
+ return word.replace(/ões$/i, 'ão');
5938
+ }
5939
+ if (/ais$/i.test(word)) {
5940
+ return word.replace(/ais$/i, 'al');
5941
+ }
5942
+ if (/eis$/i.test(word)) {
5943
+ return word.replace(/eis$/i, 'el');
5944
+ }
5945
+ if (/res$/i.test(word)) {
5946
+ return word.replace(/es$/i, '');
5947
+ }
5948
+ if (/s$/i.test(word) && !/ss$/i.test(word)) {
5949
+ return word.replace(/s$/i, '');
5950
+ }
5951
+ return word;
5952
+ }
5953
+ function lowerFirstLetter(value) {
5954
+ return value.replace(/\p{L}/u, (letter) => letter.toLocaleLowerCase('pt-BR'));
5955
+ }
5183
5956
 
5184
5957
  var dynamicFormDialogHost_component = /*#__PURE__*/Object.freeze({
5185
5958
  __proto__: null,
@@ -5229,6 +6002,7 @@ class PraxisCrudWidgetConfigEditor {
5229
6002
  componentInstanceId: this.inputs?.componentInstanceId ?? this.widgetKey,
5230
6003
  context: this.inputs?.context,
5231
6004
  enableCustomization: this.inputs?.enableCustomization ?? false,
6005
+ authoringCapability: this.inputs?.authoringCapability ?? null,
5232
6006
  };
5233
6007
  return {
5234
6008
  inputs,
@@ -5325,11 +6099,31 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
5325
6099
  type: 'PraxisDataQueryContext | null',
5326
6100
  description: 'Contexto semântico de consulta encaminhado para a tabela interna do CRUD. Preferir este contrato para novo authoring remoto.',
5327
6101
  },
6102
+ {
6103
+ name: 'metadata.resource.schemaPath',
6104
+ type: 'string | null',
6105
+ description: 'Template canonico opcional usado apenas para resolver /schemas/filtered quando resource.path precisa ser um alvo operacional concreto.',
6106
+ },
6107
+ {
6108
+ name: 'metadata.resource.title',
6109
+ type: 'string | null',
6110
+ description: 'Titulo documental do recurso usado pelo shell CRUD para derivar titulos contextuais de criacao, edicao e visualizacao quando a action nao informa label.',
6111
+ },
6112
+ {
6113
+ name: 'metadata.resource.formTitle',
6114
+ type: 'string | null',
6115
+ description: 'Titulo documental especifico para formularios do recurso, usado antes de metadata.resource.title no shell modal/drawer.',
6116
+ },
5328
6117
  {
5329
6118
  name: 'metadata.filterCriteria',
5330
6119
  type: 'Record<string, unknown> | null',
5331
6120
  description: 'Bridge declarativa de filtros encaminhada para a tabela interna. Para novo authoring, prefira metadata.queryContext.',
5332
6121
  },
6122
+ {
6123
+ name: 'metadata.form',
6124
+ type: 'FormConfig',
6125
+ 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.',
6126
+ },
5333
6127
  {
5334
6128
  name: 'metadata.actions[].form',
5335
6129
  type: 'CrudActionFormContract',
@@ -5341,6 +6135,12 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
5341
6135
  default: false,
5342
6136
  description: 'Habilita modo de customização do layout.',
5343
6137
  },
6138
+ {
6139
+ name: 'authoringCapability',
6140
+ type: 'string | null',
6141
+ default: null,
6142
+ description: 'Capability publica do contexto corporativo exigida para disponibilizar authoring governado no CRUD e na tabela interna.',
6143
+ },
5344
6144
  ],
5345
6145
  outputs: [
5346
6146
  {
@@ -5699,6 +6499,7 @@ const CRUD_AI_CAPABILITIES = {
5699
6499
  { path: 'actions[].params[].to', category: 'actions', valueKind: 'enum', allowedValues: ENUMS.paramTarget, description: 'Destino do parametro.' },
5700
6500
  { path: 'actions[].params[].name', category: 'actions', valueKind: 'string', description: 'Nome do parametro no destino.' },
5701
6501
  { path: 'actions[].form.initialValue', category: 'actions', valueKind: 'object', description: 'Seed fixo do formulario injetado em inputs.initialValue antes da abertura.' },
6502
+ { 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
6503
  { path: 'actions[].back', category: 'navigation', valueKind: 'object', description: 'BackConfig por acao.' },
5703
6504
  { path: 'actions[].back.strategy', category: 'navigation', valueKind: 'enum', allowedValues: ENUMS.backStrategy, description: 'Estrategia de retorno da acao (auto, close, navigate).' },
5704
6505
  { path: 'actions[].back.returnTo', category: 'navigation', valueKind: 'string', description: 'Rota ou destino usado quando a estrategia de retorno for navigate.' },
@@ -5736,6 +6537,7 @@ const surfaceConfigureSchema = {
5736
6537
  submitMethod: { enum: ['post', 'put', 'patch', 'delete'] },
5737
6538
  apiEndpointKey: { type: 'string' },
5738
6539
  initialValue: { type: 'object' },
6540
+ layoutPolicy: { type: 'object' },
5739
6541
  },
5740
6542
  },
5741
6543
  params: {
@@ -5789,14 +6591,28 @@ const dialogHostSchema = {
5789
6591
  width: { type: 'string' },
5790
6592
  height: { type: 'string' },
5791
6593
  minWidth: { type: 'string' },
6594
+ minHeight: { type: 'string' },
5792
6595
  maxWidth: { type: 'string' },
6596
+ maxHeight: { type: 'string' },
5793
6597
  density: { type: 'string' },
6598
+ backdropStyle: { enum: ['blur', 'dim', 'transparent'] },
5794
6599
  canMaximize: { type: 'boolean' },
5795
6600
  rememberLastState: { type: 'boolean' },
6601
+ closeOnSave: { type: 'boolean' },
5796
6602
  startMaximized: { type: 'boolean' },
5797
6603
  disableCloseOnEsc: { type: 'boolean' },
5798
6604
  disableCloseOnBackdrop: { type: 'boolean' },
5799
- fullscreenBreakpoint: { type: 'string' },
6605
+ fullscreenBreakpoint: { type: 'number' },
6606
+ position: {
6607
+ type: 'object',
6608
+ properties: {
6609
+ top: { type: 'string' },
6610
+ bottom: { type: 'string' },
6611
+ left: { type: 'string' },
6612
+ right: { type: 'string' },
6613
+ },
6614
+ },
6615
+ edgeGap: { type: 'number' },
5800
6616
  },
5801
6617
  },
5802
6618
  back: { type: 'object' },
@@ -5849,19 +6665,21 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
5849
6665
  { name: 'crudId', type: 'string', description: 'Stable CRUD instance id used for table/form identity and persistence.' },
5850
6666
  { name: 'componentInstanceId', type: 'string', description: 'Optional stable host instance id for multiple CRUD widgets on the same route.' },
5851
6667
  { name: 'context', type: 'Record<string, unknown>', description: 'Opaque host context used for authoring seeds and launcher inputs.' },
6668
+ { name: 'enableCustomization', type: 'boolean', description: 'Explicit host opt-in for CRUD authoring surfaces.' },
6669
+ { name: 'authoringCapability', type: 'string | null', description: 'Public runtime capability required before CRUD and delegated table authoring can open.' },
5852
6670
  { name: 'afterOpen', type: '{ mode: FormOpenMode; action: string }', description: 'Emitted after a CRUD action opens.' },
5853
6671
  { name: 'afterSave', type: '{ id: string | number; data: unknown }', description: 'Emitted after save; CRUD refetches the list.' },
5854
6672
  { name: 'afterDelete', type: '{ id: string | number }', description: 'Emitted after delete; CRUD refetches the list.' },
5855
6673
  ],
5856
6674
  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.' },
6675
+ { 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
6676
  { kind: 'listSurface', resolver: 'crud-list-surface', description: 'CRUD-hosted list surface and table delegation boundary.' },
5859
6677
  { kind: 'createSurface', resolver: 'crud-action-by-id:create', description: 'Create action open mode, route/form binding and launcher inputs.' },
5860
6678
  { kind: 'editSurface', resolver: 'crud-action-by-id:edit', description: 'Edit action open mode, route/form binding and launcher inputs.' },
5861
6679
  { kind: 'viewSurface', resolver: 'crud-action-by-id:view', description: 'View action open mode, route/form binding and launcher inputs.' },
5862
6680
  { 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.' },
6681
+ { 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.' },
6682
+ { 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
6683
  { kind: 'permissions', resolver: 'crud-resource-capabilities', description: 'CRUD action availability derived from resource capabilities and action permissions.' },
5866
6684
  { 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
6685
  { kind: 'childOperation', resolver: 'child-authoring-manifest-operation', description: 'Delegated form/table/dialog/settings-panel operation owned by the child component manifest.' },
@@ -5877,13 +6695,13 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
5877
6695
  effects: [{ kind: 'compile-domain-patch', handler: 'crud-resource-bind', handlerContract: {
5878
6696
  reads: ['CrudMetadata.resource', 'api_metadata', 'ResourceDiscoveryService', 'GET /{resource}/capabilities'],
5879
6697
  writes: ['CrudMetadata.resource', 'CrudMetadata.queryContext', 'PraxisCrudComponent.tableCrudContext'],
5880
- identityKeys: ['resourcePath', 'resourceKey'],
6698
+ identityKeys: ['resourcePath', 'resourceKey', 'schemaPath'],
5881
6699
  inputSchema: resourceBindSchema,
5882
6700
  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.',
6701
+ 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
6702
  } }],
5885
6703
  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'],
6704
+ affectedPaths: ['resource.path', 'resource.schemaPath', 'resource.idField', 'resource.endpointKey', 'resource.apiUrlEntry', 'queryContext', 'filterCriteria'],
5887
6705
  submissionImpact: 'affects-remote-binding',
5888
6706
  preconditions: ['crud-metadata-loaded', 'api-metadata-available'],
5889
6707
  },
@@ -6017,14 +6835,14 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
6017
6835
  target: { kind: 'dialogHost', resolver: 'crud-dialog-host-defaults', ambiguityPolicy: 'fail', required: true },
6018
6836
  inputSchema: dialogHostSchema,
6019
6837
  effects: [{ kind: 'compile-domain-patch', handler: 'crud-dialog-host-set', handlerContract: {
6020
- reads: ['CrudMetadata.defaults', 'CrudLauncherService.resolveOpenMode', 'DialogService', 'CRUD_DRAWER_ADAPTER'],
6838
+ reads: ['CrudMetadata.defaults', 'CrudLauncherService.resolveOpenMode', 'DialogService', 'DynamicFormDialogHostComponent', 'CRUD_DRAWER_ADAPTER'],
6021
6839
  writes: ['CrudMetadata.defaults.openMode', 'CrudMetadata.defaults.modal', 'CrudMetadata.defaults.back'],
6022
6840
  identityKeys: ['crudId'],
6023
6841
  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.',
6842
+ failureModes: ['open-mode-unsupported', 'drawer-runtime-unavailable', 'modal-size-invalid', 'drawer-position-lifecycle-unstable', 'back-policy-invalid'],
6843
+ description: 'Configures CRUD-owned route/modal/drawer defaults consumed by the launcher, default dialog-backed drawer runtime and optional host drawer adapter.',
6026
6844
  } }],
6027
- validators: ['open-mode-supported', 'modal-size-valid', 'drawer-adapter-available-when-needed', 'back-policy-valid', 'settings-panel-shell-compatible'],
6845
+ validators: ['open-mode-supported', 'modal-size-valid', 'drawer-runtime-available', 'drawer-position-lifecycle-stable', 'back-policy-valid', 'settings-panel-shell-compatible'],
6028
6846
  affectedPaths: ['defaults.openMode', 'defaults.modal', 'defaults.back'],
6029
6847
  submissionImpact: 'config-only',
6030
6848
  preconditions: ['crud-metadata-loaded'],
@@ -6100,7 +6918,8 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
6100
6918
  { validatorId: 'permissions-delete-valid', level: 'error', code: 'CRUD_DELETE_PERMISSION_VALID', description: 'Delete permission cannot bypass resource capabilities or confirmation policy.' },
6101
6919
  { validatorId: 'open-mode-supported', level: 'error', code: 'CRUD_OPEN_MODE_SUPPORTED', description: 'Open mode must be route, modal or drawer.' },
6102
6920
  { 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.' },
6921
+ { 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.' },
6922
+ { validatorId: 'drawer-position-lifecycle-stable', level: 'error', code: 'CRUD_DRAWER_POSITION_LIFECYCLE_STABLE', description: 'A drawer must preserve its configured viewport edge through open, remembered-size restoration, maximize and restore.' },
6104
6923
  { validatorId: 'back-policy-valid', level: 'error', code: 'CRUD_BACK_POLICY_VALID', description: 'Back policy must be valid for route/modal/drawer behavior.' },
6105
6924
  { validatorId: 'settings-panel-shell-compatible', level: 'warning', code: 'CRUD_SETTINGS_PANEL_COMPATIBLE', description: 'Authoring shell must preserve apply/save/reset semantics.' },
6106
6925
  { validatorId: 'action-permission-supported', level: 'error', code: 'CRUD_ACTION_PERMISSION_SUPPORTED', description: 'Action permissions must map to supported resource capabilities.' },
@@ -6121,6 +6940,7 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
6121
6940
  'FormConfig and FieldMetadata edits must delegate to dynamic-form or metadata-editor manifests.',
6122
6941
  'TableConfig edits must delegate to praxis-table; CRUD owns only shell orchestration and list surface binding.',
6123
6942
  'Delete behavior is destructive and requires explicit confirmation plus backend capability support.',
6943
+ 'Drawer position and edgeGap must round-trip without changing the configured edge during open, remembered-size restoration, maximize or restore.',
6124
6944
  'crudContext must remain reference-stable across change detection cycles.',
6125
6945
  ],
6126
6946
  examples: [
@@ -6132,6 +6952,7 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
6132
6952
  { id: 'crud-view-drawer-readonly', request: 'Open view in a drawer and keep the form readonly.', operationId: 'surface.view.configure', target: 'action:view', params: { actionId: 'view', openMode: 'drawer', formId: 'employee-view' }, isPositive: true },
6133
6953
  { id: 'crud-delete-enable-confirmed', request: 'Enable delete but require confirmation.', operationId: 'delete.enabled.set', target: 'action:delete', params: { enabled: true, actionId: 'delete', requiresConfirmation: true, autoDelete: true }, isPositive: true },
6134
6954
  { id: 'crud-dialog-size-medium', request: 'Make CRUD modals 900px wide and remember their last state.', operationId: 'dialog.size.set', target: 'dialogHost', params: { defaultOpenMode: 'modal', modal: { width: '900px', rememberLastState: true } }, isPositive: true },
6955
+ { id: 'crud-drawer-left-edge', request: 'Open CRUD forms in a left drawer with an 8px maximized inset.', operationId: 'dialog.size.set', target: 'dialogHost', params: { defaultOpenMode: 'drawer', modal: { position: { left: '0', top: '0' }, edgeGap: 8 } }, isPositive: true },
6135
6956
  { id: 'crud-permissions-no-delete', request: 'Hide delete when the resource capabilities do not allow it.', operationId: 'permissions.set', target: 'permissions:delete', params: { requiredCapabilities: ['delete'], denyWhenMissingCapability: true }, isPositive: true },
6136
6957
  { id: 'crud-delegate-form-layout', request: 'Add a new field to the create form.', operationId: 'form.childOperation.delegate', target: 'praxis-dynamic-form:field.add', params: { childComponentId: 'praxis-dynamic-form', childOperationId: 'field.add', reason: 'Form field semantics belong to dynamic-form' }, isPositive: true },
6137
6958
  { id: 'crud-delegate-table-column', request: 'Add a salary column to the list table.', operationId: 'form.childOperation.delegate', target: 'praxis-table:column.add', params: { childComponentId: 'praxis-table', childOperationId: 'column.add', reason: 'Table column semantics belong to praxis-table' }, isPositive: true },