@praxisui/crud 5.0.0-beta.0 → 7.0.0-beta.0

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,23 +1,39 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, InjectionToken, inject, EventEmitter, DestroyRef, ChangeDetectorRef, ViewChild, Output, Input, Component, Inject, ENVIRONMENT_INITIALIZER } from '@angular/core';
2
+ import { Injectable, InjectionToken, inject, input, signal, computed, effect, ChangeDetectionStrategy, Component, EventEmitter, DestroyRef, ChangeDetectorRef, ViewChild, Output, Input, Inject, ENVIRONMENT_INITIALIZER } from '@angular/core';
3
3
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
+ import { HttpClient } from '@angular/common/http';
4
5
  import { Router, ActivatedRoute, RouterLink } from '@angular/router';
5
6
  import { MatSnackBar } from '@angular/material/snack-bar';
6
- import { firstValueFrom } from 'rxjs';
7
- import * as i2 from '@praxisui/core';
8
- import { ASYNC_CONFIG_STORAGE, GlobalConfigService, fillUndefined, createDefaultTableConfig, PraxisI18nService, ResourceDiscoveryService, ResourceActionOpenAdapterService, ResourceSurfaceOpenAdapterService, GLOBAL_SURFACE_SERVICE, ComponentKeyService, translateUnavailableWorkflowMessage, EmptyStateCardComponent, providePraxisI18nConfig, RESOURCE_DISCOVERY_I18N_CONFIG, PraxisIconDirective, GenericCrudService, ComponentMetadataRegistry } from '@praxisui/core';
9
- import { PraxisTable } from '@praxisui/table';
7
+ import { firstValueFrom, BehaviorSubject } from 'rxjs';
8
+ import * as i2$1 from '@praxisui/core';
9
+ import { ASYNC_CONFIG_STORAGE, GlobalConfigService, CrudOperationResolutionService, fillUndefined, SETTINGS_PANEL_DATA, PraxisI18nService, providePraxisI18nConfig, createDefaultTableConfig, ResourceDiscoveryService, ResourceActionOpenAdapterService, ResourceSurfaceOpenAdapterService, GLOBAL_SURFACE_SERVICE, ComponentKeyService, translateUnavailableWorkflowMessage, EmptyStateCardComponent, RESOURCE_DISCOVERY_I18N_CONFIG, PraxisIconDirective, GenericCrudService, ComponentMetadataRegistry } from '@praxisui/core';
10
+ import { SettingsPanelService } from '@praxisui/settings-panel';
11
+ import { PraxisTableInlineAuthoringEditorComponent, PraxisTable } from '@praxisui/table';
12
+ import { ConfirmDialogComponent } from '@praxisui/dynamic-fields';
10
13
  import * as i1 from '@angular/material/dialog';
11
14
  import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
12
15
  export { MAT_DIALOG_DATA as DIALOG_DATA } from '@angular/material/dialog';
13
- import * as i1$1 from '@angular/common';
16
+ import * as i1$2 from '@angular/common';
14
17
  import { CommonModule } from '@angular/common';
15
- import * as i4 from '@angular/material/button';
18
+ import * as i1$1 from '@angular/forms';
19
+ import { FormsModule } from '@angular/forms';
20
+ import * as i2 from '@angular/material/button';
16
21
  import { MatButtonModule } from '@angular/material/button';
17
- import * as i5 from '@angular/material/icon';
22
+ import * as i3 from '@angular/material/card';
23
+ import { MatCardModule } from '@angular/material/card';
24
+ import * as i4 from '@angular/material/expansion';
25
+ import { MatExpansionModule } from '@angular/material/expansion';
26
+ import * as i5 from '@angular/material/form-field';
27
+ import { MatFormFieldModule } from '@angular/material/form-field';
28
+ import * as i6 from '@angular/material/input';
29
+ import { MatInputModule } from '@angular/material/input';
30
+ import * as i7 from '@angular/material/select';
31
+ import { MatSelectModule } from '@angular/material/select';
32
+ import * as i8 from '@angular/material/slide-toggle';
33
+ import { MatSlideToggleModule } from '@angular/material/slide-toggle';
34
+ import * as i5$1 from '@angular/material/icon';
18
35
  import { MatIconModule } from '@angular/material/icon';
19
36
  import { PraxisDynamicForm } from '@praxisui/dynamic-form';
20
- import { ConfirmDialogComponent } from '@praxisui/dynamic-fields';
21
37
  import { filter, take } from 'rxjs/operators';
22
38
 
