@praxisui/crud 6.0.0-beta.0 → 8.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,30 +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
  });
211
- if (action.form?.schemaUrl) {
212
- inputs['schemaUrl'] = action.form.schemaUrl;
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;
213
241
  }
214
- if (action.form?.submitUrl) {
215
- inputs['submitUrl'] = action.form.submitUrl;
242
+ if (resolved.submitMethod) {
243
+ inputs['submitMethod'] = resolved.submitMethod;
216
244
  }
217
- if (action.form?.submitMethod) {
218
- inputs['submitMethod'] = action.form.submitMethod;
245
+ if (resolved.apiEndpointKey != null) {
246
+ inputs['apiEndpointKey'] = resolved.apiEndpointKey;
219
247
  }
220
- if (action.form?.apiEndpointKey != null) {
221
- inputs['apiEndpointKey'] = action.form.apiEndpointKey;
248
+ if (resolved.apiUrlEntry != null) {
249
+ inputs['apiUrlEntry'] = resolved.apiUrlEntry;
222
250
  }
223
- if (action.form?.apiUrlEntry != null) {
224
- inputs['apiUrlEntry'] = action.form.apiUrlEntry;
251
+ if (action.form?.initialValue != null) {
252
+ inputs['initialValue'] = action.form.initialValue;
225
253
  }
226
254
  return inputs;
227
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
+ }
228
338
  async mergeCrudOverrides(metadata, action, componentKeyId) {
229
339
  try {
230
340
  if (!componentKeyId)
@@ -280,6 +390,294 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
280
390
  args: [{ providedIn: 'root' }]
281
391
  }] });
282
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
+ }
283
681
  function assertCrudMetadata(meta, options = {}) {
284
682
  if (meta.component !== 'praxis-crud') {
285
683
  throw new Error('Invalid component type for CRUD metadata');
@@ -289,12 +687,14 @@ function assertCrudMetadata(meta, options = {}) {
289
687
  }
290
688
  meta.actions?.forEach((action) => {
291
689
  const mode = action.openMode ?? meta.defaults?.openMode ?? 'route';
690
+ const inferredCanonical = !isExplicitCrudAction(action) && isCanonicalCrudAction(action.action);
292
691
  if (!options.allowDeferredActionBindings && mode === 'route' && !action.route) {
293
692
  throw new Error(`Route not provided for action ${action.action}`);
294
693
  }
295
694
  if (!options.allowDeferredActionBindings &&
296
695
  (mode === 'modal' || mode === 'drawer') &&
297
- !action.formId) {
696
+ !action.formId &&
697
+ !inferredCanonical) {
298
698
  throw new Error(`formId not provided for action ${action.action}`);
299
699
  }
300
700
  action.params?.forEach((p) => {
@@ -323,6 +723,8 @@ const PRAXIS_CRUD_RUNTIME_I18N_CONFIG = {
323
723
  'crud.actions.view': 'Ver',
324
724
  'crud.actions.edit': 'Editar',
325
725
  'crud.actions.delete': 'Excluir',
726
+ 'crud.delete.confirmMessage': 'Esta ação não pode ser desfeita. Deseja continuar?',
727
+ 'crud.delete.cancel': 'Cancelar',
326
728
  },
327
729
  'en-US': {
328
730
  'crud.emptyState.title': 'Connect CRUD to a resource',
@@ -333,6 +735,8 @@ const PRAXIS_CRUD_RUNTIME_I18N_CONFIG = {
333
735
  'crud.actions.view': 'View',
334
736
  'crud.actions.edit': 'Edit',
335
737
  'crud.actions.delete': 'Delete',
738
+ 'crud.delete.confirmMessage': 'This action cannot be undone. Do you want to continue?',
739
+ 'crud.delete.cancel': 'Cancel',
336
740
  },
337
741
  },
338
742
  },
@@ -372,6 +776,2526 @@ function translateCrudRuntimeText(i18n, key, fallback, params, locale) {
372
776
  return i18n.t(key, params, runtimeFallback, PRAXIS_CRUD_RUNTIME_I18N_NAMESPACE);
373
777
  }
374
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
+
375
3299
  class PraxisCrudComponent {
376
3300
  metadata;
377
3301
  crudId;
@@ -385,6 +3309,8 @@ class PraxisCrudComponent {
385
3309
  afterDelete = new EventEmitter();
386
3310
  error = new EventEmitter();
387
3311
  tableRuntimeConfigChange = new EventEmitter();
3312
+ crudAuthoringDocumentApplied = new EventEmitter();
3313
+ crudAuthoringDocumentSaved = new EventEmitter();
388
3314
  resolvedMetadata;
389
3315
  effectiveTableConfig;
390
3316
  tableConfigForBinding = createDefaultTableConfig();
@@ -392,11 +3318,14 @@ class PraxisCrudComponent {
392
3318
  tableFilterCriteria = {};
393
3319
  tableCrudContext;
394
3320
  launcher = inject(CrudLauncherService);
3321
+ http = inject(HttpClient);
395
3322
  destroyRef = inject(DestroyRef);
396
3323
  cdr = inject(ChangeDetectorRef);
397
3324
  table;
398
3325
  storage = inject(ASYNC_CONFIG_STORAGE);
3326
+ settingsPanel = inject(SettingsPanelService);
399
3327
  snack = inject(MatSnackBar);
3328
+ dialog = inject(DialogService);
400
3329
  i18n = inject(PraxisI18nService);
401
3330
  resourceDiscovery = inject(ResourceDiscoveryService);
402
3331
  actionOpenAdapter = inject(ResourceActionOpenAdapterService);
@@ -429,6 +3358,7 @@ class PraxisCrudComponent {
429
3358
  collectionCapabilitiesRequestHref = null;
430
3359
  collectionCapabilitiesResolvedHref = null;
431
3360
  collectionCapabilitiesRequestSeq = 0;
3361
+ currentAuthoringDocument;
432
3362
  onResetPreferences() {
433
3363
  try {
434
3364
  const keyId = this.componentKeyId();
@@ -443,7 +3373,7 @@ class PraxisCrudComponent {
443
3373
  catch { }
444
3374
  }
445
3375
  ngOnChanges(changes) {
446
- if (!changes['metadata']) {
3376
+ if (!changes['metadata'] && !changes['context']) {
447
3377
  return;
448
3378
  }
449
3379
  try {
@@ -451,6 +3381,15 @@ class PraxisCrudComponent {
451
3381
  ? JSON.parse(this.metadata)
452
3382
  : this.metadata;
453
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;
454
3393
  assertCrudMetadata(this.resolvedMetadata, {
455
3394
  allowDeferredActionBindings: true,
456
3395
  });
@@ -494,6 +3433,10 @@ class PraxisCrudComponent {
494
3433
  }
495
3434
  }
496
3435
  const effectiveAction = (actionMeta || { action });
3436
+ const handledByDelete = await this.tryHandleCanonicalDeleteAction(effectiveAction, row);
3437
+ if (handledByDelete) {
3438
+ return;
3439
+ }
497
3440
  let drawerCloseEmitted = false;
498
3441
  const emitDrawerClose = () => {
499
3442
  if (drawerCloseEmitted)
@@ -518,6 +3461,11 @@ class PraxisCrudComponent {
518
3461
  this.refreshTable();
519
3462
  }
520
3463
  },
3464
+ }, {
3465
+ capabilities: effectiveAction.action === 'create'
3466
+ ? this.collectionCapabilities
3467
+ : this.collectionCapabilities,
3468
+ links: this.resolveCrudRuntimeLinks(effectiveAction.action, row),
521
3469
  });
522
3470
  this.afterOpen.emit({ mode, action: effectiveAction.action });
523
3471
  if (!ref) {
@@ -644,6 +3592,53 @@ class PraxisCrudComponent {
644
3592
  onConfigureRequested() {
645
3593
  this.configureRequested.emit();
646
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
+ }
647
3642
  refreshTable() {
648
3643
  this.table.refetch();
649
3644
  }
@@ -1142,6 +4137,49 @@ class PraxisCrudComponent {
1142
4137
  openMode: action.openMode,
1143
4138
  })),
1144
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
+ },
1145
4183
  };
1146
4184
  }
1147
4185
  resolveCreateToolbarAction(meta, capabilities) {
@@ -1242,19 +4280,20 @@ class PraxisCrudComponent {
1242
4280
  }));
1243
4281
  }
1244
4282
  supportsCreateCapability(snapshot) {
1245
- return (this.hasCanonicalOperation(snapshot, 'create') ||
4283
+ return (snapshot.operations?.create?.supported === true ||
4284
+ this.hasCanonicalOperation(snapshot, 'create') ||
1246
4285
  snapshot.surfaces.some((surface) => surface.scope === 'COLLECTION' &&
1247
4286
  this.isWritableCrudSurface(surface) &&
1248
4287
  surface.availability?.allowed !== false));
1249
4288
  }
1250
4289
  supportsViewCapability(snapshot) {
1251
- return this.hasCanonicalOperation(snapshot, 'byId');
4290
+ return snapshot.operations?.view?.supported === true || this.hasCanonicalOperation(snapshot, 'byId');
1252
4291
  }
1253
4292
  supportsEditCapability(snapshot) {
1254
- return this.hasCanonicalOperation(snapshot, 'update');
4293
+ return snapshot.operations?.edit?.supported === true || this.hasCanonicalOperation(snapshot, 'update');
1255
4294
  }
1256
4295
  supportsDeleteCapability(snapshot) {
1257
- return this.hasCanonicalOperation(snapshot, 'delete');
4296
+ return snapshot.operations?.delete?.supported === true || this.hasCanonicalOperation(snapshot, 'delete');
1258
4297
  }
1259
4298
  hasCanonicalOperation(snapshot, operation) {
1260
4299
  return snapshot.canonicalOperations?.[operation] === true;
@@ -1287,6 +4326,13 @@ class PraxisCrudComponent {
1287
4326
  return null;
1288
4327
  }
1289
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
+ }
1290
4336
  isRecord(value) {
1291
4337
  return !!value && typeof value === 'object' && !Array.isArray(value);
1292
4338
  }
@@ -1335,7 +4381,7 @@ class PraxisCrudComponent {
1335
4381
  }
1336
4382
  }
1337
4383
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisCrudComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1338
- 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: [
1339
4385
  providePraxisI18nConfig(RESOURCE_DISCOVERY_I18N_CONFIG),
1340
4386
  providePraxisI18nConfig(PRAXIS_CRUD_RUNTIME_I18N_CONFIG),
1341
4387
  ], viewQueries: [{ propertyName: "table", first: true, predicate: PraxisTable, descendants: true }], usesOnChanges: true, ngImport: i0, template: `
@@ -1428,6 +4474,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1428
4474
  type: Output
1429
4475
  }], tableRuntimeConfigChange: [{
1430
4476
  type: Output
4477
+ }], crudAuthoringDocumentApplied: [{
4478
+ type: Output
4479
+ }], crudAuthoringDocumentSaved: [{
4480
+ type: Output
1431
4481
  }], table: [{
1432
4482
  type: ViewChild,
1433
4483
  args: [PraxisTable]
@@ -1449,6 +4499,7 @@ class DynamicFormDialogHostComponent {
1449
4499
  destroyRef = inject(DestroyRef);
1450
4500
  resourcePath;
1451
4501
  resourceId;
4502
+ initialValue;
1452
4503
  schemaUrl;
1453
4504
  submitUrl;
1454
4505
  submitMethod;
@@ -1498,6 +4549,7 @@ class DynamicFormDialogHostComponent {
1498
4549
  }
1499
4550
  this.idField = this.data.metadata?.resource?.idField ?? 'id';
1500
4551
  this.resourceId = this.data.inputs?.[this.idField];
4552
+ this.initialValue = this.extractInitialValue(this.data.inputs);
1501
4553
  this.schemaUrl = this.data.inputs?.['schemaUrl'] ?? null;
1502
4554
  this.submitUrl = this.data.inputs?.['submitUrl'] ?? null;
1503
4555
  this.submitMethod = this.data.inputs?.['submitMethod'] ?? null;
@@ -1540,6 +4592,30 @@ class DynamicFormDialogHostComponent {
1540
4592
  .pipe(takeUntilDestroyed(this.destroyRef))
1541
4593
  .subscribe(() => this.saveState());
1542
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
+ }
1543
4619
  ngOnInit() {
1544
4620
  // Carregar estado salvo (se habilitado)
1545
4621
  if (this.rememberState && this.stateKey) {
@@ -1658,7 +4734,7 @@ class DynamicFormDialogHostComponent {
1658
4734
  this.dialogRef.updatePosition();
1659
4735
  }
1660
4736
  }
1661
- 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 });
1662
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: `
1663
4739
  <div mat-dialog-title class="dialog-header">
1664
4740
  <h2 id="crudDialogTitle" class="dialog-title">
@@ -1694,6 +4770,7 @@ class DynamicFormDialogHostComponent {
1694
4770
  [formId]="data.action?.formId"
1695
4771
  [resourcePath]="resourcePath"
1696
4772
  [resourceId]="resourceId"
4773
+ [initialValue]="initialValue"
1697
4774
  [mode]="mode"
1698
4775
  [schemaUrl]="schemaUrl"
1699
4776
  [submitUrl]="submitUrl"
@@ -1706,7 +4783,7 @@ class DynamicFormDialogHostComponent {
1706
4783
  (formCancel)="onCancel()"
1707
4784
  ></praxis-dynamic-form>
1708
4785
  </mat-dialog-content>
1709
- `, 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"] }] });
1710
4787
  }
1711
4788
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: DynamicFormDialogHostComponent, decorators: [{
1712
4789
  type: Component,
@@ -1755,6 +4832,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1755
4832
  [formId]="data.action?.formId"
1756
4833
  [resourcePath]="resourcePath"
1757
4834
  [resourceId]="resourceId"
4835
+ [initialValue]="initialValue"
1758
4836
  [mode]="mode"
1759
4837
  [schemaUrl]="schemaUrl"
1760
4838
  [submitUrl]="submitUrl"
@@ -1774,7 +4852,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1774
4852
  }] }, { type: undefined, decorators: [{
1775
4853
  type: Inject,
1776
4854
  args: [MAT_DIALOG_DATA]
1777
- }] }, { type: DialogService }, { type: i2.GenericCrudService }, { type: undefined, decorators: [{
4855
+ }] }, { type: DialogService }, { type: i2$1.GenericCrudService }, { type: undefined, decorators: [{
1778
4856
  type: Inject,
1779
4857
  args: [ASYNC_CONFIG_STORAGE]
1780
4858
  }] }], propDecorators: { formComp: [{
@@ -1804,22 +4882,22 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
1804
4882
  {
1805
4883
  name: 'crudId',
1806
4884
  type: 'string',
1807
- 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).',
1808
4886
  },
1809
4887
  {
1810
4888
  name: 'componentInstanceId',
1811
4889
  type: 'string',
1812
- description: 'Identificador opcional para múltiplas instâncias na mesma rota',
4890
+ description: 'Identificador opcional para múltiplas instâncias na mesma rota.',
1813
4891
  },
1814
4892
  {
1815
4893
  name: 'context',
1816
4894
  type: 'Record<string, unknown>',
1817
- 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.',
1818
4896
  },
1819
4897
  {
1820
4898
  name: 'metadata.queryContext',
1821
4899
  type: 'PraxisDataQueryContext | null',
1822
- 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.',
1823
4901
  },
1824
4902
  {
1825
4903
  name: 'metadata.filterCriteria',
@@ -1835,35 +4913,49 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
1835
4913
  name: 'enableCustomization',
1836
4914
  type: 'boolean',
1837
4915
  default: false,
1838
- description: 'Habilita modo de customização do layout',
4916
+ description: 'Habilita modo de customização do layout.',
1839
4917
  },
1840
4918
  ],
1841
4919
  outputs: [
1842
4920
  {
1843
4921
  name: 'afterOpen',
1844
4922
  type: '{ mode: FormOpenMode; action: string }',
1845
- description: 'Emitido após abrir diálogo',
4923
+ description: 'Emitido após abrir diálogo.',
1846
4924
  },
1847
4925
  {
1848
4926
  name: 'afterClose',
1849
4927
  type: 'void',
1850
- description: 'Emitido após fechar diálogo',
4928
+ description: 'Emitido após fechar diálogo.',
1851
4929
  },
1852
4930
  {
1853
4931
  name: 'afterSave',
1854
4932
  type: '{ id: string | number; data: unknown }',
1855
- description: 'Emitido ao salvar',
4933
+ description: 'Emitido ao salvar.',
1856
4934
  },
1857
4935
  {
1858
4936
  name: 'afterDelete',
1859
4937
  type: '{ id: string | number }',
1860
- description: 'Emitido ao deletar',
4938
+ description: 'Emitido ao deletar.',
4939
+ },
4940
+ {
4941
+ name: 'error',
4942
+ type: 'unknown',
4943
+ description: 'Emitido em erros.',
1861
4944
  },
1862
- { name: 'error', type: 'unknown', description: 'Emitido em erros' },
1863
4945
  {
1864
4946
  name: 'configureRequested',
1865
4947
  type: 'void',
1866
- 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.',
1867
4959
  },
1868
4960
  ],
1869
4961
  actions: [
@@ -1871,13 +4963,13 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
1871
4963
  id: 'create',
1872
4964
  label: 'Criar',
1873
4965
  icon: 'add',
1874
- description: 'Emite evento ao abrir fluxo de criação',
4966
+ description: 'Emite evento ao abrir fluxo de criação.',
1875
4967
  emit: 'afterOpen',
1876
4968
  payloadSchema: {
1877
4969
  type: 'object',
1878
4970
  properties: {
1879
- action: { type: 'string', description: 'Ação executada' },
1880
- mode: { type: 'string', description: 'Modo de abertura' },
4971
+ action: { type: 'string', description: 'Ação executada.' },
4972
+ mode: { type: 'string', description: 'Modo de abertura.' },
1881
4973
  },
1882
4974
  required: ['action', 'mode'],
1883
4975
  example: { action: 'create', mode: 'route' },
@@ -1888,13 +4980,13 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
1888
4980
  id: 'view',
1889
4981
  label: 'Visualizar',
1890
4982
  icon: 'visibility',
1891
- description: 'Emite evento ao abrir fluxo de visualização',
4983
+ description: 'Emite evento ao abrir fluxo de visualização.',
1892
4984
  emit: 'afterOpen',
1893
4985
  payloadSchema: {
1894
4986
  type: 'object',
1895
4987
  properties: {
1896
- action: { type: 'string', description: 'Ação executada' },
1897
- mode: { type: 'string', description: 'Modo de abertura' },
4988
+ action: { type: 'string', description: 'Ação executada.' },
4989
+ mode: { type: 'string', description: 'Modo de abertura.' },
1898
4990
  },
1899
4991
  required: ['action', 'mode'],
1900
4992
  example: { action: 'view', mode: 'modal' },
@@ -1905,13 +4997,13 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
1905
4997
  id: 'edit',
1906
4998
  label: 'Editar',
1907
4999
  icon: 'edit',
1908
- description: 'Emite evento ao abrir fluxo de edição',
5000
+ description: 'Emite evento ao abrir fluxo de edição.',
1909
5001
  emit: 'afterOpen',
1910
5002
  payloadSchema: {
1911
5003
  type: 'object',
1912
5004
  properties: {
1913
- action: { type: 'string', description: 'Ação executada' },
1914
- mode: { type: 'string', description: 'Modo de abertura' },
5005
+ action: { type: 'string', description: 'Ação executada.' },
5006
+ mode: { type: 'string', description: 'Modo de abertura.' },
1915
5007
  },
1916
5008
  required: ['action', 'mode'],
1917
5009
  example: { action: 'edit', mode: 'drawer' },
@@ -1922,12 +5014,12 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
1922
5014
  id: 'delete',
1923
5015
  label: 'Excluir',
1924
5016
  icon: 'delete',
1925
- description: 'Emite evento após exclusão',
5017
+ description: 'Emite evento após exclusão.',
1926
5018
  emit: 'afterDelete',
1927
5019
  payloadSchema: {
1928
5020
  type: 'object',
1929
5021
  properties: {
1930
- id: { type: 'string | number', description: 'ID do registro (string ou number)' },
5022
+ id: { type: 'string | number', description: 'ID do registro (string ou number).' },
1931
5023
  },
1932
5024
  required: ['id'],
1933
5025
  example: { id: '123' },
@@ -1938,13 +5030,13 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
1938
5030
  id: 'save',
1939
5031
  label: 'Salvar',
1940
5032
  icon: 'save',
1941
- description: 'Emite evento após salvar',
5033
+ description: 'Emite evento após salvar.',
1942
5034
  emit: 'afterSave',
1943
5035
  payloadSchema: {
1944
5036
  type: 'object',
1945
5037
  properties: {
1946
- id: { type: 'string | number', description: 'ID do registro (string ou number)' },
1947
- 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.' },
1948
5040
  },
1949
5041
  required: ['id', 'data'],
1950
5042
  example: { id: '123', data: {} },
@@ -1955,7 +5047,7 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
1955
5047
  id: 'close',
1956
5048
  label: 'Fechar',
1957
5049
  icon: 'close',
1958
- description: 'Emite evento ao fechar o diálogo',
5050
+ description: 'Emite evento ao fechar o diálogo.',
1959
5051
  emit: 'afterClose',
1960
5052
  scope: 'shell',
1961
5053
  },
@@ -1963,7 +5055,7 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
1963
5055
  id: 'configure',
1964
5056
  label: 'Configurar',
1965
5057
  icon: 'tune',
1966
- description: 'Emite evento ao abrir configuração',
5058
+ description: 'Emite evento ao abrir configuração.',
1967
5059
  emit: 'configureRequested',
1968
5060
  scope: 'shell',
1969
5061
  },
@@ -2018,7 +5110,7 @@ class CrudPageHeaderComponent {
2018
5110
  <ng-content></ng-content>
2019
5111
  </div>
2020
5112
  </header>
2021
- `, 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"] }] });
2022
5114
  }
2023
5115
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: CrudPageHeaderComponent, decorators: [{
2024
5116
  type: Component,
@@ -2145,6 +5237,7 @@ const CRUD_AI_CAPABILITIES = {
2145
5237
  { path: 'actions[].params[].from', category: 'actions', valueKind: 'string', description: 'Origem do parametro.' },
2146
5238
  { path: 'actions[].params[].to', category: 'actions', valueKind: 'enum', allowedValues: ENUMS.paramTarget, description: 'Destino do parametro.' },
2147
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.' },
2148
5241
  { path: 'actions[].back', category: 'navigation', valueKind: 'object', description: 'BackConfig por acao.' },
2149
5242
  { path: 'actions[].back.strategy', category: 'navigation', valueKind: 'enum', allowedValues: ENUMS.backStrategy, description: 'Estrategia de retorno da acao (auto, close, navigate).' },
2150
5243
  { path: 'actions[].back.returnTo', category: 'navigation', valueKind: 'string', description: 'Rota ou destino usado quando a estrategia de retorno for navigate.' },
@@ -2163,4 +5256,4 @@ const CRUD_AI_CAPABILITIES = {
2163
5256
  * Generated bundle index. Do not edit.
2164
5257
  */
2165
5258
 
2166
- 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 };