generate-ui-cli 2.2.0 → 2.3.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 +177 -35
- package/dist/commands/angular.js +343 -57
- package/dist/commands/generate.js +242 -30
- package/dist/commands/login.js +87 -25
- package/dist/commands/merge.js +201 -0
- package/dist/generators/angular/feature.generator.js +2262 -637
- package/dist/generators/angular/menu.generator.js +1 -1
- package/dist/generators/angular/routes.generator.js +8 -3
- package/dist/generators/screen.generator.js +100 -5
- package/dist/generators/screen.merge.js +70 -15
- package/dist/index.js +69 -8
- package/dist/license/guard.js +2 -1
- package/dist/license/permissions.js +62 -8
- package/dist/license/token.js +19 -0
- package/dist/postinstall.js +42 -2
- package/dist/runtime/config.js +4 -0
- package/dist/runtime/project-config.js +64 -0
- package/package.json +1 -1
|
@@ -4,16 +4,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.generateFeature = generateFeature;
|
|
7
|
+
exports.generateAdminFeature = generateAdminFeature;
|
|
7
8
|
const fs_1 = __importDefault(require("fs"));
|
|
8
9
|
const path_1 = __importDefault(require("path"));
|
|
9
|
-
function generateFeature(schema,
|
|
10
|
+
function generateFeature(schema, featuresRoot, generatedRoot, schemasRoot) {
|
|
10
11
|
const rawName = schema.api.operationId;
|
|
11
12
|
const name = toPascalCase(rawName);
|
|
12
13
|
const folder = toFolderName(name);
|
|
13
14
|
const fileBase = toFileBase(name);
|
|
14
|
-
const featureDir = path_1.default.join(
|
|
15
|
+
const featureDir = path_1.default.join(generatedRoot, folder);
|
|
15
16
|
fs_1.default.mkdirSync(featureDir, { recursive: true });
|
|
16
|
-
const appRoot = path_1.default.resolve(
|
|
17
|
+
const appRoot = path_1.default.resolve(featuresRoot, '..');
|
|
17
18
|
ensureUiComponents(appRoot, schemasRoot);
|
|
18
19
|
const method = String(schema.api.method || '').toLowerCase();
|
|
19
20
|
const endpoint = String(schema.api.endpoint || '');
|
|
@@ -35,6 +36,7 @@ function generateFeature(schema, root, schemasRoot) {
|
|
|
35
36
|
}));
|
|
36
37
|
const includeBody = ['post', 'put', 'patch'].includes(method);
|
|
37
38
|
const includeParams = pathParams.length > 0 || queryParams.length > 0;
|
|
39
|
+
const shouldAutoRefresh = method === 'get' && !includeParams && !includeBody;
|
|
38
40
|
const formFields = [
|
|
39
41
|
...(includeParams
|
|
40
42
|
? [...paramFields, ...normalizedQueryFields]
|
|
@@ -47,24 +49,32 @@ function generateFeature(schema, root, schemasRoot) {
|
|
|
47
49
|
? String(schema.entity).trim()
|
|
48
50
|
: rawName;
|
|
49
51
|
const subtitle = `${method.toUpperCase()} ${endpoint}`;
|
|
52
|
+
const responseFormat = resolveResponseFormat(schema);
|
|
50
53
|
const schemaImportPath = buildSchemaImportPath(featureDir, schemasRoot, rawName);
|
|
51
54
|
/**
|
|
52
55
|
* 1️⃣ Component (sempre sobrescreve)
|
|
53
56
|
*/
|
|
54
57
|
const componentPath = path_1.default.join(featureDir, `${fileBase}.component.ts`);
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
const uiCardImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-card', 'ui-card.component'));
|
|
59
|
+
const uiButtonImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-button', 'ui-button.component'));
|
|
60
|
+
const uiSelectImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-select', 'ui-select.component'));
|
|
61
|
+
const uiCheckboxImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-checkbox', 'ui-checkbox.component'));
|
|
62
|
+
const uiInputImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-input', 'ui-input.component'));
|
|
63
|
+
const uiTextareaImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-textarea', 'ui-textarea.component'));
|
|
64
|
+
const generatedComponentSource = `
|
|
65
|
+
import { Component, OnDestroy, OnInit, AfterViewInit } from '@angular/core'
|
|
57
66
|
import { CommonModule } from '@angular/common'
|
|
58
67
|
import { FormBuilder, ReactiveFormsModule } from '@angular/forms'
|
|
59
|
-
import { UiCardComponent } from '
|
|
60
|
-
import { UiButtonComponent } from '
|
|
61
|
-
import { UiSelectComponent } from '
|
|
62
|
-
import { UiCheckboxComponent } from '
|
|
63
|
-
import { UiInputComponent } from '
|
|
64
|
-
import { UiTextareaComponent } from '
|
|
68
|
+
import { UiCardComponent } from '${uiCardImport}'
|
|
69
|
+
import { UiButtonComponent } from '${uiButtonImport}'
|
|
70
|
+
import { UiSelectComponent } from '${uiSelectImport}'
|
|
71
|
+
import { UiCheckboxComponent } from '${uiCheckboxImport}'
|
|
72
|
+
import { UiInputComponent } from '${uiInputImport}'
|
|
73
|
+
import { UiTextareaComponent } from '${uiTextareaImport}'
|
|
65
74
|
import { ${name}Service } from './${fileBase}.service.gen'
|
|
66
75
|
import { ${name}Gen } from './${fileBase}.gen'
|
|
67
76
|
import screenSchema from '${schemaImportPath}'
|
|
77
|
+
import { BehaviorSubject } from 'rxjs'
|
|
68
78
|
|
|
69
79
|
@Component({
|
|
70
80
|
selector: 'app-${toKebab(name)}',
|
|
@@ -82,13 +92,77 @@ import screenSchema from '${schemaImportPath}'
|
|
|
82
92
|
templateUrl: './${fileBase}.component.html',
|
|
83
93
|
styleUrls: ['./${fileBase}.component.scss']
|
|
84
94
|
})
|
|
85
|
-
export class ${name}Component extends ${name}Gen {
|
|
95
|
+
export class ${name}Component extends ${name}Gen implements OnInit, AfterViewInit, OnDestroy {
|
|
96
|
+
private readonly autoRefresh = ${shouldAutoRefresh}
|
|
97
|
+
private readonly allowDeepArraySearch = ${method === 'get' && includeParams && !includeBody ? 'false' : 'true'}
|
|
98
|
+
private readonly onFocus = () => {
|
|
99
|
+
if (this.autoRefresh && !this.loading) {
|
|
100
|
+
this.submit()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
private readonly onVisibility = () => {
|
|
104
|
+
if (
|
|
105
|
+
this.autoRefresh &&
|
|
106
|
+
typeof document !== 'undefined' &&
|
|
107
|
+
document.visibilityState === 'visible'
|
|
108
|
+
) {
|
|
109
|
+
this.submit()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
86
113
|
constructor(
|
|
87
114
|
protected override fb: FormBuilder,
|
|
88
115
|
protected override service: ${name}Service
|
|
89
116
|
) {
|
|
90
117
|
super(fb, service)
|
|
91
118
|
this.setSchema(screenSchema as any)
|
|
119
|
+
this.applyPrefill()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
ngOnInit() {
|
|
123
|
+
this.ensureInitialLoad()
|
|
124
|
+
this.setupAutoRefreshListeners()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
ngAfterViewInit() {
|
|
128
|
+
this.ensureInitialLoad()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
ngOnDestroy() {
|
|
132
|
+
if (!this.autoRefresh) return
|
|
133
|
+
if (typeof window !== 'undefined') {
|
|
134
|
+
window.removeEventListener('focus', this.onFocus)
|
|
135
|
+
}
|
|
136
|
+
if (typeof document !== 'undefined') {
|
|
137
|
+
document.removeEventListener('visibilitychange', this.onVisibility)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private setupAutoRefreshListeners() {
|
|
142
|
+
if (!this.autoRefresh) return
|
|
143
|
+
if (typeof window !== 'undefined') {
|
|
144
|
+
window.addEventListener('focus', this.onFocus)
|
|
145
|
+
}
|
|
146
|
+
if (typeof document !== 'undefined') {
|
|
147
|
+
document.addEventListener('visibilitychange', this.onVisibility)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private ensureInitialLoad() {
|
|
152
|
+
if (!this.autoRefresh) return
|
|
153
|
+
if (this.loading || this.result$.value !== null) return
|
|
154
|
+
this.submit()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private applyPrefill() {
|
|
158
|
+
const state = (history as any)?.state
|
|
159
|
+
const prefill = state?.prefill
|
|
160
|
+
if (prefill) {
|
|
161
|
+
this.form.patchValue(prefill)
|
|
162
|
+
if (state?.autoSubmit) {
|
|
163
|
+
this.submit()
|
|
164
|
+
}
|
|
165
|
+
}
|
|
92
166
|
}
|
|
93
167
|
|
|
94
168
|
submit() {
|
|
@@ -98,32 +172,32 @@ export class ${name}Component extends ${name}Gen {
|
|
|
98
172
|
const body = this.pick(value, this.bodyFieldNames)
|
|
99
173
|
|
|
100
174
|
this.loading = true
|
|
101
|
-
this.error
|
|
175
|
+
this.error$.next(null)
|
|
102
176
|
|
|
103
177
|
this.service
|
|
104
178
|
.execute(pathParams, queryParams, body)
|
|
105
179
|
.subscribe({
|
|
106
|
-
next: result => {
|
|
180
|
+
next: (result: any) => {
|
|
107
181
|
const normalized =
|
|
108
182
|
result && typeof result === 'object' && 'body' in result
|
|
109
183
|
? (result as any).body
|
|
110
184
|
: result
|
|
111
|
-
this.result
|
|
185
|
+
this.result$.next(normalized)
|
|
112
186
|
this.loading = false
|
|
113
187
|
},
|
|
114
|
-
error: error => {
|
|
115
|
-
this.error
|
|
188
|
+
error: (error: any) => {
|
|
189
|
+
this.error$.next(error)
|
|
116
190
|
this.loading = false
|
|
117
191
|
}
|
|
118
192
|
})
|
|
119
193
|
}
|
|
120
194
|
|
|
121
|
-
isArrayResult() {
|
|
122
|
-
return this.getRows().length > 0
|
|
195
|
+
isArrayResult(raw?: any) {
|
|
196
|
+
return this.getRows(raw).length > 0
|
|
123
197
|
}
|
|
124
198
|
|
|
125
|
-
getRows() {
|
|
126
|
-
const value = this.unwrapResult(this.result)
|
|
199
|
+
getRows(raw?: any) {
|
|
200
|
+
const value = this.unwrapResult(raw ?? this.result$.value)
|
|
127
201
|
if (Array.isArray(value)) return value
|
|
128
202
|
if (!value || typeof value !== 'object') return []
|
|
129
203
|
|
|
@@ -132,20 +206,57 @@ export class ${name}Component extends ${name}Gen {
|
|
|
132
206
|
if (Array.isArray(value[key])) return value[key]
|
|
133
207
|
}
|
|
134
208
|
|
|
209
|
+
if (!this.allowDeepArraySearch) return []
|
|
135
210
|
const found = this.findFirstArray(value, 0, 5)
|
|
136
211
|
return found ?? []
|
|
137
212
|
}
|
|
138
213
|
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
if (
|
|
142
|
-
|
|
214
|
+
private getConfiguredColumns() {
|
|
215
|
+
const columns = this.schema?.data?.table?.columns
|
|
216
|
+
if (!Array.isArray(columns)) return []
|
|
217
|
+
return columns
|
|
218
|
+
.map((entry: any) => {
|
|
219
|
+
if (typeof entry === 'string') {
|
|
220
|
+
return { key: entry, label: '', visible: true }
|
|
221
|
+
}
|
|
222
|
+
if (entry && typeof entry === 'object') {
|
|
223
|
+
const key = String(
|
|
224
|
+
entry.key ?? entry.id ?? entry.column ?? ''
|
|
225
|
+
).trim()
|
|
226
|
+
if (!key) return null
|
|
227
|
+
const label =
|
|
228
|
+
typeof entry.label === 'string' ? entry.label : ''
|
|
229
|
+
const visible = entry.visible !== false
|
|
230
|
+
return { key, label, visible }
|
|
231
|
+
}
|
|
232
|
+
return null
|
|
233
|
+
})
|
|
234
|
+
.filter(Boolean) as Array<{ key: string; label: string; visible: boolean }>
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private getColumnLabel(value: string) {
|
|
238
|
+
const configured = this.getConfiguredColumns()
|
|
239
|
+
const match = configured.find(column => column.key === value)
|
|
240
|
+
return match?.label ?? ''
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
getColumns(value?: any) {
|
|
244
|
+
const configured = this.getConfiguredColumns()
|
|
245
|
+
if (configured.length) {
|
|
246
|
+
return configured
|
|
247
|
+
.filter(column => column.visible)
|
|
248
|
+
.map(column => column.key)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const fieldsRaw = this.form.get('fields')?.value
|
|
252
|
+
if (typeof fieldsRaw === 'string' && fieldsRaw.trim().length > 0) {
|
|
253
|
+
return fieldsRaw
|
|
143
254
|
.split(',')
|
|
144
255
|
.map((value: string) => value.trim())
|
|
145
256
|
.filter(Boolean)
|
|
146
257
|
}
|
|
147
258
|
|
|
148
|
-
const rows = this.getRows()
|
|
259
|
+
const rows = this.getRows(value)
|
|
149
260
|
if (rows.length > 0 && rows[0] && typeof rows[0] === 'object') {
|
|
150
261
|
return Object.keys(rows[0])
|
|
151
262
|
}
|
|
@@ -154,6 +265,8 @@ export class ${name}Component extends ${name}Gen {
|
|
|
154
265
|
}
|
|
155
266
|
|
|
156
267
|
formatHeader(value: string) {
|
|
268
|
+
const configured = this.getColumnLabel(value)
|
|
269
|
+
if (configured) return configured
|
|
157
270
|
return value
|
|
158
271
|
.replace(/[_-]/g, ' ')
|
|
159
272
|
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
@@ -205,20 +318,64 @@ export class ${name}Component extends ${name}Gen {
|
|
|
205
318
|
return String(value)
|
|
206
319
|
}
|
|
207
320
|
|
|
208
|
-
|
|
209
|
-
|
|
321
|
+
getCardImage(row: any) {
|
|
322
|
+
if (!row || typeof row !== 'object') return ''
|
|
323
|
+
const directKeys = ['thumbnail', 'image', 'avatar', 'photo', 'picture']
|
|
324
|
+
for (const key of directKeys) {
|
|
325
|
+
if (typeof row[key] === 'string') return row[key]
|
|
326
|
+
}
|
|
327
|
+
if (Array.isArray(row.images) && row.images.length) {
|
|
328
|
+
return row.images[0]
|
|
329
|
+
}
|
|
330
|
+
return ''
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
getCardTitle(row: any) {
|
|
334
|
+
if (!row || typeof row !== 'object') return 'Item'
|
|
335
|
+
return (
|
|
336
|
+
row.title ??
|
|
337
|
+
row.name ??
|
|
338
|
+
row.label ??
|
|
339
|
+
row.id ??
|
|
340
|
+
'Item'
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
getCardSubtitle(row: any) {
|
|
345
|
+
if (!row || typeof row !== 'object') return ''
|
|
346
|
+
return (
|
|
347
|
+
row.description ??
|
|
348
|
+
row.category ??
|
|
349
|
+
row.brand ??
|
|
350
|
+
''
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
formatError(error: any) {
|
|
355
|
+
if (!error) return ''
|
|
356
|
+
if (typeof error === 'string') return error
|
|
357
|
+
if (typeof error?.message === 'string') return error.message
|
|
358
|
+
try {
|
|
359
|
+
return JSON.stringify(error)
|
|
360
|
+
} catch {
|
|
361
|
+
return String(error)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
getObjectRows(raw?: any) {
|
|
366
|
+
const value = this.unwrapResult(raw ?? this.result$.value)
|
|
210
367
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
211
368
|
return []
|
|
212
369
|
}
|
|
213
370
|
return this.flattenObject(value)
|
|
214
371
|
}
|
|
215
372
|
|
|
216
|
-
hasObjectRows() {
|
|
217
|
-
return this.getObjectRows().length > 0
|
|
373
|
+
hasObjectRows(raw?: any) {
|
|
374
|
+
return this.getObjectRows(raw).length > 0
|
|
218
375
|
}
|
|
219
376
|
|
|
220
|
-
isSingleValue() {
|
|
221
|
-
const value = this.unwrapResult(this.result)
|
|
377
|
+
isSingleValue(raw?: any) {
|
|
378
|
+
const value = this.unwrapResult(raw ?? this.result$.value)
|
|
222
379
|
return (
|
|
223
380
|
value !== null &&
|
|
224
381
|
value !== undefined &&
|
|
@@ -275,7 +432,8 @@ export class ${name}Component extends ${name}Gen {
|
|
|
275
432
|
}
|
|
276
433
|
|
|
277
434
|
}
|
|
278
|
-
|
|
435
|
+
`;
|
|
436
|
+
fs_1.default.writeFileSync(componentPath, generatedComponentSource);
|
|
279
437
|
/**
|
|
280
438
|
* 2️⃣ Arquivo gerado (sempre sobrescreve)
|
|
281
439
|
*/
|
|
@@ -283,6 +441,7 @@ export class ${name}Component extends ${name}Gen {
|
|
|
283
441
|
fs_1.default.writeFileSync(genTsPath, `
|
|
284
442
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms'
|
|
285
443
|
import { Injectable } from '@angular/core'
|
|
444
|
+
import { BehaviorSubject } from 'rxjs'
|
|
286
445
|
import { ${name}Service } from './${fileBase}.service.gen'
|
|
287
446
|
|
|
288
447
|
@Injectable()
|
|
@@ -295,8 +454,8 @@ export class ${name}Gen {
|
|
|
295
454
|
schema: any
|
|
296
455
|
|
|
297
456
|
loading = false
|
|
298
|
-
result
|
|
299
|
-
error
|
|
457
|
+
readonly result$ = new BehaviorSubject<any>(null)
|
|
458
|
+
readonly error$ = new BehaviorSubject<any>(null)
|
|
300
459
|
|
|
301
460
|
constructor(
|
|
302
461
|
protected fb: FormBuilder,
|
|
@@ -506,163 +665,1695 @@ export class ${name}Service {
|
|
|
506
665
|
formFields,
|
|
507
666
|
actionLabel,
|
|
508
667
|
method,
|
|
509
|
-
hasForm: formFields.length > 0
|
|
668
|
+
hasForm: formFields.length > 0,
|
|
669
|
+
responseFormat
|
|
510
670
|
}));
|
|
511
671
|
/**
|
|
512
672
|
* 5️⃣ SCSS base
|
|
513
673
|
*/
|
|
514
674
|
const scssPath = path_1.default.join(featureDir, `${fileBase}.component.scss`);
|
|
515
|
-
fs_1.default.writeFileSync(scssPath,
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
675
|
+
fs_1.default.writeFileSync(scssPath, buildBaseScss());
|
|
676
|
+
return {
|
|
677
|
+
path: toRouteSegment(name),
|
|
678
|
+
component: `${name}Component`,
|
|
679
|
+
folder,
|
|
680
|
+
fileBase
|
|
681
|
+
};
|
|
520
682
|
}
|
|
683
|
+
function generateAdminFeature(schema, schemaByOpId, featuresRoot, generatedRoot, schemasRoot) {
|
|
684
|
+
const smart = schema?.meta?.intelligent ?? {};
|
|
685
|
+
const adminOpId = String(schema?.api?.operationId || '');
|
|
686
|
+
const name = toPascalCase(adminOpId);
|
|
687
|
+
const folder = toFolderName(name);
|
|
688
|
+
const fileBase = toFileBase(name);
|
|
689
|
+
const featureDir = path_1.default.join(generatedRoot, folder);
|
|
690
|
+
fs_1.default.mkdirSync(featureDir, { recursive: true });
|
|
691
|
+
const appRoot = path_1.default.resolve(featuresRoot, '..');
|
|
692
|
+
ensureUiComponents(appRoot, schemasRoot);
|
|
693
|
+
const listOpId = smart.listOperationId;
|
|
694
|
+
const detailOpId = smart.detailOperationId;
|
|
695
|
+
const updateOpId = smart.updateOperationId;
|
|
696
|
+
const deleteOpId = smart.deleteOperationId;
|
|
697
|
+
const listSchema = listOpId ? schemaByOpId.get(listOpId) : null;
|
|
698
|
+
const detailSchema = detailOpId ? schemaByOpId.get(detailOpId) : null;
|
|
699
|
+
const updateSchema = updateOpId ? schemaByOpId.get(updateOpId) : null;
|
|
700
|
+
const deleteSchema = deleteOpId ? schemaByOpId.get(deleteOpId) : null;
|
|
701
|
+
const listName = listOpId ? toPascalCase(listOpId) : '';
|
|
702
|
+
const detailName = detailOpId ? toPascalCase(detailOpId) : '';
|
|
703
|
+
const updateName = updateOpId ? toPascalCase(updateOpId) : '';
|
|
704
|
+
const deleteName = deleteOpId ? toPascalCase(deleteOpId) : '';
|
|
705
|
+
const listFileBase = listOpId ? toFileBase(listName) : '';
|
|
706
|
+
const detailFileBase = detailOpId ? toFileBase(detailName) : '';
|
|
707
|
+
const updateFileBase = updateOpId ? toFileBase(updateName) : '';
|
|
708
|
+
const deleteFileBase = deleteOpId ? toFileBase(deleteName) : '';
|
|
709
|
+
const listFolder = listOpId ? toFolderName(listName) : '';
|
|
710
|
+
const detailFolder = detailOpId ? toFolderName(detailName) : '';
|
|
711
|
+
const updateFolder = updateOpId ? toFolderName(updateName) : '';
|
|
712
|
+
const deleteFolder = deleteOpId ? toFolderName(deleteName) : '';
|
|
713
|
+
const listServicePath = listOpId
|
|
714
|
+
? buildRelativeImportPath(featureDir, path_1.default.join(generatedRoot, listFolder, `${listFileBase}.service.gen`))
|
|
715
|
+
: '';
|
|
716
|
+
const deleteServicePath = deleteOpId
|
|
717
|
+
? buildRelativeImportPath(featureDir, path_1.default.join(generatedRoot, deleteFolder, `${deleteFileBase}.service.gen`))
|
|
718
|
+
: '';
|
|
719
|
+
const title = smart.label || `${schema.entity || 'Admin'}`;
|
|
720
|
+
const subtitle = listSchema?.api?.endpoint
|
|
721
|
+
? `GET ${listSchema.api.endpoint}`
|
|
722
|
+
: 'Admin list';
|
|
723
|
+
const idParam = detailSchema?.api?.pathParams?.[0]?.name ??
|
|
724
|
+
smart.idParam ??
|
|
725
|
+
'id';
|
|
726
|
+
const detailRoute = detailOpId
|
|
727
|
+
? normalizeRoutePath(detailOpId)
|
|
728
|
+
: '';
|
|
729
|
+
const updateRoute = updateOpId
|
|
730
|
+
? normalizeRoutePath(updateOpId)
|
|
731
|
+
: '';
|
|
732
|
+
const componentPath = path_1.default.join(featureDir, `${fileBase}.component.ts`);
|
|
733
|
+
const hasDelete = Boolean(deleteOpId);
|
|
734
|
+
const hasEdit = Boolean(updateOpId);
|
|
735
|
+
const hasDetail = Boolean(detailOpId);
|
|
736
|
+
const uiCardImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-card', 'ui-card.component'));
|
|
737
|
+
const uiButtonImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-button', 'ui-button.component'));
|
|
738
|
+
const uiCheckboxImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-checkbox', 'ui-checkbox.component'));
|
|
739
|
+
const uiInputImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-input', 'ui-input.component'));
|
|
740
|
+
const uiTextareaImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-textarea', 'ui-textarea.component'));
|
|
741
|
+
const uiSelectImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-select', 'ui-select.component'));
|
|
742
|
+
const uiSearchImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-search', 'ui-search.component'));
|
|
743
|
+
const uiBadgeImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-badge', 'ui-badge.component'));
|
|
744
|
+
const uiStatCardImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-stat-card', 'ui-stat-card.component'));
|
|
745
|
+
const schemaImportPath = buildSchemaImportPath(featureDir, schemasRoot, toSafeFileName(adminOpId));
|
|
746
|
+
const generatedAdminComponentSource = `
|
|
747
|
+
import { Component, OnDestroy, OnInit, AfterViewInit } from '@angular/core'
|
|
748
|
+
import { CommonModule } from '@angular/common'
|
|
749
|
+
import { Router } from '@angular/router'
|
|
750
|
+
import { FormBuilder, ReactiveFormsModule } from '@angular/forms'
|
|
751
|
+
import { UiCardComponent } from '${uiCardImport}'
|
|
752
|
+
import { UiButtonComponent } from '${uiButtonImport}'
|
|
753
|
+
import { UiCheckboxComponent } from '${uiCheckboxImport}'
|
|
754
|
+
import { UiInputComponent } from '${uiInputImport}'
|
|
755
|
+
import { UiTextareaComponent } from '${uiTextareaImport}'
|
|
756
|
+
import { UiSelectComponent } from '${uiSelectImport}'
|
|
757
|
+
import { UiSearchComponent } from '${uiSearchImport}'
|
|
758
|
+
import { UiBadgeComponent } from '${uiBadgeImport}'
|
|
759
|
+
import { UiStatCardComponent } from '${uiStatCardImport}'
|
|
760
|
+
import { ${listName}Service } from '${listServicePath}'
|
|
761
|
+
${hasDelete ? `import { ${deleteName}Service } from '${deleteServicePath}'` : ''}
|
|
762
|
+
import { BehaviorSubject, forkJoin } from 'rxjs'
|
|
763
|
+
import screenSchema from '${schemaImportPath}'
|
|
521
764
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
765
|
+
@Component({
|
|
766
|
+
selector: 'app-${toKebab(name)}',
|
|
767
|
+
standalone: true,
|
|
768
|
+
imports: [
|
|
769
|
+
CommonModule,
|
|
770
|
+
ReactiveFormsModule,
|
|
771
|
+
UiCardComponent,
|
|
772
|
+
UiButtonComponent,
|
|
773
|
+
UiSelectComponent,
|
|
774
|
+
UiCheckboxComponent,
|
|
775
|
+
UiInputComponent,
|
|
776
|
+
UiTextareaComponent,
|
|
777
|
+
UiSearchComponent,
|
|
778
|
+
UiBadgeComponent,
|
|
779
|
+
UiStatCardComponent
|
|
780
|
+
],
|
|
781
|
+
templateUrl: './${fileBase}.component.html',
|
|
782
|
+
styleUrls: ['./${fileBase}.component.scss']
|
|
783
|
+
})
|
|
784
|
+
export class ${name}Component implements OnInit, AfterViewInit, OnDestroy {
|
|
785
|
+
private readonly onFocus = () => {
|
|
786
|
+
if (!this.loading) {
|
|
787
|
+
this.refresh()
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
private readonly onVisibility = () => {
|
|
791
|
+
if (
|
|
792
|
+
typeof document !== 'undefined' &&
|
|
793
|
+
document.visibilityState === 'visible'
|
|
794
|
+
) {
|
|
795
|
+
this.refresh()
|
|
796
|
+
}
|
|
797
|
+
}
|
|
527
798
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
799
|
+
loading = false
|
|
800
|
+
readonly result$ = new BehaviorSubject<any>(null)
|
|
801
|
+
readonly error$ = new BehaviorSubject<any>(null)
|
|
802
|
+
schema = screenSchema as any
|
|
803
|
+
selected = new Set<any>()
|
|
804
|
+
confirmIds: any[] = []
|
|
805
|
+
searchTerm = ''
|
|
806
|
+
private hasInitialLoad = false
|
|
534
807
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
margin: 0 auto;
|
|
542
|
-
}
|
|
808
|
+
constructor(
|
|
809
|
+
private router: Router,
|
|
810
|
+
private listService: ${listName}Service
|
|
811
|
+
${hasDelete ? `, private deleteService: ${deleteName}Service` : ''}
|
|
812
|
+
) {
|
|
813
|
+
}
|
|
543
814
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
815
|
+
ngOnInit() {
|
|
816
|
+
this.setupAutoRefresh()
|
|
817
|
+
this.ensureInitialLoad()
|
|
818
|
+
this.setupAutoRefreshListeners()
|
|
819
|
+
}
|
|
548
820
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
margin-top: -4px;
|
|
553
|
-
}
|
|
821
|
+
ngAfterViewInit() {
|
|
822
|
+
this.ensureInitialLoad()
|
|
823
|
+
}
|
|
554
824
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
}
|
|
825
|
+
ngOnDestroy() {
|
|
826
|
+
if (typeof window !== 'undefined') {
|
|
827
|
+
window.removeEventListener('focus', this.onFocus)
|
|
828
|
+
}
|
|
829
|
+
if (typeof document !== 'undefined') {
|
|
830
|
+
document.removeEventListener('visibilitychange', this.onVisibility)
|
|
831
|
+
}
|
|
832
|
+
}
|
|
562
833
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
border-radius: 12px;
|
|
567
|
-
background: #0f172a;
|
|
568
|
-
color: #e2e8f0;
|
|
569
|
-
font-size: 12px;
|
|
570
|
-
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.25);
|
|
571
|
-
overflow: auto;
|
|
572
|
-
}
|
|
834
|
+
private setupAutoRefresh() {
|
|
835
|
+
this.load()
|
|
836
|
+
}
|
|
573
837
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
}
|
|
838
|
+
private setupAutoRefreshListeners() {
|
|
839
|
+
if (typeof window !== 'undefined') {
|
|
840
|
+
window.addEventListener('focus', this.onFocus)
|
|
841
|
+
}
|
|
842
|
+
if (typeof document !== 'undefined') {
|
|
843
|
+
document.addEventListener('visibilitychange', this.onVisibility)
|
|
844
|
+
}
|
|
845
|
+
}
|
|
583
846
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
box-shadow: 0 20px 40px rgba(15, 23, 42, 0.08);
|
|
590
|
-
padding: 18px;
|
|
591
|
-
}
|
|
847
|
+
private ensureInitialLoad() {
|
|
848
|
+
if (this.hasInitialLoad) return
|
|
849
|
+
this.hasInitialLoad = true
|
|
850
|
+
this.load()
|
|
851
|
+
}
|
|
592
852
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
853
|
+
load() {
|
|
854
|
+
if (this.loading) return
|
|
855
|
+
this.loading = true
|
|
856
|
+
this.error$.next(null)
|
|
857
|
+
this.listService
|
|
858
|
+
.execute({}, {}, {})
|
|
859
|
+
.subscribe({
|
|
860
|
+
next: (result: any) => {
|
|
861
|
+
const normalized =
|
|
862
|
+
result && typeof result === 'object' && 'body' in result
|
|
863
|
+
? (result as any).body
|
|
864
|
+
: result
|
|
865
|
+
this.result$.next(normalized)
|
|
866
|
+
this.loading = false
|
|
867
|
+
},
|
|
868
|
+
error: (error: any) => {
|
|
869
|
+
this.error$.next(error)
|
|
870
|
+
this.loading = false
|
|
871
|
+
}
|
|
872
|
+
})
|
|
873
|
+
}
|
|
597
874
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
gap: 16px;
|
|
602
|
-
border-bottom: 1px solid #e2e8f0;
|
|
603
|
-
padding-bottom: 10px;
|
|
604
|
-
}
|
|
875
|
+
refresh() {
|
|
876
|
+
this.load()
|
|
877
|
+
}
|
|
605
878
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
}
|
|
879
|
+
onSearch(value: string) {
|
|
880
|
+
this.searchTerm = value
|
|
881
|
+
}
|
|
610
882
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
font-size: 12px;
|
|
615
|
-
letter-spacing: 0.08em;
|
|
616
|
-
text-transform: uppercase;
|
|
617
|
-
}
|
|
883
|
+
isArrayResult(raw?: any) {
|
|
884
|
+
return this.getVisibleRows(raw).length > 0
|
|
885
|
+
}
|
|
618
886
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
}
|
|
887
|
+
getRows(raw?: any) {
|
|
888
|
+
const value = this.unwrapResult(raw ?? this.result$.value)
|
|
889
|
+
if (Array.isArray(value)) return value
|
|
890
|
+
if (!value || typeof value !== 'object') return []
|
|
624
891
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
border: 1px solid rgba(239, 68, 68, 0.3);
|
|
630
|
-
background: #fff1f2;
|
|
631
|
-
color: #881337;
|
|
632
|
-
box-shadow: 0 10px 24px rgba(239, 68, 68, 0.15);
|
|
633
|
-
display: grid;
|
|
634
|
-
gap: 8px;
|
|
635
|
-
}
|
|
892
|
+
const commonKeys = ['data', 'items', 'results', 'list', 'records', 'products']
|
|
893
|
+
for (const key of commonKeys) {
|
|
894
|
+
if (Array.isArray(value[key])) return value[key]
|
|
895
|
+
}
|
|
636
896
|
|
|
637
|
-
.
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
word-break: break-word;
|
|
641
|
-
}
|
|
897
|
+
const found = this.findFirstArray(value, 0, 5)
|
|
898
|
+
return found ?? []
|
|
899
|
+
}
|
|
642
900
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
color: #0f172a;
|
|
650
|
-
display: grid;
|
|
651
|
-
gap: 10px;
|
|
652
|
-
}
|
|
901
|
+
getVisibleRows(raw?: any) {
|
|
902
|
+
const rows = this.getRows(raw)
|
|
903
|
+
const term = this.searchTerm.trim().toLowerCase()
|
|
904
|
+
if (!term) return rows
|
|
905
|
+
return rows.filter(row => this.matchesSearch(row, term))
|
|
906
|
+
}
|
|
653
907
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
908
|
+
private getConfiguredColumns() {
|
|
909
|
+
const columns = this.schema?.data?.table?.columns
|
|
910
|
+
if (!Array.isArray(columns)) return []
|
|
911
|
+
return columns
|
|
912
|
+
.map((entry: any) => {
|
|
913
|
+
if (typeof entry === 'string') {
|
|
914
|
+
return { key: entry, label: '', visible: true }
|
|
915
|
+
}
|
|
916
|
+
if (entry && typeof entry === 'object') {
|
|
917
|
+
const key = String(
|
|
918
|
+
entry.key ?? entry.id ?? entry.column ?? ''
|
|
919
|
+
).trim()
|
|
920
|
+
if (!key) return null
|
|
921
|
+
const label =
|
|
922
|
+
typeof entry.label === 'string' ? entry.label : ''
|
|
923
|
+
const visible = entry.visible !== false
|
|
924
|
+
return { key, label, visible }
|
|
925
|
+
}
|
|
926
|
+
return null
|
|
927
|
+
})
|
|
928
|
+
.filter(Boolean) as Array<{ key: string; label: string; visible: boolean }>
|
|
929
|
+
}
|
|
659
930
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
931
|
+
private getColumnLabel(value: string) {
|
|
932
|
+
const configured = this.getConfiguredColumns()
|
|
933
|
+
const match = configured.find(column => column.key === value)
|
|
934
|
+
return match?.label ?? ''
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
getColumns(raw?: any) {
|
|
938
|
+
const configured = this.getConfiguredColumns()
|
|
939
|
+
if (configured.length) {
|
|
940
|
+
return configured
|
|
941
|
+
.filter(column => column.visible)
|
|
942
|
+
.map(column => column.key)
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const rows = this.getRows(raw)
|
|
946
|
+
if (rows.length > 0 && rows[0] && typeof rows[0] === 'object') {
|
|
947
|
+
return Object.keys(rows[0])
|
|
948
|
+
}
|
|
949
|
+
return []
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
formatHeader(value: string) {
|
|
953
|
+
const configured = this.getColumnLabel(value)
|
|
954
|
+
if (configured) return configured
|
|
955
|
+
return value
|
|
956
|
+
.replace(/[_-]/g, ' ')
|
|
957
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
958
|
+
.replace(/\\b\\w/g, char => char.toUpperCase())
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
getCellValue(row: any, column: string) {
|
|
962
|
+
if (!row || !column) return ''
|
|
963
|
+
|
|
964
|
+
if (column.includes('.')) {
|
|
965
|
+
return column
|
|
966
|
+
.split('.')
|
|
967
|
+
.reduce((acc, key) => (acc ? acc[key] : undefined), row) ?? ''
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const value = row[column]
|
|
971
|
+
return this.formatValue(value)
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
isImageCell(row: any, column: string) {
|
|
975
|
+
const value = this.getCellValue(row, column)
|
|
976
|
+
return (
|
|
977
|
+
typeof value === 'string' &&
|
|
978
|
+
/^https?:\\/\\//.test(value) &&
|
|
979
|
+
/(\\.png|\\.jpg|\\.jpeg|\\.svg)/i.test(value)
|
|
980
|
+
)
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
formatValue(value: any): string {
|
|
984
|
+
if (value === null || value === undefined) return ''
|
|
985
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
986
|
+
return String(value)
|
|
987
|
+
}
|
|
988
|
+
if (typeof value === 'boolean') {
|
|
989
|
+
return value ? 'Yes' : 'No'
|
|
990
|
+
}
|
|
991
|
+
if (Array.isArray(value)) {
|
|
992
|
+
return value
|
|
993
|
+
.map((item: any) => this.formatValue(item))
|
|
994
|
+
.join(', ')
|
|
995
|
+
}
|
|
996
|
+
if (typeof value === 'object') {
|
|
997
|
+
if (typeof value.common === 'string') return value.common
|
|
998
|
+
if (typeof value.official === 'string') return value.official
|
|
999
|
+
if (typeof value.name === 'string') return value.name
|
|
1000
|
+
if (typeof value.label === 'string') return value.label
|
|
1001
|
+
return JSON.stringify(value)
|
|
1002
|
+
}
|
|
1003
|
+
return String(value)
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
getCardImage(row: any) {
|
|
1007
|
+
if (!row || typeof row !== 'object') return ''
|
|
1008
|
+
const directKeys = ['thumbnail', 'image', 'avatar', 'photo', 'picture']
|
|
1009
|
+
for (const key of directKeys) {
|
|
1010
|
+
if (typeof row[key] === 'string') return row[key]
|
|
1011
|
+
}
|
|
1012
|
+
if (Array.isArray(row.images) && row.images.length) {
|
|
1013
|
+
return row.images[0]
|
|
1014
|
+
}
|
|
1015
|
+
return ''
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
getCardTitle(row: any) {
|
|
1019
|
+
if (!row || typeof row !== 'object') return 'Item'
|
|
1020
|
+
return (
|
|
1021
|
+
row.title ??
|
|
1022
|
+
row.name ??
|
|
1023
|
+
row.label ??
|
|
1024
|
+
row.id ??
|
|
1025
|
+
'Item'
|
|
1026
|
+
)
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
getCardSubtitle(row: any) {
|
|
1030
|
+
if (!row || typeof row !== 'object') return ''
|
|
1031
|
+
return (
|
|
1032
|
+
row.description ??
|
|
1033
|
+
row.category ??
|
|
1034
|
+
row.brand ??
|
|
1035
|
+
''
|
|
1036
|
+
)
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
formatError(error: any) {
|
|
1040
|
+
if (!error) return ''
|
|
1041
|
+
if (typeof error === 'string') return error
|
|
1042
|
+
if (typeof error?.message === 'string') return error.message
|
|
1043
|
+
try {
|
|
1044
|
+
return JSON.stringify(error)
|
|
1045
|
+
} catch {
|
|
1046
|
+
return String(error)
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
toggleAll(checked: boolean) {
|
|
1051
|
+
this.selected.clear()
|
|
1052
|
+
if (!checked) return
|
|
1053
|
+
for (const row of this.getVisibleRows()) {
|
|
1054
|
+
const id = this.getRowId(row)
|
|
1055
|
+
if (id !== null && id !== undefined) {
|
|
1056
|
+
this.selected.add(id)
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
toggleRow(row: any, checked: boolean) {
|
|
1062
|
+
const id = this.getRowId(row)
|
|
1063
|
+
if (id === null || id === undefined) return
|
|
1064
|
+
if (checked) {
|
|
1065
|
+
this.selected.add(id)
|
|
1066
|
+
} else {
|
|
1067
|
+
this.selected.delete(id)
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
isSelected(row: any) {
|
|
1072
|
+
const id = this.getRowId(row)
|
|
1073
|
+
if (id === null || id === undefined) return false
|
|
1074
|
+
return this.selected.has(id)
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
openDetail(row: any) {
|
|
1078
|
+
${hasDetail ? '' : 'return'}
|
|
1079
|
+
const id = this.getRowId(row)
|
|
1080
|
+
if (id === null || id === undefined) return
|
|
1081
|
+
this.router.navigate(['${detailRoute}'], {
|
|
1082
|
+
state: { prefill: { ${idParam}: id }, autoSubmit: true }
|
|
1083
|
+
})
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
openEdit(row: any) {
|
|
1087
|
+
${hasEdit ? '' : 'return'}
|
|
1088
|
+
this.router.navigate(['${updateRoute}'], {
|
|
1089
|
+
state: { prefill: row }
|
|
1090
|
+
})
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
confirmBulkDelete() {
|
|
1094
|
+
this.confirmDelete([...this.selected])
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
confirmDelete(ids: any[]) {
|
|
1098
|
+
this.confirmIds = ids.filter(
|
|
1099
|
+
id => id !== null && id !== undefined
|
|
1100
|
+
)
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
cancelDelete() {
|
|
1104
|
+
this.confirmIds = []
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
deleteConfirmed() {
|
|
1108
|
+
const ids = [...this.confirmIds]
|
|
1109
|
+
this.confirmIds = []
|
|
1110
|
+
if (!ids.length) return
|
|
1111
|
+
${hasDelete ? `const calls = ids.map(id => this.deleteService.execute({ ${idParam}: id }, {}, {}))` : 'const calls: any[] = []'}
|
|
1112
|
+
if (!calls.length) return
|
|
1113
|
+
forkJoin(calls).subscribe({
|
|
1114
|
+
next: () => {
|
|
1115
|
+
this.removeFromResult(ids)
|
|
1116
|
+
ids.forEach(id => this.selected.delete(id))
|
|
1117
|
+
},
|
|
1118
|
+
error: error => {
|
|
1119
|
+
this.error$.next(error)
|
|
1120
|
+
}
|
|
1121
|
+
})
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
getRowId(row: any) {
|
|
1125
|
+
if (!row || typeof row !== 'object') return null
|
|
1126
|
+
if (row['${idParam}'] !== undefined) return row['${idParam}']
|
|
1127
|
+
if (row['id'] !== undefined) return row['id']
|
|
1128
|
+
return null
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
getStatusVariant(row: any): 'success' | 'warning' | 'danger' | 'neutral' {
|
|
1132
|
+
const raw = this.getStatusValue(row)
|
|
1133
|
+
const value = String(raw ?? '').toLowerCase()
|
|
1134
|
+
if (/active|ok|success|instock|available/.test(value)) return 'success'
|
|
1135
|
+
if (/warning|low|pending|draft/.test(value)) return 'warning'
|
|
1136
|
+
if (/error|inactive|disabled|out|blocked|archived/.test(value)) return 'danger'
|
|
1137
|
+
return 'neutral'
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
getStatusLabel(row: any) {
|
|
1141
|
+
const raw = this.getStatusValue(row)
|
|
1142
|
+
if (raw === null || raw === undefined || raw === '') return 'Unknown'
|
|
1143
|
+
return this.formatValue(raw)
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
getStatusValue(row: any, key?: string) {
|
|
1147
|
+
if (key && row && typeof row === 'object') {
|
|
1148
|
+
return row[key]
|
|
1149
|
+
}
|
|
1150
|
+
return (
|
|
1151
|
+
row?.status ??
|
|
1152
|
+
row?.paymentStatus ??
|
|
1153
|
+
row?.availabilityStatus ??
|
|
1154
|
+
row?.state ??
|
|
1155
|
+
row?.stockStatus ??
|
|
1156
|
+
row?.result
|
|
1157
|
+
)
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
isStatusColumn(column: string) {
|
|
1161
|
+
return /status|state|availability/i.test(column)
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
isIdColumn(column: string) {
|
|
1165
|
+
return /(^id$)|(_id$)|(Id$)/.test(column)
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
isMethodColumn(column: string) {
|
|
1169
|
+
return /method|paymentMethod/i.test(column)
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
isDateColumn(column: string) {
|
|
1173
|
+
return /date|createdAt|updatedAt|timestamp/i.test(column)
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
formatMethod(value: any) {
|
|
1177
|
+
const raw = String(value ?? '')
|
|
1178
|
+
if (!raw) return ''
|
|
1179
|
+
const labels: Record<string, string> = {
|
|
1180
|
+
credit_card: 'Cartão de Crédito',
|
|
1181
|
+
bank_transfer: 'Transferência Bancária',
|
|
1182
|
+
pix: 'PIX'
|
|
1183
|
+
}
|
|
1184
|
+
return labels[raw] ?? this.formatValue(raw)
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
formatDateValue(value: any) {
|
|
1188
|
+
if (!value) return ''
|
|
1189
|
+
const date = new Date(String(value))
|
|
1190
|
+
if (Number.isNaN(date.getTime())) return this.formatValue(value)
|
|
1191
|
+
return date.toLocaleDateString('pt-BR', {
|
|
1192
|
+
day: '2-digit',
|
|
1193
|
+
month: '2-digit',
|
|
1194
|
+
year: 'numeric',
|
|
1195
|
+
hour: '2-digit',
|
|
1196
|
+
minute: '2-digit'
|
|
1197
|
+
})
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
formatCurrency(value: number) {
|
|
1201
|
+
try {
|
|
1202
|
+
return new Intl.NumberFormat('pt-BR', {
|
|
1203
|
+
style: 'currency',
|
|
1204
|
+
currency: 'BRL'
|
|
1205
|
+
}).format(value)
|
|
1206
|
+
} catch {
|
|
1207
|
+
return 'R$ ' + value.toFixed(2)
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
getStatusKey(raw?: any) {
|
|
1212
|
+
const configured = this.getConfiguredColumns().map(column => column.key)
|
|
1213
|
+
const candidates = configured.length
|
|
1214
|
+
? configured
|
|
1215
|
+
: this.getColumns(raw)
|
|
1216
|
+
const match = candidates.find(column => this.isStatusColumn(column))
|
|
1217
|
+
return match ?? ''
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
getStatusLabelValue(value: string) {
|
|
1221
|
+
const normalized = value.toLowerCase()
|
|
1222
|
+
const labels: Record<string, string> = {
|
|
1223
|
+
completed: 'Concluído',
|
|
1224
|
+
pending: 'Pendente',
|
|
1225
|
+
failed: 'Falhou',
|
|
1226
|
+
active: 'Ativo',
|
|
1227
|
+
inactive: 'Inativo'
|
|
1228
|
+
}
|
|
1229
|
+
if (labels[normalized]) return labels[normalized]
|
|
1230
|
+
return value
|
|
1231
|
+
.replace(/[_-]/g, ' ')
|
|
1232
|
+
.replace(/\\b\\w/g, char => char.toUpperCase())
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
getStatusSummary(raw?: any) {
|
|
1236
|
+
const rows = this.getVisibleRows(raw)
|
|
1237
|
+
if (!rows.length) return []
|
|
1238
|
+
const key = this.getStatusKey(raw)
|
|
1239
|
+
if (!key) return []
|
|
1240
|
+
|
|
1241
|
+
const buckets = new Map<
|
|
1242
|
+
string,
|
|
1243
|
+
{ key: string; label: string; variant: 'success' | 'warning' | 'danger' | 'neutral'; count: number; total: number }
|
|
1244
|
+
>()
|
|
1245
|
+
|
|
1246
|
+
for (const row of rows) {
|
|
1247
|
+
const rawValue = this.getStatusValue(row, key)
|
|
1248
|
+
if (rawValue === null || rawValue === undefined || rawValue === '') {
|
|
1249
|
+
continue
|
|
1250
|
+
}
|
|
1251
|
+
const value = String(rawValue)
|
|
1252
|
+
const normalized = value.toLowerCase()
|
|
1253
|
+
const current = buckets.get(normalized) ?? {
|
|
1254
|
+
key: normalized,
|
|
1255
|
+
label: this.getStatusLabelValue(value),
|
|
1256
|
+
variant: this.getStatusVariant({ [key]: normalized }),
|
|
1257
|
+
count: 0,
|
|
1258
|
+
total: 0
|
|
1259
|
+
}
|
|
1260
|
+
current.count += 1
|
|
1261
|
+
current.total += this.getAmountValue(row)
|
|
1262
|
+
buckets.set(normalized, current)
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
return Array.from(buckets.values()).sort(
|
|
1266
|
+
(a, b) => b.count - a.count
|
|
1267
|
+
)
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
getAmountValue(row: any) {
|
|
1271
|
+
const candidates = ['amount', 'total', 'price', 'value', 'paidAmount']
|
|
1272
|
+
for (const key of candidates) {
|
|
1273
|
+
const raw = row?.[key]
|
|
1274
|
+
const num = Number(raw)
|
|
1275
|
+
if (Number.isFinite(num)) return num
|
|
1276
|
+
}
|
|
1277
|
+
return 0
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
private unwrapResult(value: any) {
|
|
1282
|
+
if (!value || typeof value !== 'object') return value
|
|
1283
|
+
if (Object.prototype.hasOwnProperty.call(value, 'data')) {
|
|
1284
|
+
return value.data
|
|
1285
|
+
}
|
|
1286
|
+
if (Object.prototype.hasOwnProperty.call(value, 'result')) {
|
|
1287
|
+
return value.result
|
|
1288
|
+
}
|
|
1289
|
+
if (Object.prototype.hasOwnProperty.call(value, 'body')) {
|
|
1290
|
+
return value.body
|
|
1291
|
+
}
|
|
1292
|
+
return value
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
private removeFromResult(ids: any[]) {
|
|
1296
|
+
const value = this.unwrapResult(this.result$.value)
|
|
1297
|
+
if (Array.isArray(value)) {
|
|
1298
|
+
this.result$.next(value.filter(item => !ids.includes(this.getRowId(item))))
|
|
1299
|
+
return
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
if (!value || typeof value !== 'object') return
|
|
1303
|
+
const key = this.findArrayKey(value)
|
|
1304
|
+
if (!key) return
|
|
1305
|
+
const nextArray = (value[key] as any[]).filter(
|
|
1306
|
+
item => !ids.includes(this.getRowId(item))
|
|
1307
|
+
)
|
|
1308
|
+
this.result$.next({ ...value, [key]: nextArray })
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
private findArrayKey(value: Record<string, any>) {
|
|
1312
|
+
const commonKeys = ['data', 'items', 'results', 'list', 'records', 'products']
|
|
1313
|
+
for (const key of commonKeys) {
|
|
1314
|
+
if (Array.isArray(value[key])) return key
|
|
1315
|
+
}
|
|
1316
|
+
for (const key of Object.keys(value)) {
|
|
1317
|
+
if (Array.isArray(value[key])) return key
|
|
1318
|
+
}
|
|
1319
|
+
return null
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
private matchesSearch(row: any, term: string) {
|
|
1323
|
+
if (!row || typeof row !== 'object') return false
|
|
1324
|
+
const values = Object.values(row)
|
|
1325
|
+
for (const value of values) {
|
|
1326
|
+
const normalized = this.formatValue(value).toLowerCase()
|
|
1327
|
+
if (normalized.includes(term)) return true
|
|
1328
|
+
}
|
|
1329
|
+
return false
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
private findFirstArray(
|
|
1333
|
+
value: any,
|
|
1334
|
+
depth: number,
|
|
1335
|
+
maxDepth: number
|
|
1336
|
+
): any[] | null {
|
|
1337
|
+
if (!value || depth > maxDepth) return null
|
|
1338
|
+
if (Array.isArray(value)) return value
|
|
1339
|
+
if (typeof value !== 'object') return null
|
|
1340
|
+
|
|
1341
|
+
for (const key of Object.keys(value)) {
|
|
1342
|
+
const found = this.findFirstArray(value[key], depth + 1, maxDepth)
|
|
1343
|
+
if (found) return found
|
|
1344
|
+
}
|
|
1345
|
+
return null
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
`;
|
|
1349
|
+
fs_1.default.writeFileSync(componentPath, generatedAdminComponentSource);
|
|
1350
|
+
const htmlPath = path_1.default.join(featureDir, `${fileBase}.component.html`);
|
|
1351
|
+
const responseFormat = resolveResponseFormat(schema);
|
|
1352
|
+
const useCards = responseFormat === 'cards';
|
|
1353
|
+
const useRaw = responseFormat === 'raw';
|
|
1354
|
+
fs_1.default.writeFileSync(htmlPath, `
|
|
1355
|
+
<div class="page">
|
|
1356
|
+
<div class="admin-header">
|
|
1357
|
+
<div>
|
|
1358
|
+
<div class="admin-title">
|
|
1359
|
+
<span class="admin-icon"></span>
|
|
1360
|
+
<span>${escapeAttr(title)}</span>
|
|
1361
|
+
</div>
|
|
1362
|
+
<div class="admin-subtitle">${escapeAttr(subtitle)}</div>
|
|
1363
|
+
</div>
|
|
1364
|
+
<div class="admin-actions">
|
|
1365
|
+
<ui-button variant="ghost" (click)="refresh()">Atualizar</ui-button>
|
|
1366
|
+
${hasDelete ? '<ui-button variant="danger" [disabled]="!selected.size" (click)="confirmBulkDelete()">Excluir selecionados</ui-button>' : ''}
|
|
1367
|
+
</div>
|
|
1368
|
+
</div>
|
|
1369
|
+
|
|
1370
|
+
<div class="admin-summary" *ngIf="getStatusSummary(result$ | async).length">
|
|
1371
|
+
<ui-stat-card
|
|
1372
|
+
*ngFor="let item of getStatusSummary(result$ | async)"
|
|
1373
|
+
[title]="item.label"
|
|
1374
|
+
[value]="item.total > 0 ? formatCurrency(item.total) : (item.count + '')"
|
|
1375
|
+
[meta]="item.count + ' registros'"
|
|
1376
|
+
>
|
|
1377
|
+
<span class="stat-icon" [class.success]="item.variant === 'success'" [class.warning]="item.variant === 'warning'" [class.danger]="item.variant === 'danger'"></span>
|
|
1378
|
+
</ui-stat-card>
|
|
1379
|
+
</div>
|
|
1380
|
+
|
|
1381
|
+
<ui-card>
|
|
1382
|
+
<div class="admin-toolbar">
|
|
1383
|
+
<ui-search
|
|
1384
|
+
[value]="searchTerm"
|
|
1385
|
+
[placeholder]="'Buscar...'"
|
|
1386
|
+
(valueChange)="onSearch($event)"
|
|
1387
|
+
></ui-search>
|
|
1388
|
+
<div class="admin-toolbar__meta">
|
|
1389
|
+
<ui-badge variant="neutral">
|
|
1390
|
+
{{ getVisibleRows(result$ | async).length }} registros
|
|
1391
|
+
</ui-badge>
|
|
1392
|
+
</div>
|
|
1393
|
+
</div>
|
|
1394
|
+
|
|
1395
|
+
<ng-container *ngIf="error$ | async as error">
|
|
1396
|
+
<div class="result-error" *ngIf="error">
|
|
1397
|
+
<strong>Request failed.</strong>
|
|
1398
|
+
<div class="result-error__body">
|
|
1399
|
+
{{ formatError(error) }}
|
|
1400
|
+
</div>
|
|
1401
|
+
</div>
|
|
1402
|
+
</ng-container>
|
|
1403
|
+
|
|
1404
|
+
<ng-container *ngIf="result$ | async as result">
|
|
1405
|
+
${useRaw ? `
|
|
1406
|
+
<details class="result-raw" *ngIf="result">
|
|
1407
|
+
<summary>Raw response</summary>
|
|
1408
|
+
<pre>{{ result | json }}</pre>
|
|
1409
|
+
</details>
|
|
1410
|
+
` : useCards ? `
|
|
1411
|
+
<div class="result-cards" *ngIf="isArrayResult(result)">
|
|
1412
|
+
<article class="card-tile" *ngFor="let row of getVisibleRows(result)">
|
|
1413
|
+
<div class="card-media" *ngIf="getCardImage(row)">
|
|
1414
|
+
<img [src]="getCardImage(row)" [alt]="getCardTitle(row)" />
|
|
1415
|
+
</div>
|
|
1416
|
+
<div class="card-body">
|
|
1417
|
+
<div class="card-header">
|
|
1418
|
+
${hasDelete ? '<input type="checkbox" [checked]="isSelected(row)" (click)="$event.stopPropagation()" (change)="toggleRow(row, $event.target.checked)" />' : ''}
|
|
1419
|
+
<h3 class="card-title">{{ getCardTitle(row) }}</h3>
|
|
1420
|
+
<ui-badge [variant]="getStatusVariant(row)">
|
|
1421
|
+
{{ getStatusLabel(row) }}
|
|
1422
|
+
</ui-badge>
|
|
1423
|
+
</div>
|
|
1424
|
+
<p class="card-subtitle" *ngIf="getCardSubtitle(row)">
|
|
1425
|
+
{{ getCardSubtitle(row) }}
|
|
1426
|
+
</p>
|
|
1427
|
+
<div class="card-actions" (click)="$event.stopPropagation()">
|
|
1428
|
+
${hasDetail ? '<button class="link" type="button" (click)="openDetail(row)">Details</button>' : ''}
|
|
1429
|
+
${hasEdit ? '<button class="link" type="button" (click)="openEdit(row)">Edit</button>' : ''}
|
|
1430
|
+
${hasDelete ? '<button class="link danger" type="button" (click)="confirmDelete([getRowId(row)])">Delete</button>' : ''}
|
|
1431
|
+
</div>
|
|
1432
|
+
</div>
|
|
1433
|
+
</article>
|
|
1434
|
+
</div>
|
|
1435
|
+
` : `
|
|
1436
|
+
<div class="result-table" *ngIf="isArrayResult(result)">
|
|
1437
|
+
<table class="data-table">
|
|
1438
|
+
<thead>
|
|
1439
|
+
<tr>
|
|
1440
|
+
${hasDelete ? '<th class="select-cell"><input type="checkbox" (change)="toggleAll($event.target.checked)" /></th>' : ''}
|
|
1441
|
+
<th *ngFor="let column of getColumns(result)">
|
|
1442
|
+
{{ formatHeader(column) }}
|
|
1443
|
+
</th>
|
|
1444
|
+
${hasDetail || hasEdit || hasDelete ? '<th class="actions-cell">Actions</th>' : ''}
|
|
1445
|
+
</tr>
|
|
1446
|
+
</thead>
|
|
1447
|
+
<tbody>
|
|
1448
|
+
<tr *ngFor="let row of getVisibleRows(result)" (click)="openDetail(row)">
|
|
1449
|
+
${hasDelete ? '<td class="select-cell"><input type="checkbox" [checked]="isSelected(row)" (click)="$event.stopPropagation()" (change)="toggleRow(row, $event.target.checked)" /></td>' : ''}
|
|
1450
|
+
<td *ngFor="let column of getColumns(result)">
|
|
1451
|
+
<img
|
|
1452
|
+
*ngIf="isImageCell(row, column)"
|
|
1453
|
+
[src]="getCellValue(row, column)"
|
|
1454
|
+
[alt]="formatHeader(column)"
|
|
1455
|
+
class="cell-image"
|
|
1456
|
+
/>
|
|
1457
|
+
<ng-container *ngIf="!isImageCell(row, column)">
|
|
1458
|
+
<span *ngIf="isStatusColumn(column)" class="cell-status">
|
|
1459
|
+
<ui-badge [variant]="getStatusVariant(row)">
|
|
1460
|
+
{{ getStatusLabel(row) }}
|
|
1461
|
+
</ui-badge>
|
|
1462
|
+
</span>
|
|
1463
|
+
<span *ngIf="isIdColumn(column) && !isStatusColumn(column)" class="code-pill">
|
|
1464
|
+
{{ getCellValue(row, column) }}
|
|
1465
|
+
</span>
|
|
1466
|
+
<span *ngIf="isMethodColumn(column) && !isStatusColumn(column) && !isIdColumn(column)">
|
|
1467
|
+
{{ formatMethod(getCellValue(row, column)) }}
|
|
1468
|
+
</span>
|
|
1469
|
+
<span *ngIf="isDateColumn(column) && !isStatusColumn(column) && !isIdColumn(column) && !isMethodColumn(column)">
|
|
1470
|
+
{{ formatDateValue(getCellValue(row, column)) }}
|
|
1471
|
+
</span>
|
|
1472
|
+
<span *ngIf="!isStatusColumn(column) && !isIdColumn(column) && !isMethodColumn(column) && !isDateColumn(column)">
|
|
1473
|
+
{{ getCellValue(row, column) }}
|
|
1474
|
+
</span>
|
|
1475
|
+
</ng-container>
|
|
1476
|
+
</td>
|
|
1477
|
+
${hasDetail || hasEdit || hasDelete ? `<td class="actions-cell">
|
|
1478
|
+
<div class="row-actions" (click)="$event.stopPropagation()">
|
|
1479
|
+
${hasDetail ? '<button class="link" type="button" (click)="openDetail(row)">Details</button>' : ''}
|
|
1480
|
+
${hasEdit ? '<button class="link" type="button" (click)="openEdit(row)">Edit</button>' : ''}
|
|
1481
|
+
${hasDelete ? '<button class="link danger" type="button" (click)="confirmDelete([getRowId(row)])">Delete</button>' : ''}
|
|
1482
|
+
</div>
|
|
1483
|
+
</td>` : ''}
|
|
1484
|
+
</tr>
|
|
1485
|
+
</tbody>
|
|
1486
|
+
</table>
|
|
1487
|
+
</div>
|
|
1488
|
+
`}
|
|
1489
|
+
|
|
1490
|
+
${!useRaw ? `
|
|
1491
|
+
<details class="result-raw" *ngIf="result">
|
|
1492
|
+
<summary>Raw response</summary>
|
|
1493
|
+
<pre>{{ result | json }}</pre>
|
|
1494
|
+
</details>
|
|
1495
|
+
` : ''}
|
|
1496
|
+
</ng-container>
|
|
1497
|
+
|
|
1498
|
+
</ui-card>
|
|
1499
|
+
|
|
1500
|
+
<div class="confirm-backdrop" *ngIf="confirmIds.length">
|
|
1501
|
+
<div class="confirm-modal">
|
|
1502
|
+
<h3>Confirm delete</h3>
|
|
1503
|
+
<p>Are you sure you want to delete the selected item(s)? This action cannot be undone.</p>
|
|
1504
|
+
<div class="actions">
|
|
1505
|
+
<ui-button variant="ghost" (click)="cancelDelete()">Cancel</ui-button>
|
|
1506
|
+
<ui-button variant="danger" (click)="deleteConfirmed()">Delete</ui-button>
|
|
1507
|
+
</div>
|
|
1508
|
+
</div>
|
|
1509
|
+
</div>
|
|
1510
|
+
</div>
|
|
1511
|
+
`);
|
|
1512
|
+
const scssPath = path_1.default.join(featureDir, `${fileBase}.component.scss`);
|
|
1513
|
+
fs_1.default.writeFileSync(scssPath, `${buildBaseScss()}
|
|
1514
|
+
|
|
1515
|
+
.admin-toolbar {
|
|
1516
|
+
display: grid;
|
|
1517
|
+
gap: 12px;
|
|
1518
|
+
margin-bottom: 14px;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
.admin-toolbar__meta {
|
|
1522
|
+
display: inline-flex;
|
|
1523
|
+
justify-content: flex-end;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
.admin-actions {
|
|
1527
|
+
justify-content: flex-end;
|
|
1528
|
+
gap: 10px;
|
|
1529
|
+
margin-top: 0;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
.admin-header {
|
|
1533
|
+
display: flex;
|
|
1534
|
+
align-items: flex-start;
|
|
1535
|
+
justify-content: space-between;
|
|
1536
|
+
gap: 16px;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
.admin-title {
|
|
1540
|
+
display: flex;
|
|
1541
|
+
align-items: center;
|
|
1542
|
+
gap: 10px;
|
|
1543
|
+
font-size: 22px;
|
|
1544
|
+
font-weight: 700;
|
|
1545
|
+
color: var(--bg-ink);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
.admin-icon {
|
|
1549
|
+
width: 28px;
|
|
1550
|
+
height: 28px;
|
|
1551
|
+
border-radius: 8px;
|
|
1552
|
+
background: linear-gradient(135deg, var(--color-primary), var(--color-accent));
|
|
1553
|
+
box-shadow: 0 8px 20px rgba(114, 157, 191, 0.2);
|
|
1554
|
+
display: inline-block;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
.admin-subtitle {
|
|
1558
|
+
margin-top: 6px;
|
|
1559
|
+
font-size: 13px;
|
|
1560
|
+
color: var(--color-muted);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
.admin-summary {
|
|
1564
|
+
display: grid;
|
|
1565
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
1566
|
+
gap: 12px;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
.stat-icon {
|
|
1570
|
+
width: 14px;
|
|
1571
|
+
height: 14px;
|
|
1572
|
+
border-radius: 999px;
|
|
1573
|
+
display: inline-block;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
.stat-icon.success {
|
|
1577
|
+
background: #34d399;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
.stat-icon.warning {
|
|
1581
|
+
background: #fbbf24;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
.stat-icon.danger {
|
|
1585
|
+
background: #f87171;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
.code-pill {
|
|
1589
|
+
font-family: "DM Sans", "Segoe UI", sans-serif;
|
|
1590
|
+
font-size: 11px;
|
|
1591
|
+
background: #eef2f7;
|
|
1592
|
+
color: #334155;
|
|
1593
|
+
padding: 4px 8px;
|
|
1594
|
+
border-radius: 6px;
|
|
1595
|
+
display: inline-block;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
.cell-status {
|
|
1599
|
+
display: inline-flex;
|
|
1600
|
+
align-items: center;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
.row-actions {
|
|
1604
|
+
display: inline-flex;
|
|
1605
|
+
gap: 10px;
|
|
1606
|
+
align-items: center;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
.row-actions .link {
|
|
1610
|
+
background: none;
|
|
1611
|
+
border: none;
|
|
1612
|
+
color: #2563eb;
|
|
1613
|
+
font-weight: 600;
|
|
1614
|
+
cursor: pointer;
|
|
1615
|
+
padding: 0;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
.row-actions .link.danger {
|
|
1619
|
+
color: #dc2626;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
.select-cell {
|
|
1623
|
+
width: 48px;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
.actions-cell {
|
|
1627
|
+
white-space: nowrap;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
.confirm-backdrop {
|
|
1631
|
+
position: fixed;
|
|
1632
|
+
inset: 0;
|
|
1633
|
+
background: rgba(15, 23, 42, 0.45);
|
|
1634
|
+
display: grid;
|
|
1635
|
+
place-items: center;
|
|
1636
|
+
z-index: 50;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
.confirm-modal {
|
|
1640
|
+
background: #ffffff;
|
|
1641
|
+
border-radius: 18px;
|
|
1642
|
+
padding: 20px 22px;
|
|
1643
|
+
width: min(380px, 92vw);
|
|
1644
|
+
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.28);
|
|
1645
|
+
display: grid;
|
|
1646
|
+
gap: 12px;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
.confirm-modal h3 {
|
|
1650
|
+
margin: 0;
|
|
1651
|
+
font-size: 18px;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
.confirm-modal p {
|
|
1655
|
+
margin: 0;
|
|
1656
|
+
color: #475569;
|
|
1657
|
+
font-size: 14px;
|
|
1658
|
+
}
|
|
1659
|
+
`);
|
|
1660
|
+
return {
|
|
1661
|
+
path: toRouteSegment(adminOpId),
|
|
1662
|
+
component: `${name}Component`,
|
|
1663
|
+
folder,
|
|
1664
|
+
fileBase
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
function normalizeFields(fields) {
|
|
1668
|
+
return fields.map(field => ({
|
|
1669
|
+
name: field.name,
|
|
1670
|
+
type: field.type || 'string',
|
|
1671
|
+
required: Boolean(field.required),
|
|
1672
|
+
label: field.label || toLabel(field.name),
|
|
1673
|
+
placeholder: field.placeholder || toPlaceholder(field.name),
|
|
1674
|
+
hint: field.hint || undefined,
|
|
1675
|
+
info: field.info || undefined,
|
|
1676
|
+
options: field.options || null,
|
|
1677
|
+
defaultValue: field.defaultValue ?? null,
|
|
1678
|
+
source: 'body'
|
|
1679
|
+
}));
|
|
1680
|
+
}
|
|
1681
|
+
function normalizeQueryParams(params) {
|
|
1682
|
+
return params.map(param => {
|
|
1683
|
+
const labelText = param.label || param.name;
|
|
1684
|
+
const hintText = param.hint ||
|
|
1685
|
+
(typeof labelText === 'string' && labelText.length > 60
|
|
1686
|
+
? labelText
|
|
1687
|
+
: '');
|
|
1688
|
+
const help = resolveFieldHelp(hintText, labelText);
|
|
1689
|
+
return {
|
|
1690
|
+
name: param.name,
|
|
1691
|
+
type: param.type || 'string',
|
|
1692
|
+
required: Boolean(param.required),
|
|
1693
|
+
label: toLabel(param.name),
|
|
1694
|
+
placeholder: param.placeholder || toPlaceholder(param.name),
|
|
1695
|
+
hint: help.hint,
|
|
1696
|
+
info: help.info,
|
|
1697
|
+
options: param.options || null,
|
|
1698
|
+
defaultValue: param.defaultValue ?? null,
|
|
1699
|
+
source: 'query'
|
|
1700
|
+
};
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
function extractPathParams(endpoint) {
|
|
1704
|
+
const params = [];
|
|
1705
|
+
const regex = /{([^}]+)}/g;
|
|
1706
|
+
let match = regex.exec(endpoint);
|
|
1707
|
+
while (match) {
|
|
1708
|
+
params.push(match[1]);
|
|
1709
|
+
match = regex.exec(endpoint);
|
|
1710
|
+
}
|
|
1711
|
+
return params;
|
|
1712
|
+
}
|
|
1713
|
+
function buildFormControls(fields) {
|
|
1714
|
+
if (fields.length === 0)
|
|
1715
|
+
return '';
|
|
1716
|
+
return fields
|
|
1717
|
+
.map(field => {
|
|
1718
|
+
const value = field.defaultValue !== null && field.defaultValue !== undefined
|
|
1719
|
+
? JSON.stringify(field.defaultValue)
|
|
1720
|
+
: defaultValueFor(field.type);
|
|
1721
|
+
const validators = field.required ? ', Validators.required' : '';
|
|
1722
|
+
return ` ${field.name}: [${value}${validators}]`;
|
|
1723
|
+
})
|
|
1724
|
+
.join(',\n');
|
|
1725
|
+
}
|
|
1726
|
+
function defaultValueFor(type) {
|
|
1727
|
+
switch (type) {
|
|
1728
|
+
case 'array':
|
|
1729
|
+
return '[]';
|
|
1730
|
+
case 'boolean':
|
|
1731
|
+
return 'false';
|
|
1732
|
+
case 'number':
|
|
1733
|
+
case 'integer':
|
|
1734
|
+
return 'null';
|
|
1735
|
+
default:
|
|
1736
|
+
return "''";
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
function toLabel(value) {
|
|
1740
|
+
return stripDiacritics(String(value))
|
|
1741
|
+
.replace(/[_-]/g, ' ')
|
|
1742
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
1743
|
+
.replace(/\b\w/g, char => char.toUpperCase());
|
|
1744
|
+
}
|
|
1745
|
+
function toPlaceholder(value) {
|
|
1746
|
+
return toLabel(value);
|
|
1747
|
+
}
|
|
1748
|
+
function stripDiacritics(value) {
|
|
1749
|
+
return value
|
|
1750
|
+
.normalize('NFD')
|
|
1751
|
+
.replace(/[\u0300-\u036f]/g, '');
|
|
1752
|
+
}
|
|
1753
|
+
function normalizeWhitespace(value) {
|
|
1754
|
+
return String(value).replace(/\s+/g, ' ').trim();
|
|
1755
|
+
}
|
|
1756
|
+
function resolveFieldHelp(rawHint, label) {
|
|
1757
|
+
const hint = normalizeWhitespace(rawHint || '');
|
|
1758
|
+
if (!hint)
|
|
1759
|
+
return { hint: undefined, info: undefined };
|
|
1760
|
+
if (hint.length > 120) {
|
|
1761
|
+
return { hint: undefined, info: hint };
|
|
1762
|
+
}
|
|
1763
|
+
return { hint, info: undefined };
|
|
1764
|
+
}
|
|
1765
|
+
function escapeAttr(value) {
|
|
1766
|
+
return String(value).replace(/"/g, '"');
|
|
1767
|
+
}
|
|
1768
|
+
function defaultActionLabel(method, hasParams) {
|
|
1769
|
+
switch (method) {
|
|
1770
|
+
case 'get':
|
|
1771
|
+
return hasParams ? 'Search' : 'Load';
|
|
1772
|
+
case 'post':
|
|
1773
|
+
return 'Criar';
|
|
1774
|
+
case 'put':
|
|
1775
|
+
case 'patch':
|
|
1776
|
+
return 'Salvar';
|
|
1777
|
+
case 'delete':
|
|
1778
|
+
return 'Excluir';
|
|
1779
|
+
default:
|
|
1780
|
+
return 'Executar';
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
function resolveResponseFormat(schema) {
|
|
1784
|
+
const candidate = String(schema?.meta?.responseFormat ??
|
|
1785
|
+
schema?.response?.format ??
|
|
1786
|
+
schema?.meta?.view ??
|
|
1787
|
+
'table')
|
|
1788
|
+
.trim()
|
|
1789
|
+
.toLowerCase();
|
|
1790
|
+
if (candidate === 'cards')
|
|
1791
|
+
return 'cards';
|
|
1792
|
+
if (candidate === 'raw')
|
|
1793
|
+
return 'raw';
|
|
1794
|
+
return 'table';
|
|
1795
|
+
}
|
|
1796
|
+
function buildComponentHtml(options) {
|
|
1797
|
+
const buttonVariant = options.method === 'delete' ? 'danger' : 'primary';
|
|
1798
|
+
const useCards = options.responseFormat === 'cards';
|
|
1799
|
+
const useRaw = options.responseFormat === 'raw';
|
|
1800
|
+
if (!options.hasForm) {
|
|
1801
|
+
return `
|
|
1802
|
+
<div class="page">
|
|
1803
|
+
<ui-card title="${options.title}" subtitle="${options.subtitle}">
|
|
1804
|
+
<div class="actions">
|
|
1805
|
+
<ui-button
|
|
1806
|
+
variant="${buttonVariant}"
|
|
1807
|
+
[disabled]="form.invalid"
|
|
1808
|
+
(click)="submit()"
|
|
1809
|
+
>
|
|
1810
|
+
${options.actionLabel}
|
|
1811
|
+
</ui-button>
|
|
1812
|
+
</div>
|
|
1813
|
+
</ui-card>
|
|
1814
|
+
|
|
1815
|
+
${useRaw ? `
|
|
1816
|
+
<ng-container *ngIf="error$ | async as error">
|
|
1817
|
+
<div class="result-error" *ngIf="error">
|
|
1818
|
+
<strong>Request failed.</strong>
|
|
1819
|
+
<div class="result-error__body">
|
|
1820
|
+
{{ formatError(error) }}
|
|
1821
|
+
</div>
|
|
1822
|
+
</div>
|
|
1823
|
+
</ng-container>
|
|
1824
|
+
|
|
1825
|
+
<ng-container *ngIf="result$ | async as result">
|
|
1826
|
+
<details class="result-raw" *ngIf="result">
|
|
1827
|
+
<summary>Raw response</summary>
|
|
1828
|
+
<pre>{{ result | json }}</pre>
|
|
1829
|
+
</details>
|
|
1830
|
+
</ng-container>
|
|
1831
|
+
` : useCards ? `
|
|
1832
|
+
<ng-container *ngIf="error$ | async as error">
|
|
1833
|
+
<div class="result-error" *ngIf="error">
|
|
1834
|
+
<strong>Request failed.</strong>
|
|
1835
|
+
<div class="result-error__body">
|
|
1836
|
+
{{ formatError(error) }}
|
|
1837
|
+
</div>
|
|
1838
|
+
</div>
|
|
1839
|
+
</ng-container>
|
|
1840
|
+
|
|
1841
|
+
<ng-container *ngIf="result$ | async as result">
|
|
1842
|
+
<div class="result-cards" *ngIf="isArrayResult(result)">
|
|
1843
|
+
<article class="card-tile" *ngFor="let row of getRows(result)">
|
|
1844
|
+
<div class="card-media" *ngIf="getCardImage(row)">
|
|
1845
|
+
<img [src]="getCardImage(row)" [alt]="getCardTitle(row)" />
|
|
1846
|
+
</div>
|
|
1847
|
+
<div class="card-body">
|
|
1848
|
+
<h3 class="card-title">{{ getCardTitle(row) }}</h3>
|
|
1849
|
+
<p class="card-subtitle" *ngIf="getCardSubtitle(row)">
|
|
1850
|
+
{{ getCardSubtitle(row) }}
|
|
1851
|
+
</p>
|
|
1852
|
+
</div>
|
|
1853
|
+
</article>
|
|
1854
|
+
</div>
|
|
1855
|
+
|
|
1856
|
+
<details class="result-raw" *ngIf="result">
|
|
1857
|
+
<summary>Raw response</summary>
|
|
1858
|
+
<pre>{{ result | json }}</pre>
|
|
1859
|
+
</details>
|
|
1860
|
+
</ng-container>
|
|
1861
|
+
` : `
|
|
1862
|
+
<ng-container *ngIf="error$ | async as error">
|
|
1863
|
+
<div class="result-error" *ngIf="error">
|
|
1864
|
+
<strong>Request failed.</strong>
|
|
1865
|
+
<div class="result-error__body">
|
|
1866
|
+
{{ formatError(error) }}
|
|
1867
|
+
</div>
|
|
1868
|
+
</div>
|
|
1869
|
+
</ng-container>
|
|
1870
|
+
|
|
1871
|
+
<ng-container *ngIf="result$ | async as result">
|
|
1872
|
+
<div class="result-table" *ngIf="isArrayResult(result)">
|
|
1873
|
+
<table class="data-table">
|
|
1874
|
+
<thead>
|
|
1875
|
+
<tr>
|
|
1876
|
+
<th *ngFor="let column of getColumns(result)">
|
|
1877
|
+
{{ formatHeader(column) }}
|
|
1878
|
+
</th>
|
|
1879
|
+
</tr>
|
|
1880
|
+
</thead>
|
|
1881
|
+
<tbody>
|
|
1882
|
+
<tr *ngFor="let row of getRows(result)">
|
|
1883
|
+
<td *ngFor="let column of getColumns(result)">
|
|
1884
|
+
<img
|
|
1885
|
+
*ngIf="isImageCell(row, column)"
|
|
1886
|
+
[src]="getCellValue(row, column)"
|
|
1887
|
+
[alt]="formatHeader(column)"
|
|
1888
|
+
class="cell-image"
|
|
1889
|
+
/>
|
|
1890
|
+
<span *ngIf="!isImageCell(row, column)">
|
|
1891
|
+
{{ getCellValue(row, column) }}
|
|
1892
|
+
</span>
|
|
1893
|
+
</td>
|
|
1894
|
+
</tr>
|
|
1895
|
+
</tbody>
|
|
1896
|
+
</table>
|
|
1897
|
+
</div>
|
|
1898
|
+
|
|
1899
|
+
<div class="result-card" *ngIf="!isArrayResult(result) && hasObjectRows(result)">
|
|
1900
|
+
<div class="result-card__grid">
|
|
1901
|
+
<div class="result-card__row" *ngFor="let row of getObjectRows(result)">
|
|
1902
|
+
<span class="result-card__label">
|
|
1903
|
+
{{ formatHeader(row.key) }}
|
|
1904
|
+
</span>
|
|
1905
|
+
<span class="result-card__value">
|
|
1906
|
+
{{ row.value }}
|
|
1907
|
+
</span>
|
|
1908
|
+
</div>
|
|
1909
|
+
</div>
|
|
1910
|
+
</div>
|
|
1911
|
+
|
|
1912
|
+
<div class="result-single" *ngIf="!isArrayResult(result) && isSingleValue(result)">
|
|
1913
|
+
<strong>Result</strong>
|
|
1914
|
+
<div class="result-single__value">
|
|
1915
|
+
{{ formatValue(result) }}
|
|
1916
|
+
</div>
|
|
1917
|
+
</div>
|
|
1918
|
+
|
|
1919
|
+
<details class="result-raw" *ngIf="result">
|
|
1920
|
+
<summary>Raw response</summary>
|
|
1921
|
+
<pre>{{ result | json }}</pre>
|
|
1922
|
+
</details>
|
|
1923
|
+
</ng-container>
|
|
1924
|
+
`}
|
|
1925
|
+
</div>
|
|
1926
|
+
`;
|
|
1927
|
+
}
|
|
1928
|
+
return `
|
|
1929
|
+
<div class="page">
|
|
1930
|
+
<ui-card [title]="schema.entity || schema.api.operationId" [subtitle]="schema.api.method.toUpperCase() + ' ' + schema.api.endpoint">
|
|
1931
|
+
<p class="screen-description" *ngIf="schema.description">
|
|
1932
|
+
{{ schema.description }}
|
|
1933
|
+
</p>
|
|
1934
|
+
<form [formGroup]="form" (ngSubmit)="submit()">
|
|
1935
|
+
<div class="form-grid">
|
|
1936
|
+
<div class="form-field" *ngFor="let field of formFields">
|
|
1937
|
+
<ui-select
|
|
1938
|
+
*ngIf="isSelect(field)"
|
|
1939
|
+
[label]="field.label || field.name"
|
|
1940
|
+
[hint]="field.hint"
|
|
1941
|
+
[info]="field.info"
|
|
1942
|
+
[controlName]="field.name"
|
|
1943
|
+
[options]="getSelectOptions(field)"
|
|
1944
|
+
[invalid]="isInvalid(field)"
|
|
1945
|
+
></ui-select>
|
|
1946
|
+
|
|
1947
|
+
<ui-textarea
|
|
1948
|
+
*ngIf="isTextarea(field)"
|
|
1949
|
+
[label]="field.label || field.name"
|
|
1950
|
+
[hint]="field.hint"
|
|
1951
|
+
[info]="field.info"
|
|
1952
|
+
[controlName]="field.name"
|
|
1953
|
+
[rows]="3"
|
|
1954
|
+
[placeholder]="field.placeholder || field.label || field.name"
|
|
1955
|
+
[invalid]="isInvalid(field)"
|
|
1956
|
+
></ui-textarea>
|
|
1957
|
+
|
|
1958
|
+
<ui-checkbox
|
|
1959
|
+
*ngIf="isCheckbox(field)"
|
|
1960
|
+
[label]="field.label || field.name"
|
|
1961
|
+
[hint]="field.hint"
|
|
1962
|
+
[info]="field.info"
|
|
1963
|
+
[controlName]="field.name"
|
|
1964
|
+
[invalid]="isInvalid(field)"
|
|
1965
|
+
></ui-checkbox>
|
|
1966
|
+
|
|
1967
|
+
<ui-input
|
|
1968
|
+
*ngIf="!isSelect(field) && !isTextarea(field) && !isCheckbox(field)"
|
|
1969
|
+
[label]="field.label || field.name"
|
|
1970
|
+
[hint]="field.hint"
|
|
1971
|
+
[info]="field.info"
|
|
1972
|
+
[type]="inputType(field)"
|
|
1973
|
+
[controlName]="field.name"
|
|
1974
|
+
[placeholder]="field.placeholder || field.label || field.name"
|
|
1975
|
+
[invalid]="isInvalid(field)"
|
|
1976
|
+
></ui-input>
|
|
1977
|
+
|
|
1978
|
+
<span class="field-error" *ngIf="isInvalid(field)">
|
|
1979
|
+
Required field
|
|
1980
|
+
</span>
|
|
1981
|
+
</div>
|
|
1982
|
+
</div>
|
|
1983
|
+
<div class="actions">
|
|
1984
|
+
<ui-button
|
|
1985
|
+
type="submit"
|
|
1986
|
+
variant="${buttonVariant}"
|
|
1987
|
+
[disabled]="form.invalid"
|
|
1988
|
+
>
|
|
1989
|
+
${options.actionLabel}
|
|
1990
|
+
</ui-button>
|
|
1991
|
+
</div>
|
|
1992
|
+
</form>
|
|
1993
|
+
</ui-card>
|
|
1994
|
+
|
|
1995
|
+
${useRaw ? `
|
|
1996
|
+
<ng-container *ngIf="error$ | async as error">
|
|
1997
|
+
<div class="result-error" *ngIf="error">
|
|
1998
|
+
<strong>Request failed.</strong>
|
|
1999
|
+
<div class="result-error__body">
|
|
2000
|
+
{{ formatError(error) }}
|
|
2001
|
+
</div>
|
|
2002
|
+
</div>
|
|
2003
|
+
</ng-container>
|
|
2004
|
+
|
|
2005
|
+
<ng-container *ngIf="result$ | async as result">
|
|
2006
|
+
<details class="result-raw" *ngIf="result">
|
|
2007
|
+
<summary>Raw response</summary>
|
|
2008
|
+
<pre>{{ result | json }}</pre>
|
|
2009
|
+
</details>
|
|
2010
|
+
</ng-container>
|
|
2011
|
+
` : useCards ? `
|
|
2012
|
+
<ng-container *ngIf="error$ | async as error">
|
|
2013
|
+
<div class="result-error" *ngIf="error">
|
|
2014
|
+
<strong>Request failed.</strong>
|
|
2015
|
+
<div class="result-error__body">
|
|
2016
|
+
{{ formatError(error) }}
|
|
2017
|
+
</div>
|
|
2018
|
+
</div>
|
|
2019
|
+
</ng-container>
|
|
2020
|
+
|
|
2021
|
+
<ng-container *ngIf="result$ | async as result">
|
|
2022
|
+
<div class="result-cards" *ngIf="isArrayResult(result)">
|
|
2023
|
+
<article class="card-tile" *ngFor="let row of getRows(result)">
|
|
2024
|
+
<div class="card-media" *ngIf="getCardImage(row)">
|
|
2025
|
+
<img [src]="getCardImage(row)" [alt]="getCardTitle(row)" />
|
|
2026
|
+
</div>
|
|
2027
|
+
<div class="card-body">
|
|
2028
|
+
<h3 class="card-title">{{ getCardTitle(row) }}</h3>
|
|
2029
|
+
<p class="card-subtitle" *ngIf="getCardSubtitle(row)">
|
|
2030
|
+
{{ getCardSubtitle(row) }}
|
|
2031
|
+
</p>
|
|
2032
|
+
</div>
|
|
2033
|
+
</article>
|
|
2034
|
+
</div>
|
|
2035
|
+
|
|
2036
|
+
<details class="result-raw" *ngIf="result">
|
|
2037
|
+
<summary>Raw response</summary>
|
|
2038
|
+
<pre>{{ result | json }}</pre>
|
|
2039
|
+
</details>
|
|
2040
|
+
</ng-container>
|
|
2041
|
+
` : `
|
|
2042
|
+
<ng-container *ngIf="error$ | async as error">
|
|
2043
|
+
<div class="result-error" *ngIf="error">
|
|
2044
|
+
<strong>Request failed.</strong>
|
|
2045
|
+
<div class="result-error__body">
|
|
2046
|
+
{{ formatError(error) }}
|
|
2047
|
+
</div>
|
|
2048
|
+
</div>
|
|
2049
|
+
</ng-container>
|
|
2050
|
+
|
|
2051
|
+
<ng-container *ngIf="result$ | async as result">
|
|
2052
|
+
<div class="result-table" *ngIf="isArrayResult(result)">
|
|
2053
|
+
<table class="data-table">
|
|
2054
|
+
<thead>
|
|
2055
|
+
<tr>
|
|
2056
|
+
<th *ngFor="let column of getColumns(result)">
|
|
2057
|
+
{{ formatHeader(column) }}
|
|
2058
|
+
</th>
|
|
2059
|
+
</tr>
|
|
2060
|
+
</thead>
|
|
2061
|
+
<tbody>
|
|
2062
|
+
<tr *ngFor="let row of getRows(result)">
|
|
2063
|
+
<td *ngFor="let column of getColumns(result)">
|
|
2064
|
+
<img
|
|
2065
|
+
*ngIf="isImageCell(row, column)"
|
|
2066
|
+
[src]="getCellValue(row, column)"
|
|
2067
|
+
[alt]="formatHeader(column)"
|
|
2068
|
+
class="cell-image"
|
|
2069
|
+
/>
|
|
2070
|
+
<span *ngIf="!isImageCell(row, column)">
|
|
2071
|
+
{{ getCellValue(row, column) }}
|
|
2072
|
+
</span>
|
|
2073
|
+
</td>
|
|
2074
|
+
</tr>
|
|
2075
|
+
</tbody>
|
|
2076
|
+
</table>
|
|
2077
|
+
</div>
|
|
2078
|
+
|
|
2079
|
+
<div class="result-card" *ngIf="!isArrayResult(result) && hasObjectRows(result)">
|
|
2080
|
+
<div class="result-card__grid">
|
|
2081
|
+
<div class="result-card__row" *ngFor="let row of getObjectRows(result)">
|
|
2082
|
+
<span class="result-card__label">
|
|
2083
|
+
{{ formatHeader(row.key) }}
|
|
2084
|
+
</span>
|
|
2085
|
+
<span class="result-card__value">
|
|
2086
|
+
{{ row.value }}
|
|
2087
|
+
</span>
|
|
2088
|
+
</div>
|
|
2089
|
+
</div>
|
|
2090
|
+
</div>
|
|
2091
|
+
|
|
2092
|
+
<div class="result-single" *ngIf="!isArrayResult(result) && isSingleValue(result)">
|
|
2093
|
+
<strong>Result</strong>
|
|
2094
|
+
<div class="result-single__value">
|
|
2095
|
+
{{ formatValue(result) }}
|
|
2096
|
+
</div>
|
|
2097
|
+
</div>
|
|
2098
|
+
|
|
2099
|
+
<details class="result-raw" *ngIf="result">
|
|
2100
|
+
<summary>Raw response</summary>
|
|
2101
|
+
<pre>{{ result | json }}</pre>
|
|
2102
|
+
</details>
|
|
2103
|
+
</ng-container>
|
|
2104
|
+
`}
|
|
2105
|
+
</div>
|
|
2106
|
+
`;
|
|
2107
|
+
}
|
|
2108
|
+
function buildBaseScss() {
|
|
2109
|
+
return `
|
|
2110
|
+
:host {
|
|
2111
|
+
display: block;
|
|
2112
|
+
padding: 24px;
|
|
2113
|
+
min-height: 100vh;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
.page {
|
|
2117
|
+
display: grid;
|
|
2118
|
+
gap: 16px;
|
|
2119
|
+
min-height: calc(100vh - 48px);
|
|
2120
|
+
min-width: 0;
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
.screen-description {
|
|
2124
|
+
margin: 0 0 18px;
|
|
2125
|
+
color: #6b7280;
|
|
2126
|
+
font-size: 14px;
|
|
2127
|
+
line-height: 1.5;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
.form-grid {
|
|
2131
|
+
display: grid;
|
|
2132
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
2133
|
+
gap: 16px;
|
|
2134
|
+
width: 100%;
|
|
2135
|
+
max-width: 960px;
|
|
2136
|
+
margin: 0 auto;
|
|
2137
|
+
min-width: 0;
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
.form-field {
|
|
2141
|
+
display: grid;
|
|
2142
|
+
gap: 8px;
|
|
2143
|
+
min-width: 0;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
.field-error {
|
|
2147
|
+
color: #ef4444;
|
|
2148
|
+
font-size: 12px;
|
|
2149
|
+
margin-top: -4px;
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
.actions {
|
|
2153
|
+
display: flex;
|
|
2154
|
+
justify-content: flex-end;
|
|
2155
|
+
gap: 14px;
|
|
2156
|
+
margin-top: 20px;
|
|
2157
|
+
flex-wrap: wrap;
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
.result {
|
|
2161
|
+
margin-top: 20px;
|
|
2162
|
+
padding: 16px;
|
|
2163
|
+
border-radius: 12px;
|
|
2164
|
+
background: #0f172a;
|
|
2165
|
+
color: #e2e8f0;
|
|
2166
|
+
font-size: 12px;
|
|
2167
|
+
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.25);
|
|
2168
|
+
overflow: auto;
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
.result-table {
|
|
2172
|
+
margin-top: 20px;
|
|
2173
|
+
max-width: 100%;
|
|
2174
|
+
overflow: auto;
|
|
2175
|
+
border-radius: 16px;
|
|
2176
|
+
border: 1px solid #e2e8f0;
|
|
2177
|
+
box-shadow: 0 20px 40px rgba(15, 23, 42, 0.08);
|
|
2178
|
+
-webkit-overflow-scrolling: touch;
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
.result-card {
|
|
2182
|
+
margin-top: 20px;
|
|
2183
|
+
border-radius: 16px;
|
|
2184
|
+
border: 1px solid #e2e8f0;
|
|
2185
|
+
background: #ffffff;
|
|
2186
|
+
box-shadow: 0 20px 40px rgba(15, 23, 42, 0.08);
|
|
2187
|
+
padding: 18px;
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
.result-card__grid {
|
|
2191
|
+
display: grid;
|
|
2192
|
+
gap: 12px;
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
.result-card__row {
|
|
2196
|
+
display: flex;
|
|
2197
|
+
justify-content: space-between;
|
|
2198
|
+
gap: 16px;
|
|
2199
|
+
border-bottom: 1px solid #e2e8f0;
|
|
2200
|
+
padding-bottom: 10px;
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
.result-card__row:last-child {
|
|
2204
|
+
border-bottom: none;
|
|
2205
|
+
padding-bottom: 0;
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
.result-card__label {
|
|
2209
|
+
font-weight: 600;
|
|
2210
|
+
color: #475569;
|
|
2211
|
+
font-size: 12px;
|
|
2212
|
+
letter-spacing: 0.08em;
|
|
2213
|
+
text-transform: uppercase;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
.result-card__value {
|
|
2217
|
+
color: #0f172a;
|
|
2218
|
+
font-weight: 600;
|
|
2219
|
+
text-align: right;
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
.result-cards {
|
|
2223
|
+
margin-top: 20px;
|
|
2224
|
+
display: grid;
|
|
2225
|
+
gap: 16px;
|
|
2226
|
+
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
.card-tile {
|
|
2230
|
+
background: #ffffff;
|
|
2231
|
+
border-radius: 18px;
|
|
2232
|
+
border: 1px solid rgba(15, 23, 42, 0.08);
|
|
2233
|
+
box-shadow: 0 18px 32px rgba(15, 23, 42, 0.08);
|
|
2234
|
+
overflow: hidden;
|
|
2235
|
+
display: grid;
|
|
2236
|
+
gap: 12px;
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
.card-media img {
|
|
2240
|
+
width: 100%;
|
|
2241
|
+
height: 160px;
|
|
2242
|
+
object-fit: cover;
|
|
2243
|
+
display: block;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
.card-body {
|
|
2247
|
+
padding: 14px 16px 16px;
|
|
2248
|
+
display: grid;
|
|
2249
|
+
gap: 8px;
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
.card-header {
|
|
2253
|
+
display: flex;
|
|
2254
|
+
align-items: center;
|
|
2255
|
+
gap: 10px;
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
.card-header input[type='checkbox'] {
|
|
2259
|
+
width: 22px;
|
|
2260
|
+
height: 22px;
|
|
2261
|
+
border-radius: 8px;
|
|
2262
|
+
border: 2px solid var(--color-border);
|
|
2263
|
+
background: #ffffff;
|
|
2264
|
+
box-shadow: 0 8px 18px rgba(99, 102, 241, 0.12);
|
|
2265
|
+
accent-color: var(--color-primary-strong);
|
|
2266
|
+
cursor: pointer;
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
.card-title {
|
|
2270
|
+
margin: 0;
|
|
2271
|
+
font-size: 16px;
|
|
2272
|
+
font-weight: 700;
|
|
2273
|
+
color: #0f172a;
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
.card-subtitle {
|
|
2277
|
+
margin: 0;
|
|
2278
|
+
font-size: 13px;
|
|
2279
|
+
color: #64748b;
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
.card-actions {
|
|
2283
|
+
display: inline-flex;
|
|
2284
|
+
flex-wrap: wrap;
|
|
2285
|
+
gap: 10px;
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
.card-actions .link {
|
|
2289
|
+
border: 1px solid rgba(15, 23, 42, 0.12);
|
|
2290
|
+
background: #ffffff;
|
|
2291
|
+
color: #0f172a;
|
|
2292
|
+
font-weight: 600;
|
|
2293
|
+
font-size: 12px;
|
|
2294
|
+
padding: 6px 12px;
|
|
2295
|
+
border-radius: 999px;
|
|
2296
|
+
cursor: pointer;
|
|
2297
|
+
transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
.card-actions .link:hover {
|
|
2301
|
+
background: #f8fafc;
|
|
2302
|
+
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.12);
|
|
2303
|
+
transform: translateY(-1px);
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
.card-actions .link.danger {
|
|
2307
|
+
border-color: rgba(239, 68, 68, 0.35);
|
|
2308
|
+
color: #b91c1c;
|
|
2309
|
+
background: #fff1f2;
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
.card-actions .link.danger:hover {
|
|
2313
|
+
background: #ffe4e6;
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
.result-error {
|
|
2317
|
+
margin-top: 24px;
|
|
2318
|
+
padding: 16px 18px;
|
|
2319
|
+
border-radius: 16px;
|
|
2320
|
+
border: 1px solid rgba(239, 68, 68, 0.3);
|
|
2321
|
+
background: #fff1f2;
|
|
2322
|
+
color: #881337;
|
|
2323
|
+
box-shadow: 0 10px 24px rgba(239, 68, 68, 0.15);
|
|
2324
|
+
display: grid;
|
|
2325
|
+
gap: 8px;
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
.result-error__body {
|
|
2329
|
+
font-size: 13px;
|
|
2330
|
+
color: #7f1d1d;
|
|
2331
|
+
word-break: break-word;
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
.result-raw {
|
|
2335
|
+
margin-top: 24px;
|
|
2336
|
+
padding: 16px 18px;
|
|
2337
|
+
border-radius: 16px;
|
|
2338
|
+
border: 1px dashed rgba(15, 23, 42, 0.18);
|
|
2339
|
+
background: #f8fafc;
|
|
2340
|
+
color: #0f172a;
|
|
2341
|
+
display: grid;
|
|
2342
|
+
gap: 10px;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
.result-raw summary {
|
|
2346
|
+
cursor: pointer;
|
|
2347
|
+
font-weight: 600;
|
|
2348
|
+
color: #334155;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
.result-raw pre {
|
|
2352
|
+
margin: 0;
|
|
2353
|
+
padding: 12px;
|
|
2354
|
+
border-radius: 12px;
|
|
2355
|
+
background: #ffffff;
|
|
2356
|
+
border: 1px solid rgba(15, 23, 42, 0.08);
|
|
666
2357
|
font-size: 12px;
|
|
667
2358
|
line-height: 1.4;
|
|
668
2359
|
white-space: pre-wrap;
|
|
@@ -739,343 +2430,27 @@ export class ${name}Service {
|
|
|
739
2430
|
min-height: 0;
|
|
740
2431
|
}
|
|
741
2432
|
|
|
742
|
-
@media (max-width:
|
|
743
|
-
:host {
|
|
744
|
-
padding: 18px;
|
|
745
|
-
}
|
|
746
|
-
|
|
2433
|
+
@media (max-width: 1024px) {
|
|
747
2434
|
.form-grid {
|
|
748
|
-
grid-template-columns: 1fr;
|
|
2435
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
749
2436
|
max-width: 100%;
|
|
750
2437
|
}
|
|
751
|
-
|
|
752
|
-
.actions {
|
|
753
|
-
justify-content: stretch;
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
`);
|
|
757
|
-
return {
|
|
758
|
-
path: toRouteSegment(name),
|
|
759
|
-
component: `${name}Component`,
|
|
760
|
-
folder,
|
|
761
|
-
fileBase
|
|
762
|
-
};
|
|
763
|
-
}
|
|
764
|
-
function normalizeFields(fields) {
|
|
765
|
-
return fields.map(field => ({
|
|
766
|
-
name: field.name,
|
|
767
|
-
type: field.type || 'string',
|
|
768
|
-
required: Boolean(field.required),
|
|
769
|
-
label: field.label || toLabel(field.name),
|
|
770
|
-
placeholder: field.placeholder || toPlaceholder(field.name),
|
|
771
|
-
hint: field.hint || undefined,
|
|
772
|
-
info: field.info || undefined,
|
|
773
|
-
options: field.options || null,
|
|
774
|
-
defaultValue: field.defaultValue ?? null,
|
|
775
|
-
source: 'body'
|
|
776
|
-
}));
|
|
777
|
-
}
|
|
778
|
-
function normalizeQueryParams(params) {
|
|
779
|
-
return params.map(param => {
|
|
780
|
-
const labelText = param.label || param.name;
|
|
781
|
-
const hintText = param.hint ||
|
|
782
|
-
(typeof labelText === 'string' && labelText.length > 60
|
|
783
|
-
? labelText
|
|
784
|
-
: '');
|
|
785
|
-
const help = resolveFieldHelp(hintText, labelText);
|
|
786
|
-
return {
|
|
787
|
-
name: param.name,
|
|
788
|
-
type: param.type || 'string',
|
|
789
|
-
required: Boolean(param.required),
|
|
790
|
-
label: toLabel(param.name),
|
|
791
|
-
placeholder: param.placeholder || toPlaceholder(param.name),
|
|
792
|
-
hint: help.hint,
|
|
793
|
-
info: help.info,
|
|
794
|
-
options: param.options || null,
|
|
795
|
-
defaultValue: param.defaultValue ?? null,
|
|
796
|
-
source: 'query'
|
|
797
|
-
};
|
|
798
|
-
});
|
|
799
|
-
}
|
|
800
|
-
function extractPathParams(endpoint) {
|
|
801
|
-
const params = [];
|
|
802
|
-
const regex = /{([^}]+)}/g;
|
|
803
|
-
let match = regex.exec(endpoint);
|
|
804
|
-
while (match) {
|
|
805
|
-
params.push(match[1]);
|
|
806
|
-
match = regex.exec(endpoint);
|
|
807
|
-
}
|
|
808
|
-
return params;
|
|
809
|
-
}
|
|
810
|
-
function buildFormControls(fields) {
|
|
811
|
-
if (fields.length === 0)
|
|
812
|
-
return '';
|
|
813
|
-
return fields
|
|
814
|
-
.map(field => {
|
|
815
|
-
const value = field.defaultValue !== null && field.defaultValue !== undefined
|
|
816
|
-
? JSON.stringify(field.defaultValue)
|
|
817
|
-
: defaultValueFor(field.type);
|
|
818
|
-
const validators = field.required ? ', Validators.required' : '';
|
|
819
|
-
return ` ${field.name}: [${value}${validators}]`;
|
|
820
|
-
})
|
|
821
|
-
.join(',\n');
|
|
822
|
-
}
|
|
823
|
-
function defaultValueFor(type) {
|
|
824
|
-
switch (type) {
|
|
825
|
-
case 'array':
|
|
826
|
-
return '[]';
|
|
827
|
-
case 'boolean':
|
|
828
|
-
return 'false';
|
|
829
|
-
case 'number':
|
|
830
|
-
case 'integer':
|
|
831
|
-
return 'null';
|
|
832
|
-
default:
|
|
833
|
-
return "''";
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
function toLabel(value) {
|
|
837
|
-
return String(value)
|
|
838
|
-
.replace(/[_-]/g, ' ')
|
|
839
|
-
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
840
|
-
.replace(/\b\w/g, char => char.toUpperCase());
|
|
841
|
-
}
|
|
842
|
-
function toPlaceholder(value) {
|
|
843
|
-
return toLabel(value);
|
|
844
|
-
}
|
|
845
|
-
function normalizeWhitespace(value) {
|
|
846
|
-
return String(value).replace(/\s+/g, ' ').trim();
|
|
847
|
-
}
|
|
848
|
-
function resolveFieldHelp(rawHint, label) {
|
|
849
|
-
const hint = normalizeWhitespace(rawHint || '');
|
|
850
|
-
if (!hint)
|
|
851
|
-
return { hint: undefined, info: undefined };
|
|
852
|
-
if (hint.length > 120) {
|
|
853
|
-
return { hint: undefined, info: hint };
|
|
854
|
-
}
|
|
855
|
-
return { hint, info: undefined };
|
|
856
|
-
}
|
|
857
|
-
function escapeAttr(value) {
|
|
858
|
-
return String(value).replace(/"/g, '"');
|
|
859
|
-
}
|
|
860
|
-
function defaultActionLabel(method, hasParams) {
|
|
861
|
-
switch (method) {
|
|
862
|
-
case 'get':
|
|
863
|
-
return hasParams ? 'Buscar' : 'Carregar';
|
|
864
|
-
case 'post':
|
|
865
|
-
return 'Criar';
|
|
866
|
-
case 'put':
|
|
867
|
-
case 'patch':
|
|
868
|
-
return 'Salvar';
|
|
869
|
-
case 'delete':
|
|
870
|
-
return 'Excluir';
|
|
871
|
-
default:
|
|
872
|
-
return 'Executar';
|
|
873
|
-
}
|
|
874
2438
|
}
|
|
875
|
-
function buildComponentHtml(options) {
|
|
876
|
-
const buttonVariant = options.method === 'delete' ? 'danger' : 'primary';
|
|
877
|
-
if (!options.hasForm) {
|
|
878
|
-
return `
|
|
879
|
-
<div class="page">
|
|
880
|
-
<ui-card title="${options.title}" subtitle="${options.subtitle}">
|
|
881
|
-
<div class="actions">
|
|
882
|
-
<ui-button
|
|
883
|
-
variant="${buttonVariant}"
|
|
884
|
-
[disabled]="form.invalid"
|
|
885
|
-
(click)="submit()"
|
|
886
|
-
>
|
|
887
|
-
${options.actionLabel}
|
|
888
|
-
</ui-button>
|
|
889
|
-
</div>
|
|
890
|
-
</ui-card>
|
|
891
|
-
|
|
892
|
-
<div class="result-table" *ngIf="isArrayResult()">
|
|
893
|
-
<table class="data-table">
|
|
894
|
-
<thead>
|
|
895
|
-
<tr>
|
|
896
|
-
<th *ngFor="let column of getColumns()">
|
|
897
|
-
{{ formatHeader(column) }}
|
|
898
|
-
</th>
|
|
899
|
-
</tr>
|
|
900
|
-
</thead>
|
|
901
|
-
<tbody>
|
|
902
|
-
<tr *ngFor="let row of getRows()">
|
|
903
|
-
<td *ngFor="let column of getColumns()">
|
|
904
|
-
<img
|
|
905
|
-
*ngIf="isImageCell(row, column)"
|
|
906
|
-
[src]="getCellValue(row, column)"
|
|
907
|
-
[alt]="formatHeader(column)"
|
|
908
|
-
class="cell-image"
|
|
909
|
-
/>
|
|
910
|
-
<span *ngIf="!isImageCell(row, column)">
|
|
911
|
-
{{ getCellValue(row, column) }}
|
|
912
|
-
</span>
|
|
913
|
-
</td>
|
|
914
|
-
</tr>
|
|
915
|
-
</tbody>
|
|
916
|
-
</table>
|
|
917
|
-
</div>
|
|
918
|
-
|
|
919
|
-
<div class="result-error" *ngIf="error">
|
|
920
|
-
<strong>Request failed.</strong>
|
|
921
|
-
<div class="result-error__body">
|
|
922
|
-
{{ error?.message || (error | json) }}
|
|
923
|
-
</div>
|
|
924
|
-
</div>
|
|
925
|
-
|
|
926
|
-
<div class="result-card" *ngIf="!isArrayResult() && hasObjectRows()">
|
|
927
|
-
<div class="result-card__grid">
|
|
928
|
-
<div class="result-card__row" *ngFor="let row of getObjectRows()">
|
|
929
|
-
<span class="result-card__label">
|
|
930
|
-
{{ formatHeader(row.key) }}
|
|
931
|
-
</span>
|
|
932
|
-
<span class="result-card__value">
|
|
933
|
-
{{ row.value }}
|
|
934
|
-
</span>
|
|
935
|
-
</div>
|
|
936
|
-
</div>
|
|
937
|
-
</div>
|
|
938
|
-
|
|
939
|
-
<div class="result-single" *ngIf="!isArrayResult() && isSingleValue()">
|
|
940
|
-
<strong>Result</strong>
|
|
941
|
-
<div class="result-single__value">
|
|
942
|
-
{{ formatValue(result) }}
|
|
943
|
-
</div>
|
|
944
|
-
</div>
|
|
945
|
-
|
|
946
|
-
<details class="result-raw" *ngIf="result">
|
|
947
|
-
<summary>Raw response</summary>
|
|
948
|
-
<pre>{{ result | json }}</pre>
|
|
949
|
-
</details>
|
|
950
|
-
</div>
|
|
951
|
-
`;
|
|
952
|
-
}
|
|
953
|
-
return `
|
|
954
|
-
<div class="page">
|
|
955
|
-
<ui-card [title]="schema.entity || schema.api.operationId" [subtitle]="schema.api.method.toUpperCase() + ' ' + schema.api.endpoint">
|
|
956
|
-
<p class="screen-description" *ngIf="schema.description">
|
|
957
|
-
{{ schema.description }}
|
|
958
|
-
</p>
|
|
959
|
-
<form [formGroup]="form" (ngSubmit)="submit()">
|
|
960
|
-
<div class="form-grid">
|
|
961
|
-
<div class="form-field" *ngFor="let field of formFields">
|
|
962
|
-
<ui-select
|
|
963
|
-
*ngIf="isSelect(field)"
|
|
964
|
-
[label]="field.label || field.name"
|
|
965
|
-
[hint]="field.hint"
|
|
966
|
-
[info]="field.info"
|
|
967
|
-
[controlName]="field.name"
|
|
968
|
-
[options]="getSelectOptions(field)"
|
|
969
|
-
[invalid]="isInvalid(field)"
|
|
970
|
-
></ui-select>
|
|
971
|
-
|
|
972
|
-
<ui-textarea
|
|
973
|
-
*ngIf="isTextarea(field)"
|
|
974
|
-
[label]="field.label || field.name"
|
|
975
|
-
[hint]="field.hint"
|
|
976
|
-
[info]="field.info"
|
|
977
|
-
[controlName]="field.name"
|
|
978
|
-
[rows]="3"
|
|
979
|
-
[placeholder]="field.placeholder || field.label || field.name"
|
|
980
|
-
[invalid]="isInvalid(field)"
|
|
981
|
-
></ui-textarea>
|
|
982
|
-
|
|
983
|
-
<ui-checkbox
|
|
984
|
-
*ngIf="isCheckbox(field)"
|
|
985
|
-
[label]="field.label || field.name"
|
|
986
|
-
[hint]="field.hint"
|
|
987
|
-
[info]="field.info"
|
|
988
|
-
[controlName]="field.name"
|
|
989
|
-
[invalid]="isInvalid(field)"
|
|
990
|
-
></ui-checkbox>
|
|
991
|
-
|
|
992
|
-
<ui-input
|
|
993
|
-
*ngIf="!isSelect(field) && !isTextarea(field) && !isCheckbox(field)"
|
|
994
|
-
[label]="field.label || field.name"
|
|
995
|
-
[hint]="field.hint"
|
|
996
|
-
[info]="field.info"
|
|
997
|
-
[type]="inputType(field)"
|
|
998
|
-
[controlName]="field.name"
|
|
999
|
-
[placeholder]="field.placeholder || field.label || field.name"
|
|
1000
|
-
[invalid]="isInvalid(field)"
|
|
1001
|
-
></ui-input>
|
|
1002
|
-
|
|
1003
|
-
<span class="field-error" *ngIf="isInvalid(field)">
|
|
1004
|
-
Campo obrigatório
|
|
1005
|
-
</span>
|
|
1006
|
-
</div>
|
|
1007
|
-
</div>
|
|
1008
|
-
<div class="actions">
|
|
1009
|
-
<ui-button
|
|
1010
|
-
type="submit"
|
|
1011
|
-
variant="${buttonVariant}"
|
|
1012
|
-
[disabled]="form.invalid"
|
|
1013
|
-
>
|
|
1014
|
-
${options.actionLabel}
|
|
1015
|
-
</ui-button>
|
|
1016
|
-
</div>
|
|
1017
|
-
</form>
|
|
1018
|
-
</ui-card>
|
|
1019
|
-
|
|
1020
|
-
<div class="result-table" *ngIf="isArrayResult()">
|
|
1021
|
-
<table class="data-table">
|
|
1022
|
-
<thead>
|
|
1023
|
-
<tr>
|
|
1024
|
-
<th *ngFor="let column of getColumns()">
|
|
1025
|
-
{{ formatHeader(column) }}
|
|
1026
|
-
</th>
|
|
1027
|
-
</tr>
|
|
1028
|
-
</thead>
|
|
1029
|
-
<tbody>
|
|
1030
|
-
<tr *ngFor="let row of getRows()">
|
|
1031
|
-
<td *ngFor="let column of getColumns()">
|
|
1032
|
-
<img
|
|
1033
|
-
*ngIf="isImageCell(row, column)"
|
|
1034
|
-
[src]="getCellValue(row, column)"
|
|
1035
|
-
[alt]="formatHeader(column)"
|
|
1036
|
-
class="cell-image"
|
|
1037
|
-
/>
|
|
1038
|
-
<span *ngIf="!isImageCell(row, column)">
|
|
1039
|
-
{{ getCellValue(row, column) }}
|
|
1040
|
-
</span>
|
|
1041
|
-
</td>
|
|
1042
|
-
</tr>
|
|
1043
|
-
</tbody>
|
|
1044
|
-
</table>
|
|
1045
|
-
</div>
|
|
1046
|
-
|
|
1047
|
-
<div class="result-error" *ngIf="error">
|
|
1048
|
-
<strong>Request failed.</strong>
|
|
1049
|
-
<div class="result-error__body">
|
|
1050
|
-
{{ error?.message || (error | json) }}
|
|
1051
|
-
</div>
|
|
1052
|
-
</div>
|
|
1053
2439
|
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
{{ formatHeader(row.key) }}
|
|
1059
|
-
</span>
|
|
1060
|
-
<span class="result-card__value">
|
|
1061
|
-
{{ row.value }}
|
|
1062
|
-
</span>
|
|
1063
|
-
</div>
|
|
1064
|
-
</div>
|
|
1065
|
-
</div>
|
|
2440
|
+
@media (max-width: 820px) {
|
|
2441
|
+
:host {
|
|
2442
|
+
padding: 18px;
|
|
2443
|
+
}
|
|
1066
2444
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
</div>
|
|
1072
|
-
</div>
|
|
2445
|
+
.form-grid {
|
|
2446
|
+
grid-template-columns: 1fr;
|
|
2447
|
+
max-width: 100%;
|
|
2448
|
+
}
|
|
1073
2449
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
</div>
|
|
2450
|
+
.actions {
|
|
2451
|
+
justify-content: stretch;
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
1079
2454
|
`;
|
|
1080
2455
|
}
|
|
1081
2456
|
function buildFieldHtml(field) {
|
|
@@ -1146,11 +2521,11 @@ export class UiCardComponent {
|
|
|
1146
2521
|
}
|
|
1147
2522
|
|
|
1148
2523
|
.ui-card {
|
|
1149
|
-
border-radius:
|
|
1150
|
-
background:
|
|
1151
|
-
border: 1px solid
|
|
1152
|
-
box-shadow:
|
|
1153
|
-
padding:
|
|
2524
|
+
border-radius: 12px;
|
|
2525
|
+
background: #ffffff;
|
|
2526
|
+
border: 1px solid #e2e8f0;
|
|
2527
|
+
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.06);
|
|
2528
|
+
padding: 24px;
|
|
1154
2529
|
position: relative;
|
|
1155
2530
|
overflow: hidden;
|
|
1156
2531
|
}
|
|
@@ -1159,9 +2534,9 @@ export class UiCardComponent {
|
|
|
1159
2534
|
content: "";
|
|
1160
2535
|
position: absolute;
|
|
1161
2536
|
inset: 0 0 auto 0;
|
|
1162
|
-
height:
|
|
1163
|
-
background: linear-gradient(90deg,
|
|
1164
|
-
opacity: 0.
|
|
2537
|
+
height: 3px;
|
|
2538
|
+
background: linear-gradient(90deg, #1d4ed8, #2563eb);
|
|
2539
|
+
opacity: 0.85;
|
|
1165
2540
|
}
|
|
1166
2541
|
|
|
1167
2542
|
.ui-card__header {
|
|
@@ -1170,19 +2545,20 @@ export class UiCardComponent {
|
|
|
1170
2545
|
|
|
1171
2546
|
.ui-card__title {
|
|
1172
2547
|
margin: 0;
|
|
1173
|
-
font-size:
|
|
1174
|
-
font-weight:
|
|
1175
|
-
color:
|
|
2548
|
+
font-size: 24px;
|
|
2549
|
+
font-weight: 600;
|
|
2550
|
+
color: #0f172a;
|
|
1176
2551
|
letter-spacing: -0.02em;
|
|
2552
|
+
font-family: "Instrument Serif", Georgia, serif;
|
|
1177
2553
|
}
|
|
1178
2554
|
|
|
1179
2555
|
.ui-card__subtitle {
|
|
1180
2556
|
margin: 8px 0 0;
|
|
1181
|
-
font-size:
|
|
1182
|
-
color:
|
|
1183
|
-
letter-spacing: 0.
|
|
2557
|
+
font-size: 11px;
|
|
2558
|
+
color: #64748b;
|
|
2559
|
+
letter-spacing: 0.14em;
|
|
1184
2560
|
text-transform: uppercase;
|
|
1185
|
-
font-family: "
|
|
2561
|
+
font-family: "DM Sans", "Segoe UI", sans-serif;
|
|
1186
2562
|
}
|
|
1187
2563
|
`
|
|
1188
2564
|
},
|
|
@@ -1218,7 +2594,7 @@ export class UiCardComponent {
|
|
|
1218
2594
|
class="ui-menu__group"
|
|
1219
2595
|
*ngIf="menu.ungrouped.length"
|
|
1220
2596
|
>
|
|
1221
|
-
<h3 class="ui-menu__group-title">
|
|
2597
|
+
<h3 class="ui-menu__group-title">Other</h3>
|
|
1222
2598
|
<a
|
|
1223
2599
|
class="ui-menu__item"
|
|
1224
2600
|
*ngFor="let item of menu.ungrouped"
|
|
@@ -1241,10 +2617,10 @@ export class UiCardComponent {
|
|
|
1241
2617
|
position: sticky;
|
|
1242
2618
|
top: 24px;
|
|
1243
2619
|
align-self: flex-start;
|
|
1244
|
-
background:
|
|
1245
|
-
border: 1px solid
|
|
1246
|
-
box-shadow:
|
|
1247
|
-
border-radius:
|
|
2620
|
+
background: #ffffff;
|
|
2621
|
+
border: 1px solid #e2e8f0;
|
|
2622
|
+
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.06);
|
|
2623
|
+
border-radius: 12px;
|
|
1248
2624
|
padding: 22px;
|
|
1249
2625
|
min-width: 220px;
|
|
1250
2626
|
display: grid;
|
|
@@ -1256,7 +2632,7 @@ export class UiCardComponent {
|
|
|
1256
2632
|
align-items: center;
|
|
1257
2633
|
gap: 10px;
|
|
1258
2634
|
font-weight: 700;
|
|
1259
|
-
color:
|
|
2635
|
+
color: #0f172a;
|
|
1260
2636
|
font-size: 15px;
|
|
1261
2637
|
}
|
|
1262
2638
|
|
|
@@ -1264,8 +2640,8 @@ export class UiCardComponent {
|
|
|
1264
2640
|
width: 14px;
|
|
1265
2641
|
height: 14px;
|
|
1266
2642
|
border-radius: 999px;
|
|
1267
|
-
background: linear-gradient(135deg,
|
|
1268
|
-
box-shadow: 0
|
|
2643
|
+
background: linear-gradient(135deg, #1d4ed8, #2563eb);
|
|
2644
|
+
box-shadow: 0 8px 20px rgba(37, 99, 235, 0.24);
|
|
1269
2645
|
}
|
|
1270
2646
|
|
|
1271
2647
|
.ui-menu__group {
|
|
@@ -1276,10 +2652,10 @@ export class UiCardComponent {
|
|
|
1276
2652
|
.ui-menu__group-title {
|
|
1277
2653
|
margin: 0;
|
|
1278
2654
|
font-size: 11px;
|
|
1279
|
-
letter-spacing: 0.
|
|
2655
|
+
letter-spacing: 0.16em;
|
|
1280
2656
|
text-transform: uppercase;
|
|
1281
|
-
color:
|
|
1282
|
-
font-family: "
|
|
2657
|
+
color: #94a3b8;
|
|
2658
|
+
font-family: "DM Sans", "Segoe UI", sans-serif;
|
|
1283
2659
|
}
|
|
1284
2660
|
|
|
1285
2661
|
.ui-menu__item {
|
|
@@ -1289,8 +2665,8 @@ export class UiCardComponent {
|
|
|
1289
2665
|
font-weight: 600;
|
|
1290
2666
|
color: #1f2937;
|
|
1291
2667
|
padding: 10px 14px;
|
|
1292
|
-
border-radius:
|
|
1293
|
-
transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
|
|
2668
|
+
border-radius: 8px;
|
|
2669
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease, color 0.2s ease;
|
|
1294
2670
|
border: 1px solid transparent;
|
|
1295
2671
|
white-space: nowrap;
|
|
1296
2672
|
overflow: hidden;
|
|
@@ -1298,14 +2674,15 @@ export class UiCardComponent {
|
|
|
1298
2674
|
}
|
|
1299
2675
|
|
|
1300
2676
|
.ui-menu__item:hover {
|
|
1301
|
-
background: #
|
|
2677
|
+
background: #eff6ff;
|
|
1302
2678
|
transform: translateX(2px);
|
|
2679
|
+
color: #1d4ed8;
|
|
1303
2680
|
}
|
|
1304
2681
|
|
|
1305
2682
|
.ui-menu__item.active {
|
|
1306
|
-
background:
|
|
2683
|
+
background: #1e293b;
|
|
1307
2684
|
color: #ffffff;
|
|
1308
|
-
box-shadow: 0
|
|
2685
|
+
box-shadow: 0 10px 24px rgba(30, 41, 59, 0.24);
|
|
1309
2686
|
}
|
|
1310
2687
|
|
|
1311
2688
|
.ui-menu__item.hidden {
|
|
@@ -1378,6 +2755,7 @@ export class UiFieldComponent {
|
|
|
1378
2755
|
gap: 10px;
|
|
1379
2756
|
font-size: 13px;
|
|
1380
2757
|
color: #1f2937;
|
|
2758
|
+
min-width: 0;
|
|
1381
2759
|
}
|
|
1382
2760
|
|
|
1383
2761
|
.ui-field__label {
|
|
@@ -1393,14 +2771,14 @@ export class UiFieldComponent {
|
|
|
1393
2771
|
}
|
|
1394
2772
|
|
|
1395
2773
|
.ui-field__info {
|
|
1396
|
-
margin-left:
|
|
1397
|
-
width:
|
|
1398
|
-
height:
|
|
2774
|
+
margin-left: 6px;
|
|
2775
|
+
width: 16px;
|
|
2776
|
+
height: 16px;
|
|
1399
2777
|
border-radius: 999px;
|
|
1400
|
-
border: 1px solid
|
|
2778
|
+
border: 1px solid var(--color-border);
|
|
1401
2779
|
background: #ffffff;
|
|
1402
|
-
color:
|
|
1403
|
-
font-size:
|
|
2780
|
+
color: var(--color-primary-strong);
|
|
2781
|
+
font-size: 10px;
|
|
1404
2782
|
line-height: 1;
|
|
1405
2783
|
display: inline-flex;
|
|
1406
2784
|
align-items: center;
|
|
@@ -1409,30 +2787,34 @@ export class UiFieldComponent {
|
|
|
1409
2787
|
}
|
|
1410
2788
|
|
|
1411
2789
|
.ui-field__info:hover {
|
|
1412
|
-
background: #
|
|
2790
|
+
background: #ffffff;
|
|
1413
2791
|
}
|
|
1414
2792
|
|
|
1415
2793
|
.ui-field__info-panel {
|
|
1416
2794
|
margin-top: 8px;
|
|
1417
2795
|
padding: 10px 12px;
|
|
1418
|
-
border-radius:
|
|
1419
|
-
background: #
|
|
1420
|
-
border: 1px solid
|
|
2796
|
+
border-radius: 14px;
|
|
2797
|
+
background: #ffffff;
|
|
2798
|
+
border: 1px solid var(--color-border);
|
|
1421
2799
|
color: #475569;
|
|
1422
2800
|
font-size: 12px;
|
|
1423
2801
|
line-height: 1.4;
|
|
2802
|
+
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
|
|
1424
2803
|
}
|
|
1425
2804
|
|
|
1426
2805
|
:host ::ng-deep input,
|
|
1427
2806
|
:host ::ng-deep textarea {
|
|
1428
2807
|
width: 100%;
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
2808
|
+
max-width: 100%;
|
|
2809
|
+
min-width: 0;
|
|
2810
|
+
min-height: 2.9rem;
|
|
2811
|
+
border-radius: 16px;
|
|
2812
|
+
border: 1px solid var(--color-border);
|
|
1432
2813
|
background: #ffffff;
|
|
1433
|
-
padding: 0.
|
|
1434
|
-
font-size:
|
|
2814
|
+
padding: 0.7rem 0.95rem;
|
|
2815
|
+
font-size: 14px;
|
|
1435
2816
|
font-weight: 500;
|
|
2817
|
+
box-sizing: border-box;
|
|
1436
2818
|
box-shadow: none;
|
|
1437
2819
|
outline: none;
|
|
1438
2820
|
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
|
@@ -1440,15 +2822,15 @@ export class UiFieldComponent {
|
|
|
1440
2822
|
|
|
1441
2823
|
:host ::ng-deep input:focus,
|
|
1442
2824
|
:host ::ng-deep textarea:focus {
|
|
1443
|
-
border-color: var(--color-primary);
|
|
1444
|
-
box-shadow: 0 0 0
|
|
2825
|
+
border-color: var(--color-primary-strong);
|
|
2826
|
+
box-shadow: 0 0 0 4px var(--color-primary-soft);
|
|
1445
2827
|
transform: translateY(-1px);
|
|
1446
2828
|
}
|
|
1447
2829
|
|
|
1448
2830
|
:host ::ng-deep input.invalid,
|
|
1449
2831
|
:host ::ng-deep textarea.invalid {
|
|
1450
|
-
border-color:
|
|
1451
|
-
box-shadow: 0 0 0
|
|
2832
|
+
border-color: var(--color-accent);
|
|
2833
|
+
box-shadow: 0 0 0 3px var(--color-accent-soft);
|
|
1452
2834
|
}
|
|
1453
2835
|
|
|
1454
2836
|
:host ::ng-deep input::placeholder,
|
|
@@ -1457,12 +2839,15 @@ export class UiFieldComponent {
|
|
|
1457
2839
|
}
|
|
1458
2840
|
|
|
1459
2841
|
:host ::ng-deep input[type='checkbox'] {
|
|
1460
|
-
width:
|
|
1461
|
-
height:
|
|
2842
|
+
width: 22px;
|
|
2843
|
+
height: 22px;
|
|
1462
2844
|
padding: 0;
|
|
1463
|
-
border-radius:
|
|
1464
|
-
|
|
1465
|
-
|
|
2845
|
+
border-radius: 8px;
|
|
2846
|
+
border: 2px solid var(--color-border);
|
|
2847
|
+
background: #ffffff;
|
|
2848
|
+
box-shadow: 0 8px 18px rgba(99, 102, 241, 0.12);
|
|
2849
|
+
accent-color: var(--color-primary-strong);
|
|
2850
|
+
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
1466
2851
|
}
|
|
1467
2852
|
|
|
1468
2853
|
.field-error {
|
|
@@ -1503,31 +2888,32 @@ export class UiButtonComponent {
|
|
|
1503
2888
|
`,
|
|
1504
2889
|
scss: `
|
|
1505
2890
|
.ui-button {
|
|
1506
|
-
border:
|
|
1507
|
-
border-radius:
|
|
1508
|
-
padding:
|
|
1509
|
-
font-weight:
|
|
1510
|
-
font-size:
|
|
1511
|
-
letter-spacing: 0.
|
|
2891
|
+
border: 1px solid transparent;
|
|
2892
|
+
border-radius: 8px;
|
|
2893
|
+
padding: 10px 16px;
|
|
2894
|
+
font-weight: 600;
|
|
2895
|
+
font-size: 13px;
|
|
2896
|
+
letter-spacing: 0.01em;
|
|
1512
2897
|
cursor: pointer;
|
|
1513
2898
|
transition: transform 0.2s ease, box-shadow 0.2s ease, filter 0.2s ease;
|
|
1514
2899
|
}
|
|
1515
2900
|
|
|
1516
2901
|
.ui-button.primary {
|
|
1517
|
-
background:
|
|
2902
|
+
background: #2563eb;
|
|
1518
2903
|
color: #ffffff;
|
|
1519
|
-
box-shadow: 0
|
|
2904
|
+
box-shadow: 0 8px 20px rgba(37, 99, 235, 0.24);
|
|
1520
2905
|
}
|
|
1521
2906
|
|
|
1522
2907
|
.ui-button.ghost {
|
|
1523
|
-
background: #
|
|
1524
|
-
color: #
|
|
2908
|
+
background: #ffffff;
|
|
2909
|
+
color: #334155;
|
|
2910
|
+
border-color: #cbd5e1;
|
|
1525
2911
|
}
|
|
1526
2912
|
|
|
1527
2913
|
.ui-button.danger {
|
|
1528
|
-
background:
|
|
2914
|
+
background: #dc2626;
|
|
1529
2915
|
color: #fff;
|
|
1530
|
-
box-shadow: 0
|
|
2916
|
+
box-shadow: 0 8px 20px rgba(220, 38, 38, 0.22);
|
|
1531
2917
|
}
|
|
1532
2918
|
|
|
1533
2919
|
.ui-button:hover:not(:disabled) {
|
|
@@ -1540,6 +2926,211 @@ export class UiButtonComponent {
|
|
|
1540
2926
|
cursor: not-allowed;
|
|
1541
2927
|
box-shadow: none;
|
|
1542
2928
|
}
|
|
2929
|
+
`
|
|
2930
|
+
},
|
|
2931
|
+
{
|
|
2932
|
+
name: 'ui-search',
|
|
2933
|
+
template: `
|
|
2934
|
+
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
|
2935
|
+
|
|
2936
|
+
@Component({
|
|
2937
|
+
selector: 'ui-search',
|
|
2938
|
+
standalone: true,
|
|
2939
|
+
templateUrl: './ui-search.component.html',
|
|
2940
|
+
styleUrls: ['./ui-search.component.scss']
|
|
2941
|
+
})
|
|
2942
|
+
export class UiSearchComponent {
|
|
2943
|
+
@Input() value = ''
|
|
2944
|
+
@Input() placeholder = 'Search'
|
|
2945
|
+
@Output() valueChange = new EventEmitter<string>()
|
|
2946
|
+
|
|
2947
|
+
onInput(event: Event) {
|
|
2948
|
+
const target = event.target as HTMLInputElement | null
|
|
2949
|
+
this.valueChange.emit(target?.value ?? '')
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
`,
|
|
2953
|
+
html: `
|
|
2954
|
+
<label class="ui-search">
|
|
2955
|
+
<span class="ui-search__icon" aria-hidden="true">⌕</span>
|
|
2956
|
+
<input
|
|
2957
|
+
class="ui-search__input"
|
|
2958
|
+
type="search"
|
|
2959
|
+
[value]="value"
|
|
2960
|
+
[placeholder]="placeholder"
|
|
2961
|
+
(input)="onInput($event)"
|
|
2962
|
+
/>
|
|
2963
|
+
</label>
|
|
2964
|
+
`,
|
|
2965
|
+
scss: `
|
|
2966
|
+
:host {
|
|
2967
|
+
display: block;
|
|
2968
|
+
width: 100%;
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
.ui-search {
|
|
2972
|
+
display: flex;
|
|
2973
|
+
align-items: center;
|
|
2974
|
+
gap: 8px;
|
|
2975
|
+
min-height: 40px;
|
|
2976
|
+
border-radius: 10px;
|
|
2977
|
+
border: 1px solid #d1d5db;
|
|
2978
|
+
background: #ffffff;
|
|
2979
|
+
padding: 0 10px;
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
.ui-search:focus-within {
|
|
2983
|
+
border-color: #2563eb;
|
|
2984
|
+
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
.ui-search__icon {
|
|
2988
|
+
color: #64748b;
|
|
2989
|
+
font-size: 13px;
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
.ui-search__input {
|
|
2993
|
+
border: none;
|
|
2994
|
+
outline: none;
|
|
2995
|
+
width: 100%;
|
|
2996
|
+
font-size: 13px;
|
|
2997
|
+
color: #0f172a;
|
|
2998
|
+
background: transparent;
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
.ui-search__input::placeholder {
|
|
3002
|
+
color: #94a3b8;
|
|
3003
|
+
}
|
|
3004
|
+
`
|
|
3005
|
+
},
|
|
3006
|
+
{
|
|
3007
|
+
name: 'ui-stat-card',
|
|
3008
|
+
template: `
|
|
3009
|
+
import { Component, Input } from '@angular/core'
|
|
3010
|
+
import { NgIf } from '@angular/common'
|
|
3011
|
+
|
|
3012
|
+
@Component({
|
|
3013
|
+
selector: 'ui-stat-card',
|
|
3014
|
+
standalone: true,
|
|
3015
|
+
imports: [NgIf],
|
|
3016
|
+
templateUrl: './ui-stat-card.component.html',
|
|
3017
|
+
styleUrls: ['./ui-stat-card.component.scss']
|
|
3018
|
+
})
|
|
3019
|
+
export class UiStatCardComponent {
|
|
3020
|
+
@Input() title = ''
|
|
3021
|
+
@Input() value = ''
|
|
3022
|
+
@Input() meta = ''
|
|
3023
|
+
}
|
|
3024
|
+
`,
|
|
3025
|
+
html: `
|
|
3026
|
+
<section class="ui-stat-card">
|
|
3027
|
+
<div class="ui-stat-card__top">
|
|
3028
|
+
<div class="ui-stat-card__title">{{ title }}</div>
|
|
3029
|
+
<div class="ui-stat-card__icon">
|
|
3030
|
+
<ng-content></ng-content>
|
|
3031
|
+
</div>
|
|
3032
|
+
</div>
|
|
3033
|
+
<div class="ui-stat-card__value">{{ value }}</div>
|
|
3034
|
+
<div class="ui-stat-card__meta" *ngIf="meta">{{ meta }}</div>
|
|
3035
|
+
</section>
|
|
3036
|
+
`,
|
|
3037
|
+
scss: `
|
|
3038
|
+
:host {
|
|
3039
|
+
display: block;
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
.ui-stat-card {
|
|
3043
|
+
border-radius: 12px;
|
|
3044
|
+
background: var(--bg-surface);
|
|
3045
|
+
border: 1px solid var(--color-border);
|
|
3046
|
+
padding: 16px;
|
|
3047
|
+
box-shadow: var(--shadow-card);
|
|
3048
|
+
display: grid;
|
|
3049
|
+
gap: 8px;
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
.ui-stat-card__top {
|
|
3053
|
+
display: flex;
|
|
3054
|
+
align-items: center;
|
|
3055
|
+
justify-content: space-between;
|
|
3056
|
+
gap: 10px;
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
.ui-stat-card__title {
|
|
3060
|
+
font-size: 12px;
|
|
3061
|
+
color: var(--color-muted);
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
.ui-stat-card__value {
|
|
3065
|
+
font-size: 22px;
|
|
3066
|
+
font-weight: 700;
|
|
3067
|
+
color: var(--bg-ink);
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
.ui-stat-card__meta {
|
|
3071
|
+
font-size: 11px;
|
|
3072
|
+
color: var(--color-muted);
|
|
3073
|
+
}
|
|
3074
|
+
`
|
|
3075
|
+
},
|
|
3076
|
+
{
|
|
3077
|
+
name: 'ui-badge',
|
|
3078
|
+
template: `
|
|
3079
|
+
import { Component, Input } from '@angular/core'
|
|
3080
|
+
import { NgClass } from '@angular/common'
|
|
3081
|
+
|
|
3082
|
+
@Component({
|
|
3083
|
+
selector: 'ui-badge',
|
|
3084
|
+
standalone: true,
|
|
3085
|
+
imports: [NgClass],
|
|
3086
|
+
templateUrl: './ui-badge.component.html',
|
|
3087
|
+
styleUrls: ['./ui-badge.component.scss']
|
|
3088
|
+
})
|
|
3089
|
+
export class UiBadgeComponent {
|
|
3090
|
+
@Input() variant: 'success' | 'warning' | 'danger' | 'neutral' = 'neutral'
|
|
3091
|
+
}
|
|
3092
|
+
`,
|
|
3093
|
+
html: `
|
|
3094
|
+
<span class="ui-badge" [ngClass]="variant">
|
|
3095
|
+
<ng-content></ng-content>
|
|
3096
|
+
</span>
|
|
3097
|
+
`,
|
|
3098
|
+
scss: `
|
|
3099
|
+
:host {
|
|
3100
|
+
display: inline-flex;
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
.ui-badge {
|
|
3104
|
+
display: inline-flex;
|
|
3105
|
+
align-items: center;
|
|
3106
|
+
justify-content: center;
|
|
3107
|
+
border-radius: 999px;
|
|
3108
|
+
padding: 4px 10px;
|
|
3109
|
+
font-size: 11px;
|
|
3110
|
+
font-weight: 700;
|
|
3111
|
+
letter-spacing: 0.02em;
|
|
3112
|
+
text-transform: uppercase;
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
.ui-badge.neutral {
|
|
3116
|
+
color: #334155;
|
|
3117
|
+
background: #eef2ff;
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
.ui-badge.success {
|
|
3121
|
+
color: #047857;
|
|
3122
|
+
background: #d1fae5;
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
.ui-badge.warning {
|
|
3126
|
+
color: #92400e;
|
|
3127
|
+
background: #fef3c7;
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
.ui-badge.danger {
|
|
3131
|
+
color: #991b1b;
|
|
3132
|
+
background: #fee2e2;
|
|
3133
|
+
}
|
|
1543
3134
|
`
|
|
1544
3135
|
},
|
|
1545
3136
|
{
|
|
@@ -1617,6 +3208,7 @@ export class UiInputComponent {
|
|
|
1617
3208
|
gap: 10px;
|
|
1618
3209
|
font-size: 13px;
|
|
1619
3210
|
color: #1f2937;
|
|
3211
|
+
min-width: 0;
|
|
1620
3212
|
}
|
|
1621
3213
|
|
|
1622
3214
|
.ui-control__label {
|
|
@@ -1632,14 +3224,14 @@ export class UiInputComponent {
|
|
|
1632
3224
|
}
|
|
1633
3225
|
|
|
1634
3226
|
.ui-control__info {
|
|
1635
|
-
margin-left:
|
|
1636
|
-
width:
|
|
1637
|
-
height:
|
|
3227
|
+
margin-left: 6px;
|
|
3228
|
+
width: 16px;
|
|
3229
|
+
height: 16px;
|
|
1638
3230
|
border-radius: 999px;
|
|
1639
|
-
border: 1px solid
|
|
3231
|
+
border: 1px solid var(--color-border);
|
|
1640
3232
|
background: #ffffff;
|
|
1641
|
-
color:
|
|
1642
|
-
font-size:
|
|
3233
|
+
color: var(--color-primary-strong);
|
|
3234
|
+
font-size: 10px;
|
|
1643
3235
|
line-height: 1;
|
|
1644
3236
|
display: inline-flex;
|
|
1645
3237
|
align-items: center;
|
|
@@ -1648,43 +3240,47 @@ export class UiInputComponent {
|
|
|
1648
3240
|
}
|
|
1649
3241
|
|
|
1650
3242
|
.ui-control__info:hover {
|
|
1651
|
-
background: #
|
|
3243
|
+
background: #ffffff;
|
|
1652
3244
|
}
|
|
1653
3245
|
|
|
1654
3246
|
.ui-control__info-panel {
|
|
1655
3247
|
margin-top: 8px;
|
|
1656
3248
|
padding: 10px 12px;
|
|
1657
|
-
border-radius:
|
|
1658
|
-
background: #
|
|
1659
|
-
border: 1px solid
|
|
3249
|
+
border-radius: 14px;
|
|
3250
|
+
background: #ffffff;
|
|
3251
|
+
border: 1px solid var(--color-border);
|
|
1660
3252
|
color: #475569;
|
|
1661
3253
|
font-size: 12px;
|
|
1662
3254
|
line-height: 1.4;
|
|
3255
|
+
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
|
|
1663
3256
|
}
|
|
1664
3257
|
|
|
1665
3258
|
.ui-control__input {
|
|
1666
3259
|
width: 100%;
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
3260
|
+
max-width: 100%;
|
|
3261
|
+
min-width: 0;
|
|
3262
|
+
min-height: 2.9rem;
|
|
3263
|
+
border-radius: 14px;
|
|
3264
|
+
border: 1px solid var(--color-border);
|
|
1670
3265
|
background: #ffffff;
|
|
1671
|
-
padding: 0.
|
|
1672
|
-
font-size:
|
|
3266
|
+
padding: 0.7rem 0.95rem;
|
|
3267
|
+
font-size: 14px;
|
|
1673
3268
|
font-weight: 500;
|
|
3269
|
+
box-sizing: border-box;
|
|
1674
3270
|
box-shadow: none;
|
|
1675
3271
|
outline: none;
|
|
1676
3272
|
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
|
1677
3273
|
}
|
|
1678
3274
|
|
|
1679
3275
|
.ui-control__input:focus {
|
|
1680
|
-
border-color: var(--color-primary);
|
|
1681
|
-
box-shadow: 0 0 0
|
|
3276
|
+
border-color: var(--color-primary-strong);
|
|
3277
|
+
box-shadow: 0 0 0 4px var(--color-primary-soft);
|
|
1682
3278
|
transform: translateY(-1px);
|
|
1683
3279
|
}
|
|
1684
3280
|
|
|
1685
3281
|
.ui-control__input.invalid {
|
|
1686
|
-
border-color:
|
|
1687
|
-
box-shadow: 0 0 0
|
|
3282
|
+
border-color: var(--color-accent);
|
|
3283
|
+
box-shadow: 0 0 0 3px var(--color-accent-soft);
|
|
1688
3284
|
}
|
|
1689
3285
|
|
|
1690
3286
|
.ui-control__input::placeholder {
|
|
@@ -1767,6 +3363,7 @@ export class UiTextareaComponent {
|
|
|
1767
3363
|
gap: 10px;
|
|
1768
3364
|
font-size: 13px;
|
|
1769
3365
|
color: #1f2937;
|
|
3366
|
+
min-width: 0;
|
|
1770
3367
|
}
|
|
1771
3368
|
|
|
1772
3369
|
.ui-control__label {
|
|
@@ -1782,14 +3379,14 @@ export class UiTextareaComponent {
|
|
|
1782
3379
|
}
|
|
1783
3380
|
|
|
1784
3381
|
.ui-control__info {
|
|
1785
|
-
margin-left:
|
|
1786
|
-
width:
|
|
1787
|
-
height:
|
|
3382
|
+
margin-left: 6px;
|
|
3383
|
+
width: 16px;
|
|
3384
|
+
height: 16px;
|
|
1788
3385
|
border-radius: 999px;
|
|
1789
|
-
border: 1px solid
|
|
3386
|
+
border: 1px solid var(--color-border);
|
|
1790
3387
|
background: #ffffff;
|
|
1791
|
-
color:
|
|
1792
|
-
font-size:
|
|
3388
|
+
color: var(--color-primary-strong);
|
|
3389
|
+
font-size: 10px;
|
|
1793
3390
|
line-height: 1;
|
|
1794
3391
|
display: inline-flex;
|
|
1795
3392
|
align-items: center;
|
|
@@ -1798,43 +3395,47 @@ export class UiTextareaComponent {
|
|
|
1798
3395
|
}
|
|
1799
3396
|
|
|
1800
3397
|
.ui-control__info:hover {
|
|
1801
|
-
background: #
|
|
3398
|
+
background: #ffffff;
|
|
1802
3399
|
}
|
|
1803
3400
|
|
|
1804
3401
|
.ui-control__info-panel {
|
|
1805
3402
|
margin-top: 8px;
|
|
1806
3403
|
padding: 10px 12px;
|
|
1807
|
-
border-radius:
|
|
1808
|
-
background: #
|
|
1809
|
-
border: 1px solid
|
|
3404
|
+
border-radius: 14px;
|
|
3405
|
+
background: #ffffff;
|
|
3406
|
+
border: 1px solid var(--color-border);
|
|
1810
3407
|
color: #475569;
|
|
1811
3408
|
font-size: 12px;
|
|
1812
3409
|
line-height: 1.4;
|
|
3410
|
+
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
|
|
1813
3411
|
}
|
|
1814
3412
|
|
|
1815
3413
|
.ui-control__input {
|
|
1816
3414
|
width: 100%;
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
3415
|
+
max-width: 100%;
|
|
3416
|
+
min-width: 0;
|
|
3417
|
+
min-height: 2.9rem;
|
|
3418
|
+
border-radius: 14px;
|
|
3419
|
+
border: 1px solid var(--color-border);
|
|
1820
3420
|
background: #ffffff;
|
|
1821
|
-
padding: 0.
|
|
1822
|
-
font-size:
|
|
3421
|
+
padding: 0.7rem 0.95rem;
|
|
3422
|
+
font-size: 14px;
|
|
1823
3423
|
font-weight: 500;
|
|
3424
|
+
box-sizing: border-box;
|
|
1824
3425
|
box-shadow: none;
|
|
1825
3426
|
outline: none;
|
|
1826
3427
|
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
|
1827
3428
|
}
|
|
1828
3429
|
|
|
1829
3430
|
.ui-control__input:focus {
|
|
1830
|
-
border-color: var(--color-primary);
|
|
1831
|
-
box-shadow: 0 0 0
|
|
3431
|
+
border-color: var(--color-primary-strong);
|
|
3432
|
+
box-shadow: 0 0 0 4px var(--color-primary-soft);
|
|
1832
3433
|
transform: translateY(-1px);
|
|
1833
3434
|
}
|
|
1834
3435
|
|
|
1835
3436
|
.ui-control__input.invalid {
|
|
1836
|
-
border-color:
|
|
1837
|
-
box-shadow: 0 0 0
|
|
3437
|
+
border-color: var(--color-accent);
|
|
3438
|
+
box-shadow: 0 0 0 3px var(--color-accent-soft);
|
|
1838
3439
|
}
|
|
1839
3440
|
|
|
1840
3441
|
.ui-control__input::placeholder {
|
|
@@ -1918,6 +3519,7 @@ export class UiSelectComponent {
|
|
|
1918
3519
|
gap: 10px;
|
|
1919
3520
|
font-size: 13px;
|
|
1920
3521
|
color: #1f2937;
|
|
3522
|
+
min-width: 0;
|
|
1921
3523
|
}
|
|
1922
3524
|
|
|
1923
3525
|
.ui-control__label {
|
|
@@ -1933,14 +3535,14 @@ export class UiSelectComponent {
|
|
|
1933
3535
|
}
|
|
1934
3536
|
|
|
1935
3537
|
.ui-control__info {
|
|
1936
|
-
margin-left:
|
|
1937
|
-
width:
|
|
1938
|
-
height:
|
|
3538
|
+
margin-left: 6px;
|
|
3539
|
+
width: 16px;
|
|
3540
|
+
height: 16px;
|
|
1939
3541
|
border-radius: 999px;
|
|
1940
|
-
border: 1px solid
|
|
3542
|
+
border: 1px solid var(--color-border);
|
|
1941
3543
|
background: #ffffff;
|
|
1942
|
-
color:
|
|
1943
|
-
font-size:
|
|
3544
|
+
color: var(--color-primary-strong);
|
|
3545
|
+
font-size: 10px;
|
|
1944
3546
|
line-height: 1;
|
|
1945
3547
|
display: inline-flex;
|
|
1946
3548
|
align-items: center;
|
|
@@ -1949,29 +3551,33 @@ export class UiSelectComponent {
|
|
|
1949
3551
|
}
|
|
1950
3552
|
|
|
1951
3553
|
.ui-control__info:hover {
|
|
1952
|
-
background: #
|
|
3554
|
+
background: #ffffff;
|
|
1953
3555
|
}
|
|
1954
3556
|
|
|
1955
3557
|
.ui-control__info-panel {
|
|
1956
3558
|
margin-top: 8px;
|
|
1957
3559
|
padding: 10px 12px;
|
|
1958
|
-
border-radius:
|
|
1959
|
-
background: #
|
|
1960
|
-
border: 1px solid
|
|
3560
|
+
border-radius: 14px;
|
|
3561
|
+
background: #ffffff;
|
|
3562
|
+
border: 1px solid var(--color-border);
|
|
1961
3563
|
color: #475569;
|
|
1962
3564
|
font-size: 12px;
|
|
1963
3565
|
line-height: 1.4;
|
|
3566
|
+
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
|
|
1964
3567
|
}
|
|
1965
3568
|
|
|
1966
3569
|
.ui-control__select {
|
|
1967
3570
|
width: 100%;
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
3571
|
+
max-width: 100%;
|
|
3572
|
+
min-width: 0;
|
|
3573
|
+
min-height: 2.9rem;
|
|
3574
|
+
border-radius: 14px;
|
|
3575
|
+
border: 1px solid var(--color-border);
|
|
1971
3576
|
background: #ffffff;
|
|
1972
|
-
padding: 0.
|
|
1973
|
-
font-size:
|
|
3577
|
+
padding: 0.7rem 2.3rem 0.7rem 0.95rem;
|
|
3578
|
+
font-size: 14px;
|
|
1974
3579
|
font-weight: 500;
|
|
3580
|
+
box-sizing: border-box;
|
|
1975
3581
|
box-shadow: none;
|
|
1976
3582
|
outline: none;
|
|
1977
3583
|
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
|
@@ -1983,14 +3589,14 @@ export class UiSelectComponent {
|
|
|
1983
3589
|
}
|
|
1984
3590
|
|
|
1985
3591
|
.ui-control__select:focus {
|
|
1986
|
-
border-color: var(--color-primary);
|
|
1987
|
-
box-shadow: 0 0 0
|
|
3592
|
+
border-color: var(--color-primary-strong);
|
|
3593
|
+
box-shadow: 0 0 0 4px var(--color-primary-soft);
|
|
1988
3594
|
transform: translateY(-1px);
|
|
1989
3595
|
}
|
|
1990
3596
|
|
|
1991
3597
|
.ui-control__select.invalid {
|
|
1992
|
-
border-color:
|
|
1993
|
-
box-shadow: 0 0 0
|
|
3598
|
+
border-color: var(--color-accent);
|
|
3599
|
+
box-shadow: 0 0 0 3px var(--color-accent-soft);
|
|
1994
3600
|
}
|
|
1995
3601
|
`
|
|
1996
3602
|
},
|
|
@@ -2066,6 +3672,7 @@ export class UiCheckboxComponent {
|
|
|
2066
3672
|
gap: 10px;
|
|
2067
3673
|
font-size: 13px;
|
|
2068
3674
|
color: #1f2937;
|
|
3675
|
+
min-width: 0;
|
|
2069
3676
|
}
|
|
2070
3677
|
|
|
2071
3678
|
.ui-control__label {
|
|
@@ -2081,14 +3688,14 @@ export class UiCheckboxComponent {
|
|
|
2081
3688
|
}
|
|
2082
3689
|
|
|
2083
3690
|
.ui-control__info {
|
|
2084
|
-
margin-left:
|
|
2085
|
-
width:
|
|
2086
|
-
height:
|
|
3691
|
+
margin-left: 6px;
|
|
3692
|
+
width: 16px;
|
|
3693
|
+
height: 16px;
|
|
2087
3694
|
border-radius: 999px;
|
|
2088
|
-
border: 1px solid
|
|
3695
|
+
border: 1px solid var(--color-border);
|
|
2089
3696
|
background: #ffffff;
|
|
2090
|
-
color:
|
|
2091
|
-
font-size:
|
|
3697
|
+
color: var(--color-primary-strong);
|
|
3698
|
+
font-size: 10px;
|
|
2092
3699
|
line-height: 1;
|
|
2093
3700
|
display: inline-flex;
|
|
2094
3701
|
align-items: center;
|
|
@@ -2097,27 +3704,31 @@ export class UiCheckboxComponent {
|
|
|
2097
3704
|
}
|
|
2098
3705
|
|
|
2099
3706
|
.ui-control__info:hover {
|
|
2100
|
-
background: #
|
|
3707
|
+
background: #ffffff;
|
|
2101
3708
|
}
|
|
2102
3709
|
|
|
2103
3710
|
.ui-control__info-panel {
|
|
2104
3711
|
margin-top: 8px;
|
|
2105
3712
|
padding: 10px 12px;
|
|
2106
|
-
border-radius:
|
|
2107
|
-
background: #
|
|
2108
|
-
border: 1px solid
|
|
3713
|
+
border-radius: 14px;
|
|
3714
|
+
background: #ffffff;
|
|
3715
|
+
border: 1px solid var(--color-border);
|
|
2109
3716
|
color: #475569;
|
|
2110
3717
|
font-size: 12px;
|
|
2111
3718
|
line-height: 1.4;
|
|
3719
|
+
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
|
|
2112
3720
|
}
|
|
2113
3721
|
|
|
2114
3722
|
.ui-control__checkbox {
|
|
2115
|
-
width:
|
|
2116
|
-
height:
|
|
3723
|
+
width: 22px;
|
|
3724
|
+
height: 22px;
|
|
2117
3725
|
padding: 0;
|
|
2118
|
-
border-radius:
|
|
2119
|
-
|
|
2120
|
-
|
|
3726
|
+
border-radius: 8px;
|
|
3727
|
+
border: 2px solid var(--color-border);
|
|
3728
|
+
background: #ffffff;
|
|
3729
|
+
box-shadow: 0 8px 18px rgba(99, 102, 241, 0.12);
|
|
3730
|
+
accent-color: var(--color-primary-strong);
|
|
3731
|
+
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
2121
3732
|
}
|
|
2122
3733
|
`
|
|
2123
3734
|
}
|
|
@@ -2184,7 +3795,12 @@ function toFolderName(operationId) {
|
|
|
2184
3795
|
return toFileBase(operationId);
|
|
2185
3796
|
}
|
|
2186
3797
|
function toFileBase(operationId) {
|
|
2187
|
-
return operationId;
|
|
3798
|
+
return String(operationId).replace(/[\\/]/g, '');
|
|
3799
|
+
}
|
|
3800
|
+
function toSafeFileName(value) {
|
|
3801
|
+
return String(value)
|
|
3802
|
+
.replace(/[\\/]/g, '-')
|
|
3803
|
+
.replace(/\s+/g, '-');
|
|
2188
3804
|
}
|
|
2189
3805
|
function toKebab(value) {
|
|
2190
3806
|
return value
|
|
@@ -2201,6 +3817,15 @@ function toPascalCase(value) {
|
|
|
2201
3817
|
.map(part => part[0].toUpperCase() + part.slice(1))
|
|
2202
3818
|
.join('');
|
|
2203
3819
|
}
|
|
3820
|
+
function normalizeRoutePath(value) {
|
|
3821
|
+
const trimmed = String(value ?? '').trim();
|
|
3822
|
+
if (!trimmed)
|
|
3823
|
+
return trimmed;
|
|
3824
|
+
if (trimmed.includes('/'))
|
|
3825
|
+
return trimmed.replace(/^\//, '');
|
|
3826
|
+
const pascal = toPascalCase(trimmed);
|
|
3827
|
+
return toRouteSegment(pascal);
|
|
3828
|
+
}
|
|
2204
3829
|
function buildSchemaImportPath(featureDir, schemasRoot, rawName) {
|
|
2205
3830
|
const schemaFile = path_1.default.join(schemasRoot, 'overlays', `${rawName}.screen.json`);
|
|
2206
3831
|
let relativePath = path_1.default.relative(featureDir, schemaFile);
|