angular-data-mapper 1.0.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/.claude/settings.local.json +10 -0
- package/LICENSE +190 -0
- package/PUBLISHING.md +75 -0
- package/README.md +214 -0
- package/angular.json +121 -0
- package/package.json +67 -0
- package/projects/demo-app/public/favicon.ico +0 -0
- package/projects/demo-app/src/app/app.config.ts +12 -0
- package/projects/demo-app/src/app/app.html +36 -0
- package/projects/demo-app/src/app/app.routes.ts +62 -0
- package/projects/demo-app/src/app/app.scss +65 -0
- package/projects/demo-app/src/app/app.ts +11 -0
- package/projects/demo-app/src/app/layout/app-layout.component.ts +294 -0
- package/projects/demo-app/src/app/pages/mapper-page/mapper-page.component.html +87 -0
- package/projects/demo-app/src/app/pages/mapper-page/mapper-page.component.scss +202 -0
- package/projects/demo-app/src/app/pages/mapper-page/mapper-page.component.ts +192 -0
- package/projects/demo-app/src/app/pages/mappings-page/add-mapping-dialog.component.ts +163 -0
- package/projects/demo-app/src/app/pages/mappings-page/mappings-page.component.ts +306 -0
- package/projects/demo-app/src/app/pages/schema-creator-page/schema-creator-page.component.ts +88 -0
- package/projects/demo-app/src/app/pages/schema-editor-page/schema-editor-page.component.html +108 -0
- package/projects/demo-app/src/app/pages/schema-editor-page/schema-editor-page.component.scss +317 -0
- package/projects/demo-app/src/app/pages/schema-editor-page/schema-editor-page.component.ts +129 -0
- package/projects/demo-app/src/app/services/app-state.service.ts +233 -0
- package/projects/demo-app/src/app/services/sample-data.service.ts +228 -0
- package/projects/demo-app/src/index.html +15 -0
- package/projects/demo-app/src/main.ts +6 -0
- package/projects/demo-app/src/styles.scss +54 -0
- package/projects/demo-app/tsconfig.app.json +13 -0
- package/projects/ngx-data-mapper/ng-package.json +7 -0
- package/projects/ngx-data-mapper/package.json +40 -0
- package/projects/ngx-data-mapper/src/lib/components/array-filter-modal/array-filter-modal.component.html +183 -0
- package/projects/ngx-data-mapper/src/lib/components/array-filter-modal/array-filter-modal.component.scss +352 -0
- package/projects/ngx-data-mapper/src/lib/components/array-filter-modal/array-filter-modal.component.ts +277 -0
- package/projects/ngx-data-mapper/src/lib/components/array-selector-modal/array-selector-modal.component.html +174 -0
- package/projects/ngx-data-mapper/src/lib/components/array-selector-modal/array-selector-modal.component.scss +357 -0
- package/projects/ngx-data-mapper/src/lib/components/array-selector-modal/array-selector-modal.component.ts +258 -0
- package/projects/ngx-data-mapper/src/lib/components/condition-builder/condition-builder.component.html +139 -0
- package/projects/ngx-data-mapper/src/lib/components/condition-builder/condition-builder.component.scss +213 -0
- package/projects/ngx-data-mapper/src/lib/components/condition-builder/condition-builder.component.ts +261 -0
- package/projects/ngx-data-mapper/src/lib/components/data-mapper/data-mapper.component.html +199 -0
- package/projects/ngx-data-mapper/src/lib/components/data-mapper/data-mapper.component.scss +321 -0
- package/projects/ngx-data-mapper/src/lib/components/data-mapper/data-mapper.component.ts +618 -0
- package/projects/ngx-data-mapper/src/lib/components/default-value-popover/default-value-popover.component.html +67 -0
- package/projects/ngx-data-mapper/src/lib/components/default-value-popover/default-value-popover.component.scss +97 -0
- package/projects/ngx-data-mapper/src/lib/components/default-value-popover/default-value-popover.component.ts +105 -0
- package/projects/ngx-data-mapper/src/lib/components/schema-editor/schema-editor.component.html +552 -0
- package/projects/ngx-data-mapper/src/lib/components/schema-editor/schema-editor.component.scss +824 -0
- package/projects/ngx-data-mapper/src/lib/components/schema-editor/schema-editor.component.ts +730 -0
- package/projects/ngx-data-mapper/src/lib/components/schema-tree/schema-tree.component.html +82 -0
- package/projects/ngx-data-mapper/src/lib/components/schema-tree/schema-tree.component.scss +352 -0
- package/projects/ngx-data-mapper/src/lib/components/schema-tree/schema-tree.component.ts +225 -0
- package/projects/ngx-data-mapper/src/lib/components/transformation-popover/transformation-popover.component.html +346 -0
- package/projects/ngx-data-mapper/src/lib/components/transformation-popover/transformation-popover.component.scss +511 -0
- package/projects/ngx-data-mapper/src/lib/components/transformation-popover/transformation-popover.component.ts +368 -0
- package/projects/ngx-data-mapper/src/lib/models/json-schema.model.ts +164 -0
- package/projects/ngx-data-mapper/src/lib/models/schema.model.ts +173 -0
- package/projects/ngx-data-mapper/src/lib/services/mapping.service.ts +615 -0
- package/projects/ngx-data-mapper/src/lib/services/schema-parser.service.ts +270 -0
- package/projects/ngx-data-mapper/src/lib/services/svg-connector.service.ts +135 -0
- package/projects/ngx-data-mapper/src/lib/services/transformation.service.ts +453 -0
- package/projects/ngx-data-mapper/src/public-api.ts +22 -0
- package/projects/ngx-data-mapper/tsconfig.lib.json +13 -0
- package/projects/ngx-data-mapper/tsconfig.lib.prod.json +9 -0
- package/tsconfig.json +28 -0
package/projects/ngx-data-mapper/src/lib/components/schema-editor/schema-editor.component.html
ADDED
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
<div class="schema-editor">
|
|
2
|
+
<!-- Schema Header -->
|
|
3
|
+
<div class="editor-header">
|
|
4
|
+
@if (showSchemaName) {
|
|
5
|
+
<div class="schema-name-section">
|
|
6
|
+
<mat-form-field appearance="outline" class="schema-name-field">
|
|
7
|
+
<mat-label>Schema Name</mat-label>
|
|
8
|
+
<input
|
|
9
|
+
#schemaNameInput
|
|
10
|
+
matInput
|
|
11
|
+
[value]="schemaName()"
|
|
12
|
+
(input)="onSchemaNameChange($any($event.target).value, schemaNameInput)"
|
|
13
|
+
placeholder="Enter schema name"
|
|
14
|
+
/>
|
|
15
|
+
</mat-form-field>
|
|
16
|
+
</div>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@if (showJsonToggle) {
|
|
20
|
+
<mat-button-toggle-group [value]="viewMode()" (change)="setViewMode($event.value)" class="view-toggle">
|
|
21
|
+
<mat-button-toggle value="visual">Visual</mat-button-toggle>
|
|
22
|
+
<mat-button-toggle value="json">JSON</mat-button-toggle>
|
|
23
|
+
</mat-button-toggle-group>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
<div class="header-actions">
|
|
27
|
+
@if (viewMode() === 'visual') {
|
|
28
|
+
<button mat-flat-button color="primary" (click)="addField()">
|
|
29
|
+
<mat-icon>add</mat-icon>
|
|
30
|
+
Add Field
|
|
31
|
+
</button>
|
|
32
|
+
} @else {
|
|
33
|
+
<button mat-stroked-button (click)="copyJson()" matTooltip="Copy JSON to clipboard">
|
|
34
|
+
<mat-icon>content_copy</mat-icon>
|
|
35
|
+
Copy
|
|
36
|
+
</button>
|
|
37
|
+
<button mat-stroked-button (click)="formatJson()">
|
|
38
|
+
<mat-icon>auto_fix_high</mat-icon>
|
|
39
|
+
Format
|
|
40
|
+
</button>
|
|
41
|
+
<button mat-flat-button color="primary" (click)="applyJsonChanges()" [disabled]="jsonError()">
|
|
42
|
+
<mat-icon>check</mat-icon>
|
|
43
|
+
Apply
|
|
44
|
+
</button>
|
|
45
|
+
}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<!-- JSON View -->
|
|
50
|
+
@if (viewMode() === 'json') {
|
|
51
|
+
<div class="json-view">
|
|
52
|
+
@if (jsonError()) {
|
|
53
|
+
<div class="json-error">
|
|
54
|
+
<mat-icon>error</mat-icon>
|
|
55
|
+
{{ jsonError() }}
|
|
56
|
+
</div>
|
|
57
|
+
}
|
|
58
|
+
<textarea
|
|
59
|
+
class="json-textarea"
|
|
60
|
+
[value]="jsonText()"
|
|
61
|
+
(input)="onJsonTextChange($any($event.target).value)"
|
|
62
|
+
spellcheck="false"
|
|
63
|
+
></textarea>
|
|
64
|
+
</div>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
<!-- Fields List (Visual View) -->
|
|
68
|
+
<div class="fields-container" [class.hidden]="viewMode() === 'json'">
|
|
69
|
+
@if (fields().length === 0) {
|
|
70
|
+
<div class="empty-state">
|
|
71
|
+
<mat-icon>schema</mat-icon>
|
|
72
|
+
<p>No fields yet. Click "Add Field" to get started.</p>
|
|
73
|
+
</div>
|
|
74
|
+
} @else {
|
|
75
|
+
<div
|
|
76
|
+
class="fields-list"
|
|
77
|
+
cdkDropList
|
|
78
|
+
[cdkDropListData]="fields()"
|
|
79
|
+
(cdkDropListDropped)="onFieldDrop($event)"
|
|
80
|
+
>
|
|
81
|
+
@for (field of fields(); track field.id) {
|
|
82
|
+
<div class="field-wrapper" cdkDrag>
|
|
83
|
+
<!-- Drag Placeholder -->
|
|
84
|
+
<div class="drag-placeholder" *cdkDragPlaceholder></div>
|
|
85
|
+
|
|
86
|
+
<!-- Drag Preview -->
|
|
87
|
+
<div *cdkDragPreview class="drag-preview">
|
|
88
|
+
<mat-icon>{{ getTypeIcon(field.type) }}</mat-icon>
|
|
89
|
+
{{ field.name || 'unnamed' }}
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div
|
|
93
|
+
class="field-item"
|
|
94
|
+
[class.is-editing]="field.isEditing"
|
|
95
|
+
[class.is-complex]="field.type === 'object' || field.type === 'array'"
|
|
96
|
+
>
|
|
97
|
+
<!-- Left section: field controls -->
|
|
98
|
+
<div class="field-left">
|
|
99
|
+
<!-- Drag Handle -->
|
|
100
|
+
<div class="drag-handle" cdkDragHandle matTooltip="Drag to reorder">
|
|
101
|
+
<mat-icon>drag_indicator</mat-icon>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<!-- Indent/Outdent Buttons -->
|
|
105
|
+
<div class="indent-buttons">
|
|
106
|
+
<button
|
|
107
|
+
class="indent-btn"
|
|
108
|
+
[disabled]="!canIndent(field, fields())"
|
|
109
|
+
(click)="indentField(field, fields())"
|
|
110
|
+
matTooltip="Move into previous object/array"
|
|
111
|
+
>
|
|
112
|
+
<mat-icon>chevron_right</mat-icon>
|
|
113
|
+
</button>
|
|
114
|
+
<button
|
|
115
|
+
class="indent-btn"
|
|
116
|
+
disabled
|
|
117
|
+
matTooltip="Move out of parent"
|
|
118
|
+
>
|
|
119
|
+
<mat-icon>chevron_left</mat-icon>
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<!-- Expand/Collapse -->
|
|
124
|
+
@if (field.type === 'object' || field.type === 'array') {
|
|
125
|
+
<button
|
|
126
|
+
class="expand-btn"
|
|
127
|
+
(click)="toggleExpand(field)"
|
|
128
|
+
[matTooltip]="field.expanded ? 'Collapse' : 'Expand'"
|
|
129
|
+
>
|
|
130
|
+
<mat-icon>{{ field.expanded ? 'expand_more' : 'chevron_right' }}</mat-icon>
|
|
131
|
+
</button>
|
|
132
|
+
} @else {
|
|
133
|
+
<span class="expand-placeholder"></span>
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
<!-- Type Icon -->
|
|
137
|
+
<mat-icon class="type-icon" [matTooltip]="field.type">{{ getTypeIcon(field.type) }}</mat-icon>
|
|
138
|
+
|
|
139
|
+
<!-- Field Name -->
|
|
140
|
+
@if (field.isEditing) {
|
|
141
|
+
<input
|
|
142
|
+
class="field-name-input"
|
|
143
|
+
[value]="field.name"
|
|
144
|
+
(input)="onFieldNameChange(field, $event)"
|
|
145
|
+
(blur)="stopEdit(field)"
|
|
146
|
+
(keydown)="onFieldNameKeydown($event, field)"
|
|
147
|
+
placeholder="Field name"
|
|
148
|
+
autofocus
|
|
149
|
+
/>
|
|
150
|
+
} @else {
|
|
151
|
+
<span class="field-name" (dblclick)="startEdit(field)">
|
|
152
|
+
{{ field.name || 'unnamed' }}
|
|
153
|
+
@if (field.type === 'array') {
|
|
154
|
+
<span class="array-indicator">[]</span>
|
|
155
|
+
}
|
|
156
|
+
</span>
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
<!-- Required Toggle -->
|
|
160
|
+
<button
|
|
161
|
+
class="required-btn"
|
|
162
|
+
[class.is-required]="field.required"
|
|
163
|
+
(click)="toggleRequired(field)"
|
|
164
|
+
[matTooltip]="field.required ? 'Required (click to make optional)' : 'Optional (click to make required)'"
|
|
165
|
+
>
|
|
166
|
+
<mat-icon>{{ field.required ? 'star' : 'star_border' }}</mat-icon>
|
|
167
|
+
</button>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<!-- Middle section: description (flexible) -->
|
|
171
|
+
<input
|
|
172
|
+
class="description-input"
|
|
173
|
+
[value]="field.description || ''"
|
|
174
|
+
(input)="onDescriptionChange(field, $any($event.target).value)"
|
|
175
|
+
placeholder="Description..."
|
|
176
|
+
/>
|
|
177
|
+
|
|
178
|
+
<!-- Right section: type and actions (fixed position) -->
|
|
179
|
+
<div class="field-right">
|
|
180
|
+
<!-- Type Selector -->
|
|
181
|
+
<mat-form-field appearance="outline" class="type-selector">
|
|
182
|
+
<mat-select [value]="field.type" (selectionChange)="onFieldTypeChange(field, $event.value)">
|
|
183
|
+
@for (type of fieldTypes; track type.value) {
|
|
184
|
+
<mat-option [value]="type.value">
|
|
185
|
+
<mat-icon>{{ type.icon }}</mat-icon>
|
|
186
|
+
{{ type.label }}
|
|
187
|
+
</mat-option>
|
|
188
|
+
}
|
|
189
|
+
</mat-select>
|
|
190
|
+
</mat-form-field>
|
|
191
|
+
|
|
192
|
+
<!-- Actions -->
|
|
193
|
+
<div class="field-actions">
|
|
194
|
+
@if (field.type === 'object' || field.type === 'array') {
|
|
195
|
+
<button
|
|
196
|
+
mat-icon-button
|
|
197
|
+
(click)="addChildField(field)"
|
|
198
|
+
matTooltip="Add child field"
|
|
199
|
+
>
|
|
200
|
+
<mat-icon>add_circle_outline</mat-icon>
|
|
201
|
+
</button>
|
|
202
|
+
}
|
|
203
|
+
@if (field.type === 'string' || field.type === 'number') {
|
|
204
|
+
<button
|
|
205
|
+
mat-icon-button
|
|
206
|
+
(click)="toggleValuesEditor(field)"
|
|
207
|
+
[matTooltip]="field.allowedValues?.length ? 'Edit allowed values (' + field.allowedValues!.length + ')' : 'Add allowed values'"
|
|
208
|
+
[class.has-values]="field.allowedValues?.length"
|
|
209
|
+
>
|
|
210
|
+
<mat-icon>list</mat-icon>
|
|
211
|
+
</button>
|
|
212
|
+
}
|
|
213
|
+
@if (field.type !== 'object' && field.type !== 'array') {
|
|
214
|
+
<button
|
|
215
|
+
mat-icon-button
|
|
216
|
+
(click)="toggleDefaultEditor(field)"
|
|
217
|
+
[matTooltip]="field.defaultValue !== undefined ? 'Default: ' + field.defaultValue : 'Set default value'"
|
|
218
|
+
[class.has-default]="field.defaultValue !== undefined"
|
|
219
|
+
>
|
|
220
|
+
<mat-icon>{{ field.defaultValue !== undefined ? 'label' : 'label_outline' }}</mat-icon>
|
|
221
|
+
</button>
|
|
222
|
+
}
|
|
223
|
+
<button mat-icon-button [matMenuTriggerFor]="fieldMenu" matTooltip="More options">
|
|
224
|
+
<mat-icon>more_vert</mat-icon>
|
|
225
|
+
</button>
|
|
226
|
+
<mat-menu #fieldMenu="matMenu">
|
|
227
|
+
<button mat-menu-item (click)="startEdit(field)">
|
|
228
|
+
<mat-icon>edit</mat-icon>
|
|
229
|
+
<span>Rename</span>
|
|
230
|
+
</button>
|
|
231
|
+
<button mat-menu-item (click)="duplicateField(field, fields())">
|
|
232
|
+
<mat-icon>content_copy</mat-icon>
|
|
233
|
+
<span>Duplicate</span>
|
|
234
|
+
</button>
|
|
235
|
+
<button mat-menu-item (click)="deleteField(field, fields())" class="delete-action">
|
|
236
|
+
<mat-icon>delete</mat-icon>
|
|
237
|
+
<span>Delete</span>
|
|
238
|
+
</button>
|
|
239
|
+
</mat-menu>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<!-- Allowed Values Editor -->
|
|
245
|
+
@if (field.isEditingValues && (field.type === 'string' || field.type === 'number')) {
|
|
246
|
+
<div class="allowed-values-editor">
|
|
247
|
+
<div class="values-header">
|
|
248
|
+
<span class="values-label">Allowed values:</span>
|
|
249
|
+
<input
|
|
250
|
+
#valueInput
|
|
251
|
+
class="value-input"
|
|
252
|
+
type="text"
|
|
253
|
+
placeholder="Type value and press Enter"
|
|
254
|
+
(keydown)="onAllowedValueKeydown($event, field, valueInput)"
|
|
255
|
+
/>
|
|
256
|
+
<button class="add-value-btn" (click)="addAllowedValue(field, $event)" matTooltip="Add value">
|
|
257
|
+
<mat-icon>add</mat-icon>
|
|
258
|
+
</button>
|
|
259
|
+
</div>
|
|
260
|
+
@if (field.allowedValues && field.allowedValues.length > 0) {
|
|
261
|
+
<div class="values-list">
|
|
262
|
+
@for (value of field.allowedValues; track value; let vi = $index) {
|
|
263
|
+
<span class="value-chip">
|
|
264
|
+
{{ value }}
|
|
265
|
+
<button class="remove-value-btn" (click)="removeAllowedValue(field, vi)" matTooltip="Remove">
|
|
266
|
+
<mat-icon>close</mat-icon>
|
|
267
|
+
</button>
|
|
268
|
+
</span>
|
|
269
|
+
}
|
|
270
|
+
</div>
|
|
271
|
+
} @else {
|
|
272
|
+
<div class="no-values">No values defined yet</div>
|
|
273
|
+
}
|
|
274
|
+
</div>
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
<!-- Default Value Editor -->
|
|
278
|
+
@if (field.isEditingDefault && field.type !== 'object' && field.type !== 'array') {
|
|
279
|
+
<div class="default-value-editor">
|
|
280
|
+
<div class="default-header">
|
|
281
|
+
<span class="default-label">Default value:</span>
|
|
282
|
+
@if (field.type === 'boolean') {
|
|
283
|
+
<select
|
|
284
|
+
class="default-select"
|
|
285
|
+
[value]="field.defaultValue?.toString() || ''"
|
|
286
|
+
(change)="onDefaultValueChange(field, $any($event.target).value)"
|
|
287
|
+
>
|
|
288
|
+
<option value="">No default</option>
|
|
289
|
+
<option value="true">true</option>
|
|
290
|
+
<option value="false">false</option>
|
|
291
|
+
</select>
|
|
292
|
+
} @else {
|
|
293
|
+
<input
|
|
294
|
+
class="default-input"
|
|
295
|
+
[type]="field.type === 'number' ? 'number' : 'text'"
|
|
296
|
+
[value]="field.defaultValue ?? ''"
|
|
297
|
+
(input)="onDefaultValueChange(field, $any($event.target).value)"
|
|
298
|
+
(keydown)="onDefaultValueKeydown($event, field)"
|
|
299
|
+
placeholder="Enter default value"
|
|
300
|
+
/>
|
|
301
|
+
}
|
|
302
|
+
@if (field.defaultValue !== undefined) {
|
|
303
|
+
<button class="clear-default-btn" (click)="clearDefaultValue(field)" matTooltip="Clear default">
|
|
304
|
+
<mat-icon>close</mat-icon>
|
|
305
|
+
</button>
|
|
306
|
+
}
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
<!-- Nested Children -->
|
|
312
|
+
@if ((field.type === 'object' || field.type === 'array') && field.expanded && field.children) {
|
|
313
|
+
<div
|
|
314
|
+
class="nested-fields"
|
|
315
|
+
cdkDropList
|
|
316
|
+
[cdkDropListData]="field.children"
|
|
317
|
+
(cdkDropListDropped)="onFieldDrop($event)"
|
|
318
|
+
>
|
|
319
|
+
@if (field.children.length > 0) {
|
|
320
|
+
@for (child of field.children; track child.id) {
|
|
321
|
+
<div class="field-wrapper" cdkDrag>
|
|
322
|
+
<div class="drag-placeholder" *cdkDragPlaceholder></div>
|
|
323
|
+
<div *cdkDragPreview class="drag-preview">
|
|
324
|
+
<mat-icon>{{ getTypeIcon(child.type) }}</mat-icon>
|
|
325
|
+
{{ child.name || 'unnamed' }}
|
|
326
|
+
</div>
|
|
327
|
+
<ng-container *ngTemplateOutlet="fieldItemTemplate; context: { field: child, parentList: field.children, level: 1 }"></ng-container>
|
|
328
|
+
|
|
329
|
+
<!-- Allowed Values Editor for nested field -->
|
|
330
|
+
@if (child.isEditingValues && (child.type === 'string' || child.type === 'number')) {
|
|
331
|
+
<div class="allowed-values-editor">
|
|
332
|
+
<div class="values-header">
|
|
333
|
+
<span class="values-label">Allowed values:</span>
|
|
334
|
+
<input
|
|
335
|
+
#childValueInput
|
|
336
|
+
class="value-input"
|
|
337
|
+
type="text"
|
|
338
|
+
placeholder="Type value and press Enter"
|
|
339
|
+
(keydown)="onAllowedValueKeydown($event, child, childValueInput)"
|
|
340
|
+
/>
|
|
341
|
+
<button class="add-value-btn" (click)="addAllowedValue(child, $event)" matTooltip="Add value">
|
|
342
|
+
<mat-icon>add</mat-icon>
|
|
343
|
+
</button>
|
|
344
|
+
</div>
|
|
345
|
+
@if (child.allowedValues && child.allowedValues.length > 0) {
|
|
346
|
+
<div class="values-list">
|
|
347
|
+
@for (value of child.allowedValues; track value; let vi = $index) {
|
|
348
|
+
<span class="value-chip">
|
|
349
|
+
{{ value }}
|
|
350
|
+
<button class="remove-value-btn" (click)="removeAllowedValue(child, vi)" matTooltip="Remove">
|
|
351
|
+
<mat-icon>close</mat-icon>
|
|
352
|
+
</button>
|
|
353
|
+
</span>
|
|
354
|
+
}
|
|
355
|
+
</div>
|
|
356
|
+
} @else {
|
|
357
|
+
<div class="no-values">No values defined yet</div>
|
|
358
|
+
}
|
|
359
|
+
</div>
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
<!-- Default Value Editor for nested field -->
|
|
363
|
+
@if (child.isEditingDefault && child.type !== 'object' && child.type !== 'array') {
|
|
364
|
+
<div class="default-value-editor">
|
|
365
|
+
<div class="default-header">
|
|
366
|
+
<span class="default-label">Default value:</span>
|
|
367
|
+
@if (child.type === 'boolean') {
|
|
368
|
+
<select
|
|
369
|
+
class="default-select"
|
|
370
|
+
[value]="child.defaultValue?.toString() || ''"
|
|
371
|
+
(change)="onDefaultValueChange(child, $any($event.target).value)"
|
|
372
|
+
>
|
|
373
|
+
<option value="">No default</option>
|
|
374
|
+
<option value="true">true</option>
|
|
375
|
+
<option value="false">false</option>
|
|
376
|
+
</select>
|
|
377
|
+
} @else {
|
|
378
|
+
<input
|
|
379
|
+
class="default-input"
|
|
380
|
+
[type]="child.type === 'number' ? 'number' : 'text'"
|
|
381
|
+
[value]="child.defaultValue ?? ''"
|
|
382
|
+
(input)="onDefaultValueChange(child, $any($event.target).value)"
|
|
383
|
+
(keydown)="onDefaultValueKeydown($event, child)"
|
|
384
|
+
placeholder="Enter default value"
|
|
385
|
+
/>
|
|
386
|
+
}
|
|
387
|
+
@if (child.defaultValue !== undefined) {
|
|
388
|
+
<button class="clear-default-btn" (click)="clearDefaultValue(child)" matTooltip="Clear default">
|
|
389
|
+
<mat-icon>close</mat-icon>
|
|
390
|
+
</button>
|
|
391
|
+
}
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
}
|
|
395
|
+
</div>
|
|
396
|
+
}
|
|
397
|
+
} @else {
|
|
398
|
+
<div class="empty-nested">
|
|
399
|
+
<span>No child fields</span>
|
|
400
|
+
<button mat-button (click)="addChildField(field)" color="primary">
|
|
401
|
+
<mat-icon>add</mat-icon>
|
|
402
|
+
Add field
|
|
403
|
+
</button>
|
|
404
|
+
</div>
|
|
405
|
+
}
|
|
406
|
+
</div>
|
|
407
|
+
}
|
|
408
|
+
</div>
|
|
409
|
+
}
|
|
410
|
+
</div>
|
|
411
|
+
}
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
|
|
415
|
+
<!-- Field Item Template (for nested fields) -->
|
|
416
|
+
<ng-template #fieldItemTemplate let-field="field" let-parentList="parentList" let-level="level">
|
|
417
|
+
<div
|
|
418
|
+
class="field-item"
|
|
419
|
+
[class.is-editing]="field.isEditing"
|
|
420
|
+
[class.is-complex]="field.type === 'object' || field.type === 'array'"
|
|
421
|
+
>
|
|
422
|
+
<!-- Left section: field controls -->
|
|
423
|
+
<div class="field-left">
|
|
424
|
+
<div class="drag-handle" cdkDragHandle matTooltip="Drag to reorder">
|
|
425
|
+
<mat-icon>drag_indicator</mat-icon>
|
|
426
|
+
</div>
|
|
427
|
+
|
|
428
|
+
<div class="indent-buttons">
|
|
429
|
+
<button
|
|
430
|
+
class="indent-btn"
|
|
431
|
+
[disabled]="!canIndent(field, parentList)"
|
|
432
|
+
(click)="indentField(field, parentList)"
|
|
433
|
+
matTooltip="Move into previous object/array"
|
|
434
|
+
>
|
|
435
|
+
<mat-icon>chevron_right</mat-icon>
|
|
436
|
+
</button>
|
|
437
|
+
<button
|
|
438
|
+
class="indent-btn"
|
|
439
|
+
(click)="outdentField(field, parentList, level)"
|
|
440
|
+
matTooltip="Move out of parent"
|
|
441
|
+
>
|
|
442
|
+
<mat-icon>chevron_left</mat-icon>
|
|
443
|
+
</button>
|
|
444
|
+
</div>
|
|
445
|
+
|
|
446
|
+
@if (field.type === 'object' || field.type === 'array') {
|
|
447
|
+
<button class="expand-btn" (click)="toggleExpand(field)" [matTooltip]="field.expanded ? 'Collapse' : 'Expand'">
|
|
448
|
+
<mat-icon>{{ field.expanded ? 'expand_more' : 'chevron_right' }}</mat-icon>
|
|
449
|
+
</button>
|
|
450
|
+
} @else {
|
|
451
|
+
<span class="expand-placeholder"></span>
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
<mat-icon class="type-icon" [matTooltip]="field.type">{{ getTypeIcon(field.type) }}</mat-icon>
|
|
455
|
+
|
|
456
|
+
@if (field.isEditing) {
|
|
457
|
+
<input
|
|
458
|
+
class="field-name-input"
|
|
459
|
+
[value]="field.name"
|
|
460
|
+
(input)="onFieldNameChange(field, $event)"
|
|
461
|
+
(blur)="stopEdit(field)"
|
|
462
|
+
(keydown)="onFieldNameKeydown($event, field)"
|
|
463
|
+
placeholder="Field name"
|
|
464
|
+
autofocus
|
|
465
|
+
/>
|
|
466
|
+
} @else {
|
|
467
|
+
<span class="field-name" (dblclick)="startEdit(field)">
|
|
468
|
+
{{ field.name || 'unnamed' }}
|
|
469
|
+
@if (field.type === 'array') {
|
|
470
|
+
<span class="array-indicator">[]</span>
|
|
471
|
+
}
|
|
472
|
+
</span>
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
<button
|
|
476
|
+
class="required-btn"
|
|
477
|
+
[class.is-required]="field.required"
|
|
478
|
+
(click)="toggleRequired(field)"
|
|
479
|
+
[matTooltip]="field.required ? 'Required' : 'Optional'"
|
|
480
|
+
>
|
|
481
|
+
<mat-icon>{{ field.required ? 'star' : 'star_border' }}</mat-icon>
|
|
482
|
+
</button>
|
|
483
|
+
</div>
|
|
484
|
+
|
|
485
|
+
<!-- Middle section: description (flexible) -->
|
|
486
|
+
<input
|
|
487
|
+
class="description-input"
|
|
488
|
+
[value]="field.description || ''"
|
|
489
|
+
(input)="onDescriptionChange(field, $any($event.target).value)"
|
|
490
|
+
placeholder="Description..."
|
|
491
|
+
/>
|
|
492
|
+
|
|
493
|
+
<!-- Right section: type and actions (fixed position) -->
|
|
494
|
+
<div class="field-right">
|
|
495
|
+
<mat-form-field appearance="outline" class="type-selector">
|
|
496
|
+
<mat-select [value]="field.type" (selectionChange)="onFieldTypeChange(field, $event.value)">
|
|
497
|
+
@for (type of fieldTypes; track type.value) {
|
|
498
|
+
<mat-option [value]="type.value">
|
|
499
|
+
<mat-icon>{{ type.icon }}</mat-icon>
|
|
500
|
+
{{ type.label }}
|
|
501
|
+
</mat-option>
|
|
502
|
+
}
|
|
503
|
+
</mat-select>
|
|
504
|
+
</mat-form-field>
|
|
505
|
+
|
|
506
|
+
<div class="field-actions">
|
|
507
|
+
@if (field.type === 'object' || field.type === 'array') {
|
|
508
|
+
<button mat-icon-button (click)="addChildField(field)" matTooltip="Add child field">
|
|
509
|
+
<mat-icon>add_circle_outline</mat-icon>
|
|
510
|
+
</button>
|
|
511
|
+
}
|
|
512
|
+
@if (field.type === 'string' || field.type === 'number') {
|
|
513
|
+
<button
|
|
514
|
+
mat-icon-button
|
|
515
|
+
(click)="toggleValuesEditor(field)"
|
|
516
|
+
[matTooltip]="field.allowedValues?.length ? 'Edit allowed values' : 'Add allowed values'"
|
|
517
|
+
[class.has-values]="field.allowedValues?.length"
|
|
518
|
+
>
|
|
519
|
+
<mat-icon>list</mat-icon>
|
|
520
|
+
</button>
|
|
521
|
+
}
|
|
522
|
+
@if (field.type !== 'object' && field.type !== 'array') {
|
|
523
|
+
<button
|
|
524
|
+
mat-icon-button
|
|
525
|
+
(click)="toggleDefaultEditor(field)"
|
|
526
|
+
[matTooltip]="field.defaultValue !== undefined ? 'Default: ' + field.defaultValue : 'Set default value'"
|
|
527
|
+
[class.has-default]="field.defaultValue !== undefined"
|
|
528
|
+
>
|
|
529
|
+
<mat-icon>{{ field.defaultValue !== undefined ? 'label' : 'label_outline' }}</mat-icon>
|
|
530
|
+
</button>
|
|
531
|
+
}
|
|
532
|
+
<button mat-icon-button [matMenuTriggerFor]="nestedMenu" matTooltip="More options">
|
|
533
|
+
<mat-icon>more_vert</mat-icon>
|
|
534
|
+
</button>
|
|
535
|
+
<mat-menu #nestedMenu="matMenu">
|
|
536
|
+
<button mat-menu-item (click)="startEdit(field)">
|
|
537
|
+
<mat-icon>edit</mat-icon>
|
|
538
|
+
<span>Rename</span>
|
|
539
|
+
</button>
|
|
540
|
+
<button mat-menu-item (click)="duplicateField(field, parentList)">
|
|
541
|
+
<mat-icon>content_copy</mat-icon>
|
|
542
|
+
<span>Duplicate</span>
|
|
543
|
+
</button>
|
|
544
|
+
<button mat-menu-item (click)="deleteField(field, parentList)" class="delete-action">
|
|
545
|
+
<mat-icon>delete</mat-icon>
|
|
546
|
+
<span>Delete</span>
|
|
547
|
+
</button>
|
|
548
|
+
</mat-menu>
|
|
549
|
+
</div>
|
|
550
|
+
</div>
|
|
551
|
+
</div>
|
|
552
|
+
</ng-template>
|