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