@praxisui/crud 0.0.1
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/LICENSE +7 -0
- package/README.md +63 -0
- package/fesm2022/praxisui-crud.mjs +1025 -0
- package/fesm2022/praxisui-crud.mjs.map +1 -0
- package/index.d.ts +224 -0
- package/package.json +40 -0
|
@@ -0,0 +1,1025 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { Injectable, InjectionToken, inject, EventEmitter, ViewChild, Output, Input, Component, DestroyRef, Inject, ENVIRONMENT_INITIALIZER } from '@angular/core';
|
|
3
|
+
import { PraxisTable } from '@praxisui/table';
|
|
4
|
+
import { Router, RouterLink } from '@angular/router';
|
|
5
|
+
import * as i1 from '@angular/material/dialog';
|
|
6
|
+
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
|
7
|
+
export { MAT_DIALOG_DATA as DIALOG_DATA } from '@angular/material/dialog';
|
|
8
|
+
import * as i2 from '@praxisui/core';
|
|
9
|
+
import { CONFIG_STORAGE, GlobalConfigService, fillUndefined, EmptyStateCardComponent, PraxisIconDirective, GenericCrudService, ComponentMetadataRegistry } from '@praxisui/core';
|
|
10
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
11
|
+
import { MatSnackBar } from '@angular/material/snack-bar';
|
|
12
|
+
import * as i1$1 from '@angular/common';
|
|
13
|
+
import { CommonModule } from '@angular/common';
|
|
14
|
+
import * as i4 from '@angular/material/button';
|
|
15
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
16
|
+
import * as i5 from '@angular/material/icon';
|
|
17
|
+
import { MatIconModule } from '@angular/material/icon';
|
|
18
|
+
import { PraxisDynamicForm } from '@praxisui/dynamic-form';
|
|
19
|
+
import { ConfirmDialogComponent } from '@praxisui/dynamic-fields';
|
|
20
|
+
import { filter } from 'rxjs/operators';
|
|
21
|
+
|
|
22
|
+
class DialogService {
|
|
23
|
+
matDialog;
|
|
24
|
+
zone;
|
|
25
|
+
constructor(matDialog, zone) {
|
|
26
|
+
this.matDialog = matDialog;
|
|
27
|
+
this.zone = zone;
|
|
28
|
+
}
|
|
29
|
+
open(component, config) {
|
|
30
|
+
return this.zone.run(() => this.matDialog.open(component, config));
|
|
31
|
+
}
|
|
32
|
+
async openAsync(loader, config) {
|
|
33
|
+
const component = await loader();
|
|
34
|
+
return this.zone.run(() => this.matDialog.open(component, config));
|
|
35
|
+
}
|
|
36
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: DialogService, deps: [{ token: i1.MatDialog }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
37
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: DialogService, providedIn: 'root' });
|
|
38
|
+
}
|
|
39
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: DialogService, decorators: [{
|
|
40
|
+
type: Injectable,
|
|
41
|
+
args: [{ providedIn: 'root' }]
|
|
42
|
+
}], ctorParameters: () => [{ type: i1.MatDialog }, { type: i0.NgZone }] });
|
|
43
|
+
|
|
44
|
+
const CRUD_DRAWER_ADAPTER = new InjectionToken('CRUD_DRAWER_ADAPTER');
|
|
45
|
+
|
|
46
|
+
class CrudLauncherService {
|
|
47
|
+
router = inject(Router);
|
|
48
|
+
dialog = inject(DialogService);
|
|
49
|
+
storage = inject(CONFIG_STORAGE);
|
|
50
|
+
global = inject(GlobalConfigService);
|
|
51
|
+
drawerAdapter = (() => {
|
|
52
|
+
try {
|
|
53
|
+
return inject(CRUD_DRAWER_ADAPTER);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
})();
|
|
59
|
+
async launch(action, row, metadata) {
|
|
60
|
+
// Carregar overrides de CRUD (se houver) e mesclar em uma cópia local
|
|
61
|
+
const merged = this.mergeCrudOverrides(metadata, action);
|
|
62
|
+
const mode = this.resolveOpenMode(merged.action, merged.metadata);
|
|
63
|
+
console.debug('[CRUD:Launcher] mode=', mode, 'action=', action);
|
|
64
|
+
if (mode === 'route') {
|
|
65
|
+
if (!merged.action.route) {
|
|
66
|
+
throw new Error(`Route not provided for action ${merged.action.action}`);
|
|
67
|
+
}
|
|
68
|
+
const url = this.buildRoute(merged.action, row, merged.metadata);
|
|
69
|
+
await this.router.navigateByUrl(url);
|
|
70
|
+
return { mode };
|
|
71
|
+
}
|
|
72
|
+
if (mode === 'drawer' && this.drawerAdapter) {
|
|
73
|
+
const inputs = this.mapInputs(merged.action, row);
|
|
74
|
+
const idField = merged.metadata.resource?.idField ?? 'id';
|
|
75
|
+
if (row && inputs[idField] === undefined && row[idField] !== undefined) {
|
|
76
|
+
inputs[idField] = row[idField];
|
|
77
|
+
}
|
|
78
|
+
await Promise.resolve(this.drawerAdapter.open({ action: merged.action, metadata: merged.metadata, inputs }));
|
|
79
|
+
return { mode };
|
|
80
|
+
}
|
|
81
|
+
if (!merged.action.formId) {
|
|
82
|
+
throw new Error(`formId not provided for action ${merged.action.action}`);
|
|
83
|
+
}
|
|
84
|
+
const inputs = this.mapInputs(merged.action, row);
|
|
85
|
+
const idField = merged.metadata.resource?.idField ?? 'id';
|
|
86
|
+
if (row && inputs[idField] === undefined && row[idField] !== undefined) {
|
|
87
|
+
inputs[idField] = row[idField];
|
|
88
|
+
}
|
|
89
|
+
const modalCfg = { ...(merged.metadata.defaults?.modal || {}) };
|
|
90
|
+
console.debug('[CRUD:Launcher] opening dialog with:', {
|
|
91
|
+
action: merged.action.action,
|
|
92
|
+
formId: merged.action.formId,
|
|
93
|
+
inputs,
|
|
94
|
+
modalCfg,
|
|
95
|
+
resourcePath: merged.metadata.resource?.path ?? merged.metadata.table?.resourcePath,
|
|
96
|
+
});
|
|
97
|
+
const panelClasses = ['pfx-dialog-pane', 'pfx-dialog-frosted'];
|
|
98
|
+
if (modalCfg.panelClass) {
|
|
99
|
+
panelClasses.push(modalCfg.panelClass);
|
|
100
|
+
}
|
|
101
|
+
// Backdrop style presets
|
|
102
|
+
const backdropClasses = [];
|
|
103
|
+
const style = modalCfg.backdropStyle;
|
|
104
|
+
if (!style || style === 'blur') {
|
|
105
|
+
backdropClasses.push('pfx-blur-backdrop');
|
|
106
|
+
}
|
|
107
|
+
else if (style === 'dim') {
|
|
108
|
+
backdropClasses.push('cdk-overlay-dark-backdrop');
|
|
109
|
+
}
|
|
110
|
+
else if (style === 'transparent') {
|
|
111
|
+
backdropClasses.push('pfx-transparent-backdrop');
|
|
112
|
+
}
|
|
113
|
+
if (modalCfg.backdropClass) {
|
|
114
|
+
backdropClasses.push(modalCfg.backdropClass);
|
|
115
|
+
}
|
|
116
|
+
const ref = await this.dialog.openAsync(() => Promise.resolve().then(function () { return dynamicFormDialogHost_component; }).then((m) => m.DynamicFormDialogHostComponent), {
|
|
117
|
+
...modalCfg,
|
|
118
|
+
panelClass: panelClasses,
|
|
119
|
+
backdropClass: backdropClasses,
|
|
120
|
+
autoFocus: modalCfg.autoFocus ?? true,
|
|
121
|
+
restoreFocus: modalCfg.restoreFocus ?? true,
|
|
122
|
+
minWidth: '360px',
|
|
123
|
+
maxWidth: '95vw',
|
|
124
|
+
ariaLabelledBy: 'crudDialogTitle',
|
|
125
|
+
data: { action: merged.action, row, metadata: merged.metadata, inputs },
|
|
126
|
+
});
|
|
127
|
+
return { mode, ref };
|
|
128
|
+
}
|
|
129
|
+
resolveOpenMode(action, metadata) {
|
|
130
|
+
// Local precedence: action > metadata.defaults
|
|
131
|
+
const local = action.openMode ?? metadata.defaults?.openMode;
|
|
132
|
+
if (local)
|
|
133
|
+
return local;
|
|
134
|
+
// Global fallback (action-specific, then general), then hard fallback 'route'
|
|
135
|
+
try {
|
|
136
|
+
const globalCrud = this.global.getCrud();
|
|
137
|
+
const actionName = action.action;
|
|
138
|
+
const globalMode = (actionName && globalCrud?.actionDefaults?.[actionName]?.openMode) ?? globalCrud?.defaults?.openMode;
|
|
139
|
+
let resolved = globalMode ?? 'route';
|
|
140
|
+
// Safety: if modal/drawer but no formId, degrade to route to avoid runtime error
|
|
141
|
+
if ((resolved === 'modal' || resolved === 'drawer') && !action.formId) {
|
|
142
|
+
resolved = 'route';
|
|
143
|
+
}
|
|
144
|
+
return resolved;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return 'route';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
buildRoute(action, row, metadata) {
|
|
151
|
+
let route = action.route;
|
|
152
|
+
const query = {};
|
|
153
|
+
action.params?.forEach((p) => {
|
|
154
|
+
const value = row?.[p.from];
|
|
155
|
+
if (p.to === 'routeParam') {
|
|
156
|
+
if (value === undefined || value === null) {
|
|
157
|
+
throw new Error(`Missing value for route param ${p.name}`);
|
|
158
|
+
}
|
|
159
|
+
const re = new RegExp(`:${p.name}\\b`, 'g');
|
|
160
|
+
route = route.replace(re, encodeURIComponent(String(value)));
|
|
161
|
+
}
|
|
162
|
+
else if (p.to === 'query' && value !== undefined && value !== null) {
|
|
163
|
+
query[p.name] = String(value);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
const missing = route.match(/:[a-zA-Z0-9_-]+/g);
|
|
167
|
+
if (missing) {
|
|
168
|
+
throw new Error(`Missing route parameters for action "${action.action}": ${missing
|
|
169
|
+
.map((m) => m.slice(1))
|
|
170
|
+
.join(', ')}`);
|
|
171
|
+
}
|
|
172
|
+
// Ensure resource is present for routed forms so the formId is deterministic
|
|
173
|
+
try {
|
|
174
|
+
const resourcePath = metadata.resource?.path || metadata.table?.resourcePath;
|
|
175
|
+
if (resourcePath && query['resource'] === undefined) {
|
|
176
|
+
query['resource'] = resourcePath;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch { }
|
|
180
|
+
// ReturnTo param for graceful back navigation on routed forms
|
|
181
|
+
const back = action.back || metadata.defaults?.back;
|
|
182
|
+
const returnTo = back?.returnTo || this.router.url;
|
|
183
|
+
if (returnTo) {
|
|
184
|
+
query['returnTo'] = returnTo;
|
|
185
|
+
}
|
|
186
|
+
const queryString = new URLSearchParams(query).toString();
|
|
187
|
+
return queryString ? `${route}?${queryString}` : route;
|
|
188
|
+
}
|
|
189
|
+
mapInputs(action, row) {
|
|
190
|
+
const inputs = {};
|
|
191
|
+
action.params?.forEach((p) => {
|
|
192
|
+
if (p.to === 'input') {
|
|
193
|
+
inputs[p.name] = row?.[p.from];
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
return inputs;
|
|
197
|
+
}
|
|
198
|
+
mergeCrudOverrides(metadata, action) {
|
|
199
|
+
try {
|
|
200
|
+
const tableId = metadata.resource?.path || metadata.table?.resourcePath || 'default';
|
|
201
|
+
const key = `crud-overrides:${tableId}`;
|
|
202
|
+
const saved = this.storage.loadConfig(key) || {};
|
|
203
|
+
const overDefaults = saved.defaults || {};
|
|
204
|
+
const overActions = saved.actions || {};
|
|
205
|
+
// Merge defaults (shallow for defaults, shallow for modal/back)
|
|
206
|
+
let mergedDefaults = {
|
|
207
|
+
...(metadata.defaults || {}),
|
|
208
|
+
...('openMode' in overDefaults ? { openMode: overDefaults.openMode } : {}),
|
|
209
|
+
modal: { ...(metadata.defaults?.modal || {}), ...(overDefaults.modal || {}) },
|
|
210
|
+
back: { ...(metadata.defaults?.back || {}), ...(overDefaults.back || {}) },
|
|
211
|
+
header: { ...(metadata.defaults?.header || {}), ...(overDefaults.header || {}) },
|
|
212
|
+
};
|
|
213
|
+
// Fill missing defaults from GlobalConfig (as last fallback)
|
|
214
|
+
try {
|
|
215
|
+
const globalCrud = this.global.getCrud();
|
|
216
|
+
if (globalCrud?.defaults) {
|
|
217
|
+
mergedDefaults = fillUndefined(mergedDefaults, globalCrud.defaults);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch { }
|
|
221
|
+
// Merge metadata copy
|
|
222
|
+
const mergedMetadata = {
|
|
223
|
+
...metadata,
|
|
224
|
+
defaults: mergedDefaults,
|
|
225
|
+
};
|
|
226
|
+
// Action-level override
|
|
227
|
+
const actOv = overActions[action.action] || {};
|
|
228
|
+
const mergedAction = {
|
|
229
|
+
...action,
|
|
230
|
+
...('openMode' in actOv ? { openMode: actOv.openMode } : {}),
|
|
231
|
+
...('formId' in actOv ? { formId: actOv.formId } : {}),
|
|
232
|
+
...('route' in actOv ? { route: actOv.route } : {}),
|
|
233
|
+
};
|
|
234
|
+
return { metadata: mergedMetadata, action: mergedAction };
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return { metadata, action };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: CrudLauncherService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
241
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: CrudLauncherService, providedIn: 'root' });
|
|
242
|
+
}
|
|
243
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: CrudLauncherService, decorators: [{
|
|
244
|
+
type: Injectable,
|
|
245
|
+
args: [{ providedIn: 'root' }]
|
|
246
|
+
}] });
|
|
247
|
+
|
|
248
|
+
function assertCrudMetadata(meta) {
|
|
249
|
+
meta.actions?.forEach((action) => {
|
|
250
|
+
const mode = action.openMode ?? meta.defaults?.openMode ?? 'route';
|
|
251
|
+
if (mode === 'route' && !action.route) {
|
|
252
|
+
throw new Error(`Route not provided for action ${action.action}`);
|
|
253
|
+
}
|
|
254
|
+
if ((mode === 'modal' || mode === 'drawer') && !action.formId) {
|
|
255
|
+
throw new Error(`formId not provided for action ${action.action}`);
|
|
256
|
+
}
|
|
257
|
+
action.params?.forEach((p) => {
|
|
258
|
+
if (!['routeParam', 'query', 'input'].includes(p.to)) {
|
|
259
|
+
throw new Error(`Invalid param mapping target: ${p.to}`);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
class PraxisCrudComponent {
|
|
266
|
+
/** Habilita visual de debug para alinhamento/layouot */
|
|
267
|
+
debugLayout = false;
|
|
268
|
+
/** JSON inline ou chave/URL resolvida pelo MetadataResolver */
|
|
269
|
+
metadata;
|
|
270
|
+
context;
|
|
271
|
+
/** Encaminha o modo de edição de layout para a tabela interna */
|
|
272
|
+
editModeEnabled = false;
|
|
273
|
+
/** CTA: usado pelo Builder para abrir configuração de metadados quando vazio */
|
|
274
|
+
configureRequested = new EventEmitter();
|
|
275
|
+
afterOpen = new EventEmitter();
|
|
276
|
+
afterClose = new EventEmitter();
|
|
277
|
+
afterSave = new EventEmitter();
|
|
278
|
+
afterDelete = new EventEmitter();
|
|
279
|
+
error = new EventEmitter();
|
|
280
|
+
resolvedMetadata;
|
|
281
|
+
/** Configuração efetiva da tabela com melhorias automáticas (ex.: botão Adicionar). */
|
|
282
|
+
effectiveTableConfig;
|
|
283
|
+
launcher = inject(CrudLauncherService);
|
|
284
|
+
table;
|
|
285
|
+
storage = inject(CONFIG_STORAGE);
|
|
286
|
+
snack = inject(MatSnackBar);
|
|
287
|
+
/**
|
|
288
|
+
* Stable CRUD context passed to PraxisTable.
|
|
289
|
+
* Previously this was created via a getter, producing a new object each CD tick
|
|
290
|
+
* and causing excessive re-renders. Now we compute it only when metadata changes.
|
|
291
|
+
*/
|
|
292
|
+
tableCrudContext;
|
|
293
|
+
onResetPreferences() {
|
|
294
|
+
try {
|
|
295
|
+
const id = this.resolvedMetadata?.resource?.path || 'default';
|
|
296
|
+
const key = `crud-overrides:${id}`;
|
|
297
|
+
this.storage.clearConfig(key);
|
|
298
|
+
this.snack.open('Overrides de CRUD redefinidos', undefined, { duration: 2000 });
|
|
299
|
+
}
|
|
300
|
+
catch { }
|
|
301
|
+
}
|
|
302
|
+
ngOnChanges(changes) {
|
|
303
|
+
if (changes['metadata']) {
|
|
304
|
+
try {
|
|
305
|
+
this.resolvedMetadata =
|
|
306
|
+
typeof this.metadata === 'string'
|
|
307
|
+
? JSON.parse(this.metadata)
|
|
308
|
+
: this.metadata;
|
|
309
|
+
assertCrudMetadata(this.resolvedMetadata);
|
|
310
|
+
this.effectiveTableConfig = this.buildEffectiveTableConfig(this.resolvedMetadata);
|
|
311
|
+
// Build a stable table context when metadata changes
|
|
312
|
+
this.tableCrudContext = this.buildTableCrudContext(this.resolvedMetadata);
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
this.error.emit(err);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async onAction(action, row) {
|
|
320
|
+
try {
|
|
321
|
+
document.activeElement?.blur();
|
|
322
|
+
let actionMeta = this.resolvedMetadata.actions?.find((a) => a.action === action);
|
|
323
|
+
// Fallback: if metadata.actions is missing or doesn't include the action,
|
|
324
|
+
// synthesize from table CRUD context or let overrides drive behavior.
|
|
325
|
+
if (!actionMeta) {
|
|
326
|
+
const ctxAction = this.tableCrudContext?.actions?.find((a) => a.action === action);
|
|
327
|
+
if (ctxAction) {
|
|
328
|
+
actionMeta = {
|
|
329
|
+
action: ctxAction.action,
|
|
330
|
+
openMode: ctxAction.openMode,
|
|
331
|
+
formId: ctxAction.formId,
|
|
332
|
+
route: ctxAction.route,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
// Minimal stub – CrudLauncherService will merge overrides/defaults
|
|
337
|
+
actionMeta = { action };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const effectiveAction = (actionMeta || { action });
|
|
341
|
+
const { mode, ref } = await this.launcher.launch(effectiveAction, row, this.resolvedMetadata);
|
|
342
|
+
this.afterOpen.emit({ mode, action: effectiveAction.action });
|
|
343
|
+
if (ref) {
|
|
344
|
+
const idField = this.getIdField();
|
|
345
|
+
ref
|
|
346
|
+
.afterClosed()
|
|
347
|
+
.pipe(takeUntilDestroyed())
|
|
348
|
+
.subscribe((result) => {
|
|
349
|
+
this.afterClose.emit();
|
|
350
|
+
if (result?.type === 'save') {
|
|
351
|
+
const data = result.data;
|
|
352
|
+
const id = data?.[idField];
|
|
353
|
+
this.afterSave.emit({ id, data: result.data });
|
|
354
|
+
this.refreshTable();
|
|
355
|
+
}
|
|
356
|
+
if (result?.type === 'delete') {
|
|
357
|
+
const data = result.data;
|
|
358
|
+
const id = data?.[idField];
|
|
359
|
+
this.afterDelete.emit({ id });
|
|
360
|
+
this.refreshTable();
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
catch (err) {
|
|
366
|
+
this.error.emit(err);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
refreshTable() {
|
|
370
|
+
this.table.refetch();
|
|
371
|
+
}
|
|
372
|
+
getIdField() {
|
|
373
|
+
return this.resolvedMetadata?.resource?.idField || 'id';
|
|
374
|
+
}
|
|
375
|
+
onConfigureRequested() {
|
|
376
|
+
this.configureRequested.emit();
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Constrói uma configuração de tabela efetiva a partir dos metadados,
|
|
380
|
+
* adicionando automaticamente a ação de "Adicionar" na toolbar quando aplicável.
|
|
381
|
+
* - Evita duplicidade se a toolbar já tiver ação equivalente.
|
|
382
|
+
* - Garante visibilidade da toolbar quando a ação é injetada.
|
|
383
|
+
*/
|
|
384
|
+
buildEffectiveTableConfig(meta) {
|
|
385
|
+
const base = meta.table;
|
|
386
|
+
if (!base)
|
|
387
|
+
return undefined;
|
|
388
|
+
// Clonar para evitar mutações
|
|
389
|
+
const cfg = JSON.parse(JSON.stringify(base));
|
|
390
|
+
const ensureToolbar = () => {
|
|
391
|
+
if (!cfg.toolbar) {
|
|
392
|
+
cfg.toolbar = { visible: true, position: 'top' };
|
|
393
|
+
}
|
|
394
|
+
return cfg.toolbar;
|
|
395
|
+
};
|
|
396
|
+
// Detectar se já existe ação de adicionar na toolbar
|
|
397
|
+
const hasToolbarAdd = (cfg.toolbar?.actions || []).some((a) => this.isAddLike(a));
|
|
398
|
+
// Procurar mapeamento de ação "adicionar/criar" nos metadados do CRUD
|
|
399
|
+
const addAction = (meta.actions || []).find((a) => this.isAddLike(a));
|
|
400
|
+
// Se não houver mapeamento "add/create", não vamos injetar nada
|
|
401
|
+
if (!addAction) {
|
|
402
|
+
return hasToolbarAdd ? cfg : undefined;
|
|
403
|
+
}
|
|
404
|
+
// Se já existe na toolbar, apenas garantir visibilidade se necessário
|
|
405
|
+
if (hasToolbarAdd) {
|
|
406
|
+
const tb = ensureToolbar();
|
|
407
|
+
tb.visible = true;
|
|
408
|
+
return cfg;
|
|
409
|
+
}
|
|
410
|
+
// Injetar ação na toolbar
|
|
411
|
+
const tb = ensureToolbar();
|
|
412
|
+
tb.visible = true;
|
|
413
|
+
if (!tb.actions)
|
|
414
|
+
tb.actions = [];
|
|
415
|
+
const injected = {
|
|
416
|
+
id: addAction.id || 'add',
|
|
417
|
+
label: addAction.label || 'Adicionar',
|
|
418
|
+
icon: addAction.icon || 'add',
|
|
419
|
+
type: 'button',
|
|
420
|
+
color: 'primary',
|
|
421
|
+
position: 'end',
|
|
422
|
+
action: addAction.action || 'add',
|
|
423
|
+
};
|
|
424
|
+
tb.actions.push(injected);
|
|
425
|
+
return cfg;
|
|
426
|
+
}
|
|
427
|
+
/** Heurística leve para identificar ações do tipo "adicionar/criar" */
|
|
428
|
+
isAddLike(a) {
|
|
429
|
+
if (!a)
|
|
430
|
+
return false;
|
|
431
|
+
const normalize = (s) => String(s || '').trim().toLowerCase();
|
|
432
|
+
const id = normalize(a.action || a.id || a.code || a.key || a.name || a.type);
|
|
433
|
+
const icon = normalize(a.icon);
|
|
434
|
+
const label = normalize(a.label);
|
|
435
|
+
const addIds = new Set(['add', 'create', 'novo', 'new', 'incluir', 'inserir']);
|
|
436
|
+
if (addIds.has(id))
|
|
437
|
+
return true;
|
|
438
|
+
if (icon === 'add' || icon === 'add_circle' || icon === 'add_box')
|
|
439
|
+
return true;
|
|
440
|
+
if (label === 'adicionar' || label === 'novo' || label === 'criar' || label === 'incluir')
|
|
441
|
+
return true;
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
/** Builds a stable CRUD context object for PraxisTable based on metadata. */
|
|
445
|
+
buildTableCrudContext(meta) {
|
|
446
|
+
if (!meta)
|
|
447
|
+
return undefined;
|
|
448
|
+
// Provide a minimal default action set when metadata.actions is empty,
|
|
449
|
+
// so the editor can expose per-action overrides entirely via UI.
|
|
450
|
+
const baseActions = meta.actions && meta.actions.length
|
|
451
|
+
? meta.actions
|
|
452
|
+
: [{ action: 'create' }, { action: 'view' }, { action: 'edit' }];
|
|
453
|
+
return {
|
|
454
|
+
tableId: meta.resource?.path || meta.table?.resourcePath || 'default',
|
|
455
|
+
resourcePath: meta.resource?.path || meta.table?.resourcePath,
|
|
456
|
+
defaults: meta.defaults,
|
|
457
|
+
actions: baseActions.map((a) => ({
|
|
458
|
+
action: a.action,
|
|
459
|
+
label: a.label,
|
|
460
|
+
formId: a.formId,
|
|
461
|
+
route: a.route,
|
|
462
|
+
openMode: a.openMode,
|
|
463
|
+
})),
|
|
464
|
+
idField: meta.resource?.idField || 'id',
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PraxisCrudComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
468
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.4", type: PraxisCrudComponent, isStandalone: true, selector: "praxis-crud", inputs: { debugLayout: "debugLayout", metadata: "metadata", context: "context", editModeEnabled: "editModeEnabled" }, outputs: { configureRequested: "configureRequested", afterOpen: "afterOpen", afterClose: "afterClose", afterSave: "afterSave", afterDelete: "afterDelete", error: "error" }, viewQueries: [{ propertyName: "table", first: true, predicate: PraxisTable, descendants: true }], usesOnChanges: true, ngImport: i0, template: `
|
|
469
|
+
@if ($any(resolvedMetadata)?.resource?.path) {
|
|
470
|
+
<praxis-table
|
|
471
|
+
[config]="effectiveTableConfig || resolvedMetadata.table"
|
|
472
|
+
[resourcePath]="resolvedMetadata.resource?.path"
|
|
473
|
+
[tableId]="resolvedMetadata.resource?.path || 'default'"
|
|
474
|
+
[crudContext]="tableCrudContext"
|
|
475
|
+
[editModeEnabled]="editModeEnabled"
|
|
476
|
+
(rowAction)="onAction($event.action, $event.row)"
|
|
477
|
+
(toolbarAction)="onAction($event.action)"
|
|
478
|
+
(reset)="onResetPreferences()"
|
|
479
|
+
[debugLayout]="debugLayout"
|
|
480
|
+
></praxis-table>
|
|
481
|
+
} @else {
|
|
482
|
+
@if (editModeEnabled) {
|
|
483
|
+
<praxis-empty-state-card
|
|
484
|
+
icon="table_rows"
|
|
485
|
+
[title]="'Conecte o CRUD a um recurso'"
|
|
486
|
+
[description]="'Informe os metadados (resourcePath / schema) para habilitar a tabela e ações.'"
|
|
487
|
+
[primaryAction]="{ label: 'Configurar metadados', icon: 'bolt', action: onConfigureRequested.bind(this) }"
|
|
488
|
+
/>
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
`, isInline: true, dependencies: [{ kind: "component", type: PraxisTable, selector: "praxis-table", inputs: ["config", "resourcePath", "filterCriteria", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "showToolbar", "toolbarV2", "autoDelete", "editModeEnabled", "dense", "tableId", "debugLayout", "crudContext", "idField"], outputs: ["rowClick", "rowAction", "toolbarAction", "bulkAction", "rowDoubleClick", "schemaStatusChange", "beforeDelete", "afterDelete", "deleteError", "beforeBulkDelete", "afterBulkDelete", "bulkDeleteError"] }, { kind: "component", type: EmptyStateCardComponent, selector: "praxis-empty-state-card", inputs: ["icon", "title", "description", "primaryAction", "secondaryActions", "inline"] }] });
|
|
492
|
+
}
|
|
493
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PraxisCrudComponent, decorators: [{
|
|
494
|
+
type: Component,
|
|
495
|
+
args: [{
|
|
496
|
+
selector: 'praxis-crud',
|
|
497
|
+
standalone: true,
|
|
498
|
+
imports: [PraxisTable, EmptyStateCardComponent],
|
|
499
|
+
template: `
|
|
500
|
+
@if ($any(resolvedMetadata)?.resource?.path) {
|
|
501
|
+
<praxis-table
|
|
502
|
+
[config]="effectiveTableConfig || resolvedMetadata.table"
|
|
503
|
+
[resourcePath]="resolvedMetadata.resource?.path"
|
|
504
|
+
[tableId]="resolvedMetadata.resource?.path || 'default'"
|
|
505
|
+
[crudContext]="tableCrudContext"
|
|
506
|
+
[editModeEnabled]="editModeEnabled"
|
|
507
|
+
(rowAction)="onAction($event.action, $event.row)"
|
|
508
|
+
(toolbarAction)="onAction($event.action)"
|
|
509
|
+
(reset)="onResetPreferences()"
|
|
510
|
+
[debugLayout]="debugLayout"
|
|
511
|
+
></praxis-table>
|
|
512
|
+
} @else {
|
|
513
|
+
@if (editModeEnabled) {
|
|
514
|
+
<praxis-empty-state-card
|
|
515
|
+
icon="table_rows"
|
|
516
|
+
[title]="'Conecte o CRUD a um recurso'"
|
|
517
|
+
[description]="'Informe os metadados (resourcePath / schema) para habilitar a tabela e ações.'"
|
|
518
|
+
[primaryAction]="{ label: 'Configurar metadados', icon: 'bolt', action: onConfigureRequested.bind(this) }"
|
|
519
|
+
/>
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
`,
|
|
523
|
+
}]
|
|
524
|
+
}], propDecorators: { debugLayout: [{
|
|
525
|
+
type: Input
|
|
526
|
+
}], metadata: [{
|
|
527
|
+
type: Input,
|
|
528
|
+
args: [{ required: true }]
|
|
529
|
+
}], context: [{
|
|
530
|
+
type: Input
|
|
531
|
+
}], editModeEnabled: [{
|
|
532
|
+
type: Input
|
|
533
|
+
}], configureRequested: [{
|
|
534
|
+
type: Output
|
|
535
|
+
}], afterOpen: [{
|
|
536
|
+
type: Output
|
|
537
|
+
}], afterClose: [{
|
|
538
|
+
type: Output
|
|
539
|
+
}], afterSave: [{
|
|
540
|
+
type: Output
|
|
541
|
+
}], afterDelete: [{
|
|
542
|
+
type: Output
|
|
543
|
+
}], error: [{
|
|
544
|
+
type: Output
|
|
545
|
+
}], table: [{
|
|
546
|
+
type: ViewChild,
|
|
547
|
+
args: [PraxisTable]
|
|
548
|
+
}] } });
|
|
549
|
+
|
|
550
|
+
class DynamicFormDialogHostComponent {
|
|
551
|
+
dialogRef;
|
|
552
|
+
data;
|
|
553
|
+
dialogService;
|
|
554
|
+
crud;
|
|
555
|
+
configStorage;
|
|
556
|
+
formComp;
|
|
557
|
+
modal = {};
|
|
558
|
+
maximized = false;
|
|
559
|
+
initialSize = {};
|
|
560
|
+
rememberState = false;
|
|
561
|
+
stateKey;
|
|
562
|
+
destroyRef = inject(DestroyRef);
|
|
563
|
+
resourcePath;
|
|
564
|
+
resourceId;
|
|
565
|
+
mode = 'create';
|
|
566
|
+
backConfig;
|
|
567
|
+
idField = 'id';
|
|
568
|
+
texts = {
|
|
569
|
+
title: 'Formulário',
|
|
570
|
+
close: 'Fechar',
|
|
571
|
+
closeLabel: 'Fechar diálogo',
|
|
572
|
+
maximizeLabel: 'Maximizar',
|
|
573
|
+
restoreLabel: 'Restaurar',
|
|
574
|
+
discardTitle: 'Descartar alterações?',
|
|
575
|
+
discardMessage: 'Você tem alterações não salvas. Deseja fechar assim mesmo?',
|
|
576
|
+
discardConfirm: 'Descartar',
|
|
577
|
+
discardCancel: 'Cancelar',
|
|
578
|
+
};
|
|
579
|
+
constructor(dialogRef, data, dialogService, crud, configStorage) {
|
|
580
|
+
this.dialogRef = dialogRef;
|
|
581
|
+
this.data = data;
|
|
582
|
+
this.dialogService = dialogService;
|
|
583
|
+
this.crud = crud;
|
|
584
|
+
this.configStorage = configStorage;
|
|
585
|
+
this.dialogRef.disableClose = true;
|
|
586
|
+
// i18n
|
|
587
|
+
this.texts = {
|
|
588
|
+
...this.texts,
|
|
589
|
+
...(this.data.metadata?.i18n?.crudDialog || {}),
|
|
590
|
+
};
|
|
591
|
+
// defaults do modal (inclui canMaximize: true)
|
|
592
|
+
this.modal = {
|
|
593
|
+
canMaximize: true,
|
|
594
|
+
...(this.data.metadata?.defaults?.modal || {}),
|
|
595
|
+
};
|
|
596
|
+
this.rememberState = !!this.modal.rememberLastState;
|
|
597
|
+
this.stateKey = this.data.action?.formId
|
|
598
|
+
? `crud-dialog-state:${this.data.action.formId}`
|
|
599
|
+
: undefined;
|
|
600
|
+
// derivar path/id/mode
|
|
601
|
+
this.resourcePath =
|
|
602
|
+
this.data.metadata?.resource?.path ??
|
|
603
|
+
this.data.metadata?.table?.resourcePath;
|
|
604
|
+
if (this.resourcePath) {
|
|
605
|
+
this.crud.configure(this.resourcePath, this.data.metadata?.resource?.endpointKey);
|
|
606
|
+
}
|
|
607
|
+
this.idField = this.data.metadata?.resource?.idField ?? 'id';
|
|
608
|
+
this.resourceId = this.data.inputs?.[this.idField];
|
|
609
|
+
const act = this.data.action?.action;
|
|
610
|
+
this.mode = act === 'edit' ? 'edit' : act === 'view' ? 'view' : 'create';
|
|
611
|
+
// Back config: defaults from metadata/action, overridden by saved per-form config
|
|
612
|
+
const defaults = (this.data.action?.back || this.data.metadata?.defaults?.back) || {};
|
|
613
|
+
const saved = this.data.action?.formId
|
|
614
|
+
? this.configStorage.loadConfig(`crud-back:${this.data.action.formId}`)
|
|
615
|
+
: null;
|
|
616
|
+
this.backConfig = { ...defaults, ...(saved || {}) };
|
|
617
|
+
console.debug('[CRUD:Host] constructed', {
|
|
618
|
+
action: this.data?.action,
|
|
619
|
+
resourcePath: this.resourcePath,
|
|
620
|
+
resourceId: this.resourceId,
|
|
621
|
+
mode: this.mode,
|
|
622
|
+
modal: this.modal,
|
|
623
|
+
backConfig: this.backConfig,
|
|
624
|
+
});
|
|
625
|
+
// Esc
|
|
626
|
+
if (!this.modal.disableCloseOnEsc) {
|
|
627
|
+
this.dialogRef
|
|
628
|
+
.keydownEvents()
|
|
629
|
+
.pipe(filter((e) => e.key === 'Escape'), takeUntilDestroyed(this.destroyRef))
|
|
630
|
+
.subscribe(() => this.onCancel());
|
|
631
|
+
}
|
|
632
|
+
// Backdrop
|
|
633
|
+
if (!this.modal.disableCloseOnBackdrop) {
|
|
634
|
+
this.dialogRef
|
|
635
|
+
.backdropClick()
|
|
636
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
637
|
+
.subscribe(() => this.onCancel());
|
|
638
|
+
}
|
|
639
|
+
// Salvar estado ao fechar, se aplicável
|
|
640
|
+
this.dialogRef
|
|
641
|
+
.afterClosed()
|
|
642
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
643
|
+
.subscribe(() => this.saveState());
|
|
644
|
+
}
|
|
645
|
+
ngOnInit() {
|
|
646
|
+
// Carregar estado salvo (se habilitado)
|
|
647
|
+
const saved = this.rememberState && this.stateKey
|
|
648
|
+
? this.configStorage.loadConfig(this.stateKey)
|
|
649
|
+
: null;
|
|
650
|
+
this.initialSize = {
|
|
651
|
+
width: saved?.width ?? this.modal.width,
|
|
652
|
+
height: saved?.height ?? this.modal.height,
|
|
653
|
+
};
|
|
654
|
+
console.debug('[CRUD:Host] ngOnInit', {
|
|
655
|
+
initialSize: this.initialSize,
|
|
656
|
+
startMaximized: this.modal.startMaximized,
|
|
657
|
+
fullscreenBreakpoint: this.modal.fullscreenBreakpoint,
|
|
658
|
+
});
|
|
659
|
+
let shouldMax = false;
|
|
660
|
+
if (saved && typeof saved.maximized === 'boolean') {
|
|
661
|
+
shouldMax = !!saved.maximized;
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
shouldMax =
|
|
665
|
+
this.modal.startMaximized ||
|
|
666
|
+
(this.modal.fullscreenBreakpoint &&
|
|
667
|
+
window.innerWidth <= this.modal.fullscreenBreakpoint);
|
|
668
|
+
}
|
|
669
|
+
if (shouldMax) {
|
|
670
|
+
this.toggleMaximize(true);
|
|
671
|
+
}
|
|
672
|
+
else if (this.initialSize.width || this.initialSize.height) {
|
|
673
|
+
this.dialogRef.updateSize(this.initialSize.width, this.initialSize.height);
|
|
674
|
+
this.dialogRef.updatePosition();
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
onSave(result) {
|
|
678
|
+
if (this.modal.closeOnSave === false) {
|
|
679
|
+
// Não fechar: manter aberto e opcionalmente salvar estado atual
|
|
680
|
+
this.saveState();
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
this.dialogRef.close({ type: 'save', data: result });
|
|
684
|
+
}
|
|
685
|
+
onCancel() {
|
|
686
|
+
const dirty = this.formComp?.form.dirty;
|
|
687
|
+
const backCfg = (this.data.action?.back || this.data.metadata?.defaults?.back) || {};
|
|
688
|
+
const confirm = backCfg.confirmOnDirty ?? true;
|
|
689
|
+
if (dirty && confirm) {
|
|
690
|
+
const ref = this.dialogService.open(ConfirmDialogComponent, {
|
|
691
|
+
data: {
|
|
692
|
+
title: this.texts.discardTitle,
|
|
693
|
+
message: this.texts.discardMessage,
|
|
694
|
+
confirmText: this.texts.discardConfirm,
|
|
695
|
+
cancelText: this.texts.discardCancel,
|
|
696
|
+
type: 'warning',
|
|
697
|
+
},
|
|
698
|
+
});
|
|
699
|
+
ref
|
|
700
|
+
.afterClosed()
|
|
701
|
+
.pipe(filter((confirmed) => !!confirmed), takeUntilDestroyed(this.destroyRef))
|
|
702
|
+
.subscribe(() => this.dialogRef.close());
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
this.dialogRef.close();
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
toggleMaximize(initial = false) {
|
|
709
|
+
this.maximized = initial ? true : !this.maximized;
|
|
710
|
+
const gap = this.maximized ? this.modal.edgeGap ?? 8 : undefined;
|
|
711
|
+
const width = this.maximized
|
|
712
|
+
? `calc(100vw - ${2 * (gap ?? 0)}px)`
|
|
713
|
+
: this.initialSize.width;
|
|
714
|
+
const height = this.maximized
|
|
715
|
+
? `calc(100dvh - ${2 * (gap ?? 0)}px)`
|
|
716
|
+
: this.initialSize.height;
|
|
717
|
+
this.dialogRef.updateSize(width, height);
|
|
718
|
+
this.dialogRef.updatePosition();
|
|
719
|
+
const pane = this.dialogRef
|
|
720
|
+
?._containerInstance?._elementRef?.nativeElement?.parentElement;
|
|
721
|
+
if (pane && pane.classList.contains('pfx-dialog-pane')) {
|
|
722
|
+
pane.style.margin = this.maximized ? `${gap}px` : '';
|
|
723
|
+
}
|
|
724
|
+
this.saveState();
|
|
725
|
+
}
|
|
726
|
+
saveState() {
|
|
727
|
+
if (!this.rememberState || !this.stateKey)
|
|
728
|
+
return;
|
|
729
|
+
const pane = this.dialogRef
|
|
730
|
+
?._containerInstance?._elementRef?.nativeElement?.parentElement;
|
|
731
|
+
const style = pane ? getComputedStyle(pane) : null;
|
|
732
|
+
const currentWidth = style?.width || this.initialSize.width;
|
|
733
|
+
const currentHeight = style?.height || this.initialSize.height;
|
|
734
|
+
this.configStorage.saveConfig(this.stateKey, {
|
|
735
|
+
maximized: this.maximized,
|
|
736
|
+
width: currentWidth,
|
|
737
|
+
height: currentHeight,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: DynamicFormDialogHostComponent, deps: [{ token: MatDialogRef }, { token: MAT_DIALOG_DATA }, { token: DialogService }, { token: i2.GenericCrudService }, { token: CONFIG_STORAGE }], target: i0.ɵɵFactoryTarget.Component });
|
|
741
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.4", 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: `
|
|
742
|
+
<div class="dialog-header">
|
|
743
|
+
<h2 id="crudDialogTitle" class="dialog-title">
|
|
744
|
+
{{ data.action?.label || texts.title }}
|
|
745
|
+
</h2>
|
|
746
|
+
<span class="spacer"></span>
|
|
747
|
+
@if (modal.canMaximize) {
|
|
748
|
+
<button
|
|
749
|
+
mat-icon-button
|
|
750
|
+
type="button"
|
|
751
|
+
(click)="toggleMaximize()"
|
|
752
|
+
[attr.aria-label]="
|
|
753
|
+
maximized ? texts.restoreLabel : texts.maximizeLabel
|
|
754
|
+
"
|
|
755
|
+
>
|
|
756
|
+
<mat-icon>{{
|
|
757
|
+
maximized ? 'close_fullscreen' : 'open_in_full'
|
|
758
|
+
}}</mat-icon>
|
|
759
|
+
</button>
|
|
760
|
+
}
|
|
761
|
+
<button
|
|
762
|
+
mat-icon-button
|
|
763
|
+
type="button"
|
|
764
|
+
(click)="onCancel()"
|
|
765
|
+
[attr.aria-label]="texts.closeLabel"
|
|
766
|
+
cdkFocusInitial
|
|
767
|
+
>
|
|
768
|
+
<mat-icon [praxisIcon]="'close'"></mat-icon>
|
|
769
|
+
</button>
|
|
770
|
+
</div>
|
|
771
|
+
|
|
772
|
+
<mat-dialog-content
|
|
773
|
+
class="dialog-content"
|
|
774
|
+
aria-labelledby="crudDialogTitle"
|
|
775
|
+
>
|
|
776
|
+
<praxis-dynamic-form
|
|
777
|
+
[formId]="data.action?.formId"
|
|
778
|
+
[resourcePath]="resourcePath"
|
|
779
|
+
[resourceId]="resourceId"
|
|
780
|
+
[mode]="mode"
|
|
781
|
+
[backConfig]="backConfig"
|
|
782
|
+
(formSubmit)="onSave($event)"
|
|
783
|
+
(formCancel)="onCancel()"
|
|
784
|
+
></praxis-dynamic-form>
|
|
785
|
+
</mat-dialog-content>
|
|
786
|
+
`, 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);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:color-mix(in srgb,var(--md-sys-color-primary),transparent 90%)}.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.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "component", type: PraxisDynamicForm, selector: "praxis-dynamic-form", inputs: ["resourcePath", "resourceId", "mode", "config", "schemaSource", "editModeEnabled", "formId", "layout", "backConfig", "hooks", "removeEmptyContainersOnSave", "reactiveValidation", "reactiveValidationDebounceMs", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "readonlyModeGlobal", "disabledModeGlobal", "presentationModeGlobal", "visibleGlobal", "customEndpoints"], outputs: ["formSubmit", "formCancel", "formReset", "configChange", "formReady", "valueChange", "syncCompleted", "initializationError", "editModeEnabledChange", "customAction", "actionConfirmation", "schemaStatusChange"] }] });
|
|
787
|
+
}
|
|
788
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: DynamicFormDialogHostComponent, decorators: [{
|
|
789
|
+
type: Component,
|
|
790
|
+
args: [{ selector: 'praxis-dynamic-form-dialog-host', standalone: true, imports: [
|
|
791
|
+
CommonModule,
|
|
792
|
+
MatDialogModule,
|
|
793
|
+
MatButtonModule,
|
|
794
|
+
MatIconModule,
|
|
795
|
+
PraxisIconDirective,
|
|
796
|
+
PraxisDynamicForm,
|
|
797
|
+
], providers: [GenericCrudService], host: {
|
|
798
|
+
class: 'praxis-dialog',
|
|
799
|
+
'[attr.data-density]': 'modal.density || "default"',
|
|
800
|
+
}, template: `
|
|
801
|
+
<div class="dialog-header">
|
|
802
|
+
<h2 id="crudDialogTitle" class="dialog-title">
|
|
803
|
+
{{ data.action?.label || texts.title }}
|
|
804
|
+
</h2>
|
|
805
|
+
<span class="spacer"></span>
|
|
806
|
+
@if (modal.canMaximize) {
|
|
807
|
+
<button
|
|
808
|
+
mat-icon-button
|
|
809
|
+
type="button"
|
|
810
|
+
(click)="toggleMaximize()"
|
|
811
|
+
[attr.aria-label]="
|
|
812
|
+
maximized ? texts.restoreLabel : texts.maximizeLabel
|
|
813
|
+
"
|
|
814
|
+
>
|
|
815
|
+
<mat-icon>{{
|
|
816
|
+
maximized ? 'close_fullscreen' : 'open_in_full'
|
|
817
|
+
}}</mat-icon>
|
|
818
|
+
</button>
|
|
819
|
+
}
|
|
820
|
+
<button
|
|
821
|
+
mat-icon-button
|
|
822
|
+
type="button"
|
|
823
|
+
(click)="onCancel()"
|
|
824
|
+
[attr.aria-label]="texts.closeLabel"
|
|
825
|
+
cdkFocusInitial
|
|
826
|
+
>
|
|
827
|
+
<mat-icon [praxisIcon]="'close'"></mat-icon>
|
|
828
|
+
</button>
|
|
829
|
+
</div>
|
|
830
|
+
|
|
831
|
+
<mat-dialog-content
|
|
832
|
+
class="dialog-content"
|
|
833
|
+
aria-labelledby="crudDialogTitle"
|
|
834
|
+
>
|
|
835
|
+
<praxis-dynamic-form
|
|
836
|
+
[formId]="data.action?.formId"
|
|
837
|
+
[resourcePath]="resourcePath"
|
|
838
|
+
[resourceId]="resourceId"
|
|
839
|
+
[mode]="mode"
|
|
840
|
+
[backConfig]="backConfig"
|
|
841
|
+
(formSubmit)="onSave($event)"
|
|
842
|
+
(formCancel)="onCancel()"
|
|
843
|
+
></praxis-dynamic-form>
|
|
844
|
+
</mat-dialog-content>
|
|
845
|
+
`, 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);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:color-mix(in srgb,var(--md-sys-color-primary),transparent 90%)}.dialog-footer{position:sticky;bottom:0;z-index:1;padding:var(--dlg-pad)}\n"] }]
|
|
846
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
847
|
+
type: Inject,
|
|
848
|
+
args: [MatDialogRef]
|
|
849
|
+
}] }, { type: undefined, decorators: [{
|
|
850
|
+
type: Inject,
|
|
851
|
+
args: [MAT_DIALOG_DATA]
|
|
852
|
+
}] }, { type: DialogService }, { type: i2.GenericCrudService }, { type: undefined, decorators: [{
|
|
853
|
+
type: Inject,
|
|
854
|
+
args: [CONFIG_STORAGE]
|
|
855
|
+
}] }], propDecorators: { formComp: [{
|
|
856
|
+
type: ViewChild,
|
|
857
|
+
args: [PraxisDynamicForm]
|
|
858
|
+
}] } });
|
|
859
|
+
|
|
860
|
+
var dynamicFormDialogHost_component = /*#__PURE__*/Object.freeze({
|
|
861
|
+
__proto__: null,
|
|
862
|
+
DynamicFormDialogHostComponent: DynamicFormDialogHostComponent
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
/** Metadata for PraxisCrudComponent */
|
|
866
|
+
const PRAXIS_CRUD_COMPONENT_METADATA = {
|
|
867
|
+
id: 'praxis-crud',
|
|
868
|
+
selector: 'praxis-crud',
|
|
869
|
+
component: PraxisCrudComponent,
|
|
870
|
+
friendlyName: 'Praxis CRUD',
|
|
871
|
+
description: 'Tabela com operações de CRUD via metadados.',
|
|
872
|
+
icon: 'table_chart',
|
|
873
|
+
inputs: [
|
|
874
|
+
{
|
|
875
|
+
name: 'metadata',
|
|
876
|
+
type: 'CrudMetadata | string',
|
|
877
|
+
description: 'Metadados de configuração do CRUD',
|
|
878
|
+
},
|
|
879
|
+
{
|
|
880
|
+
name: 'context',
|
|
881
|
+
type: 'Record<string, unknown>',
|
|
882
|
+
description: 'Contexto adicional para resoluções',
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
name: 'editModeEnabled',
|
|
886
|
+
type: 'boolean',
|
|
887
|
+
default: false,
|
|
888
|
+
description: 'Habilita modo de edição do layout',
|
|
889
|
+
},
|
|
890
|
+
],
|
|
891
|
+
outputs: [
|
|
892
|
+
{
|
|
893
|
+
name: 'afterOpen',
|
|
894
|
+
type: '{ mode: FormOpenMode; action: string }',
|
|
895
|
+
description: 'Emitido após abrir diálogo',
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
name: 'afterClose',
|
|
899
|
+
type: 'void',
|
|
900
|
+
description: 'Emitido após fechar diálogo',
|
|
901
|
+
},
|
|
902
|
+
{
|
|
903
|
+
name: 'afterSave',
|
|
904
|
+
type: '{ id: string | number; data: unknown }',
|
|
905
|
+
description: 'Emitido ao salvar',
|
|
906
|
+
},
|
|
907
|
+
{
|
|
908
|
+
name: 'afterDelete',
|
|
909
|
+
type: '{ id: string | number }',
|
|
910
|
+
description: 'Emitido ao deletar',
|
|
911
|
+
},
|
|
912
|
+
{ name: 'error', type: 'unknown', description: 'Emitido em erros' },
|
|
913
|
+
{
|
|
914
|
+
name: 'configureRequested',
|
|
915
|
+
type: 'void',
|
|
916
|
+
description: 'Emitido quando CTA de configuração é acionado em modo edição',
|
|
917
|
+
},
|
|
918
|
+
],
|
|
919
|
+
tags: ['widget', 'crud', 'configurable', 'hasWizard', 'stable'],
|
|
920
|
+
lib: '@praxisui/crud',
|
|
921
|
+
};
|
|
922
|
+
/** Provider para auto-registrar metadados do componente CRUD. */
|
|
923
|
+
function providePraxisCrudMetadata() {
|
|
924
|
+
return {
|
|
925
|
+
provide: ENVIRONMENT_INITIALIZER,
|
|
926
|
+
multi: true,
|
|
927
|
+
useFactory: (registry) => () => {
|
|
928
|
+
registry.register(PRAXIS_CRUD_COMPONENT_METADATA);
|
|
929
|
+
},
|
|
930
|
+
deps: [ComponentMetadataRegistry],
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
class CrudPageHeaderComponent {
|
|
935
|
+
title = '';
|
|
936
|
+
backLabel;
|
|
937
|
+
showBack = true;
|
|
938
|
+
variant = 'ghost';
|
|
939
|
+
sticky = true;
|
|
940
|
+
divider = true;
|
|
941
|
+
returnTo;
|
|
942
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: CrudPageHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
943
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.4", type: CrudPageHeaderComponent, isStandalone: true, selector: "praxis-crud-page-header", inputs: { title: "title", backLabel: "backLabel", showBack: "showBack", variant: "variant", sticky: "sticky", divider: "divider", returnTo: "returnTo" }, ngImport: i0, template: `
|
|
944
|
+
<header
|
|
945
|
+
class="crud-header"
|
|
946
|
+
[class.sticky]="sticky"
|
|
947
|
+
[class.with-divider]="divider"
|
|
948
|
+
>
|
|
949
|
+
<div class="left">
|
|
950
|
+
<a
|
|
951
|
+
*ngIf="showBack && returnTo"
|
|
952
|
+
class="back-btn"
|
|
953
|
+
[class.ghost]="variant === 'ghost'"
|
|
954
|
+
[class.tonal]="variant === 'tonal'"
|
|
955
|
+
[class.outlined]="variant === 'outlined'"
|
|
956
|
+
[routerLink]="returnTo"
|
|
957
|
+
[attr.aria-label]="backLabel || 'Voltar'"
|
|
958
|
+
mat-button
|
|
959
|
+
>
|
|
960
|
+
<mat-icon [praxisIcon]="'arrow_back'"></mat-icon>
|
|
961
|
+
<span class="label" [class.hide-on-narrow]="true">{{ backLabel || 'Voltar' }}</span>
|
|
962
|
+
</a>
|
|
963
|
+
<h2 class="title" [attr.title]="title">{{ title }}</h2>
|
|
964
|
+
</div>
|
|
965
|
+
<div class="right">
|
|
966
|
+
<ng-content></ng-content>
|
|
967
|
+
</div>
|
|
968
|
+
</header>
|
|
969
|
+
`, 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, transparent)}.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, rgba(0,0,0,.08))}.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:color-mix(in srgb,var(--md-sys-color-primary) 8%,transparent)}.back-btn.tonal{border-radius:20px;padding:4px 10px;background:var(--md-sys-color-surface-container, rgba(0,0,0,.02));box-shadow:inset 0 0 0 1px var(--md-sys-color-outline-variant, rgba(0,0,0,.06))}.back-btn.tonal:hover{background:color-mix(in srgb,var(--md-sys-color-primary) 10%,var(--md-sys-color-surface-container))}.back-btn.outlined{border-radius:20px;padding:4px 10px;border:1px solid var(--md-sys-color-outline-variant, rgba(0,0,0,.12));background:var(--md-sys-color-surface, transparent)}.back-btn.outlined:hover{border-color:color-mix(in srgb,var(--md-sys-color-primary) 40%,var(--md-sys-color-outline-variant));background:color-mix(in srgb,var(--md-sys-color-primary) 6%,transparent)}@media (max-width: 599px){.label.hide-on-narrow{display:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }] });
|
|
970
|
+
}
|
|
971
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: CrudPageHeaderComponent, decorators: [{
|
|
972
|
+
type: Component,
|
|
973
|
+
args: [{ selector: 'praxis-crud-page-header', standalone: true, imports: [CommonModule, RouterLink, MatButtonModule, MatIconModule, PraxisIconDirective], template: `
|
|
974
|
+
<header
|
|
975
|
+
class="crud-header"
|
|
976
|
+
[class.sticky]="sticky"
|
|
977
|
+
[class.with-divider]="divider"
|
|
978
|
+
>
|
|
979
|
+
<div class="left">
|
|
980
|
+
<a
|
|
981
|
+
*ngIf="showBack && returnTo"
|
|
982
|
+
class="back-btn"
|
|
983
|
+
[class.ghost]="variant === 'ghost'"
|
|
984
|
+
[class.tonal]="variant === 'tonal'"
|
|
985
|
+
[class.outlined]="variant === 'outlined'"
|
|
986
|
+
[routerLink]="returnTo"
|
|
987
|
+
[attr.aria-label]="backLabel || 'Voltar'"
|
|
988
|
+
mat-button
|
|
989
|
+
>
|
|
990
|
+
<mat-icon [praxisIcon]="'arrow_back'"></mat-icon>
|
|
991
|
+
<span class="label" [class.hide-on-narrow]="true">{{ backLabel || 'Voltar' }}</span>
|
|
992
|
+
</a>
|
|
993
|
+
<h2 class="title" [attr.title]="title">{{ title }}</h2>
|
|
994
|
+
</div>
|
|
995
|
+
<div class="right">
|
|
996
|
+
<ng-content></ng-content>
|
|
997
|
+
</div>
|
|
998
|
+
</header>
|
|
999
|
+
`, styles: [".crud-header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 0;background:var(--md-sys-color-surface, transparent)}.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, rgba(0,0,0,.08))}.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:color-mix(in srgb,var(--md-sys-color-primary) 8%,transparent)}.back-btn.tonal{border-radius:20px;padding:4px 10px;background:var(--md-sys-color-surface-container, rgba(0,0,0,.02));box-shadow:inset 0 0 0 1px var(--md-sys-color-outline-variant, rgba(0,0,0,.06))}.back-btn.tonal:hover{background:color-mix(in srgb,var(--md-sys-color-primary) 10%,var(--md-sys-color-surface-container))}.back-btn.outlined{border-radius:20px;padding:4px 10px;border:1px solid var(--md-sys-color-outline-variant, rgba(0,0,0,.12));background:var(--md-sys-color-surface, transparent)}.back-btn.outlined:hover{border-color:color-mix(in srgb,var(--md-sys-color-primary) 40%,var(--md-sys-color-outline-variant));background:color-mix(in srgb,var(--md-sys-color-primary) 6%,transparent)}@media (max-width: 599px){.label.hide-on-narrow{display:none}}\n"] }]
|
|
1000
|
+
}], propDecorators: { title: [{
|
|
1001
|
+
type: Input
|
|
1002
|
+
}], backLabel: [{
|
|
1003
|
+
type: Input
|
|
1004
|
+
}], showBack: [{
|
|
1005
|
+
type: Input
|
|
1006
|
+
}], variant: [{
|
|
1007
|
+
type: Input
|
|
1008
|
+
}], sticky: [{
|
|
1009
|
+
type: Input
|
|
1010
|
+
}], divider: [{
|
|
1011
|
+
type: Input
|
|
1012
|
+
}], returnTo: [{
|
|
1013
|
+
type: Input
|
|
1014
|
+
}] } });
|
|
1015
|
+
|
|
1016
|
+
/*
|
|
1017
|
+
* Public API Surface of praxis-crud
|
|
1018
|
+
*/
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Generated bundle index. Do not edit.
|
|
1022
|
+
*/
|
|
1023
|
+
|
|
1024
|
+
export { CRUD_DRAWER_ADAPTER, CrudLauncherService, CrudPageHeaderComponent, DialogService, DynamicFormDialogHostComponent, PRAXIS_CRUD_COMPONENT_METADATA, PraxisCrudComponent, assertCrudMetadata, providePraxisCrudMetadata };
|
|
1025
|
+
//# sourceMappingURL=praxisui-crud.mjs.map
|