23
39
  class DialogService {
@@ -57,6 +73,7 @@ class CrudLauncherService {
57
73
  dialog = inject(DialogService);
58
74
  storage = inject(ASYNC_CONFIG_STORAGE);
59
75
  global = inject(GlobalConfigService);
76
+ operationResolver = inject(CrudOperationResolutionService);
60
77
  drawerAdapter = (() => {
61
78
  try {
62
79
  return inject(CRUD_DRAWER_ADAPTER);
@@ -65,7 +82,7 @@ class CrudLauncherService {
65
82
  return undefined;
66
83
  }
67
84
  })();
68
- async launch(action, row, metadata, componentKeyId, drawerCallbacks) {
85
+ async launch(action, row, metadata, componentKeyId, drawerCallbacks, runtime) {
69
86
  // Carregar overrides de CRUD (se houver) e mesclar em uma cópia local
70
87
  const merged = await this.mergeCrudOverrides(metadata, action, componentKeyId || undefined);
71
88
  const mode = this.resolveOpenMode(merged.action, merged.metadata);
@@ -79,13 +96,14 @@ class CrudLauncherService {
79
96
  return { mode };
80
97
  }
81
98
  if (mode === 'drawer' && this.drawerAdapter) {
82
- const inputs = this.mapInputs(merged.action, row);
99
+ const actionForLaunch = this.resolveActionForLaunch(merged.action, merged.metadata);
100
+ const inputs = this.mapInputs(actionForLaunch, row, merged.metadata, runtime);
83
101
  const idField = merged.metadata.resource?.idField ?? 'id';
84
102
  if (row && inputs[idField] === undefined && row[idField] !== undefined) {
85
103
  inputs[idField] = row[idField];
86
104
  }
87
105
  await Promise.resolve(this.drawerAdapter.open({
88
- action: merged.action,
106
+ action: actionForLaunch,
89
107
  metadata: merged.metadata,
90
108
  inputs,
91
109
  onClose: drawerCallbacks?.onClose,
@@ -93,18 +111,22 @@ class CrudLauncherService {
93
111
  }));
94
112
  return { mode };
95
113
  }
96
- if (!merged.action.formId) {
97
- throw new Error(`formId not provided for action ${merged.action.action}`);
114
+ const actionForLaunch = this.resolveActionForLaunch(merged.action, merged.metadata);
115
+ if (!actionForLaunch.formId) {
116
+ throw new Error(`formId not provided for action ${actionForLaunch.action}`);
98
117
  }
99
- const inputs = this.mapInputs(merged.action, row);
118
+ const inputs = this.mapInputs(actionForLaunch, row, merged.metadata, runtime);
100
119
  const idField = merged.metadata.resource?.idField ?? 'id';
101
- if (row && inputs[idField] === undefined && row[idField] !== undefined) {
120
+ if (actionForLaunch.action !== 'create' &&
121
+ row &&
122
+ inputs[idField] === undefined &&
123
+ row[idField] !== undefined) {
102
124
  inputs[idField] = row[idField];
103
125
  }
104
126
  const modalCfg = { ...(merged.metadata.defaults?.modal || {}) };
105
127
  console.debug('[CRUD:Launcher] opening dialog with:', {
106
128
  action: merged.action.action,
107
- formId: merged.action.formId,
129
+ formId: actionForLaunch.formId,
108
130
  inputs,
109
131
  modalCfg,
110
132
  resourcePath: merged.metadata.resource?.path ?? merged.metadata.table?.resourcePath,
@@ -137,7 +159,7 @@ class CrudLauncherService {
137
159
  minWidth: '360px',
138
160
  maxWidth: '95vw',
139
161
  ariaLabelledBy: 'crudDialogTitle',
140
- data: { action: merged.action, row, metadata: merged.metadata, inputs },
162
+ data: { action: actionForLaunch, row, metadata: merged.metadata, inputs },
141
163
  });
142
164
  return { mode, ref };
143
165
  }
@@ -152,8 +174,10 @@ class CrudLauncherService {
152
174
  const actionName = action.action;
153
175
  const globalMode = (actionName && globalCrud?.actionDefaults?.[actionName]?.openMode) ?? globalCrud?.defaults?.openMode;
154
176
  let resolved = globalMode ?? 'route';
155
- // Safety: if modal/drawer but no formId, degrade to route to avoid runtime error
156
- if ((resolved === 'modal' || resolved === 'drawer') && !action.formId) {
177
+ // Safety: if modal/drawer but there is no formId and the action cannot be inferred, degrade to route.
178
+ if ((resolved === 'modal' || resolved === 'drawer') &&
179
+ !action.formId &&
180
+ !this.isCanonicalCrudAction(action.action)) {
157
181
  resolved = 'route';
158
182
  }
159
183
  return resolved;
@@ -201,15 +225,116 @@ class CrudLauncherService {
201
225
  const queryString = new URLSearchParams(query).toString();
202
226
  return queryString ? `${route}?${queryString}` : route;
203
227
  }
204
- mapInputs(action, row) {
228
+ mapInputs(action, row, metadata, runtime) {
205
229
  const inputs = {};
206
230
  action.params?.forEach((p) => {
207
231
  if (p.to === 'input') {
208
232
  inputs[p.name] = row?.[p.from];
209
233
  }
210
234
  });
235
+ const resolved = this.resolveActionFormContract(action, row, metadata, runtime);
236
+ if (resolved.schemaUrl) {
237
+ inputs['schemaUrl'] = resolved.schemaUrl;
238
+ }
239
+ if (resolved.submitUrl) {
240
+ inputs['submitUrl'] = resolved.submitUrl;
241
+ }
242
+ if (resolved.submitMethod) {
243
+ inputs['submitMethod'] = resolved.submitMethod;
244
+ }
245
+ if (resolved.apiEndpointKey != null) {
246
+ inputs['apiEndpointKey'] = resolved.apiEndpointKey;
247
+ }
248
+ if (resolved.apiUrlEntry != null) {
249
+ inputs['apiUrlEntry'] = resolved.apiUrlEntry;
250
+ }
251
+ if (action.form?.initialValue != null) {
252
+ inputs['initialValue'] = action.form.initialValue;
253
+ }
211
254
  return inputs;
212
255
  }
256
+ resolveActionForLaunch(action, metadata) {
257
+ if (action.formId) {
258
+ return action;
259
+ }
260
+ if (!this.isCanonicalCrudAction(action.action)) {
261
+ return action;
262
+ }
263
+ const formId = this.buildInferredFormId(action, metadata);
264
+ return formId ? { ...action, formId } : action;
265
+ }
266
+ resolveActionFormContract(action, row, metadata, runtime) {
267
+ if (this.isExplicitCrudAction(action)) {
268
+ return {
269
+ schemaUrl: action.form?.schemaUrl ?? null,
270
+ submitUrl: action.form?.submitUrl ?? null,
271
+ submitMethod: action.form?.submitMethod ?? null,
272
+ apiEndpointKey: action.form?.apiEndpointKey ?? null,
273
+ apiUrlEntry: action.form?.apiUrlEntry ?? null,
274
+ };
275
+ }
276
+ if (!this.isCanonicalCrudAction(action.action)) {
277
+ return {
278
+ apiEndpointKey: action.form?.apiEndpointKey ?? null,
279
+ apiUrlEntry: action.form?.apiUrlEntry ?? null,
280
+ };
281
+ }
282
+ const resourcePath = String(metadata.resource?.path ?? metadata.table?.resourcePath ?? '').trim();
283
+ if (!resourcePath) {
284
+ return {
285
+ apiEndpointKey: action.form?.apiEndpointKey ?? null,
286
+ apiUrlEntry: action.form?.apiUrlEntry ?? null,
287
+ };
288
+ }
289
+ const idField = String(metadata.resource?.idField ?? 'id');
290
+ const resourceId = row?.[idField];
291
+ const resolved = this.operationResolver.resolve({
292
+ operation: action.action,
293
+ resourcePath,
294
+ resourceId: resourceId ?? null,
295
+ capabilities: runtime?.capabilities ?? null,
296
+ links: runtime?.links ?? null,
297
+ explicit: null,
298
+ endpointKey: action.form?.apiEndpointKey ??
299
+ metadata.resource?.endpointKey ??
300
+ undefined,
301
+ apiUrlEntry: action.form?.apiUrlEntry ?? null,
302
+ });
303
+ return {
304
+ schemaUrl: resolved?.schemaUrl ?? null,
305
+ submitUrl: resolved?.submitUrl ?? null,
306
+ submitMethod: resolved?.submitMethod ?? null,
307
+ apiEndpointKey: action.form?.apiEndpointKey ?? metadata.resource?.endpointKey ?? null,
308
+ apiUrlEntry: action.form?.apiUrlEntry ?? null,
309
+ };
310
+ }
311
+ resolveRuntimeContract(action, row, metadata, runtime) {
312
+ return this.resolveActionFormContract(action, row, metadata, runtime);
313
+ }
314
+ buildInferredFormId(action, metadata) {
315
+ const resourcePath = String(metadata.resource?.path ?? metadata.table?.resourcePath ?? '').trim();
316
+ if (!resourcePath) {
317
+ return undefined;
318
+ }
319
+ const sanitizedResource = resourcePath
320
+ .replace(/^\/+/, '')
321
+ .replace(/\/+$/, '')
322
+ .replace(/[^a-zA-Z0-9/_-]+/g, '')
323
+ .replace(/[\/_]+/g, '-');
324
+ return `${sanitizedResource || 'crud'}-${String(action.action || '').trim().toLowerCase()}`;
325
+ }
326
+ isCanonicalCrudAction(actionName) {
327
+ const normalized = String(actionName || '').trim().toLowerCase();
328
+ return normalized === 'create' || normalized === 'view' || normalized === 'edit' || normalized === 'delete';
329
+ }
330
+ isExplicitCrudAction(action) {
331
+ if (action.mode === 'explicit') {
332
+ return true;
333
+ }
334
+ return !!(String(action.form?.schemaUrl || '').trim() ||
335
+ String(action.form?.submitUrl || '').trim() ||
336
+ String(action.form?.submitMethod || '').trim());
337
+ }
213
338
  async mergeCrudOverrides(metadata, action, componentKeyId) {
214
339
  try {
215
340
  if (!componentKeyId)
@@ -265,6 +390,294 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
265
390
  args: [{ providedIn: 'root' }]
266
391
  }] });
267
392
 
393
+ const DOCUMENT_KIND = 'praxis.crud.editor';
394
+ const DOCUMENT_VERSION = 1;
395
+ const CANONICAL_ACTIONS = ['create', 'view', 'edit', 'delete'];
396
+ const OPEN_MODES$1 = ['route', 'modal', 'drawer'];
397
+ function createCrudAuthoringDocument(source) {
398
+ return normalizeCrudAuthoringDocument({
399
+ kind: DOCUMENT_KIND,
400
+ version: DOCUMENT_VERSION,
401
+ metadata: asCrudMetadata(source?.metadata),
402
+ });
403
+ }
404
+ function parseLegacyOrCrudDocument(raw) {
405
+ const obj = asRecord(raw);
406
+ if (obj.kind === DOCUMENT_KIND && obj.version === DOCUMENT_VERSION) {
407
+ return normalizeCrudAuthoringDocument({
408
+ kind: DOCUMENT_KIND,
409
+ version: DOCUMENT_VERSION,
410
+ metadata: asCrudMetadata(obj.metadata),
411
+ });
412
+ }
413
+ if (obj.document) {
414
+ return parseLegacyOrCrudDocument(obj.document);
415
+ }
416
+ if (isCrudMetadataLike(obj)) {
417
+ return createCrudAuthoringDocument({ metadata: obj });
418
+ }
419
+ if (isCrudMetadataLike(asRecord(obj.metadata))) {
420
+ return createCrudAuthoringDocument({ metadata: obj.metadata });
421
+ }
422
+ return createCrudAuthoringDocument({ metadata: raw });
423
+ }
424
+ function normalizeCrudAuthoringDocument(doc) {
425
+ return {
426
+ kind: DOCUMENT_KIND,
427
+ version: DOCUMENT_VERSION,
428
+ metadata: normalizeCrudMetadata(doc?.metadata),
429
+ };
430
+ }
431
+ function serializeCrudAuthoringDocument(doc) {
432
+ const normalized = normalizeCrudAuthoringDocument(doc);
433
+ return stripUndefinedDeep({
434
+ kind: DOCUMENT_KIND,
435
+ version: DOCUMENT_VERSION,
436
+ metadata: normalized.metadata,
437
+ });
438
+ }
439
+ function validateCrudAuthoringDocument(doc, context) {
440
+ const diagnostics = [];
441
+ const normalized = normalizeCrudAuthoringDocument(doc);
442
+ const metadata = normalized.metadata;
443
+ if (metadata.component !== 'praxis-crud') {
444
+ diagnostics.push(errorDiagnostic('crud.metadata.component.invalid', 'metadata.component must be praxis-crud', 'metadata.component'));
445
+ }
446
+ if (!metadata.table || typeof metadata.table !== 'object') {
447
+ diagnostics.push(errorDiagnostic('crud.metadata.table.required', 'metadata.table is required', 'metadata.table'));
448
+ }
449
+ if (metadata.resource &&
450
+ 'path' in metadata.resource &&
451
+ !trimString(metadata.resource.path)) {
452
+ diagnostics.push(errorDiagnostic('crud.metadata.resource.path.required', 'metadata.resource.path must not be empty when resource is provided', 'metadata.resource.path'));
453
+ }
454
+ if (!trimString(metadata.resource?.path) && !Array.isArray(metadata.data)) {
455
+ diagnostics.push(warnDiagnostic('crud.metadata.resource-or-data.missing', 'Provide metadata.resource.path or metadata.data so the CRUD can resolve its source', 'metadata.resource.path'));
456
+ }
457
+ if (context?.requireCanonicalActions) {
458
+ for (const actionName of CANONICAL_ACTIONS) {
459
+ if (!findCrudAction(metadata.actions, actionName)) {
460
+ diagnostics.push(infoDiagnostic(`crud.metadata.actions.${actionName}.missing`, `Canonical CRUD action ${actionName} is not declared yet`, `metadata.actions.${actionName}`));
461
+ }
462
+ }
463
+ }
464
+ for (const action of metadata.actions || []) {
465
+ validateCrudAction(action, metadata, diagnostics);
466
+ }
467
+ return diagnostics;
468
+ }
469
+ function findCrudAction(actions, actionName) {
470
+ return (actions || []).find((action) => action?.action === actionName);
471
+ }
472
+ function validateCrudAction(action, metadata, diagnostics) {
473
+ const actionName = trimString(action?.action) || 'unknown';
474
+ const effectiveOpenMode = normalizeOpenMode(action?.openMode ?? metadata.defaults?.openMode) || 'route';
475
+ const inferredCanonical = isCanonicalCrudAction$1(action?.action) && !isExplicitCrudAction$1(action);
476
+ if (action.openMode && !normalizeOpenMode(action.openMode)) {
477
+ diagnostics.push(errorDiagnostic('crud.metadata.actions.openMode.invalid', 'action.openMode must be route, modal or drawer', `metadata.actions.${actionName}.openMode`));
478
+ }
479
+ if (effectiveOpenMode === 'route' && !trimString(action.route)) {
480
+ diagnostics.push(errorDiagnostic('crud.metadata.actions.route.required', 'route is required when the effective open mode is route', `metadata.actions.${actionName}.route`));
481
+ }
482
+ if ((effectiveOpenMode === 'modal' || effectiveOpenMode === 'drawer') &&
483
+ !trimString(action.formId) &&
484
+ !inferredCanonical) {
485
+ diagnostics.push(errorDiagnostic('crud.metadata.actions.formId.required', 'formId is required when the effective open mode is modal or drawer', `metadata.actions.${actionName}.formId`));
486
+ }
487
+ const hasSubmitUrl = !!trimString(action.form?.submitUrl);
488
+ const hasSubmitMethod = !!trimString(action.form?.submitMethod);
489
+ if (hasSubmitUrl !== hasSubmitMethod) {
490
+ diagnostics.push(errorDiagnostic('crud.metadata.actions.form.submit-contract.invalid', 'form.submitUrl and form.submitMethod must be provided together', `metadata.actions.${actionName}.form`));
491
+ }
492
+ }
493
+ function normalizeCrudMetadata(metadata) {
494
+ const base = cloneJson(metadata || {
495
+ component: 'praxis-crud',
496
+ table: { columns: [] },
497
+ });
498
+ const hasExplicitComponent = Object.prototype.hasOwnProperty.call(base, 'component');
499
+ const hasExplicitTable = Object.prototype.hasOwnProperty.call(base, 'table');
500
+ const normalized = {
501
+ ...base,
502
+ component: hasExplicitComponent && typeof base.component === 'string'
503
+ ? base.component
504
+ : 'praxis-crud',
505
+ table: (hasExplicitTable ? base.table : { columns: [] }),
506
+ };
507
+ if (base.resource && typeof base.resource === 'object') {
508
+ normalized.resource = stripUndefinedShallow({
509
+ path: trimString(base.resource.path) || undefined,
510
+ idField: typeof base.resource.idField === 'string'
511
+ ? trimString(base.resource.idField) || undefined
512
+ : base.resource.idField,
513
+ endpointKey: trimString(base.resource.endpointKey) || undefined,
514
+ });
515
+ }
516
+ if (base.defaults && typeof base.defaults === 'object') {
517
+ normalized.defaults = stripUndefinedDeep({
518
+ ...base.defaults,
519
+ openMode: normalizeOpenMode(base.defaults.openMode),
520
+ back: normalizeBackConfig(base.defaults.back),
521
+ header: base.defaults.header
522
+ ? {
523
+ ...base.defaults.header,
524
+ backLabel: trimString(base.defaults.header.backLabel) || undefined,
525
+ variant: normalizeHeaderVariant(base.defaults.header.variant),
526
+ }
527
+ : undefined,
528
+ });
529
+ }
530
+ if (Array.isArray(base.actions)) {
531
+ normalized.actions = [...base.actions]
532
+ .map((action) => normalizeCrudAction(action))
533
+ .sort(compareCrudActions);
534
+ }
535
+ return stripUndefinedDeep(normalized);
536
+ }
537
+ function normalizeCrudAction(action) {
538
+ return stripUndefinedDeep({
539
+ ...action,
540
+ action: trimString(action?.action) || action?.action,
541
+ id: trimString(action?.id) || undefined,
542
+ label: trimString(action?.label) || undefined,
543
+ openMode: normalizeOpenMode(action?.openMode),
544
+ route: trimString(action?.route) || undefined,
545
+ formId: trimString(action?.formId) || undefined,
546
+ back: normalizeBackConfig(action?.back),
547
+ params: Array.isArray(action?.params)
548
+ ? action.params
549
+ .map((param) => stripUndefinedDeep({
550
+ from: trimString(param?.from) || undefined,
551
+ to: normalizeParamTarget(param?.to),
552
+ name: trimString(param?.name) || undefined,
553
+ }))
554
+ .filter((param) => !!(param.from || param.to || param.name))
555
+ : undefined,
556
+ form: action?.form
557
+ ? {
558
+ schemaUrl: trimString(action.form.schemaUrl) || undefined,
559
+ submitUrl: trimString(action.form.submitUrl) || undefined,
560
+ submitMethod: trimString(action.form.submitMethod) || undefined,
561
+ apiEndpointKey: trimString(action.form.apiEndpointKey) || undefined,
562
+ apiUrlEntry: action.form.apiUrlEntry || undefined,
563
+ initialValue: normalizeInitialValue(action.form.initialValue),
564
+ }
565
+ : undefined,
566
+ });
567
+ }
568
+ function isCanonicalCrudAction$1(actionName) {
569
+ const normalized = trimString(actionName) || '';
570
+ return CANONICAL_ACTIONS.includes(normalized);
571
+ }
572
+ function isExplicitCrudAction$1(action) {
573
+ if (action?.mode === 'explicit') {
574
+ return true;
575
+ }
576
+ return !!(trimString(action?.form?.schemaUrl) ||
577
+ trimString(action?.form?.submitUrl) ||
578
+ trimString(action?.form?.submitMethod));
579
+ }
580
+ function compareCrudActions(left, right) {
581
+ return resolveActionWeight(left?.action) - resolveActionWeight(right?.action);
582
+ }
583
+ function resolveActionWeight(actionName) {
584
+ const normalized = trimString(actionName) || '';
585
+ const index = CANONICAL_ACTIONS.indexOf(normalized);
586
+ return index >= 0 ? index : CANONICAL_ACTIONS.length + normalized.localeCompare('');
587
+ }
588
+ function normalizeOpenMode(value) {
589
+ const trimmed = trimString(value);
590
+ return OPEN_MODES$1.includes(trimmed)
591
+ ? trimmed
592
+ : undefined;
593
+ }
594
+ function normalizeHeaderVariant(value) {
595
+ const trimmed = trimString(value);
596
+ return trimmed && ['ghost', 'tonal', 'outlined'].includes(trimmed)
597
+ ? trimmed
598
+ : undefined;
599
+ }
600
+ function normalizeBackConfig(value) {
601
+ if (!value || typeof value !== 'object') {
602
+ return undefined;
603
+ }
604
+ const back = value;
605
+ const strategy = trimString(back['strategy']);
606
+ return stripUndefinedDeep({
607
+ strategy: strategy && ['auto', 'close', 'navigate'].includes(strategy) ? strategy : undefined,
608
+ returnTo: trimString(back['returnTo']) || undefined,
609
+ confirmOnDirty: typeof back['confirmOnDirty'] === 'boolean' ? back['confirmOnDirty'] : undefined,
610
+ });
611
+ }
612
+ function normalizeParamTarget(value) {
613
+ const trimmed = trimString(value);
614
+ return trimmed && ['routeParam', 'query', 'input'].includes(trimmed)
615
+ ? trimmed
616
+ : undefined;
617
+ }
618
+ function normalizeInitialValue(value) {
619
+ return value && typeof value === 'object' && !Array.isArray(value)
620
+ ? cloneJson(value)
621
+ : undefined;
622
+ }
623
+ function isCrudMetadataLike(value) {
624
+ return value.component === 'praxis-crud';
625
+ }
626
+ function asCrudMetadata(raw) {
627
+ return (raw && typeof raw === 'object'
628
+ ? raw
629
+ : { component: 'praxis-crud', table: { columns: [] } });
630
+ }
631
+ function asRecord(raw) {
632
+ return raw && typeof raw === 'object' ? raw : {};
633
+ }
634
+ function trimString(raw) {
635
+ if (typeof raw !== 'string')
636
+ return undefined;
637
+ const trimmed = raw.trim();
638
+ return trimmed ? trimmed : undefined;
639
+ }
640
+ function cloneJson(value) {
641
+ return value == null ? value : JSON.parse(JSON.stringify(value));
642
+ }
643
+ function stripUndefinedShallow(value) {
644
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
645
+ }
646
+ function stripUndefinedDeep(value) {
647
+ if (Array.isArray(value)) {
648
+ return value
649
+ .map((entry) => stripUndefinedDeep(entry))
650
+ .filter((entry) => entry !== undefined);
651
+ }
652
+ if (value && typeof value === 'object') {
653
+ return Object.fromEntries(Object.entries(value)
654
+ .map(([key, entry]) => [key, stripUndefinedDeep(entry)])
655
+ .filter(([, entry]) => entry !== undefined));
656
+ }
657
+ return value;
658
+ }
659
+ function errorDiagnostic(code, message, path) {
660
+ return { level: 'error', code, message, path };
661
+ }
662
+ function warnDiagnostic(code, message, path) {
663
+ return { level: 'warning', code, message, path };
664
+ }
665
+ function infoDiagnostic(code, message, path) {
666
+ return { level: 'info', code, message, path };
667
+ }
668
+
669
+ function isCanonicalCrudAction(actionName) {
670
+ const normalized = String(actionName || '').trim().toLowerCase();
671
+ return normalized === 'create' || normalized === 'view' || normalized === 'edit' || normalized === 'delete';
672
+ }
673
+ function isExplicitCrudAction(action) {
674
+ if (action.mode === 'explicit') {
675
+ return true;
676
+ }
677
+ return !!(String(action.form?.schemaUrl || '').trim() ||
678
+ String(action.form?.submitUrl || '').trim() ||
679
+ String(action.form?.submitMethod || '').trim());
680
+ }
268
681
  function assertCrudMetadata(meta, options = {}) {
269
682
  if (meta.component !== 'praxis-crud') {
270
683
  throw new Error('Invalid component type for CRUD metadata');
@@ -274,12 +687,14 @@ function assertCrudMetadata(meta, options = {}) {
274
687
  }
275
688
  meta.actions?.forEach((action) => {
276
689
  const mode = action.openMode ?? meta.defaults?.openMode ?? 'route';
690
+ const inferredCanonical = !isExplicitCrudAction(action) && isCanonicalCrudAction(action.action);
277
691
  if (!options.allowDeferredActionBindings && mode === 'route' && !action.route) {
278
692
  throw new Error(`Route not provided for action ${action.action}`);
279
693
  }
280
694
  if (!options.allowDeferredActionBindings &&
281
695
  (mode === 'modal' || mode === 'drawer') &&
282
- !action.formId) {
696
+ !action.formId &&
697
+ !inferredCanonical) {
283
698
  throw new Error(`formId not provided for action ${action.action}`);
284
699
  }
285
700
  action.params?.forEach((p) => {
@@ -287,6 +702,11 @@ function assertCrudMetadata(meta, options = {}) {
287
702
  throw new Error(`Invalid param mapping target: ${p.to}`);
288
703
  }
289
704
  });
705
+ const hasSubmitUrl = !!String(action.form?.submitUrl || '').trim();
706
+ const hasSubmitMethod = !!String(action.form?.submitMethod || '').trim();
707
+ if (hasSubmitUrl !== hasSubmitMethod) {
708
+ throw new Error(`Crud action ${action.action} requires form.submitUrl and form.submitMethod together`);
709
+ }
290
710
  });
291
711
  }
292
712
 
@@ -303,6 +723,8 @@ const PRAXIS_CRUD_RUNTIME_I18N_CONFIG = {
303
723
  'crud.actions.view': 'Ver',
304
724
  'crud.actions.edit': 'Editar',
305
725
  'crud.actions.delete': 'Excluir',
726
+ 'crud.delete.confirmMessage': 'Esta ação não pode ser desfeita. Deseja continuar?',
727
+ 'crud.delete.cancel': 'Cancelar',
306
728
  },
307
729
  'en-US': {
308
730
  'crud.emptyState.title': 'Connect CRUD to a resource',
@@ -313,6 +735,8 @@ const PRAXIS_CRUD_RUNTIME_I18N_CONFIG = {
313
735
  'crud.actions.view': 'View',
314
736
  'crud.actions.edit': 'Edit',
315
737
  'crud.actions.delete': 'Delete',
738
+ 'crud.delete.confirmMessage': 'This action cannot be undone. Do you want to continue?',
739
+ 'crud.delete.cancel': 'Cancel',
316
740
  },
317
741
  },
318
742
  },
@@ -352,6 +776,2526 @@ function translateCrudRuntimeText(i18n, key, fallback, params, locale) {
352
776
  return i18n.t(key, params, runtimeFallback, PRAXIS_CRUD_RUNTIME_I18N_NAMESPACE);
353
777
  }
354
778
 
779
+ const PRAXIS_CRUD_AUTHORING_I18N_NAMESPACE = 'praxisCrudAuthoring';
780
+ const PRAXIS_CRUD_AUTHORING_I18N_CONFIG = {
781
+ namespaces: {
782
+ [PRAXIS_CRUD_AUTHORING_I18N_NAMESPACE]: {
783
+ 'pt-BR': {
784
+ 'crud.authoring.title': 'Configurações do CRUD',
785
+ 'crud.authoring.subtitle': 'Edite o documento canônico do fluxo CRUD sem deslocar a semântica para o host.',
786
+ 'crud.authoring.section.connection': 'Conexão',
787
+ 'crud.authoring.section.actions': 'Ações',
788
+ 'crud.authoring.section.defaults': 'Abertura e cabeçalho',
789
+ 'crud.authoring.section.table': 'Tabela',
790
+ 'crud.authoring.section.json': 'JSON',
791
+ 'crud.authoring.overview.connection': 'Conexão',
792
+ 'crud.authoring.overview.defaults': 'Defaults',
793
+ 'crud.authoring.overview.actions': 'Ações',
794
+ 'crud.authoring.overview.table': 'Tabela',
795
+ 'crud.authoring.overview.connectionMissing': 'Recurso ainda não configurado',
796
+ 'crud.authoring.overview.connectionIdField': 'ID: {value}',
797
+ 'crud.authoring.overview.connectionEndpoint': 'API: {value}',
798
+ 'crud.authoring.overview.connectionDetailsPending': 'Identificador e endpoint ainda são opcionais aqui.',
799
+ 'crud.authoring.overview.backVisible': 'Voltar visível',
800
+ 'crud.authoring.overview.backHidden': 'Voltar oculto',
801
+ 'crud.authoring.overview.modalDensity': 'Modal: {value}',
802
+ 'crud.authoring.overview.backStrategy': 'Voltar: {value}',
803
+ 'crud.authoring.overview.modalRememberState': 'Estado lembrado',
804
+ 'crud.authoring.overview.backConfirmOnDirty': 'Confirmar ao sair',
805
+ 'crud.authoring.overview.headerSticky': 'Cabeçalho fixo',
806
+ 'crud.authoring.overview.headerBreadcrumbs': 'Breadcrumbs',
807
+ 'crud.authoring.overview.columns': 'colunas',
808
+ 'crud.authoring.overview.columnsCount': '{count} colunas',
809
+ 'crud.authoring.overview.validation': 'Validação',
810
+ 'crud.authoring.overview.validationErrors': '{count} erros',
811
+ 'crud.authoring.overview.validationWarnings': '{count} alertas',
812
+ 'crud.authoring.overview.validationClean': 'Sem diagnósticos bloqueantes',
813
+ 'crud.authoring.overview.validationGrouped': 'Agrupado por seção abaixo para troubleshooting mais rápido.',
814
+ 'crud.authoring.overview.validationAligned': 'Orientação da shell e diagnósticos estão alinhados.',
815
+ 'crud.authoring.overview.actionsReady': '{count} prontas',
816
+ 'crud.authoring.overview.actionsPending': '{count} pendentes',
817
+ 'crud.authoring.overview.actionsInvalid': '{count} inválidas',
818
+ 'crud.authoring.overview.actionsTracked': '{count} blocos de ação rastreados',
819
+ 'crud.authoring.overview.actionsNext': 'Próxima: {action}. {summary}',
820
+ 'crud.authoring.section.status.ready': 'Pronta',
821
+ 'crud.authoring.section.status.pending': 'Pede atenção',
822
+ 'crud.authoring.section.status.invalid': 'Inválida',
823
+ 'crud.authoring.section.actions.invalidSummary': '{count} blocos de ação têm erros de validação.',
824
+ 'crud.authoring.section.actions.readySummary': 'Todas as ações principais do CRUD estão configuradas.',
825
+ 'crud.authoring.section.actions.pendingSummary': '{count} de {total} blocos de ação estão prontos.',
826
+ 'crud.authoring.section.actions.invalidSummaryDetailed': '{count} blocos de ação têm erros de validação. Comece por {action}.',
827
+ 'crud.authoring.section.actions.pendingSummaryDetailed': '{count} de {total} blocos de ação estão prontos. Próxima: {action}.',
828
+ 'crud.authoring.nextFocus.connection': 'Próximo foco: conexão',
829
+ 'crud.authoring.nextFocus.defaults': 'Próximo foco: defaults',
830
+ 'crud.authoring.nextFocus.actions': 'Próximo foco: ações',
831
+ 'crud.authoring.nextFocus.table': 'Próximo foco: tabela',
832
+ 'crud.authoring.nextFocus.invalid': 'Resolva os diagnósticos bloqueantes desta seção antes de seguir com confiança.',
833
+ 'crud.authoring.nextFocus.pending': 'Esta é a próxima melhor seção para completar o fluxo CRUD canônico.',
834
+ 'crud.authoring.nextFocus.actionsInvalid': 'Resolva os diagnósticos bloqueantes em {action}. {summary}.',
835
+ 'crud.authoring.nextFocus.actionsPending': 'Complete {action} na sequência. {summary}.',
836
+ 'crud.authoring.nextFocus.ready': 'Todas as seções rastreadas estão em bom estado.',
837
+ 'crud.authoring.nextFocus.complete': 'Concluído',
838
+ 'crud.authoring.nextFocus.completeTitle': 'Orientação da shell concluída',
839
+ 'crud.authoring.health.invalid': 'Inválidas',
840
+ 'crud.authoring.health.pending': 'Pedem atenção',
841
+ 'crud.authoring.health.ready': 'Saudáveis',
842
+ 'crud.authoring.health.invalidEmpty': 'Nenhuma seção inválida',
843
+ 'crud.authoring.health.pendingEmpty': 'Nenhuma seção pendente',
844
+ 'crud.authoring.health.readyEmpty': 'Nenhuma seção pronta ainda',
845
+ 'crud.authoring.diagnostics.errors': 'Erros',
846
+ 'crud.authoring.diagnostics.warnings': 'Alertas',
847
+ 'crud.authoring.diagnostics.errorSummary': '{count} diagnósticos bloqueantes nesta seção.',
848
+ 'crud.authoring.diagnostics.warningSummary': '{count} diagnósticos que valem revisão nesta seção.',
849
+ 'crud.authoring.connection.resourcePath': 'Recurso',
850
+ 'crud.authoring.connection.resourcePath.placeholder': 'ex.: api/human-resources/cargos',
851
+ 'crud.authoring.connection.idField': 'Campo identificador',
852
+ 'crud.authoring.connection.idField.placeholder': 'ex.: id',
853
+ 'crud.authoring.connection.endpointKey': 'Endpoint/API',
854
+ 'crud.authoring.defaults.openMode': 'Modo de abertura padrão',
855
+ 'crud.authoring.defaults.header.showBack': 'Exibir botão Voltar',
856
+ 'crud.authoring.defaults.header.backLabel': 'Texto do botão Voltar',
857
+ 'crud.authoring.defaults.header.variant': 'Variante do botão',
858
+ 'crud.authoring.defaults.header.sticky': 'Cabeçalho fixo',
859
+ 'crud.authoring.defaults.header.breadcrumbs': 'Exibir breadcrumbs',
860
+ 'crud.authoring.defaults.header.divider': 'Exibir divisor inferior',
861
+ 'crud.authoring.defaults.modal.group': 'Apresentação do modal',
862
+ 'crud.authoring.defaults.modal.density': 'Densidade do modal',
863
+ 'crud.authoring.defaults.modal.density.default': 'Padrão',
864
+ 'crud.authoring.defaults.modal.density.compact': 'Compacta',
865
+ 'crud.authoring.defaults.modal.fullscreenBreakpoint': 'Breakpoint para fullscreen',
866
+ 'crud.authoring.defaults.modal.canMaximize': 'Exibir controle de maximizar',
867
+ 'crud.authoring.defaults.modal.startMaximized': 'Iniciar maximizado',
868
+ 'crud.authoring.defaults.modal.rememberLastState': 'Lembrar o último tamanho do modal',
869
+ 'crud.authoring.defaults.modal.disableCloseOnEsc': 'Bloquear fechamento por Escape',
870
+ 'crud.authoring.defaults.modal.disableCloseOnBackdrop': 'Bloquear fechamento por backdrop',
871
+ 'crud.authoring.defaults.modal.noteDensity': 'Densidade: {value}',
872
+ 'crud.authoring.defaults.modal.noteNoMaximize': 'Maximizar oculto',
873
+ 'crud.authoring.defaults.modal.noteStartMaximized': 'Abre maximizado',
874
+ 'crud.authoring.defaults.modal.noteRememberState': 'Lembra o último tamanho',
875
+ 'crud.authoring.defaults.modal.noteEscLocked': 'Escape bloqueado',
876
+ 'crud.authoring.defaults.modal.noteBackdropLocked': 'Backdrop bloqueado',
877
+ 'crud.authoring.defaults.modal.noteBreakpoint': 'Fullscreen em {value}px',
878
+ 'crud.authoring.defaults.back.group': 'Comportamento de voltar',
879
+ 'crud.authoring.defaults.back.strategy': 'Estratégia de voltar',
880
+ 'crud.authoring.defaults.back.strategy.auto': 'Auto',
881
+ 'crud.authoring.defaults.back.strategy.close': 'Fechar',
882
+ 'crud.authoring.defaults.back.strategy.navigate': 'Navegar',
883
+ 'crud.authoring.defaults.back.returnTo': 'Rota de retorno',
884
+ 'crud.authoring.defaults.back.confirmOnDirty': 'Confirmar ao sair de formulários alterados',
885
+ 'crud.authoring.defaults.back.noteStrategy': 'Estratégia: {value}',
886
+ 'crud.authoring.defaults.back.noteReturnTo': 'Retorno: {value}',
887
+ 'crud.authoring.defaults.back.noteConfirmOnDirty': 'Confirmação ao sair',
888
+ 'crud.authoring.action.mode': 'Modo de abertura',
889
+ 'crud.authoring.action.route': 'Rota',
890
+ 'crud.authoring.action.route.placeholder': 'ex.: /cargos/view/:id',
891
+ 'crud.authoring.action.formId': 'formId',
892
+ 'crud.authoring.action.formId.placeholder': 'ex.: cargos-edit',
893
+ 'crud.authoring.action.schemaUrl': 'Schema URL',
894
+ 'crud.authoring.action.submitUrl': 'Submit URL',
895
+ 'crud.authoring.action.submitMethod': 'Método de submit',
896
+ 'crud.authoring.action.apiEndpointKey': 'API endpoint key',
897
+ 'crud.authoring.action.apiUrlEntry': 'API URL entry',
898
+ 'crud.authoring.action.advanced': 'Detalhes de submit e API',
899
+ 'crud.authoring.action.bindingGroup': 'Vínculo',
900
+ 'crud.authoring.action.schemaGroup': 'Contrato de schema',
901
+ 'crud.authoring.action.submitGroup': 'Contrato de submit',
902
+ 'crud.authoring.action.apiGroup': 'Mapeamento de API',
903
+ 'crud.authoring.action.inputsGroup': 'Seed e mapeamento de inputs',
904
+ 'crud.authoring.action.inputsNote': 'Opcional: use Parâmetros para valores derivados da linha e Valor inicial apenas para seed fixo do formulário.',
905
+ 'crud.authoring.action.paramsSubgroup': 'Mapeamentos derivados da linha',
906
+ 'crud.authoring.action.initialValueSubgroup': 'Seed fixo do formulário',
907
+ 'crud.authoring.action.backGroup': 'Override do comportamento de voltar',
908
+ 'crud.authoring.action.backStrategy': 'Override da estratégia de voltar',
909
+ 'crud.authoring.action.backReturnTo': 'Rota de retorno da ação',
910
+ 'crud.authoring.action.backConfirmOnDirty': 'Confirmar ao sair de formulários alterados',
911
+ 'crud.authoring.action.backUseDefaults': 'Usar defaults do CRUD',
912
+ 'crud.authoring.action.backUsesDefaults': 'Usando os defaults do CRUD para voltar',
913
+ 'crud.authoring.action.backNoteStrategy': 'Estratégia: {value}',
914
+ 'crud.authoring.action.backNoteReturnTo': 'Retorno: {value}',
915
+ 'crud.authoring.action.backNoteConfirmOnDirty': 'Confirmação ao sair',
916
+ 'crud.authoring.action.paramFrom': 'Campo de origem',
917
+ 'crud.authoring.action.paramTo': 'Destino',
918
+ 'crud.authoring.action.paramName': 'Nome no destino',
919
+ 'crud.authoring.action.paramAdd': 'Adicionar mapeamento',
920
+ 'crud.authoring.action.paramRemove': 'Remover mapeamento',
921
+ 'crud.authoring.action.paramTarget.routeParam': 'Parâmetro de rota',
922
+ 'crud.authoring.action.paramTarget.query': 'Query string',
923
+ 'crud.authoring.action.paramTarget.input': 'Input do formulário',
924
+ 'crud.authoring.action.initialValue': 'Seed inicial do formulário (JSON)',
925
+ 'crud.authoring.action.paramsSummary': '{count} mapeamentos',
926
+ 'crud.authoring.action.paramsSummaryCompact': '{count} map.',
927
+ 'crud.authoring.action.paramsNone': 'Nenhum mapeamento de parâmetro ainda',
928
+ 'crud.authoring.action.paramsNoteEmpty': 'Mapeie campos da linha atual para parâmetros de rota, query string ou inputs do formulário.',
929
+ 'crud.authoring.action.paramsNoteConfigured': '{count} mapeamentos para {targets}.',
930
+ 'crud.authoring.action.paramsNoteReady': 'Mapeamentos prontos',
931
+ 'crud.authoring.action.initialValueReady': 'Seed fixo do formulário configurado',
932
+ 'crud.authoring.action.initialValueReadyCompact': 'Seed',
933
+ 'crud.authoring.action.initialValueEmpty': 'Nenhum seed fixo do formulário ainda',
934
+ 'crud.authoring.action.initialValueInvalid': 'O JSON do seed inicial ainda não é válido',
935
+ 'crud.authoring.action.initialValueNoteConfigured': 'Objeto injetado em inputs.initialValue antes da abertura do formulário.',
936
+ 'crud.authoring.action.initialValueNoteReady': 'Seed fixo pronto',
937
+ 'crud.authoring.action.initialValueNoteEmpty': 'Use isso apenas para valores fixos que não vêm da linha selecionada.',
938
+ 'crud.authoring.action.binding': 'Vínculo',
939
+ 'crud.authoring.action.bindingMissing': 'Vínculo ainda não configurado',
940
+ 'crud.authoring.action.schema': 'Schema',
941
+ 'crud.authoring.action.schemaPending': 'Contrato de schema ainda não configurado',
942
+ 'crud.authoring.action.api': 'API',
943
+ 'crud.authoring.action.apiEntryReady': 'API URL entry customizado configurado',
944
+ 'crud.authoring.action.apiPending': 'Mapeamento de API ainda não configurado',
945
+ 'crud.authoring.action.groupsInvalid': 'Inválido: {groups}',
946
+ 'crud.authoring.action.groupsPending': 'Pendente: {groups}',
947
+ 'crud.authoring.action.groupsReady': 'Todos os grupos prontos',
948
+ 'crud.authoring.action.primaryInvalid': 'Resolver: {groups}',
949
+ 'crud.authoring.action.primaryPending': 'Próximo: {groups}',
950
+ 'crud.authoring.action.primaryReady': 'Núcleo pronto',
951
+ 'crud.authoring.action.advancedPending': 'Pendente: {groups}',
952
+ 'crud.authoring.action.advancedReady': 'Avançado pronto',
953
+ 'crud.authoring.action.editorialInvalid': 'Resolver: {groups}',
954
+ 'crud.authoring.action.editorialPending': 'Fechar: {groups}',
955
+ 'crud.authoring.action.editorialReady': 'Pronta para uso',
956
+ 'crud.authoring.action.submitPending': 'Detalhes de submit/API ainda não configurados',
957
+ 'crud.authoring.action.status.ready': 'Pronta',
958
+ 'crud.authoring.action.status.pending': 'Pendente',
959
+ 'crud.authoring.action.status.invalid': 'Inválida',
960
+ 'crud.authoring.mode.route': 'Rota',
961
+ 'crud.authoring.mode.modal': 'Modal',
962
+ 'crud.authoring.mode.drawer': 'Drawer',
963
+ 'crud.authoring.header.variant.ghost': 'Ghost',
964
+ 'crud.authoring.header.variant.tonal': 'Tonal',
965
+ 'crud.authoring.header.variant.outlined': 'Outlined',
966
+ 'crud.authoring.submitMethod.post': 'POST',
967
+ 'crud.authoring.submitMethod.put': 'PUT',
968
+ 'crud.authoring.submitMethod.patch': 'PATCH',
969
+ 'crud.authoring.action.create': 'Criar',
970
+ 'crud.authoring.action.view': 'Visualizar',
971
+ 'crud.authoring.action.edit': 'Editar',
972
+ 'crud.authoring.table.summary': 'A configuração detalhada da tabela continua sendo tratada pela superfície canônica de TableConfig. Nesta fase, o editor de CRUD preserva o snapshot atual da tabela sem duplicar o editor completo.',
973
+ 'crud.authoring.json.hint': 'O JSON abaixo representa o documento canônico de authoring do CRUD.',
974
+ 'crud.authoring.table.structureSummary': '{count} colunas | {density}',
975
+ 'crud.authoring.table.paginationSummary': 'Página com {value}',
976
+ 'crud.authoring.table.paginationOff': 'Paginação desativada',
977
+ 'crud.authoring.table.sortingClient': 'Ordenação client-side',
978
+ 'crud.authoring.table.sortingServer': 'Ordenação server-side',
979
+ 'crud.authoring.table.sortingOff': 'Ordenação desativada',
980
+ 'crud.authoring.table.statesDefault': 'Textos padrão',
981
+ 'crud.authoring.table.statesCustom': '{count} estados customizados',
982
+ 'crud.authoring.table.sectionSummary': '{structure}. {behavior}. {states}.',
983
+ 'crud.authoring.table.flowSummary': 'A apresentação principal continua visível abaixo. Paginação, ordenação, textos de estado e colunas ficam nos painéis avançados da tabela.',
984
+ 'crud.authoring.table.density.compact': 'Compacta',
985
+ 'crud.authoring.table.density.comfortable': 'Confortável',
986
+ 'crud.authoring.table.density.spacious': 'Espaçada',
987
+ 'crud.authoring.json.summaryErrors': 'O documento canônico ainda possui erros de validação.',
988
+ 'crud.authoring.json.summaryWarnings': 'O documento canônico possui diagnósticos que valem revisão.',
989
+ 'crud.authoring.json.summaryClean': 'O documento canônico está estruturalmente consistente.',
990
+ 'crud.authoring.validation.routeRequired': 'Informe uma rota quando o modo efetivo for route.',
991
+ 'crud.authoring.validation.formIdRequired': 'Informe um formId quando o modo efetivo for modal ou drawer.',
992
+ 'crud.authoring.validation.submitContract': 'submitUrl e submitMethod devem ser informados juntos.',
993
+ 'crud.authoring.overview.actionsNextCompact': 'Próxima: {action}. {summary}',
994
+ 'crud.authoring.action.focusInvalid': '{count} grupos bloqueando',
995
+ 'crud.authoring.action.focusPending': '{count} grupos pendentes',
996
+ 'crud.authoring.action.focusReady': 'Pronta',
997
+ 'crud.authoring.action.headerReady': 'Pronta',
998
+ 'crud.authoring.action.bindingReadyNote': 'Vínculo configurado',
999
+ 'crud.authoring.action.schemaReadyNote': 'Schema vinculado',
1000
+ 'crud.authoring.action.submitReadyNote': 'Contrato de submit pronto',
1001
+ 'crud.authoring.action.apiReadyNote': 'Mapeamento de API pronto',
1002
+ },
1003
+ 'en-US': {
1004
+ 'crud.authoring.title': 'CRUD settings',
1005
+ 'crud.authoring.subtitle': 'Edit the canonical CRUD flow document without shifting semantics to the host.',
1006
+ 'crud.authoring.section.connection': 'Connection',
1007
+ 'crud.authoring.section.actions': 'Actions',
1008
+ 'crud.authoring.section.defaults': 'Open mode and header',
1009
+ 'crud.authoring.section.table': 'Table',
1010
+ 'crud.authoring.section.json': 'JSON',
1011
+ 'crud.authoring.overview.connection': 'Connection',
1012
+ 'crud.authoring.overview.defaults': 'Defaults',
1013
+ 'crud.authoring.overview.actions': 'Actions',
1014
+ 'crud.authoring.overview.table': 'Table',
1015
+ 'crud.authoring.overview.connectionMissing': 'Resource not configured yet',
1016
+ 'crud.authoring.overview.connectionIdField': 'ID: {value}',
1017
+ 'crud.authoring.overview.connectionEndpoint': 'API: {value}',
1018
+ 'crud.authoring.overview.connectionDetailsPending': 'Identifier and endpoint details are still optional here.',
1019
+ 'crud.authoring.overview.backVisible': 'Back visible',
1020
+ 'crud.authoring.overview.backHidden': 'Back hidden',
1021
+ 'crud.authoring.overview.modalDensity': 'Modal: {value}',
1022
+ 'crud.authoring.overview.backStrategy': 'Back: {value}',
1023
+ 'crud.authoring.overview.modalRememberState': 'State remembered',
1024
+ 'crud.authoring.overview.backConfirmOnDirty': 'Dirty confirm',
1025
+ 'crud.authoring.overview.headerSticky': 'Sticky header',
1026
+ 'crud.authoring.overview.headerBreadcrumbs': 'Breadcrumbs',
1027
+ 'crud.authoring.overview.columns': 'columns',
1028
+ 'crud.authoring.overview.columnsCount': '{count} columns',
1029
+ 'crud.authoring.overview.validation': 'Validation',
1030
+ 'crud.authoring.overview.validationErrors': '{count} errors',
1031
+ 'crud.authoring.overview.validationWarnings': '{count} warnings',
1032
+ 'crud.authoring.overview.validationClean': 'No blocking diagnostics',
1033
+ 'crud.authoring.overview.validationGrouped': 'Grouped by section below for faster troubleshooting.',
1034
+ 'crud.authoring.overview.validationAligned': 'Shell guidance and diagnostics are aligned.',
1035
+ 'crud.authoring.overview.actionsReady': '{count} ready',
1036
+ 'crud.authoring.overview.actionsPending': '{count} pending',
1037
+ 'crud.authoring.overview.actionsInvalid': '{count} invalid',
1038
+ 'crud.authoring.overview.actionsTracked': '{count} tracked action blocks',
1039
+ 'crud.authoring.overview.actionsNext': 'Next: {action}. {summary}',
1040
+ 'crud.authoring.section.status.ready': 'Ready',
1041
+ 'crud.authoring.section.status.pending': 'Needs attention',
1042
+ 'crud.authoring.section.status.invalid': 'Invalid',
1043
+ 'crud.authoring.section.actions.invalidSummary': '{count} action blocks have validation errors.',
1044
+ 'crud.authoring.section.actions.readySummary': 'All primary CRUD actions are configured.',
1045
+ 'crud.authoring.section.actions.pendingSummary': '{count} of {total} action blocks are ready.',
1046
+ 'crud.authoring.section.actions.invalidSummaryDetailed': '{count} action blocks have validation errors. Start with {action}.',
1047
+ 'crud.authoring.section.actions.pendingSummaryDetailed': '{count} of {total} action blocks are ready. Next: {action}.',
1048
+ 'crud.authoring.nextFocus.connection': 'Next focus: connection',
1049
+ 'crud.authoring.nextFocus.defaults': 'Next focus: defaults',
1050
+ 'crud.authoring.nextFocus.actions': 'Next focus: actions',
1051
+ 'crud.authoring.nextFocus.table': 'Next focus: table',
1052
+ 'crud.authoring.nextFocus.invalid': 'Resolve the blocking diagnostics in this section before saving with confidence.',
1053
+ 'crud.authoring.nextFocus.pending': 'This section is the next best place to complete the canonical CRUD flow.',
1054
+ 'crud.authoring.nextFocus.actionsInvalid': 'Resolve the blocking diagnostics in {action}. {summary}.',
1055
+ 'crud.authoring.nextFocus.actionsPending': 'Complete {action} next. {summary}.',
1056
+ 'crud.authoring.nextFocus.ready': 'All tracked sections are currently in a good state.',
1057
+ 'crud.authoring.nextFocus.complete': 'Complete',
1058
+ 'crud.authoring.nextFocus.completeTitle': 'Shell guidance complete',
1059
+ 'crud.authoring.health.invalid': 'Invalid',
1060
+ 'crud.authoring.health.pending': 'Needs attention',
1061
+ 'crud.authoring.health.ready': 'Healthy',
1062
+ 'crud.authoring.health.invalidEmpty': 'No invalid sections',
1063
+ 'crud.authoring.health.pendingEmpty': 'No pending sections',
1064
+ 'crud.authoring.health.readyEmpty': 'No ready sections yet',
1065
+ 'crud.authoring.diagnostics.errors': 'Errors',
1066
+ 'crud.authoring.diagnostics.warnings': 'Warnings',
1067
+ 'crud.authoring.diagnostics.errorSummary': '{count} blocking diagnostics in this section.',
1068
+ 'crud.authoring.diagnostics.warningSummary': '{count} diagnostics worth reviewing in this section.',
1069
+ 'crud.authoring.connection.resourcePath': 'Resource',
1070
+ 'crud.authoring.connection.resourcePath.placeholder': 'e.g. api/human-resources/cargos',
1071
+ 'crud.authoring.connection.idField': 'Identifier field',
1072
+ 'crud.authoring.connection.idField.placeholder': 'e.g. id',
1073
+ 'crud.authoring.connection.endpointKey': 'Endpoint/API',
1074
+ 'crud.authoring.defaults.openMode': 'Default open mode',
1075
+ 'crud.authoring.defaults.header.showBack': 'Show back button',
1076
+ 'crud.authoring.defaults.header.backLabel': 'Back button label',
1077
+ 'crud.authoring.defaults.header.variant': 'Button variant',
1078
+ 'crud.authoring.defaults.header.sticky': 'Sticky header',
1079
+ 'crud.authoring.defaults.header.breadcrumbs': 'Show breadcrumbs',
1080
+ 'crud.authoring.defaults.header.divider': 'Show bottom divider',
1081
+ 'crud.authoring.defaults.modal.group': 'Modal presentation',
1082
+ 'crud.authoring.defaults.modal.density': 'Modal density',
1083
+ 'crud.authoring.defaults.modal.density.default': 'Default',
1084
+ 'crud.authoring.defaults.modal.density.compact': 'Compact',
1085
+ 'crud.authoring.defaults.modal.fullscreenBreakpoint': 'Fullscreen breakpoint',
1086
+ 'crud.authoring.defaults.modal.canMaximize': 'Show maximize control',
1087
+ 'crud.authoring.defaults.modal.startMaximized': 'Start maximized',
1088
+ 'crud.authoring.defaults.modal.rememberLastState': 'Remember last modal size',
1089
+ 'crud.authoring.defaults.modal.disableCloseOnEsc': 'Block close on Escape',
1090
+ 'crud.authoring.defaults.modal.disableCloseOnBackdrop': 'Block close on backdrop',
1091
+ 'crud.authoring.defaults.modal.noteDensity': 'Density: {value}',
1092
+ 'crud.authoring.defaults.modal.noteNoMaximize': 'Maximize hidden',
1093
+ 'crud.authoring.defaults.modal.noteStartMaximized': 'Starts maximized',
1094
+ 'crud.authoring.defaults.modal.noteRememberState': 'Last size remembered',
1095
+ 'crud.authoring.defaults.modal.noteEscLocked': 'Escape locked',
1096
+ 'crud.authoring.defaults.modal.noteBackdropLocked': 'Backdrop locked',
1097
+ 'crud.authoring.defaults.modal.noteBreakpoint': 'Fullscreen at {value}px',
1098
+ 'crud.authoring.defaults.back.group': 'Back behavior',
1099
+ 'crud.authoring.defaults.back.strategy': 'Back strategy',
1100
+ 'crud.authoring.defaults.back.strategy.auto': 'Auto',
1101
+ 'crud.authoring.defaults.back.strategy.close': 'Close',
1102
+ 'crud.authoring.defaults.back.strategy.navigate': 'Navigate',
1103
+ 'crud.authoring.defaults.back.returnTo': 'Return route',
1104
+ 'crud.authoring.defaults.back.confirmOnDirty': 'Confirm when leaving dirty forms',
1105
+ 'crud.authoring.defaults.back.noteStrategy': 'Strategy: {value}',
1106
+ 'crud.authoring.defaults.back.noteReturnTo': 'Return: {value}',
1107
+ 'crud.authoring.defaults.back.noteConfirmOnDirty': 'Dirty confirmation',
1108
+ 'crud.authoring.action.mode': 'Open mode',
1109
+ 'crud.authoring.action.route': 'Route',
1110
+ 'crud.authoring.action.route.placeholder': 'e.g. /cargos/view/:id',
1111
+ 'crud.authoring.action.formId': 'formId',
1112
+ 'crud.authoring.action.formId.placeholder': 'e.g. cargos-edit',
1113
+ 'crud.authoring.action.schemaUrl': 'Schema URL',
1114
+ 'crud.authoring.action.submitUrl': 'Submit URL',
1115
+ 'crud.authoring.action.submitMethod': 'Submit method',
1116
+ 'crud.authoring.action.apiEndpointKey': 'API endpoint key',
1117
+ 'crud.authoring.action.apiUrlEntry': 'API URL entry',
1118
+ 'crud.authoring.action.advanced': 'Submit and API details',
1119
+ 'crud.authoring.action.bindingGroup': 'Binding',
1120
+ 'crud.authoring.action.schemaGroup': 'Schema contract',
1121
+ 'crud.authoring.action.submitGroup': 'Submit contract',
1122
+ 'crud.authoring.action.apiGroup': 'API mapping',
1123
+ 'crud.authoring.action.inputsGroup': 'Input seeding',
1124
+ 'crud.authoring.action.inputsNote': 'Optional: use Params for row-derived values and Initial value only for fixed form seed.',
1125
+ 'crud.authoring.action.paramsSubgroup': 'Row-derived mappings',
1126
+ 'crud.authoring.action.initialValueSubgroup': 'Fixed form seed',
1127
+ 'crud.authoring.action.backGroup': 'Back behavior override',
1128
+ 'crud.authoring.action.backStrategy': 'Back strategy override',
1129
+ 'crud.authoring.action.backReturnTo': 'Action return route',
1130
+ 'crud.authoring.action.backConfirmOnDirty': 'Confirm when leaving dirty forms',
1131
+ 'crud.authoring.action.backUseDefaults': 'Use defaults',
1132
+ 'crud.authoring.action.backUsesDefaults': 'Using CRUD defaults for back behavior',
1133
+ 'crud.authoring.action.backNoteStrategy': 'Strategy: {value}',
1134
+ 'crud.authoring.action.backNoteReturnTo': 'Return: {value}',
1135
+ 'crud.authoring.action.backNoteConfirmOnDirty': 'Dirty confirmation',
1136
+ 'crud.authoring.action.paramFrom': 'From field',
1137
+ 'crud.authoring.action.paramTo': 'Target',
1138
+ 'crud.authoring.action.paramName': 'Destination name',
1139
+ 'crud.authoring.action.paramAdd': 'Add mapping',
1140
+ 'crud.authoring.action.paramRemove': 'Remove mapping',
1141
+ 'crud.authoring.action.paramTarget.routeParam': 'Route param',
1142
+ 'crud.authoring.action.paramTarget.query': 'Query string',
1143
+ 'crud.authoring.action.paramTarget.input': 'Form input',
1144
+ 'crud.authoring.action.initialValue': 'Initial form seed (JSON)',
1145
+ 'crud.authoring.action.paramsSummary': '{count} mappings',
1146
+ 'crud.authoring.action.paramsSummaryCompact': '{count} map',
1147
+ 'crud.authoring.action.paramsNone': 'No param mappings yet',
1148
+ 'crud.authoring.action.paramsNoteEmpty': 'Map fields from the current row into route params, query string, or dialog inputs.',
1149
+ 'crud.authoring.action.paramsNoteConfigured': '{count} mappings into {targets}.',
1150
+ 'crud.authoring.action.paramsNoteReady': 'Mappings ready',
1151
+ 'crud.authoring.action.initialValueReady': 'Initial value seed configured',
1152
+ 'crud.authoring.action.initialValueReadyCompact': 'Seed',
1153
+ 'crud.authoring.action.initialValueEmpty': 'No fixed form seed yet',
1154
+ 'crud.authoring.action.initialValueInvalid': 'Initial value JSON is not valid yet',
1155
+ 'crud.authoring.action.initialValueNoteConfigured': 'Seeded object injected into inputs.initialValue before form open.',
1156
+ 'crud.authoring.action.initialValueNoteReady': 'Fixed seed ready',
1157
+ 'crud.authoring.action.initialValueNoteEmpty': 'Use this only for fixed seed values that do not come from the selected row.',
1158
+ 'crud.authoring.action.binding': 'Binding',
1159
+ 'crud.authoring.action.bindingMissing': 'Binding not configured yet',
1160
+ 'crud.authoring.action.schema': 'Schema',
1161
+ 'crud.authoring.action.schemaPending': 'Schema contract not configured yet',
1162
+ 'crud.authoring.action.api': 'API',
1163
+ 'crud.authoring.action.apiEntryReady': 'Custom API URL entry configured',
1164
+ 'crud.authoring.action.apiPending': 'API mapping not configured yet',
1165
+ 'crud.authoring.action.groupsInvalid': 'Invalid: {groups}',
1166
+ 'crud.authoring.action.groupsPending': 'Pending: {groups}',
1167
+ 'crud.authoring.action.groupsReady': 'All groups ready',
1168
+ 'crud.authoring.action.primaryInvalid': 'Resolve: {groups}',
1169
+ 'crud.authoring.action.primaryPending': 'Next: {groups}',
1170
+ 'crud.authoring.action.primaryReady': 'Core ready',
1171
+ 'crud.authoring.action.advancedPending': 'Pending: {groups}',
1172
+ 'crud.authoring.action.advancedReady': 'Advanced ready',
1173
+ 'crud.authoring.action.editorialInvalid': 'Resolve: {groups}',
1174
+ 'crud.authoring.action.editorialPending': 'Finish: {groups}',
1175
+ 'crud.authoring.action.editorialReady': 'Ready to use',
1176
+ 'crud.authoring.action.submitPending': 'Submit/API details not configured yet',
1177
+ 'crud.authoring.action.status.ready': 'Ready',
1178
+ 'crud.authoring.action.status.pending': 'Pending',
1179
+ 'crud.authoring.action.status.invalid': 'Invalid',
1180
+ 'crud.authoring.mode.route': 'Route',
1181
+ 'crud.authoring.mode.modal': 'Modal',
1182
+ 'crud.authoring.mode.drawer': 'Drawer',
1183
+ 'crud.authoring.header.variant.ghost': 'Ghost',
1184
+ 'crud.authoring.header.variant.tonal': 'Tonal',
1185
+ 'crud.authoring.header.variant.outlined': 'Outlined',
1186
+ 'crud.authoring.submitMethod.post': 'POST',
1187
+ 'crud.authoring.submitMethod.put': 'PUT',
1188
+ 'crud.authoring.submitMethod.patch': 'PATCH',
1189
+ 'crud.authoring.action.create': 'Create',
1190
+ 'crud.authoring.action.view': 'View',
1191
+ 'crud.authoring.action.edit': 'Edit',
1192
+ 'crud.authoring.table.summary': 'Detailed table configuration remains owned by the canonical TableConfig surface. In this phase, the CRUD editor preserves the current table snapshot without duplicating the full table editor.',
1193
+ 'crud.authoring.table.structureSummary': '{count} columns | {density}',
1194
+ 'crud.authoring.table.paginationSummary': 'Page size {value}',
1195
+ 'crud.authoring.table.paginationOff': 'Pagination off',
1196
+ 'crud.authoring.table.sortingClient': 'Client sorting',
1197
+ 'crud.authoring.table.sortingServer': 'Server sorting',
1198
+ 'crud.authoring.table.sortingOff': 'Sorting off',
1199
+ 'crud.authoring.table.statesDefault': 'Default state copy',
1200
+ 'crud.authoring.table.statesCustom': '{count} custom states',
1201
+ 'crud.authoring.table.sectionSummary': '{structure}. {behavior}. {states}.',
1202
+ 'crud.authoring.table.flowSummary': 'Core presentation stays visible below. Pagination, sorting, state copy, and columns stay in advanced table panels.',
1203
+ 'crud.authoring.table.density.compact': 'Compact',
1204
+ 'crud.authoring.table.density.comfortable': 'Comfortable',
1205
+ 'crud.authoring.table.density.spacious': 'Spacious',
1206
+ 'crud.authoring.json.hint': 'The JSON below represents the canonical CRUD authoring document.',
1207
+ 'crud.authoring.json.summaryErrors': 'The canonical document still has validation errors.',
1208
+ 'crud.authoring.json.summaryWarnings': 'The canonical document has diagnostics worth reviewing.',
1209
+ 'crud.authoring.json.summaryClean': 'The canonical document is structurally consistent.',
1210
+ 'crud.authoring.validation.routeRequired': 'Provide a route when the effective mode is route.',
1211
+ 'crud.authoring.validation.formIdRequired': 'Provide a formId when the effective mode is modal or drawer.',
1212
+ 'crud.authoring.validation.submitContract': 'submitUrl and submitMethod must be provided together.',
1213
+ 'crud.authoring.overview.actionsNextCompact': 'Next: {action}. {summary}',
1214
+ 'crud.authoring.action.focusInvalid': '{count} blocking groups',
1215
+ 'crud.authoring.action.focusPending': '{count} groups still pending',
1216
+ 'crud.authoring.action.focusReady': 'Ready',
1217
+ 'crud.authoring.action.headerReady': 'Ready',
1218
+ 'crud.authoring.action.bindingReadyNote': 'Binding configured',
1219
+ 'crud.authoring.action.schemaReadyNote': 'Schema linked',
1220
+ 'crud.authoring.action.submitReadyNote': 'Submit contract ready',
1221
+ 'crud.authoring.action.apiReadyNote': 'API mapping ready',
1222
+ },
1223
+ },
1224
+ },
1225
+ };
1226
+ function translateCrudAuthoringText(i18n, key, fallback, params) {
1227
+ return i18n.t(key, params, fallback, PRAXIS_CRUD_AUTHORING_I18N_NAMESPACE);
1228
+ }
1229
+
1230
+ const ACTION_KEYS = ['create', 'view', 'edit'];
1231
+ const OPEN_MODES = ['route', 'modal', 'drawer'];
1232
+ const HEADER_VARIANTS = ['ghost', 'tonal', 'outlined'];
1233
+ const SUBMIT_METHODS = ['post', 'put', 'patch'];
1234
+ const MODAL_DENSITIES = ['default', 'compact'];
1235
+ const BACK_STRATEGIES = ['auto', 'close', 'navigate'];
1236
+ const PARAM_TARGETS = ['routeParam', 'query', 'input'];
1237
+ class CrudMetadataEditorComponent {
1238
+ documentInput = input(null, ...(ngDevMode ? [{ debugName: "documentInput", alias: 'document' }] : [{ alias: 'document' }]));
1239
+ metadataInput = input(null, ...(ngDevMode ? [{ debugName: "metadataInput", alias: 'metadata' }] : [{ alias: 'metadata' }]));
1240
+ crudIdInput = input(null, ...(ngDevMode ? [{ debugName: "crudIdInput", alias: 'crudId' }] : [{ alias: 'crudId' }]));
1241
+ readonlyInput = input(false, ...(ngDevMode ? [{ debugName: "readonlyInput", alias: 'readonly' }] : [{ alias: 'readonly' }]));
1242
+ isDirty$ = new BehaviorSubject(false);
1243
+ isValid$ = new BehaviorSubject(true);
1244
+ isBusy$ = new BehaviorSubject(false);
1245
+ actionKeys = ACTION_KEYS;
1246
+ healthBuckets = ['invalid', 'pending', 'ready'];
1247
+ openModes = OPEN_MODES;
1248
+ headerVariants = HEADER_VARIANTS;
1249
+ submitMethods = SUBMIT_METHODS;
1250
+ modalDensities = MODAL_DENSITIES;
1251
+ backStrategies = BACK_STRATEGIES;
1252
+ paramTargets = PARAM_TARGETS;
1253
+ initialValueDrafts = signal({}, ...(ngDevMode ? [{ debugName: "initialValueDrafts" }] : []));
1254
+ injectedData = inject(SETTINGS_PANEL_DATA, { optional: true });
1255
+ i18n = inject(PraxisI18nService);
1256
+ currentDocument = signal(createCrudAuthoringDocument({}), ...(ngDevMode ? [{ debugName: "currentDocument" }] : []));
1257
+ initialDocument = signal(createCrudAuthoringDocument({}), ...(ngDevMode ? [{ debugName: "initialDocument" }] : []));
1258
+ lastExternalSignature = null;
1259
+ effectiveCrudId = computed(() => this.crudIdInput() || this.injectedData?.crudId || null, ...(ngDevMode ? [{ debugName: "effectiveCrudId" }] : []));
1260
+ isReadonly = computed(() => !!(this.readonlyInput() || this.injectedData?.readonly), ...(ngDevMode ? [{ debugName: "isReadonly" }] : []));
1261
+ diagnostics = computed(() => validateCrudAuthoringDocument(this.currentDocument(), { requireCanonicalActions: true }), ...(ngDevMode ? [{ debugName: "diagnostics" }] : []));
1262
+ errorCount = computed(() => this.diagnostics().filter((issue) => issue.level === 'error').length, ...(ngDevMode ? [{ debugName: "errorCount" }] : []));
1263
+ warningCount = computed(() => this.diagnostics().filter((issue) => issue.level !== 'error').length, ...(ngDevMode ? [{ debugName: "warningCount" }] : []));
1264
+ serializedDocument = computed(() => JSON.stringify(serializeCrudAuthoringDocument(this.currentDocument()), null, 2), ...(ngDevMode ? [{ debugName: "serializedDocument" }] : []));
1265
+ tableConfig = computed(() => this.currentDocument().metadata.table || { columns: [] }, ...(ngDevMode ? [{ debugName: "tableConfig" }] : []));
1266
+ resourcePath = computed(() => this.currentDocument().metadata.resource?.path || '', ...(ngDevMode ? [{ debugName: "resourcePath" }] : []));
1267
+ resourceIdField = computed(() => String(this.currentDocument().metadata.resource?.idField ?? ''), ...(ngDevMode ? [{ debugName: "resourceIdField" }] : []));
1268
+ resourceEndpointKey = computed(() => String(this.currentDocument().metadata.resource?.endpointKey ?? ''), ...(ngDevMode ? [{ debugName: "resourceEndpointKey" }] : []));
1269
+ defaultsOpenMode = computed(() => this.currentDocument().metadata.defaults?.openMode || 'route', ...(ngDevMode ? [{ debugName: "defaultsOpenMode" }] : []));
1270
+ headerShowBack = computed(() => !!this.currentDocument().metadata.defaults?.header?.showBack, ...(ngDevMode ? [{ debugName: "headerShowBack" }] : []));
1271
+ headerBackLabel = computed(() => this.currentDocument().metadata.defaults?.header?.backLabel || '', ...(ngDevMode ? [{ debugName: "headerBackLabel" }] : []));
1272
+ headerVariant = computed(() => this.currentDocument().metadata.defaults?.header?.variant || 'ghost', ...(ngDevMode ? [{ debugName: "headerVariant" }] : []));
1273
+ headerSticky = computed(() => !!this.currentDocument().metadata.defaults?.header?.sticky, ...(ngDevMode ? [{ debugName: "headerSticky" }] : []));
1274
+ headerBreadcrumbs = computed(() => !!this.currentDocument().metadata.defaults?.header?.breadcrumbs, ...(ngDevMode ? [{ debugName: "headerBreadcrumbs" }] : []));
1275
+ headerDivider = computed(() => !!this.currentDocument().metadata.defaults?.header?.divider, ...(ngDevMode ? [{ debugName: "headerDivider" }] : []));
1276
+ modalDensity = computed(() => String(this.currentDocument().metadata.defaults?.modal?.density ?? 'default'), ...(ngDevMode ? [{ debugName: "modalDensity" }] : []));
1277
+ modalCanMaximize = computed(() => this.currentDocument().metadata.defaults?.modal?.canMaximize ?? true, ...(ngDevMode ? [{ debugName: "modalCanMaximize" }] : []));
1278
+ modalStartMaximized = computed(() => !!this.currentDocument().metadata.defaults?.modal?.startMaximized, ...(ngDevMode ? [{ debugName: "modalStartMaximized" }] : []));
1279
+ modalRememberLastState = computed(() => !!this.currentDocument().metadata.defaults?.modal?.rememberLastState, ...(ngDevMode ? [{ debugName: "modalRememberLastState" }] : []));
1280
+ modalDisableCloseOnEsc = computed(() => !!this.currentDocument().metadata.defaults?.modal?.disableCloseOnEsc, ...(ngDevMode ? [{ debugName: "modalDisableCloseOnEsc" }] : []));
1281
+ modalDisableCloseOnBackdrop = computed(() => !!this.currentDocument().metadata.defaults?.modal?.disableCloseOnBackdrop, ...(ngDevMode ? [{ debugName: "modalDisableCloseOnBackdrop" }] : []));
1282
+ modalFullscreenBreakpoint = computed(() => String(this.currentDocument().metadata.defaults?.modal?.fullscreenBreakpoint ?? ''), ...(ngDevMode ? [{ debugName: "modalFullscreenBreakpoint" }] : []));
1283
+ backStrategy = computed(() => String(this.currentDocument().metadata.defaults?.back?.strategy ?? 'auto'), ...(ngDevMode ? [{ debugName: "backStrategy" }] : []));
1284
+ backReturnTo = computed(() => this.currentDocument().metadata.defaults?.back?.returnTo || '', ...(ngDevMode ? [{ debugName: "backReturnTo" }] : []));
1285
+ backConfirmOnDirty = computed(() => !!this.currentDocument().metadata.defaults?.back?.confirmOnDirty, ...(ngDevMode ? [{ debugName: "backConfirmOnDirty" }] : []));
1286
+ nextFocusSection = computed(() => {
1287
+ for (const section of ['connection', 'defaults', 'actions', 'table']) {
1288
+ if (this.sectionStatus(section) === 'invalid') {
1289
+ return section;
1290
+ }
1291
+ }
1292
+ for (const section of ['connection', 'defaults', 'actions', 'table']) {
1293
+ if (this.sectionStatus(section) === 'pending') {
1294
+ return section;
1295
+ }
1296
+ }
1297
+ return 'connection';
1298
+ }, ...(ngDevMode ? [{ debugName: "nextFocusSection" }] : []));
1299
+ nextFocusStatus = computed(() => this.sectionStatus(this.nextFocusSection()), ...(ngDevMode ? [{ debugName: "nextFocusStatus" }] : []));
1300
+ visibleHealthBuckets = computed(() => {
1301
+ const attentionBuckets = ['invalid', 'pending']
1302
+ .filter((bucket) => this.hasHealthBucketContent(bucket));
1303
+ if (attentionBuckets.length) {
1304
+ return attentionBuckets;
1305
+ }
1306
+ return this.hasHealthBucketContent('ready') ? ['ready'] : [];
1307
+ }, ...(ngDevMode ? [{ debugName: "visibleHealthBuckets" }] : []));
1308
+ showHealthMap = computed(() => this.visibleHealthBuckets().length > 1, ...(ngDevMode ? [{ debugName: "showHealthMap" }] : []));
1309
+ groupedDiagnostics = computed(() => ['connection', 'defaults', 'actions', 'table']
1310
+ .map((section) => {
1311
+ const issues = this.sectionDiagnostics(section);
1312
+ return {
1313
+ section,
1314
+ issues,
1315
+ hasError: issues.some((issue) => issue.level === 'error'),
1316
+ };
1317
+ })
1318
+ .filter((group) => group.issues.length > 0), ...(ngDevMode ? [{ debugName: "groupedDiagnostics" }] : []));
1319
+ constructor() {
1320
+ effect(() => {
1321
+ const seed = this.resolveExternalSeed();
1322
+ const signature = JSON.stringify(seed);
1323
+ if (signature === this.lastExternalSignature) {
1324
+ return;
1325
+ }
1326
+ this.lastExternalSignature = signature;
1327
+ const parsed = parseLegacyOrCrudDocument(seed);
1328
+ this.initialDocument.set(parsed);
1329
+ this.currentDocument.set(parsed);
1330
+ this.syncState();
1331
+ });
1332
+ }
1333
+ getSettingsValue() {
1334
+ return this.buildPayload();
1335
+ }
1336
+ onSave() {
1337
+ return this.buildPayload();
1338
+ }
1339
+ reset() {
1340
+ this.currentDocument.set(this.initialDocument());
1341
+ this.initialValueDrafts.set({});
1342
+ this.syncState();
1343
+ }
1344
+ actionValue(actionName) {
1345
+ return (findCrudAction(this.currentDocument().metadata.actions, actionName) || {
1346
+ id: actionName,
1347
+ action: actionName,
1348
+ label: this.actionLabel(actionName),
1349
+ });
1350
+ }
1351
+ actionApiUrlEntryDraft(actionName) {
1352
+ const value = this.actionValue(actionName).form?.apiUrlEntry;
1353
+ return value ? JSON.stringify(value, null, 2) : '';
1354
+ }
1355
+ actionParams(actionName) {
1356
+ return [...(this.actionValue(actionName).params || [])];
1357
+ }
1358
+ actionInitialValueDraft(actionName) {
1359
+ const draft = this.initialValueDrafts()[actionName];
1360
+ if (draft !== undefined) {
1361
+ return draft;
1362
+ }
1363
+ const value = this.actionValue(actionName).form?.initialValue;
1364
+ return value ? JSON.stringify(value, null, 2) : '';
1365
+ }
1366
+ actionLabel(actionName) {
1367
+ return this.tx(`crud.authoring.action.${actionName}`, actionName);
1368
+ }
1369
+ actionSummary(actionName) {
1370
+ const action = this.actionValue(actionName);
1371
+ const mode = (action.openMode || this.defaultsOpenMode());
1372
+ const binding = action.formId || action.route || this.modeLabel(mode);
1373
+ if (this.actionStatus(actionName) === 'ready') {
1374
+ return `${binding} | ${this.tx('crud.authoring.action.headerReady', 'Ready')}`;
1375
+ }
1376
+ return `${binding} | ${this.actionEditorialSummary(actionName)}`;
1377
+ }
1378
+ actionEffectiveBindingSummary(actionName) {
1379
+ const action = this.actionValue(actionName);
1380
+ if (action.formId) {
1381
+ return `${this.tx('crud.authoring.action.binding', 'Binding')}: ${action.formId}`;
1382
+ }
1383
+ if (action.route) {
1384
+ return `${this.tx('crud.authoring.action.binding', 'Binding')}: ${action.route}`;
1385
+ }
1386
+ return this.tx('crud.authoring.action.bindingMissing', 'Binding not configured yet');
1387
+ }
1388
+ actionSubmitSummary(actionName) {
1389
+ const action = this.actionValue(actionName);
1390
+ const method = action.form?.submitMethod?.toUpperCase();
1391
+ const submitUrl = action.form?.submitUrl;
1392
+ if (method && submitUrl) {
1393
+ return `${method} ${submitUrl}`;
1394
+ }
1395
+ if (action.form?.schemaUrl) {
1396
+ return `${this.tx('crud.authoring.action.schema', 'Schema')}: ${action.form.schemaUrl}`;
1397
+ }
1398
+ return this.tx('crud.authoring.action.submitPending', 'Submit/API details not configured yet');
1399
+ }
1400
+ actionSchemaSummary(actionName) {
1401
+ const schemaUrl = this.actionValue(actionName).form?.schemaUrl;
1402
+ if (schemaUrl) {
1403
+ return `${this.tx('crud.authoring.action.schema', 'Schema')}: ${schemaUrl}`;
1404
+ }
1405
+ return this.tx('crud.authoring.action.schemaPending', 'Schema contract not configured yet');
1406
+ }
1407
+ actionApiSummary(actionName) {
1408
+ const form = this.actionValue(actionName).form;
1409
+ if (form?.apiEndpointKey) {
1410
+ return `${this.tx('crud.authoring.action.api', 'API')}: ${form.apiEndpointKey}`;
1411
+ }
1412
+ if (form?.apiUrlEntry) {
1413
+ return this.tx('crud.authoring.action.apiEntryReady', 'Custom API URL entry configured');
1414
+ }
1415
+ return this.tx('crud.authoring.action.apiPending', 'API mapping not configured yet');
1416
+ }
1417
+ actionInputsNote(actionName) {
1418
+ void actionName;
1419
+ return this.tx('crud.authoring.action.inputsNote', 'Optional: use Params for row-derived values and Initial value only for fixed form seed.');
1420
+ }
1421
+ actionInputsSummary(actionName) {
1422
+ const parts = [];
1423
+ const params = this.actionParams(actionName);
1424
+ if (params.length) {
1425
+ parts.push(this.tx('crud.authoring.action.paramsSummary', '{count} mappings')
1426
+ .replace('{count}', String(params.length)));
1427
+ }
1428
+ else {
1429
+ parts.push(this.tx('crud.authoring.action.paramsNone', 'No param mappings yet'));
1430
+ }
1431
+ const draft = this.actionInitialValueDraft(actionName).trim();
1432
+ const hasInitialValue = !!this.actionValue(actionName).form?.initialValue;
1433
+ if (draft && !hasInitialValue) {
1434
+ parts.push(this.tx('crud.authoring.action.initialValueInvalid', 'Initial value JSON is not valid yet'));
1435
+ }
1436
+ else if (hasInitialValue) {
1437
+ parts.push(this.tx('crud.authoring.action.initialValueReady', 'Initial value seed configured'));
1438
+ }
1439
+ else {
1440
+ parts.push(this.tx('crud.authoring.action.initialValueEmpty', 'No fixed form seed yet'));
1441
+ }
1442
+ return parts.join(' | ');
1443
+ }
1444
+ actionParamsNote(actionName) {
1445
+ const params = this.actionParams(actionName);
1446
+ if (!params.length) {
1447
+ return this.tx('crud.authoring.action.paramsNoteEmpty', 'Map fields from the current row into route params, query string, or dialog inputs.');
1448
+ }
1449
+ return this.tx('crud.authoring.action.paramsNoteReady', 'Mappings ready');
1450
+ }
1451
+ actionInitialValueNote(actionName) {
1452
+ const draft = this.actionInitialValueDraft(actionName).trim();
1453
+ const hasInitialValue = !!this.actionValue(actionName).form?.initialValue;
1454
+ if (draft && !hasInitialValue) {
1455
+ return this.tx('crud.authoring.action.initialValueInvalid', 'Initial value JSON is not valid yet');
1456
+ }
1457
+ if (hasInitialValue) {
1458
+ return this.tx('crud.authoring.action.initialValueNoteReady', 'Fixed seed ready');
1459
+ }
1460
+ return this.tx('crud.authoring.action.initialValueNoteEmpty', 'Use this only for fixed seed values that do not come from the selected row.');
1461
+ }
1462
+ actionAdvancedSummary(actionName) {
1463
+ const submitStatus = this.actionGroupStatus(actionName, 'submit');
1464
+ const apiStatus = this.actionGroupStatus(actionName, 'api');
1465
+ const inputsSummary = this.actionInputsSummaryBadge(actionName);
1466
+ if (submitStatus === 'ready' && apiStatus === 'ready') {
1467
+ return this.tx('crud.authoring.action.advancedReady', 'Advanced ready');
1468
+ }
1469
+ const pending = [];
1470
+ if (submitStatus !== 'ready') {
1471
+ pending.push(this.tx('crud.authoring.action.submitGroup', 'Submit contract'));
1472
+ }
1473
+ if (apiStatus !== 'ready') {
1474
+ pending.push(this.tx('crud.authoring.action.apiGroup', 'API mapping'));
1475
+ }
1476
+ const summary = this.tx('crud.authoring.action.advancedPending', 'Pending: {groups}')
1477
+ .replace('{groups}', pending.join(', '));
1478
+ return inputsSummary ? `${summary} · ${inputsSummary}` : summary;
1479
+ }
1480
+ actionPrimarySummary(actionName) {
1481
+ const bindingStatus = this.actionGroupStatus(actionName, 'binding');
1482
+ const schemaStatus = this.actionGroupStatus(actionName, 'schema');
1483
+ if (bindingStatus === 'ready' && schemaStatus === 'ready') {
1484
+ return this.tx('crud.authoring.action.primaryReady', 'Core ready');
1485
+ }
1486
+ const invalid = [];
1487
+ if (bindingStatus === 'invalid') {
1488
+ invalid.push(this.tx('crud.authoring.action.bindingGroup', 'Binding'));
1489
+ }
1490
+ if (schemaStatus === 'invalid') {
1491
+ invalid.push(this.tx('crud.authoring.action.schemaGroup', 'Schema contract'));
1492
+ }
1493
+ if (invalid.length) {
1494
+ return this.tx('crud.authoring.action.primaryInvalid', 'Resolve: {groups}')
1495
+ .replace('{groups}', invalid.join(', '));
1496
+ }
1497
+ const pending = [];
1498
+ if (bindingStatus !== 'ready') {
1499
+ pending.push(this.tx('crud.authoring.action.bindingGroup', 'Binding'));
1500
+ }
1501
+ if (schemaStatus !== 'ready') {
1502
+ pending.push(this.tx('crud.authoring.action.schemaGroup', 'Schema contract'));
1503
+ }
1504
+ return this.tx('crud.authoring.action.primaryPending', 'Next: {groups}')
1505
+ .replace('{groups}', pending.join(', '));
1506
+ }
1507
+ actionBindingNote(actionName) {
1508
+ if (this.actionGroupStatus(actionName, 'binding') === 'ready') {
1509
+ return this.tx('crud.authoring.action.bindingReadyNote', 'Binding configured');
1510
+ }
1511
+ return this.actionEffectiveBindingSummary(actionName);
1512
+ }
1513
+ actionSchemaNote(actionName) {
1514
+ if (this.actionGroupStatus(actionName, 'schema') === 'ready') {
1515
+ return this.tx('crud.authoring.action.schemaReadyNote', 'Schema linked');
1516
+ }
1517
+ return this.actionSchemaSummary(actionName);
1518
+ }
1519
+ actionSubmitNote(actionName) {
1520
+ if (this.actionGroupStatus(actionName, 'submit') === 'ready') {
1521
+ return this.tx('crud.authoring.action.submitReadyNote', 'Submit contract ready');
1522
+ }
1523
+ return this.actionSubmitSummary(actionName);
1524
+ }
1525
+ actionApiNote(actionName) {
1526
+ if (this.actionGroupStatus(actionName, 'api') === 'ready') {
1527
+ return this.tx('crud.authoring.action.apiReadyNote', 'API mapping ready');
1528
+ }
1529
+ return this.actionApiSummary(actionName);
1530
+ }
1531
+ actionBackStrategy(actionName) {
1532
+ return String(this.actionValue(actionName).back?.strategy ?? '');
1533
+ }
1534
+ actionBackReturnTo(actionName) {
1535
+ return this.actionValue(actionName).back?.returnTo || '';
1536
+ }
1537
+ actionBackConfirmOnDirty(actionName) {
1538
+ return !!this.actionValue(actionName).back?.confirmOnDirty;
1539
+ }
1540
+ actionBackNote(actionName) {
1541
+ const actionBack = this.actionValue(actionName).back;
1542
+ if (!actionBack || Object.keys(actionBack).length === 0) {
1543
+ return this.tx('crud.authoring.action.backUsesDefaults', 'Using CRUD defaults for back behavior');
1544
+ }
1545
+ const parts = [];
1546
+ if (actionBack.strategy) {
1547
+ parts.push(this.tx('crud.authoring.action.backNoteStrategy', 'Strategy: {value}')
1548
+ .replace('{value}', this.backStrategyLabel(actionBack.strategy)));
1549
+ }
1550
+ if (actionBack.returnTo) {
1551
+ parts.push(this.tx('crud.authoring.action.backNoteReturnTo', 'Return: {value}')
1552
+ .replace('{value}', actionBack.returnTo));
1553
+ }
1554
+ if (actionBack.confirmOnDirty) {
1555
+ parts.push(this.tx('crud.authoring.action.backNoteConfirmOnDirty', 'Dirty confirmation'));
1556
+ }
1557
+ return parts.join(' | ');
1558
+ }
1559
+ actionGroupStatus(actionName, group) {
1560
+ const action = this.actionValue(actionName);
1561
+ const effectiveMode = (action.openMode || this.defaultsOpenMode());
1562
+ const actionDiagnostics = this.actionDiagnostics(actionName);
1563
+ if (group === 'binding') {
1564
+ const hasBindingError = actionDiagnostics.some((issue) => (issue.code === 'crud.metadata.actions.route.required'
1565
+ || issue.code === 'crud.metadata.actions.formId.required'));
1566
+ if (hasBindingError) {
1567
+ return 'invalid';
1568
+ }
1569
+ const hasBinding = effectiveMode === 'route'
1570
+ ? !!String(action.route || '').trim()
1571
+ : !!String(action.formId || '').trim();
1572
+ return hasBinding ? 'ready' : 'pending';
1573
+ }
1574
+ if (group === 'schema') {
1575
+ return String(action.form?.schemaUrl || '').trim() ? 'ready' : 'pending';
1576
+ }
1577
+ if (group === 'submit') {
1578
+ const hasSubmitError = actionDiagnostics.some((issue) => issue.code === 'crud.metadata.actions.form.submit-contract.invalid');
1579
+ if (hasSubmitError) {
1580
+ return 'invalid';
1581
+ }
1582
+ const hasSubmitPair = !!String(action.form?.submitUrl || '').trim()
1583
+ && !!String(action.form?.submitMethod || '').trim();
1584
+ return hasSubmitPair ? 'ready' : 'pending';
1585
+ }
1586
+ const hasApiEndpoint = !!String(action.form?.apiEndpointKey || '').trim();
1587
+ const hasApiEntry = !!String(this.actionApiUrlEntryDraft(actionName) || '').trim();
1588
+ return hasApiEndpoint || hasApiEntry ? 'ready' : 'pending';
1589
+ }
1590
+ actionGroupStatusLabel(actionName, group) {
1591
+ return {
1592
+ ready: this.tx('crud.authoring.action.status.ready', 'Ready'),
1593
+ pending: this.tx('crud.authoring.action.status.pending', 'Pending'),
1594
+ invalid: this.tx('crud.authoring.action.status.invalid', 'Invalid'),
1595
+ }[this.actionGroupStatus(actionName, group)];
1596
+ }
1597
+ actionGroupLabel(group) {
1598
+ return {
1599
+ binding: this.tx('crud.authoring.action.bindingGroup', 'Binding'),
1600
+ schema: this.tx('crud.authoring.action.schemaGroup', 'Schema contract'),
1601
+ submit: this.tx('crud.authoring.action.submitGroup', 'Submit contract'),
1602
+ api: this.tx('crud.authoring.action.apiGroup', 'API mapping'),
1603
+ }[group];
1604
+ }
1605
+ actionGroupsSummary(actionName) {
1606
+ const invalidGroups = this.actionGroupLabelsByStatus(actionName, 'invalid');
1607
+ if (invalidGroups.length) {
1608
+ return this.tx('crud.authoring.action.groupsInvalid', 'Invalid: {groups}')
1609
+ .replace('{groups}', invalidGroups.join(', '));
1610
+ }
1611
+ const pendingGroups = this.actionGroupLabelsByStatus(actionName, 'pending');
1612
+ if (pendingGroups.length) {
1613
+ return this.tx('crud.authoring.action.groupsPending', 'Pending: {groups}')
1614
+ .replace('{groups}', pendingGroups.join(', '));
1615
+ }
1616
+ return this.tx('crud.authoring.action.groupsReady', 'All groups ready');
1617
+ }
1618
+ actionEditorialSummary(actionName) {
1619
+ const invalidGroups = this.actionGroupLabelsByStatus(actionName, 'invalid');
1620
+ if (invalidGroups.length) {
1621
+ return this.tx('crud.authoring.action.editorialInvalid', 'Resolve: {groups}')
1622
+ .replace('{groups}', invalidGroups.join(', '));
1623
+ }
1624
+ const pendingGroups = this.actionGroupLabelsByStatus(actionName, 'pending');
1625
+ if (pendingGroups.length) {
1626
+ return this.tx('crud.authoring.action.editorialPending', 'Finish: {groups}')
1627
+ .replace('{groups}', pendingGroups.join(', '));
1628
+ }
1629
+ return this.tx('crud.authoring.action.editorialReady', 'Ready to use');
1630
+ }
1631
+ actionPanelExpanded(actionName) {
1632
+ return this.actionStatus(actionName) !== 'ready';
1633
+ }
1634
+ actionAdvancedPanelExpanded(actionName) {
1635
+ return this.actionGroupStatus(actionName, 'submit') !== 'ready'
1636
+ || this.actionGroupStatus(actionName, 'api') !== 'ready';
1637
+ }
1638
+ actionStatus(actionName) {
1639
+ const diagnosticPaths = this.actionDiagnostics(actionName);
1640
+ if (diagnosticPaths.some((issue) => issue.level === 'error')) {
1641
+ return 'invalid';
1642
+ }
1643
+ const action = this.actionValue(actionName);
1644
+ const effectiveMode = (action.openMode || this.defaultsOpenMode());
1645
+ const hasBinding = effectiveMode === 'route'
1646
+ ? !!String(action.route || '').trim()
1647
+ : !!String(action.formId || '').trim();
1648
+ const hasSubmitPair = !!String(action.form?.submitUrl || '').trim()
1649
+ && !!String(action.form?.submitMethod || '').trim();
1650
+ const hasSchema = !!String(action.form?.schemaUrl || '').trim();
1651
+ if (hasBinding && (hasSubmitPair || hasSchema)) {
1652
+ return 'ready';
1653
+ }
1654
+ return 'pending';
1655
+ }
1656
+ actionStatusLabel(actionName) {
1657
+ return {
1658
+ ready: this.tx('crud.authoring.action.status.ready', 'Ready'),
1659
+ pending: this.tx('crud.authoring.action.status.pending', 'Pending'),
1660
+ invalid: this.tx('crud.authoring.action.status.invalid', 'Invalid'),
1661
+ }[this.actionStatus(actionName)];
1662
+ }
1663
+ actionShowStatusChip(actionName) {
1664
+ return this.actionStatus(actionName) !== 'ready';
1665
+ }
1666
+ connectionOverview() {
1667
+ const path = this.resourcePath();
1668
+ if (!path) {
1669
+ return this.tx('crud.authoring.overview.connectionMissing', 'Resource not configured yet');
1670
+ }
1671
+ return path;
1672
+ }
1673
+ connectionOverviewNote() {
1674
+ const idField = this.resourceIdField();
1675
+ const endpointKey = this.resourceEndpointKey();
1676
+ const parts = [];
1677
+ if (idField) {
1678
+ parts.push(this.tx('crud.authoring.overview.connectionIdField', 'ID: {value}').replace('{value}', idField));
1679
+ }
1680
+ if (endpointKey) {
1681
+ parts.push(this.tx('crud.authoring.overview.connectionEndpoint', 'API: {value}').replace('{value}', endpointKey));
1682
+ }
1683
+ if (!parts.length) {
1684
+ return this.tx('crud.authoring.overview.connectionDetailsPending', 'Identifier and endpoint details are still optional here.');
1685
+ }
1686
+ return parts.join(' · ');
1687
+ }
1688
+ defaultsOverview() {
1689
+ return this.modeLabel(this.defaultsOpenMode());
1690
+ }
1691
+ defaultsOverviewNote() {
1692
+ const parts = [
1693
+ this.headerShowBack()
1694
+ ? this.tx('crud.authoring.overview.backVisible', 'Back visible')
1695
+ : this.tx('crud.authoring.overview.backHidden', 'Back hidden'),
1696
+ ];
1697
+ if (this.modalDensity() !== 'default') {
1698
+ parts.push(this.tx('crud.authoring.overview.modalDensity', 'Modal: {value}')
1699
+ .replace('{value}', this.modalDensityLabel(this.modalDensity())));
1700
+ }
1701
+ if (this.backStrategy() !== 'auto') {
1702
+ parts.push(this.tx('crud.authoring.overview.backStrategy', 'Back: {value}')
1703
+ .replace('{value}', this.backStrategyLabel(this.backStrategy())));
1704
+ }
1705
+ if (this.modalRememberLastState()) {
1706
+ parts.push(this.tx('crud.authoring.overview.modalRememberState', 'State remembered'));
1707
+ }
1708
+ if (this.backConfirmOnDirty()) {
1709
+ parts.push(this.tx('crud.authoring.overview.backConfirmOnDirty', 'Dirty confirm'));
1710
+ }
1711
+ if (this.headerSticky()) {
1712
+ parts.push(this.tx('crud.authoring.overview.headerSticky', 'Sticky header'));
1713
+ }
1714
+ if (this.headerBreadcrumbs()) {
1715
+ parts.push(this.tx('crud.authoring.overview.headerBreadcrumbs', 'Breadcrumbs'));
1716
+ }
1717
+ return parts.join(' · ');
1718
+ }
1719
+ modalDefaultsNote() {
1720
+ const parts = [
1721
+ this.tx('crud.authoring.defaults.modal.noteDensity', 'Density: {value}')
1722
+ .replace('{value}', this.modalDensityLabel(this.modalDensity())),
1723
+ ];
1724
+ if (!this.modalCanMaximize()) {
1725
+ parts.push(this.tx('crud.authoring.defaults.modal.noteNoMaximize', 'Maximize hidden'));
1726
+ }
1727
+ if (this.modalStartMaximized()) {
1728
+ parts.push(this.tx('crud.authoring.defaults.modal.noteStartMaximized', 'Starts maximized'));
1729
+ }
1730
+ if (this.modalRememberLastState()) {
1731
+ parts.push(this.tx('crud.authoring.defaults.modal.noteRememberState', 'Last size remembered'));
1732
+ }
1733
+ if (this.modalDisableCloseOnEsc()) {
1734
+ parts.push(this.tx('crud.authoring.defaults.modal.noteEscLocked', 'Escape locked'));
1735
+ }
1736
+ if (this.modalDisableCloseOnBackdrop()) {
1737
+ parts.push(this.tx('crud.authoring.defaults.modal.noteBackdropLocked', 'Backdrop locked'));
1738
+ }
1739
+ if (this.modalFullscreenBreakpoint().trim()) {
1740
+ parts.push(this.tx('crud.authoring.defaults.modal.noteBreakpoint', 'Fullscreen at {value}px')
1741
+ .replace('{value}', this.modalFullscreenBreakpoint().trim()));
1742
+ }
1743
+ return parts.join(' · ');
1744
+ }
1745
+ backDefaultsNote() {
1746
+ const parts = [
1747
+ this.tx('crud.authoring.defaults.back.noteStrategy', 'Strategy: {value}')
1748
+ .replace('{value}', this.backStrategyLabel(this.backStrategy())),
1749
+ ];
1750
+ if (this.backReturnTo().trim()) {
1751
+ parts.push(this.tx('crud.authoring.defaults.back.noteReturnTo', 'Return: {value}')
1752
+ .replace('{value}', this.backReturnTo().trim()));
1753
+ }
1754
+ if (this.backConfirmOnDirty()) {
1755
+ parts.push(this.tx('crud.authoring.defaults.back.noteConfirmOnDirty', 'Dirty confirmation'));
1756
+ }
1757
+ return parts.join(' · ');
1758
+ }
1759
+ actionsOverview() {
1760
+ const readyCount = this.actionKeys.filter((actionName) => this.actionStatus(actionName) === 'ready').length;
1761
+ const pendingCount = this.actionKeys.filter((actionName) => this.actionStatus(actionName) === 'pending').length;
1762
+ const invalidCount = this.actionKeys.filter((actionName) => this.actionStatus(actionName) === 'invalid').length;
1763
+ const parts = [
1764
+ this.tx('crud.authoring.overview.actionsReady', '{count} ready').replace('{count}', String(readyCount)),
1765
+ ];
1766
+ if (pendingCount > 0) {
1767
+ parts.push(this.tx('crud.authoring.overview.actionsPending', '{count} pending').replace('{count}', String(pendingCount)));
1768
+ }
1769
+ if (invalidCount > 0) {
1770
+ parts.push(this.tx('crud.authoring.overview.actionsInvalid', '{count} invalid').replace('{count}', String(invalidCount)));
1771
+ }
1772
+ return parts.join(' · ');
1773
+ }
1774
+ actionsOverviewNote() {
1775
+ const focusedAction = this.prioritizedActionKey();
1776
+ if (focusedAction && this.actionStatus(focusedAction) !== 'ready') {
1777
+ return this.tx('crud.authoring.overview.actionsNextCompact', 'Next: {action}. {summary}')
1778
+ .replace('{action}', this.actionLabel(focusedAction))
1779
+ .replace('{summary}', this.actionFocusSummary(focusedAction));
1780
+ }
1781
+ return this.tx('crud.authoring.overview.actionsTracked', '{count} tracked action blocks')
1782
+ .replace('{count}', String(this.actionKeys.length));
1783
+ }
1784
+ tableOverview() {
1785
+ const table = this.tableConfig();
1786
+ const title = table.toolbar?.title;
1787
+ const count = table.columns?.length || 0;
1788
+ if (title) {
1789
+ return title;
1790
+ }
1791
+ return `${count} ${this.tx('crud.authoring.overview.columns', 'columns')}`;
1792
+ }
1793
+ tableOverviewNote() {
1794
+ const table = this.tableConfig();
1795
+ const subtitle = table.toolbar?.subtitle;
1796
+ const parts = [this.tableBehaviorSummary(), this.tableStatesSummary()];
1797
+ if (subtitle) {
1798
+ parts.unshift(subtitle);
1799
+ }
1800
+ return parts.join(' · ');
1801
+ }
1802
+ tableStructureSummary() {
1803
+ const table = this.tableConfig();
1804
+ const count = table.columns?.length || 0;
1805
+ const density = table.appearance?.density
1806
+ ? this.tx(`crud.authoring.table.density.${table.appearance.density}`, table.appearance.density)
1807
+ : this.tx('crud.authoring.table.density.comfortable', 'Comfortable');
1808
+ return this.tx('crud.authoring.table.structureSummary', '{count} columns | {density}')
1809
+ .replace('{count}', String(count))
1810
+ .replace('{density}', density);
1811
+ }
1812
+ tableBehaviorSummary() {
1813
+ const table = this.tableConfig();
1814
+ const pagination = table.behavior?.pagination;
1815
+ const sorting = table.behavior?.sorting;
1816
+ const parts = [];
1817
+ if (pagination?.enabled === false) {
1818
+ parts.push(this.tx('crud.authoring.table.paginationOff', 'Pagination off'));
1819
+ }
1820
+ else {
1821
+ parts.push(this.tx('crud.authoring.table.paginationSummary', 'Page size {value}')
1822
+ .replace('{value}', String(pagination?.pageSize || 10)));
1823
+ }
1824
+ if (sorting?.enabled === false) {
1825
+ parts.push(this.tx('crud.authoring.table.sortingOff', 'Sorting off'));
1826
+ }
1827
+ else {
1828
+ parts.push(sorting?.strategy === 'server'
1829
+ ? this.tx('crud.authoring.table.sortingServer', 'Server sorting')
1830
+ : this.tx('crud.authoring.table.sortingClient', 'Client sorting'));
1831
+ }
1832
+ return parts.join(' | ');
1833
+ }
1834
+ tableStatesSummary() {
1835
+ const states = this.tableConfig().messages?.states;
1836
+ const customized = [
1837
+ states?.loading,
1838
+ states?.empty,
1839
+ states?.noResults,
1840
+ states?.error,
1841
+ ].filter((value) => !!String(value || '').trim()).length;
1842
+ if (!customized) {
1843
+ return this.tx('crud.authoring.table.statesDefault', 'Default state copy');
1844
+ }
1845
+ return this.tx('crud.authoring.table.statesCustom', '{count} custom states')
1846
+ .replace('{count}', String(customized));
1847
+ }
1848
+ tableEditingFlowSummary() {
1849
+ return this.tx('crud.authoring.table.flowSummary', 'Core presentation stays visible below. Pagination, sorting, state copy, and columns stay in advanced table panels.');
1850
+ }
1851
+ validationOverview() {
1852
+ if (this.errorCount() > 0) {
1853
+ return this.tx('crud.authoring.overview.validationErrors', '{count} errors')
1854
+ .replace('{count}', String(this.errorCount()));
1855
+ }
1856
+ if (this.warningCount() > 0) {
1857
+ return this.tx('crud.authoring.overview.validationWarnings', '{count} warnings')
1858
+ .replace('{count}', String(this.warningCount()));
1859
+ }
1860
+ return this.tx('crud.authoring.overview.validationClean', 'No blocking diagnostics');
1861
+ }
1862
+ validationOverviewNote() {
1863
+ if (this.errorCount() > 0 || this.warningCount() > 0) {
1864
+ return this.tx('crud.authoring.overview.validationGrouped', 'Grouped by section below for faster troubleshooting.');
1865
+ }
1866
+ return this.tx('crud.authoring.overview.validationAligned', 'Shell guidance and diagnostics are aligned.');
1867
+ }
1868
+ jsonSectionSummary() {
1869
+ if (this.errorCount() > 0) {
1870
+ return this.tx('crud.authoring.json.summaryErrors', 'The canonical document still has validation errors.');
1871
+ }
1872
+ if (this.warningCount() > 0) {
1873
+ return this.tx('crud.authoring.json.summaryWarnings', 'The canonical document has diagnostics worth reviewing.');
1874
+ }
1875
+ return this.tx('crud.authoring.json.summaryClean', 'The canonical document is structurally consistent.');
1876
+ }
1877
+ sectionStatus(section) {
1878
+ if (this.sectionDiagnostics(section).some((issue) => issue.level === 'error')) {
1879
+ return 'invalid';
1880
+ }
1881
+ if (section === 'connection') {
1882
+ return this.resourcePath().trim() ? 'ready' : 'pending';
1883
+ }
1884
+ if (section === 'defaults') {
1885
+ return this.defaultsOpenMode() && this.headerVariant() ? 'ready' : 'pending';
1886
+ }
1887
+ if (section === 'actions') {
1888
+ return this.actionKeys.every((actionName) => this.actionStatus(actionName) === 'ready')
1889
+ ? 'ready'
1890
+ : 'pending';
1891
+ }
1892
+ const table = this.tableConfig();
1893
+ const hasTitle = !!String(table.toolbar?.title || '').trim();
1894
+ const hasColumns = (table.columns?.length || 0) > 0;
1895
+ return hasTitle || hasColumns ? 'ready' : 'pending';
1896
+ }
1897
+ sectionStatusLabel(section) {
1898
+ return {
1899
+ ready: this.tx('crud.authoring.section.status.ready', 'Ready'),
1900
+ pending: this.tx('crud.authoring.section.status.pending', 'Needs attention'),
1901
+ invalid: this.tx('crud.authoring.section.status.invalid', 'Invalid'),
1902
+ }[this.sectionStatus(section)];
1903
+ }
1904
+ sectionSummary(section) {
1905
+ if (section === 'connection') {
1906
+ return this.connectionOverview();
1907
+ }
1908
+ if (section === 'defaults') {
1909
+ return this.defaultsOverview();
1910
+ }
1911
+ if (section === 'actions') {
1912
+ const readyCount = this.actionKeys.filter((actionName) => this.actionStatus(actionName) === 'ready').length;
1913
+ const invalidCount = this.actionKeys.filter((actionName) => this.actionStatus(actionName) === 'invalid').length;
1914
+ const focusedAction = this.prioritizedActionKey();
1915
+ if (invalidCount > 0) {
1916
+ return this.tx('crud.authoring.section.actions.invalidSummaryDetailed', '{count} action blocks have validation errors. Start with {action}.')
1917
+ .replace('{count}', String(invalidCount))
1918
+ .replace('{action}', focusedAction ? this.actionLabel(focusedAction) : this.actionLabel('create'));
1919
+ }
1920
+ if (readyCount === this.actionKeys.length) {
1921
+ return this.tx('crud.authoring.section.actions.readySummary', 'All primary CRUD actions are configured.');
1922
+ }
1923
+ return this.tx('crud.authoring.section.actions.pendingSummaryDetailed', '{count} of {total} action blocks are ready. Next: {action}.')
1924
+ .replace('{count}', String(readyCount))
1925
+ .replace('{total}', String(this.actionKeys.length))
1926
+ .replace('{action}', focusedAction ? this.actionLabel(focusedAction) : this.actionLabel('create'));
1927
+ }
1928
+ return this.tx('crud.authoring.table.sectionSummary', '{structure}. {behavior}. {states}.')
1929
+ .replace('{structure}', this.tableStructureSummary())
1930
+ .replace('{behavior}', this.tableBehaviorSummary())
1931
+ .replace('{states}', this.tableStatesSummary());
1932
+ }
1933
+ nextFocusTitle() {
1934
+ if (this.nextFocusStatus() === 'ready') {
1935
+ return this.tx('crud.authoring.nextFocus.completeTitle', 'Shell guidance complete');
1936
+ }
1937
+ return this.tx(`crud.authoring.nextFocus.${this.nextFocusSection()}`, this.nextFocusSection());
1938
+ }
1939
+ nextFocusSummary() {
1940
+ if (this.nextFocusStatus() === 'invalid') {
1941
+ if (this.nextFocusSection() === 'actions') {
1942
+ const focusedAction = this.prioritizedActionKey();
1943
+ if (focusedAction) {
1944
+ return this.tx('crud.authoring.nextFocus.actionsInvalid', 'Resolve {action} first. {summary}.')
1945
+ .replace('{action}', this.actionLabel(focusedAction))
1946
+ .replace('{summary}', this.actionFocusSummary(focusedAction));
1947
+ }
1948
+ }
1949
+ return this.tx('crud.authoring.nextFocus.invalid', 'Resolve the blocking diagnostics in this section before saving with confidence.');
1950
+ }
1951
+ if (this.nextFocusStatus() === 'pending') {
1952
+ if (this.nextFocusSection() === 'actions') {
1953
+ const focusedAction = this.prioritizedActionKey();
1954
+ if (focusedAction) {
1955
+ return this.tx('crud.authoring.nextFocus.actionsPending', 'Open {action} next. {summary}.')
1956
+ .replace('{action}', this.actionLabel(focusedAction))
1957
+ .replace('{summary}', this.actionFocusSummary(focusedAction));
1958
+ }
1959
+ }
1960
+ return this.tx('crud.authoring.nextFocus.pending', 'This section is the next best place to complete the canonical CRUD flow.');
1961
+ }
1962
+ return this.tx('crud.authoring.nextFocus.ready', 'All tracked sections are currently in a good state.');
1963
+ }
1964
+ nextFocusChipLabel() {
1965
+ if (this.nextFocusStatus() === 'ready') {
1966
+ return this.tx('crud.authoring.nextFocus.complete', 'Complete');
1967
+ }
1968
+ return this.sectionStatusLabel(this.nextFocusSection());
1969
+ }
1970
+ healthBucketLabel(bucket) {
1971
+ return {
1972
+ invalid: this.tx('crud.authoring.health.invalid', 'Invalid'),
1973
+ pending: this.tx('crud.authoring.health.pending', 'Needs attention'),
1974
+ ready: this.tx('crud.authoring.health.ready', 'Healthy'),
1975
+ }[bucket];
1976
+ }
1977
+ healthBucketSummary(bucket) {
1978
+ const sections = ['connection', 'defaults', 'actions', 'table']
1979
+ .filter((section) => this.sectionStatus(section) === bucket);
1980
+ if (!sections.length) {
1981
+ return this.tx(`crud.authoring.health.${bucket}Empty`, 'None');
1982
+ }
1983
+ return sections.map((section) => this.sectionDisplayLabel(section)).join(' · ');
1984
+ }
1985
+ diagnosticsGroupStatusLabel(section, issues) {
1986
+ const hasError = issues.some((issue) => issue.level === 'error');
1987
+ if (hasError) {
1988
+ return this.tx('crud.authoring.diagnostics.errors', 'Errors');
1989
+ }
1990
+ return this.tx('crud.authoring.diagnostics.warnings', 'Warnings');
1991
+ }
1992
+ diagnosticsGroupSummary(section, issues) {
1993
+ const errorCount = issues.filter((issue) => issue.level === 'error').length;
1994
+ const warningCount = issues.length - errorCount;
1995
+ if (errorCount > 0) {
1996
+ return this.tx('crud.authoring.diagnostics.errorSummary', '{count} blocking diagnostics in this section.')
1997
+ .replace('{count}', String(errorCount));
1998
+ }
1999
+ return this.tx('crud.authoring.diagnostics.warningSummary', '{count} diagnostics worth reviewing in this section.')
2000
+ .replace('{count}', String(warningCount));
2001
+ }
2002
+ modeLabel(mode) {
2003
+ return this.tx(`crud.authoring.mode.${mode}`, mode);
2004
+ }
2005
+ variantLabel(variant) {
2006
+ return this.tx(`crud.authoring.header.variant.${variant}`, variant);
2007
+ }
2008
+ modalDensityLabel(density) {
2009
+ return this.tx(`crud.authoring.defaults.modal.density.${density}`, density);
2010
+ }
2011
+ backStrategyLabel(strategy) {
2012
+ return this.tx(`crud.authoring.defaults.back.strategy.${strategy}`, strategy);
2013
+ }
2014
+ actionInputsSummaryBadge(actionName) {
2015
+ const paramsCount = this.actionParams(actionName).length;
2016
+ const hasInitialValue = !!this.actionValue(actionName).form?.initialValue;
2017
+ const parts = [];
2018
+ if (paramsCount > 0) {
2019
+ parts.push(this.tx('crud.authoring.action.paramsSummaryCompact', '{count} map')
2020
+ .replace('{count}', String(paramsCount)));
2021
+ }
2022
+ if (hasInitialValue) {
2023
+ parts.push(this.tx('crud.authoring.action.initialValueReadyCompact', 'Seed'));
2024
+ }
2025
+ return parts.join(' · ');
2026
+ }
2027
+ paramTargetLabel(target) {
2028
+ return this.tx(`crud.authoring.action.paramTarget.${target}`, target);
2029
+ }
2030
+ diagnosticMessage(code, fallback) {
2031
+ const key = {
2032
+ 'crud.metadata.actions.route.required': 'crud.authoring.validation.routeRequired',
2033
+ 'crud.metadata.actions.formId.required': 'crud.authoring.validation.formIdRequired',
2034
+ 'crud.metadata.actions.form.submit-contract.invalid': 'crud.authoring.validation.submitContract',
2035
+ }[code];
2036
+ return key ? this.tx(key, fallback) : fallback;
2037
+ }
2038
+ setResourceField(field, value) {
2039
+ this.updateMetadata((metadata) => ({
2040
+ ...metadata,
2041
+ resource: {
2042
+ ...(metadata.resource || { path: '' }),
2043
+ [field]: typeof value === 'string' ? value : value ?? undefined,
2044
+ },
2045
+ }));
2046
+ }
2047
+ setDefaultsOpenMode(value) {
2048
+ this.updateMetadata((metadata) => ({
2049
+ ...metadata,
2050
+ defaults: {
2051
+ ...(metadata.defaults || {}),
2052
+ openMode: value || 'route',
2053
+ },
2054
+ }));
2055
+ }
2056
+ setHeaderField(field, value) {
2057
+ this.updateMetadata((metadata) => ({
2058
+ ...metadata,
2059
+ defaults: {
2060
+ ...(metadata.defaults || {}),
2061
+ header: {
2062
+ ...(metadata.defaults?.header || {}),
2063
+ [field]: value,
2064
+ },
2065
+ },
2066
+ }));
2067
+ }
2068
+ setModalField(field, value) {
2069
+ const nextValue = field === 'fullscreenBreakpoint'
2070
+ ? this.normalizeOptionalNumber(value)
2071
+ : value;
2072
+ this.updateMetadata((metadata) => ({
2073
+ ...metadata,
2074
+ defaults: {
2075
+ ...(metadata.defaults || {}),
2076
+ modal: {
2077
+ ...(metadata.defaults?.modal || {}),
2078
+ [field]: nextValue,
2079
+ },
2080
+ },
2081
+ }));
2082
+ }
2083
+ setBackField(field, value) {
2084
+ this.updateMetadata((metadata) => ({
2085
+ ...metadata,
2086
+ defaults: {
2087
+ ...(metadata.defaults || {}),
2088
+ back: {
2089
+ ...(metadata.defaults?.back || {}),
2090
+ [field]: typeof value === 'string' ? value : value ?? undefined,
2091
+ },
2092
+ },
2093
+ }));
2094
+ }
2095
+ setActionField(actionName, field, value) {
2096
+ this.patchAction(actionName, (action) => ({
2097
+ ...action,
2098
+ [field]: typeof value === 'string' ? value : value ?? undefined,
2099
+ }));
2100
+ }
2101
+ setActionFormField(actionName, field, value) {
2102
+ this.patchAction(actionName, (action) => ({
2103
+ ...action,
2104
+ form: {
2105
+ ...(action.form || {}),
2106
+ [field]: typeof value === 'string' ? value : value ?? undefined,
2107
+ },
2108
+ }));
2109
+ }
2110
+ addActionParam(actionName) {
2111
+ this.patchAction(actionName, (action) => ({
2112
+ ...action,
2113
+ params: [...(action.params || []), { from: '', to: 'input', name: '' }],
2114
+ }));
2115
+ }
2116
+ removeActionParam(actionName, index) {
2117
+ this.patchAction(actionName, (action) => {
2118
+ const params = [...(action.params || [])];
2119
+ params.splice(index, 1);
2120
+ return {
2121
+ ...action,
2122
+ params: params.length ? params : undefined,
2123
+ };
2124
+ });
2125
+ }
2126
+ setActionParamField(actionName, index, field, value) {
2127
+ this.patchAction(actionName, (action) => {
2128
+ const params = [...(action.params || [])];
2129
+ const current = params[index] || { from: '', to: 'input', name: '' };
2130
+ params[index] = {
2131
+ ...current,
2132
+ [field]: typeof value === 'string' ? value : value ?? undefined,
2133
+ };
2134
+ return {
2135
+ ...action,
2136
+ params,
2137
+ };
2138
+ });
2139
+ }
2140
+ setActionInitialValue(actionName, raw) {
2141
+ this.initialValueDrafts.update((drafts) => ({
2142
+ ...drafts,
2143
+ [actionName]: raw,
2144
+ }));
2145
+ const trimmed = raw.trim();
2146
+ if (!trimmed) {
2147
+ this.patchAction(actionName, (action) => ({
2148
+ ...action,
2149
+ form: {
2150
+ ...(action.form || {}),
2151
+ initialValue: undefined,
2152
+ },
2153
+ }));
2154
+ return;
2155
+ }
2156
+ try {
2157
+ const parsed = JSON.parse(trimmed);
2158
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
2159
+ return;
2160
+ }
2161
+ this.patchAction(actionName, (action) => ({
2162
+ ...action,
2163
+ form: {
2164
+ ...(action.form || {}),
2165
+ initialValue: parsed,
2166
+ },
2167
+ }));
2168
+ }
2169
+ catch {
2170
+ return;
2171
+ }
2172
+ }
2173
+ setActionBackField(actionName, field, value) {
2174
+ this.patchAction(actionName, (action) => ({
2175
+ ...action,
2176
+ back: {
2177
+ ...(action.back || {}),
2178
+ [field]: typeof value === 'string' ? value || undefined : value ?? undefined,
2179
+ },
2180
+ }));
2181
+ }
2182
+ setActionApiUrlEntry(actionName, raw) {
2183
+ let parsed = undefined;
2184
+ const trimmed = raw.trim();
2185
+ if (trimmed) {
2186
+ try {
2187
+ parsed = JSON.parse(trimmed);
2188
+ }
2189
+ catch {
2190
+ parsed = trimmed;
2191
+ }
2192
+ }
2193
+ this.patchAction(actionName, (action) => ({
2194
+ ...action,
2195
+ form: {
2196
+ ...(action.form || {}),
2197
+ apiUrlEntry: parsed,
2198
+ },
2199
+ }));
2200
+ }
2201
+ setTableConfig(config) {
2202
+ this.updateMetadata((metadata) => ({
2203
+ ...metadata,
2204
+ table: cloneTableConfig(config),
2205
+ }));
2206
+ }
2207
+ patchAction(actionName, patcher) {
2208
+ this.updateMetadata((metadata) => {
2209
+ const actions = [...(metadata.actions || [])];
2210
+ const index = actions.findIndex((entry) => entry?.action === actionName);
2211
+ const current = index >= 0 ? actions[index] : { action: actionName, label: this.actionLabel(actionName) };
2212
+ const next = patcher(current);
2213
+ if (index >= 0) {
2214
+ actions[index] = next;
2215
+ }
2216
+ else {
2217
+ actions.push(next);
2218
+ }
2219
+ return { ...metadata, actions };
2220
+ });
2221
+ }
2222
+ updateMetadata(updater) {
2223
+ const next = normalizeCrudAuthoringDocument({
2224
+ ...this.currentDocument(),
2225
+ metadata: updater(this.currentDocument().metadata),
2226
+ });
2227
+ this.currentDocument.set(next);
2228
+ this.syncState();
2229
+ }
2230
+ buildPayload() {
2231
+ const document = normalizeCrudAuthoringDocument(this.currentDocument());
2232
+ return {
2233
+ document,
2234
+ metadata: document.metadata,
2235
+ id: this.effectiveCrudId(),
2236
+ };
2237
+ }
2238
+ resolveExternalSeed() {
2239
+ return this.documentInput()
2240
+ || this.metadataInput()
2241
+ || this.injectedData?.document
2242
+ || this.injectedData?.metadata
2243
+ || null;
2244
+ }
2245
+ syncState() {
2246
+ const currentSignature = JSON.stringify(serializeCrudAuthoringDocument(this.currentDocument()));
2247
+ const initialSignature = JSON.stringify(serializeCrudAuthoringDocument(this.initialDocument()));
2248
+ this.isDirty$.next(currentSignature !== initialSignature);
2249
+ this.isValid$.next(!this.diagnostics().some((issue) => issue.level === 'error'));
2250
+ }
2251
+ tx(key, fallback) {
2252
+ return translateCrudAuthoringText(this.i18n, key, fallback);
2253
+ }
2254
+ normalizeOptionalNumber(value) {
2255
+ const trimmed = String(value ?? '').trim();
2256
+ if (!trimmed) {
2257
+ return undefined;
2258
+ }
2259
+ const parsed = Number(trimmed);
2260
+ return Number.isFinite(parsed) ? parsed : undefined;
2261
+ }
2262
+ hasHealthBucketContent(bucket) {
2263
+ return ['connection', 'defaults', 'actions', 'table']
2264
+ .some((section) => this.sectionStatus(section) === bucket);
2265
+ }
2266
+ sectionDiagnostics(section) {
2267
+ const prefix = {
2268
+ connection: 'metadata.resource',
2269
+ defaults: 'metadata.defaults',
2270
+ actions: 'metadata.actions',
2271
+ table: 'metadata.table',
2272
+ }[section];
2273
+ return this.diagnostics().filter((issue) => String(issue.path || '').includes(prefix));
2274
+ }
2275
+ actionDiagnostics(actionName) {
2276
+ return this.diagnostics()
2277
+ .filter((issue) => String(issue.path || '').includes(`metadata.actions.${actionName}`));
2278
+ }
2279
+ actionGroupLabelsByStatus(actionName, status) {
2280
+ return ['binding', 'schema', 'submit', 'api']
2281
+ .filter((group) => this.actionGroupStatus(actionName, group) === status)
2282
+ .map((group) => this.actionGroupLabel(group));
2283
+ }
2284
+ actionFocusSummary(actionName) {
2285
+ const invalidGroups = this.actionGroupLabelsByStatus(actionName, 'invalid');
2286
+ if (invalidGroups.length) {
2287
+ return this.tx('crud.authoring.action.focusInvalid', '{count} blocking groups')
2288
+ .replace('{count}', String(invalidGroups.length));
2289
+ }
2290
+ const pendingGroups = this.actionGroupLabelsByStatus(actionName, 'pending');
2291
+ if (pendingGroups.length) {
2292
+ return this.tx('crud.authoring.action.focusPending', '{count} groups still pending')
2293
+ .replace('{count}', String(pendingGroups.length));
2294
+ }
2295
+ return this.tx('crud.authoring.action.focusReady', 'Ready');
2296
+ }
2297
+ prioritizedActionKey() {
2298
+ return this.actionKeys.find((actionName) => this.actionStatus(actionName) === 'invalid')
2299
+ || this.actionKeys.find((actionName) => this.actionStatus(actionName) === 'pending')
2300
+ || null;
2301
+ }
2302
+ sectionDisplayLabel(section) {
2303
+ return {
2304
+ connection: this.tx('crud.authoring.section.connection', 'Connection'),
2305
+ defaults: this.tx('crud.authoring.section.defaults', 'Open mode and header'),
2306
+ actions: this.tx('crud.authoring.section.actions', 'Actions'),
2307
+ table: this.tx('crud.authoring.section.table', 'Table'),
2308
+ }[section];
2309
+ }
2310
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: CrudMetadataEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2311
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: CrudMetadataEditorComponent, isStandalone: true, selector: "praxis-crud-metadata-editor", inputs: { documentInput: { classPropertyName: "documentInput", publicName: "document", isSignal: true, isRequired: false, transformFunction: null }, metadataInput: { classPropertyName: "metadataInput", publicName: "metadata", isSignal: true, isRequired: false, transformFunction: null }, crudIdInput: { classPropertyName: "crudIdInput", publicName: "crudId", isSignal: true, isRequired: false, transformFunction: null }, readonlyInput: { classPropertyName: "readonlyInput", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null } }, providers: [providePraxisI18nConfig(PRAXIS_CRUD_AUTHORING_I18N_CONFIG)], ngImport: i0, template: `
2312
+ <section class="editor-shell" data-testid="crud-metadata-editor">
2313
+ <header class="editor-header">
2314
+ <div>
2315
+ <h2>{{ tx('crud.authoring.title', 'CRUD settings') }}</h2>
2316
+ <p>{{ tx('crud.authoring.subtitle', 'Edit the canonical CRUD flow document.') }}</p>
2317
+ </div>
2318
+ <span class="editor-chip" data-testid="crud-metadata-editor-id">{{ effectiveCrudId() || 'lab' }}</span>
2319
+ </header>
2320
+
2321
+ <section class="editor-overview" data-testid="crud-editor-overview">
2322
+ <article class="editor-overview-card">
2323
+ <span class="editor-overview-label">{{ tx('crud.authoring.overview.connection', 'Connection') }}</span>
2324
+ <strong class="editor-overview-value" data-testid="crud-editor-overview-connection">{{ connectionOverview() }}</strong>
2325
+ <p class="editor-overview-note" data-testid="crud-editor-overview-connection-note">{{ connectionOverviewNote() }}</p>
2326
+ </article>
2327
+ <article class="editor-overview-card">
2328
+ <span class="editor-overview-label">{{ tx('crud.authoring.overview.defaults', 'Defaults') }}</span>
2329
+ <strong class="editor-overview-value" data-testid="crud-editor-overview-defaults">{{ defaultsOverview() }}</strong>
2330
+ <p class="editor-overview-note" data-testid="crud-editor-overview-defaults-note">{{ defaultsOverviewNote() }}</p>
2331
+ </article>
2332
+ <article class="editor-overview-card">
2333
+ <span class="editor-overview-label">{{ tx('crud.authoring.overview.actions', 'Actions') }}</span>
2334
+ <strong class="editor-overview-value" data-testid="crud-editor-overview-actions">{{ actionsOverview() }}</strong>
2335
+ <p class="editor-overview-note" data-testid="crud-editor-overview-actions-note">{{ actionsOverviewNote() }}</p>
2336
+ </article>
2337
+ <article class="editor-overview-card">
2338
+ <span class="editor-overview-label">{{ tx('crud.authoring.overview.table', 'Table') }}</span>
2339
+ <strong class="editor-overview-value" data-testid="crud-editor-overview-table">{{ tableOverview() }}</strong>
2340
+ <p class="editor-overview-note" data-testid="crud-editor-overview-table-note">{{ tableOverviewNote() }}</p>
2341
+ </article>
2342
+ <article class="editor-overview-card" [class.editor-overview-card--error]="errorCount() > 0" [class.editor-overview-card--warn]="errorCount() === 0 && warningCount() > 0">
2343
+ <span class="editor-overview-label">{{ tx('crud.authoring.overview.validation', 'Validation') }}</span>
2344
+ <strong class="editor-overview-value" data-testid="crud-editor-overview-validation">{{ validationOverview() }}</strong>
2345
+ <p class="editor-overview-note" data-testid="crud-editor-overview-validation-note">{{ validationOverviewNote() }}</p>
2346
+ </article>
2347
+ </section>
2348
+
2349
+ @if (showHealthMap()) {
2350
+ <section class="editor-health-map" data-testid="crud-editor-health-map">
2351
+ @for (bucket of visibleHealthBuckets(); track bucket) {
2352
+ <article
2353
+ class="editor-health-card"
2354
+ [class.editor-health-card--error]="bucket === 'invalid'"
2355
+ [class.editor-health-card--warn]="bucket === 'pending'"
2356
+ [attr.data-testid]="'crud-editor-health-' + bucket"
2357
+ >
2358
+ <span class="editor-overview-label">{{ healthBucketLabel(bucket) }}</span>
2359
+ <strong>{{ healthBucketSummary(bucket) }}</strong>
2360
+ </article>
2361
+ }
2362
+ </section>
2363
+ }
2364
+
2365
+ <section class="editor-focus" data-testid="crud-editor-next-focus" [class.editor-focus--success]="nextFocusStatus() === 'ready'">
2366
+ <span
2367
+ class="editor-focus-chip"
2368
+ [class.editor-focus-chip--error]="nextFocusStatus() === 'invalid'"
2369
+ [class.editor-focus-chip--warn]="nextFocusStatus() === 'pending'"
2370
+ >
2371
+ {{ nextFocusChipLabel() }}
2372
+ </span>
2373
+ <div class="editor-focus-copy">
2374
+ <strong>{{ nextFocusTitle() }}</strong>
2375
+ <p>{{ nextFocusSummary() }}</p>
2376
+ </div>
2377
+ </section>
2378
+
2379
+ <mat-card class="editor-card">
2380
+ <div class="editor-section-header">
2381
+ <div>
2382
+ <h3>{{ tx('crud.authoring.section.connection', 'Connection') }}</h3>
2383
+ <p class="editor-section-note">{{ sectionSummary('connection') }}</p>
2384
+ </div>
2385
+ <span class="action-summary-chip" [class.action-summary-chip--error]="sectionStatus('connection') === 'invalid'" [class.action-summary-chip--warn]="sectionStatus('connection') === 'pending'">
2386
+ {{ sectionStatusLabel('connection') }}
2387
+ </span>
2388
+ </div>
2389
+ <div class="editor-grid">
2390
+ <mat-form-field appearance="outline">
2391
+ <mat-label>{{ tx('crud.authoring.connection.resourcePath', 'Resource') }}</mat-label>
2392
+ <input matInput data-testid="crud-editor-resource-path" [ngModel]="resourcePath()" (ngModelChange)="setResourceField('path', $event)" [disabled]="isReadonly()" />
2393
+ </mat-form-field>
2394
+ <mat-form-field appearance="outline">
2395
+ <mat-label>{{ tx('crud.authoring.connection.idField', 'Identifier field') }}</mat-label>
2396
+ <input matInput data-testid="crud-editor-resource-idField" [ngModel]="resourceIdField()" (ngModelChange)="setResourceField('idField', $event)" [disabled]="isReadonly()" />
2397
+ </mat-form-field>
2398
+ <mat-form-field appearance="outline">
2399
+ <mat-label>{{ tx('crud.authoring.connection.endpointKey', 'Endpoint/API') }}</mat-label>
2400
+ <input matInput data-testid="crud-editor-resource-endpointKey" [ngModel]="resourceEndpointKey()" (ngModelChange)="setResourceField('endpointKey', $event)" [disabled]="isReadonly()" />
2401
+ </mat-form-field>
2402
+ </div>
2403
+ </mat-card>
2404
+
2405
+ <mat-card class="editor-card">
2406
+ <div class="editor-section-header">
2407
+ <div>
2408
+ <h3>{{ tx('crud.authoring.section.defaults', 'Open mode and header') }}</h3>
2409
+ <p class="editor-section-note">{{ sectionSummary('defaults') }}</p>
2410
+ </div>
2411
+ <span class="action-summary-chip" [class.action-summary-chip--error]="sectionStatus('defaults') === 'invalid'" [class.action-summary-chip--warn]="sectionStatus('defaults') === 'pending'">
2412
+ {{ sectionStatusLabel('defaults') }}
2413
+ </span>
2414
+ </div>
2415
+ <div class="editor-grid">
2416
+ <mat-form-field appearance="outline">
2417
+ <mat-label>{{ tx('crud.authoring.defaults.openMode', 'Default open mode') }}</mat-label>
2418
+ <mat-select data-testid="crud-editor-defaults-openMode" [ngModel]="defaultsOpenMode()" (ngModelChange)="setDefaultsOpenMode($event)" [disabled]="isReadonly()">
2419
+ @for (mode of openModes; track mode) {
2420
+ <mat-option [value]="mode">{{ modeLabel(mode) }}</mat-option>
2421
+ }
2422
+ </mat-select>
2423
+ </mat-form-field>
2424
+ <mat-form-field appearance="outline">
2425
+ <mat-label>{{ tx('crud.authoring.defaults.header.backLabel', 'Back button label') }}</mat-label>
2426
+ <input matInput data-testid="crud-editor-header-backLabel" [ngModel]="headerBackLabel()" (ngModelChange)="setHeaderField('backLabel', $event)" [disabled]="isReadonly()" />
2427
+ </mat-form-field>
2428
+ <mat-form-field appearance="outline">
2429
+ <mat-label>{{ tx('crud.authoring.defaults.header.variant', 'Button variant') }}</mat-label>
2430
+ <mat-select data-testid="crud-editor-header-variant" [ngModel]="headerVariant()" (ngModelChange)="setHeaderField('variant', $event)" [disabled]="isReadonly()">
2431
+ @for (variant of headerVariants; track variant) {
2432
+ <mat-option [value]="variant">{{ variantLabel(variant) }}</mat-option>
2433
+ }
2434
+ </mat-select>
2435
+ </mat-form-field>
2436
+ </div>
2437
+ <div class="editor-toggles">
2438
+ <mat-slide-toggle data-testid="crud-editor-header-showBack" [ngModel]="headerShowBack()" (ngModelChange)="setHeaderField('showBack', $event)" [disabled]="isReadonly()">
2439
+ {{ tx('crud.authoring.defaults.header.showBack', 'Show back button') }}
2440
+ </mat-slide-toggle>
2441
+ <mat-slide-toggle data-testid="crud-editor-header-sticky" [ngModel]="headerSticky()" (ngModelChange)="setHeaderField('sticky', $event)" [disabled]="isReadonly()">
2442
+ {{ tx('crud.authoring.defaults.header.sticky', 'Sticky header') }}
2443
+ </mat-slide-toggle>
2444
+ <mat-slide-toggle data-testid="crud-editor-header-breadcrumbs" [ngModel]="headerBreadcrumbs()" (ngModelChange)="setHeaderField('breadcrumbs', $event)" [disabled]="isReadonly()">
2445
+ {{ tx('crud.authoring.defaults.header.breadcrumbs', 'Show breadcrumbs') }}
2446
+ </mat-slide-toggle>
2447
+ <mat-slide-toggle data-testid="crud-editor-header-divider" [ngModel]="headerDivider()" (ngModelChange)="setHeaderField('divider', $event)" [disabled]="isReadonly()">
2448
+ {{ tx('crud.authoring.defaults.header.divider', 'Show bottom divider') }}
2449
+ </mat-slide-toggle>
2450
+ </div>
2451
+ <section class="action-group" data-testid="crud-editor-defaults-modal-group">
2452
+ <div class="action-group__header">
2453
+ <div>
2454
+ <strong>{{ tx('crud.authoring.defaults.modal.group', 'Modal presentation') }}</strong>
2455
+ <p class="editor-section-note">{{ modalDefaultsNote() }}</p>
2456
+ </div>
2457
+ </div>
2458
+ <div class="editor-grid">
2459
+ <mat-form-field appearance="outline">
2460
+ <mat-label>{{ tx('crud.authoring.defaults.modal.density', 'Modal density') }}</mat-label>
2461
+ <mat-select data-testid="crud-editor-modal-density" [ngModel]="modalDensity()" (ngModelChange)="setModalField('density', $event)" [disabled]="isReadonly()">
2462
+ @for (density of modalDensities; track density) {
2463
+ <mat-option [value]="density">{{ modalDensityLabel(density) }}</mat-option>
2464
+ }
2465
+ </mat-select>
2466
+ </mat-form-field>
2467
+ <mat-form-field appearance="outline">
2468
+ <mat-label>{{ tx('crud.authoring.defaults.modal.fullscreenBreakpoint', 'Fullscreen breakpoint') }}</mat-label>
2469
+ <input matInput data-testid="crud-editor-modal-fullscreenBreakpoint" [ngModel]="modalFullscreenBreakpoint()" (ngModelChange)="setModalField('fullscreenBreakpoint', $event)" [disabled]="isReadonly()" />
2470
+ </mat-form-field>
2471
+ </div>
2472
+ <div class="editor-toggles">
2473
+ <mat-slide-toggle data-testid="crud-editor-modal-canMaximize" [ngModel]="modalCanMaximize()" (ngModelChange)="setModalField('canMaximize', $event)" [disabled]="isReadonly()">
2474
+ {{ tx('crud.authoring.defaults.modal.canMaximize', 'Show maximize control') }}
2475
+ </mat-slide-toggle>
2476
+ <mat-slide-toggle data-testid="crud-editor-modal-startMaximized" [ngModel]="modalStartMaximized()" (ngModelChange)="setModalField('startMaximized', $event)" [disabled]="isReadonly()">
2477
+ {{ tx('crud.authoring.defaults.modal.startMaximized', 'Start maximized') }}
2478
+ </mat-slide-toggle>
2479
+ <mat-slide-toggle data-testid="crud-editor-modal-rememberLastState" [ngModel]="modalRememberLastState()" (ngModelChange)="setModalField('rememberLastState', $event)" [disabled]="isReadonly()">
2480
+ {{ tx('crud.authoring.defaults.modal.rememberLastState', 'Remember last modal size') }}
2481
+ </mat-slide-toggle>
2482
+ <mat-slide-toggle data-testid="crud-editor-modal-disableCloseOnEsc" [ngModel]="modalDisableCloseOnEsc()" (ngModelChange)="setModalField('disableCloseOnEsc', $event)" [disabled]="isReadonly()">
2483
+ {{ tx('crud.authoring.defaults.modal.disableCloseOnEsc', 'Block close on Escape') }}
2484
+ </mat-slide-toggle>
2485
+ <mat-slide-toggle data-testid="crud-editor-modal-disableCloseOnBackdrop" [ngModel]="modalDisableCloseOnBackdrop()" (ngModelChange)="setModalField('disableCloseOnBackdrop', $event)" [disabled]="isReadonly()">
2486
+ {{ tx('crud.authoring.defaults.modal.disableCloseOnBackdrop', 'Block close on backdrop') }}
2487
+ </mat-slide-toggle>
2488
+ </div>
2489
+ </section>
2490
+ <section class="action-group" data-testid="crud-editor-defaults-back-group">
2491
+ <div class="action-group__header">
2492
+ <div>
2493
+ <strong>{{ tx('crud.authoring.defaults.back.group', 'Back behavior') }}</strong>
2494
+ <p class="editor-section-note">{{ backDefaultsNote() }}</p>
2495
+ </div>
2496
+ </div>
2497
+ <div class="editor-grid">
2498
+ <mat-form-field appearance="outline">
2499
+ <mat-label>{{ tx('crud.authoring.defaults.back.strategy', 'Back strategy') }}</mat-label>
2500
+ <mat-select data-testid="crud-editor-back-strategy" [ngModel]="backStrategy()" (ngModelChange)="setBackField('strategy', $event)" [disabled]="isReadonly()">
2501
+ @for (strategy of backStrategies; track strategy) {
2502
+ <mat-option [value]="strategy">{{ backStrategyLabel(strategy) }}</mat-option>
2503
+ }
2504
+ </mat-select>
2505
+ </mat-form-field>
2506
+ <mat-form-field appearance="outline">
2507
+ <mat-label>{{ tx('crud.authoring.defaults.back.returnTo', 'Return route') }}</mat-label>
2508
+ <input matInput data-testid="crud-editor-back-returnTo" [ngModel]="backReturnTo()" (ngModelChange)="setBackField('returnTo', $event)" [disabled]="isReadonly()" />
2509
+ </mat-form-field>
2510
+ </div>
2511
+ <div class="editor-toggles">
2512
+ <mat-slide-toggle data-testid="crud-editor-back-confirmOnDirty" [ngModel]="backConfirmOnDirty()" (ngModelChange)="setBackField('confirmOnDirty', $event)" [disabled]="isReadonly()">
2513
+ {{ tx('crud.authoring.defaults.back.confirmOnDirty', 'Confirm when leaving dirty forms') }}
2514
+ </mat-slide-toggle>
2515
+ </div>
2516
+ </section>
2517
+ </mat-card>
2518
+
2519
+ <section class="editor-actions">
2520
+ <div class="editor-section-header">
2521
+ <div>
2522
+ <h3>{{ tx('crud.authoring.section.actions', 'Actions') }}</h3>
2523
+ <p class="editor-section-note">{{ sectionSummary('actions') }}</p>
2524
+ </div>
2525
+ <span class="action-summary-chip" [class.action-summary-chip--error]="sectionStatus('actions') === 'invalid'" [class.action-summary-chip--warn]="sectionStatus('actions') === 'pending'">
2526
+ {{ sectionStatusLabel('actions') }}
2527
+ </span>
2528
+ </div>
2529
+ <div class="action-grid">
2530
+ @for (actionName of actionKeys; track actionName) {
2531
+ <mat-expansion-panel [expanded]="actionPanelExpanded(actionName)" class="editor-card" [attr.data-testid]="'crud-editor-action-' + actionName + '-panel'">
2532
+ <mat-expansion-panel-header [attr.data-testid]="'crud-editor-action-' + actionName + '-panel-header'">
2533
+ <mat-panel-title>{{ actionLabel(actionName) }}</mat-panel-title>
2534
+ <mat-panel-description [attr.data-testid]="'crud-editor-action-' + actionName + '-header-summary'">{{ actionSummary(actionName) }}</mat-panel-description>
2535
+ </mat-expansion-panel-header>
2536
+ <section class="action-summary-strip" [attr.data-testid]="'crud-editor-action-' + actionName + '-summary'">
2537
+ @if (actionShowStatusChip(actionName)) {
2538
+ <span class="action-summary-chip" [class.action-summary-chip--error]="actionStatus(actionName) === 'invalid'" [class.action-summary-chip--warn]="actionStatus(actionName) === 'pending'" [attr.data-testid]="'crud-editor-action-' + actionName + '-status-chip'">
2539
+ {{ actionStatusLabel(actionName) }}
2540
+ </span>
2541
+ }
2542
+ <span class="action-summary-chip" [class.action-summary-chip--error]="actionStatus(actionName) === 'invalid'" [class.action-summary-chip--warn]="actionStatus(actionName) !== 'ready'" [attr.data-testid]="'crud-editor-action-' + actionName + '-primary-summary'">
2543
+ {{ actionPrimarySummary(actionName) }}
2544
+ </span>
2545
+ <span class="action-summary-chip" [attr.data-testid]="'crud-editor-action-' + actionName + '-advanced-summary'">{{ actionAdvancedSummary(actionName) }}</span>
2546
+ </section>
2547
+ <section class="action-group" [attr.data-testid]="'crud-editor-action-' + actionName + '-binding-group'">
2548
+ <div class="action-group__header">
2549
+ <div>
2550
+ <strong>{{ tx('crud.authoring.action.bindingGroup', 'Binding') }}</strong>
2551
+ <p class="editor-section-note">{{ actionBindingNote(actionName) }}</p>
2552
+ </div>
2553
+ <span class="action-summary-chip" [class.action-summary-chip--error]="actionGroupStatus(actionName, 'binding') === 'invalid'" [class.action-summary-chip--warn]="actionGroupStatus(actionName, 'binding') === 'pending'" [attr.data-testid]="'crud-editor-action-' + actionName + '-binding-status'">
2554
+ {{ actionGroupStatusLabel(actionName, 'binding') }}
2555
+ </span>
2556
+ </div>
2557
+ <div class="editor-grid">
2558
+ <mat-form-field appearance="outline">
2559
+ <mat-label>{{ tx('crud.authoring.action.mode', 'Open mode') }}</mat-label>
2560
+ <mat-select [attr.data-testid]="'crud-editor-action-' + actionName + '-openMode'" [ngModel]="actionValue(actionName).openMode || ''" (ngModelChange)="setActionField(actionName, 'openMode', $event)" [disabled]="isReadonly()">
2561
+ <mat-option value="">Default</mat-option>
2562
+ @for (mode of openModes; track mode) {
2563
+ <mat-option [value]="mode">{{ modeLabel(mode) }}</mat-option>
2564
+ }
2565
+ </mat-select>
2566
+ </mat-form-field>
2567
+ <mat-form-field appearance="outline">
2568
+ <mat-label>{{ tx('crud.authoring.action.route', 'Route') }}</mat-label>
2569
+ <input matInput [attr.data-testid]="'crud-editor-action-' + actionName + '-route'" [ngModel]="actionValue(actionName).route || ''" (ngModelChange)="setActionField(actionName, 'route', $event)" [disabled]="isReadonly()" />
2570
+ </mat-form-field>
2571
+ <mat-form-field appearance="outline">
2572
+ <mat-label>{{ tx('crud.authoring.action.formId', 'formId') }}</mat-label>
2573
+ <input matInput [attr.data-testid]="'crud-editor-action-' + actionName + '-formId'" [ngModel]="actionValue(actionName).formId || ''" (ngModelChange)="setActionField(actionName, 'formId', $event)" [disabled]="isReadonly()" />
2574
+ </mat-form-field>
2575
+ </div>
2576
+ </section>
2577
+ <section class="action-group" [attr.data-testid]="'crud-editor-action-' + actionName + '-schema-group'">
2578
+ <div class="action-group__header">
2579
+ <div>
2580
+ <strong>{{ tx('crud.authoring.action.schemaGroup', 'Schema contract') }}</strong>
2581
+ <p class="editor-section-note">{{ actionSchemaNote(actionName) }}</p>
2582
+ </div>
2583
+ <span class="action-summary-chip" [class.action-summary-chip--error]="actionGroupStatus(actionName, 'schema') === 'invalid'" [class.action-summary-chip--warn]="actionGroupStatus(actionName, 'schema') === 'pending'" [attr.data-testid]="'crud-editor-action-' + actionName + '-schema-status'">
2584
+ {{ actionGroupStatusLabel(actionName, 'schema') }}
2585
+ </span>
2586
+ </div>
2587
+ <div class="editor-grid">
2588
+ <mat-form-field appearance="outline">
2589
+ <mat-label>{{ tx('crud.authoring.action.schemaUrl', 'Schema URL') }}</mat-label>
2590
+ <input matInput [attr.data-testid]="'crud-editor-action-' + actionName + '-schemaUrl'" [ngModel]="actionValue(actionName).form?.schemaUrl || ''" (ngModelChange)="setActionFormField(actionName, 'schemaUrl', $event)" [disabled]="isReadonly()" />
2591
+ </mat-form-field>
2592
+ </div>
2593
+ </section>
2594
+ <mat-expansion-panel class="action-advanced-panel" [expanded]="actionAdvancedPanelExpanded(actionName)" [attr.data-testid]="'crud-editor-action-' + actionName + '-advanced-panel'">
2595
+ <mat-expansion-panel-header [attr.data-testid]="'crud-editor-action-' + actionName + '-advanced-header'">
2596
+ <mat-panel-title>{{ tx('crud.authoring.action.advanced', 'Submit and API details') }}</mat-panel-title>
2597
+ <mat-panel-description>{{ actionAdvancedSummary(actionName) }}</mat-panel-description>
2598
+ </mat-expansion-panel-header>
2599
+ <section class="action-group action-group--advanced" [attr.data-testid]="'crud-editor-action-' + actionName + '-submit-group'">
2600
+ <div class="action-group__header">
2601
+ <div>
2602
+ <strong>{{ tx('crud.authoring.action.submitGroup', 'Submit contract') }}</strong>
2603
+ <p class="editor-section-note">{{ actionSubmitNote(actionName) }}</p>
2604
+ </div>
2605
+ <span class="action-summary-chip" [class.action-summary-chip--error]="actionGroupStatus(actionName, 'submit') === 'invalid'" [class.action-summary-chip--warn]="actionGroupStatus(actionName, 'submit') === 'pending'" [attr.data-testid]="'crud-editor-action-' + actionName + '-submit-status'">
2606
+ {{ actionGroupStatusLabel(actionName, 'submit') }}
2607
+ </span>
2608
+ </div>
2609
+ <div class="editor-grid">
2610
+ <mat-form-field appearance="outline">
2611
+ <mat-label>{{ tx('crud.authoring.action.submitUrl', 'Submit URL') }}</mat-label>
2612
+ <input matInput [attr.data-testid]="'crud-editor-action-' + actionName + '-submitUrl'" [ngModel]="actionValue(actionName).form?.submitUrl || ''" (ngModelChange)="setActionFormField(actionName, 'submitUrl', $event)" [disabled]="isReadonly()" />
2613
+ </mat-form-field>
2614
+ <mat-form-field appearance="outline">
2615
+ <mat-label>{{ tx('crud.authoring.action.submitMethod', 'Submit method') }}</mat-label>
2616
+ <mat-select [attr.data-testid]="'crud-editor-action-' + actionName + '-submitMethod'" [ngModel]="actionValue(actionName).form?.submitMethod || ''" (ngModelChange)="setActionFormField(actionName, 'submitMethod', $event)" [disabled]="isReadonly()">
2617
+ <mat-option value="">None</mat-option>
2618
+ @for (method of submitMethods; track method) {
2619
+ <mat-option [value]="method">{{ method.toUpperCase() }}</mat-option>
2620
+ }
2621
+ </mat-select>
2622
+ </mat-form-field>
2623
+ </div>
2624
+ </section>
2625
+ <section class="action-group action-group--advanced" [attr.data-testid]="'crud-editor-action-' + actionName + '-api-group'">
2626
+ <div class="action-group__header">
2627
+ <div>
2628
+ <strong>{{ tx('crud.authoring.action.apiGroup', 'API mapping') }}</strong>
2629
+ <p class="editor-section-note">{{ actionApiNote(actionName) }}</p>
2630
+ </div>
2631
+ <span class="action-summary-chip" [class.action-summary-chip--error]="actionGroupStatus(actionName, 'api') === 'invalid'" [class.action-summary-chip--warn]="actionGroupStatus(actionName, 'api') === 'pending'" [attr.data-testid]="'crud-editor-action-' + actionName + '-api-status'">
2632
+ {{ actionGroupStatusLabel(actionName, 'api') }}
2633
+ </span>
2634
+ </div>
2635
+ <div class="editor-grid">
2636
+ <mat-form-field appearance="outline">
2637
+ <mat-label>{{ tx('crud.authoring.action.apiEndpointKey', 'API endpoint key') }}</mat-label>
2638
+ <input matInput [attr.data-testid]="'crud-editor-action-' + actionName + '-apiEndpointKey'" [ngModel]="actionValue(actionName).form?.apiEndpointKey || ''" (ngModelChange)="setActionFormField(actionName, 'apiEndpointKey', $event)" [disabled]="isReadonly()" />
2639
+ </mat-form-field>
2640
+ <mat-form-field appearance="outline">
2641
+ <mat-label>{{ tx('crud.authoring.action.apiUrlEntry', 'API URL entry') }}</mat-label>
2642
+ <textarea matInput rows="3" [attr.data-testid]="'crud-editor-action-' + actionName + '-apiUrlEntry'" [ngModel]="actionApiUrlEntryDraft(actionName)" (ngModelChange)="setActionApiUrlEntry(actionName, $event)" [disabled]="isReadonly()"></textarea>
2643
+ </mat-form-field>
2644
+ </div>
2645
+ </section>
2646
+ <section class="action-group action-group--advanced" [attr.data-testid]="'crud-editor-action-' + actionName + '-inputs-group'">
2647
+ <div class="action-group__header">
2648
+ <div>
2649
+ <strong>{{ tx('crud.authoring.action.inputsGroup', 'Input seeding') }}</strong>
2650
+ <p class="editor-section-note">{{ actionInputsNote(actionName) }}</p>
2651
+ </div>
2652
+ </div>
2653
+ <section class="action-subgroup" [attr.data-testid]="'crud-editor-action-' + actionName + '-params-subgroup'">
2654
+ <div class="action-subgroup__header">
2655
+ <strong>{{ tx('crud.authoring.action.paramsSubgroup', 'Row-derived mappings') }}</strong>
2656
+ <p class="editor-section-note">{{ actionParamsNote(actionName) }}</p>
2657
+ </div>
2658
+ <div class="action-param-list">
2659
+ @for (param of actionParams(actionName); track $index) {
2660
+ <div class="action-param-row" [attr.data-testid]="'crud-editor-action-' + actionName + '-param-' + $index">
2661
+ <div class="editor-grid editor-grid--compact">
2662
+ <mat-form-field appearance="outline">
2663
+ <mat-label>{{ tx('crud.authoring.action.paramFrom', 'From field') }}</mat-label>
2664
+ <input matInput [attr.data-testid]="'crud-editor-action-' + actionName + '-param-' + $index + '-from'" [ngModel]="param.from" (ngModelChange)="setActionParamField(actionName, $index, 'from', $event)" [disabled]="isReadonly()" />
2665
+ </mat-form-field>
2666
+ <mat-form-field appearance="outline">
2667
+ <mat-label>{{ tx('crud.authoring.action.paramTo', 'Target') }}</mat-label>
2668
+ <mat-select [attr.data-testid]="'crud-editor-action-' + actionName + '-param-' + $index + '-to'" [ngModel]="param.to" (ngModelChange)="setActionParamField(actionName, $index, 'to', $event)" [disabled]="isReadonly()">
2669
+ @for (target of paramTargets; track target) {
2670
+ <mat-option [value]="target">{{ paramTargetLabel(target) }}</mat-option>
2671
+ }
2672
+ </mat-select>
2673
+ </mat-form-field>
2674
+ <mat-form-field appearance="outline">
2675
+ <mat-label>{{ tx('crud.authoring.action.paramName', 'Destination name') }}</mat-label>
2676
+ <input matInput [attr.data-testid]="'crud-editor-action-' + actionName + '-param-' + $index + '-name'" [ngModel]="param.name" (ngModelChange)="setActionParamField(actionName, $index, 'name', $event)" [disabled]="isReadonly()" />
2677
+ </mat-form-field>
2678
+ </div>
2679
+ <button mat-stroked-button type="button" [attr.data-testid]="'crud-editor-action-' + actionName + '-param-' + $index + '-remove'" (click)="removeActionParam(actionName, $index)" [disabled]="isReadonly()">
2680
+ {{ tx('crud.authoring.action.paramRemove', 'Remove mapping') }}
2681
+ </button>
2682
+ </div>
2683
+ }
2684
+ <button mat-stroked-button type="button" [attr.data-testid]="'crud-editor-action-' + actionName + '-param-add'" (click)="addActionParam(actionName)" [disabled]="isReadonly()">
2685
+ {{ tx('crud.authoring.action.paramAdd', 'Add mapping') }}
2686
+ </button>
2687
+ </div>
2688
+ </section>
2689
+ <section class="action-subgroup" [attr.data-testid]="'crud-editor-action-' + actionName + '-initialValue-subgroup'">
2690
+ <div class="action-subgroup__header">
2691
+ <strong>{{ tx('crud.authoring.action.initialValueSubgroup', 'Fixed form seed') }}</strong>
2692
+ <p class="editor-section-note">{{ actionInitialValueNote(actionName) }}</p>
2693
+ </div>
2694
+ <mat-form-field appearance="outline">
2695
+ <mat-label>{{ tx('crud.authoring.action.initialValue', 'Initial value seed (JSON)') }}</mat-label>
2696
+ <textarea matInput rows="5" [attr.data-testid]="'crud-editor-action-' + actionName + '-initialValue'" [ngModel]="actionInitialValueDraft(actionName)" (ngModelChange)="setActionInitialValue(actionName, $event)" [disabled]="isReadonly()"></textarea>
2697
+ </mat-form-field>
2698
+ </section>
2699
+ </section>
2700
+ <section class="action-group action-group--advanced" [attr.data-testid]="'crud-editor-action-' + actionName + '-back-group'">
2701
+ <div class="action-group__header">
2702
+ <div>
2703
+ <strong>{{ tx('crud.authoring.action.backGroup', 'Back behavior override') }}</strong>
2704
+ <p class="editor-section-note">{{ actionBackNote(actionName) }}</p>
2705
+ </div>
2706
+ </div>
2707
+ <div class="editor-grid">
2708
+ <mat-form-field appearance="outline">
2709
+ <mat-label>{{ tx('crud.authoring.action.backStrategy', 'Back strategy override') }}</mat-label>
2710
+ <mat-select [attr.data-testid]="'crud-editor-action-' + actionName + '-back-strategy'" [ngModel]="actionBackStrategy(actionName)" (ngModelChange)="setActionBackField(actionName, 'strategy', $event)" [disabled]="isReadonly()">
2711
+ <mat-option value="">{{ tx('crud.authoring.action.backUseDefaults', 'Use defaults') }}</mat-option>
2712
+ @for (strategy of backStrategies; track strategy) {
2713
+ <mat-option [value]="strategy">{{ backStrategyLabel(strategy) }}</mat-option>
2714
+ }
2715
+ </mat-select>
2716
+ </mat-form-field>
2717
+ <mat-form-field appearance="outline">
2718
+ <mat-label>{{ tx('crud.authoring.action.backReturnTo', 'Action return route') }}</mat-label>
2719
+ <input matInput [attr.data-testid]="'crud-editor-action-' + actionName + '-back-returnTo'" [ngModel]="actionBackReturnTo(actionName)" (ngModelChange)="setActionBackField(actionName, 'returnTo', $event)" [disabled]="isReadonly()" />
2720
+ </mat-form-field>
2721
+ </div>
2722
+ <div class="editor-toggles">
2723
+ <mat-slide-toggle [attr.data-testid]="'crud-editor-action-' + actionName + '-back-confirmOnDirty'" [ngModel]="actionBackConfirmOnDirty(actionName)" (ngModelChange)="setActionBackField(actionName, 'confirmOnDirty', $event)" [disabled]="isReadonly()">
2724
+ {{ tx('crud.authoring.action.backConfirmOnDirty', 'Confirm when leaving dirty forms') }}
2725
+ </mat-slide-toggle>
2726
+ </div>
2727
+ </section>
2728
+ </mat-expansion-panel>
2729
+ </mat-expansion-panel>
2730
+ }
2731
+ </div>
2732
+ </section>
2733
+
2734
+ <mat-card class="editor-card">
2735
+ <div class="editor-section-header">
2736
+ <div>
2737
+ <h3>{{ tx('crud.authoring.section.table', 'Table') }}</h3>
2738
+ <p class="editor-section-note">{{ sectionSummary('table') }}</p>
2739
+ </div>
2740
+ <span class="action-summary-chip" [class.action-summary-chip--error]="sectionStatus('table') === 'invalid'" [class.action-summary-chip--warn]="sectionStatus('table') === 'pending'">
2741
+ {{ sectionStatusLabel('table') }}
2742
+ </span>
2743
+ </div>
2744
+ <section class="action-summary-strip" data-testid="crud-editor-table-summary-strip">
2745
+ <span class="action-summary-chip" data-testid="crud-editor-table-summary-structure">{{ tableStructureSummary() }}</span>
2746
+ <span class="action-summary-chip" data-testid="crud-editor-table-summary-behavior">{{ tableBehaviorSummary() }}</span>
2747
+ <span class="action-summary-chip" data-testid="crud-editor-table-summary-states">{{ tableStatesSummary() }}</span>
2748
+ </section>
2749
+ <p class="editor-section-note" data-testid="crud-editor-table-flow-note">{{ tableEditingFlowSummary() }}</p>
2750
+ <praxis-table-inline-authoring-editor
2751
+ data-testid="crud-editor-table-inline"
2752
+ [config]="tableConfig()"
2753
+ [readonly]="isReadonly()"
2754
+ (configChange)="setTableConfig($event)"
2755
+ />
2756
+ </mat-card>
2757
+
2758
+ <mat-card class="editor-card">
2759
+ <h3>{{ tx('crud.authoring.section.json', 'JSON') }}</h3>
2760
+ <p class="editor-section-note" data-testid="crud-editor-json-summary">{{ jsonSectionSummary() }}</p>
2761
+ @if (diagnostics().length) {
2762
+ <section class="diagnostics-groups" data-testid="crud-editor-diagnostics">
2763
+ @for (group of groupedDiagnostics(); track group.section) {
2764
+ <article class="diagnostics-group" [attr.data-testid]="'crud-editor-diagnostics-' + group.section">
2765
+ <div class="editor-section-header">
2766
+ <div>
2767
+ <strong>{{ sectionDisplayLabel(group.section) }}</strong>
2768
+ <p class="editor-section-note">{{ diagnosticsGroupSummary(group.section, group.issues) }}</p>
2769
+ </div>
2770
+ <span class="action-summary-chip" [class.action-summary-chip--error]="group.hasError" [class.action-summary-chip--warn]="!group.hasError">
2771
+ {{ diagnosticsGroupStatusLabel(group.section, group.issues) }}
2772
+ </span>
2773
+ </div>
2774
+ <ul class="diagnostics">
2775
+ @for (issue of group.issues; track issue.code + issue.path) {
2776
+ <li [class.error]="issue.level === 'error'">{{ issue.level }}: {{ diagnosticMessage(issue.code, issue.message) }}</li>
2777
+ }
2778
+ </ul>
2779
+ </article>
2780
+ }
2781
+ </section>
2782
+ }
2783
+ <pre data-testid="crud-editor-json">{{ serializedDocument() }}</pre>
2784
+ </mat-card>
2785
+ </section>
2786
+ `, isInline: true, styles: [":host{display:block;min-width:0;color:var(--md-sys-color-on-surface,#1a1b20)}.editor-shell{display:grid;gap:16px}.editor-header{display:flex;justify-content:space-between;gap:16px;align-items:start}.editor-overview,.editor-health-map{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}.editor-health-card{display:grid;gap:4px;padding:14px;border-radius:18px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}.editor-health-card--warn{background:color-mix(in srgb,var(--md-sys-color-tertiary-container,#f8e08e) 78%,transparent)}.editor-health-card--error{background:color-mix(in srgb,var(--md-sys-color-error-container,#f9dedc) 82%,transparent);color:var(--md-sys-color-on-error-container,#410e0b)}.editor-focus{display:flex;gap:12px;align-items:start;padding:14px 16px;border-radius:18px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}.editor-focus--success{background:color-mix(in srgb,var(--md-sys-color-secondary-container,#d7e3ff) 70%,transparent)}.editor-focus-chip{padding:6px 10px;border-radius:999px;font-size:12px;background:color-mix(in srgb,var(--md-sys-color-secondary-container,#d7e3ff) 70%,transparent);white-space:nowrap}.editor-focus-chip--warn{background:color-mix(in srgb,var(--md-sys-color-tertiary-container,#f8e08e) 78%,transparent)}.editor-focus-chip--error{background:color-mix(in srgb,var(--md-sys-color-error-container,#f9dedc) 82%,transparent);color:var(--md-sys-color-on-error-container,#410e0b)}.editor-focus-copy{display:grid;gap:4px}.editor-focus-copy p{margin:0;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.editor-overview-card{display:grid;gap:4px;padding:14px;border-radius:18px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}.editor-overview-card--warn{background:color-mix(in srgb,var(--md-sys-color-tertiary-container,#f8e08e) 78%,transparent)}.editor-overview-card--error{background:color-mix(in srgb,var(--md-sys-color-error-container,#f9dedc) 82%,transparent);color:var(--md-sys-color-on-error-container,#410e0b)}.editor-overview-label{font-size:12px;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.editor-overview-value{line-height:1.3;overflow-wrap:anywhere}.editor-overview-note{margin:0;font-size:12px;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.editor-header h2,.editor-card h3{margin:0}.editor-header p{margin:6px 0 0;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.editor-chip{padding:6px 10px;border-radius:999px;background:color-mix(in srgb,var(--md-sys-color-primary,#1263b4) 12%,transparent);font-size:12px}.editor-card{display:grid;gap:14px;padding:16px;border-radius:20px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline,#c5c7ce) 70%,transparent)}.editor-section-header{display:flex;justify-content:space-between;gap:12px;align-items:start}.editor-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-grid--compact{grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}.editor-toggles{display:grid;gap:10px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-actions,.action-grid{display:grid;gap:14px}.action-summary-strip{display:flex;flex-wrap:wrap;gap:8px}.action-summary-chip{padding:6px 10px;border-radius:999px;font-size:12px;background:color-mix(in srgb,var(--md-sys-color-secondary-container,#d7e3ff) 70%,transparent)}.action-summary-chip--warn{background:color-mix(in srgb,var(--md-sys-color-tertiary-container,#f8e08e) 78%,transparent)}.action-summary-chip--error{background:color-mix(in srgb,var(--md-sys-color-error-container,#f9dedc) 82%,transparent);color:var(--md-sys-color-on-error-container,#410e0b)}.action-group{display:grid;gap:10px;padding:12px;border-radius:16px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 88%,transparent)}.action-group--advanced{background:color-mix(in srgb,var(--md-sys-color-surface-container,#f5f7fb) 90%,transparent)}.action-group__header{display:grid;gap:4px}.action-subgroup{display:grid;gap:10px;padding:10px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-container-low,#fafbfd) 92%,transparent)}.action-subgroup__header{display:grid;gap:4px}.action-param-list{display:grid;gap:12px}.action-param-row{display:grid;gap:8px}.action-advanced-panel{border:1px solid color-mix(in srgb,var(--md-sys-color-outline,#c5c7ce) 65%,transparent);border-radius:16px}.editor-section-note{margin:0;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.diagnostics-groups{display:grid;gap:12px}.diagnostics-group{display:grid;gap:10px;padding:12px;border-radius:16px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}.diagnostics{display:grid;gap:8px;margin:0;padding-left:18px}.diagnostics .error{color:var(--md-sys-color-error,#b3261e)}pre{margin:0;overflow:auto;max-height:320px;padding:12px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i3.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "component", type: i4.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i4.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i4.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "directive", type: i4.MatExpansionPanelDescription, selector: "mat-panel-description" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i7.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i7.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i8.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "component", type: PraxisTableInlineAuthoringEditorComponent, selector: "praxis-table-inline-authoring-editor", inputs: ["config", "readonly"], outputs: ["configChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2787
+ }
2788
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: CrudMetadataEditorComponent, decorators: [{
2789
+ type: Component,
2790
+ args: [{ selector: 'praxis-crud-metadata-editor', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [
2791
+ CommonModule,
2792
+ FormsModule,
2793
+ MatButtonModule,
2794
+ MatCardModule,
2795
+ MatExpansionModule,
2796
+ MatFormFieldModule,
2797
+ MatInputModule,
2798
+ MatSelectModule,
2799
+ MatSlideToggleModule,
2800
+ PraxisTableInlineAuthoringEditorComponent,
2801
+ ], providers: [providePraxisI18nConfig(PRAXIS_CRUD_AUTHORING_I18N_CONFIG)], template: `
2802
+ <section class="editor-shell" data-testid="crud-metadata-editor">
2803
+ <header class="editor-header">
2804
+ <div>
2805
+ <h2>{{ tx('crud.authoring.title', 'CRUD settings') }}</h2>
2806
+ <p>{{ tx('crud.authoring.subtitle', 'Edit the canonical CRUD flow document.') }}</p>
2807
+ </div>
2808
+ <span class="editor-chip" data-testid="crud-metadata-editor-id">{{ effectiveCrudId() || 'lab' }}</span>
2809
+ </header>
2810
+
2811
+ <section class="editor-overview" data-testid="crud-editor-overview">
2812
+ <article class="editor-overview-card">
2813
+ <span class="editor-overview-label">{{ tx('crud.authoring.overview.connection', 'Connection') }}</span>
2814
+ <strong class="editor-overview-value" data-testid="crud-editor-overview-connection">{{ connectionOverview() }}</strong>
2815
+ <p class="editor-overview-note" data-testid="crud-editor-overview-connection-note">{{ connectionOverviewNote() }}</p>
2816
+ </article>
2817
+ <article class="editor-overview-card">
2818
+ <span class="editor-overview-label">{{ tx('crud.authoring.overview.defaults', 'Defaults') }}</span>
2819
+ <strong class="editor-overview-value" data-testid="crud-editor-overview-defaults">{{ defaultsOverview() }}</strong>
2820
+ <p class="editor-overview-note" data-testid="crud-editor-overview-defaults-note">{{ defaultsOverviewNote() }}</p>
2821
+ </article>
2822
+ <article class="editor-overview-card">
2823
+ <span class="editor-overview-label">{{ tx('crud.authoring.overview.actions', 'Actions') }}</span>
2824
+ <strong class="editor-overview-value" data-testid="crud-editor-overview-actions">{{ actionsOverview() }}</strong>
2825
+ <p class="editor-overview-note" data-testid="crud-editor-overview-actions-note">{{ actionsOverviewNote() }}</p>
2826
+ </article>
2827
+ <article class="editor-overview-card">
2828
+ <span class="editor-overview-label">{{ tx('crud.authoring.overview.table', 'Table') }}</span>
2829
+ <strong class="editor-overview-value" data-testid="crud-editor-overview-table">{{ tableOverview() }}</strong>
2830
+ <p class="editor-overview-note" data-testid="crud-editor-overview-table-note">{{ tableOverviewNote() }}</p>
2831
+ </article>
2832
+ <article class="editor-overview-card" [class.editor-overview-card--error]="errorCount() > 0" [class.editor-overview-card--warn]="errorCount() === 0 && warningCount() > 0">
2833
+ <span class="editor-overview-label">{{ tx('crud.authoring.overview.validation', 'Validation') }}</span>
2834
+ <strong class="editor-overview-value" data-testid="crud-editor-overview-validation">{{ validationOverview() }}</strong>
2835
+ <p class="editor-overview-note" data-testid="crud-editor-overview-validation-note">{{ validationOverviewNote() }}</p>
2836
+ </article>
2837
+ </section>
2838
+
2839
+ @if (showHealthMap()) {
2840
+ <section class="editor-health-map" data-testid="crud-editor-health-map">
2841
+ @for (bucket of visibleHealthBuckets(); track bucket) {
2842
+ <article
2843
+ class="editor-health-card"
2844
+ [class.editor-health-card--error]="bucket === 'invalid'"
2845
+ [class.editor-health-card--warn]="bucket === 'pending'"
2846
+ [attr.data-testid]="'crud-editor-health-' + bucket"
2847
+ >
2848
+ <span class="editor-overview-label">{{ healthBucketLabel(bucket) }}</span>
2849
+ <strong>{{ healthBucketSummary(bucket) }}</strong>
2850
+ </article>
2851
+ }
2852
+ </section>
2853
+ }
2854
+
2855
+ <section class="editor-focus" data-testid="crud-editor-next-focus" [class.editor-focus--success]="nextFocusStatus() === 'ready'">
2856
+ <span
2857
+ class="editor-focus-chip"
2858
+ [class.editor-focus-chip--error]="nextFocusStatus() === 'invalid'"
2859
+ [class.editor-focus-chip--warn]="nextFocusStatus() === 'pending'"
2860
+ >
2861
+ {{ nextFocusChipLabel() }}
2862
+ </span>
2863
+ <div class="editor-focus-copy">
2864
+ <strong>{{ nextFocusTitle() }}</strong>
2865
+ <p>{{ nextFocusSummary() }}</p>
2866
+ </div>
2867
+ </section>
2868
+
2869
+ <mat-card class="editor-card">
2870
+ <div class="editor-section-header">
2871
+ <div>
2872
+ <h3>{{ tx('crud.authoring.section.connection', 'Connection') }}</h3>
2873
+ <p class="editor-section-note">{{ sectionSummary('connection') }}</p>
2874
+ </div>
2875
+ <span class="action-summary-chip" [class.action-summary-chip--error]="sectionStatus('connection') === 'invalid'" [class.action-summary-chip--warn]="sectionStatus('connection') === 'pending'">
2876
+ {{ sectionStatusLabel('connection') }}
2877
+ </span>
2878
+ </div>
2879
+ <div class="editor-grid">
2880
+ <mat-form-field appearance="outline">
2881
+ <mat-label>{{ tx('crud.authoring.connection.resourcePath', 'Resource') }}</mat-label>
2882
+ <input matInput data-testid="crud-editor-resource-path" [ngModel]="resourcePath()" (ngModelChange)="setResourceField('path', $event)" [disabled]="isReadonly()" />
2883
+ </mat-form-field>
2884
+ <mat-form-field appearance="outline">
2885
+ <mat-label>{{ tx('crud.authoring.connection.idField', 'Identifier field') }}</mat-label>
2886
+ <input matInput data-testid="crud-editor-resource-idField" [ngModel]="resourceIdField()" (ngModelChange)="setResourceField('idField', $event)" [disabled]="isReadonly()" />
2887
+ </mat-form-field>
2888
+ <mat-form-field appearance="outline">
2889
+ <mat-label>{{ tx('crud.authoring.connection.endpointKey', 'Endpoint/API') }}</mat-label>
2890
+ <input matInput data-testid="crud-editor-resource-endpointKey" [ngModel]="resourceEndpointKey()" (ngModelChange)="setResourceField('endpointKey', $event)" [disabled]="isReadonly()" />
2891
+ </mat-form-field>
2892
+ </div>
2893
+ </mat-card>
2894
+
2895
+ <mat-card class="editor-card">
2896
+ <div class="editor-section-header">
2897
+ <div>
2898
+ <h3>{{ tx('crud.authoring.section.defaults', 'Open mode and header') }}</h3>
2899
+ <p class="editor-section-note">{{ sectionSummary('defaults') }}</p>
2900
+ </div>
2901
+ <span class="action-summary-chip" [class.action-summary-chip--error]="sectionStatus('defaults') === 'invalid'" [class.action-summary-chip--warn]="sectionStatus('defaults') === 'pending'">
2902
+ {{ sectionStatusLabel('defaults') }}
2903
+ </span>
2904
+ </div>
2905
+ <div class="editor-grid">
2906
+ <mat-form-field appearance="outline">
2907
+ <mat-label>{{ tx('crud.authoring.defaults.openMode', 'Default open mode') }}</mat-label>
2908
+ <mat-select data-testid="crud-editor-defaults-openMode" [ngModel]="defaultsOpenMode()" (ngModelChange)="setDefaultsOpenMode($event)" [disabled]="isReadonly()">
2909
+ @for (mode of openModes; track mode) {
2910
+ <mat-option [value]="mode">{{ modeLabel(mode) }}</mat-option>
2911
+ }
2912
+ </mat-select>
2913
+ </mat-form-field>
2914
+ <mat-form-field appearance="outline">
2915
+ <mat-label>{{ tx('crud.authoring.defaults.header.backLabel', 'Back button label') }}</mat-label>
2916
+ <input matInput data-testid="crud-editor-header-backLabel" [ngModel]="headerBackLabel()" (ngModelChange)="setHeaderField('backLabel', $event)" [disabled]="isReadonly()" />
2917
+ </mat-form-field>
2918
+ <mat-form-field appearance="outline">
2919
+ <mat-label>{{ tx('crud.authoring.defaults.header.variant', 'Button variant') }}</mat-label>
2920
+ <mat-select data-testid="crud-editor-header-variant" [ngModel]="headerVariant()" (ngModelChange)="setHeaderField('variant', $event)" [disabled]="isReadonly()">
2921
+ @for (variant of headerVariants; track variant) {
2922
+ <mat-option [value]="variant">{{ variantLabel(variant) }}</mat-option>
2923
+ }
2924
+ </mat-select>
2925
+ </mat-form-field>
2926
+ </div>
2927
+ <div class="editor-toggles">
2928
+ <mat-slide-toggle data-testid="crud-editor-header-showBack" [ngModel]="headerShowBack()" (ngModelChange)="setHeaderField('showBack', $event)" [disabled]="isReadonly()">
2929
+ {{ tx('crud.authoring.defaults.header.showBack', 'Show back button') }}
2930
+ </mat-slide-toggle>
2931
+ <mat-slide-toggle data-testid="crud-editor-header-sticky" [ngModel]="headerSticky()" (ngModelChange)="setHeaderField('sticky', $event)" [disabled]="isReadonly()">
2932
+ {{ tx('crud.authoring.defaults.header.sticky', 'Sticky header') }}
2933
+ </mat-slide-toggle>
2934
+ <mat-slide-toggle data-testid="crud-editor-header-breadcrumbs" [ngModel]="headerBreadcrumbs()" (ngModelChange)="setHeaderField('breadcrumbs', $event)" [disabled]="isReadonly()">
2935
+ {{ tx('crud.authoring.defaults.header.breadcrumbs', 'Show breadcrumbs') }}
2936
+ </mat-slide-toggle>
2937
+ <mat-slide-toggle data-testid="crud-editor-header-divider" [ngModel]="headerDivider()" (ngModelChange)="setHeaderField('divider', $event)" [disabled]="isReadonly()">
2938
+ {{ tx('crud.authoring.defaults.header.divider', 'Show bottom divider') }}
2939
+ </mat-slide-toggle>
2940
+ </div>
2941
+ <section class="action-group" data-testid="crud-editor-defaults-modal-group">
2942
+ <div class="action-group__header">
2943
+ <div>
2944
+ <strong>{{ tx('crud.authoring.defaults.modal.group', 'Modal presentation') }}</strong>
2945
+ <p class="editor-section-note">{{ modalDefaultsNote() }}</p>
2946
+ </div>
2947
+ </div>
2948
+ <div class="editor-grid">
2949
+ <mat-form-field appearance="outline">
2950
+ <mat-label>{{ tx('crud.authoring.defaults.modal.density', 'Modal density') }}</mat-label>
2951
+ <mat-select data-testid="crud-editor-modal-density" [ngModel]="modalDensity()" (ngModelChange)="setModalField('density', $event)" [disabled]="isReadonly()">
2952
+ @for (density of modalDensities; track density) {
2953
+ <mat-option [value]="density">{{ modalDensityLabel(density) }}</mat-option>
2954
+ }
2955
+ </mat-select>
2956
+ </mat-form-field>
2957
+ <mat-form-field appearance="outline">
2958
+ <mat-label>{{ tx('crud.authoring.defaults.modal.fullscreenBreakpoint', 'Fullscreen breakpoint') }}</mat-label>
2959
+ <input matInput data-testid="crud-editor-modal-fullscreenBreakpoint" [ngModel]="modalFullscreenBreakpoint()" (ngModelChange)="setModalField('fullscreenBreakpoint', $event)" [disabled]="isReadonly()" />
2960
+ </mat-form-field>
2961
+ </div>
2962
+ <div class="editor-toggles">
2963
+ <mat-slide-toggle data-testid="crud-editor-modal-canMaximize" [ngModel]="modalCanMaximize()" (ngModelChange)="setModalField('canMaximize', $event)" [disabled]="isReadonly()">
2964
+ {{ tx('crud.authoring.defaults.modal.canMaximize', 'Show maximize control') }}
2965
+ </mat-slide-toggle>
2966
+ <mat-slide-toggle data-testid="crud-editor-modal-startMaximized" [ngModel]="modalStartMaximized()" (ngModelChange)="setModalField('startMaximized', $event)" [disabled]="isReadonly()">
2967
+ {{ tx('crud.authoring.defaults.modal.startMaximized', 'Start maximized') }}
2968
+ </mat-slide-toggle>
2969
+ <mat-slide-toggle data-testid="crud-editor-modal-rememberLastState" [ngModel]="modalRememberLastState()" (ngModelChange)="setModalField('rememberLastState', $event)" [disabled]="isReadonly()">
2970
+ {{ tx('crud.authoring.defaults.modal.rememberLastState', 'Remember last modal size') }}
2971
+ </mat-slide-toggle>
2972
+ <mat-slide-toggle data-testid="crud-editor-modal-disableCloseOnEsc" [ngModel]="modalDisableCloseOnEsc()" (ngModelChange)="setModalField('disableCloseOnEsc', $event)" [disabled]="isReadonly()">
2973
+ {{ tx('crud.authoring.defaults.modal.disableCloseOnEsc', 'Block close on Escape') }}
2974
+ </mat-slide-toggle>
2975
+ <mat-slide-toggle data-testid="crud-editor-modal-disableCloseOnBackdrop" [ngModel]="modalDisableCloseOnBackdrop()" (ngModelChange)="setModalField('disableCloseOnBackdrop', $event)" [disabled]="isReadonly()">
2976
+ {{ tx('crud.authoring.defaults.modal.disableCloseOnBackdrop', 'Block close on backdrop') }}
2977
+ </mat-slide-toggle>
2978
+ </div>
2979
+ </section>
2980
+ <section class="action-group" data-testid="crud-editor-defaults-back-group">
2981
+ <div class="action-group__header">
2982
+ <div>
2983
+ <strong>{{ tx('crud.authoring.defaults.back.group', 'Back behavior') }}</strong>
2984
+ <p class="editor-section-note">{{ backDefaultsNote() }}</p>
2985
+ </div>
2986
+ </div>
2987
+ <div class="editor-grid">
2988
+ <mat-form-field appearance="outline">
2989
+ <mat-label>{{ tx('crud.authoring.defaults.back.strategy', 'Back strategy') }}</mat-label>
2990
+ <mat-select data-testid="crud-editor-back-strategy" [ngModel]="backStrategy()" (ngModelChange)="setBackField('strategy', $event)" [disabled]="isReadonly()">
2991
+ @for (strategy of backStrategies; track strategy) {
2992
+ <mat-option [value]="strategy">{{ backStrategyLabel(strategy) }}</mat-option>
2993
+ }
2994
+ </mat-select>
2995
+ </mat-form-field>
2996
+ <mat-form-field appearance="outline">
2997
+ <mat-label>{{ tx('crud.authoring.defaults.back.returnTo', 'Return route') }}</mat-label>
2998
+ <input matInput data-testid="crud-editor-back-returnTo" [ngModel]="backReturnTo()" (ngModelChange)="setBackField('returnTo', $event)" [disabled]="isReadonly()" />
2999
+ </mat-form-field>
3000
+ </div>
3001
+ <div class="editor-toggles">
3002
+ <mat-slide-toggle data-testid="crud-editor-back-confirmOnDirty" [ngModel]="backConfirmOnDirty()" (ngModelChange)="setBackField('confirmOnDirty', $event)" [disabled]="isReadonly()">
3003
+ {{ tx('crud.authoring.defaults.back.confirmOnDirty', 'Confirm when leaving dirty forms') }}
3004
+ </mat-slide-toggle>
3005
+ </div>
3006
+ </section>
3007
+ </mat-card>
3008
+
3009
+ <section class="editor-actions">
3010
+ <div class="editor-section-header">
3011
+ <div>
3012
+ <h3>{{ tx('crud.authoring.section.actions', 'Actions') }}</h3>
3013
+ <p class="editor-section-note">{{ sectionSummary('actions') }}</p>
3014
+ </div>
3015
+ <span class="action-summary-chip" [class.action-summary-chip--error]="sectionStatus('actions') === 'invalid'" [class.action-summary-chip--warn]="sectionStatus('actions') === 'pending'">
3016
+ {{ sectionStatusLabel('actions') }}
3017
+ </span>
3018
+ </div>
3019
+ <div class="action-grid">
3020
+ @for (actionName of actionKeys; track actionName) {
3021
+ <mat-expansion-panel [expanded]="actionPanelExpanded(actionName)" class="editor-card" [attr.data-testid]="'crud-editor-action-' + actionName + '-panel'">
3022
+ <mat-expansion-panel-header [attr.data-testid]="'crud-editor-action-' + actionName + '-panel-header'">
3023
+ <mat-panel-title>{{ actionLabel(actionName) }}</mat-panel-title>
3024
+ <mat-panel-description [attr.data-testid]="'crud-editor-action-' + actionName + '-header-summary'">{{ actionSummary(actionName) }}</mat-panel-description>
3025
+ </mat-expansion-panel-header>
3026
+ <section class="action-summary-strip" [attr.data-testid]="'crud-editor-action-' + actionName + '-summary'">
3027
+ @if (actionShowStatusChip(actionName)) {
3028
+ <span class="action-summary-chip" [class.action-summary-chip--error]="actionStatus(actionName) === 'invalid'" [class.action-summary-chip--warn]="actionStatus(actionName) === 'pending'" [attr.data-testid]="'crud-editor-action-' + actionName + '-status-chip'">
3029
+ {{ actionStatusLabel(actionName) }}
3030
+ </span>
3031
+ }
3032
+ <span class="action-summary-chip" [class.action-summary-chip--error]="actionStatus(actionName) === 'invalid'" [class.action-summary-chip--warn]="actionStatus(actionName) !== 'ready'" [attr.data-testid]="'crud-editor-action-' + actionName + '-primary-summary'">
3033
+ {{ actionPrimarySummary(actionName) }}
3034
+ </span>
3035
+ <span class="action-summary-chip" [attr.data-testid]="'crud-editor-action-' + actionName + '-advanced-summary'">{{ actionAdvancedSummary(actionName) }}</span>
3036
+ </section>
3037
+ <section class="action-group" [attr.data-testid]="'crud-editor-action-' + actionName + '-binding-group'">
3038
+ <div class="action-group__header">
3039
+ <div>
3040
+ <strong>{{ tx('crud.authoring.action.bindingGroup', 'Binding') }}</strong>
3041
+ <p class="editor-section-note">{{ actionBindingNote(actionName) }}</p>
3042
+ </div>
3043
+ <span class="action-summary-chip" [class.action-summary-chip--error]="actionGroupStatus(actionName, 'binding') === 'invalid'" [class.action-summary-chip--warn]="actionGroupStatus(actionName, 'binding') === 'pending'" [attr.data-testid]="'crud-editor-action-' + actionName + '-binding-status'">
3044
+ {{ actionGroupStatusLabel(actionName, 'binding') }}
3045
+ </span>
3046
+ </div>
3047
+ <div class="editor-grid">
3048
+ <mat-form-field appearance="outline">
3049
+ <mat-label>{{ tx('crud.authoring.action.mode', 'Open mode') }}</mat-label>
3050
+ <mat-select [attr.data-testid]="'crud-editor-action-' + actionName + '-openMode'" [ngModel]="actionValue(actionName).openMode || ''" (ngModelChange)="setActionField(actionName, 'openMode', $event)" [disabled]="isReadonly()">
3051
+ <mat-option value="">Default</mat-option>
3052
+ @for (mode of openModes; track mode) {
3053
+ <mat-option [value]="mode">{{ modeLabel(mode) }}</mat-option>
3054
+ }
3055
+ </mat-select>
3056
+ </mat-form-field>
3057
+ <mat-form-field appearance="outline">
3058
+ <mat-label>{{ tx('crud.authoring.action.route', 'Route') }}</mat-label>
3059
+ <input matInput [attr.data-testid]="'crud-editor-action-' + actionName + '-route'" [ngModel]="actionValue(actionName).route || ''" (ngModelChange)="setActionField(actionName, 'route', $event)" [disabled]="isReadonly()" />
3060
+ </mat-form-field>
3061
+ <mat-form-field appearance="outline">
3062
+ <mat-label>{{ tx('crud.authoring.action.formId', 'formId') }}</mat-label>
3063
+ <input matInput [attr.data-testid]="'crud-editor-action-' + actionName + '-formId'" [ngModel]="actionValue(actionName).formId || ''" (ngModelChange)="setActionField(actionName, 'formId', $event)" [disabled]="isReadonly()" />
3064
+ </mat-form-field>
3065
+ </div>
3066
+ </section>
3067
+ <section class="action-group" [attr.data-testid]="'crud-editor-action-' + actionName + '-schema-group'">
3068
+ <div class="action-group__header">
3069
+ <div>
3070
+ <strong>{{ tx('crud.authoring.action.schemaGroup', 'Schema contract') }}</strong>
3071
+ <p class="editor-section-note">{{ actionSchemaNote(actionName) }}</p>
3072
+ </div>
3073
+ <span class="action-summary-chip" [class.action-summary-chip--error]="actionGroupStatus(actionName, 'schema') === 'invalid'" [class.action-summary-chip--warn]="actionGroupStatus(actionName, 'schema') === 'pending'" [attr.data-testid]="'crud-editor-action-' + actionName + '-schema-status'">
3074
+ {{ actionGroupStatusLabel(actionName, 'schema') }}
3075
+ </span>
3076
+ </div>
3077
+ <div class="editor-grid">
3078
+ <mat-form-field appearance="outline">
3079
+ <mat-label>{{ tx('crud.authoring.action.schemaUrl', 'Schema URL') }}</mat-label>
3080
+ <input matInput [attr.data-testid]="'crud-editor-action-' + actionName + '-schemaUrl'" [ngModel]="actionValue(actionName).form?.schemaUrl || ''" (ngModelChange)="setActionFormField(actionName, 'schemaUrl', $event)" [disabled]="isReadonly()" />
3081
+ </mat-form-field>
3082
+ </div>
3083
+ </section>
3084
+ <mat-expansion-panel class="action-advanced-panel" [expanded]="actionAdvancedPanelExpanded(actionName)" [attr.data-testid]="'crud-editor-action-' + actionName + '-advanced-panel'">
3085
+ <mat-expansion-panel-header [attr.data-testid]="'crud-editor-action-' + actionName + '-advanced-header'">
3086
+ <mat-panel-title>{{ tx('crud.authoring.action.advanced', 'Submit and API details') }}</mat-panel-title>
3087
+ <mat-panel-description>{{ actionAdvancedSummary(actionName) }}</mat-panel-description>
3088
+ </mat-expansion-panel-header>
3089
+ <section class="action-group action-group--advanced" [attr.data-testid]="'crud-editor-action-' + actionName + '-submit-group'">
3090
+ <div class="action-group__header">
3091
+ <div>
3092
+ <strong>{{ tx('crud.authoring.action.submitGroup', 'Submit contract') }}</strong>
3093
+ <p class="editor-section-note">{{ actionSubmitNote(actionName) }}</p>
3094
+ </div>
3095
+ <span class="action-summary-chip" [class.action-summary-chip--error]="actionGroupStatus(actionName, 'submit') === 'invalid'" [class.action-summary-chip--warn]="actionGroupStatus(actionName, 'submit') === 'pending'" [attr.data-testid]="'crud-editor-action-' + actionName + '-submit-status'">
3096
+ {{ actionGroupStatusLabel(actionName, 'submit') }}
3097
+ </span>
3098
+ </div>
3099
+ <div class="editor-grid">
3100
+ <mat-form-field appearance="outline">
3101
+ <mat-label>{{ tx('crud.authoring.action.submitUrl', 'Submit URL') }}</mat-label>
3102
+ <input matInput [attr.data-testid]="'crud-editor-action-' + actionName + '-submitUrl'" [ngModel]="actionValue(actionName).form?.submitUrl || ''" (ngModelChange)="setActionFormField(actionName, 'submitUrl', $event)" [disabled]="isReadonly()" />
3103
+ </mat-form-field>
3104
+ <mat-form-field appearance="outline">
3105
+ <mat-label>{{ tx('crud.authoring.action.submitMethod', 'Submit method') }}</mat-label>
3106
+ <mat-select [attr.data-testid]="'crud-editor-action-' + actionName + '-submitMethod'" [ngModel]="actionValue(actionName).form?.submitMethod || ''" (ngModelChange)="setActionFormField(actionName, 'submitMethod', $event)" [disabled]="isReadonly()">
3107
+ <mat-option value="">None</mat-option>
3108
+ @for (method of submitMethods; track method) {
3109
+ <mat-option [value]="method">{{ method.toUpperCase() }}</mat-option>
3110
+ }
3111
+ </mat-select>
3112
+ </mat-form-field>
3113
+ </div>
3114
+ </section>
3115
+ <section class="action-group action-group--advanced" [attr.data-testid]="'crud-editor-action-' + actionName + '-api-group'">
3116
+ <div class="action-group__header">
3117
+ <div>
3118
+ <strong>{{ tx('crud.authoring.action.apiGroup', 'API mapping') }}</strong>
3119
+ <p class="editor-section-note">{{ actionApiNote(actionName) }}</p>
3120
+ </div>
3121
+ <span class="action-summary-chip" [class.action-summary-chip--error]="actionGroupStatus(actionName, 'api') === 'invalid'" [class.action-summary-chip--warn]="actionGroupStatus(actionName, 'api') === 'pending'" [attr.data-testid]="'crud-editor-action-' + actionName + '-api-status'">
3122
+ {{ actionGroupStatusLabel(actionName, 'api') }}
3123
+ </span>
3124
+ </div>
3125
+ <div class="editor-grid">
3126
+ <mat-form-field appearance="outline">
3127
+ <mat-label>{{ tx('crud.authoring.action.apiEndpointKey', 'API endpoint key') }}</mat-label>
3128
+ <input matInput [attr.data-testid]="'crud-editor-action-' + actionName + '-apiEndpointKey'" [ngModel]="actionValue(actionName).form?.apiEndpointKey || ''" (ngModelChange)="setActionFormField(actionName, 'apiEndpointKey', $event)" [disabled]="isReadonly()" />
3129
+ </mat-form-field>
3130
+ <mat-form-field appearance="outline">
3131
+ <mat-label>{{ tx('crud.authoring.action.apiUrlEntry', 'API URL entry') }}</mat-label>
3132
+ <textarea matInput rows="3" [attr.data-testid]="'crud-editor-action-' + actionName + '-apiUrlEntry'" [ngModel]="actionApiUrlEntryDraft(actionName)" (ngModelChange)="setActionApiUrlEntry(actionName, $event)" [disabled]="isReadonly()"></textarea>
3133
+ </mat-form-field>
3134
+ </div>
3135
+ </section>
3136
+ <section class="action-group action-group--advanced" [attr.data-testid]="'crud-editor-action-' + actionName + '-inputs-group'">
3137
+ <div class="action-group__header">
3138
+ <div>
3139
+ <strong>{{ tx('crud.authoring.action.inputsGroup', 'Input seeding') }}</strong>
3140
+ <p class="editor-section-note">{{ actionInputsNote(actionName) }}</p>
3141
+ </div>
3142
+ </div>
3143
+ <section class="action-subgroup" [attr.data-testid]="'crud-editor-action-' + actionName + '-params-subgroup'">
3144
+ <div class="action-subgroup__header">
3145
+ <strong>{{ tx('crud.authoring.action.paramsSubgroup', 'Row-derived mappings') }}</strong>
3146
+ <p class="editor-section-note">{{ actionParamsNote(actionName) }}</p>
3147
+ </div>
3148
+ <div class="action-param-list">
3149
+ @for (param of actionParams(actionName); track $index) {
3150
+ <div class="action-param-row" [attr.data-testid]="'crud-editor-action-' + actionName + '-param-' + $index">
3151
+ <div class="editor-grid editor-grid--compact">
3152
+ <mat-form-field appearance="outline">
3153
+ <mat-label>{{ tx('crud.authoring.action.paramFrom', 'From field') }}</mat-label>
3154
+ <input matInput [attr.data-testid]="'crud-editor-action-' + actionName + '-param-' + $index + '-from'" [ngModel]="param.from" (ngModelChange)="setActionParamField(actionName, $index, 'from', $event)" [disabled]="isReadonly()" />
3155
+ </mat-form-field>
3156
+ <mat-form-field appearance="outline">
3157
+ <mat-label>{{ tx('crud.authoring.action.paramTo', 'Target') }}</mat-label>
3158
+ <mat-select [attr.data-testid]="'crud-editor-action-' + actionName + '-param-' + $index + '-to'" [ngModel]="param.to" (ngModelChange)="setActionParamField(actionName, $index, 'to', $event)" [disabled]="isReadonly()">
3159
+ @for (target of paramTargets; track target) {
3160
+ <mat-option [value]="target">{{ paramTargetLabel(target) }}</mat-option>
3161
+ }
3162
+ </mat-select>
3163
+ </mat-form-field>
3164
+ <mat-form-field appearance="outline">
3165
+ <mat-label>{{ tx('crud.authoring.action.paramName', 'Destination name') }}</mat-label>
3166
+ <input matInput [attr.data-testid]="'crud-editor-action-' + actionName + '-param-' + $index + '-name'" [ngModel]="param.name" (ngModelChange)="setActionParamField(actionName, $index, 'name', $event)" [disabled]="isReadonly()" />
3167
+ </mat-form-field>
3168
+ </div>
3169
+ <button mat-stroked-button type="button" [attr.data-testid]="'crud-editor-action-' + actionName + '-param-' + $index + '-remove'" (click)="removeActionParam(actionName, $index)" [disabled]="isReadonly()">
3170
+ {{ tx('crud.authoring.action.paramRemove', 'Remove mapping') }}
3171
+ </button>
3172
+ </div>
3173
+ }
3174
+ <button mat-stroked-button type="button" [attr.data-testid]="'crud-editor-action-' + actionName + '-param-add'" (click)="addActionParam(actionName)" [disabled]="isReadonly()">
3175
+ {{ tx('crud.authoring.action.paramAdd', 'Add mapping') }}
3176
+ </button>
3177
+ </div>
3178
+ </section>
3179
+ <section class="action-subgroup" [attr.data-testid]="'crud-editor-action-' + actionName + '-initialValue-subgroup'">
3180
+ <div class="action-subgroup__header">
3181
+ <strong>{{ tx('crud.authoring.action.initialValueSubgroup', 'Fixed form seed') }}</strong>
3182
+ <p class="editor-section-note">{{ actionInitialValueNote(actionName) }}</p>
3183
+ </div>
3184
+ <mat-form-field appearance="outline">
3185
+ <mat-label>{{ tx('crud.authoring.action.initialValue', 'Initial value seed (JSON)') }}</mat-label>
3186
+ <textarea matInput rows="5" [attr.data-testid]="'crud-editor-action-' + actionName + '-initialValue'" [ngModel]="actionInitialValueDraft(actionName)" (ngModelChange)="setActionInitialValue(actionName, $event)" [disabled]="isReadonly()"></textarea>
3187
+ </mat-form-field>
3188
+ </section>
3189
+ </section>
3190
+ <section class="action-group action-group--advanced" [attr.data-testid]="'crud-editor-action-' + actionName + '-back-group'">
3191
+ <div class="action-group__header">
3192
+ <div>
3193
+ <strong>{{ tx('crud.authoring.action.backGroup', 'Back behavior override') }}</strong>
3194
+ <p class="editor-section-note">{{ actionBackNote(actionName) }}</p>
3195
+ </div>
3196
+ </div>
3197
+ <div class="editor-grid">
3198
+ <mat-form-field appearance="outline">
3199
+ <mat-label>{{ tx('crud.authoring.action.backStrategy', 'Back strategy override') }}</mat-label>
3200
+ <mat-select [attr.data-testid]="'crud-editor-action-' + actionName + '-back-strategy'" [ngModel]="actionBackStrategy(actionName)" (ngModelChange)="setActionBackField(actionName, 'strategy', $event)" [disabled]="isReadonly()">
3201
+ <mat-option value="">{{ tx('crud.authoring.action.backUseDefaults', 'Use defaults') }}</mat-option>
3202
+ @for (strategy of backStrategies; track strategy) {
3203
+ <mat-option [value]="strategy">{{ backStrategyLabel(strategy) }}</mat-option>
3204
+ }
3205
+ </mat-select>
3206
+ </mat-form-field>
3207
+ <mat-form-field appearance="outline">
3208
+ <mat-label>{{ tx('crud.authoring.action.backReturnTo', 'Action return route') }}</mat-label>
3209
+ <input matInput [attr.data-testid]="'crud-editor-action-' + actionName + '-back-returnTo'" [ngModel]="actionBackReturnTo(actionName)" (ngModelChange)="setActionBackField(actionName, 'returnTo', $event)" [disabled]="isReadonly()" />
3210
+ </mat-form-field>
3211
+ </div>
3212
+ <div class="editor-toggles">
3213
+ <mat-slide-toggle [attr.data-testid]="'crud-editor-action-' + actionName + '-back-confirmOnDirty'" [ngModel]="actionBackConfirmOnDirty(actionName)" (ngModelChange)="setActionBackField(actionName, 'confirmOnDirty', $event)" [disabled]="isReadonly()">
3214
+ {{ tx('crud.authoring.action.backConfirmOnDirty', 'Confirm when leaving dirty forms') }}
3215
+ </mat-slide-toggle>
3216
+ </div>
3217
+ </section>
3218
+ </mat-expansion-panel>
3219
+ </mat-expansion-panel>
3220
+ }
3221
+ </div>
3222
+ </section>
3223
+
3224
+ <mat-card class="editor-card">
3225
+ <div class="editor-section-header">
3226
+ <div>
3227
+ <h3>{{ tx('crud.authoring.section.table', 'Table') }}</h3>
3228
+ <p class="editor-section-note">{{ sectionSummary('table') }}</p>
3229
+ </div>
3230
+ <span class="action-summary-chip" [class.action-summary-chip--error]="sectionStatus('table') === 'invalid'" [class.action-summary-chip--warn]="sectionStatus('table') === 'pending'">
3231
+ {{ sectionStatusLabel('table') }}
3232
+ </span>
3233
+ </div>
3234
+ <section class="action-summary-strip" data-testid="crud-editor-table-summary-strip">
3235
+ <span class="action-summary-chip" data-testid="crud-editor-table-summary-structure">{{ tableStructureSummary() }}</span>
3236
+ <span class="action-summary-chip" data-testid="crud-editor-table-summary-behavior">{{ tableBehaviorSummary() }}</span>
3237
+ <span class="action-summary-chip" data-testid="crud-editor-table-summary-states">{{ tableStatesSummary() }}</span>
3238
+ </section>
3239
+ <p class="editor-section-note" data-testid="crud-editor-table-flow-note">{{ tableEditingFlowSummary() }}</p>
3240
+ <praxis-table-inline-authoring-editor
3241
+ data-testid="crud-editor-table-inline"
3242
+ [config]="tableConfig()"
3243
+ [readonly]="isReadonly()"
3244
+ (configChange)="setTableConfig($event)"
3245
+ />
3246
+ </mat-card>
3247
+
3248
+ <mat-card class="editor-card">
3249
+ <h3>{{ tx('crud.authoring.section.json', 'JSON') }}</h3>
3250
+ <p class="editor-section-note" data-testid="crud-editor-json-summary">{{ jsonSectionSummary() }}</p>
3251
+ @if (diagnostics().length) {
3252
+ <section class="diagnostics-groups" data-testid="crud-editor-diagnostics">
3253
+ @for (group of groupedDiagnostics(); track group.section) {
3254
+ <article class="diagnostics-group" [attr.data-testid]="'crud-editor-diagnostics-' + group.section">
3255
+ <div class="editor-section-header">
3256
+ <div>
3257
+ <strong>{{ sectionDisplayLabel(group.section) }}</strong>
3258
+ <p class="editor-section-note">{{ diagnosticsGroupSummary(group.section, group.issues) }}</p>
3259
+ </div>
3260
+ <span class="action-summary-chip" [class.action-summary-chip--error]="group.hasError" [class.action-summary-chip--warn]="!group.hasError">
3261
+ {{ diagnosticsGroupStatusLabel(group.section, group.issues) }}
3262
+ </span>
3263
+ </div>
3264
+ <ul class="diagnostics">
3265
+ @for (issue of group.issues; track issue.code + issue.path) {
3266
+ <li [class.error]="issue.level === 'error'">{{ issue.level }}: {{ diagnosticMessage(issue.code, issue.message) }}</li>
3267
+ }
3268
+ </ul>
3269
+ </article>
3270
+ }
3271
+ </section>
3272
+ }
3273
+ <pre data-testid="crud-editor-json">{{ serializedDocument() }}</pre>
3274
+ </mat-card>
3275
+ </section>
3276
+ `, styles: [":host{display:block;min-width:0;color:var(--md-sys-color-on-surface,#1a1b20)}.editor-shell{display:grid;gap:16px}.editor-header{display:flex;justify-content:space-between;gap:16px;align-items:start}.editor-overview,.editor-health-map{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}.editor-health-card{display:grid;gap:4px;padding:14px;border-radius:18px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}.editor-health-card--warn{background:color-mix(in srgb,var(--md-sys-color-tertiary-container,#f8e08e) 78%,transparent)}.editor-health-card--error{background:color-mix(in srgb,var(--md-sys-color-error-container,#f9dedc) 82%,transparent);color:var(--md-sys-color-on-error-container,#410e0b)}.editor-focus{display:flex;gap:12px;align-items:start;padding:14px 16px;border-radius:18px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}.editor-focus--success{background:color-mix(in srgb,var(--md-sys-color-secondary-container,#d7e3ff) 70%,transparent)}.editor-focus-chip{padding:6px 10px;border-radius:999px;font-size:12px;background:color-mix(in srgb,var(--md-sys-color-secondary-container,#d7e3ff) 70%,transparent);white-space:nowrap}.editor-focus-chip--warn{background:color-mix(in srgb,var(--md-sys-color-tertiary-container,#f8e08e) 78%,transparent)}.editor-focus-chip--error{background:color-mix(in srgb,var(--md-sys-color-error-container,#f9dedc) 82%,transparent);color:var(--md-sys-color-on-error-container,#410e0b)}.editor-focus-copy{display:grid;gap:4px}.editor-focus-copy p{margin:0;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.editor-overview-card{display:grid;gap:4px;padding:14px;border-radius:18px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}.editor-overview-card--warn{background:color-mix(in srgb,var(--md-sys-color-tertiary-container,#f8e08e) 78%,transparent)}.editor-overview-card--error{background:color-mix(in srgb,var(--md-sys-color-error-container,#f9dedc) 82%,transparent);color:var(--md-sys-color-on-error-container,#410e0b)}.editor-overview-label{font-size:12px;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.editor-overview-value{line-height:1.3;overflow-wrap:anywhere}.editor-overview-note{margin:0;font-size:12px;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.editor-header h2,.editor-card h3{margin:0}.editor-header p{margin:6px 0 0;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.editor-chip{padding:6px 10px;border-radius:999px;background:color-mix(in srgb,var(--md-sys-color-primary,#1263b4) 12%,transparent);font-size:12px}.editor-card{display:grid;gap:14px;padding:16px;border-radius:20px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline,#c5c7ce) 70%,transparent)}.editor-section-header{display:flex;justify-content:space-between;gap:12px;align-items:start}.editor-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-grid--compact{grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}.editor-toggles{display:grid;gap:10px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-actions,.action-grid{display:grid;gap:14px}.action-summary-strip{display:flex;flex-wrap:wrap;gap:8px}.action-summary-chip{padding:6px 10px;border-radius:999px;font-size:12px;background:color-mix(in srgb,var(--md-sys-color-secondary-container,#d7e3ff) 70%,transparent)}.action-summary-chip--warn{background:color-mix(in srgb,var(--md-sys-color-tertiary-container,#f8e08e) 78%,transparent)}.action-summary-chip--error{background:color-mix(in srgb,var(--md-sys-color-error-container,#f9dedc) 82%,transparent);color:var(--md-sys-color-on-error-container,#410e0b)}.action-group{display:grid;gap:10px;padding:12px;border-radius:16px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 88%,transparent)}.action-group--advanced{background:color-mix(in srgb,var(--md-sys-color-surface-container,#f5f7fb) 90%,transparent)}.action-group__header{display:grid;gap:4px}.action-subgroup{display:grid;gap:10px;padding:10px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-container-low,#fafbfd) 92%,transparent)}.action-subgroup__header{display:grid;gap:4px}.action-param-list{display:grid;gap:12px}.action-param-row{display:grid;gap:8px}.action-advanced-panel{border:1px solid color-mix(in srgb,var(--md-sys-color-outline,#c5c7ce) 65%,transparent);border-radius:16px}.editor-section-note{margin:0;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.diagnostics-groups{display:grid;gap:12px}.diagnostics-group{display:grid;gap:10px;padding:12px;border-radius:16px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}.diagnostics{display:grid;gap:8px;margin:0;padding-left:18px}.diagnostics .error{color:var(--md-sys-color-error,#b3261e)}pre{margin:0;overflow:auto;max-height:320px;padding:12px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}\n"] }]
3277
+ }], ctorParameters: () => [], propDecorators: { documentInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "document", required: false }] }], metadataInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "metadata", required: false }] }], crudIdInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "crudId", required: false }] }], readonlyInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }] } });
3278
+ function cloneTableConfig(config) {
3279
+ return JSON.parse(JSON.stringify(config || { columns: [] }));
3280
+ }
3281
+
3282
+ function openCrudMetadataEditor(settings, opts) {
3283
+ return settings.open({
3284
+ id: opts?.id ?? `crud-authoring:${opts?.crudId || 'default'}`,
3285
+ title: opts?.title ?? 'CRUD settings',
3286
+ titleIcon: opts?.titleIcon ?? 'tune',
3287
+ content: {
3288
+ component: CrudMetadataEditorComponent,
3289
+ inputs: {
3290
+ document: opts?.document ?? null,
3291
+ metadata: opts?.metadata ?? null,
3292
+ crudId: opts?.crudId ?? null,
3293
+ readonly: opts?.readonly ?? false,
3294
+ },
3295
+ },
3296
+ });
3297
+ }
3298
+
355
3299
  class PraxisCrudComponent {
356
3300
  metadata;
357
3301
  crudId;
@@ -365,6 +3309,8 @@ class PraxisCrudComponent {
365
3309
  afterDelete = new EventEmitter();
366
3310
  error = new EventEmitter();
367
3311
  tableRuntimeConfigChange = new EventEmitter();
3312
+ crudAuthoringDocumentApplied = new EventEmitter();
3313
+ crudAuthoringDocumentSaved = new EventEmitter();
368
3314
  resolvedMetadata;
369
3315
  effectiveTableConfig;
370
3316
  tableConfigForBinding = createDefaultTableConfig();
@@ -372,11 +3318,14 @@ class PraxisCrudComponent {
372
3318
  tableFilterCriteria = {};
373
3319
  tableCrudContext;
374
3320
  launcher = inject(CrudLauncherService);
3321
+ http = inject(HttpClient);
375
3322
  destroyRef = inject(DestroyRef);
376
3323
  cdr = inject(ChangeDetectorRef);
377
3324
  table;
378
3325
  storage = inject(ASYNC_CONFIG_STORAGE);
3326
+ settingsPanel = inject(SettingsPanelService);
379
3327
  snack = inject(MatSnackBar);
3328
+ dialog = inject(DialogService);
380
3329
  i18n = inject(PraxisI18nService);
381
3330
  resourceDiscovery = inject(ResourceDiscoveryService);
382
3331
  actionOpenAdapter = inject(ResourceActionOpenAdapterService);
@@ -409,6 +3358,7 @@ class PraxisCrudComponent {
409
3358
  collectionCapabilitiesRequestHref = null;
410
3359
  collectionCapabilitiesResolvedHref = null;
411
3360
  collectionCapabilitiesRequestSeq = 0;
3361
+ currentAuthoringDocument;
412
3362
  onResetPreferences() {
413
3363
  try {
414
3364
  const keyId = this.componentKeyId();
@@ -423,7 +3373,7 @@ class PraxisCrudComponent {
423
3373
  catch { }
424
3374
  }
425
3375
  ngOnChanges(changes) {
426
- if (!changes['metadata']) {
3376
+ if (!changes['metadata'] && !changes['context']) {
427
3377
  return;
428
3378
  }
429
3379
  try {
@@ -431,6 +3381,15 @@ class PraxisCrudComponent {
431
3381
  ? JSON.parse(this.metadata)
432
3382
  : this.metadata;
433
3383
  this.resolvedMetadata = parsed;
3384
+ const externalAuthoringSeed = this.context && typeof this.context === 'object'
3385
+ ? (this.context['authoringDocument'] ?? this.context['authoringMetadata'])
3386
+ : undefined;
3387
+ this.currentAuthoringDocument = externalAuthoringSeed
3388
+ ? parseLegacyOrCrudDocument(externalAuthoringSeed)
3389
+ : createCrudAuthoringDocument({
3390
+ metadata: this.resolvedMetadata,
3391
+ });
3392
+ this.resolvedMetadata = this.currentAuthoringDocument.metadata;
434
3393
  assertCrudMetadata(this.resolvedMetadata, {
435
3394
  allowDeferredActionBindings: true,
436
3395
  });
@@ -474,6 +3433,10 @@ class PraxisCrudComponent {
474
3433
  }
475
3434
  }
476
3435
  const effectiveAction = (actionMeta || { action });
3436
+ const handledByDelete = await this.tryHandleCanonicalDeleteAction(effectiveAction, row);
3437
+ if (handledByDelete) {
3438
+ return;
3439
+ }
477
3440
  let drawerCloseEmitted = false;
478
3441
  const emitDrawerClose = () => {
479
3442
  if (drawerCloseEmitted)
@@ -498,6 +3461,11 @@ class PraxisCrudComponent {
498
3461
  this.refreshTable();
499
3462
  }
500
3463
  },
3464
+ }, {
3465
+ capabilities: effectiveAction.action === 'create'
3466
+ ? this.collectionCapabilities
3467
+ : this.collectionCapabilities,
3468
+ links: this.resolveCrudRuntimeLinks(effectiveAction.action, row),
501
3469
  });
502
3470
  this.afterOpen.emit({ mode, action: effectiveAction.action });
503
3471
  if (!ref) {
@@ -624,6 +3592,53 @@ class PraxisCrudComponent {
624
3592
  onConfigureRequested() {
625
3593
  this.configureRequested.emit();
626
3594
  }
3595
+ async tryHandleCanonicalDeleteAction(action, row) {
3596
+ const normalizedAction = String(action.action || '').trim().toLowerCase();
3597
+ if (normalizedAction !== 'delete') {
3598
+ return false;
3599
+ }
3600
+ const effectiveOpenMode = action.openMode ?? this.resolvedMetadata.defaults?.openMode ?? 'route';
3601
+ if (effectiveOpenMode === 'route' && action.route) {
3602
+ return false;
3603
+ }
3604
+ const contract = this.launcher.resolveRuntimeContract(action, row, this.resolvedMetadata, {
3605
+ capabilities: this.collectionCapabilities,
3606
+ links: this.resolveCrudRuntimeLinks(action.action, row),
3607
+ });
3608
+ const submitUrl = String(contract.submitUrl || '').trim();
3609
+ const submitMethod = String(contract.submitMethod || 'delete').trim().toUpperCase();
3610
+ if (!submitUrl) {
3611
+ throw new Error('Delete action could not resolve submitUrl.');
3612
+ }
3613
+ if (submitMethod !== 'DELETE') {
3614
+ throw new Error(`Delete action resolved unsupported method "${submitMethod}".`);
3615
+ }
3616
+ const ref = this.dialog.open(ConfirmDialogComponent, {
3617
+ autoFocus: false,
3618
+ restoreFocus: true,
3619
+ data: {
3620
+ title: action.label || this.getCrudActionLabel('delete'),
3621
+ message: this.resolvedMetadata.i18n?.crudDialog?.['deleteConfirmMessage'] ||
3622
+ 'Esta ação não pode ser desfeita. Deseja continuar?',
3623
+ confirmText: this.resolvedMetadata.i18n?.crudDialog?.['deleteConfirmButton'] ||
3624
+ this.getCrudActionLabel('delete'),
3625
+ cancelText: this.resolvedMetadata.i18n?.crudDialog?.['deleteCancelButton'] || this.tx('crud.delete.cancel', 'Cancel'),
3626
+ type: 'warning',
3627
+ },
3628
+ });
3629
+ this.afterOpen.emit({ mode: 'modal', action: action.action });
3630
+ const confirmed = await firstValueFrom(ref.afterClosed());
3631
+ this.afterClose.emit();
3632
+ if (!confirmed) {
3633
+ return true;
3634
+ }
3635
+ await firstValueFrom(this.http.request('DELETE', submitUrl));
3636
+ const idField = this.getIdField();
3637
+ const id = row?.[idField];
3638
+ this.afterDelete.emit({ id });
3639
+ this.refreshTable();
3640
+ return true;
3641
+ }
627
3642
  refreshTable() {
628
3643
  this.table.refetch();
629
3644
  }
@@ -1122,6 +4137,49 @@ class PraxisCrudComponent {
1122
4137
  openMode: action.openMode,
1123
4138
  })),
1124
4139
  idField: meta.resource?.idField || 'id',
4140
+ openAuthoring: this.openCrudAuthoringFromTable,
4141
+ };
4142
+ }
4143
+ openCrudAuthoringFromTable = () => {
4144
+ const seed = this.currentAuthoringDocument
4145
+ || createCrudAuthoringDocument({ metadata: this.resolvedMetadata });
4146
+ const ref = openCrudMetadataEditor(this.settingsPanel, {
4147
+ crudId: this.crudId,
4148
+ document: seed,
4149
+ title: this.tx('crud.authoring.title', 'Configurações do CRUD'),
4150
+ titleIcon: 'table_chart',
4151
+ });
4152
+ ref.applied$
4153
+ .pipe(takeUntilDestroyed(this.destroyRef))
4154
+ .subscribe((payload) => this.applyCrudAuthoringPayload(payload, 'applied'));
4155
+ ref.saved$
4156
+ .pipe(takeUntilDestroyed(this.destroyRef))
4157
+ .subscribe((payload) => this.applyCrudAuthoringPayload(payload, 'saved'));
4158
+ };
4159
+ applyCrudAuthoringPayload(payload, eventName) {
4160
+ const next = parseLegacyOrCrudDocument(payload);
4161
+ this.currentAuthoringDocument = next;
4162
+ this.resolvedMetadata = next.metadata;
4163
+ this.tableQueryContext = this.resolveQueryContext(this.resolvedMetadata);
4164
+ this.tableFilterCriteria = this.resolveFilterCriteria(this.resolvedMetadata);
4165
+ this.applyResolvedCrudState(this.resolvedMetadata);
4166
+ this.cdr.markForCheck();
4167
+ const widgetPersistencePayload = this.buildWidgetPersistencePayload(next);
4168
+ if (eventName === 'applied') {
4169
+ this.crudAuthoringDocumentApplied.emit(widgetPersistencePayload);
4170
+ }
4171
+ if (eventName === 'saved') {
4172
+ this.crudAuthoringDocumentSaved.emit(widgetPersistencePayload);
4173
+ }
4174
+ }
4175
+ buildWidgetPersistencePayload(document) {
4176
+ return {
4177
+ document,
4178
+ metadata: document.metadata,
4179
+ inputPatch: {
4180
+ metadata: document.metadata,
4181
+ 'context.authoringDocument': document,
4182
+ },
1125
4183
  };
1126
4184
  }
1127
4185
  resolveCreateToolbarAction(meta, capabilities) {
@@ -1222,19 +4280,20 @@ class PraxisCrudComponent {
1222
4280
  }));
1223
4281
  }
1224
4282
  supportsCreateCapability(snapshot) {
1225
- return (this.hasCanonicalOperation(snapshot, 'create') ||
4283
+ return (snapshot.operations?.create?.supported === true ||
4284
+ this.hasCanonicalOperation(snapshot, 'create') ||
1226
4285
  snapshot.surfaces.some((surface) => surface.scope === 'COLLECTION' &&
1227
4286
  this.isWritableCrudSurface(surface) &&
1228
4287
  surface.availability?.allowed !== false));
1229
4288
  }
1230
4289
  supportsViewCapability(snapshot) {
1231
- return this.hasCanonicalOperation(snapshot, 'byId');
4290
+ return snapshot.operations?.view?.supported === true || this.hasCanonicalOperation(snapshot, 'byId');
1232
4291
  }
1233
4292
  supportsEditCapability(snapshot) {
1234
- return this.hasCanonicalOperation(snapshot, 'update');
4293
+ return snapshot.operations?.edit?.supported === true || this.hasCanonicalOperation(snapshot, 'update');
1235
4294
  }
1236
4295
  supportsDeleteCapability(snapshot) {
1237
- return this.hasCanonicalOperation(snapshot, 'delete');
4296
+ return snapshot.operations?.delete?.supported === true || this.hasCanonicalOperation(snapshot, 'delete');
1238
4297
  }
1239
4298
  hasCanonicalOperation(snapshot, operation) {
1240
4299
  return snapshot.canonicalOperations?.[operation] === true;
@@ -1267,6 +4326,13 @@ class PraxisCrudComponent {
1267
4326
  return null;
1268
4327
  }
1269
4328
  }
4329
+ resolveCrudRuntimeLinks(action, row) {
4330
+ const normalizedAction = String(action || '').trim().toLowerCase();
4331
+ if (normalizedAction === 'create') {
4332
+ return this.tableCollectionLinks;
4333
+ }
4334
+ return row?._links ?? null;
4335
+ }
1270
4336
  isRecord(value) {
1271
4337
  return !!value && typeof value === 'object' && !Array.isArray(value);
1272
4338
  }
@@ -1315,7 +4381,7 @@ class PraxisCrudComponent {
1315
4381
  }
1316
4382
  }
1317
4383
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisCrudComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1318
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", 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", tableRuntimeConfigChange: "tableRuntimeConfigChange" }, providers: [
4384
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", 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", tableRuntimeConfigChange: "tableRuntimeConfigChange", crudAuthoringDocumentApplied: "crudAuthoringDocumentApplied", crudAuthoringDocumentSaved: "crudAuthoringDocumentSaved" }, providers: [
1319
4385
  providePraxisI18nConfig(RESOURCE_DISCOVERY_I18N_CONFIG),
1320
4386
  providePraxisI18nConfig(PRAXIS_CRUD_RUNTIME_I18N_CONFIG),
1321
4387
  ], viewQueries: [{ propertyName: "table", first: true, predicate: PraxisTable, descendants: true }], usesOnChanges: true, ngImport: i0, template: `
@@ -1408,6 +4474,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1408
4474
  type: Output
1409
4475
  }], tableRuntimeConfigChange: [{
1410
4476
  type: Output
4477
+ }], crudAuthoringDocumentApplied: [{
4478
+ type: Output
4479
+ }], crudAuthoringDocumentSaved: [{
4480
+ type: Output
1411
4481
  }], table: [{
1412
4482
  type: ViewChild,
1413
4483
  args: [PraxisTable]
@@ -1429,6 +4499,7 @@ class DynamicFormDialogHostComponent {
1429
4499
  destroyRef = inject(DestroyRef);
1430
4500
  resourcePath;
1431
4501
  resourceId;
4502
+ initialValue;
1432
4503
  schemaUrl;
1433
4504
  submitUrl;
1434
4505
  submitMethod;
@@ -1478,6 +4549,7 @@ class DynamicFormDialogHostComponent {
1478
4549
  }
1479
4550
  this.idField = this.data.metadata?.resource?.idField ?? 'id';
1480
4551
  this.resourceId = this.data.inputs?.[this.idField];
4552
+ this.initialValue = this.extractInitialValue(this.data.inputs);
1481
4553
  this.schemaUrl = this.data.inputs?.['schemaUrl'] ?? null;
1482
4554
  this.submitUrl = this.data.inputs?.['submitUrl'] ?? null;
1483
4555
  this.submitMethod = this.data.inputs?.['submitMethod'] ?? null;
@@ -1520,6 +4592,30 @@ class DynamicFormDialogHostComponent {
1520
4592
  .pipe(takeUntilDestroyed(this.destroyRef))
1521
4593
  .subscribe(() => this.saveState());
1522
4594
  }
4595
+ extractInitialValue(inputs) {
4596
+ if (!inputs || typeof inputs !== 'object') {
4597
+ return null;
4598
+ }
4599
+ const reserved = new Set([
4600
+ this.idField,
4601
+ 'schemaUrl',
4602
+ 'submitUrl',
4603
+ 'submitMethod',
4604
+ 'apiEndpointKey',
4605
+ 'apiUrlEntry',
4606
+ 'initialValue',
4607
+ ]);
4608
+ const explicit = inputs['initialValue'] && typeof inputs['initialValue'] === 'object'
4609
+ ? { ...inputs['initialValue'] }
4610
+ : {};
4611
+ for (const [key, value] of Object.entries(inputs)) {
4612
+ if (reserved.has(key) || value === undefined) {
4613
+ continue;
4614
+ }
4615
+ explicit[key] = value;
4616
+ }
4617
+ return Object.keys(explicit).length ? explicit : null;
4618
+ }
1523
4619
  ngOnInit() {
1524
4620
  // Carregar estado salvo (se habilitado)
1525
4621
  if (this.rememberState && this.stateKey) {
@@ -1638,7 +4734,7 @@ class DynamicFormDialogHostComponent {
1638
4734
  this.dialogRef.updatePosition();
1639
4735
  }
1640
4736
  }
1641
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: DynamicFormDialogHostComponent, deps: [{ token: MatDialogRef }, { token: MAT_DIALOG_DATA }, { token: DialogService }, { token: i2.GenericCrudService }, { token: ASYNC_CONFIG_STORAGE }], target: i0.ɵɵFactoryTarget.Component });
4737
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", 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 });
1642
4738
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", 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: `
1643
4739
  <div mat-dialog-title class="dialog-header">
1644
4740
  <h2 id="crudDialogTitle" class="dialog-title">
@@ -1674,6 +4770,7 @@ class DynamicFormDialogHostComponent {
1674
4770
  [formId]="data.action?.formId"
1675
4771
  [resourcePath]="resourcePath"
1676
4772
  [resourceId]="resourceId"
4773
+ [initialValue]="initialValue"
1677
4774
  [mode]="mode"
1678
4775
  [schemaUrl]="schemaUrl"
1679
4776
  [submitUrl]="submitUrl"
@@ -1686,7 +4783,7 @@ class DynamicFormDialogHostComponent {
1686
4783
  (formCancel)="onCancel()"
1687
4784
  ></praxis-dynamic-form>
1688
4785
  </mat-dialog-content>
1689
- `, 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: CommonModule }, { 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: i4.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5.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", "editorialContext", "mode", "config", "schemaSource", "schemaUrl", "submitUrl", "submitMethod", "responseSchemaUrl", "apiEndpointKey", "apiUrlEntry", "enableCustomization", "formId", "componentInstanceId", "layout", "backConfig", "hooks", "removeEmptyContainersOnSave", "reactiveValidation", "reactiveValidationDebounceMs", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "readonlyModeGlobal", "disabledModeGlobal", "presentationModeGlobal", "visibleGlobal", "customEndpoints"], outputs: ["formSubmit", "formCancel", "formReset", "configChange", "formReady", "valueChange", "syncCompleted", "initializationError", "loadingStateChange", "enableCustomizationChange", "customAction", "actionConfirmation", "schemaStatusChange", "fieldRenderError", "widgetEvent"] }] });
4786
+ `, 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: CommonModule }, { 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: i5$1.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", "schemaSource", "schemaUrl", "submitUrl", "submitMethod", "responseSchemaUrl", "apiEndpointKey", "apiUrlEntry", "enableCustomization", "formId", "componentInstanceId", "layout", "backConfig", "hooks", "removeEmptyContainersOnSave", "reactiveValidation", "reactiveValidationDebounceMs", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "readonlyModeGlobal", "disabledModeGlobal", "presentationModeGlobal", "visibleGlobal", "customEndpoints"], outputs: ["formSubmit", "formCancel", "formReset", "configChange", "formReady", "valueChange", "syncCompleted", "initializationError", "loadingStateChange", "enableCustomizationChange", "customAction", "actionConfirmation", "schemaStatusChange", "fieldRenderError"] }] });
1690
4787
  }
1691
4788
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: DynamicFormDialogHostComponent, decorators: [{
1692
4789
  type: Component,
@@ -1735,6 +4832,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1735
4832
  [formId]="data.action?.formId"
1736
4833
  [resourcePath]="resourcePath"
1737
4834
  [resourceId]="resourceId"
4835
+ [initialValue]="initialValue"
1738
4836
  [mode]="mode"
1739
4837
  [schemaUrl]="schemaUrl"
1740
4838
  [submitUrl]="submitUrl"
@@ -1754,7 +4852,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1754
4852
  }] }, { type: undefined, decorators: [{
1755
4853
  type: Inject,
1756
4854
  args: [MAT_DIALOG_DATA]
1757
- }] }, { type: DialogService }, { type: i2.GenericCrudService }, { type: undefined, decorators: [{
4855
+ }] }, { type: DialogService }, { type: i2$1.GenericCrudService }, { type: undefined, decorators: [{
1758
4856
  type: Inject,
1759
4857
  args: [ASYNC_CONFIG_STORAGE]
1760
4858
  }] }], propDecorators: { formComp: [{
@@ -1784,61 +4882,80 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
1784
4882
  {
1785
4883
  name: 'crudId',
1786
4884
  type: 'string',
1787
- description: 'Identificador do CRUD (base para tabela/formulário e persistência)',
4885
+ description: 'Identificador do CRUD (base para tabela/formulário e persistência).',
1788
4886
  },
1789
4887
  {
1790
4888
  name: 'componentInstanceId',
1791
4889
  type: 'string',
1792
- description: 'Identificador opcional para múltiplas instâncias na mesma rota',
4890
+ description: 'Identificador opcional para múltiplas instâncias na mesma rota.',
1793
4891
  },
1794
4892
  {
1795
4893
  name: 'context',
1796
4894
  type: 'Record<string, unknown>',
1797
- description: 'Contexto opaco do host. A implementacao atual o expõe como Input, mas nao o consome no launcher nem no tableCrudContext.',
4895
+ description: 'Contexto opaco do host. A implementação atual o expõe como Input e o usa para seeds editoriais do authoring.',
1798
4896
  },
1799
4897
  {
1800
4898
  name: 'metadata.queryContext',
1801
4899
  type: 'PraxisDataQueryContext | null',
1802
- description: 'Contexto semantico de consulta encaminhado para a tabela interna do CRUD. Preferir este contrato para novo authoring remoto.',
4900
+ description: 'Contexto semântico de consulta encaminhado para a tabela interna do CRUD. Preferir este contrato para novo authoring remoto.',
1803
4901
  },
1804
4902
  {
1805
4903
  name: 'metadata.filterCriteria',
1806
4904
  type: 'Record<string, unknown> | null',
1807
4905
  description: 'Bridge declarativa de filtros encaminhada para a tabela interna. Para novo authoring, prefira metadata.queryContext.',
1808
4906
  },
4907
+ {
4908
+ name: 'metadata.actions[].form',
4909
+ type: 'CrudActionFormContract',
4910
+ description: 'Contrato canônico explícito para modal/drawer quando o host precisa informar schemaUrl, submitUrl e submitMethod sem depender do fallback genérico por resource.path.',
4911
+ },
1809
4912
  {
1810
4913
  name: 'enableCustomization',
1811
4914
  type: 'boolean',
1812
4915
  default: false,
1813
- description: 'Habilita modo de customização do layout',
4916
+ description: 'Habilita modo de customização do layout.',
1814
4917
  },
1815
4918
  ],
1816
4919
  outputs: [
1817
4920
  {
1818
4921
  name: 'afterOpen',
1819
4922
  type: '{ mode: FormOpenMode; action: string }',
1820
- description: 'Emitido após abrir diálogo',
4923
+ description: 'Emitido após abrir diálogo.',
1821
4924
  },
1822
4925
  {
1823
4926
  name: 'afterClose',
1824
4927
  type: 'void',
1825
- description: 'Emitido após fechar diálogo',
4928
+ description: 'Emitido após fechar diálogo.',
1826
4929
  },
1827
4930
  {
1828
4931
  name: 'afterSave',
1829
4932
  type: '{ id: string | number; data: unknown }',
1830
- description: 'Emitido ao salvar',
4933
+ description: 'Emitido ao salvar.',
1831
4934
  },
1832
4935
  {
1833
4936
  name: 'afterDelete',
1834
4937
  type: '{ id: string | number }',
1835
- description: 'Emitido ao deletar',
4938
+ description: 'Emitido ao deletar.',
4939
+ },
4940
+ {
4941
+ name: 'error',
4942
+ type: 'unknown',
4943
+ description: 'Emitido em erros.',
1836
4944
  },
1837
- { name: 'error', type: 'unknown', description: 'Emitido em erros' },
1838
4945
  {
1839
4946
  name: 'configureRequested',
1840
4947
  type: 'void',
1841
- description: 'Emitido quando CTA de configuração é acionado em modo edição',
4948
+ description: 'Emitido quando o CTA de configuração é acionado em modo de edição.',
4949
+ },
4950
+ {
4951
+ name: 'crudAuthoringDocumentApplied',
4952
+ type: 'CrudAuthoringWidgetPersistenceEvent',
4953
+ description: 'Emitido quando o editor canônico do CRUD aplica um documento e publica o patch de inputs do widget para hosts como praxis-dynamic-page.',
4954
+ },
4955
+ {
4956
+ name: 'crudAuthoringDocumentSaved',
4957
+ type: 'CrudAuthoringWidgetPersistenceEvent',
4958
+ description: 'Emitido quando o editor canônico do CRUD salva um documento e publica o patch de inputs do widget para hosts como praxis-dynamic-page.',
1842
4959
  },
1843
4960
  ],
1844
4961
  actions: [
@@ -1846,13 +4963,13 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
1846
4963
  id: 'create',
1847
4964
  label: 'Criar',
1848
4965
  icon: 'add',
1849
- description: 'Emite evento ao abrir fluxo de criação',
4966
+ description: 'Emite evento ao abrir fluxo de criação.',
1850
4967
  emit: 'afterOpen',
1851
4968
  payloadSchema: {
1852
4969
  type: 'object',
1853
4970
  properties: {
1854
- action: { type: 'string', description: 'Ação executada' },
1855
- mode: { type: 'string', description: 'Modo de abertura' },
4971
+ action: { type: 'string', description: 'Ação executada.' },
4972
+ mode: { type: 'string', description: 'Modo de abertura.' },
1856
4973
  },
1857
4974
  required: ['action', 'mode'],
1858
4975
  example: { action: 'create', mode: 'route' },
@@ -1863,13 +4980,13 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
1863
4980
  id: 'view',
1864
4981
  label: 'Visualizar',
1865
4982
  icon: 'visibility',
1866
- description: 'Emite evento ao abrir fluxo de visualização',
4983
+ description: 'Emite evento ao abrir fluxo de visualização.',
1867
4984
  emit: 'afterOpen',
1868
4985
  payloadSchema: {
1869
4986
  type: 'object',
1870
4987
  properties: {
1871
- action: { type: 'string', description: 'Ação executada' },
1872
- mode: { type: 'string', description: 'Modo de abertura' },
4988
+ action: { type: 'string', description: 'Ação executada.' },
4989
+ mode: { type: 'string', description: 'Modo de abertura.' },
1873
4990
  },
1874
4991
  required: ['action', 'mode'],
1875
4992
  example: { action: 'view', mode: 'modal' },
@@ -1880,13 +4997,13 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
1880
4997
  id: 'edit',
1881
4998
  label: 'Editar',
1882
4999
  icon: 'edit',
1883
- description: 'Emite evento ao abrir fluxo de edição',
5000
+ description: 'Emite evento ao abrir fluxo de edição.',
1884
5001
  emit: 'afterOpen',
1885
5002
  payloadSchema: {
1886
5003
  type: 'object',
1887
5004
  properties: {
1888
- action: { type: 'string', description: 'Ação executada' },
1889
- mode: { type: 'string', description: 'Modo de abertura' },
5005
+ action: { type: 'string', description: 'Ação executada.' },
5006
+ mode: { type: 'string', description: 'Modo de abertura.' },
1890
5007
  },
1891
5008
  required: ['action', 'mode'],
1892
5009
  example: { action: 'edit', mode: 'drawer' },
@@ -1897,12 +5014,12 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
1897
5014
  id: 'delete',
1898
5015
  label: 'Excluir',
1899
5016
  icon: 'delete',
1900
- description: 'Emite evento após exclusão',
5017
+ description: 'Emite evento após exclusão.',
1901
5018
  emit: 'afterDelete',
1902
5019
  payloadSchema: {
1903
5020
  type: 'object',
1904
5021
  properties: {
1905
- id: { type: 'string | number', description: 'ID do registro (string ou number)' },
5022
+ id: { type: 'string | number', description: 'ID do registro (string ou number).' },
1906
5023
  },
1907
5024
  required: ['id'],
1908
5025
  example: { id: '123' },
@@ -1913,13 +5030,13 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
1913
5030
  id: 'save',
1914
5031
  label: 'Salvar',
1915
5032
  icon: 'save',
1916
- description: 'Emite evento após salvar',
5033
+ description: 'Emite evento após salvar.',
1917
5034
  emit: 'afterSave',
1918
5035
  payloadSchema: {
1919
5036
  type: 'object',
1920
5037
  properties: {
1921
- id: { type: 'string | number', description: 'ID do registro (string ou number)' },
1922
- data: { type: 'object', description: 'Dados salvos' },
5038
+ id: { type: 'string | number', description: 'ID do registro (string ou number).' },
5039
+ data: { type: 'object', description: 'Dados salvos.' },
1923
5040
  },
1924
5041
  required: ['id', 'data'],
1925
5042
  example: { id: '123', data: {} },
@@ -1930,7 +5047,7 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
1930
5047
  id: 'close',
1931
5048
  label: 'Fechar',
1932
5049
  icon: 'close',
1933
- description: 'Emite evento ao fechar o diálogo',
5050
+ description: 'Emite evento ao fechar o diálogo.',
1934
5051
  emit: 'afterClose',
1935
5052
  scope: 'shell',
1936
5053
  },
@@ -1938,7 +5055,7 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
1938
5055
  id: 'configure',
1939
5056
  label: 'Configurar',
1940
5057
  icon: 'tune',
1941
- description: 'Emite evento ao abrir configuração',
5058
+ description: 'Emite evento ao abrir configuração.',
1942
5059
  emit: 'configureRequested',
1943
5060
  scope: 'shell',
1944
5061
  },
@@ -1993,7 +5110,7 @@ class CrudPageHeaderComponent {
1993
5110
  <ng-content></ng-content>
1994
5111
  </div>
1995
5112
  </header>
1996
- `, isInline: true, styles: [".crud-header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 0;background:var(--md-sys-color-surface)}.crud-header.sticky{position:sticky;top:0;z-index:10;-webkit-backdrop-filter:saturate(110%);backdrop-filter:saturate(110%)}.crud-header.with-divider{border-bottom:1px solid var(--md-sys-color-outline-variant)}.left{display:flex;align-items:center;gap:8px;min-width:0}.right{display:flex;align-items:center;gap:8px}.title{margin:0;font-weight:600;color:var(--md-sys-color-on-surface, currentColor);font:var(--mdc-typography-title-large, 600 20px/28px system-ui);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.back-btn{display:inline-flex;align-items:center;gap:6px;text-decoration:none}.back-btn .mat-mdc-button{padding-left:0}.back-btn mat-icon{color:var(--md-sys-color-on-surface-variant, currentColor)}.back-btn .label{color:var(--md-sys-color-on-surface-variant, currentColor)}.back-btn.ghost{border-radius:20px;padding:4px 8px}.back-btn.ghost:hover{background:var(--md-sys-color-primary-container)}.back-btn.tonal{border-radius:20px;padding:4px 10px;background:var(--md-sys-color-surface-container);box-shadow:inset 0 0 0 1px var(--md-sys-color-outline-variant)}.back-btn.tonal:hover{background:var(--md-sys-color-primary-container)}.back-btn.outlined{border-radius:20px;padding:4px 10px;border:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface)}.back-btn.outlined:hover{border-color:var(--md-sys-color-primary);background:var(--md-sys-color-primary-container)}@media(max-width:599px){.label.hide-on-narrow{display:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }] });
5113
+ `, isInline: true, styles: [".crud-header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 0;background:var(--md-sys-color-surface)}.crud-header.sticky{position:sticky;top:0;z-index:10;-webkit-backdrop-filter:saturate(110%);backdrop-filter:saturate(110%)}.crud-header.with-divider{border-bottom:1px solid var(--md-sys-color-outline-variant)}.left{display:flex;align-items:center;gap:8px;min-width:0}.right{display:flex;align-items:center;gap:8px}.title{margin:0;font-weight:600;color:var(--md-sys-color-on-surface, currentColor);font:var(--mdc-typography-title-large, 600 20px/28px system-ui);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.back-btn{display:inline-flex;align-items:center;gap:6px;text-decoration:none}.back-btn .mat-mdc-button{padding-left:0}.back-btn mat-icon{color:var(--md-sys-color-on-surface-variant, currentColor)}.back-btn .label{color:var(--md-sys-color-on-surface-variant, currentColor)}.back-btn.ghost{border-radius:20px;padding:4px 8px}.back-btn.ghost:hover{background:var(--md-sys-color-primary-container)}.back-btn.tonal{border-radius:20px;padding:4px 10px;background:var(--md-sys-color-surface-container);box-shadow:inset 0 0 0 1px var(--md-sys-color-outline-variant)}.back-btn.tonal:hover{background:var(--md-sys-color-primary-container)}.back-btn.outlined{border-radius:20px;padding:4px 10px;border:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface)}.back-btn.outlined:hover{border-color:var(--md-sys-color-primary);background:var(--md-sys-color-primary-container)}@media(max-width:599px){.label.hide-on-narrow{display:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }] });
1997
5114
  }
1998
5115
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: CrudPageHeaderComponent, decorators: [{
1999
5116
  type: Component,
@@ -2120,6 +5237,7 @@ const CRUD_AI_CAPABILITIES = {
2120
5237
  { path: 'actions[].params[].from', category: 'actions', valueKind: 'string', description: 'Origem do parametro.' },
2121
5238
  { path: 'actions[].params[].to', category: 'actions', valueKind: 'enum', allowedValues: ENUMS.paramTarget, description: 'Destino do parametro.' },
2122
5239
  { path: 'actions[].params[].name', category: 'actions', valueKind: 'string', description: 'Nome do parametro no destino.' },
5240
+ { path: 'actions[].form.initialValue', category: 'actions', valueKind: 'object', description: 'Seed fixo do formulario injetado em inputs.initialValue antes da abertura.' },
2123
5241
  { path: 'actions[].back', category: 'navigation', valueKind: 'object', description: 'BackConfig por acao.' },
2124
5242
  { path: 'actions[].back.strategy', category: 'navigation', valueKind: 'enum', allowedValues: ENUMS.backStrategy, description: 'Estrategia de retorno da acao (auto, close, navigate).' },
2125
5243
  { path: 'actions[].back.returnTo', category: 'navigation', valueKind: 'string', description: 'Rota ou destino usado quando a estrategia de retorno for navigate.' },
@@ -2138,4 +5256,4 @@ const CRUD_AI_CAPABILITIES = {
2138
5256
  * Generated bundle index. Do not edit.
2139
5257
  */
2140
5258
 
2141
- export { CRUD_AI_CAPABILITIES, CrudLauncherService, CrudPageHeaderComponent, DialogService, DynamicFormDialogHostComponent, PRAXIS_CRUD_COMPONENT_METADATA, PraxisCrudComponent, assertCrudMetadata, providePraxisCrudMetadata };
5259
+ export { CRUD_AI_CAPABILITIES, CrudLauncherService, CrudMetadataEditorComponent, CrudPageHeaderComponent, DialogService, DynamicFormDialogHostComponent, PRAXIS_CRUD_COMPONENT_METADATA, PraxisCrudComponent, assertCrudMetadata, createCrudAuthoringDocument, findCrudAction, normalizeCrudAuthoringDocument, openCrudMetadataEditor, parseLegacyOrCrudDocument, providePraxisCrudMetadata, serializeCrudAuthoringDocument, validateCrudAuthoringDocument };