generate-ui-cli 2.1.7 → 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 +216 -27
- package/dist/commands/angular.js +535 -40
- package/dist/commands/generate.js +350 -20
- package/dist/commands/login.js +131 -42
- package/dist/commands/merge.js +201 -0
- package/dist/generate-ui/routes.gen.js +4 -0
- package/dist/generators/angular/feature.generator.js +3183 -583
- package/dist/generators/angular/menu.generator.js +165 -0
- 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 +88 -12
- package/dist/license/guard.js +2 -1
- package/dist/license/permissions.js +63 -9
- package/dist/license/token.js +38 -3
- package/dist/postinstall.js +47 -0
- package/dist/runtime/config.js +4 -0
- package/dist/runtime/logger.js +29 -0
- package/dist/runtime/project-config.js +64 -0
- package/package.json +1 -1
|
@@ -4,17 +4,18 @@ 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
|
-
ensureUiComponents(appRoot);
|
|
17
|
+
const appRoot = path_1.default.resolve(featuresRoot, '..');
|
|
18
|
+
ensureUiComponents(appRoot, schemasRoot);
|
|
18
19
|
const method = String(schema.api.method || '').toLowerCase();
|
|
19
20
|
const endpoint = String(schema.api.endpoint || '');
|
|
20
21
|
const baseUrl = String(schema.api.baseUrl || 'https://api.realworld.io/api');
|
|
@@ -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,44 +49,120 @@ 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
|
-
|
|
57
|
-
|
|
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'
|
|
66
|
+
import { CommonModule } from '@angular/common'
|
|
58
67
|
import { FormBuilder, ReactiveFormsModule } from '@angular/forms'
|
|
59
|
-
import { UiCardComponent } from '
|
|
60
|
-
import {
|
|
61
|
-
import {
|
|
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}'
|
|
62
74
|
import { ${name}Service } from './${fileBase}.service.gen'
|
|
63
75
|
import { ${name}Gen } from './${fileBase}.gen'
|
|
64
76
|
import screenSchema from '${schemaImportPath}'
|
|
77
|
+
import { BehaviorSubject } from 'rxjs'
|
|
65
78
|
|
|
66
79
|
@Component({
|
|
67
80
|
selector: 'app-${toKebab(name)}',
|
|
68
81
|
standalone: true,
|
|
69
82
|
imports: [
|
|
70
|
-
|
|
71
|
-
NgFor,
|
|
72
|
-
JsonPipe,
|
|
83
|
+
CommonModule,
|
|
73
84
|
ReactiveFormsModule,
|
|
74
85
|
UiCardComponent,
|
|
75
|
-
|
|
76
|
-
|
|
86
|
+
UiButtonComponent,
|
|
87
|
+
UiSelectComponent,
|
|
88
|
+
UiCheckboxComponent,
|
|
89
|
+
UiInputComponent,
|
|
90
|
+
UiTextareaComponent
|
|
77
91
|
],
|
|
78
92
|
templateUrl: './${fileBase}.component.html',
|
|
79
93
|
styleUrls: ['./${fileBase}.component.scss']
|
|
80
94
|
})
|
|
81
|
-
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
|
+
|
|
82
113
|
constructor(
|
|
83
114
|
protected override fb: FormBuilder,
|
|
84
115
|
protected override service: ${name}Service
|
|
85
116
|
) {
|
|
86
117
|
super(fb, service)
|
|
87
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
|
+
}
|
|
88
166
|
}
|
|
89
167
|
|
|
90
168
|
submit() {
|
|
@@ -94,53 +172,91 @@ export class ${name}Component extends ${name}Gen {
|
|
|
94
172
|
const body = this.pick(value, this.bodyFieldNames)
|
|
95
173
|
|
|
96
174
|
this.loading = true
|
|
97
|
-
this.error
|
|
175
|
+
this.error$.next(null)
|
|
98
176
|
|
|
99
177
|
this.service
|
|
100
178
|
.execute(pathParams, queryParams, body)
|
|
101
179
|
.subscribe({
|
|
102
|
-
next: result => {
|
|
103
|
-
|
|
180
|
+
next: (result: any) => {
|
|
181
|
+
const normalized =
|
|
182
|
+
result && typeof result === 'object' && 'body' in result
|
|
183
|
+
? (result as any).body
|
|
184
|
+
: result
|
|
185
|
+
this.result$.next(normalized)
|
|
104
186
|
this.loading = false
|
|
105
187
|
},
|
|
106
|
-
error: error => {
|
|
107
|
-
this.error
|
|
188
|
+
error: (error: any) => {
|
|
189
|
+
this.error$.next(error)
|
|
108
190
|
this.loading = false
|
|
109
191
|
}
|
|
110
192
|
})
|
|
111
193
|
}
|
|
112
194
|
|
|
113
|
-
isArrayResult() {
|
|
114
|
-
return this.getRows().length > 0
|
|
195
|
+
isArrayResult(raw?: any) {
|
|
196
|
+
return this.getRows(raw).length > 0
|
|
115
197
|
}
|
|
116
198
|
|
|
117
|
-
getRows() {
|
|
118
|
-
const value = this.result
|
|
199
|
+
getRows(raw?: any) {
|
|
200
|
+
const value = this.unwrapResult(raw ?? this.result$.value)
|
|
119
201
|
if (Array.isArray(value)) return value
|
|
120
202
|
if (!value || typeof value !== 'object') return []
|
|
121
203
|
|
|
122
|
-
const commonKeys = ['data', 'items', 'results', 'list', 'records']
|
|
204
|
+
const commonKeys = ['data', 'items', 'results', 'list', 'records', 'products']
|
|
123
205
|
for (const key of commonKeys) {
|
|
124
206
|
if (Array.isArray(value[key])) return value[key]
|
|
125
207
|
}
|
|
126
208
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
209
|
+
if (!this.allowDeepArraySearch) return []
|
|
210
|
+
const found = this.findFirstArray(value, 0, 5)
|
|
211
|
+
return found ?? []
|
|
212
|
+
}
|
|
130
213
|
|
|
131
|
-
|
|
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 }>
|
|
132
235
|
}
|
|
133
236
|
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
138
254
|
.split(',')
|
|
139
255
|
.map((value: string) => value.trim())
|
|
140
256
|
.filter(Boolean)
|
|
141
257
|
}
|
|
142
258
|
|
|
143
|
-
const rows = this.getRows()
|
|
259
|
+
const rows = this.getRows(value)
|
|
144
260
|
if (rows.length > 0 && rows[0] && typeof rows[0] === 'object') {
|
|
145
261
|
return Object.keys(rows[0])
|
|
146
262
|
}
|
|
@@ -149,10 +265,12 @@ export class ${name}Component extends ${name}Gen {
|
|
|
149
265
|
}
|
|
150
266
|
|
|
151
267
|
formatHeader(value: string) {
|
|
268
|
+
const configured = this.getColumnLabel(value)
|
|
269
|
+
if (configured) return configured
|
|
152
270
|
return value
|
|
153
271
|
.replace(/[_-]/g, ' ')
|
|
154
272
|
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
155
|
-
.replace(
|
|
273
|
+
.replace(/\\b\\w/g, char => char.toUpperCase())
|
|
156
274
|
}
|
|
157
275
|
|
|
158
276
|
getCellValue(row: any, column: string) {
|
|
@@ -177,7 +295,7 @@ export class ${name}Component extends ${name}Gen {
|
|
|
177
295
|
)
|
|
178
296
|
}
|
|
179
297
|
|
|
180
|
-
|
|
298
|
+
formatValue(value: any): string {
|
|
181
299
|
if (value === null || value === undefined) return ''
|
|
182
300
|
if (typeof value === 'string' || typeof value === 'number') {
|
|
183
301
|
return String(value)
|
|
@@ -200,14 +318,87 @@ export class ${name}Component extends ${name}Gen {
|
|
|
200
318
|
return String(value)
|
|
201
319
|
}
|
|
202
320
|
|
|
203
|
-
|
|
204
|
-
|
|
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)
|
|
205
367
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
206
368
|
return []
|
|
207
369
|
}
|
|
208
370
|
return this.flattenObject(value)
|
|
209
371
|
}
|
|
210
372
|
|
|
373
|
+
hasObjectRows(raw?: any) {
|
|
374
|
+
return this.getObjectRows(raw).length > 0
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
isSingleValue(raw?: any) {
|
|
378
|
+
const value = this.unwrapResult(raw ?? this.result$.value)
|
|
379
|
+
return (
|
|
380
|
+
value !== null &&
|
|
381
|
+
value !== undefined &&
|
|
382
|
+
(typeof value === 'string' ||
|
|
383
|
+
typeof value === 'number' ||
|
|
384
|
+
typeof value === 'boolean')
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private unwrapResult(value: any) {
|
|
389
|
+
if (!value || typeof value !== 'object') return value
|
|
390
|
+
if (Object.prototype.hasOwnProperty.call(value, 'data')) {
|
|
391
|
+
return value.data
|
|
392
|
+
}
|
|
393
|
+
if (Object.prototype.hasOwnProperty.call(value, 'result')) {
|
|
394
|
+
return value.result
|
|
395
|
+
}
|
|
396
|
+
if (Object.prototype.hasOwnProperty.call(value, 'body')) {
|
|
397
|
+
return value.body
|
|
398
|
+
}
|
|
399
|
+
return value
|
|
400
|
+
}
|
|
401
|
+
|
|
211
402
|
private flattenObject(
|
|
212
403
|
value: Record<string, any>,
|
|
213
404
|
prefix = ''
|
|
@@ -224,8 +415,25 @@ export class ${name}Component extends ${name}Gen {
|
|
|
224
415
|
return rows
|
|
225
416
|
}
|
|
226
417
|
|
|
418
|
+
private findFirstArray(
|
|
419
|
+
value: any,
|
|
420
|
+
depth: number,
|
|
421
|
+
maxDepth: number
|
|
422
|
+
): any[] | null {
|
|
423
|
+
if (!value || depth > maxDepth) return null
|
|
424
|
+
if (Array.isArray(value)) return value
|
|
425
|
+
if (typeof value !== 'object') return null
|
|
426
|
+
|
|
427
|
+
for (const key of Object.keys(value)) {
|
|
428
|
+
const found = this.findFirstArray(value[key], depth + 1, maxDepth)
|
|
429
|
+
if (found) return found
|
|
430
|
+
}
|
|
431
|
+
return null
|
|
432
|
+
}
|
|
433
|
+
|
|
227
434
|
}
|
|
228
|
-
|
|
435
|
+
`;
|
|
436
|
+
fs_1.default.writeFileSync(componentPath, generatedComponentSource);
|
|
229
437
|
/**
|
|
230
438
|
* 2️⃣ Arquivo gerado (sempre sobrescreve)
|
|
231
439
|
*/
|
|
@@ -233,6 +441,7 @@ export class ${name}Component extends ${name}Gen {
|
|
|
233
441
|
fs_1.default.writeFileSync(genTsPath, `
|
|
234
442
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms'
|
|
235
443
|
import { Injectable } from '@angular/core'
|
|
444
|
+
import { BehaviorSubject } from 'rxjs'
|
|
236
445
|
import { ${name}Service } from './${fileBase}.service.gen'
|
|
237
446
|
|
|
238
447
|
@Injectable()
|
|
@@ -245,8 +454,8 @@ export class ${name}Gen {
|
|
|
245
454
|
schema: any
|
|
246
455
|
|
|
247
456
|
loading = false
|
|
248
|
-
result
|
|
249
|
-
error
|
|
457
|
+
readonly result$ = new BehaviorSubject<any>(null)
|
|
458
|
+
readonly error$ = new BehaviorSubject<any>(null)
|
|
250
459
|
|
|
251
460
|
constructor(
|
|
252
461
|
protected fb: FormBuilder,
|
|
@@ -280,10 +489,19 @@ export class ${name}Gen {
|
|
|
280
489
|
}
|
|
281
490
|
|
|
282
491
|
protected isSelect(field: any) {
|
|
492
|
+
if (field?.ui === 'select' || field?.ui === 'dropdown') return true
|
|
283
493
|
return Array.isArray(field.options) && field.options.length > 0
|
|
284
494
|
}
|
|
285
495
|
|
|
496
|
+
protected getSelectOptions(field: any) {
|
|
497
|
+
if (Array.isArray(field.options) && field.options.length > 0) {
|
|
498
|
+
return field.options
|
|
499
|
+
}
|
|
500
|
+
return []
|
|
501
|
+
}
|
|
502
|
+
|
|
286
503
|
protected isCheckbox(field: any) {
|
|
504
|
+
if (field?.ui === 'select' || field?.ui === 'dropdown') return false
|
|
287
505
|
return field.type === 'boolean'
|
|
288
506
|
}
|
|
289
507
|
|
|
@@ -447,519 +665,2812 @@ export class ${name}Service {
|
|
|
447
665
|
formFields,
|
|
448
666
|
actionLabel,
|
|
449
667
|
method,
|
|
450
|
-
hasForm: formFields.length > 0
|
|
668
|
+
hasForm: formFields.length > 0,
|
|
669
|
+
responseFormat
|
|
451
670
|
}));
|
|
452
671
|
/**
|
|
453
672
|
* 5️⃣ SCSS base
|
|
454
673
|
*/
|
|
455
674
|
const scssPath = path_1.default.join(featureDir, `${fileBase}.component.scss`);
|
|
456
|
-
fs_1.default.writeFileSync(scssPath,
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
display: grid;
|
|
464
|
-
gap: 16px;
|
|
675
|
+
fs_1.default.writeFileSync(scssPath, buildBaseScss());
|
|
676
|
+
return {
|
|
677
|
+
path: toRouteSegment(name),
|
|
678
|
+
component: `${name}Component`,
|
|
679
|
+
folder,
|
|
680
|
+
fileBase
|
|
681
|
+
};
|
|
465
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}'
|
|
466
764
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
+
}
|
|
473
798
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
|
482
807
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
}
|
|
808
|
+
constructor(
|
|
809
|
+
private router: Router,
|
|
810
|
+
private listService: ${listName}Service
|
|
811
|
+
${hasDelete ? `, private deleteService: ${deleteName}Service` : ''}
|
|
812
|
+
) {
|
|
813
|
+
}
|
|
490
814
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
color: #e2e8f0;
|
|
497
|
-
font-size: 12px;
|
|
498
|
-
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.25);
|
|
499
|
-
overflow: auto;
|
|
500
|
-
}
|
|
815
|
+
ngOnInit() {
|
|
816
|
+
this.setupAutoRefresh()
|
|
817
|
+
this.ensureInitialLoad()
|
|
818
|
+
this.setupAutoRefreshListeners()
|
|
819
|
+
}
|
|
501
820
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
border-radius: 16px;
|
|
506
|
-
border: 1px solid #e2e8f0;
|
|
507
|
-
box-shadow: 0 20px 40px rgba(15, 23, 42, 0.08);
|
|
508
|
-
}
|
|
821
|
+
ngAfterViewInit() {
|
|
822
|
+
this.ensureInitialLoad()
|
|
823
|
+
}
|
|
509
824
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
}
|
|
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
|
+
}
|
|
518
833
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
}
|
|
834
|
+
private setupAutoRefresh() {
|
|
835
|
+
this.load()
|
|
836
|
+
}
|
|
523
837
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
}
|
|
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
|
+
}
|
|
531
846
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
847
|
+
private ensureInitialLoad() {
|
|
848
|
+
if (this.hasInitialLoad) return
|
|
849
|
+
this.hasInitialLoad = true
|
|
850
|
+
this.load()
|
|
851
|
+
}
|
|
536
852
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
+
}
|
|
544
874
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
text-align: right;
|
|
549
|
-
}
|
|
875
|
+
refresh() {
|
|
876
|
+
this.load()
|
|
877
|
+
}
|
|
550
878
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
background: #ffffff;
|
|
555
|
-
font-size: 14px;
|
|
556
|
-
}
|
|
879
|
+
onSearch(value: string) {
|
|
880
|
+
this.searchTerm = value
|
|
881
|
+
}
|
|
557
882
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
}
|
|
883
|
+
isArrayResult(raw?: any) {
|
|
884
|
+
return this.getVisibleRows(raw).length > 0
|
|
885
|
+
}
|
|
561
886
|
|
|
562
|
-
|
|
563
|
-
.
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
border-bottom: 1px solid #e2e8f0;
|
|
567
|
-
color: #0f172a;
|
|
568
|
-
vertical-align: middle;
|
|
569
|
-
}
|
|
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 []
|
|
570
891
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
text-transform: uppercase;
|
|
576
|
-
color: #475569;
|
|
577
|
-
}
|
|
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
|
+
}
|
|
578
896
|
|
|
579
|
-
.
|
|
580
|
-
|
|
581
|
-
}
|
|
897
|
+
const found = this.findFirstArray(value, 0, 5)
|
|
898
|
+
return found ?? []
|
|
899
|
+
}
|
|
582
900
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
}
|
|
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
|
+
}
|
|
590
907
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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 }>
|
|
594
929
|
}
|
|
595
930
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
931
|
+
private getColumnLabel(value: string) {
|
|
932
|
+
const configured = this.getConfiguredColumns()
|
|
933
|
+
const match = configured.find(column => column.key === value)
|
|
934
|
+
return match?.label ?? ''
|
|
599
935
|
}
|
|
600
936
|
|
|
601
|
-
|
|
602
|
-
|
|
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
|
|
603
1346
|
}
|
|
604
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>
|
|
605
1511
|
`);
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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;
|
|
612
1519
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
required: Boolean(field.required),
|
|
618
|
-
label: field.label || toLabel(field.name),
|
|
619
|
-
placeholder: field.placeholder || toPlaceholder(field.name),
|
|
620
|
-
hint: field.hint || undefined,
|
|
621
|
-
info: field.info || undefined,
|
|
622
|
-
options: field.options || null,
|
|
623
|
-
defaultValue: field.defaultValue ?? null,
|
|
624
|
-
source: 'body'
|
|
625
|
-
}));
|
|
1520
|
+
|
|
1521
|
+
.admin-toolbar__meta {
|
|
1522
|
+
display: inline-flex;
|
|
1523
|
+
justify-content: flex-end;
|
|
626
1524
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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);
|
|
2357
|
+
font-size: 12px;
|
|
2358
|
+
line-height: 1.4;
|
|
2359
|
+
white-space: pre-wrap;
|
|
2360
|
+
word-break: break-word;
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
.result-single {
|
|
2364
|
+
margin-top: 24px;
|
|
2365
|
+
padding: 16px 18px;
|
|
2366
|
+
border-radius: 16px;
|
|
2367
|
+
border: 1px solid rgba(15, 23, 42, 0.08);
|
|
2368
|
+
background: #ffffff;
|
|
2369
|
+
color: #0f172a;
|
|
2370
|
+
display: grid;
|
|
2371
|
+
gap: 8px;
|
|
2372
|
+
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.08);
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
.result-single__value {
|
|
2376
|
+
font-size: 18px;
|
|
2377
|
+
font-weight: 700;
|
|
2378
|
+
color: #0f172a;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
.data-table {
|
|
2382
|
+
width: max-content;
|
|
2383
|
+
min-width: 100%;
|
|
2384
|
+
border-collapse: collapse;
|
|
2385
|
+
background: #ffffff;
|
|
2386
|
+
font-size: 14px;
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
.data-table thead {
|
|
2390
|
+
background: #f8fafc;
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
.data-table th,
|
|
2394
|
+
.data-table td {
|
|
2395
|
+
padding: 12px 14px;
|
|
2396
|
+
text-align: left;
|
|
2397
|
+
border-bottom: 1px solid #e2e8f0;
|
|
2398
|
+
color: #0f172a;
|
|
2399
|
+
vertical-align: middle;
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
.data-table th {
|
|
2403
|
+
font-weight: 700;
|
|
2404
|
+
font-size: 12px;
|
|
2405
|
+
letter-spacing: 0.08em;
|
|
2406
|
+
text-transform: uppercase;
|
|
2407
|
+
color: #475569;
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
.data-table tbody tr:hover {
|
|
2411
|
+
background: #f1f5f9;
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
.cell-image {
|
|
2415
|
+
width: 44px;
|
|
2416
|
+
height: 28px;
|
|
2417
|
+
object-fit: cover;
|
|
2418
|
+
border-radius: 6px;
|
|
2419
|
+
box-shadow: 0 6px 12px rgba(15, 23, 42, 0.16);
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
:host ::ng-deep ui-card .ui-card {
|
|
2423
|
+
display: flex;
|
|
2424
|
+
flex-direction: column;
|
|
2425
|
+
max-height: calc(100vh - 160px);
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
:host ::ng-deep ui-card .ui-card__body {
|
|
2429
|
+
overflow: auto;
|
|
2430
|
+
min-height: 0;
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
@media (max-width: 1024px) {
|
|
2434
|
+
.form-grid {
|
|
2435
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
2436
|
+
max-width: 100%;
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
@media (max-width: 820px) {
|
|
2441
|
+
:host {
|
|
2442
|
+
padding: 18px;
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
.form-grid {
|
|
2446
|
+
grid-template-columns: 1fr;
|
|
2447
|
+
max-width: 100%;
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
.actions {
|
|
2451
|
+
justify-content: stretch;
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
`;
|
|
2455
|
+
}
|
|
2456
|
+
function buildFieldHtml(field) {
|
|
2457
|
+
return '';
|
|
2458
|
+
}
|
|
2459
|
+
function inputTypeFor(type) {
|
|
2460
|
+
switch (type) {
|
|
2461
|
+
case 'number':
|
|
2462
|
+
case 'integer':
|
|
2463
|
+
return 'number';
|
|
2464
|
+
case 'boolean':
|
|
2465
|
+
return 'checkbox';
|
|
2466
|
+
default:
|
|
2467
|
+
return 'text';
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
function httpCallForMethod(method) {
|
|
2471
|
+
switch (method) {
|
|
2472
|
+
case 'get':
|
|
2473
|
+
return 'return this.http.get(url)';
|
|
2474
|
+
case 'delete':
|
|
2475
|
+
return 'return this.http.delete(url)';
|
|
2476
|
+
case 'post':
|
|
2477
|
+
return 'return this.http.post(url, body)';
|
|
2478
|
+
case 'put':
|
|
2479
|
+
return 'return this.http.put(url, body)';
|
|
2480
|
+
case 'patch':
|
|
2481
|
+
return 'return this.http.patch(url, body)';
|
|
2482
|
+
default:
|
|
2483
|
+
return 'return this.http.get(url)';
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
function ensureUiComponents(appRoot, schemasRoot) {
|
|
2487
|
+
const uiRoot = path_1.default.join(appRoot, 'ui');
|
|
2488
|
+
const components = [
|
|
2489
|
+
{
|
|
2490
|
+
name: 'ui-card',
|
|
2491
|
+
template: `
|
|
2492
|
+
import { Component, Input } from '@angular/core'
|
|
2493
|
+
import { NgIf } from '@angular/common'
|
|
2494
|
+
|
|
2495
|
+
@Component({
|
|
2496
|
+
selector: 'ui-card',
|
|
2497
|
+
standalone: true,
|
|
2498
|
+
imports: [NgIf],
|
|
2499
|
+
templateUrl: './ui-card.component.html',
|
|
2500
|
+
styleUrls: ['./ui-card.component.scss']
|
|
2501
|
+
})
|
|
2502
|
+
export class UiCardComponent {
|
|
2503
|
+
@Input() title?: string
|
|
2504
|
+
@Input() subtitle?: string
|
|
2505
|
+
}
|
|
2506
|
+
`,
|
|
2507
|
+
html: `
|
|
2508
|
+
<section class="ui-card">
|
|
2509
|
+
<header class="ui-card__header" *ngIf="title || subtitle">
|
|
2510
|
+
<h2 class="ui-card__title" *ngIf="title">{{ title }}</h2>
|
|
2511
|
+
<p class="ui-card__subtitle" *ngIf="subtitle">{{ subtitle }}</p>
|
|
2512
|
+
</header>
|
|
2513
|
+
<div class="ui-card__body">
|
|
2514
|
+
<ng-content></ng-content>
|
|
2515
|
+
</div>
|
|
2516
|
+
</section>
|
|
2517
|
+
`,
|
|
2518
|
+
scss: `
|
|
2519
|
+
:host {
|
|
2520
|
+
display: block;
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
.ui-card {
|
|
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;
|
|
2529
|
+
position: relative;
|
|
2530
|
+
overflow: hidden;
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
.ui-card::before {
|
|
2534
|
+
content: "";
|
|
2535
|
+
position: absolute;
|
|
2536
|
+
inset: 0 0 auto 0;
|
|
2537
|
+
height: 3px;
|
|
2538
|
+
background: linear-gradient(90deg, #1d4ed8, #2563eb);
|
|
2539
|
+
opacity: 0.85;
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
.ui-card__header {
|
|
2543
|
+
margin-bottom: 18px;
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
.ui-card__title {
|
|
2547
|
+
margin: 0;
|
|
2548
|
+
font-size: 24px;
|
|
2549
|
+
font-weight: 600;
|
|
2550
|
+
color: #0f172a;
|
|
2551
|
+
letter-spacing: -0.02em;
|
|
2552
|
+
font-family: "Instrument Serif", Georgia, serif;
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
.ui-card__subtitle {
|
|
2556
|
+
margin: 8px 0 0;
|
|
2557
|
+
font-size: 11px;
|
|
2558
|
+
color: #64748b;
|
|
2559
|
+
letter-spacing: 0.14em;
|
|
2560
|
+
text-transform: uppercase;
|
|
2561
|
+
font-family: "DM Sans", "Segoe UI", sans-serif;
|
|
2562
|
+
}
|
|
2563
|
+
`
|
|
2564
|
+
},
|
|
2565
|
+
{
|
|
2566
|
+
name: 'ui-menu',
|
|
2567
|
+
template: '',
|
|
2568
|
+
html: `
|
|
2569
|
+
<nav class="ui-menu">
|
|
2570
|
+
<div class="ui-menu__brand">
|
|
2571
|
+
<span class="ui-menu__brand-pill"></span>
|
|
2572
|
+
<span class="ui-menu__brand-title">{{ title }}</span>
|
|
2573
|
+
</div>
|
|
2574
|
+
|
|
2575
|
+
<ng-container *ngFor="let group of menu.groups">
|
|
2576
|
+
<section
|
|
2577
|
+
class="ui-menu__group"
|
|
2578
|
+
*ngIf="!group.hidden && group.items.length"
|
|
2579
|
+
>
|
|
2580
|
+
<h3 class="ui-menu__group-title">{{ group.label }}</h3>
|
|
2581
|
+
<a
|
|
2582
|
+
class="ui-menu__item"
|
|
2583
|
+
*ngFor="let item of group.items"
|
|
2584
|
+
[routerLink]="item.route"
|
|
2585
|
+
routerLinkActive="active"
|
|
2586
|
+
[class.hidden]="item.hidden"
|
|
2587
|
+
>
|
|
2588
|
+
{{ item.label }}
|
|
2589
|
+
</a>
|
|
2590
|
+
</section>
|
|
2591
|
+
</ng-container>
|
|
2592
|
+
|
|
2593
|
+
<section
|
|
2594
|
+
class="ui-menu__group"
|
|
2595
|
+
*ngIf="menu.ungrouped.length"
|
|
2596
|
+
>
|
|
2597
|
+
<h3 class="ui-menu__group-title">Other</h3>
|
|
2598
|
+
<a
|
|
2599
|
+
class="ui-menu__item"
|
|
2600
|
+
*ngFor="let item of menu.ungrouped"
|
|
2601
|
+
[routerLink]="item.route"
|
|
2602
|
+
routerLinkActive="active"
|
|
2603
|
+
[class.hidden]="item.hidden"
|
|
2604
|
+
>
|
|
2605
|
+
{{ item.label }}
|
|
2606
|
+
</a>
|
|
2607
|
+
</section>
|
|
2608
|
+
</nav>
|
|
2609
|
+
`,
|
|
2610
|
+
scss: `
|
|
2611
|
+
:host {
|
|
2612
|
+
display: block;
|
|
2613
|
+
height: 100%;
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
.ui-menu {
|
|
2617
|
+
position: sticky;
|
|
2618
|
+
top: 24px;
|
|
2619
|
+
align-self: flex-start;
|
|
2620
|
+
background: #ffffff;
|
|
2621
|
+
border: 1px solid #e2e8f0;
|
|
2622
|
+
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.06);
|
|
2623
|
+
border-radius: 12px;
|
|
2624
|
+
padding: 22px;
|
|
2625
|
+
min-width: 220px;
|
|
2626
|
+
display: grid;
|
|
2627
|
+
gap: 22px;
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
.ui-menu__brand {
|
|
2631
|
+
display: flex;
|
|
2632
|
+
align-items: center;
|
|
2633
|
+
gap: 10px;
|
|
2634
|
+
font-weight: 700;
|
|
2635
|
+
color: #0f172a;
|
|
2636
|
+
font-size: 15px;
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
.ui-menu__brand-pill {
|
|
2640
|
+
width: 14px;
|
|
2641
|
+
height: 14px;
|
|
2642
|
+
border-radius: 999px;
|
|
2643
|
+
background: linear-gradient(135deg, #1d4ed8, #2563eb);
|
|
2644
|
+
box-shadow: 0 8px 20px rgba(37, 99, 235, 0.24);
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
.ui-menu__group {
|
|
2648
|
+
display: grid;
|
|
2649
|
+
gap: 10px;
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
.ui-menu__group-title {
|
|
2653
|
+
margin: 0;
|
|
2654
|
+
font-size: 11px;
|
|
2655
|
+
letter-spacing: 0.16em;
|
|
2656
|
+
text-transform: uppercase;
|
|
2657
|
+
color: #94a3b8;
|
|
2658
|
+
font-family: "DM Sans", "Segoe UI", sans-serif;
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
.ui-menu__item {
|
|
2662
|
+
display: block;
|
|
2663
|
+
text-decoration: none;
|
|
2664
|
+
font-size: 14px;
|
|
2665
|
+
font-weight: 600;
|
|
2666
|
+
color: #1f2937;
|
|
2667
|
+
padding: 10px 14px;
|
|
2668
|
+
border-radius: 8px;
|
|
2669
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease, color 0.2s ease;
|
|
2670
|
+
border: 1px solid transparent;
|
|
2671
|
+
white-space: nowrap;
|
|
2672
|
+
overflow: hidden;
|
|
2673
|
+
text-overflow: ellipsis;
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
.ui-menu__item:hover {
|
|
2677
|
+
background: #eff6ff;
|
|
2678
|
+
transform: translateX(2px);
|
|
2679
|
+
color: #1d4ed8;
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
.ui-menu__item.active {
|
|
2683
|
+
background: #1e293b;
|
|
2684
|
+
color: #ffffff;
|
|
2685
|
+
box-shadow: 0 10px 24px rgba(30, 41, 59, 0.24);
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
.ui-menu__item.hidden {
|
|
2689
|
+
display: none;
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
@media (max-width: 900px) {
|
|
2693
|
+
.ui-menu {
|
|
2694
|
+
position: static;
|
|
2695
|
+
width: 100%;
|
|
2696
|
+
min-width: auto;
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
`
|
|
2700
|
+
},
|
|
2701
|
+
{
|
|
2702
|
+
name: 'ui-field',
|
|
2703
|
+
template: `
|
|
2704
|
+
import { Component, Input } from '@angular/core'
|
|
2705
|
+
import { NgIf } from '@angular/common'
|
|
2706
|
+
|
|
2707
|
+
@Component({
|
|
2708
|
+
selector: 'ui-field',
|
|
2709
|
+
standalone: true,
|
|
2710
|
+
imports: [NgIf],
|
|
2711
|
+
templateUrl: './ui-field.component.html',
|
|
2712
|
+
styleUrls: ['./ui-field.component.scss']
|
|
2713
|
+
})
|
|
2714
|
+
export class UiFieldComponent {
|
|
2715
|
+
@Input() label = ''
|
|
2716
|
+
@Input() hint = ''
|
|
2717
|
+
@Input() info = ''
|
|
2718
|
+
infoOpen = false
|
|
2719
|
+
|
|
2720
|
+
toggleInfo(event: MouseEvent) {
|
|
2721
|
+
event.preventDefault()
|
|
2722
|
+
event.stopPropagation()
|
|
2723
|
+
this.infoOpen = !this.infoOpen
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
`,
|
|
2727
|
+
html: `
|
|
2728
|
+
<label class="ui-field">
|
|
2729
|
+
<span class="ui-field__label" *ngIf="label">
|
|
2730
|
+
{{ label }}
|
|
2731
|
+
<button
|
|
2732
|
+
class="ui-field__info"
|
|
2733
|
+
type="button"
|
|
2734
|
+
*ngIf="info"
|
|
2735
|
+
(click)="toggleInfo($event)"
|
|
2736
|
+
[attr.aria-expanded]="infoOpen"
|
|
2737
|
+
>
|
|
2738
|
+
i
|
|
2739
|
+
</button>
|
|
2740
|
+
</span>
|
|
2741
|
+
<div class="ui-field__info-panel" *ngIf="info && infoOpen">
|
|
2742
|
+
{{ info }}
|
|
2743
|
+
</div>
|
|
2744
|
+
<ng-content></ng-content>
|
|
2745
|
+
<span class="ui-field__hint" *ngIf="hint && !info">{{ hint }}</span>
|
|
2746
|
+
</label>
|
|
2747
|
+
`,
|
|
2748
|
+
scss: `
|
|
2749
|
+
:host {
|
|
2750
|
+
display: block;
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
.ui-field {
|
|
2754
|
+
display: grid;
|
|
2755
|
+
gap: 10px;
|
|
2756
|
+
font-size: 13px;
|
|
2757
|
+
color: #1f2937;
|
|
2758
|
+
min-width: 0;
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
.ui-field__label {
|
|
2762
|
+
font-weight: 700;
|
|
2763
|
+
line-height: 1.4;
|
|
2764
|
+
word-break: break-word;
|
|
2765
|
+
letter-spacing: 0.01em;
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
.ui-field__hint {
|
|
2769
|
+
color: #94a3b8;
|
|
2770
|
+
font-size: 12px;
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
.ui-field__info {
|
|
2774
|
+
margin-left: 6px;
|
|
2775
|
+
width: 16px;
|
|
2776
|
+
height: 16px;
|
|
2777
|
+
border-radius: 999px;
|
|
2778
|
+
border: 1px solid var(--color-border);
|
|
2779
|
+
background: #ffffff;
|
|
2780
|
+
color: var(--color-primary-strong);
|
|
2781
|
+
font-size: 10px;
|
|
2782
|
+
line-height: 1;
|
|
2783
|
+
display: inline-flex;
|
|
2784
|
+
align-items: center;
|
|
2785
|
+
justify-content: center;
|
|
2786
|
+
cursor: pointer;
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
.ui-field__info:hover {
|
|
2790
|
+
background: #ffffff;
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
.ui-field__info-panel {
|
|
2794
|
+
margin-top: 8px;
|
|
2795
|
+
padding: 10px 12px;
|
|
2796
|
+
border-radius: 14px;
|
|
2797
|
+
background: #ffffff;
|
|
2798
|
+
border: 1px solid var(--color-border);
|
|
2799
|
+
color: #475569;
|
|
2800
|
+
font-size: 12px;
|
|
2801
|
+
line-height: 1.4;
|
|
2802
|
+
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
:host ::ng-deep input,
|
|
2806
|
+
:host ::ng-deep textarea {
|
|
2807
|
+
width: 100%;
|
|
2808
|
+
max-width: 100%;
|
|
2809
|
+
min-width: 0;
|
|
2810
|
+
min-height: 2.9rem;
|
|
2811
|
+
border-radius: 16px;
|
|
2812
|
+
border: 1px solid var(--color-border);
|
|
2813
|
+
background: #ffffff;
|
|
2814
|
+
padding: 0.7rem 0.95rem;
|
|
2815
|
+
font-size: 14px;
|
|
2816
|
+
font-weight: 500;
|
|
2817
|
+
box-sizing: border-box;
|
|
2818
|
+
box-shadow: none;
|
|
2819
|
+
outline: none;
|
|
2820
|
+
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
:host ::ng-deep input:focus,
|
|
2824
|
+
:host ::ng-deep textarea:focus {
|
|
2825
|
+
border-color: var(--color-primary-strong);
|
|
2826
|
+
box-shadow: 0 0 0 4px var(--color-primary-soft);
|
|
2827
|
+
transform: translateY(-1px);
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
:host ::ng-deep input.invalid,
|
|
2831
|
+
:host ::ng-deep textarea.invalid {
|
|
2832
|
+
border-color: var(--color-accent);
|
|
2833
|
+
box-shadow: 0 0 0 3px var(--color-accent-soft);
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
:host ::ng-deep input::placeholder,
|
|
2837
|
+
:host ::ng-deep textarea::placeholder {
|
|
2838
|
+
color: #94a3b8;
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
:host ::ng-deep input[type='checkbox'] {
|
|
2842
|
+
width: 22px;
|
|
2843
|
+
height: 22px;
|
|
2844
|
+
padding: 0;
|
|
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;
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
.field-error {
|
|
2854
|
+
color: #ef4444;
|
|
2855
|
+
font-size: 12px;
|
|
2856
|
+
margin-top: -4px;
|
|
2857
|
+
}
|
|
2858
|
+
`
|
|
2859
|
+
},
|
|
2860
|
+
{
|
|
2861
|
+
name: 'ui-button',
|
|
2862
|
+
template: `
|
|
2863
|
+
import { Component, Input } from '@angular/core'
|
|
2864
|
+
import { NgClass } from '@angular/common'
|
|
2865
|
+
|
|
2866
|
+
@Component({
|
|
2867
|
+
selector: 'ui-button',
|
|
2868
|
+
standalone: true,
|
|
2869
|
+
imports: [NgClass],
|
|
2870
|
+
templateUrl: './ui-button.component.html',
|
|
2871
|
+
styleUrls: ['./ui-button.component.scss']
|
|
2872
|
+
})
|
|
2873
|
+
export class UiButtonComponent {
|
|
2874
|
+
@Input() type: 'button' | 'submit' | 'reset' = 'button'
|
|
2875
|
+
@Input() variant: 'primary' | 'ghost' | 'danger' = 'primary'
|
|
2876
|
+
@Input() disabled = false
|
|
2877
|
+
}
|
|
2878
|
+
`,
|
|
2879
|
+
html: `
|
|
2880
|
+
<button
|
|
2881
|
+
class="ui-button"
|
|
2882
|
+
[ngClass]="variant"
|
|
2883
|
+
[attr.type]="type"
|
|
2884
|
+
[disabled]="disabled"
|
|
2885
|
+
>
|
|
2886
|
+
<ng-content></ng-content>
|
|
2887
|
+
</button>
|
|
2888
|
+
`,
|
|
2889
|
+
scss: `
|
|
2890
|
+
.ui-button {
|
|
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;
|
|
2897
|
+
cursor: pointer;
|
|
2898
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease, filter 0.2s ease;
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
.ui-button.primary {
|
|
2902
|
+
background: #2563eb;
|
|
2903
|
+
color: #ffffff;
|
|
2904
|
+
box-shadow: 0 8px 20px rgba(37, 99, 235, 0.24);
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
.ui-button.ghost {
|
|
2908
|
+
background: #ffffff;
|
|
2909
|
+
color: #334155;
|
|
2910
|
+
border-color: #cbd5e1;
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
.ui-button.danger {
|
|
2914
|
+
background: #dc2626;
|
|
2915
|
+
color: #fff;
|
|
2916
|
+
box-shadow: 0 8px 20px rgba(220, 38, 38, 0.22);
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
.ui-button:hover:not(:disabled) {
|
|
2920
|
+
transform: translateY(-1px);
|
|
2921
|
+
filter: brightness(1.02);
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
.ui-button:disabled {
|
|
2925
|
+
opacity: 0.6;
|
|
2926
|
+
cursor: not-allowed;
|
|
2927
|
+
box-shadow: none;
|
|
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'
|
|
648
3091
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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;
|
|
658
3101
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
.join(',\n');
|
|
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;
|
|
671
3113
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
case 'boolean':
|
|
677
|
-
return 'false';
|
|
678
|
-
case 'number':
|
|
679
|
-
case 'integer':
|
|
680
|
-
return 'null';
|
|
681
|
-
default:
|
|
682
|
-
return "''";
|
|
683
|
-
}
|
|
3114
|
+
|
|
3115
|
+
.ui-badge.neutral {
|
|
3116
|
+
color: #334155;
|
|
3117
|
+
background: #eef2ff;
|
|
684
3118
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
.replace(/\b\w/g, char => char.toUpperCase());
|
|
3119
|
+
|
|
3120
|
+
.ui-badge.success {
|
|
3121
|
+
color: #047857;
|
|
3122
|
+
background: #d1fae5;
|
|
690
3123
|
}
|
|
691
|
-
|
|
692
|
-
|
|
3124
|
+
|
|
3125
|
+
.ui-badge.warning {
|
|
3126
|
+
color: #92400e;
|
|
3127
|
+
background: #fef3c7;
|
|
693
3128
|
}
|
|
694
|
-
|
|
695
|
-
|
|
3129
|
+
|
|
3130
|
+
.ui-badge.danger {
|
|
3131
|
+
color: #991b1b;
|
|
3132
|
+
background: #fee2e2;
|
|
696
3133
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
3134
|
+
`
|
|
3135
|
+
},
|
|
3136
|
+
{
|
|
3137
|
+
name: 'ui-input',
|
|
3138
|
+
template: `
|
|
3139
|
+
import { Component, Input } from '@angular/core'
|
|
3140
|
+
import { NgIf } from '@angular/common'
|
|
3141
|
+
import {
|
|
3142
|
+
ControlContainer,
|
|
3143
|
+
FormGroupDirective,
|
|
3144
|
+
ReactiveFormsModule
|
|
3145
|
+
} from '@angular/forms'
|
|
3146
|
+
|
|
3147
|
+
@Component({
|
|
3148
|
+
selector: 'ui-input',
|
|
3149
|
+
standalone: true,
|
|
3150
|
+
imports: [NgIf, ReactiveFormsModule],
|
|
3151
|
+
viewProviders: [
|
|
3152
|
+
{ provide: ControlContainer, useExisting: FormGroupDirective }
|
|
3153
|
+
],
|
|
3154
|
+
templateUrl: './ui-input.component.html',
|
|
3155
|
+
styleUrls: ['./ui-input.component.scss']
|
|
3156
|
+
})
|
|
3157
|
+
export class UiInputComponent {
|
|
3158
|
+
@Input() label = ''
|
|
3159
|
+
@Input() hint = ''
|
|
3160
|
+
@Input() info = ''
|
|
3161
|
+
@Input() controlName = ''
|
|
3162
|
+
@Input() placeholder = ''
|
|
3163
|
+
@Input() type: 'text' | 'number' | 'email' | 'password' | 'search' | 'tel' | 'url' = 'text'
|
|
3164
|
+
@Input() invalid = false
|
|
3165
|
+
infoOpen = false
|
|
3166
|
+
|
|
3167
|
+
toggleInfo(event: MouseEvent) {
|
|
3168
|
+
event.preventDefault()
|
|
3169
|
+
event.stopPropagation()
|
|
3170
|
+
this.infoOpen = !this.infoOpen
|
|
3171
|
+
}
|
|
705
3172
|
}
|
|
706
|
-
|
|
707
|
-
|
|
3173
|
+
`,
|
|
3174
|
+
html: `
|
|
3175
|
+
<label class="ui-control">
|
|
3176
|
+
<span class="ui-control__label" *ngIf="label">
|
|
3177
|
+
{{ label }}
|
|
3178
|
+
<button
|
|
3179
|
+
class="ui-control__info"
|
|
3180
|
+
type="button"
|
|
3181
|
+
*ngIf="info"
|
|
3182
|
+
(click)="toggleInfo($event)"
|
|
3183
|
+
[attr.aria-expanded]="infoOpen"
|
|
3184
|
+
>
|
|
3185
|
+
i
|
|
3186
|
+
</button>
|
|
3187
|
+
</span>
|
|
3188
|
+
<div class="ui-control__info-panel" *ngIf="info && infoOpen">
|
|
3189
|
+
{{ info }}
|
|
3190
|
+
</div>
|
|
3191
|
+
<input
|
|
3192
|
+
class="ui-control__input"
|
|
3193
|
+
[type]="type"
|
|
3194
|
+
[formControlName]="controlName"
|
|
3195
|
+
[placeholder]="placeholder"
|
|
3196
|
+
[class.invalid]="invalid"
|
|
3197
|
+
/>
|
|
3198
|
+
<span class="ui-control__hint" *ngIf="hint && !info">{{ hint }}</span>
|
|
3199
|
+
</label>
|
|
3200
|
+
`,
|
|
3201
|
+
scss: `
|
|
3202
|
+
:host {
|
|
3203
|
+
display: block;
|
|
708
3204
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
case 'patch':
|
|
717
|
-
return 'Salvar';
|
|
718
|
-
case 'delete':
|
|
719
|
-
return 'Excluir';
|
|
720
|
-
default:
|
|
721
|
-
return 'Executar';
|
|
722
|
-
}
|
|
3205
|
+
|
|
3206
|
+
.ui-control {
|
|
3207
|
+
display: grid;
|
|
3208
|
+
gap: 10px;
|
|
3209
|
+
font-size: 13px;
|
|
3210
|
+
color: #1f2937;
|
|
3211
|
+
min-width: 0;
|
|
723
3212
|
}
|
|
724
|
-
function buildComponentHtml(options) {
|
|
725
|
-
const buttonVariant = options.method === 'delete' ? 'danger' : 'primary';
|
|
726
|
-
if (!options.hasForm) {
|
|
727
|
-
return `
|
|
728
|
-
<div class="page">
|
|
729
|
-
<ui-card title="${options.title}" subtitle="${options.subtitle}">
|
|
730
|
-
<div class="actions">
|
|
731
|
-
<ui-button
|
|
732
|
-
variant="${buttonVariant}"
|
|
733
|
-
[disabled]="form.invalid"
|
|
734
|
-
(click)="submit()"
|
|
735
|
-
>
|
|
736
|
-
${options.actionLabel}
|
|
737
|
-
</ui-button>
|
|
738
|
-
</div>
|
|
739
|
-
</ui-card>
|
|
740
|
-
</div>
|
|
741
|
-
`;
|
|
742
|
-
}
|
|
743
|
-
return `
|
|
744
|
-
<div class="page">
|
|
745
|
-
<ui-card [title]="schema.entity || schema.api.operationId" [subtitle]="schema.api.method.toUpperCase() + ' ' + schema.api.endpoint">
|
|
746
|
-
<p class="screen-description" *ngIf="schema.description">
|
|
747
|
-
{{ schema.description }}
|
|
748
|
-
</p>
|
|
749
|
-
<form [formGroup]="form" (ngSubmit)="submit()">
|
|
750
|
-
<div class="form-grid">
|
|
751
|
-
<ui-field
|
|
752
|
-
*ngFor="let field of formFields"
|
|
753
|
-
[label]="field.label || field.name"
|
|
754
|
-
[hint]="field.hint"
|
|
755
|
-
[info]="field.info"
|
|
756
|
-
>
|
|
757
|
-
<select
|
|
758
|
-
*ngIf="isSelect(field)"
|
|
759
|
-
[formControlName]="field.name"
|
|
760
|
-
[class.invalid]="isInvalid(field)"
|
|
761
|
-
>
|
|
762
|
-
<option
|
|
763
|
-
*ngFor="let option of field.options"
|
|
764
|
-
[value]="option"
|
|
765
|
-
>
|
|
766
|
-
{{ option }}
|
|
767
|
-
</option>
|
|
768
|
-
</select>
|
|
769
|
-
|
|
770
|
-
<textarea
|
|
771
|
-
*ngIf="isTextarea(field)"
|
|
772
|
-
rows="3"
|
|
773
|
-
[formControlName]="field.name"
|
|
774
|
-
[placeholder]="field.placeholder || field.label || field.name"
|
|
775
|
-
[class.invalid]="isInvalid(field)"
|
|
776
|
-
></textarea>
|
|
777
3213
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
3214
|
+
.ui-control__label {
|
|
3215
|
+
font-weight: 700;
|
|
3216
|
+
line-height: 1.4;
|
|
3217
|
+
word-break: break-word;
|
|
3218
|
+
letter-spacing: 0.01em;
|
|
3219
|
+
}
|
|
783
3220
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
[placeholder]="field.placeholder || field.label || field.name"
|
|
789
|
-
[class.invalid]="isInvalid(field)"
|
|
790
|
-
/>
|
|
3221
|
+
.ui-control__hint {
|
|
3222
|
+
color: #94a3b8;
|
|
3223
|
+
font-size: 12px;
|
|
3224
|
+
}
|
|
791
3225
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
</ui-card>
|
|
3226
|
+
.ui-control__info {
|
|
3227
|
+
margin-left: 6px;
|
|
3228
|
+
width: 16px;
|
|
3229
|
+
height: 16px;
|
|
3230
|
+
border-radius: 999px;
|
|
3231
|
+
border: 1px solid var(--color-border);
|
|
3232
|
+
background: #ffffff;
|
|
3233
|
+
color: var(--color-primary-strong);
|
|
3234
|
+
font-size: 10px;
|
|
3235
|
+
line-height: 1;
|
|
3236
|
+
display: inline-flex;
|
|
3237
|
+
align-items: center;
|
|
3238
|
+
justify-content: center;
|
|
3239
|
+
cursor: pointer;
|
|
3240
|
+
}
|
|
808
3241
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
<tr>
|
|
813
|
-
<th *ngFor="let column of getColumns()">
|
|
814
|
-
{{ formatHeader(column) }}
|
|
815
|
-
</th>
|
|
816
|
-
</tr>
|
|
817
|
-
</thead>
|
|
818
|
-
<tbody>
|
|
819
|
-
<tr *ngFor="let row of getRows()">
|
|
820
|
-
<td *ngFor="let column of getColumns()">
|
|
821
|
-
<img
|
|
822
|
-
*ngIf="isImageCell(row, column)"
|
|
823
|
-
[src]="getCellValue(row, column)"
|
|
824
|
-
[alt]="formatHeader(column)"
|
|
825
|
-
class="cell-image"
|
|
826
|
-
/>
|
|
827
|
-
<span *ngIf="!isImageCell(row, column)">
|
|
828
|
-
{{ getCellValue(row, column) }}
|
|
829
|
-
</span>
|
|
830
|
-
</td>
|
|
831
|
-
</tr>
|
|
832
|
-
</tbody>
|
|
833
|
-
</table>
|
|
834
|
-
</div>
|
|
3242
|
+
.ui-control__info:hover {
|
|
3243
|
+
background: #ffffff;
|
|
3244
|
+
}
|
|
835
3245
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
</div>
|
|
847
|
-
</div>
|
|
848
|
-
</div>
|
|
849
|
-
`;
|
|
3246
|
+
.ui-control__info-panel {
|
|
3247
|
+
margin-top: 8px;
|
|
3248
|
+
padding: 10px 12px;
|
|
3249
|
+
border-radius: 14px;
|
|
3250
|
+
background: #ffffff;
|
|
3251
|
+
border: 1px solid var(--color-border);
|
|
3252
|
+
color: #475569;
|
|
3253
|
+
font-size: 12px;
|
|
3254
|
+
line-height: 1.4;
|
|
3255
|
+
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
|
|
850
3256
|
}
|
|
851
|
-
|
|
852
|
-
|
|
3257
|
+
|
|
3258
|
+
.ui-control__input {
|
|
3259
|
+
width: 100%;
|
|
3260
|
+
max-width: 100%;
|
|
3261
|
+
min-width: 0;
|
|
3262
|
+
min-height: 2.9rem;
|
|
3263
|
+
border-radius: 14px;
|
|
3264
|
+
border: 1px solid var(--color-border);
|
|
3265
|
+
background: #ffffff;
|
|
3266
|
+
padding: 0.7rem 0.95rem;
|
|
3267
|
+
font-size: 14px;
|
|
3268
|
+
font-weight: 500;
|
|
3269
|
+
box-sizing: border-box;
|
|
3270
|
+
box-shadow: none;
|
|
3271
|
+
outline: none;
|
|
3272
|
+
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
|
853
3273
|
}
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
case 'boolean':
|
|
860
|
-
return 'checkbox';
|
|
861
|
-
default:
|
|
862
|
-
return 'text';
|
|
863
|
-
}
|
|
3274
|
+
|
|
3275
|
+
.ui-control__input:focus {
|
|
3276
|
+
border-color: var(--color-primary-strong);
|
|
3277
|
+
box-shadow: 0 0 0 4px var(--color-primary-soft);
|
|
3278
|
+
transform: translateY(-1px);
|
|
864
3279
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
case 'delete':
|
|
870
|
-
return 'return this.http.delete(url)';
|
|
871
|
-
case 'post':
|
|
872
|
-
return 'return this.http.post(url, body)';
|
|
873
|
-
case 'put':
|
|
874
|
-
return 'return this.http.put(url, body)';
|
|
875
|
-
case 'patch':
|
|
876
|
-
return 'return this.http.patch(url, body)';
|
|
877
|
-
default:
|
|
878
|
-
return 'return this.http.get(url)';
|
|
879
|
-
}
|
|
3280
|
+
|
|
3281
|
+
.ui-control__input.invalid {
|
|
3282
|
+
border-color: var(--color-accent);
|
|
3283
|
+
box-shadow: 0 0 0 3px var(--color-accent-soft);
|
|
880
3284
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
3285
|
+
|
|
3286
|
+
.ui-control__input::placeholder {
|
|
3287
|
+
color: #94a3b8;
|
|
3288
|
+
}
|
|
3289
|
+
`
|
|
3290
|
+
},
|
|
884
3291
|
{
|
|
885
|
-
name: 'ui-
|
|
3292
|
+
name: 'ui-textarea',
|
|
886
3293
|
template: `
|
|
887
3294
|
import { Component, Input } from '@angular/core'
|
|
888
3295
|
import { NgIf } from '@angular/common'
|
|
3296
|
+
import {
|
|
3297
|
+
ControlContainer,
|
|
3298
|
+
FormGroupDirective,
|
|
3299
|
+
ReactiveFormsModule
|
|
3300
|
+
} from '@angular/forms'
|
|
889
3301
|
|
|
890
3302
|
@Component({
|
|
891
|
-
selector: 'ui-
|
|
3303
|
+
selector: 'ui-textarea',
|
|
892
3304
|
standalone: true,
|
|
893
|
-
imports: [NgIf],
|
|
894
|
-
|
|
895
|
-
|
|
3305
|
+
imports: [NgIf, ReactiveFormsModule],
|
|
3306
|
+
viewProviders: [
|
|
3307
|
+
{ provide: ControlContainer, useExisting: FormGroupDirective }
|
|
3308
|
+
],
|
|
3309
|
+
templateUrl: './ui-textarea.component.html',
|
|
3310
|
+
styleUrls: ['./ui-textarea.component.scss']
|
|
896
3311
|
})
|
|
897
|
-
export class
|
|
898
|
-
@Input()
|
|
899
|
-
@Input()
|
|
3312
|
+
export class UiTextareaComponent {
|
|
3313
|
+
@Input() label = ''
|
|
3314
|
+
@Input() hint = ''
|
|
3315
|
+
@Input() info = ''
|
|
3316
|
+
@Input() controlName = ''
|
|
3317
|
+
@Input() placeholder = ''
|
|
3318
|
+
@Input() rows = 3
|
|
3319
|
+
@Input() invalid = false
|
|
3320
|
+
infoOpen = false
|
|
3321
|
+
|
|
3322
|
+
toggleInfo(event: MouseEvent) {
|
|
3323
|
+
event.preventDefault()
|
|
3324
|
+
event.stopPropagation()
|
|
3325
|
+
this.infoOpen = !this.infoOpen
|
|
3326
|
+
}
|
|
900
3327
|
}
|
|
901
3328
|
`,
|
|
902
3329
|
html: `
|
|
903
|
-
<
|
|
904
|
-
<
|
|
905
|
-
|
|
906
|
-
<
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
3330
|
+
<label class="ui-control">
|
|
3331
|
+
<span class="ui-control__label" *ngIf="label">
|
|
3332
|
+
{{ label }}
|
|
3333
|
+
<button
|
|
3334
|
+
class="ui-control__info"
|
|
3335
|
+
type="button"
|
|
3336
|
+
*ngIf="info"
|
|
3337
|
+
(click)="toggleInfo($event)"
|
|
3338
|
+
[attr.aria-expanded]="infoOpen"
|
|
3339
|
+
>
|
|
3340
|
+
i
|
|
3341
|
+
</button>
|
|
3342
|
+
</span>
|
|
3343
|
+
<div class="ui-control__info-panel" *ngIf="info && infoOpen">
|
|
3344
|
+
{{ info }}
|
|
910
3345
|
</div>
|
|
911
|
-
|
|
3346
|
+
<textarea
|
|
3347
|
+
class="ui-control__input"
|
|
3348
|
+
[formControlName]="controlName"
|
|
3349
|
+
[rows]="rows"
|
|
3350
|
+
[placeholder]="placeholder"
|
|
3351
|
+
[class.invalid]="invalid"
|
|
3352
|
+
></textarea>
|
|
3353
|
+
<span class="ui-control__hint" *ngIf="hint && !info">{{ hint }}</span>
|
|
3354
|
+
</label>
|
|
912
3355
|
`,
|
|
913
3356
|
scss: `
|
|
914
3357
|
:host {
|
|
915
3358
|
display: block;
|
|
916
3359
|
}
|
|
917
3360
|
|
|
918
|
-
.ui-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
3361
|
+
.ui-control {
|
|
3362
|
+
display: grid;
|
|
3363
|
+
gap: 10px;
|
|
3364
|
+
font-size: 13px;
|
|
3365
|
+
color: #1f2937;
|
|
3366
|
+
min-width: 0;
|
|
924
3367
|
}
|
|
925
3368
|
|
|
926
|
-
.ui-
|
|
927
|
-
|
|
3369
|
+
.ui-control__label {
|
|
3370
|
+
font-weight: 700;
|
|
3371
|
+
line-height: 1.4;
|
|
3372
|
+
word-break: break-word;
|
|
3373
|
+
letter-spacing: 0.01em;
|
|
928
3374
|
}
|
|
929
3375
|
|
|
930
|
-
.ui-
|
|
931
|
-
|
|
932
|
-
font-size:
|
|
933
|
-
font-weight: 700;
|
|
934
|
-
color: #0f172a;
|
|
3376
|
+
.ui-control__hint {
|
|
3377
|
+
color: #94a3b8;
|
|
3378
|
+
font-size: 12px;
|
|
935
3379
|
}
|
|
936
3380
|
|
|
937
|
-
.ui-
|
|
938
|
-
margin:
|
|
3381
|
+
.ui-control__info {
|
|
3382
|
+
margin-left: 6px;
|
|
3383
|
+
width: 16px;
|
|
3384
|
+
height: 16px;
|
|
3385
|
+
border-radius: 999px;
|
|
3386
|
+
border: 1px solid var(--color-border);
|
|
3387
|
+
background: #ffffff;
|
|
3388
|
+
color: var(--color-primary-strong);
|
|
3389
|
+
font-size: 10px;
|
|
3390
|
+
line-height: 1;
|
|
3391
|
+
display: inline-flex;
|
|
3392
|
+
align-items: center;
|
|
3393
|
+
justify-content: center;
|
|
3394
|
+
cursor: pointer;
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
.ui-control__info:hover {
|
|
3398
|
+
background: #ffffff;
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
.ui-control__info-panel {
|
|
3402
|
+
margin-top: 8px;
|
|
3403
|
+
padding: 10px 12px;
|
|
3404
|
+
border-radius: 14px;
|
|
3405
|
+
background: #ffffff;
|
|
3406
|
+
border: 1px solid var(--color-border);
|
|
3407
|
+
color: #475569;
|
|
939
3408
|
font-size: 12px;
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
3409
|
+
line-height: 1.4;
|
|
3410
|
+
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3413
|
+
.ui-control__input {
|
|
3414
|
+
width: 100%;
|
|
3415
|
+
max-width: 100%;
|
|
3416
|
+
min-width: 0;
|
|
3417
|
+
min-height: 2.9rem;
|
|
3418
|
+
border-radius: 14px;
|
|
3419
|
+
border: 1px solid var(--color-border);
|
|
3420
|
+
background: #ffffff;
|
|
3421
|
+
padding: 0.7rem 0.95rem;
|
|
3422
|
+
font-size: 14px;
|
|
3423
|
+
font-weight: 500;
|
|
3424
|
+
box-sizing: border-box;
|
|
3425
|
+
box-shadow: none;
|
|
3426
|
+
outline: none;
|
|
3427
|
+
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
|
3428
|
+
}
|
|
3429
|
+
|
|
3430
|
+
.ui-control__input:focus {
|
|
3431
|
+
border-color: var(--color-primary-strong);
|
|
3432
|
+
box-shadow: 0 0 0 4px var(--color-primary-soft);
|
|
3433
|
+
transform: translateY(-1px);
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
.ui-control__input.invalid {
|
|
3437
|
+
border-color: var(--color-accent);
|
|
3438
|
+
box-shadow: 0 0 0 3px var(--color-accent-soft);
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
.ui-control__input::placeholder {
|
|
3442
|
+
color: #94a3b8;
|
|
943
3443
|
}
|
|
944
3444
|
`
|
|
945
3445
|
},
|
|
946
3446
|
{
|
|
947
|
-
name: 'ui-
|
|
3447
|
+
name: 'ui-select',
|
|
948
3448
|
template: `
|
|
949
3449
|
import { Component, Input } from '@angular/core'
|
|
950
|
-
import { NgIf } from '@angular/common'
|
|
3450
|
+
import { NgFor, NgIf } from '@angular/common'
|
|
3451
|
+
import {
|
|
3452
|
+
ControlContainer,
|
|
3453
|
+
FormGroupDirective,
|
|
3454
|
+
ReactiveFormsModule
|
|
3455
|
+
} from '@angular/forms'
|
|
951
3456
|
|
|
952
3457
|
@Component({
|
|
953
|
-
selector: 'ui-
|
|
3458
|
+
selector: 'ui-select',
|
|
954
3459
|
standalone: true,
|
|
955
|
-
imports: [NgIf],
|
|
956
|
-
|
|
957
|
-
|
|
3460
|
+
imports: [NgFor, NgIf, ReactiveFormsModule],
|
|
3461
|
+
viewProviders: [
|
|
3462
|
+
{ provide: ControlContainer, useExisting: FormGroupDirective }
|
|
3463
|
+
],
|
|
3464
|
+
templateUrl: './ui-select.component.html',
|
|
3465
|
+
styleUrls: ['./ui-select.component.scss']
|
|
958
3466
|
})
|
|
959
|
-
export class
|
|
3467
|
+
export class UiSelectComponent {
|
|
960
3468
|
@Input() label = ''
|
|
961
3469
|
@Input() hint = ''
|
|
962
3470
|
@Input() info = ''
|
|
3471
|
+
@Input() controlName = ''
|
|
3472
|
+
@Input() options: any[] = []
|
|
3473
|
+
@Input() invalid = false
|
|
963
3474
|
infoOpen = false
|
|
964
3475
|
|
|
965
3476
|
toggleInfo(event: MouseEvent) {
|
|
@@ -970,11 +3481,11 @@ export class UiFieldComponent {
|
|
|
970
3481
|
}
|
|
971
3482
|
`,
|
|
972
3483
|
html: `
|
|
973
|
-
<label class="ui-
|
|
974
|
-
<span class="ui-
|
|
3484
|
+
<label class="ui-control">
|
|
3485
|
+
<span class="ui-control__label" *ngIf="label">
|
|
975
3486
|
{{ label }}
|
|
976
3487
|
<button
|
|
977
|
-
class="ui-
|
|
3488
|
+
class="ui-control__info"
|
|
978
3489
|
type="button"
|
|
979
3490
|
*ngIf="info"
|
|
980
3491
|
(click)="toggleInfo($event)"
|
|
@@ -983,11 +3494,19 @@ export class UiFieldComponent {
|
|
|
983
3494
|
i
|
|
984
3495
|
</button>
|
|
985
3496
|
</span>
|
|
986
|
-
<div class="ui-
|
|
3497
|
+
<div class="ui-control__info-panel" *ngIf="info && infoOpen">
|
|
987
3498
|
{{ info }}
|
|
988
3499
|
</div>
|
|
989
|
-
<
|
|
990
|
-
|
|
3500
|
+
<select
|
|
3501
|
+
class="ui-control__select"
|
|
3502
|
+
[formControlName]="controlName"
|
|
3503
|
+
[class.invalid]="invalid"
|
|
3504
|
+
>
|
|
3505
|
+
<option *ngFor="let option of options" [value]="option">
|
|
3506
|
+
{{ option }}
|
|
3507
|
+
</option>
|
|
3508
|
+
</select>
|
|
3509
|
+
<span class="ui-control__hint" *ngIf="hint && !info">{{ hint }}</span>
|
|
991
3510
|
</label>
|
|
992
3511
|
`,
|
|
993
3512
|
scss: `
|
|
@@ -995,33 +3514,35 @@ export class UiFieldComponent {
|
|
|
995
3514
|
display: block;
|
|
996
3515
|
}
|
|
997
3516
|
|
|
998
|
-
.ui-
|
|
3517
|
+
.ui-control {
|
|
999
3518
|
display: grid;
|
|
1000
|
-
gap:
|
|
1001
|
-
font-size:
|
|
1002
|
-
color: #
|
|
3519
|
+
gap: 10px;
|
|
3520
|
+
font-size: 13px;
|
|
3521
|
+
color: #1f2937;
|
|
3522
|
+
min-width: 0;
|
|
1003
3523
|
}
|
|
1004
3524
|
|
|
1005
|
-
.ui-
|
|
3525
|
+
.ui-control__label {
|
|
1006
3526
|
font-weight: 700;
|
|
1007
3527
|
line-height: 1.4;
|
|
1008
3528
|
word-break: break-word;
|
|
3529
|
+
letter-spacing: 0.01em;
|
|
1009
3530
|
}
|
|
1010
3531
|
|
|
1011
|
-
.ui-
|
|
3532
|
+
.ui-control__hint {
|
|
1012
3533
|
color: #94a3b8;
|
|
1013
3534
|
font-size: 12px;
|
|
1014
3535
|
}
|
|
1015
3536
|
|
|
1016
|
-
.ui-
|
|
1017
|
-
margin-left:
|
|
1018
|
-
width:
|
|
1019
|
-
height:
|
|
3537
|
+
.ui-control__info {
|
|
3538
|
+
margin-left: 6px;
|
|
3539
|
+
width: 16px;
|
|
3540
|
+
height: 16px;
|
|
1020
3541
|
border-radius: 999px;
|
|
1021
|
-
border: 1px solid
|
|
3542
|
+
border: 1px solid var(--color-border);
|
|
1022
3543
|
background: #ffffff;
|
|
1023
|
-
color:
|
|
1024
|
-
font-size:
|
|
3544
|
+
color: var(--color-primary-strong);
|
|
3545
|
+
font-size: 10px;
|
|
1025
3546
|
line-height: 1;
|
|
1026
3547
|
display: inline-flex;
|
|
1027
3548
|
align-items: center;
|
|
@@ -1029,151 +3550,185 @@ export class UiFieldComponent {
|
|
|
1029
3550
|
cursor: pointer;
|
|
1030
3551
|
}
|
|
1031
3552
|
|
|
1032
|
-
.ui-
|
|
1033
|
-
background: #
|
|
3553
|
+
.ui-control__info:hover {
|
|
3554
|
+
background: #ffffff;
|
|
1034
3555
|
}
|
|
1035
3556
|
|
|
1036
|
-
.ui-
|
|
3557
|
+
.ui-control__info-panel {
|
|
1037
3558
|
margin-top: 8px;
|
|
1038
3559
|
padding: 10px 12px;
|
|
1039
|
-
border-radius:
|
|
1040
|
-
background: #
|
|
1041
|
-
border: 1px solid
|
|
3560
|
+
border-radius: 14px;
|
|
3561
|
+
background: #ffffff;
|
|
3562
|
+
border: 1px solid var(--color-border);
|
|
1042
3563
|
color: #475569;
|
|
1043
3564
|
font-size: 12px;
|
|
1044
3565
|
line-height: 1.4;
|
|
3566
|
+
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
|
|
1045
3567
|
}
|
|
1046
3568
|
|
|
1047
|
-
|
|
1048
|
-
:host ::ng-deep textarea,
|
|
1049
|
-
:host ::ng-deep select {
|
|
3569
|
+
.ui-control__select {
|
|
1050
3570
|
width: 100%;
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
3571
|
+
max-width: 100%;
|
|
3572
|
+
min-width: 0;
|
|
3573
|
+
min-height: 2.9rem;
|
|
3574
|
+
border-radius: 14px;
|
|
3575
|
+
border: 1px solid var(--color-border);
|
|
1054
3576
|
background: #ffffff;
|
|
1055
|
-
padding: 0.
|
|
3577
|
+
padding: 0.7rem 2.3rem 0.7rem 0.95rem;
|
|
1056
3578
|
font-size: 14px;
|
|
1057
3579
|
font-weight: 500;
|
|
3580
|
+
box-sizing: border-box;
|
|
1058
3581
|
box-shadow: none;
|
|
1059
3582
|
outline: none;
|
|
1060
|
-
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
:host ::ng-deep input:focus,
|
|
1064
|
-
:host ::ng-deep textarea:focus,
|
|
1065
|
-
:host ::ng-deep select:focus {
|
|
1066
|
-
border-color: #6366f1;
|
|
1067
|
-
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
:host ::ng-deep input.invalid,
|
|
1071
|
-
:host ::ng-deep textarea.invalid,
|
|
1072
|
-
:host ::ng-deep select.invalid {
|
|
1073
|
-
border-color: #ef4444;
|
|
1074
|
-
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.18);
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
:host ::ng-deep input::placeholder,
|
|
1078
|
-
:host ::ng-deep textarea::placeholder {
|
|
1079
|
-
color: #94a3b8;
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
:host ::ng-deep select {
|
|
1083
|
-
padding-right: 2.2rem;
|
|
3583
|
+
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
|
1084
3584
|
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='8' viewBox='0 0 14 8' fill='none'><path d='M1 1.5L7 6.5L13 1.5' stroke='%236b7280' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/></svg>");
|
|
1085
3585
|
background-repeat: no-repeat;
|
|
1086
|
-
background-position: right 0.
|
|
3586
|
+
background-position: right 0.9rem center;
|
|
1087
3587
|
background-size: 14px 8px;
|
|
1088
3588
|
appearance: none;
|
|
1089
3589
|
}
|
|
1090
3590
|
|
|
1091
|
-
:
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
:host ::ng-deep input[type='checkbox'] {
|
|
1097
|
-
width: 20px;
|
|
1098
|
-
height: 20px;
|
|
1099
|
-
padding: 0;
|
|
1100
|
-
border-radius: 6px;
|
|
1101
|
-
box-shadow: none;
|
|
1102
|
-
accent-color: #6366f1;
|
|
3591
|
+
.ui-control__select:focus {
|
|
3592
|
+
border-color: var(--color-primary-strong);
|
|
3593
|
+
box-shadow: 0 0 0 4px var(--color-primary-soft);
|
|
3594
|
+
transform: translateY(-1px);
|
|
1103
3595
|
}
|
|
1104
3596
|
|
|
1105
|
-
.
|
|
1106
|
-
color:
|
|
1107
|
-
|
|
1108
|
-
margin-top: -4px;
|
|
3597
|
+
.ui-control__select.invalid {
|
|
3598
|
+
border-color: var(--color-accent);
|
|
3599
|
+
box-shadow: 0 0 0 3px var(--color-accent-soft);
|
|
1109
3600
|
}
|
|
1110
3601
|
`
|
|
1111
3602
|
},
|
|
1112
3603
|
{
|
|
1113
|
-
name: 'ui-
|
|
3604
|
+
name: 'ui-checkbox',
|
|
1114
3605
|
template: `
|
|
1115
3606
|
import { Component, Input } from '@angular/core'
|
|
1116
|
-
import {
|
|
3607
|
+
import { NgIf } from '@angular/common'
|
|
3608
|
+
import {
|
|
3609
|
+
ControlContainer,
|
|
3610
|
+
FormGroupDirective,
|
|
3611
|
+
ReactiveFormsModule
|
|
3612
|
+
} from '@angular/forms'
|
|
1117
3613
|
|
|
1118
3614
|
@Component({
|
|
1119
|
-
selector: 'ui-
|
|
3615
|
+
selector: 'ui-checkbox',
|
|
1120
3616
|
standalone: true,
|
|
1121
|
-
imports: [
|
|
1122
|
-
|
|
1123
|
-
|
|
3617
|
+
imports: [NgIf, ReactiveFormsModule],
|
|
3618
|
+
viewProviders: [
|
|
3619
|
+
{ provide: ControlContainer, useExisting: FormGroupDirective }
|
|
3620
|
+
],
|
|
3621
|
+
templateUrl: './ui-checkbox.component.html',
|
|
3622
|
+
styleUrls: ['./ui-checkbox.component.scss']
|
|
1124
3623
|
})
|
|
1125
|
-
export class
|
|
1126
|
-
@Input()
|
|
1127
|
-
@Input()
|
|
1128
|
-
@Input()
|
|
3624
|
+
export class UiCheckboxComponent {
|
|
3625
|
+
@Input() label = ''
|
|
3626
|
+
@Input() hint = ''
|
|
3627
|
+
@Input() info = ''
|
|
3628
|
+
@Input() controlName = ''
|
|
3629
|
+
@Input() invalid = false
|
|
3630
|
+
infoOpen = false
|
|
3631
|
+
|
|
3632
|
+
toggleInfo(event: MouseEvent) {
|
|
3633
|
+
event.preventDefault()
|
|
3634
|
+
event.stopPropagation()
|
|
3635
|
+
this.infoOpen = !this.infoOpen
|
|
3636
|
+
}
|
|
1129
3637
|
}
|
|
1130
3638
|
`,
|
|
1131
3639
|
html: `
|
|
1132
|
-
<
|
|
1133
|
-
class="ui-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
3640
|
+
<label class="ui-control">
|
|
3641
|
+
<span class="ui-control__label" *ngIf="label">
|
|
3642
|
+
{{ label }}
|
|
3643
|
+
<button
|
|
3644
|
+
class="ui-control__info"
|
|
3645
|
+
type="button"
|
|
3646
|
+
*ngIf="info"
|
|
3647
|
+
(click)="toggleInfo($event)"
|
|
3648
|
+
[attr.aria-expanded]="infoOpen"
|
|
3649
|
+
>
|
|
3650
|
+
i
|
|
3651
|
+
</button>
|
|
3652
|
+
</span>
|
|
3653
|
+
<div class="ui-control__info-panel" *ngIf="info && infoOpen">
|
|
3654
|
+
{{ info }}
|
|
3655
|
+
</div>
|
|
3656
|
+
<input
|
|
3657
|
+
class="ui-control__checkbox"
|
|
3658
|
+
type="checkbox"
|
|
3659
|
+
[formControlName]="controlName"
|
|
3660
|
+
[class.invalid]="invalid"
|
|
3661
|
+
/>
|
|
3662
|
+
<span class="ui-control__hint" *ngIf="hint && !info">{{ hint }}</span>
|
|
3663
|
+
</label>
|
|
1140
3664
|
`,
|
|
1141
3665
|
scss: `
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
3666
|
+
:host {
|
|
3667
|
+
display: block;
|
|
3668
|
+
}
|
|
3669
|
+
|
|
3670
|
+
.ui-control {
|
|
3671
|
+
display: grid;
|
|
3672
|
+
gap: 10px;
|
|
3673
|
+
font-size: 13px;
|
|
3674
|
+
color: #1f2937;
|
|
3675
|
+
min-width: 0;
|
|
3676
|
+
}
|
|
3677
|
+
|
|
3678
|
+
.ui-control__label {
|
|
1146
3679
|
font-weight: 700;
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
3680
|
+
line-height: 1.4;
|
|
3681
|
+
word-break: break-word;
|
|
3682
|
+
letter-spacing: 0.01em;
|
|
1150
3683
|
}
|
|
1151
3684
|
|
|
1152
|
-
.ui-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
box-shadow: 0 8px 18px rgba(99, 102, 241, 0.22);
|
|
3685
|
+
.ui-control__hint {
|
|
3686
|
+
color: #94a3b8;
|
|
3687
|
+
font-size: 12px;
|
|
1156
3688
|
}
|
|
1157
3689
|
|
|
1158
|
-
.ui-
|
|
1159
|
-
|
|
1160
|
-
|
|
3690
|
+
.ui-control__info {
|
|
3691
|
+
margin-left: 6px;
|
|
3692
|
+
width: 16px;
|
|
3693
|
+
height: 16px;
|
|
3694
|
+
border-radius: 999px;
|
|
3695
|
+
border: 1px solid var(--color-border);
|
|
3696
|
+
background: #ffffff;
|
|
3697
|
+
color: var(--color-primary-strong);
|
|
3698
|
+
font-size: 10px;
|
|
3699
|
+
line-height: 1;
|
|
3700
|
+
display: inline-flex;
|
|
3701
|
+
align-items: center;
|
|
3702
|
+
justify-content: center;
|
|
3703
|
+
cursor: pointer;
|
|
1161
3704
|
}
|
|
1162
3705
|
|
|
1163
|
-
.ui-
|
|
1164
|
-
background:
|
|
1165
|
-
color: #fff;
|
|
1166
|
-
box-shadow: 0 8px 18px rgba(239, 68, 68, 0.25);
|
|
3706
|
+
.ui-control__info:hover {
|
|
3707
|
+
background: #ffffff;
|
|
1167
3708
|
}
|
|
1168
3709
|
|
|
1169
|
-
.ui-
|
|
1170
|
-
|
|
3710
|
+
.ui-control__info-panel {
|
|
3711
|
+
margin-top: 8px;
|
|
3712
|
+
padding: 10px 12px;
|
|
3713
|
+
border-radius: 14px;
|
|
3714
|
+
background: #ffffff;
|
|
3715
|
+
border: 1px solid var(--color-border);
|
|
3716
|
+
color: #475569;
|
|
3717
|
+
font-size: 12px;
|
|
3718
|
+
line-height: 1.4;
|
|
3719
|
+
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
|
|
1171
3720
|
}
|
|
1172
3721
|
|
|
1173
|
-
.ui-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
3722
|
+
.ui-control__checkbox {
|
|
3723
|
+
width: 22px;
|
|
3724
|
+
height: 22px;
|
|
3725
|
+
padding: 0;
|
|
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;
|
|
1177
3732
|
}
|
|
1178
3733
|
`
|
|
1179
3734
|
}
|
|
@@ -1197,13 +3752,36 @@ export class UiButtonComponent {
|
|
|
1197
3752
|
const existing = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
1198
3753
|
return !existing.includes(marker);
|
|
1199
3754
|
};
|
|
3755
|
+
if (component.name === 'ui-menu') {
|
|
3756
|
+
const menuImportPath = schemasRoot
|
|
3757
|
+
? buildRelativeImportPath(componentDir, path_1.default.join(schemasRoot, 'menu.gen'))
|
|
3758
|
+
: './menu.gen';
|
|
3759
|
+
component.template = `
|
|
3760
|
+
import { Component, Input } from '@angular/core'
|
|
3761
|
+
import { NgFor, NgIf } from '@angular/common'
|
|
3762
|
+
import { RouterLink, RouterLinkActive } from '@angular/router'
|
|
3763
|
+
import { GeneratedMenu, generatedMenu } from '${menuImportPath}'
|
|
3764
|
+
|
|
3765
|
+
@Component({
|
|
3766
|
+
selector: 'ui-menu',
|
|
3767
|
+
standalone: true,
|
|
3768
|
+
imports: [NgIf, NgFor, RouterLink, RouterLinkActive],
|
|
3769
|
+
templateUrl: './ui-menu.component.html',
|
|
3770
|
+
styleUrls: ['./ui-menu.component.scss']
|
|
3771
|
+
})
|
|
3772
|
+
export class UiMenuComponent {
|
|
3773
|
+
@Input() menu: GeneratedMenu = generatedMenu
|
|
3774
|
+
@Input() title = 'Generate UI'
|
|
3775
|
+
}
|
|
3776
|
+
`;
|
|
3777
|
+
}
|
|
1200
3778
|
if (shouldOverwrite(tsPath, 'infoOpen')) {
|
|
1201
3779
|
fs_1.default.writeFileSync(tsPath, component.template.trimStart());
|
|
1202
3780
|
}
|
|
1203
|
-
if (shouldOverwrite(htmlPath, 'ui-
|
|
3781
|
+
if (shouldOverwrite(htmlPath, 'ui-control__info-panel')) {
|
|
1204
3782
|
fs_1.default.writeFileSync(htmlPath, component.html.trimStart());
|
|
1205
3783
|
}
|
|
1206
|
-
if (shouldOverwrite(scssPath, 'ui-
|
|
3784
|
+
if (shouldOverwrite(scssPath, 'ui-control__info-panel')) {
|
|
1207
3785
|
fs_1.default.writeFileSync(scssPath, component.scss.trimStart());
|
|
1208
3786
|
}
|
|
1209
3787
|
}
|
|
@@ -1217,7 +3795,12 @@ function toFolderName(operationId) {
|
|
|
1217
3795
|
return toFileBase(operationId);
|
|
1218
3796
|
}
|
|
1219
3797
|
function toFileBase(operationId) {
|
|
1220
|
-
return operationId;
|
|
3798
|
+
return String(operationId).replace(/[\\/]/g, '');
|
|
3799
|
+
}
|
|
3800
|
+
function toSafeFileName(value) {
|
|
3801
|
+
return String(value)
|
|
3802
|
+
.replace(/[\\/]/g, '-')
|
|
3803
|
+
.replace(/\s+/g, '-');
|
|
1221
3804
|
}
|
|
1222
3805
|
function toKebab(value) {
|
|
1223
3806
|
return value
|
|
@@ -1234,6 +3817,15 @@ function toPascalCase(value) {
|
|
|
1234
3817
|
.map(part => part[0].toUpperCase() + part.slice(1))
|
|
1235
3818
|
.join('');
|
|
1236
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
|
+
}
|
|
1237
3829
|
function buildSchemaImportPath(featureDir, schemasRoot, rawName) {
|
|
1238
3830
|
const schemaFile = path_1.default.join(schemasRoot, 'overlays', `${rawName}.screen.json`);
|
|
1239
3831
|
let relativePath = path_1.default.relative(featureDir, schemaFile);
|
|
@@ -1243,6 +3835,14 @@ function buildSchemaImportPath(featureDir, schemasRoot, rawName) {
|
|
|
1243
3835
|
}
|
|
1244
3836
|
return relativePath;
|
|
1245
3837
|
}
|
|
3838
|
+
function buildRelativeImportPath(fromDir, targetFile) {
|
|
3839
|
+
let relativePath = path_1.default.relative(fromDir, targetFile);
|
|
3840
|
+
relativePath = toPosixPath(relativePath);
|
|
3841
|
+
if (!relativePath.startsWith('.')) {
|
|
3842
|
+
relativePath = `./${relativePath}`;
|
|
3843
|
+
}
|
|
3844
|
+
return relativePath;
|
|
3845
|
+
}
|
|
1246
3846
|
function toPosixPath(value) {
|
|
1247
3847
|
return value.split(path_1.default.sep).join(path_1.default.posix.sep);
|
|
1248
3848
|
}
|