@expeed/ngx-data-mapper 1.0.0 → 1.2.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.
|
@@ -4,29 +4,122 @@ import * as i1 from '@angular/common';
|
|
|
4
4
|
import { CommonModule } from '@angular/common';
|
|
5
5
|
import * as i3 from '@angular/material/icon';
|
|
6
6
|
import { MatIconModule } from '@angular/material/icon';
|
|
7
|
-
import * as i2
|
|
7
|
+
import * as i2 from '@angular/material/button';
|
|
8
8
|
import { MatButtonModule } from '@angular/material/button';
|
|
9
|
-
import * as
|
|
9
|
+
import * as i7 from '@angular/material/tooltip';
|
|
10
10
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
11
|
-
import * as i2 from '@angular/forms';
|
|
11
|
+
import * as i2$1 from '@angular/forms';
|
|
12
12
|
import { FormsModule } from '@angular/forms';
|
|
13
|
-
import * as
|
|
13
|
+
import * as i4 from '@angular/material/select';
|
|
14
14
|
import { MatSelectModule } from '@angular/material/select';
|
|
15
|
-
import * as i5
|
|
15
|
+
import * as i5 from '@angular/material/input';
|
|
16
16
|
import { MatInputModule } from '@angular/material/input';
|
|
17
17
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
18
|
+
import * as i8 from '@angular/material/checkbox';
|
|
19
|
+
import { MatCheckboxModule } from '@angular/material/checkbox';
|
|
20
|
+
import * as i10 from '@angular/cdk/drag-drop';
|
|
21
|
+
import { moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop';
|
|
18
22
|
import * as i6 from '@angular/material/radio';
|
|
19
23
|
import { MatRadioModule } from '@angular/material/radio';
|
|
20
|
-
import * as i7 from '@angular/material/slide-toggle';
|
|
24
|
+
import * as i7$1 from '@angular/material/slide-toggle';
|
|
21
25
|
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
|
22
26
|
import * as i9 from '@angular/material/divider';
|
|
23
27
|
import { MatDividerModule } from '@angular/material/divider';
|
|
24
28
|
import * as i6$1 from '@angular/material/datepicker';
|
|
25
29
|
import { MatDatepickerModule } from '@angular/material/datepicker';
|
|
26
30
|
import { MatNativeDateModule } from '@angular/material/core';
|
|
27
|
-
import * as
|
|
31
|
+
import * as i8$1 from '@angular/material/menu';
|
|
28
32
|
import { MatMenuModule } from '@angular/material/menu';
|
|
29
|
-
import
|
|
33
|
+
import * as i9$1 from '@angular/material/button-toggle';
|
|
34
|
+
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Standard JSON Schema (draft-07) TypeScript interfaces
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* Convert JSON Schema to flat field list for UI rendering
|
|
41
|
+
*/
|
|
42
|
+
function schemaToFields(schema, parentPath = '') {
|
|
43
|
+
const fields = [];
|
|
44
|
+
if (schema.type === 'object' && schema.properties) {
|
|
45
|
+
for (const [name, propSchema] of Object.entries(schema.properties)) {
|
|
46
|
+
const path = parentPath ? `${parentPath}.${name}` : name;
|
|
47
|
+
const field = {
|
|
48
|
+
name,
|
|
49
|
+
path,
|
|
50
|
+
schema: propSchema,
|
|
51
|
+
expanded: false,
|
|
52
|
+
};
|
|
53
|
+
if (propSchema.type === 'object' && propSchema.properties) {
|
|
54
|
+
field.children = schemaToFields(propSchema, path);
|
|
55
|
+
}
|
|
56
|
+
else if (propSchema.type === 'array' && propSchema.items) {
|
|
57
|
+
field.children = schemaToFields(propSchema.items, `${path}[]`);
|
|
58
|
+
}
|
|
59
|
+
fields.push(field);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return fields;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get the simple type for display purposes
|
|
66
|
+
*/
|
|
67
|
+
function getSchemaType(schema) {
|
|
68
|
+
if (Array.isArray(schema.type)) {
|
|
69
|
+
return schema.type.filter(t => t !== 'null').join(' | ');
|
|
70
|
+
}
|
|
71
|
+
if (schema.type === 'integer') {
|
|
72
|
+
return 'number';
|
|
73
|
+
}
|
|
74
|
+
if (schema.format === 'date' || schema.format === 'date-time') {
|
|
75
|
+
return 'date';
|
|
76
|
+
}
|
|
77
|
+
return schema.type || 'any';
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Create an empty JSON Schema for a new schema definition
|
|
81
|
+
*/
|
|
82
|
+
function createEmptySchema(title = 'New Schema') {
|
|
83
|
+
return {
|
|
84
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
85
|
+
title,
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties: {},
|
|
88
|
+
required: [],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Add a property to a schema
|
|
93
|
+
*/
|
|
94
|
+
function addProperty(schema, name, type, options) {
|
|
95
|
+
const newSchema = { ...schema };
|
|
96
|
+
newSchema.properties = { ...newSchema.properties };
|
|
97
|
+
const propSchema = { type };
|
|
98
|
+
if (options?.description) {
|
|
99
|
+
propSchema.description = options.description;
|
|
100
|
+
}
|
|
101
|
+
if (type === 'object') {
|
|
102
|
+
propSchema.properties = {};
|
|
103
|
+
}
|
|
104
|
+
else if (type === 'array') {
|
|
105
|
+
propSchema.items = { type: 'string' };
|
|
106
|
+
}
|
|
107
|
+
newSchema.properties[name] = propSchema;
|
|
108
|
+
if (options?.required) {
|
|
109
|
+
newSchema.required = [...(newSchema.required || []), name];
|
|
110
|
+
}
|
|
111
|
+
return newSchema;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Remove a property from a schema
|
|
115
|
+
*/
|
|
116
|
+
function removeProperty(schema, name) {
|
|
117
|
+
const newSchema = { ...schema };
|
|
118
|
+
newSchema.properties = { ...newSchema.properties };
|
|
119
|
+
delete newSchema.properties[name];
|
|
120
|
+
newSchema.required = (newSchema.required || []).filter(r => r !== name);
|
|
121
|
+
return newSchema;
|
|
122
|
+
}
|
|
30
123
|
|
|
31
124
|
class MappingService {
|
|
32
125
|
mappings = signal([], ...(ngDevMode ? [{ debugName: "mappings" }] : []));
|
|
@@ -34,16 +127,21 @@ class MappingService {
|
|
|
34
127
|
arrayToObjectMappings = signal([], ...(ngDevMode ? [{ debugName: "arrayToObjectMappings" }] : []));
|
|
35
128
|
defaultValues = signal([], ...(ngDevMode ? [{ debugName: "defaultValues" }] : []));
|
|
36
129
|
selectedMappingId = signal(null, ...(ngDevMode ? [{ debugName: "selectedMappingId" }] : []));
|
|
130
|
+
_sourceSchemaRef = signal(null, ...(ngDevMode ? [{ debugName: "_sourceSchemaRef" }] : []));
|
|
131
|
+
_targetSchemaRef = signal(null, ...(ngDevMode ? [{ debugName: "_targetSchemaRef" }] : []));
|
|
37
132
|
dragState = signal({
|
|
38
133
|
isDragging: false,
|
|
39
134
|
sourceField: null,
|
|
40
135
|
startPoint: null,
|
|
41
136
|
currentPoint: null,
|
|
137
|
+
dragMode: 'new',
|
|
42
138
|
}, ...(ngDevMode ? [{ debugName: "dragState" }] : []));
|
|
43
139
|
allMappings = computed(() => this.mappings(), ...(ngDevMode ? [{ debugName: "allMappings" }] : []));
|
|
44
140
|
allArrayMappings = computed(() => this.arrayMappings(), ...(ngDevMode ? [{ debugName: "allArrayMappings" }] : []));
|
|
45
141
|
allArrayToObjectMappings = computed(() => this.arrayToObjectMappings(), ...(ngDevMode ? [{ debugName: "allArrayToObjectMappings" }] : []));
|
|
46
142
|
allDefaultValues = computed(() => this.defaultValues(), ...(ngDevMode ? [{ debugName: "allDefaultValues" }] : []));
|
|
143
|
+
sourceSchemaRef = computed(() => this._sourceSchemaRef(), ...(ngDevMode ? [{ debugName: "sourceSchemaRef" }] : []));
|
|
144
|
+
targetSchemaRef = computed(() => this._targetSchemaRef(), ...(ngDevMode ? [{ debugName: "targetSchemaRef" }] : []));
|
|
47
145
|
selectedMapping = computed(() => {
|
|
48
146
|
const id = this.selectedMappingId();
|
|
49
147
|
return this.mappings().find((m) => m.id === id) || null;
|
|
@@ -58,6 +156,26 @@ class MappingService {
|
|
|
58
156
|
sourceField: field,
|
|
59
157
|
startPoint,
|
|
60
158
|
currentPoint: startPoint,
|
|
159
|
+
dragMode: 'new',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
startEndpointDrag(mappingId, endpointType, startPoint, sourceFieldIndex) {
|
|
163
|
+
const mapping = this.mappings().find(m => m.id === mappingId);
|
|
164
|
+
if (!mapping)
|
|
165
|
+
return;
|
|
166
|
+
// For source endpoint, we show the line from target back to cursor
|
|
167
|
+
// For target endpoint, we show the line from source(s) to cursor
|
|
168
|
+
const dragMode = endpointType === 'source' ? 'move-source' : 'move-target';
|
|
169
|
+
this.dragState.set({
|
|
170
|
+
isDragging: true,
|
|
171
|
+
sourceField: endpointType === 'source'
|
|
172
|
+
? mapping.sourceFields[sourceFieldIndex ?? 0]
|
|
173
|
+
: mapping.targetField,
|
|
174
|
+
startPoint,
|
|
175
|
+
currentPoint: startPoint,
|
|
176
|
+
dragMode,
|
|
177
|
+
mappingId,
|
|
178
|
+
sourceFieldIndex,
|
|
61
179
|
});
|
|
62
180
|
}
|
|
63
181
|
updateDragPosition(currentPoint) {
|
|
@@ -72,8 +190,62 @@ class MappingService {
|
|
|
72
190
|
sourceField: null,
|
|
73
191
|
startPoint: null,
|
|
74
192
|
currentPoint: null,
|
|
193
|
+
dragMode: 'new',
|
|
75
194
|
});
|
|
76
195
|
}
|
|
196
|
+
changeSourceField(mappingId, newSourceField, sourceFieldIndex) {
|
|
197
|
+
const mapping = this.mappings().find(m => m.id === mappingId);
|
|
198
|
+
if (!mapping)
|
|
199
|
+
return;
|
|
200
|
+
// Don't allow if new source is the same as target
|
|
201
|
+
if (newSourceField.id === mapping.targetField.id)
|
|
202
|
+
return;
|
|
203
|
+
// Don't allow if source already exists in the mapping
|
|
204
|
+
if (mapping.sourceFields.some(sf => sf.id === newSourceField.id))
|
|
205
|
+
return;
|
|
206
|
+
if (sourceFieldIndex !== undefined && mapping.sourceFields.length > 1) {
|
|
207
|
+
// Replace specific source field in multi-source mapping
|
|
208
|
+
const newSourceFields = [...mapping.sourceFields];
|
|
209
|
+
newSourceFields[sourceFieldIndex] = newSourceField;
|
|
210
|
+
this.mappings.update(mappings => mappings.map(m => m.id === mappingId ? { ...m, sourceFields: newSourceFields } : m));
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
// Single source mapping - replace the source field
|
|
214
|
+
this.mappings.update(mappings => mappings.map(m => m.id === mappingId ? { ...m, sourceFields: [newSourceField] } : m));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
changeTargetField(mappingId, newTargetField) {
|
|
218
|
+
const mapping = this.mappings().find(m => m.id === mappingId);
|
|
219
|
+
if (!mapping)
|
|
220
|
+
return;
|
|
221
|
+
// Don't allow if new target is same as any source
|
|
222
|
+
if (mapping.sourceFields.some(sf => sf.id === newTargetField.id))
|
|
223
|
+
return;
|
|
224
|
+
// Check if another mapping already targets this field
|
|
225
|
+
const existingMapping = this.mappings().find(m => m.targetField.id === newTargetField.id && m.id !== mappingId);
|
|
226
|
+
if (existingMapping) {
|
|
227
|
+
// Merge into existing mapping (add sources)
|
|
228
|
+
const mergedSources = [
|
|
229
|
+
...existingMapping.sourceFields,
|
|
230
|
+
...mapping.sourceFields.filter(sf => !existingMapping.sourceFields.some(esf => esf.id === sf.id)),
|
|
231
|
+
];
|
|
232
|
+
this.mappings.update(mappings => mappings
|
|
233
|
+
.filter(m => m.id !== mappingId) // Remove old mapping
|
|
234
|
+
.map(m => m.id === existingMapping.id
|
|
235
|
+
? {
|
|
236
|
+
...m,
|
|
237
|
+
sourceFields: mergedSources,
|
|
238
|
+
transformations: mergedSources.length > 1
|
|
239
|
+
? [{ type: 'concat', separator: ' ' }]
|
|
240
|
+
: m.transformations,
|
|
241
|
+
}
|
|
242
|
+
: m));
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
// Simply update the target field
|
|
246
|
+
this.mappings.update(mappings => mappings.map(m => m.id === mappingId ? { ...m, targetField: newTargetField } : m));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
77
249
|
createMapping(sourceFields, targetField, transformation) {
|
|
78
250
|
// Check if this is an array-to-array mapping
|
|
79
251
|
const sourceField = sourceFields[0];
|
|
@@ -97,9 +269,9 @@ class MappingService {
|
|
|
97
269
|
...existingMapping.sourceFields,
|
|
98
270
|
...sourceFields.filter((sf) => !existingMapping.sourceFields.some((esf) => esf.id === sf.id)),
|
|
99
271
|
],
|
|
100
|
-
|
|
101
|
-
? { type: 'concat', separator: ' ', ...transformation }
|
|
102
|
-
: transformation
|
|
272
|
+
transformations: existingMapping.sourceFields.length + sourceFields.length > 1
|
|
273
|
+
? [{ type: 'concat', separator: ' ', ...transformation }]
|
|
274
|
+
: transformation ? [transformation] : [{ type: 'direct' }],
|
|
103
275
|
};
|
|
104
276
|
this.mappings.update((mappings) => mappings.map((m) => (m.id === existingMapping.id ? updatedMapping : m)));
|
|
105
277
|
return updatedMapping;
|
|
@@ -108,7 +280,7 @@ class MappingService {
|
|
|
108
280
|
id: this.generateId(),
|
|
109
281
|
sourceFields,
|
|
110
282
|
targetField,
|
|
111
|
-
|
|
283
|
+
transformations: transformation ? [transformation] : [{ type: 'direct' }],
|
|
112
284
|
isArrayMapping: false, // Only true for array-to-array connections
|
|
113
285
|
arrayMappingId, // Links to parent array mapping if within array context
|
|
114
286
|
isArrayToObjectMapping: false,
|
|
@@ -150,7 +322,7 @@ class MappingService {
|
|
|
150
322
|
id: arrayMapping.id,
|
|
151
323
|
sourceFields: [sourceArray],
|
|
152
324
|
targetField: targetArray,
|
|
153
|
-
|
|
325
|
+
transformations: [{ type: 'direct' }],
|
|
154
326
|
isArrayMapping: true,
|
|
155
327
|
};
|
|
156
328
|
this.mappings.update((mappings) => [...mappings, fieldMapping]);
|
|
@@ -177,7 +349,7 @@ class MappingService {
|
|
|
177
349
|
id: arrayToObjectMapping.id,
|
|
178
350
|
sourceFields: [sourceArray],
|
|
179
351
|
targetField: targetObject,
|
|
180
|
-
|
|
352
|
+
transformations: [{ type: 'direct' }],
|
|
181
353
|
isArrayToObjectMapping: true,
|
|
182
354
|
};
|
|
183
355
|
this.mappings.update((mappings) => [...mappings, fieldMapping]);
|
|
@@ -241,8 +413,8 @@ class MappingService {
|
|
|
241
413
|
updateMapping(mappingId, updates) {
|
|
242
414
|
this.mappings.update((mappings) => mappings.map((m) => (m.id === mappingId ? { ...m, ...updates } : m)));
|
|
243
415
|
}
|
|
244
|
-
|
|
245
|
-
this.mappings.update((mappings) => mappings.map((m) => m.id === mappingId ? { ...m,
|
|
416
|
+
updateTransformations(mappingId, transformations) {
|
|
417
|
+
this.mappings.update((mappings) => mappings.map((m) => m.id === mappingId ? { ...m, transformations } : m));
|
|
246
418
|
}
|
|
247
419
|
removeMapping(mappingId) {
|
|
248
420
|
this.mappings.update((mappings) => mappings.filter((m) => m.id !== mappingId));
|
|
@@ -262,9 +434,9 @@ class MappingService {
|
|
|
262
434
|
? {
|
|
263
435
|
...m,
|
|
264
436
|
sourceFields: m.sourceFields.filter((sf) => sf.id !== sourceFieldId),
|
|
265
|
-
|
|
266
|
-
? { type: 'direct' }
|
|
267
|
-
: m.
|
|
437
|
+
transformations: m.sourceFields.length - 1 === 1
|
|
438
|
+
? [{ type: 'direct' }]
|
|
439
|
+
: m.transformations,
|
|
268
440
|
}
|
|
269
441
|
: m));
|
|
270
442
|
}
|
|
@@ -284,13 +456,20 @@ class MappingService {
|
|
|
284
456
|
this.arrayToObjectMappings.set([]);
|
|
285
457
|
this.defaultValues.set([]);
|
|
286
458
|
this.selectedMappingId.set(null);
|
|
459
|
+
this._sourceSchemaRef.set(null);
|
|
460
|
+
this._targetSchemaRef.set(null);
|
|
461
|
+
}
|
|
462
|
+
setSourceSchemaRef(ref) {
|
|
463
|
+
this._sourceSchemaRef.set(ref);
|
|
464
|
+
}
|
|
465
|
+
setTargetSchemaRef(ref) {
|
|
466
|
+
this._targetSchemaRef.set(ref);
|
|
287
467
|
}
|
|
288
468
|
// Default value methods
|
|
289
469
|
setDefaultValue(targetField, value) {
|
|
290
|
-
const valueType = this.getValueType(targetField.type);
|
|
291
470
|
const existingDefault = this.defaultValues().find(d => d.targetField.id === targetField.id);
|
|
292
471
|
if (existingDefault) {
|
|
293
|
-
const updated = { ...existingDefault, value
|
|
472
|
+
const updated = { ...existingDefault, value };
|
|
294
473
|
this.defaultValues.update(dv => dv.map(d => d.id === existingDefault.id ? updated : d));
|
|
295
474
|
return updated;
|
|
296
475
|
}
|
|
@@ -298,7 +477,6 @@ class MappingService {
|
|
|
298
477
|
id: this.generateId(),
|
|
299
478
|
targetField,
|
|
300
479
|
value,
|
|
301
|
-
valueType,
|
|
302
480
|
};
|
|
303
481
|
this.defaultValues.update(dv => [...dv, newDefault]);
|
|
304
482
|
return newDefault;
|
|
@@ -312,22 +490,46 @@ class MappingService {
|
|
|
312
490
|
hasDefaultValue(targetFieldId) {
|
|
313
491
|
return this.defaultValues().some(d => d.targetField.id === targetFieldId);
|
|
314
492
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
493
|
+
exportMappings(name, description) {
|
|
494
|
+
const exportData = {
|
|
495
|
+
version: '1.0',
|
|
496
|
+
name: name || 'Mapping Configuration',
|
|
497
|
+
description: description || '',
|
|
498
|
+
mappings: this.mappings(),
|
|
499
|
+
arrayMappings: this.arrayMappings(),
|
|
500
|
+
arrayToObjectMappings: this.arrayToObjectMappings(),
|
|
501
|
+
defaultValues: this.defaultValues(),
|
|
502
|
+
};
|
|
503
|
+
// Include schema refs if set
|
|
504
|
+
if (this._sourceSchemaRef()) {
|
|
505
|
+
exportData['sourceSchemaRef'] = this._sourceSchemaRef();
|
|
321
506
|
}
|
|
507
|
+
if (this._targetSchemaRef()) {
|
|
508
|
+
exportData['targetSchemaRef'] = this._targetSchemaRef();
|
|
509
|
+
}
|
|
510
|
+
return JSON.stringify(exportData, null, 2);
|
|
322
511
|
}
|
|
323
|
-
|
|
512
|
+
/**
|
|
513
|
+
* Export mappings as a MappingDocument object (not stringified)
|
|
514
|
+
*/
|
|
515
|
+
exportMappingsAsObject(name, description) {
|
|
324
516
|
const exportData = {
|
|
517
|
+
version: '1.0',
|
|
518
|
+
name: name || 'Mapping Configuration',
|
|
519
|
+
description: description || '',
|
|
325
520
|
mappings: this.mappings(),
|
|
326
521
|
arrayMappings: this.arrayMappings(),
|
|
327
522
|
arrayToObjectMappings: this.arrayToObjectMappings(),
|
|
328
523
|
defaultValues: this.defaultValues(),
|
|
329
524
|
};
|
|
330
|
-
|
|
525
|
+
// Include schema refs if set
|
|
526
|
+
if (this._sourceSchemaRef()) {
|
|
527
|
+
exportData['sourceSchemaRef'] = this._sourceSchemaRef();
|
|
528
|
+
}
|
|
529
|
+
if (this._targetSchemaRef()) {
|
|
530
|
+
exportData['targetSchemaRef'] = this._targetSchemaRef();
|
|
531
|
+
}
|
|
532
|
+
return exportData;
|
|
331
533
|
}
|
|
332
534
|
importMappings(json) {
|
|
333
535
|
try {
|
|
@@ -337,6 +539,8 @@ class MappingService {
|
|
|
337
539
|
this.arrayMappings.set(data.arrayMappings || []);
|
|
338
540
|
this.arrayToObjectMappings.set(data.arrayToObjectMappings || []);
|
|
339
541
|
this.defaultValues.set(data.defaultValues || []);
|
|
542
|
+
this._sourceSchemaRef.set(data.sourceSchemaRef || null);
|
|
543
|
+
this._targetSchemaRef.set(data.targetSchemaRef || null);
|
|
340
544
|
}
|
|
341
545
|
else {
|
|
342
546
|
// Legacy format
|
|
@@ -377,6 +581,10 @@ class TransformationService {
|
|
|
377
581
|
return String(values[0] ?? '').toUpperCase();
|
|
378
582
|
case 'lowercase':
|
|
379
583
|
return String(values[0] ?? '').toLowerCase();
|
|
584
|
+
case 'trim':
|
|
585
|
+
return String(values[0] ?? '').trim();
|
|
586
|
+
case 'mask':
|
|
587
|
+
return this.applyMask(String(values[0] ?? ''), config.pattern ?? '');
|
|
380
588
|
case 'dateFormat':
|
|
381
589
|
return this.formatDate(values[0], config.inputFormat, config.outputFormat);
|
|
382
590
|
case 'extractYear':
|
|
@@ -395,12 +603,83 @@ class TransformationService {
|
|
|
395
603
|
return this.formatNumber(values[0], config.decimalPlaces, config.prefix, config.suffix);
|
|
396
604
|
case 'template':
|
|
397
605
|
return this.applyTemplate(config.template ?? '', sourceFields, sourceValues);
|
|
398
|
-
case 'custom':
|
|
399
|
-
return this.applyCustomExpression(config.expression ?? '', sourceFields, sourceValues);
|
|
400
606
|
default:
|
|
401
607
|
return String(values[0] ?? '');
|
|
402
608
|
}
|
|
403
609
|
}
|
|
610
|
+
/**
|
|
611
|
+
* Apply a transformation to a single value (for chained transformations)
|
|
612
|
+
*/
|
|
613
|
+
applyTransformationToValue(value, config) {
|
|
614
|
+
const str = String(value ?? '');
|
|
615
|
+
switch (config.type) {
|
|
616
|
+
case 'direct':
|
|
617
|
+
return str;
|
|
618
|
+
case 'concat':
|
|
619
|
+
// For single value, just return it (concat needs multiple values)
|
|
620
|
+
return str;
|
|
621
|
+
case 'substring':
|
|
622
|
+
return str.substring(config.startIndex ?? 0, config.endIndex ?? str.length);
|
|
623
|
+
case 'replace':
|
|
624
|
+
return str.replace(new RegExp(config.searchValue ?? '', 'g'), config.replaceValue ?? '');
|
|
625
|
+
case 'uppercase':
|
|
626
|
+
return str.toUpperCase();
|
|
627
|
+
case 'lowercase':
|
|
628
|
+
return str.toLowerCase();
|
|
629
|
+
case 'trim':
|
|
630
|
+
return str.trim();
|
|
631
|
+
case 'mask':
|
|
632
|
+
return this.applyMask(str, config.pattern ?? '');
|
|
633
|
+
case 'dateFormat':
|
|
634
|
+
return this.formatDate(value, config.inputFormat, config.outputFormat);
|
|
635
|
+
case 'extractYear':
|
|
636
|
+
return this.extractDatePart(value, 'year');
|
|
637
|
+
case 'extractMonth':
|
|
638
|
+
return this.extractDatePart(value, 'month');
|
|
639
|
+
case 'extractDay':
|
|
640
|
+
return this.extractDatePart(value, 'day');
|
|
641
|
+
case 'extractHour':
|
|
642
|
+
return this.extractDatePart(value, 'hour');
|
|
643
|
+
case 'extractMinute':
|
|
644
|
+
return this.extractDatePart(value, 'minute');
|
|
645
|
+
case 'extractSecond':
|
|
646
|
+
return this.extractDatePart(value, 'second');
|
|
647
|
+
case 'numberFormat':
|
|
648
|
+
return this.formatNumber(value, config.decimalPlaces, config.prefix, config.suffix);
|
|
649
|
+
case 'template':
|
|
650
|
+
// For single value, replace {0} with the value
|
|
651
|
+
return (config.template ?? '').replace(/\{0\}/g, str);
|
|
652
|
+
default:
|
|
653
|
+
return str;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Apply multiple transformations in sequence, respecting conditions
|
|
658
|
+
*/
|
|
659
|
+
applyTransformations(sourceValues, sourceFields, transformations) {
|
|
660
|
+
if (transformations.length === 0) {
|
|
661
|
+
return '';
|
|
662
|
+
}
|
|
663
|
+
// Get initial value for first transformation
|
|
664
|
+
const initialValues = sourceFields.map((f) => this.getValueByPath(sourceValues, f.path));
|
|
665
|
+
const initialValue = initialValues.length === 1 ? initialValues[0] : initialValues.join(' ');
|
|
666
|
+
// Apply first transformation using source fields (check condition first)
|
|
667
|
+
let result;
|
|
668
|
+
if (this.isConditionMet(initialValue, transformations[0])) {
|
|
669
|
+
result = this.applyTransformation(sourceValues, sourceFields, transformations[0]);
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
result = String(initialValue ?? '');
|
|
673
|
+
}
|
|
674
|
+
// Apply subsequent transformations to the result
|
|
675
|
+
for (let i = 1; i < transformations.length; i++) {
|
|
676
|
+
if (this.isConditionMet(result, transformations[i])) {
|
|
677
|
+
result = this.applyTransformationToValue(result, transformations[i]);
|
|
678
|
+
}
|
|
679
|
+
// If condition not met, result passes through unchanged
|
|
680
|
+
}
|
|
681
|
+
return result;
|
|
682
|
+
}
|
|
404
683
|
getValueByPath(obj, path) {
|
|
405
684
|
return path.split('.').reduce((acc, part) => {
|
|
406
685
|
if (acc && typeof acc === 'object') {
|
|
@@ -411,10 +690,10 @@ class TransformationService {
|
|
|
411
690
|
}
|
|
412
691
|
applyTemplate(template, sourceFields, sourceValues) {
|
|
413
692
|
let result = template;
|
|
414
|
-
|
|
693
|
+
// Replace positional placeholders {0}, {1}, etc.
|
|
694
|
+
sourceFields.forEach((field, index) => {
|
|
415
695
|
const value = this.getValueByPath(sourceValues, field.path);
|
|
416
|
-
result = result.replace(new RegExp(`\\{${
|
|
417
|
-
result = result.replace(new RegExp(`\\{${field.path}\\}`, 'g'), String(value ?? ''));
|
|
696
|
+
result = result.replace(new RegExp(`\\{${index}\\}`, 'g'), String(value ?? ''));
|
|
418
697
|
});
|
|
419
698
|
return result;
|
|
420
699
|
}
|
|
@@ -478,21 +757,24 @@ class TransformationService {
|
|
|
478
757
|
: num.toString();
|
|
479
758
|
return `${prefix ?? ''}${formatted}${suffix ?? ''}`;
|
|
480
759
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
760
|
+
applyMask(input, pattern) {
|
|
761
|
+
if (!pattern)
|
|
762
|
+
return input;
|
|
763
|
+
let result = '';
|
|
764
|
+
let inputIndex = 0;
|
|
765
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
766
|
+
const patternChar = pattern.charAt(i);
|
|
767
|
+
if (patternChar === '#') {
|
|
768
|
+
if (inputIndex < input.length) {
|
|
769
|
+
result += input.charAt(inputIndex);
|
|
770
|
+
inputIndex++;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
result += patternChar;
|
|
775
|
+
}
|
|
495
776
|
}
|
|
777
|
+
return result;
|
|
496
778
|
}
|
|
497
779
|
getTransformationLabel(type) {
|
|
498
780
|
const labels = {
|
|
@@ -502,6 +784,8 @@ class TransformationService {
|
|
|
502
784
|
replace: 'Find & Replace',
|
|
503
785
|
uppercase: 'Uppercase',
|
|
504
786
|
lowercase: 'Lowercase',
|
|
787
|
+
trim: 'Trim',
|
|
788
|
+
mask: 'Mask',
|
|
505
789
|
dateFormat: 'Date Format',
|
|
506
790
|
extractYear: 'Extract Year',
|
|
507
791
|
extractMonth: 'Extract Month',
|
|
@@ -511,7 +795,6 @@ class TransformationService {
|
|
|
511
795
|
extractSecond: 'Extract Second',
|
|
512
796
|
numberFormat: 'Number Format',
|
|
513
797
|
template: 'Template',
|
|
514
|
-
custom: 'Custom Expression',
|
|
515
798
|
};
|
|
516
799
|
return labels[type];
|
|
517
800
|
}
|
|
@@ -523,6 +806,8 @@ class TransformationService {
|
|
|
523
806
|
{ type: 'replace', label: 'Find & Replace', category: 'String' },
|
|
524
807
|
{ type: 'uppercase', label: 'Uppercase', category: 'String' },
|
|
525
808
|
{ type: 'lowercase', label: 'Lowercase', category: 'String' },
|
|
809
|
+
{ type: 'trim', label: 'Trim', category: 'String' },
|
|
810
|
+
{ type: 'mask', label: 'Mask', category: 'String' },
|
|
526
811
|
{ type: 'template', label: 'Template', category: 'String' },
|
|
527
812
|
{ type: 'dateFormat', label: 'Format Date', category: 'Date' },
|
|
528
813
|
{ type: 'extractYear', label: 'Extract Year', category: 'Date' },
|
|
@@ -532,9 +817,77 @@ class TransformationService {
|
|
|
532
817
|
{ type: 'extractMinute', label: 'Extract Minute', category: 'Date' },
|
|
533
818
|
{ type: 'extractSecond', label: 'Extract Second', category: 'Date' },
|
|
534
819
|
{ type: 'numberFormat', label: 'Number Format', category: 'Number' },
|
|
535
|
-
{ type: 'custom', label: 'Custom Expression', category: 'Advanced' },
|
|
536
820
|
];
|
|
537
821
|
}
|
|
822
|
+
// Condition evaluation methods
|
|
823
|
+
evaluateCondition(value, condition) {
|
|
824
|
+
return this.evaluateGroup(value, condition);
|
|
825
|
+
}
|
|
826
|
+
evaluateGroup(value, group) {
|
|
827
|
+
if (group.children.length === 0) {
|
|
828
|
+
return true; // Empty group is always true
|
|
829
|
+
}
|
|
830
|
+
if (group.logic === 'and') {
|
|
831
|
+
return group.children.every(child => this.evaluateItem(value, child));
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
return group.children.some(child => this.evaluateItem(value, child));
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
evaluateItem(value, item) {
|
|
838
|
+
if (item.type === 'group') {
|
|
839
|
+
return this.evaluateGroup(value, item);
|
|
840
|
+
}
|
|
841
|
+
else {
|
|
842
|
+
return this.evaluateConditionItem(value, item);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
evaluateConditionItem(value, condition) {
|
|
846
|
+
const strValue = String(value ?? '');
|
|
847
|
+
const numValue = Number(value);
|
|
848
|
+
const condValue = condition.value;
|
|
849
|
+
switch (condition.operator) {
|
|
850
|
+
case 'equals':
|
|
851
|
+
return strValue === String(condValue);
|
|
852
|
+
case 'notEquals':
|
|
853
|
+
return strValue !== String(condValue);
|
|
854
|
+
case 'contains':
|
|
855
|
+
return strValue.includes(String(condValue));
|
|
856
|
+
case 'notContains':
|
|
857
|
+
return !strValue.includes(String(condValue));
|
|
858
|
+
case 'startsWith':
|
|
859
|
+
return strValue.startsWith(String(condValue));
|
|
860
|
+
case 'endsWith':
|
|
861
|
+
return strValue.endsWith(String(condValue));
|
|
862
|
+
case 'isEmpty':
|
|
863
|
+
return strValue === '' || value === null || value === undefined;
|
|
864
|
+
case 'isNotEmpty':
|
|
865
|
+
return strValue !== '' && value !== null && value !== undefined;
|
|
866
|
+
case 'greaterThan':
|
|
867
|
+
return !isNaN(numValue) && numValue > Number(condValue);
|
|
868
|
+
case 'lessThan':
|
|
869
|
+
return !isNaN(numValue) && numValue < Number(condValue);
|
|
870
|
+
case 'greaterThanOrEqual':
|
|
871
|
+
return !isNaN(numValue) && numValue >= Number(condValue);
|
|
872
|
+
case 'lessThanOrEqual':
|
|
873
|
+
return !isNaN(numValue) && numValue <= Number(condValue);
|
|
874
|
+
case 'isTrue':
|
|
875
|
+
return value === true || strValue.toLowerCase() === 'true';
|
|
876
|
+
case 'isFalse':
|
|
877
|
+
return value === false || strValue.toLowerCase() === 'false';
|
|
878
|
+
default:
|
|
879
|
+
return true;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Check if a transformation's condition is met
|
|
884
|
+
*/
|
|
885
|
+
isConditionMet(value, config) {
|
|
886
|
+
if (!config.condition?.enabled || !config.condition.root) {
|
|
887
|
+
return true; // No condition means always apply
|
|
888
|
+
}
|
|
889
|
+
return this.evaluateCondition(value, config.condition.root);
|
|
890
|
+
}
|
|
538
891
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: TransformationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
539
892
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: TransformationService, providedIn: 'root' });
|
|
540
893
|
}
|
|
@@ -585,15 +938,17 @@ class SvgConnectorService {
|
|
|
585
938
|
}
|
|
586
939
|
calculateConnectionPoint(rect, side, containerRect) {
|
|
587
940
|
const relativeY = rect.top - containerRect.top + rect.height / 2;
|
|
941
|
+
// Offset to position endpoint circles outside the schema panels
|
|
942
|
+
const endpointOffset = 8;
|
|
588
943
|
if (side === 'source') {
|
|
589
944
|
return {
|
|
590
|
-
x: rect.right - containerRect.left,
|
|
945
|
+
x: rect.right - containerRect.left + endpointOffset,
|
|
591
946
|
y: relativeY,
|
|
592
947
|
};
|
|
593
948
|
}
|
|
594
949
|
else {
|
|
595
950
|
return {
|
|
596
|
-
x: rect.left - containerRect.left,
|
|
951
|
+
x: rect.left - containerRect.left - endpointOffset,
|
|
597
952
|
y: relativeY,
|
|
598
953
|
};
|
|
599
954
|
}
|
|
@@ -761,7 +1116,7 @@ class SchemaParserService {
|
|
|
761
1116
|
return field;
|
|
762
1117
|
}
|
|
763
1118
|
mapType(schema) {
|
|
764
|
-
const type = schema.type;
|
|
1119
|
+
const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
|
|
765
1120
|
const format = schema.format;
|
|
766
1121
|
// Check format first for date types
|
|
767
1122
|
if (format === 'date' || format === 'date-time' || format === 'time') {
|
|
@@ -859,6 +1214,7 @@ class SchemaTreeComponent {
|
|
|
859
1214
|
fieldDragStart = new EventEmitter();
|
|
860
1215
|
fieldDragEnd = new EventEmitter();
|
|
861
1216
|
fieldDrop = new EventEmitter();
|
|
1217
|
+
sourceDrop = new EventEmitter(); // For endpoint dragging - drop on source field
|
|
862
1218
|
fieldPositionsChanged = new EventEmitter();
|
|
863
1219
|
fieldDefaultValueClick = new EventEmitter();
|
|
864
1220
|
schemaFieldsContainer;
|
|
@@ -910,21 +1266,45 @@ class SchemaTreeComponent {
|
|
|
910
1266
|
onDragStart(event, field) {
|
|
911
1267
|
if (this.side !== 'source')
|
|
912
1268
|
return;
|
|
1269
|
+
// Don't start new drag if endpoint dragging is in progress
|
|
1270
|
+
const dragState = this.mappingService.currentDragState();
|
|
1271
|
+
if (dragState.isDragging)
|
|
1272
|
+
return;
|
|
913
1273
|
const element = event.currentTarget;
|
|
914
1274
|
const rect = element.getBoundingClientRect();
|
|
915
1275
|
this.fieldDragStart.emit({ field, element, rect });
|
|
916
1276
|
}
|
|
1277
|
+
isEndpointDragMode() {
|
|
1278
|
+
const dragState = this.mappingService.currentDragState();
|
|
1279
|
+
return dragState.isDragging && (dragState.dragMode === 'move-source' || dragState.dragMode === 'move-target');
|
|
1280
|
+
}
|
|
1281
|
+
isSourceEndpointDragging() {
|
|
1282
|
+
const dragState = this.mappingService.currentDragState();
|
|
1283
|
+
return dragState.isDragging && dragState.dragMode === 'move-source';
|
|
1284
|
+
}
|
|
1285
|
+
isTargetEndpointDragging() {
|
|
1286
|
+
const dragState = this.mappingService.currentDragState();
|
|
1287
|
+
return dragState.isDragging && dragState.dragMode === 'move-target';
|
|
1288
|
+
}
|
|
917
1289
|
onDragOver(event) {
|
|
918
1290
|
if (this.side === 'target') {
|
|
919
1291
|
event.preventDefault();
|
|
920
1292
|
}
|
|
921
1293
|
}
|
|
922
1294
|
onDrop(event, field) {
|
|
923
|
-
if (this.side !== 'target')
|
|
924
|
-
return;
|
|
925
1295
|
const element = event.currentTarget;
|
|
926
1296
|
const rect = element.getBoundingClientRect();
|
|
927
|
-
|
|
1297
|
+
// Check if endpoint dragging is in progress (via MappingService drag state)
|
|
1298
|
+
const dragState = this.mappingService.currentDragState();
|
|
1299
|
+
// Handle source drop during endpoint dragging (moving source endpoint)
|
|
1300
|
+
if (this.side === 'source' && dragState.isDragging && dragState.dragMode === 'move-source') {
|
|
1301
|
+
this.sourceDrop.emit({ field, element, rect });
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
// Handle target drop (either endpoint dragging or new mapping)
|
|
1305
|
+
if (this.side === 'target') {
|
|
1306
|
+
this.fieldDrop.emit({ field, element, rect });
|
|
1307
|
+
}
|
|
928
1308
|
}
|
|
929
1309
|
getTypeIcon(type) {
|
|
930
1310
|
const icons = {
|
|
@@ -961,7 +1341,7 @@ class SchemaTreeComponent {
|
|
|
961
1341
|
const defaultValue = this.defaultValues.find(d => d.targetField.id === field.id);
|
|
962
1342
|
if (!defaultValue || defaultValue.value === null)
|
|
963
1343
|
return '';
|
|
964
|
-
if (defaultValue.
|
|
1344
|
+
if (defaultValue.targetField.type === 'date' && defaultValue.value) {
|
|
965
1345
|
return new Date(defaultValue.value).toLocaleDateString();
|
|
966
1346
|
}
|
|
967
1347
|
return String(defaultValue.value);
|
|
@@ -987,11 +1367,11 @@ class SchemaTreeComponent {
|
|
|
987
1367
|
return field.id;
|
|
988
1368
|
}
|
|
989
1369
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SchemaTreeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
990
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: SchemaTreeComponent, isStandalone: true, selector: "schema-tree", inputs: { schema: "schema", side: "side", mappings: "mappings", defaultValues: "defaultValues" }, outputs: { fieldDragStart: "fieldDragStart", fieldDragEnd: "fieldDragEnd", fieldDrop: "fieldDrop", fieldPositionsChanged: "fieldPositionsChanged", fieldDefaultValueClick: "fieldDefaultValueClick" }, viewQueries: [{ propertyName: "schemaFieldsContainer", first: true, predicate: ["schemaFields"], descendants: true }, { propertyName: "fieldItems", predicate: ["fieldItem"], descendants: true }], ngImport: i0, template: "<div class=\"schema-tree\" [class.source]=\"side === 'source'\" [class.target]=\"side === 'target'\">\n <div class=\"schema-header\">\n <span class=\"schema-title\">{{ schema.name }}</span>\n <span class=\"schema-badge\">{{ side === 'source' ? 'Source' : 'Target' }}</span>\n </div>\n\n <div class=\"schema-fields\" #schemaFields>\n <ng-container *ngTemplateOutlet=\"fieldList; context: { fields: schema.fields, level: 0 }\"></ng-container>\n </div>\n</div>\n\n<ng-template #fieldList let-fields=\"fields\" let-level=\"level\">\n @for (field of fields; track trackByFieldId($index, field)) {\n <div\n #fieldItem\n class=\"field-item\"\n [class.mapped]=\"isFieldMapped(field)\"\n [class.has-default]=\"hasDefaultValue(field)\"\n [class.has-children]=\"field.children && field.children.length > 0\"\n [class.expanded]=\"field.expanded\"\n [class.is-array]=\"field.type === 'array'\"\n [class.draggable]=\"side === 'source' && ((!field.children || field.children.length === 0) || field.type === 'array')\"\n [class.droppable]=\"side === 'target' && ((!field.children || field.children.length === 0) || field.type === 'array' || field.type === 'object')\"\n [class.clickable]=\"side === 'target' && (!isFieldMapped(field) || hasDefaultValue(field)) && field.type !== 'object' && field.type !== 'array'\"\n [style.padding-left.px]=\"16 + level * 20\"\n [attr.data-field-id]=\"field.id\"\n (mousedown)=\"onDragStart($event, field)\"\n (mouseup)=\"onDrop($event, field)\"\n (click)=\"onFieldClick($event, field)\"\n >\n <!-- Expand/Collapse button for nested objects -->\n @if (field.children && field.children.length > 0) {\n <button class=\"expand-btn\" (click)=\"toggleExpand(field, $event)\">\n <mat-icon>{{ field.expanded ? 'expand_more' : 'chevron_right' }}</mat-icon>\n </button>\n } @else {\n <span class=\"expand-placeholder\"></span>\n }\n\n <!-- Field type icon -->\n <mat-icon class=\"type-icon\" [matTooltip]=\"field.type\">{{ getTypeIcon(field.type) }}</mat-icon>\n\n <!-- Field name with array indicator -->\n <span class=\"field-name\">{{ field.name }}@if (field.type === 'array') {<span class=\"array-indicator\">[]</span>}</span>\n\n <!-- Mapping indicator -->\n @if (isFieldMapped(field)) {\n <span class=\"mapping-indicator\" [matTooltip]=\"getFieldMappingCount(field) + ' mapping(s)'\">\n <mat-icon>{{ field.type === 'array' ? 'loop' : 'link' }}</mat-icon>\n @if (getFieldMappingCount(field) > 1) {\n <span class=\"mapping-count\">{{ getFieldMappingCount(field) }}</span>\n }\n </span>\n }\n\n <!-- Default value indicator -->\n @if (hasDefaultValue(field)) {\n <span class=\"default-indicator\" [matTooltip]=\"'Default: ' + getDefaultValueDisplay(field)\">\n <mat-icon>edit</mat-icon>\n <span class=\"default-value\">{{ getDefaultValueDisplay(field) }}</span>\n </span>\n }\n\n <!-- Connection point - show for leaf nodes, arrays, and objects (on target for array-to-object) -->\n @if ((!field.children || field.children.length === 0) || field.type === 'array' || (side === 'target' && field.type === 'object')) {\n <div class=\"connection-point\" [class.source]=\"side === 'source'\" [class.target]=\"side === 'target'\" [class.array-point]=\"field.type === 'array'\" [class.object-point]=\"field.type === 'object'\">\n <span class=\"point-dot\"></span>\n </div>\n }\n </div>\n\n <!-- Nested children -->\n @if (field.children && field.children.length > 0 && field.expanded) {\n <div class=\"nested-fields\">\n <ng-container *ngTemplateOutlet=\"fieldList; context: { fields: field.children, level: level + 1 }\"></ng-container>\n </div>\n }\n }\n</ng-template>\n", styles: [":host{display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden}.schema-tree{background:var(--surface-card, #ffffff);border-radius:12px;box-shadow:0 2px 8px #00000014;overflow:hidden;height:100%;min-height:0;display:flex;flex-direction:column;flex:1}.schema-tree.source .schema-header{background:linear-gradient(135deg,#6366f1,#8b5cf6)}.schema-tree.source .connection-point{right:8px}.schema-tree.target .schema-header{background:linear-gradient(135deg,#10b981,#059669)}.schema-tree.target .connection-point{left:8px}.schema-header{padding:16px 20px;color:#fff;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-shrink:0}.schema-title{font-size:16px;font-weight:600;letter-spacing:.3px}.schema-badge{font-size:11px;font-weight:500;text-transform:uppercase;letter-spacing:.5px;background:#fff3;padding:4px 10px;border-radius:20px}.schema-fields{flex:1;overflow-y:auto;overflow-x:hidden;padding:8px 0;min-height:0}.field-item{display:flex;align-items:center;padding:10px 16px;gap:8px;cursor:default;transition:background-color .15s ease;position:relative;-webkit-user-select:none;user-select:none}.field-item:hover{background-color:var(--surface-hover, #f8fafc)}.field-item.mapped{background-color:var(--surface-mapped, #f0fdf4)}.field-item.mapped:hover{background-color:#dcfce7}.field-item.draggable{cursor:grab}.field-item.draggable:active{cursor:grabbing}.field-item.draggable:hover .connection-point .point-dot{transform:scale(1.3);background:#6366f1}.field-item.droppable{cursor:pointer}.field-item.droppable:hover .connection-point .point-dot{transform:scale(1.3);background:#10b981}.expand-btn{background:none;border:none;padding:0;width:24px;height:24px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:#94a3b8;border-radius:4px;transition:all .15s ease}.expand-btn:hover{background-color:#e2e8f0;color:#475569}.expand-btn mat-icon{font-size:20px;width:20px;height:20px}.expand-placeholder{width:24px;height:24px;flex-shrink:0}.type-icon{font-size:18px;width:18px;height:18px;color:#64748b;flex-shrink:0}.field-name{flex:1;font-size:14px;color:#1e293b;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.field-name .array-indicator{color:#f59e0b;font-weight:600;margin-left:2px}.field-item.is-array{background-color:#fffbeb}.field-item.is-array:hover,.field-item.is-array.mapped{background-color:#fef3c7}.field-item.is-array .type-icon{color:#f59e0b}.connection-point.array-point .point-dot{background:#f59e0b;box-shadow:0 0 0 3px #f59e0b4d}.connection-point.object-point .point-dot{background:#8b5cf6;box-shadow:0 0 0 3px #8b5cf64d}.mapping-indicator{display:flex;align-items:center;gap:2px;color:#10b981}.mapping-indicator mat-icon{font-size:16px;width:16px;height:16px}.mapping-indicator .mapping-count{font-size:11px;font-weight:600;background:#10b981;color:#fff;padding:1px 5px;border-radius:10px;min-width:16px;text-align:center}.connection-point{position:absolute;top:50%;transform:translateY(-50%);width:20px;height:20px;display:flex;align-items:center;justify-content:center}.connection-point.source{right:8px}.connection-point.target{left:8px}.point-dot{width:10px;height:10px;border-radius:50%;background:#cbd5e1;transition:all .2s ease;box-shadow:0 0 0 3px #cbd5e14d}.mapped .point-dot{background:#10b981;box-shadow:0 0 0 3px #10b9814d}.nested-fields{border-left:2px solid #e2e8f0;margin-left:28px}.field-item.has-default{background-color:#eff6ff}.field-item.has-default:hover{background-color:#dbeafe}.field-item.clickable{cursor:pointer}.field-item.clickable:hover:not(.has-default){background-color:#f1f5f9}.field-item.clickable:hover:not(.has-default):after{content:\"Click to set default\";position:absolute;right:32px;font-size:11px;color:#64748b;font-style:italic}.field-item.clickable.has-default:hover:after{content:\"Click to edit\";position:absolute;right:32px;font-size:11px;color:#3b82f6;font-style:italic}.default-indicator{display:flex;align-items:center;gap:4px;color:#3b82f6;font-size:12px;background:#dbeafe;padding:2px 8px;border-radius:4px;max-width:100px;overflow:hidden}.default-indicator mat-icon{font-size:14px;width:14px;height:14px}.default-indicator .default-value{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i8.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] });
|
|
1370
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: SchemaTreeComponent, isStandalone: true, selector: "schema-tree", inputs: { schema: "schema", side: "side", mappings: "mappings", defaultValues: "defaultValues" }, outputs: { fieldDragStart: "fieldDragStart", fieldDragEnd: "fieldDragEnd", fieldDrop: "fieldDrop", sourceDrop: "sourceDrop", fieldPositionsChanged: "fieldPositionsChanged", fieldDefaultValueClick: "fieldDefaultValueClick" }, viewQueries: [{ propertyName: "schemaFieldsContainer", first: true, predicate: ["schemaFields"], descendants: true }, { propertyName: "fieldItems", predicate: ["fieldItem"], descendants: true }], ngImport: i0, template: "<div class=\"schema-tree\" [class.source]=\"side === 'source'\" [class.target]=\"side === 'target'\">\n <div class=\"schema-header\">\n <span class=\"schema-title\">{{ schema.name }}</span>\n <span class=\"schema-badge\">{{ side === 'source' ? 'Source' : 'Target' }}</span>\n </div>\n\n <div class=\"schema-fields\" #schemaFields>\n <ng-container *ngTemplateOutlet=\"fieldList; context: { fields: schema.fields, level: 0 }\"></ng-container>\n </div>\n</div>\n\n<ng-template #fieldList let-fields=\"fields\" let-level=\"level\">\n @for (field of fields; track trackByFieldId($index, field)) {\n <div\n #fieldItem\n class=\"field-item\"\n [class.mapped]=\"isFieldMapped(field)\"\n [class.has-default]=\"hasDefaultValue(field)\"\n [class.has-children]=\"field.children && field.children.length > 0\"\n [class.expanded]=\"field.expanded\"\n [class.is-array]=\"field.type === 'array'\"\n [class.draggable]=\"side === 'source' && ((!field.children || field.children.length === 0) || field.type === 'array') && !isEndpointDragMode()\"\n [class.droppable]=\"(side === 'target' && ((!field.children || field.children.length === 0) || field.type === 'array' || field.type === 'object')) || (side === 'source' && isSourceEndpointDragging() && ((!field.children || field.children.length === 0) || field.type === 'array'))\"\n [class.endpoint-drop-target]=\"(side === 'source' && isSourceEndpointDragging()) || (side === 'target' && isTargetEndpointDragging())\"\n [class.clickable]=\"side === 'target' && (!isFieldMapped(field) || hasDefaultValue(field)) && field.type !== 'object' && field.type !== 'array'\"\n [style.padding-left.px]=\"16 + level * 20\"\n [attr.data-field-id]=\"field.id\"\n (mousedown)=\"onDragStart($event, field)\"\n (mouseup)=\"onDrop($event, field)\"\n (click)=\"onFieldClick($event, field)\"\n >\n <!-- Expand/Collapse button for nested objects -->\n @if (field.children && field.children.length > 0) {\n <button class=\"expand-btn\" (click)=\"toggleExpand(field, $event)\">\n <mat-icon>{{ field.expanded ? 'expand_more' : 'chevron_right' }}</mat-icon>\n </button>\n } @else {\n <span class=\"expand-placeholder\"></span>\n }\n\n <!-- Field type icon -->\n <mat-icon class=\"type-icon\" [matTooltip]=\"field.type\">{{ getTypeIcon(field.type) }}</mat-icon>\n\n <!-- Field name with array indicator -->\n <span class=\"field-name\">{{ field.name }}@if (field.type === 'array') {<span class=\"array-indicator\">[]</span>}</span>\n\n <!-- Mapping indicator -->\n @if (isFieldMapped(field)) {\n <span class=\"mapping-indicator\" [matTooltip]=\"getFieldMappingCount(field) + ' mapping(s)'\">\n <mat-icon>{{ field.type === 'array' ? 'loop' : 'link' }}</mat-icon>\n @if (getFieldMappingCount(field) > 1) {\n <span class=\"mapping-count\">{{ getFieldMappingCount(field) }}</span>\n }\n </span>\n }\n\n <!-- Default value indicator -->\n @if (hasDefaultValue(field)) {\n <span class=\"default-indicator\" [matTooltip]=\"'Default: ' + getDefaultValueDisplay(field)\">\n <mat-icon>edit</mat-icon>\n <span class=\"default-value\">{{ getDefaultValueDisplay(field) }}</span>\n </span>\n }\n\n <!-- Connection point - show for leaf nodes, arrays, and objects (on target for array-to-object) -->\n @if ((!field.children || field.children.length === 0) || field.type === 'array' || (side === 'target' && field.type === 'object')) {\n <div class=\"connection-point\" [class.source]=\"side === 'source'\" [class.target]=\"side === 'target'\" [class.array-point]=\"field.type === 'array'\" [class.object-point]=\"field.type === 'object'\">\n <span class=\"point-dot\"></span>\n </div>\n }\n </div>\n\n <!-- Nested children -->\n @if (field.children && field.children.length > 0 && field.expanded) {\n <div class=\"nested-fields\">\n <ng-container *ngTemplateOutlet=\"fieldList; context: { fields: field.children, level: level + 1 }\"></ng-container>\n </div>\n }\n }\n</ng-template>\n", styles: [":host{display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden}.schema-tree{background:var(--data-mapper-panel-bg, var(--surface-card, #ffffff));border-radius:var(--data-mapper-panel-border-radius, 12px);border:var(--data-mapper-panel-border, none);box-shadow:var(--data-mapper-panel-shadow, 0 2px 8px rgba(0, 0, 0, .08));overflow:hidden;height:100%;min-height:0;display:flex;flex-direction:column;flex:1}.schema-tree.source .schema-header{background:linear-gradient(135deg,#6366f1,#8b5cf6)}.schema-tree.source .connection-point{right:8px}.schema-tree.target .schema-header{background:linear-gradient(135deg,#10b981,#059669)}.schema-tree.target .connection-point{left:8px}.schema-header{padding:16px 20px;color:#fff;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-shrink:0}.schema-title{font-size:16px;font-weight:600;letter-spacing:.3px}.schema-badge{font-size:11px;font-weight:500;text-transform:uppercase;letter-spacing:.5px;background:#fff3;padding:4px 10px;border-radius:20px}.schema-fields{flex:1;overflow-y:auto;overflow-x:hidden;padding:8px 0;min-height:0}.field-item{display:flex;align-items:center;padding:10px 16px;gap:8px;cursor:default;transition:background-color .15s ease;position:relative;-webkit-user-select:none;user-select:none}.field-item:hover{background-color:var(--surface-hover, #f8fafc)}.field-item.mapped{background-color:var(--surface-mapped, #f0fdf4)}.field-item.mapped:hover{background-color:#dcfce7}.field-item.draggable{cursor:grab}.field-item.draggable:active{cursor:grabbing}.field-item.draggable:hover .connection-point .point-dot{transform:scale(1.3);background:#6366f1}.field-item.droppable{cursor:pointer}.field-item.droppable:hover .connection-point .point-dot{transform:scale(1.3);background:#10b981}.field-item.endpoint-drop-target{background-color:#fef3c7}.field-item.endpoint-drop-target:hover{background-color:#fde68a}.field-item.endpoint-drop-target .connection-point .point-dot{animation:pulse-drop-target 1s ease-in-out infinite}@keyframes pulse-drop-target{0%,to{transform:scale(1);box-shadow:0 0 0 3px #f59e0b4d}50%{transform:scale(1.2);box-shadow:0 0 0 6px #f59e0b33}}.expand-btn{background:none;border:none;padding:0;width:24px;height:24px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:#94a3b8;border-radius:4px;transition:all .15s ease}.expand-btn:hover{background-color:#e2e8f0;color:#475569}.expand-btn mat-icon{font-size:20px;width:20px;height:20px}.expand-placeholder{width:24px;height:24px;flex-shrink:0}.type-icon{font-size:18px;width:18px;height:18px;color:#64748b;flex-shrink:0}.field-name{flex:1;font-size:14px;color:#1e293b;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.field-name .array-indicator{color:#f59e0b;font-weight:600;margin-left:2px}.field-item.is-array{background-color:#fffbeb}.field-item.is-array:hover,.field-item.is-array.mapped{background-color:#fef3c7}.field-item.is-array .type-icon{color:#f59e0b}.connection-point.array-point .point-dot{background:#f59e0b;box-shadow:0 0 0 3px #f59e0b4d}.connection-point.object-point .point-dot{background:#8b5cf6;box-shadow:0 0 0 3px #8b5cf64d}.mapping-indicator{display:flex;align-items:center;gap:2px;color:#10b981}.mapping-indicator mat-icon{font-size:16px;width:16px;height:16px}.mapping-indicator .mapping-count{font-size:11px;font-weight:600;background:#10b981;color:#fff;padding:1px 5px;border-radius:10px;min-width:16px;text-align:center}.connection-point{position:absolute;top:50%;transform:translateY(-50%);width:20px;height:20px;display:flex;align-items:center;justify-content:center}.connection-point.source{right:8px}.connection-point.target{left:8px}.point-dot{width:10px;height:10px;border-radius:50%;background:#cbd5e1;transition:all .2s ease;box-shadow:0 0 0 3px #cbd5e14d}.mapped .point-dot{background:#10b981;box-shadow:0 0 0 3px #10b9814d}.nested-fields{border-left:2px solid #e2e8f0;margin-left:28px}.field-item.has-default{background-color:#eff6ff}.field-item.has-default:hover{background-color:#dbeafe}.field-item.clickable{cursor:pointer}.field-item.clickable:hover:not(.has-default){background-color:#f1f5f9}.field-item.clickable:hover:not(.has-default):after{content:\"Click to set default\";position:absolute;right:32px;font-size:11px;color:#64748b;font-style:italic}.field-item.clickable.has-default:hover:after{content:\"Click to edit\";position:absolute;right:32px;font-size:11px;color:#3b82f6;font-style:italic}.default-indicator{display:flex;align-items:center;gap:4px;color:#3b82f6;font-size:12px;background:#dbeafe;padding:2px 8px;border-radius:4px;max-width:100px;overflow:hidden}.default-indicator mat-icon{font-size:14px;width:14px;height:14px}.default-indicator .default-value{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] });
|
|
991
1371
|
}
|
|
992
1372
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SchemaTreeComponent, decorators: [{
|
|
993
1373
|
type: Component,
|
|
994
|
-
args: [{ selector: 'schema-tree', standalone: true, imports: [CommonModule, MatIconModule, MatTooltipModule], template: "<div class=\"schema-tree\" [class.source]=\"side === 'source'\" [class.target]=\"side === 'target'\">\n <div class=\"schema-header\">\n <span class=\"schema-title\">{{ schema.name }}</span>\n <span class=\"schema-badge\">{{ side === 'source' ? 'Source' : 'Target' }}</span>\n </div>\n\n <div class=\"schema-fields\" #schemaFields>\n <ng-container *ngTemplateOutlet=\"fieldList; context: { fields: schema.fields, level: 0 }\"></ng-container>\n </div>\n</div>\n\n<ng-template #fieldList let-fields=\"fields\" let-level=\"level\">\n @for (field of fields; track trackByFieldId($index, field)) {\n <div\n #fieldItem\n class=\"field-item\"\n [class.mapped]=\"isFieldMapped(field)\"\n [class.has-default]=\"hasDefaultValue(field)\"\n [class.has-children]=\"field.children && field.children.length > 0\"\n [class.expanded]=\"field.expanded\"\n [class.is-array]=\"field.type === 'array'\"\n [class.draggable]=\"side === 'source' && ((!field.children || field.children.length === 0) || field.type === 'array')\"\n [class.droppable]=\"side === 'target' && ((!field.children || field.children.length === 0) || field.type === 'array' || field.type === 'object')\"\n [class.clickable]=\"side === 'target' && (!isFieldMapped(field) || hasDefaultValue(field)) && field.type !== 'object' && field.type !== 'array'\"\n [style.padding-left.px]=\"16 + level * 20\"\n [attr.data-field-id]=\"field.id\"\n (mousedown)=\"onDragStart($event, field)\"\n (mouseup)=\"onDrop($event, field)\"\n (click)=\"onFieldClick($event, field)\"\n >\n <!-- Expand/Collapse button for nested objects -->\n @if (field.children && field.children.length > 0) {\n <button class=\"expand-btn\" (click)=\"toggleExpand(field, $event)\">\n <mat-icon>{{ field.expanded ? 'expand_more' : 'chevron_right' }}</mat-icon>\n </button>\n } @else {\n <span class=\"expand-placeholder\"></span>\n }\n\n <!-- Field type icon -->\n <mat-icon class=\"type-icon\" [matTooltip]=\"field.type\">{{ getTypeIcon(field.type) }}</mat-icon>\n\n <!-- Field name with array indicator -->\n <span class=\"field-name\">{{ field.name }}@if (field.type === 'array') {<span class=\"array-indicator\">[]</span>}</span>\n\n <!-- Mapping indicator -->\n @if (isFieldMapped(field)) {\n <span class=\"mapping-indicator\" [matTooltip]=\"getFieldMappingCount(field) + ' mapping(s)'\">\n <mat-icon>{{ field.type === 'array' ? 'loop' : 'link' }}</mat-icon>\n @if (getFieldMappingCount(field) > 1) {\n <span class=\"mapping-count\">{{ getFieldMappingCount(field) }}</span>\n }\n </span>\n }\n\n <!-- Default value indicator -->\n @if (hasDefaultValue(field)) {\n <span class=\"default-indicator\" [matTooltip]=\"'Default: ' + getDefaultValueDisplay(field)\">\n <mat-icon>edit</mat-icon>\n <span class=\"default-value\">{{ getDefaultValueDisplay(field) }}</span>\n </span>\n }\n\n <!-- Connection point - show for leaf nodes, arrays, and objects (on target for array-to-object) -->\n @if ((!field.children || field.children.length === 0) || field.type === 'array' || (side === 'target' && field.type === 'object')) {\n <div class=\"connection-point\" [class.source]=\"side === 'source'\" [class.target]=\"side === 'target'\" [class.array-point]=\"field.type === 'array'\" [class.object-point]=\"field.type === 'object'\">\n <span class=\"point-dot\"></span>\n </div>\n }\n </div>\n\n <!-- Nested children -->\n @if (field.children && field.children.length > 0 && field.expanded) {\n <div class=\"nested-fields\">\n <ng-container *ngTemplateOutlet=\"fieldList; context: { fields: field.children, level: level + 1 }\"></ng-container>\n </div>\n }\n }\n</ng-template>\n", styles: [":host{display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden}.schema-tree{background:var(--surface-card, #ffffff);border-radius:12px;box-shadow:0 2px 8px
|
|
1374
|
+
args: [{ selector: 'schema-tree', standalone: true, imports: [CommonModule, MatIconModule, MatTooltipModule], template: "<div class=\"schema-tree\" [class.source]=\"side === 'source'\" [class.target]=\"side === 'target'\">\n <div class=\"schema-header\">\n <span class=\"schema-title\">{{ schema.name }}</span>\n <span class=\"schema-badge\">{{ side === 'source' ? 'Source' : 'Target' }}</span>\n </div>\n\n <div class=\"schema-fields\" #schemaFields>\n <ng-container *ngTemplateOutlet=\"fieldList; context: { fields: schema.fields, level: 0 }\"></ng-container>\n </div>\n</div>\n\n<ng-template #fieldList let-fields=\"fields\" let-level=\"level\">\n @for (field of fields; track trackByFieldId($index, field)) {\n <div\n #fieldItem\n class=\"field-item\"\n [class.mapped]=\"isFieldMapped(field)\"\n [class.has-default]=\"hasDefaultValue(field)\"\n [class.has-children]=\"field.children && field.children.length > 0\"\n [class.expanded]=\"field.expanded\"\n [class.is-array]=\"field.type === 'array'\"\n [class.draggable]=\"side === 'source' && ((!field.children || field.children.length === 0) || field.type === 'array') && !isEndpointDragMode()\"\n [class.droppable]=\"(side === 'target' && ((!field.children || field.children.length === 0) || field.type === 'array' || field.type === 'object')) || (side === 'source' && isSourceEndpointDragging() && ((!field.children || field.children.length === 0) || field.type === 'array'))\"\n [class.endpoint-drop-target]=\"(side === 'source' && isSourceEndpointDragging()) || (side === 'target' && isTargetEndpointDragging())\"\n [class.clickable]=\"side === 'target' && (!isFieldMapped(field) || hasDefaultValue(field)) && field.type !== 'object' && field.type !== 'array'\"\n [style.padding-left.px]=\"16 + level * 20\"\n [attr.data-field-id]=\"field.id\"\n (mousedown)=\"onDragStart($event, field)\"\n (mouseup)=\"onDrop($event, field)\"\n (click)=\"onFieldClick($event, field)\"\n >\n <!-- Expand/Collapse button for nested objects -->\n @if (field.children && field.children.length > 0) {\n <button class=\"expand-btn\" (click)=\"toggleExpand(field, $event)\">\n <mat-icon>{{ field.expanded ? 'expand_more' : 'chevron_right' }}</mat-icon>\n </button>\n } @else {\n <span class=\"expand-placeholder\"></span>\n }\n\n <!-- Field type icon -->\n <mat-icon class=\"type-icon\" [matTooltip]=\"field.type\">{{ getTypeIcon(field.type) }}</mat-icon>\n\n <!-- Field name with array indicator -->\n <span class=\"field-name\">{{ field.name }}@if (field.type === 'array') {<span class=\"array-indicator\">[]</span>}</span>\n\n <!-- Mapping indicator -->\n @if (isFieldMapped(field)) {\n <span class=\"mapping-indicator\" [matTooltip]=\"getFieldMappingCount(field) + ' mapping(s)'\">\n <mat-icon>{{ field.type === 'array' ? 'loop' : 'link' }}</mat-icon>\n @if (getFieldMappingCount(field) > 1) {\n <span class=\"mapping-count\">{{ getFieldMappingCount(field) }}</span>\n }\n </span>\n }\n\n <!-- Default value indicator -->\n @if (hasDefaultValue(field)) {\n <span class=\"default-indicator\" [matTooltip]=\"'Default: ' + getDefaultValueDisplay(field)\">\n <mat-icon>edit</mat-icon>\n <span class=\"default-value\">{{ getDefaultValueDisplay(field) }}</span>\n </span>\n }\n\n <!-- Connection point - show for leaf nodes, arrays, and objects (on target for array-to-object) -->\n @if ((!field.children || field.children.length === 0) || field.type === 'array' || (side === 'target' && field.type === 'object')) {\n <div class=\"connection-point\" [class.source]=\"side === 'source'\" [class.target]=\"side === 'target'\" [class.array-point]=\"field.type === 'array'\" [class.object-point]=\"field.type === 'object'\">\n <span class=\"point-dot\"></span>\n </div>\n }\n </div>\n\n <!-- Nested children -->\n @if (field.children && field.children.length > 0 && field.expanded) {\n <div class=\"nested-fields\">\n <ng-container *ngTemplateOutlet=\"fieldList; context: { fields: field.children, level: level + 1 }\"></ng-container>\n </div>\n }\n }\n</ng-template>\n", styles: [":host{display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden}.schema-tree{background:var(--data-mapper-panel-bg, var(--surface-card, #ffffff));border-radius:var(--data-mapper-panel-border-radius, 12px);border:var(--data-mapper-panel-border, none);box-shadow:var(--data-mapper-panel-shadow, 0 2px 8px rgba(0, 0, 0, .08));overflow:hidden;height:100%;min-height:0;display:flex;flex-direction:column;flex:1}.schema-tree.source .schema-header{background:linear-gradient(135deg,#6366f1,#8b5cf6)}.schema-tree.source .connection-point{right:8px}.schema-tree.target .schema-header{background:linear-gradient(135deg,#10b981,#059669)}.schema-tree.target .connection-point{left:8px}.schema-header{padding:16px 20px;color:#fff;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-shrink:0}.schema-title{font-size:16px;font-weight:600;letter-spacing:.3px}.schema-badge{font-size:11px;font-weight:500;text-transform:uppercase;letter-spacing:.5px;background:#fff3;padding:4px 10px;border-radius:20px}.schema-fields{flex:1;overflow-y:auto;overflow-x:hidden;padding:8px 0;min-height:0}.field-item{display:flex;align-items:center;padding:10px 16px;gap:8px;cursor:default;transition:background-color .15s ease;position:relative;-webkit-user-select:none;user-select:none}.field-item:hover{background-color:var(--surface-hover, #f8fafc)}.field-item.mapped{background-color:var(--surface-mapped, #f0fdf4)}.field-item.mapped:hover{background-color:#dcfce7}.field-item.draggable{cursor:grab}.field-item.draggable:active{cursor:grabbing}.field-item.draggable:hover .connection-point .point-dot{transform:scale(1.3);background:#6366f1}.field-item.droppable{cursor:pointer}.field-item.droppable:hover .connection-point .point-dot{transform:scale(1.3);background:#10b981}.field-item.endpoint-drop-target{background-color:#fef3c7}.field-item.endpoint-drop-target:hover{background-color:#fde68a}.field-item.endpoint-drop-target .connection-point .point-dot{animation:pulse-drop-target 1s ease-in-out infinite}@keyframes pulse-drop-target{0%,to{transform:scale(1);box-shadow:0 0 0 3px #f59e0b4d}50%{transform:scale(1.2);box-shadow:0 0 0 6px #f59e0b33}}.expand-btn{background:none;border:none;padding:0;width:24px;height:24px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:#94a3b8;border-radius:4px;transition:all .15s ease}.expand-btn:hover{background-color:#e2e8f0;color:#475569}.expand-btn mat-icon{font-size:20px;width:20px;height:20px}.expand-placeholder{width:24px;height:24px;flex-shrink:0}.type-icon{font-size:18px;width:18px;height:18px;color:#64748b;flex-shrink:0}.field-name{flex:1;font-size:14px;color:#1e293b;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.field-name .array-indicator{color:#f59e0b;font-weight:600;margin-left:2px}.field-item.is-array{background-color:#fffbeb}.field-item.is-array:hover,.field-item.is-array.mapped{background-color:#fef3c7}.field-item.is-array .type-icon{color:#f59e0b}.connection-point.array-point .point-dot{background:#f59e0b;box-shadow:0 0 0 3px #f59e0b4d}.connection-point.object-point .point-dot{background:#8b5cf6;box-shadow:0 0 0 3px #8b5cf64d}.mapping-indicator{display:flex;align-items:center;gap:2px;color:#10b981}.mapping-indicator mat-icon{font-size:16px;width:16px;height:16px}.mapping-indicator .mapping-count{font-size:11px;font-weight:600;background:#10b981;color:#fff;padding:1px 5px;border-radius:10px;min-width:16px;text-align:center}.connection-point{position:absolute;top:50%;transform:translateY(-50%);width:20px;height:20px;display:flex;align-items:center;justify-content:center}.connection-point.source{right:8px}.connection-point.target{left:8px}.point-dot{width:10px;height:10px;border-radius:50%;background:#cbd5e1;transition:all .2s ease;box-shadow:0 0 0 3px #cbd5e14d}.mapped .point-dot{background:#10b981;box-shadow:0 0 0 3px #10b9814d}.nested-fields{border-left:2px solid #e2e8f0;margin-left:28px}.field-item.has-default{background-color:#eff6ff}.field-item.has-default:hover{background-color:#dbeafe}.field-item.clickable{cursor:pointer}.field-item.clickable:hover:not(.has-default){background-color:#f1f5f9}.field-item.clickable:hover:not(.has-default):after{content:\"Click to set default\";position:absolute;right:32px;font-size:11px;color:#64748b;font-style:italic}.field-item.clickable.has-default:hover:after{content:\"Click to edit\";position:absolute;right:32px;font-size:11px;color:#3b82f6;font-style:italic}.default-indicator{display:flex;align-items:center;gap:4px;color:#3b82f6;font-size:12px;background:#dbeafe;padding:2px 8px;border-radius:4px;max-width:100px;overflow:hidden}.default-indicator mat-icon{font-size:14px;width:14px;height:14px}.default-indicator .default-value{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}\n"] }]
|
|
995
1375
|
}], propDecorators: { schema: [{
|
|
996
1376
|
type: Input
|
|
997
1377
|
}], side: [{
|
|
@@ -1006,6 +1386,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
1006
1386
|
type: Output
|
|
1007
1387
|
}], fieldDrop: [{
|
|
1008
1388
|
type: Output
|
|
1389
|
+
}], sourceDrop: [{
|
|
1390
|
+
type: Output
|
|
1009
1391
|
}], fieldPositionsChanged: [{
|
|
1010
1392
|
type: Output
|
|
1011
1393
|
}], fieldDefaultValueClick: [{
|
|
@@ -1018,6 +1400,215 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
1018
1400
|
args: ['fieldItem']
|
|
1019
1401
|
}] } });
|
|
1020
1402
|
|
|
1403
|
+
class ConditionBuilderComponent {
|
|
1404
|
+
// Available fields for conditions (optional - if not provided, uses 'value' as the only field)
|
|
1405
|
+
fields = [];
|
|
1406
|
+
// The condition group to edit
|
|
1407
|
+
condition = null;
|
|
1408
|
+
// Whether to show a compact/inline version
|
|
1409
|
+
compact = false;
|
|
1410
|
+
// Emits when condition changes
|
|
1411
|
+
conditionChange = new EventEmitter();
|
|
1412
|
+
rootGroup;
|
|
1413
|
+
// Default field for transformation conditions (the input value)
|
|
1414
|
+
defaultField = { path: 'value', name: 'Input value', type: 'string' };
|
|
1415
|
+
// Operators by type
|
|
1416
|
+
stringOperators = [
|
|
1417
|
+
{ value: 'equals', label: 'equals', needsValue: true },
|
|
1418
|
+
{ value: 'notEquals', label: 'not equals', needsValue: true },
|
|
1419
|
+
{ value: 'contains', label: 'contains', needsValue: true },
|
|
1420
|
+
{ value: 'notContains', label: 'not contain', needsValue: true },
|
|
1421
|
+
{ value: 'startsWith', label: 'starts with', needsValue: true },
|
|
1422
|
+
{ value: 'endsWith', label: 'ends with', needsValue: true },
|
|
1423
|
+
{ value: 'isEmpty', label: 'is empty', needsValue: false },
|
|
1424
|
+
{ value: 'isNotEmpty', label: 'is not empty', needsValue: false },
|
|
1425
|
+
];
|
|
1426
|
+
numberOperators = [
|
|
1427
|
+
{ value: 'equals', label: 'equals', needsValue: true },
|
|
1428
|
+
{ value: 'notEquals', label: 'not equals', needsValue: true },
|
|
1429
|
+
{ value: 'greaterThan', label: '>', needsValue: true },
|
|
1430
|
+
{ value: 'lessThan', label: '<', needsValue: true },
|
|
1431
|
+
{ value: 'greaterThanOrEqual', label: '>=', needsValue: true },
|
|
1432
|
+
{ value: 'lessThanOrEqual', label: '<=', needsValue: true },
|
|
1433
|
+
];
|
|
1434
|
+
booleanOperators = [
|
|
1435
|
+
{ value: 'isTrue', label: 'is true', needsValue: false },
|
|
1436
|
+
{ value: 'isFalse', label: 'is false', needsValue: false },
|
|
1437
|
+
];
|
|
1438
|
+
ngOnInit() {
|
|
1439
|
+
if (this.condition) {
|
|
1440
|
+
this.rootGroup = this.cloneGroup(this.condition);
|
|
1441
|
+
}
|
|
1442
|
+
else {
|
|
1443
|
+
this.rootGroup = this.createEmptyGroup();
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
get availableFields() {
|
|
1447
|
+
return this.fields.length > 0 ? this.fields : [this.defaultField];
|
|
1448
|
+
}
|
|
1449
|
+
get showFieldSelector() {
|
|
1450
|
+
return this.fields.length > 1;
|
|
1451
|
+
}
|
|
1452
|
+
createEmptyGroup() {
|
|
1453
|
+
return {
|
|
1454
|
+
id: this.generateId(),
|
|
1455
|
+
type: 'group',
|
|
1456
|
+
logic: 'and',
|
|
1457
|
+
children: [],
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
generateId() {
|
|
1461
|
+
return `cond-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
1462
|
+
}
|
|
1463
|
+
cloneGroup(group) {
|
|
1464
|
+
return {
|
|
1465
|
+
...group,
|
|
1466
|
+
children: group.children.map((child) => child.type === 'group' ? this.cloneGroup(child) : { ...child }),
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
getOperatorsForField(fieldPath) {
|
|
1470
|
+
const field = this.availableFields.find((f) => f.path === fieldPath);
|
|
1471
|
+
if (!field)
|
|
1472
|
+
return this.stringOperators;
|
|
1473
|
+
switch (field.type) {
|
|
1474
|
+
case 'number':
|
|
1475
|
+
return this.numberOperators;
|
|
1476
|
+
case 'boolean':
|
|
1477
|
+
return this.booleanOperators;
|
|
1478
|
+
default:
|
|
1479
|
+
return this.stringOperators;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
operatorNeedsValue(operator) {
|
|
1483
|
+
const allOperators = [...this.stringOperators, ...this.numberOperators, ...this.booleanOperators];
|
|
1484
|
+
const op = allOperators.find((o) => o.value === operator);
|
|
1485
|
+
return op?.needsValue ?? true;
|
|
1486
|
+
}
|
|
1487
|
+
isCondition(item) {
|
|
1488
|
+
return item.type === 'condition';
|
|
1489
|
+
}
|
|
1490
|
+
isGroup(item) {
|
|
1491
|
+
return item.type === 'group';
|
|
1492
|
+
}
|
|
1493
|
+
addCondition(group) {
|
|
1494
|
+
const firstField = this.availableFields[0];
|
|
1495
|
+
const newCondition = {
|
|
1496
|
+
id: this.generateId(),
|
|
1497
|
+
type: 'condition',
|
|
1498
|
+
field: firstField.path,
|
|
1499
|
+
fieldName: firstField.name,
|
|
1500
|
+
operator: 'isNotEmpty',
|
|
1501
|
+
value: '',
|
|
1502
|
+
valueType: firstField.type,
|
|
1503
|
+
};
|
|
1504
|
+
group.children = [...group.children, newCondition];
|
|
1505
|
+
this.emitChange();
|
|
1506
|
+
}
|
|
1507
|
+
addGroup(parentGroup) {
|
|
1508
|
+
const newGroup = {
|
|
1509
|
+
id: this.generateId(),
|
|
1510
|
+
type: 'group',
|
|
1511
|
+
logic: parentGroup.logic === 'and' ? 'or' : 'and',
|
|
1512
|
+
children: [],
|
|
1513
|
+
};
|
|
1514
|
+
parentGroup.children = [...parentGroup.children, newGroup];
|
|
1515
|
+
this.emitChange();
|
|
1516
|
+
}
|
|
1517
|
+
removeItem(parentGroup, itemId) {
|
|
1518
|
+
parentGroup.children = parentGroup.children.filter((c) => c.id !== itemId);
|
|
1519
|
+
this.emitChange();
|
|
1520
|
+
}
|
|
1521
|
+
onFieldChange(condition, fieldPath) {
|
|
1522
|
+
const field = this.availableFields.find((f) => f.path === fieldPath);
|
|
1523
|
+
if (field) {
|
|
1524
|
+
condition.field = fieldPath;
|
|
1525
|
+
condition.fieldName = field.name;
|
|
1526
|
+
condition.valueType = field.type;
|
|
1527
|
+
const operators = this.getOperatorsForField(fieldPath);
|
|
1528
|
+
condition.operator = operators[0].value;
|
|
1529
|
+
condition.value = '';
|
|
1530
|
+
}
|
|
1531
|
+
this.emitChange();
|
|
1532
|
+
}
|
|
1533
|
+
onOperatorChange(condition, operator) {
|
|
1534
|
+
condition.operator = operator;
|
|
1535
|
+
if (!this.operatorNeedsValue(operator)) {
|
|
1536
|
+
condition.value = '';
|
|
1537
|
+
}
|
|
1538
|
+
this.emitChange();
|
|
1539
|
+
}
|
|
1540
|
+
onValueChange(condition, value) {
|
|
1541
|
+
if (condition.valueType === 'number') {
|
|
1542
|
+
condition.value = parseFloat(value) || 0;
|
|
1543
|
+
}
|
|
1544
|
+
else {
|
|
1545
|
+
condition.value = value;
|
|
1546
|
+
}
|
|
1547
|
+
this.emitChange();
|
|
1548
|
+
}
|
|
1549
|
+
onLogicChange(group, logic) {
|
|
1550
|
+
group.logic = logic;
|
|
1551
|
+
this.emitChange();
|
|
1552
|
+
}
|
|
1553
|
+
emitChange() {
|
|
1554
|
+
this.rootGroup = this.cloneGroup(this.rootGroup);
|
|
1555
|
+
this.conditionChange.emit(this.rootGroup);
|
|
1556
|
+
}
|
|
1557
|
+
getConditionSummary() {
|
|
1558
|
+
return this.summarizeGroup(this.rootGroup);
|
|
1559
|
+
}
|
|
1560
|
+
summarizeGroup(group) {
|
|
1561
|
+
if (group.children.length === 0)
|
|
1562
|
+
return '';
|
|
1563
|
+
const parts = group.children.map(child => {
|
|
1564
|
+
if (child.type === 'condition') {
|
|
1565
|
+
return this.summarizeCondition(child);
|
|
1566
|
+
}
|
|
1567
|
+
else {
|
|
1568
|
+
return `(${this.summarizeGroup(child)})`;
|
|
1569
|
+
}
|
|
1570
|
+
});
|
|
1571
|
+
return parts.join(` ${group.logic.toUpperCase()} `);
|
|
1572
|
+
}
|
|
1573
|
+
summarizeCondition(cond) {
|
|
1574
|
+
const opLabel = this.getOperatorLabel(cond.operator);
|
|
1575
|
+
if (this.operatorNeedsValue(cond.operator)) {
|
|
1576
|
+
return `${opLabel} "${cond.value}"`;
|
|
1577
|
+
}
|
|
1578
|
+
return opLabel;
|
|
1579
|
+
}
|
|
1580
|
+
getOperatorLabel(operator) {
|
|
1581
|
+
const allOperators = [...this.stringOperators, ...this.numberOperators, ...this.booleanOperators];
|
|
1582
|
+
const op = allOperators.find((o) => o.value === operator);
|
|
1583
|
+
return op?.label || operator;
|
|
1584
|
+
}
|
|
1585
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ConditionBuilderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1586
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: ConditionBuilderComponent, isStandalone: true, selector: "condition-builder", inputs: { fields: "fields", condition: "condition", compact: "compact" }, outputs: { conditionChange: "conditionChange" }, ngImport: i0, template: "<div class=\"condition-builder\" [class.compact]=\"compact\">\r\n <ng-container *ngTemplateOutlet=\"groupTemplate; context: { group: rootGroup, isRoot: true }\"></ng-container>\r\n</div>\r\n\r\n<!-- Recursive group template -->\r\n<ng-template #groupTemplate let-group=\"group\" let-isRoot=\"isRoot\" let-parentGroup=\"parentGroup\">\r\n <div class=\"filter-group\" [class.root-group]=\"isRoot\" [class.nested-group]=\"!isRoot\">\r\n <!-- Group header with logic toggle (only show if multiple children or nested) -->\r\n @if (group.children.length > 1 || !isRoot) {\r\n <div class=\"group-header\">\r\n <div class=\"logic-toggle\">\r\n <button\r\n type=\"button\"\r\n class=\"logic-btn\"\r\n [class.active]=\"group.logic === 'and'\"\r\n (click)=\"onLogicChange(group, 'and')\"\r\n >\r\n AND\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"logic-btn\"\r\n [class.active]=\"group.logic === 'or'\"\r\n (click)=\"onLogicChange(group, 'or')\"\r\n >\r\n OR\r\n </button>\r\n </div>\r\n @if (!isRoot) {\r\n <button mat-icon-button class=\"remove-group-btn\" matTooltip=\"Remove group\" (click)=\"removeItem(parentGroup, group.id)\">\r\n <mat-icon>close</mat-icon>\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Group children -->\r\n <div class=\"group-children\">\r\n @for (item of group.children; track item.id; let i = $index) {\r\n <!-- Logic connector between items -->\r\n @if (i > 0) {\r\n <div class=\"logic-connector\">\r\n <span class=\"logic-badge\" [class.and]=\"group.logic === 'and'\" [class.or]=\"group.logic === 'or'\">\r\n {{ group.logic | uppercase }}\r\n </span>\r\n </div>\r\n }\r\n\r\n @if (isCondition(item)) {\r\n <!-- Condition row -->\r\n <div class=\"condition-row\">\r\n <!-- Field selector (only if multiple fields) -->\r\n @if (showFieldSelector) {\r\n <mat-form-field appearance=\"outline\" class=\"field-select\">\r\n <mat-select [value]=\"item.field\" (selectionChange)=\"onFieldChange(item, $event.value)\">\r\n @for (field of availableFields; track field.path) {\r\n <mat-option [value]=\"field.path\">{{ field.name }}</mat-option>\r\n }\r\n </mat-select>\r\n </mat-form-field>\r\n }\r\n\r\n <!-- Operator selector -->\r\n <mat-form-field appearance=\"outline\" class=\"operator-select\">\r\n <mat-select [value]=\"item.operator\" (selectionChange)=\"onOperatorChange(item, $event.value)\">\r\n @for (op of getOperatorsForField(item.field); track op.value) {\r\n <mat-option [value]=\"op.value\">{{ op.label }}</mat-option>\r\n }\r\n </mat-select>\r\n </mat-form-field>\r\n\r\n <!-- Value input (only if operator needs value) -->\r\n @if (operatorNeedsValue(item.operator)) {\r\n @if (item.valueType === 'boolean') {\r\n <mat-slide-toggle\r\n [checked]=\"item.value === true\"\r\n (change)=\"onValueChange(item, $event.checked)\"\r\n class=\"bool-toggle\"\r\n >\r\n </mat-slide-toggle>\r\n } @else if (item.valueType === 'number') {\r\n <mat-form-field appearance=\"outline\" class=\"value-input\">\r\n <input\r\n matInput\r\n type=\"number\"\r\n [value]=\"item.value\"\r\n (input)=\"onValueChange(item, $any($event.target).value)\"\r\n placeholder=\"value\"\r\n />\r\n </mat-form-field>\r\n } @else {\r\n <mat-form-field appearance=\"outline\" class=\"value-input\">\r\n <input\r\n matInput\r\n type=\"text\"\r\n [value]=\"item.value\"\r\n (input)=\"onValueChange(item, $any($event.target).value)\"\r\n placeholder=\"value\"\r\n />\r\n </mat-form-field>\r\n }\r\n }\r\n\r\n <!-- Remove condition button -->\r\n <button mat-icon-button class=\"remove-btn\" matTooltip=\"Remove\" (click)=\"removeItem(group, item.id)\">\r\n <mat-icon>close</mat-icon>\r\n </button>\r\n </div>\r\n } @else if (isGroup(item)) {\r\n <!-- Nested group -->\r\n <ng-container *ngTemplateOutlet=\"groupTemplate; context: { group: item, isRoot: false, parentGroup: group }\"></ng-container>\r\n }\r\n }\r\n\r\n <!-- Empty state -->\r\n @if (group.children.length === 0) {\r\n <div class=\"empty-group\">\r\n <span>No conditions defined</span>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Group actions -->\r\n <div class=\"group-actions\">\r\n <button type=\"button\" class=\"add-btn\" (click)=\"addCondition(group)\">\r\n <mat-icon>add</mat-icon>\r\n @if (!compact) {\r\n <span>Add condition</span>\r\n }\r\n </button>\r\n @if (!compact && group.children.length > 0) {\r\n <button type=\"button\" class=\"add-btn add-group-btn\" (click)=\"addGroup(group)\">\r\n <mat-icon>folder_open</mat-icon>\r\n <span>Add group</span>\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n</ng-template>\r\n", styles: [".condition-builder.compact .filter-group{padding:8px}.condition-builder.compact .condition-row{gap:6px}.condition-builder.compact .operator-select{min-width:100px}.condition-builder.compact .value-input{min-width:80px}.condition-builder.compact .add-btn{padding:4px 8px;font-size:12px}.filter-group{background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:12px}.filter-group.nested-group{margin-top:8px;background:#f1f5f9;border-color:#cbd5e1}.group-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.logic-toggle{display:flex;gap:4px}.logic-btn{padding:4px 12px;border:1px solid #cbd5e1;background:#fff;border-radius:4px;font-size:11px;font-weight:600;color:#64748b;cursor:pointer;transition:all .15s ease}.logic-btn:hover{border-color:#94a3b8}.logic-btn.active{background:#6366f1;border-color:#6366f1;color:#fff}.group-children{display:flex;flex-direction:column;gap:8px}.logic-connector{display:flex;align-items:center;justify-content:center;padding:4px 0}.logic-badge{font-size:10px;font-weight:700;padding:2px 8px;border-radius:4px}.logic-badge.and{background:#dbeafe;color:#1d4ed8}.logic-badge.or{background:#fef3c7;color:#b45309}.condition-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.field-select{min-width:120px}.operator-select{min-width:130px}.value-input{min-width:100px;flex:1}.bool-toggle{margin:0 8px}.remove-btn,.remove-group-btn{width:28px;height:28px;line-height:28px;flex-shrink:0}.remove-btn mat-icon,.remove-group-btn mat-icon{font-size:18px;width:18px;height:18px;color:#94a3b8}.remove-btn:hover mat-icon,.remove-group-btn:hover mat-icon{color:#ef4444}.empty-group{display:flex;align-items:center;justify-content:center;padding:12px;color:#94a3b8;font-size:13px;font-style:italic}.group-actions{display:flex;gap:8px;margin-top:8px;padding-top:8px;border-top:1px dashed #e2e8f0}.add-btn{display:flex;align-items:center;gap:4px;padding:6px 12px;background:#fff;border:1px dashed #cbd5e1;border-radius:6px;font-size:13px;color:#64748b;cursor:pointer;transition:all .15s ease}.add-btn mat-icon{font-size:16px;width:16px;height:16px}.add-btn:hover{border-color:#6366f1;color:#6366f1;background:#f5f3ff}.add-btn.add-group-btn{border-style:solid}::ng-deep .condition-builder .mat-mdc-form-field{font-size:13px}::ng-deep .condition-builder .mat-mdc-form-field-subscript-wrapper{display:none}::ng-deep .condition-builder .mdc-text-field--outlined{--mdc-outlined-text-field-container-shape: 6px}::ng-deep .condition-builder .mat-mdc-form-field-infix{padding-top:8px!important;padding-bottom:8px!important;min-height:36px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "component", type: i4.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i4.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "ngmodule", type: MatRadioModule }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i7$1.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "pipe", type: i1.UpperCasePipe, name: "uppercase" }] });
|
|
1587
|
+
}
|
|
1588
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ConditionBuilderComponent, decorators: [{
|
|
1589
|
+
type: Component,
|
|
1590
|
+
args: [{ selector: 'condition-builder', standalone: true, imports: [
|
|
1591
|
+
CommonModule,
|
|
1592
|
+
FormsModule,
|
|
1593
|
+
MatButtonModule,
|
|
1594
|
+
MatIconModule,
|
|
1595
|
+
MatSelectModule,
|
|
1596
|
+
MatInputModule,
|
|
1597
|
+
MatFormFieldModule,
|
|
1598
|
+
MatRadioModule,
|
|
1599
|
+
MatSlideToggleModule,
|
|
1600
|
+
MatTooltipModule,
|
|
1601
|
+
], template: "<div class=\"condition-builder\" [class.compact]=\"compact\">\r\n <ng-container *ngTemplateOutlet=\"groupTemplate; context: { group: rootGroup, isRoot: true }\"></ng-container>\r\n</div>\r\n\r\n<!-- Recursive group template -->\r\n<ng-template #groupTemplate let-group=\"group\" let-isRoot=\"isRoot\" let-parentGroup=\"parentGroup\">\r\n <div class=\"filter-group\" [class.root-group]=\"isRoot\" [class.nested-group]=\"!isRoot\">\r\n <!-- Group header with logic toggle (only show if multiple children or nested) -->\r\n @if (group.children.length > 1 || !isRoot) {\r\n <div class=\"group-header\">\r\n <div class=\"logic-toggle\">\r\n <button\r\n type=\"button\"\r\n class=\"logic-btn\"\r\n [class.active]=\"group.logic === 'and'\"\r\n (click)=\"onLogicChange(group, 'and')\"\r\n >\r\n AND\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"logic-btn\"\r\n [class.active]=\"group.logic === 'or'\"\r\n (click)=\"onLogicChange(group, 'or')\"\r\n >\r\n OR\r\n </button>\r\n </div>\r\n @if (!isRoot) {\r\n <button mat-icon-button class=\"remove-group-btn\" matTooltip=\"Remove group\" (click)=\"removeItem(parentGroup, group.id)\">\r\n <mat-icon>close</mat-icon>\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Group children -->\r\n <div class=\"group-children\">\r\n @for (item of group.children; track item.id; let i = $index) {\r\n <!-- Logic connector between items -->\r\n @if (i > 0) {\r\n <div class=\"logic-connector\">\r\n <span class=\"logic-badge\" [class.and]=\"group.logic === 'and'\" [class.or]=\"group.logic === 'or'\">\r\n {{ group.logic | uppercase }}\r\n </span>\r\n </div>\r\n }\r\n\r\n @if (isCondition(item)) {\r\n <!-- Condition row -->\r\n <div class=\"condition-row\">\r\n <!-- Field selector (only if multiple fields) -->\r\n @if (showFieldSelector) {\r\n <mat-form-field appearance=\"outline\" class=\"field-select\">\r\n <mat-select [value]=\"item.field\" (selectionChange)=\"onFieldChange(item, $event.value)\">\r\n @for (field of availableFields; track field.path) {\r\n <mat-option [value]=\"field.path\">{{ field.name }}</mat-option>\r\n }\r\n </mat-select>\r\n </mat-form-field>\r\n }\r\n\r\n <!-- Operator selector -->\r\n <mat-form-field appearance=\"outline\" class=\"operator-select\">\r\n <mat-select [value]=\"item.operator\" (selectionChange)=\"onOperatorChange(item, $event.value)\">\r\n @for (op of getOperatorsForField(item.field); track op.value) {\r\n <mat-option [value]=\"op.value\">{{ op.label }}</mat-option>\r\n }\r\n </mat-select>\r\n </mat-form-field>\r\n\r\n <!-- Value input (only if operator needs value) -->\r\n @if (operatorNeedsValue(item.operator)) {\r\n @if (item.valueType === 'boolean') {\r\n <mat-slide-toggle\r\n [checked]=\"item.value === true\"\r\n (change)=\"onValueChange(item, $event.checked)\"\r\n class=\"bool-toggle\"\r\n >\r\n </mat-slide-toggle>\r\n } @else if (item.valueType === 'number') {\r\n <mat-form-field appearance=\"outline\" class=\"value-input\">\r\n <input\r\n matInput\r\n type=\"number\"\r\n [value]=\"item.value\"\r\n (input)=\"onValueChange(item, $any($event.target).value)\"\r\n placeholder=\"value\"\r\n />\r\n </mat-form-field>\r\n } @else {\r\n <mat-form-field appearance=\"outline\" class=\"value-input\">\r\n <input\r\n matInput\r\n type=\"text\"\r\n [value]=\"item.value\"\r\n (input)=\"onValueChange(item, $any($event.target).value)\"\r\n placeholder=\"value\"\r\n />\r\n </mat-form-field>\r\n }\r\n }\r\n\r\n <!-- Remove condition button -->\r\n <button mat-icon-button class=\"remove-btn\" matTooltip=\"Remove\" (click)=\"removeItem(group, item.id)\">\r\n <mat-icon>close</mat-icon>\r\n </button>\r\n </div>\r\n } @else if (isGroup(item)) {\r\n <!-- Nested group -->\r\n <ng-container *ngTemplateOutlet=\"groupTemplate; context: { group: item, isRoot: false, parentGroup: group }\"></ng-container>\r\n }\r\n }\r\n\r\n <!-- Empty state -->\r\n @if (group.children.length === 0) {\r\n <div class=\"empty-group\">\r\n <span>No conditions defined</span>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Group actions -->\r\n <div class=\"group-actions\">\r\n <button type=\"button\" class=\"add-btn\" (click)=\"addCondition(group)\">\r\n <mat-icon>add</mat-icon>\r\n @if (!compact) {\r\n <span>Add condition</span>\r\n }\r\n </button>\r\n @if (!compact && group.children.length > 0) {\r\n <button type=\"button\" class=\"add-btn add-group-btn\" (click)=\"addGroup(group)\">\r\n <mat-icon>folder_open</mat-icon>\r\n <span>Add group</span>\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n</ng-template>\r\n", styles: [".condition-builder.compact .filter-group{padding:8px}.condition-builder.compact .condition-row{gap:6px}.condition-builder.compact .operator-select{min-width:100px}.condition-builder.compact .value-input{min-width:80px}.condition-builder.compact .add-btn{padding:4px 8px;font-size:12px}.filter-group{background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:12px}.filter-group.nested-group{margin-top:8px;background:#f1f5f9;border-color:#cbd5e1}.group-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.logic-toggle{display:flex;gap:4px}.logic-btn{padding:4px 12px;border:1px solid #cbd5e1;background:#fff;border-radius:4px;font-size:11px;font-weight:600;color:#64748b;cursor:pointer;transition:all .15s ease}.logic-btn:hover{border-color:#94a3b8}.logic-btn.active{background:#6366f1;border-color:#6366f1;color:#fff}.group-children{display:flex;flex-direction:column;gap:8px}.logic-connector{display:flex;align-items:center;justify-content:center;padding:4px 0}.logic-badge{font-size:10px;font-weight:700;padding:2px 8px;border-radius:4px}.logic-badge.and{background:#dbeafe;color:#1d4ed8}.logic-badge.or{background:#fef3c7;color:#b45309}.condition-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.field-select{min-width:120px}.operator-select{min-width:130px}.value-input{min-width:100px;flex:1}.bool-toggle{margin:0 8px}.remove-btn,.remove-group-btn{width:28px;height:28px;line-height:28px;flex-shrink:0}.remove-btn mat-icon,.remove-group-btn mat-icon{font-size:18px;width:18px;height:18px;color:#94a3b8}.remove-btn:hover mat-icon,.remove-group-btn:hover mat-icon{color:#ef4444}.empty-group{display:flex;align-items:center;justify-content:center;padding:12px;color:#94a3b8;font-size:13px;font-style:italic}.group-actions{display:flex;gap:8px;margin-top:8px;padding-top:8px;border-top:1px dashed #e2e8f0}.add-btn{display:flex;align-items:center;gap:4px;padding:6px 12px;background:#fff;border:1px dashed #cbd5e1;border-radius:6px;font-size:13px;color:#64748b;cursor:pointer;transition:all .15s ease}.add-btn mat-icon{font-size:16px;width:16px;height:16px}.add-btn:hover{border-color:#6366f1;color:#6366f1;background:#f5f3ff}.add-btn.add-group-btn{border-style:solid}::ng-deep .condition-builder .mat-mdc-form-field{font-size:13px}::ng-deep .condition-builder .mat-mdc-form-field-subscript-wrapper{display:none}::ng-deep .condition-builder .mdc-text-field--outlined{--mdc-outlined-text-field-container-shape: 6px}::ng-deep .condition-builder .mat-mdc-form-field-infix{padding-top:8px!important;padding-bottom:8px!important;min-height:36px}\n"] }]
|
|
1602
|
+
}], propDecorators: { fields: [{
|
|
1603
|
+
type: Input
|
|
1604
|
+
}], condition: [{
|
|
1605
|
+
type: Input
|
|
1606
|
+
}], compact: [{
|
|
1607
|
+
type: Input
|
|
1608
|
+
}], conditionChange: [{
|
|
1609
|
+
type: Output
|
|
1610
|
+
}] } });
|
|
1611
|
+
|
|
1021
1612
|
class TransformationPopoverComponent {
|
|
1022
1613
|
mapping;
|
|
1023
1614
|
position = { x: 0, y: 0 };
|
|
@@ -1026,9 +1617,15 @@ class TransformationPopoverComponent {
|
|
|
1026
1617
|
delete = new EventEmitter();
|
|
1027
1618
|
close = new EventEmitter();
|
|
1028
1619
|
transformationService = inject(TransformationService);
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1620
|
+
// Array of transformation steps
|
|
1621
|
+
steps = [];
|
|
1622
|
+
// Preview results for each step
|
|
1623
|
+
stepPreviews = [];
|
|
1624
|
+
// Input values for each step (to show input → output)
|
|
1625
|
+
stepInputs = [];
|
|
1626
|
+
finalPreview = '';
|
|
1627
|
+
// Track which step is expanded (-1 means none, used in single-step mode)
|
|
1628
|
+
expandedStepIndex = -1;
|
|
1032
1629
|
availableTransformations = this.transformationService.getAvailableTransformations();
|
|
1033
1630
|
ngOnInit() {
|
|
1034
1631
|
this.initFromMapping();
|
|
@@ -1040,54 +1637,156 @@ class TransformationPopoverComponent {
|
|
|
1040
1637
|
}
|
|
1041
1638
|
initFromMapping() {
|
|
1042
1639
|
if (this.mapping) {
|
|
1043
|
-
|
|
1044
|
-
this.
|
|
1640
|
+
// Copy transformations array, with fallback for empty/undefined
|
|
1641
|
+
if (this.mapping.transformations && this.mapping.transformations.length > 0) {
|
|
1642
|
+
this.steps = this.mapping.transformations.map(t => ({ ...t }));
|
|
1643
|
+
}
|
|
1644
|
+
else {
|
|
1645
|
+
// Fallback to a single direct transformation
|
|
1646
|
+
this.steps = [{ type: 'direct' }];
|
|
1647
|
+
}
|
|
1045
1648
|
this.updatePreview();
|
|
1046
1649
|
}
|
|
1047
1650
|
}
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1651
|
+
// Check if we're in multi-step mode (more than one step)
|
|
1652
|
+
get isMultiStep() {
|
|
1653
|
+
return this.steps.length > 1;
|
|
1654
|
+
}
|
|
1655
|
+
onStepTypeChange(index) {
|
|
1656
|
+
const step = this.steps[index];
|
|
1053
1657
|
// Set defaults based on type
|
|
1054
|
-
switch (
|
|
1658
|
+
switch (step.type) {
|
|
1055
1659
|
case 'concat':
|
|
1056
|
-
|
|
1057
|
-
|
|
1660
|
+
step.separator = step.separator ?? ' ';
|
|
1661
|
+
step.template = step.template ?? this.getDefaultTemplate();
|
|
1058
1662
|
break;
|
|
1059
1663
|
case 'substring':
|
|
1060
|
-
|
|
1061
|
-
|
|
1664
|
+
step.startIndex = step.startIndex ?? 0;
|
|
1665
|
+
step.endIndex = step.endIndex ?? 10;
|
|
1062
1666
|
break;
|
|
1063
1667
|
case 'replace':
|
|
1064
|
-
|
|
1065
|
-
|
|
1668
|
+
step.searchValue = step.searchValue ?? '';
|
|
1669
|
+
step.replaceValue = step.replaceValue ?? '';
|
|
1066
1670
|
break;
|
|
1067
1671
|
case 'dateFormat':
|
|
1068
|
-
|
|
1672
|
+
step.outputFormat = step.outputFormat ?? 'YYYY-MM-DD';
|
|
1069
1673
|
break;
|
|
1070
1674
|
case 'numberFormat':
|
|
1071
|
-
|
|
1675
|
+
step.decimalPlaces = step.decimalPlaces ?? 2;
|
|
1676
|
+
break;
|
|
1677
|
+
case 'mask':
|
|
1678
|
+
step.pattern = step.pattern ?? '(###) ###-####';
|
|
1072
1679
|
break;
|
|
1073
1680
|
}
|
|
1074
1681
|
this.updatePreview();
|
|
1075
1682
|
}
|
|
1076
1683
|
getDefaultTemplate() {
|
|
1077
|
-
return this.mapping.sourceFields.map((
|
|
1684
|
+
return this.mapping.sourceFields.map((_, i) => `{${i}}`).join(' ');
|
|
1685
|
+
}
|
|
1686
|
+
addStep() {
|
|
1687
|
+
this.steps.push({ type: 'direct' });
|
|
1688
|
+
// Expand the newly added step
|
|
1689
|
+
this.expandedStepIndex = this.steps.length - 1;
|
|
1690
|
+
this.updatePreview();
|
|
1691
|
+
}
|
|
1692
|
+
removeStep(index) {
|
|
1693
|
+
if (this.steps.length > 1) {
|
|
1694
|
+
this.steps.splice(index, 1);
|
|
1695
|
+
// Collapse after deletion
|
|
1696
|
+
this.expandedStepIndex = -1;
|
|
1697
|
+
this.updatePreview();
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
onStepDrop(event) {
|
|
1701
|
+
if (event.previousIndex !== event.currentIndex) {
|
|
1702
|
+
moveItemInArray(this.steps, event.previousIndex, event.currentIndex);
|
|
1703
|
+
// Move expanded index with the step
|
|
1704
|
+
if (this.expandedStepIndex === event.previousIndex) {
|
|
1705
|
+
this.expandedStepIndex = event.currentIndex;
|
|
1706
|
+
}
|
|
1707
|
+
else if (this.expandedStepIndex > event.previousIndex &&
|
|
1708
|
+
this.expandedStepIndex <= event.currentIndex) {
|
|
1709
|
+
this.expandedStepIndex--;
|
|
1710
|
+
}
|
|
1711
|
+
else if (this.expandedStepIndex < event.previousIndex &&
|
|
1712
|
+
this.expandedStepIndex >= event.currentIndex) {
|
|
1713
|
+
this.expandedStepIndex++;
|
|
1714
|
+
}
|
|
1715
|
+
this.updatePreview();
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
toggleStep(index) {
|
|
1719
|
+
if (this.expandedStepIndex === index) {
|
|
1720
|
+
this.expandedStepIndex = -1; // Collapse
|
|
1721
|
+
}
|
|
1722
|
+
else {
|
|
1723
|
+
this.expandedStepIndex = index; // Expand
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
isStepExpanded(index) {
|
|
1727
|
+
return this.expandedStepIndex === index;
|
|
1078
1728
|
}
|
|
1079
1729
|
updatePreview() {
|
|
1080
1730
|
if (!this.mapping || !this.sampleData) {
|
|
1081
|
-
this.
|
|
1731
|
+
this.stepPreviews = [];
|
|
1732
|
+
this.stepInputs = [];
|
|
1733
|
+
this.finalPreview = '';
|
|
1082
1734
|
return;
|
|
1083
1735
|
}
|
|
1084
|
-
this.
|
|
1736
|
+
this.stepPreviews = [];
|
|
1737
|
+
this.stepInputs = [];
|
|
1738
|
+
let currentValue = null;
|
|
1739
|
+
// Get initial input from source fields
|
|
1740
|
+
const initialValues = this.mapping.sourceFields
|
|
1741
|
+
.map(f => this.getValueByPath(this.sampleData, f.path))
|
|
1742
|
+
.map(v => String(v ?? ''));
|
|
1743
|
+
const initialInput = initialValues.join(', ');
|
|
1744
|
+
// For the first step, use the source fields from sample data
|
|
1745
|
+
for (let i = 0; i < this.steps.length; i++) {
|
|
1746
|
+
const step = this.steps[i];
|
|
1747
|
+
if (i === 0) {
|
|
1748
|
+
// First step: input is from source fields
|
|
1749
|
+
this.stepInputs.push(initialInput);
|
|
1750
|
+
// Check condition before applying transformation
|
|
1751
|
+
if (this.transformationService.isConditionMet(initialInput, step)) {
|
|
1752
|
+
currentValue = this.transformationService.applyTransformation(this.sampleData, this.mapping.sourceFields, step);
|
|
1753
|
+
}
|
|
1754
|
+
else {
|
|
1755
|
+
currentValue = initialInput; // Pass through if condition not met
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
else {
|
|
1759
|
+
// Subsequent steps: input is previous output
|
|
1760
|
+
this.stepInputs.push(this.stepPreviews[i - 1]);
|
|
1761
|
+
// Check condition before applying transformation
|
|
1762
|
+
if (this.transformationService.isConditionMet(currentValue, step)) {
|
|
1763
|
+
currentValue = this.transformationService.applyTransformationToValue(currentValue, step);
|
|
1764
|
+
}
|
|
1765
|
+
// If condition not met, currentValue passes through unchanged
|
|
1766
|
+
}
|
|
1767
|
+
this.stepPreviews.push(String(currentValue ?? ''));
|
|
1768
|
+
}
|
|
1769
|
+
this.finalPreview = this.stepPreviews[this.stepPreviews.length - 1] || '';
|
|
1770
|
+
}
|
|
1771
|
+
getValueByPath(obj, path) {
|
|
1772
|
+
return path.split('.').reduce((acc, part) => {
|
|
1773
|
+
if (acc && typeof acc === 'object') {
|
|
1774
|
+
return acc[part];
|
|
1775
|
+
}
|
|
1776
|
+
return undefined;
|
|
1777
|
+
}, obj);
|
|
1085
1778
|
}
|
|
1086
1779
|
onConfigChange() {
|
|
1087
1780
|
this.updatePreview();
|
|
1088
1781
|
}
|
|
1089
1782
|
onSave() {
|
|
1090
|
-
|
|
1783
|
+
// Filter out 'direct' transformations if they're not the only one
|
|
1784
|
+
const cleanedSteps = this.steps.length === 1
|
|
1785
|
+
? this.steps
|
|
1786
|
+
: this.steps.filter(s => s.type !== 'direct');
|
|
1787
|
+
// If all filtered out, keep at least one direct
|
|
1788
|
+
const finalSteps = cleanedSteps.length > 0 ? cleanedSteps : [{ type: 'direct' }];
|
|
1789
|
+
this.save.emit(finalSteps);
|
|
1091
1790
|
}
|
|
1092
1791
|
onDelete() {
|
|
1093
1792
|
this.delete.emit();
|
|
@@ -1104,8 +1803,88 @@ class TransformationPopoverComponent {
|
|
|
1104
1803
|
top: `${this.position.y}px`,
|
|
1105
1804
|
};
|
|
1106
1805
|
}
|
|
1806
|
+
getStepTypeLabel(type) {
|
|
1807
|
+
const t = this.availableTransformations.find(t => t.type === type);
|
|
1808
|
+
return t?.label || type;
|
|
1809
|
+
}
|
|
1810
|
+
// Condition methods
|
|
1811
|
+
hasCondition(step) {
|
|
1812
|
+
return step.condition?.enabled === true;
|
|
1813
|
+
}
|
|
1814
|
+
toggleCondition(step, enabled) {
|
|
1815
|
+
if (enabled) {
|
|
1816
|
+
if (!step.condition) {
|
|
1817
|
+
step.condition = {
|
|
1818
|
+
enabled: true,
|
|
1819
|
+
root: this.createEmptyConditionGroup(),
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
else {
|
|
1823
|
+
step.condition.enabled = true;
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
else {
|
|
1827
|
+
if (step.condition) {
|
|
1828
|
+
step.condition.enabled = false;
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
onConditionChange(step, group) {
|
|
1833
|
+
if (step.condition) {
|
|
1834
|
+
step.condition.root = group;
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
createEmptyConditionGroup() {
|
|
1838
|
+
return {
|
|
1839
|
+
id: `cond-${Date.now()}`,
|
|
1840
|
+
type: 'group',
|
|
1841
|
+
logic: 'and',
|
|
1842
|
+
children: [],
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
getConditionSummary(step) {
|
|
1846
|
+
if (!step.condition?.enabled || !step.condition.root)
|
|
1847
|
+
return '';
|
|
1848
|
+
return this.summarizeConditionGroup(step.condition.root);
|
|
1849
|
+
}
|
|
1850
|
+
summarizeConditionGroup(group) {
|
|
1851
|
+
if (group.children.length === 0)
|
|
1852
|
+
return '';
|
|
1853
|
+
const parts = group.children.map(child => {
|
|
1854
|
+
if (child.type === 'condition') {
|
|
1855
|
+
const opLabel = this.getOperatorLabel(child.operator);
|
|
1856
|
+
if (['isEmpty', 'isNotEmpty', 'isTrue', 'isFalse'].includes(child.operator)) {
|
|
1857
|
+
return opLabel;
|
|
1858
|
+
}
|
|
1859
|
+
return `${opLabel} "${child.value}"`;
|
|
1860
|
+
}
|
|
1861
|
+
else {
|
|
1862
|
+
return `(${this.summarizeConditionGroup(child)})`;
|
|
1863
|
+
}
|
|
1864
|
+
});
|
|
1865
|
+
return parts.join(` ${group.logic.toUpperCase()} `);
|
|
1866
|
+
}
|
|
1867
|
+
getOperatorLabel(operator) {
|
|
1868
|
+
const labels = {
|
|
1869
|
+
equals: '=',
|
|
1870
|
+
notEquals: '!=',
|
|
1871
|
+
contains: 'contains',
|
|
1872
|
+
notContains: 'not contain',
|
|
1873
|
+
startsWith: 'starts with',
|
|
1874
|
+
endsWith: 'ends with',
|
|
1875
|
+
isEmpty: 'is empty',
|
|
1876
|
+
isNotEmpty: 'is not empty',
|
|
1877
|
+
greaterThan: '>',
|
|
1878
|
+
lessThan: '<',
|
|
1879
|
+
greaterThanOrEqual: '>=',
|
|
1880
|
+
lessThanOrEqual: '<=',
|
|
1881
|
+
isTrue: 'is true',
|
|
1882
|
+
isFalse: 'is false',
|
|
1883
|
+
};
|
|
1884
|
+
return labels[operator] || operator;
|
|
1885
|
+
}
|
|
1107
1886
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: TransformationPopoverComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1108
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: TransformationPopoverComponent, isStandalone: true, selector: "transformation-popover", inputs: { mapping: "mapping", position: "position", sampleData: "sampleData" }, outputs: { save: "save", delete: "delete", close: "close" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"transformation-popover\" [ngStyle]=\"getPopoverStyle()\">\n <div class=\"popover-arrow\"></div>\n\n <div class=\"popover-header\">\n <span class=\"popover-title\">Transformation</span>\n <button mat-icon-button class=\"close-btn\" (click)=\"onClose()\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n\n <div class=\"popover-content\">\n <!-- Source/Target Info -->\n <div class=\"mapping-info\">\n <div class=\"info-row\">\n <span class=\"info-label\">Source:</span>\n <span class=\"info-value\">{{ getSourceFieldNames() }}</span>\n </div>\n <div class=\"info-row\">\n <span class=\"info-label\">Target:</span>\n <span class=\"info-value\">{{ mapping.targetField.name }}</span>\n </div>\n </div>\n\n <!-- Transformation Type -->\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Transformation Type</mat-label>\n <mat-select [(ngModel)]=\"transformationType\" (selectionChange)=\"onTypeChange()\">\n @for (t of availableTransformations; track t.type) {\n <mat-option [value]=\"t.type\">{{ t.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <!-- Type-specific options -->\n @switch (transformationType) {\n @case ('concat') {\n <div class=\"config-section\">\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Template</mat-label>\n <input\n matInput\n [(ngModel)]=\"config.template\"\n (ngModelChange)=\"onConfigChange()\"\n placeholder=\"{field1} - {field2}\"\n />\n <mat-hint>Use curly braces around fieldName for values</mat-hint>\n </mat-form-field>\n </div>\n }\n\n @case ('substring') {\n <div class=\"config-section config-row\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Start Index</mat-label>\n <input\n matInput\n type=\"number\"\n [(ngModel)]=\"config.startIndex\"\n (ngModelChange)=\"onConfigChange()\"\n min=\"0\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>End Index</mat-label>\n <input\n matInput\n type=\"number\"\n [(ngModel)]=\"config.endIndex\"\n (ngModelChange)=\"onConfigChange()\"\n />\n </mat-form-field>\n </div>\n }\n\n @case ('replace') {\n <div class=\"config-section\">\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Search For</mat-label>\n <input\n matInput\n [(ngModel)]=\"config.searchValue\"\n (ngModelChange)=\"onConfigChange()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Replace With</mat-label>\n <input\n matInput\n [(ngModel)]=\"config.replaceValue\"\n (ngModelChange)=\"onConfigChange()\"\n />\n </mat-form-field>\n </div>\n }\n\n @case ('dateFormat') {\n <div class=\"config-section\">\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Output Format</mat-label>\n <input\n matInput\n [(ngModel)]=\"config.outputFormat\"\n (ngModelChange)=\"onConfigChange()\"\n placeholder=\"YYYY-MM-DD\"\n />\n <mat-hint>YYYY, MM, DD, HH, mm, ss</mat-hint>\n </mat-form-field>\n </div>\n }\n\n @case ('numberFormat') {\n <div class=\"config-section\">\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Decimal Places</mat-label>\n <input\n matInput\n type=\"number\"\n [(ngModel)]=\"config.decimalPlaces\"\n (ngModelChange)=\"onConfigChange()\"\n min=\"0\"\n />\n </mat-form-field>\n <div class=\"config-row\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Prefix</mat-label>\n <input\n matInput\n [(ngModel)]=\"config.prefix\"\n (ngModelChange)=\"onConfigChange()\"\n placeholder=\"$\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Suffix</mat-label>\n <input\n matInput\n [(ngModel)]=\"config.suffix\"\n (ngModelChange)=\"onConfigChange()\"\n />\n </mat-form-field>\n </div>\n </div>\n }\n\n @case ('template') {\n <div class=\"config-section\">\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Template Expression</mat-label>\n <textarea\n matInput\n [(ngModel)]=\"config.template\"\n (ngModelChange)=\"onConfigChange()\"\n rows=\"3\"\n placeholder=\"Hello {firstName}, your ID is {id}\"\n ></textarea>\n </mat-form-field>\n </div>\n }\n\n @case ('custom') {\n <div class=\"config-section\">\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>JavaScript Expression</mat-label>\n <textarea\n matInput\n [(ngModel)]=\"config.expression\"\n (ngModelChange)=\"onConfigChange()\"\n rows=\"3\"\n placeholder=\"fieldName.toUpperCase()\"\n ></textarea>\n <mat-hint>Use field names as variables</mat-hint>\n </mat-form-field>\n </div>\n }\n }\n\n <!-- Preview Section -->\n <div class=\"preview-section\">\n <span class=\"preview-label\">Preview:</span>\n <div class=\"preview-value\">{{ preview || '(empty)' }}</div>\n </div>\n </div>\n\n <div class=\"popover-actions\">\n <button mat-button color=\"warn\" (click)=\"onDelete()\" matTooltip=\"Remove this mapping\">\n <mat-icon>delete</mat-icon>\n Delete\n </button>\n <div class=\"action-spacer\"></div>\n <button mat-button (click)=\"onClose()\">Cancel</button>\n <button mat-flat-button color=\"primary\" (click)=\"onSave()\">Apply</button>\n </div>\n</div>\n\n<!-- Backdrop -->\n<div class=\"popover-backdrop\" (click)=\"onClose()\"></div>\n", styles: [".popover-backdrop{position:fixed;inset:0;background:#0000004d;z-index:999}.transformation-popover{position:fixed;z-index:1000;width:360px;background:#fff;border-radius:12px;box-shadow:0 10px 40px #0003;transform:translate(-50%,-50%);animation:popoverIn .2s ease-out}@keyframes popoverIn{0%{opacity:0;transform:translate(-50%,-50%) scale(.95)}to{opacity:1;transform:translate(-50%,-50%) scale(1)}}.popover-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid #e2e8f0;background:#f8fafc;border-radius:12px 12px 0 0}.popover-title{font-size:16px;font-weight:600;color:#1e293b}.close-btn{width:32px;height:32px;line-height:32px}.close-btn mat-icon{font-size:20px;width:20px;height:20px}.popover-content{padding:20px;max-height:400px;overflow-y:auto}.mapping-info{background:#f1f5f9;border-radius:8px;padding:12px 16px;margin-bottom:16px}.info-row{display:flex;align-items:center;gap:8px}.info-row+.info-row{margin-top:8px}.info-label{font-size:12px;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:.5px;width:60px}.info-value{font-size:14px;color:#1e293b;font-weight:500}.full-width{width:100%}.config-section{margin-top:12px}.config-row{display:flex;gap:12px}.config-row mat-form-field{flex:1}.preview-section{margin-top:16px;padding:12px 16px;background:linear-gradient(135deg,#eff6ff,#f0fdf4);border-radius:8px;border:1px solid #e0e7ff}.preview-label{font-size:11px;font-weight:600;color:#6366f1;text-transform:uppercase;letter-spacing:.5px;display:block;margin-bottom:6px}.preview-value{font-size:14px;color:#1e293b;font-family:Monaco,Menlo,monospace;word-break:break-all;min-height:20px}.popover-actions{display:flex;align-items:center;gap:8px;padding:16px 20px;border-top:1px solid #e2e8f0;background:#f8fafc;border-radius:0 0 12px 12px}.action-spacer{flex:1}::ng-deep .transformation-popover .mat-mdc-form-field{font-size:14px}::ng-deep .transformation-popover .mat-mdc-text-field-wrapper{background:#fff}::ng-deep .transformation-popover .mat-mdc-form-field-subscript-wrapper{font-size:11px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2$1.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "directive", type: i5.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "component", type: i5.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i5.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5$1.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i8.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] });
|
|
1887
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: TransformationPopoverComponent, isStandalone: true, selector: "transformation-popover", inputs: { mapping: "mapping", position: "position", sampleData: "sampleData" }, outputs: { save: "save", delete: "delete", close: "close" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"transformation-popover\" [ngStyle]=\"getPopoverStyle()\">\r\n <div class=\"popover-arrow\"></div>\r\n\r\n <div class=\"popover-header\">\r\n <span class=\"popover-title\">Transformation</span>\r\n <button mat-icon-button class=\"close-btn\" (click)=\"onClose()\">\r\n <mat-icon>close</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <div class=\"popover-content\">\r\n <!-- Source/Target Info -->\r\n <div class=\"mapping-info\">\r\n <div class=\"info-row\">\r\n <span class=\"info-label\">Source:</span>\r\n <span class=\"info-value\">{{ getSourceFieldNames() }}</span>\r\n </div>\r\n <div class=\"info-row\">\r\n <span class=\"info-label\">Target:</span>\r\n <span class=\"info-value\">{{ mapping.targetField.name }}</span>\r\n </div>\r\n </div>\r\n\r\n <!-- Single Step Mode (default, clean UI) -->\r\n @if (!isMultiStep) {\r\n <ng-container *ngTemplateOutlet=\"stepConfig; context: { step: steps[0], index: 0, showHeader: false }\"></ng-container>\r\n\r\n <!-- Preview Section -->\r\n <div class=\"preview-section\">\r\n <span class=\"preview-label\">Preview:</span>\r\n <div class=\"preview-value\">{{ stepPreviews[0] || '(empty)' }}</div>\r\n </div>\r\n }\r\n\r\n <!-- Multi-Step Mode -->\r\n @if (isMultiStep) {\r\n <div class=\"steps-container\" cdkDropList (cdkDropListDropped)=\"onStepDrop($event)\">\r\n @for (step of steps; track $index; let i = $index) {\r\n <div class=\"step-card\" [class.expanded]=\"isStepExpanded(i)\" cdkDrag>\r\n <!-- Collapsed View -->\r\n @if (!isStepExpanded(i)) {\r\n <div class=\"step-collapsed\" (click)=\"toggleStep(i)\">\r\n <mat-icon class=\"drag-handle\" cdkDragHandle (click)=\"$event.stopPropagation()\">drag_indicator</mat-icon>\r\n <div class=\"step-collapsed-header\">\r\n <div class=\"step-title-row\">\r\n <span class=\"step-number\">Step {{ i + 1 }}: {{ getStepTypeLabel(step.type) }}</span>\r\n @if (hasCondition(step)) {\r\n <span class=\"condition-badge\" matTooltip=\"{{ getConditionSummary(step) }}\">\r\n <mat-icon>filter_alt</mat-icon>\r\n if\r\n </span>\r\n }\r\n </div>\r\n <div class=\"step-collapsed-actions\">\r\n <button\r\n mat-icon-button\r\n class=\"expand-btn\"\r\n (click)=\"toggleStep(i); $event.stopPropagation()\"\r\n matTooltip=\"Edit step\"\r\n >\r\n <mat-icon>edit</mat-icon>\r\n </button>\r\n <button\r\n mat-icon-button\r\n class=\"remove-step-btn\"\r\n (click)=\"removeStep(i); $event.stopPropagation()\"\r\n matTooltip=\"Remove step\"\r\n >\r\n <mat-icon>close</mat-icon>\r\n </button>\r\n </div>\r\n </div>\r\n <div class=\"step-collapsed-preview\">\r\n <span class=\"step-input\">\"{{ (stepInputs[i] || '') | slice:0:20 }}{{ (stepInputs[i] || '').length > 20 ? '...' : '' }}\"</span>\r\n <mat-icon class=\"arrow-icon\">arrow_forward</mat-icon>\r\n <span class=\"step-output\">\"{{ (stepPreviews[i] || '') | slice:0:20 }}{{ (stepPreviews[i] || '').length > 20 ? '...' : '' }}\"</span>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Expanded View -->\r\n @if (isStepExpanded(i)) {\r\n <div class=\"step-expanded\">\r\n <mat-icon class=\"drag-handle\" cdkDragHandle>drag_indicator</mat-icon>\r\n <div class=\"step-header\">\r\n <span class=\"step-number\">Step {{ i + 1 }}</span>\r\n <div class=\"step-header-actions\">\r\n <button\r\n mat-icon-button\r\n class=\"collapse-btn\"\r\n (click)=\"toggleStep(i)\"\r\n matTooltip=\"Collapse\"\r\n >\r\n <mat-icon>expand_less</mat-icon>\r\n </button>\r\n <button\r\n mat-icon-button\r\n class=\"remove-step-btn\"\r\n (click)=\"removeStep(i)\"\r\n matTooltip=\"Remove step\"\r\n >\r\n <mat-icon>close</mat-icon>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <div class=\"step-content\">\r\n <ng-container *ngTemplateOutlet=\"stepConfig; context: { step: step, index: i, showHeader: false }\"></ng-container>\r\n </div>\r\n\r\n <!-- Step Preview -->\r\n <div class=\"step-preview\">\r\n <mat-icon class=\"preview-arrow\">arrow_downward</mat-icon>\r\n <span class=\"step-preview-value\">{{ stepPreviews[i] || '(empty)' }}</span>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Final Preview -->\r\n <div class=\"preview-section final-preview\">\r\n <span class=\"preview-label\">Final Result:</span>\r\n <div class=\"preview-value\">{{ finalPreview || '(empty)' }}</div>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Add Step Button - Always visible -->\r\n <div class=\"add-step-section\">\r\n <button mat-stroked-button class=\"add-step-btn\" (click)=\"addStep()\">\r\n <mat-icon>add</mat-icon>\r\n @if (isMultiStep) {\r\n Add Step\r\n } @else {\r\n Add Transformation Step\r\n }\r\n </button>\r\n </div>\r\n\r\n <div class=\"popover-actions\">\r\n <button mat-button color=\"warn\" (click)=\"onDelete()\" matTooltip=\"Remove this mapping\">\r\n <mat-icon>delete</mat-icon>\r\n Delete\r\n </button>\r\n <div class=\"action-spacer\"></div>\r\n <button mat-button (click)=\"onClose()\">Cancel</button>\r\n <button mat-flat-button color=\"primary\" (click)=\"onSave()\">Apply</button>\r\n </div>\r\n</div>\r\n\r\n<!-- Backdrop -->\r\n<div class=\"popover-backdrop\" (click)=\"onClose()\"></div>\r\n\r\n<!-- Step Configuration Template -->\r\n<ng-template #stepConfig let-step=\"step\" let-index=\"index\" let-showHeader=\"showHeader\">\r\n <!-- Transformation Type -->\r\n <mat-form-field appearance=\"outline\" class=\"full-width\">\r\n <mat-label>Transformation Type</mat-label>\r\n <mat-select [(ngModel)]=\"step.type\" (selectionChange)=\"onStepTypeChange(index)\">\r\n @for (t of availableTransformations; track t.type) {\r\n <mat-option [value]=\"t.type\">{{ t.label }}</mat-option>\r\n }\r\n </mat-select>\r\n </mat-form-field>\r\n\r\n <!-- Type-specific options -->\r\n @switch (step.type) {\r\n @case ('concat') {\r\n <div class=\"config-section\">\r\n <mat-form-field appearance=\"outline\" class=\"full-width\">\r\n <mat-label>Separator</mat-label>\r\n <input\r\n matInput\r\n [(ngModel)]=\"step.separator\"\r\n (ngModelChange)=\"onConfigChange()\"\r\n placeholder=\" \"\r\n />\r\n <mat-hint>Join values with this (default: space)</mat-hint>\r\n </mat-form-field>\r\n <div class=\"or-divider\">or use template for custom format</div>\r\n <mat-form-field appearance=\"outline\" class=\"full-width\">\r\n <mat-label>Template</mat-label>\r\n <input\r\n matInput\r\n [(ngModel)]=\"step.template\"\r\n (ngModelChange)=\"onConfigChange()\"\r\n [placeholder]=\"'{0} - {1}'\"\r\n />\r\n <mat-hint>Overrides separator if set</mat-hint>\r\n </mat-form-field>\r\n </div>\r\n }\r\n\r\n @case ('substring') {\r\n <div class=\"config-section config-row\">\r\n <mat-form-field appearance=\"outline\">\r\n <mat-label>Start Index</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [(ngModel)]=\"step.startIndex\"\r\n (ngModelChange)=\"onConfigChange()\"\r\n min=\"0\"\r\n />\r\n </mat-form-field>\r\n <mat-form-field appearance=\"outline\">\r\n <mat-label>End Index</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [(ngModel)]=\"step.endIndex\"\r\n (ngModelChange)=\"onConfigChange()\"\r\n />\r\n </mat-form-field>\r\n </div>\r\n }\r\n\r\n @case ('replace') {\r\n <div class=\"config-section\">\r\n <mat-form-field appearance=\"outline\" class=\"full-width\">\r\n <mat-label>Search For</mat-label>\r\n <input\r\n matInput\r\n [(ngModel)]=\"step.searchValue\"\r\n (ngModelChange)=\"onConfigChange()\"\r\n />\r\n </mat-form-field>\r\n <mat-form-field appearance=\"outline\" class=\"full-width\">\r\n <mat-label>Replace With</mat-label>\r\n <input\r\n matInput\r\n [(ngModel)]=\"step.replaceValue\"\r\n (ngModelChange)=\"onConfigChange()\"\r\n />\r\n </mat-form-field>\r\n </div>\r\n }\r\n\r\n @case ('dateFormat') {\r\n <div class=\"config-section\">\r\n <mat-form-field appearance=\"outline\" class=\"full-width\">\r\n <mat-label>Output Format</mat-label>\r\n <input\r\n matInput\r\n [(ngModel)]=\"step.outputFormat\"\r\n (ngModelChange)=\"onConfigChange()\"\r\n placeholder=\"YYYY-MM-DD\"\r\n />\r\n <mat-hint>YYYY, MM, DD, HH, mm, ss</mat-hint>\r\n </mat-form-field>\r\n </div>\r\n }\r\n\r\n @case ('numberFormat') {\r\n <div class=\"config-section\">\r\n <mat-form-field appearance=\"outline\" class=\"full-width\">\r\n <mat-label>Decimal Places</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [(ngModel)]=\"step.decimalPlaces\"\r\n (ngModelChange)=\"onConfigChange()\"\r\n min=\"0\"\r\n />\r\n </mat-form-field>\r\n <div class=\"config-row\">\r\n <mat-form-field appearance=\"outline\">\r\n <mat-label>Prefix</mat-label>\r\n <input\r\n matInput\r\n [(ngModel)]=\"step.prefix\"\r\n (ngModelChange)=\"onConfigChange()\"\r\n placeholder=\"$\"\r\n />\r\n </mat-form-field>\r\n <mat-form-field appearance=\"outline\">\r\n <mat-label>Suffix</mat-label>\r\n <input\r\n matInput\r\n [(ngModel)]=\"step.suffix\"\r\n (ngModelChange)=\"onConfigChange()\"\r\n />\r\n </mat-form-field>\r\n </div>\r\n </div>\r\n }\r\n\r\n @case ('mask') {\r\n <div class=\"config-section\">\r\n <mat-form-field appearance=\"outline\" class=\"full-width\">\r\n <mat-label>Pattern</mat-label>\r\n <input\r\n matInput\r\n [(ngModel)]=\"step.pattern\"\r\n (ngModelChange)=\"onConfigChange()\"\r\n placeholder=\"(###) ###-####\"\r\n />\r\n <mat-hint># = character from input</mat-hint>\r\n </mat-form-field>\r\n </div>\r\n }\r\n\r\n @case ('template') {\r\n <div class=\"config-section\">\r\n <mat-form-field appearance=\"outline\" class=\"full-width\">\r\n <mat-label>Template Expression</mat-label>\r\n <textarea\r\n matInput\r\n [(ngModel)]=\"step.template\"\r\n (ngModelChange)=\"onConfigChange()\"\r\n rows=\"3\"\r\n [placeholder]=\"'Hello {0}, your ID is {1}'\"\r\n ></textarea>\r\n @if (index === 0) {\r\n <mat-hint>Use {{ '{' }}0{{ '}' }}, {{ '{' }}1{{ '}' }}, etc. for source fields</mat-hint>\r\n } @else {\r\n <mat-hint>Use {{ '{' }}0{{ '}' }} for the value from previous step</mat-hint>\r\n }\r\n </mat-form-field>\r\n </div>\r\n }\r\n }\r\n\r\n <!-- Condition Section -->\r\n <div class=\"condition-section\">\r\n <mat-checkbox\r\n [checked]=\"hasCondition(step)\"\r\n (change)=\"toggleCondition(step, $event.checked)\"\r\n class=\"condition-checkbox\"\r\n >\r\n Apply conditionally\r\n </mat-checkbox>\r\n\r\n @if (hasCondition(step)) {\r\n <div class=\"condition-content\">\r\n <condition-builder\r\n [condition]=\"step.condition?.root || null\"\r\n [compact]=\"true\"\r\n (conditionChange)=\"onConditionChange(step, $event)\"\r\n ></condition-builder>\r\n </div>\r\n }\r\n </div>\r\n</ng-template>\r\n", styles: ["@charset \"UTF-8\";.popover-backdrop{position:fixed;inset:0;background:#0000004d;z-index:999}.transformation-popover{position:fixed;z-index:1000;width:420px;background:#fff;border-radius:12px;box-shadow:0 10px 40px #0003;transform:translate(-50%,-50%);animation:popoverIn .2s ease-out}@keyframes popoverIn{0%{opacity:0;transform:translate(-50%,-50%) scale(.95)}to{opacity:1;transform:translate(-50%,-50%) scale(1)}}.popover-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid #e2e8f0;background:#f8fafc;border-radius:12px 12px 0 0}.popover-title{font-size:16px;font-weight:600;color:#1e293b}.close-btn{width:32px;height:32px;line-height:32px}.close-btn mat-icon{font-size:20px;width:20px;height:20px}.popover-content{padding:20px;max-height:500px;overflow-y:auto}.mapping-info{background:#f1f5f9;border-radius:8px;padding:12px 16px;margin-bottom:16px}.info-row{display:flex;align-items:center;gap:8px}.info-row+.info-row{margin-top:8px}.info-label{font-size:12px;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:.5px;width:60px}.info-value{font-size:14px;color:#1e293b;font-weight:500}.full-width{width:100%}.config-section{margin-top:12px}.or-divider{text-align:center;font-size:11px;color:#94a3b8;margin:8px 0}.condition-section{margin-top:16px;padding-top:12px;border-top:1px dashed #e2e8f0}.condition-checkbox{font-size:13px;color:#64748b}.condition-content{margin-top:12px}.config-row{display:flex;gap:12px}.config-row mat-form-field{flex:1}.preview-section{margin-top:16px;padding:12px 16px;background:linear-gradient(135deg,#eff6ff,#f0fdf4);border-radius:8px;border:1px solid #e0e7ff}.preview-label{font-size:11px;font-weight:600;color:#6366f1;text-transform:uppercase;letter-spacing:.5px;display:block;margin-bottom:6px}.preview-value{font-size:14px;color:#1e293b;font-family:Monaco,Menlo,monospace;word-break:break-all;min-height:20px}.steps-container{display:flex;flex-direction:column;gap:12px}.step-card{background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;overflow:hidden;transition:all .2s ease}.step-card.expanded{border-color:#6366f1;border-width:2px;background:#fff}.step-collapsed{display:grid;grid-template-columns:auto 1fr;gap:0 12px;padding:12px;cursor:pointer;transition:background .15s ease}.step-collapsed:hover{background:#f1f5f9}.step-collapsed .drag-handle{grid-row:span 2;align-self:center}.step-collapsed-header{display:flex;align-items:center;flex-wrap:wrap;gap:4px}.step-collapsed-header .step-title-row{display:flex;align-items:center;gap:8px;flex:1}.step-collapsed-header .step-number{flex-shrink:0}.condition-badge{display:inline-flex;align-items:center;gap:2px;padding:2px 6px;background:#fef3c7;border:1px solid #f59e0b;border-radius:4px;font-size:10px;font-weight:600;color:#b45309;cursor:help}.condition-badge mat-icon{font-size:12px;width:12px;height:12px}.step-collapsed-actions{display:flex;align-items:center;gap:4px}.step-collapsed-preview{display:flex;align-items:center;gap:8px;margin-top:8px;font-size:12px;font-family:Monaco,Menlo,monospace}.step-input{color:#64748b;max-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.arrow-icon{font-size:14px;width:14px;height:14px;color:#6366f1}.step-output{color:#059669;font-weight:500;max-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.step-expanded{display:grid;grid-template-columns:auto 1fr;gap:0 12px;padding:12px}.step-expanded .drag-handle{grid-row:1;align-self:center}.step-expanded .step-header,.step-expanded .step-content,.step-expanded .step-preview{grid-column:2}.step-header{display:flex;align-items:center;margin-bottom:12px}.step-header .step-number{flex:1}.step-header-actions{display:flex;align-items:center;gap:4px}.step-number{font-size:12px;font-weight:600;color:#6366f1;text-transform:uppercase;letter-spacing:.5px}.expand-btn,.collapse-btn{width:28px;height:28px;line-height:28px}.expand-btn mat-icon,.collapse-btn mat-icon{font-size:18px;width:18px;height:18px;color:#64748b}.expand-btn:hover mat-icon,.collapse-btn:hover mat-icon{color:#6366f1}.remove-step-btn{width:28px;height:28px;line-height:28px}.remove-step-btn mat-icon{font-size:18px;width:18px;height:18px;color:#94a3b8}.remove-step-btn:hover mat-icon{color:#ef4444}.step-preview{display:flex;align-items:center;gap:8px;margin-top:12px;padding:8px 12px;background:#f1f5f9;border-radius:6px}.preview-arrow{font-size:16px;width:16px;height:16px;color:#6366f1}.step-preview-value{font-size:13px;color:#475569;font-family:Monaco,Menlo,monospace;word-break:break-all;flex:1}.add-step-section{padding:12px 20px;border-top:1px solid #e2e8f0;background:#fafafa}.add-step-btn{width:100%;border-style:dashed;color:#64748b}.add-step-btn mat-icon{font-size:18px;width:18px;height:18px;margin-right:4px}.add-step-btn:hover{border-color:#6366f1;color:#6366f1;background:#f5f3ff}.final-preview{margin-top:16px;background:linear-gradient(135deg,#ecfdf5,#d1fae5);border:2px solid #10b981;border-radius:8px;position:relative}.final-preview .preview-label{color:#059669;font-size:12px;font-weight:700}.final-preview .preview-value{color:#065f46;font-weight:600}.final-preview:before{content:\"\\2713\";position:absolute;right:12px;top:10px;color:#10b981;font-size:16px;font-weight:700}.popover-actions{display:flex;align-items:center;gap:8px;padding:16px 20px;border-top:1px solid #e2e8f0;background:#f8fafc;border-radius:0 0 12px 12px}.action-spacer{flex:1}::ng-deep .transformation-popover .mat-mdc-form-field{font-size:14px}::ng-deep .transformation-popover .mat-mdc-text-field-wrapper{background:#fff}::ng-deep .transformation-popover .mat-mdc-form-field-subscript-wrapper{font-size:11px}.drag-handle{cursor:grab;color:#94a3b8;font-size:20px;width:20px;height:20px;flex-shrink:0}.drag-handle:hover{color:#6366f1}.drag-handle:active{cursor:grabbing}.step-card.cdk-drag-preview{box-shadow:0 5px 20px #00000040;border-radius:8px;background:#fff}.step-card.cdk-drag-placeholder{opacity:.4;background:#e2e8f0;border:2px dashed #94a3b8}.step-card.cdk-drag-animating{transition:transform .2s ease}.steps-container.cdk-drop-list-dragging .step-card:not(.cdk-drag-placeholder){transition:transform .2s ease}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i2$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "component", type: i4.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i4.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i8.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i10.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i10.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i10.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "component", type: ConditionBuilderComponent, selector: "condition-builder", inputs: ["fields", "condition", "compact"], outputs: ["conditionChange"] }, { kind: "pipe", type: i1.SlicePipe, name: "slice" }] });
|
|
1109
1888
|
}
|
|
1110
1889
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: TransformationPopoverComponent, decorators: [{
|
|
1111
1890
|
type: Component,
|
|
@@ -1118,7 +1897,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
1118
1897
|
MatInputModule,
|
|
1119
1898
|
MatFormFieldModule,
|
|
1120
1899
|
MatTooltipModule,
|
|
1121
|
-
|
|
1900
|
+
MatCheckboxModule,
|
|
1901
|
+
DragDropModule,
|
|
1902
|
+
ConditionBuilderComponent,
|
|
1903
|
+
], template: "<div class=\"transformation-popover\" [ngStyle]=\"getPopoverStyle()\">\r\n <div class=\"popover-arrow\"></div>\r\n\r\n <div class=\"popover-header\">\r\n <span class=\"popover-title\">Transformation</span>\r\n <button mat-icon-button class=\"close-btn\" (click)=\"onClose()\">\r\n <mat-icon>close</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <div class=\"popover-content\">\r\n <!-- Source/Target Info -->\r\n <div class=\"mapping-info\">\r\n <div class=\"info-row\">\r\n <span class=\"info-label\">Source:</span>\r\n <span class=\"info-value\">{{ getSourceFieldNames() }}</span>\r\n </div>\r\n <div class=\"info-row\">\r\n <span class=\"info-label\">Target:</span>\r\n <span class=\"info-value\">{{ mapping.targetField.name }}</span>\r\n </div>\r\n </div>\r\n\r\n <!-- Single Step Mode (default, clean UI) -->\r\n @if (!isMultiStep) {\r\n <ng-container *ngTemplateOutlet=\"stepConfig; context: { step: steps[0], index: 0, showHeader: false }\"></ng-container>\r\n\r\n <!-- Preview Section -->\r\n <div class=\"preview-section\">\r\n <span class=\"preview-label\">Preview:</span>\r\n <div class=\"preview-value\">{{ stepPreviews[0] || '(empty)' }}</div>\r\n </div>\r\n }\r\n\r\n <!-- Multi-Step Mode -->\r\n @if (isMultiStep) {\r\n <div class=\"steps-container\" cdkDropList (cdkDropListDropped)=\"onStepDrop($event)\">\r\n @for (step of steps; track $index; let i = $index) {\r\n <div class=\"step-card\" [class.expanded]=\"isStepExpanded(i)\" cdkDrag>\r\n <!-- Collapsed View -->\r\n @if (!isStepExpanded(i)) {\r\n <div class=\"step-collapsed\" (click)=\"toggleStep(i)\">\r\n <mat-icon class=\"drag-handle\" cdkDragHandle (click)=\"$event.stopPropagation()\">drag_indicator</mat-icon>\r\n <div class=\"step-collapsed-header\">\r\n <div class=\"step-title-row\">\r\n <span class=\"step-number\">Step {{ i + 1 }}: {{ getStepTypeLabel(step.type) }}</span>\r\n @if (hasCondition(step)) {\r\n <span class=\"condition-badge\" matTooltip=\"{{ getConditionSummary(step) }}\">\r\n <mat-icon>filter_alt</mat-icon>\r\n if\r\n </span>\r\n }\r\n </div>\r\n <div class=\"step-collapsed-actions\">\r\n <button\r\n mat-icon-button\r\n class=\"expand-btn\"\r\n (click)=\"toggleStep(i); $event.stopPropagation()\"\r\n matTooltip=\"Edit step\"\r\n >\r\n <mat-icon>edit</mat-icon>\r\n </button>\r\n <button\r\n mat-icon-button\r\n class=\"remove-step-btn\"\r\n (click)=\"removeStep(i); $event.stopPropagation()\"\r\n matTooltip=\"Remove step\"\r\n >\r\n <mat-icon>close</mat-icon>\r\n </button>\r\n </div>\r\n </div>\r\n <div class=\"step-collapsed-preview\">\r\n <span class=\"step-input\">\"{{ (stepInputs[i] || '') | slice:0:20 }}{{ (stepInputs[i] || '').length > 20 ? '...' : '' }}\"</span>\r\n <mat-icon class=\"arrow-icon\">arrow_forward</mat-icon>\r\n <span class=\"step-output\">\"{{ (stepPreviews[i] || '') | slice:0:20 }}{{ (stepPreviews[i] || '').length > 20 ? '...' : '' }}\"</span>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Expanded View -->\r\n @if (isStepExpanded(i)) {\r\n <div class=\"step-expanded\">\r\n <mat-icon class=\"drag-handle\" cdkDragHandle>drag_indicator</mat-icon>\r\n <div class=\"step-header\">\r\n <span class=\"step-number\">Step {{ i + 1 }}</span>\r\n <div class=\"step-header-actions\">\r\n <button\r\n mat-icon-button\r\n class=\"collapse-btn\"\r\n (click)=\"toggleStep(i)\"\r\n matTooltip=\"Collapse\"\r\n >\r\n <mat-icon>expand_less</mat-icon>\r\n </button>\r\n <button\r\n mat-icon-button\r\n class=\"remove-step-btn\"\r\n (click)=\"removeStep(i)\"\r\n matTooltip=\"Remove step\"\r\n >\r\n <mat-icon>close</mat-icon>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <div class=\"step-content\">\r\n <ng-container *ngTemplateOutlet=\"stepConfig; context: { step: step, index: i, showHeader: false }\"></ng-container>\r\n </div>\r\n\r\n <!-- Step Preview -->\r\n <div class=\"step-preview\">\r\n <mat-icon class=\"preview-arrow\">arrow_downward</mat-icon>\r\n <span class=\"step-preview-value\">{{ stepPreviews[i] || '(empty)' }}</span>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Final Preview -->\r\n <div class=\"preview-section final-preview\">\r\n <span class=\"preview-label\">Final Result:</span>\r\n <div class=\"preview-value\">{{ finalPreview || '(empty)' }}</div>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Add Step Button - Always visible -->\r\n <div class=\"add-step-section\">\r\n <button mat-stroked-button class=\"add-step-btn\" (click)=\"addStep()\">\r\n <mat-icon>add</mat-icon>\r\n @if (isMultiStep) {\r\n Add Step\r\n } @else {\r\n Add Transformation Step\r\n }\r\n </button>\r\n </div>\r\n\r\n <div class=\"popover-actions\">\r\n <button mat-button color=\"warn\" (click)=\"onDelete()\" matTooltip=\"Remove this mapping\">\r\n <mat-icon>delete</mat-icon>\r\n Delete\r\n </button>\r\n <div class=\"action-spacer\"></div>\r\n <button mat-button (click)=\"onClose()\">Cancel</button>\r\n <button mat-flat-button color=\"primary\" (click)=\"onSave()\">Apply</button>\r\n </div>\r\n</div>\r\n\r\n<!-- Backdrop -->\r\n<div class=\"popover-backdrop\" (click)=\"onClose()\"></div>\r\n\r\n<!-- Step Configuration Template -->\r\n<ng-template #stepConfig let-step=\"step\" let-index=\"index\" let-showHeader=\"showHeader\">\r\n <!-- Transformation Type -->\r\n <mat-form-field appearance=\"outline\" class=\"full-width\">\r\n <mat-label>Transformation Type</mat-label>\r\n <mat-select [(ngModel)]=\"step.type\" (selectionChange)=\"onStepTypeChange(index)\">\r\n @for (t of availableTransformations; track t.type) {\r\n <mat-option [value]=\"t.type\">{{ t.label }}</mat-option>\r\n }\r\n </mat-select>\r\n </mat-form-field>\r\n\r\n <!-- Type-specific options -->\r\n @switch (step.type) {\r\n @case ('concat') {\r\n <div class=\"config-section\">\r\n <mat-form-field appearance=\"outline\" class=\"full-width\">\r\n <mat-label>Separator</mat-label>\r\n <input\r\n matInput\r\n [(ngModel)]=\"step.separator\"\r\n (ngModelChange)=\"onConfigChange()\"\r\n placeholder=\" \"\r\n />\r\n <mat-hint>Join values with this (default: space)</mat-hint>\r\n </mat-form-field>\r\n <div class=\"or-divider\">or use template for custom format</div>\r\n <mat-form-field appearance=\"outline\" class=\"full-width\">\r\n <mat-label>Template</mat-label>\r\n <input\r\n matInput\r\n [(ngModel)]=\"step.template\"\r\n (ngModelChange)=\"onConfigChange()\"\r\n [placeholder]=\"'{0} - {1}'\"\r\n />\r\n <mat-hint>Overrides separator if set</mat-hint>\r\n </mat-form-field>\r\n </div>\r\n }\r\n\r\n @case ('substring') {\r\n <div class=\"config-section config-row\">\r\n <mat-form-field appearance=\"outline\">\r\n <mat-label>Start Index</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [(ngModel)]=\"step.startIndex\"\r\n (ngModelChange)=\"onConfigChange()\"\r\n min=\"0\"\r\n />\r\n </mat-form-field>\r\n <mat-form-field appearance=\"outline\">\r\n <mat-label>End Index</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [(ngModel)]=\"step.endIndex\"\r\n (ngModelChange)=\"onConfigChange()\"\r\n />\r\n </mat-form-field>\r\n </div>\r\n }\r\n\r\n @case ('replace') {\r\n <div class=\"config-section\">\r\n <mat-form-field appearance=\"outline\" class=\"full-width\">\r\n <mat-label>Search For</mat-label>\r\n <input\r\n matInput\r\n [(ngModel)]=\"step.searchValue\"\r\n (ngModelChange)=\"onConfigChange()\"\r\n />\r\n </mat-form-field>\r\n <mat-form-field appearance=\"outline\" class=\"full-width\">\r\n <mat-label>Replace With</mat-label>\r\n <input\r\n matInput\r\n [(ngModel)]=\"step.replaceValue\"\r\n (ngModelChange)=\"onConfigChange()\"\r\n />\r\n </mat-form-field>\r\n </div>\r\n }\r\n\r\n @case ('dateFormat') {\r\n <div class=\"config-section\">\r\n <mat-form-field appearance=\"outline\" class=\"full-width\">\r\n <mat-label>Output Format</mat-label>\r\n <input\r\n matInput\r\n [(ngModel)]=\"step.outputFormat\"\r\n (ngModelChange)=\"onConfigChange()\"\r\n placeholder=\"YYYY-MM-DD\"\r\n />\r\n <mat-hint>YYYY, MM, DD, HH, mm, ss</mat-hint>\r\n </mat-form-field>\r\n </div>\r\n }\r\n\r\n @case ('numberFormat') {\r\n <div class=\"config-section\">\r\n <mat-form-field appearance=\"outline\" class=\"full-width\">\r\n <mat-label>Decimal Places</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [(ngModel)]=\"step.decimalPlaces\"\r\n (ngModelChange)=\"onConfigChange()\"\r\n min=\"0\"\r\n />\r\n </mat-form-field>\r\n <div class=\"config-row\">\r\n <mat-form-field appearance=\"outline\">\r\n <mat-label>Prefix</mat-label>\r\n <input\r\n matInput\r\n [(ngModel)]=\"step.prefix\"\r\n (ngModelChange)=\"onConfigChange()\"\r\n placeholder=\"$\"\r\n />\r\n </mat-form-field>\r\n <mat-form-field appearance=\"outline\">\r\n <mat-label>Suffix</mat-label>\r\n <input\r\n matInput\r\n [(ngModel)]=\"step.suffix\"\r\n (ngModelChange)=\"onConfigChange()\"\r\n />\r\n </mat-form-field>\r\n </div>\r\n </div>\r\n }\r\n\r\n @case ('mask') {\r\n <div class=\"config-section\">\r\n <mat-form-field appearance=\"outline\" class=\"full-width\">\r\n <mat-label>Pattern</mat-label>\r\n <input\r\n matInput\r\n [(ngModel)]=\"step.pattern\"\r\n (ngModelChange)=\"onConfigChange()\"\r\n placeholder=\"(###) ###-####\"\r\n />\r\n <mat-hint># = character from input</mat-hint>\r\n </mat-form-field>\r\n </div>\r\n }\r\n\r\n @case ('template') {\r\n <div class=\"config-section\">\r\n <mat-form-field appearance=\"outline\" class=\"full-width\">\r\n <mat-label>Template Expression</mat-label>\r\n <textarea\r\n matInput\r\n [(ngModel)]=\"step.template\"\r\n (ngModelChange)=\"onConfigChange()\"\r\n rows=\"3\"\r\n [placeholder]=\"'Hello {0}, your ID is {1}'\"\r\n ></textarea>\r\n @if (index === 0) {\r\n <mat-hint>Use {{ '{' }}0{{ '}' }}, {{ '{' }}1{{ '}' }}, etc. for source fields</mat-hint>\r\n } @else {\r\n <mat-hint>Use {{ '{' }}0{{ '}' }} for the value from previous step</mat-hint>\r\n }\r\n </mat-form-field>\r\n </div>\r\n }\r\n }\r\n\r\n <!-- Condition Section -->\r\n <div class=\"condition-section\">\r\n <mat-checkbox\r\n [checked]=\"hasCondition(step)\"\r\n (change)=\"toggleCondition(step, $event.checked)\"\r\n class=\"condition-checkbox\"\r\n >\r\n Apply conditionally\r\n </mat-checkbox>\r\n\r\n @if (hasCondition(step)) {\r\n <div class=\"condition-content\">\r\n <condition-builder\r\n [condition]=\"step.condition?.root || null\"\r\n [compact]=\"true\"\r\n (conditionChange)=\"onConditionChange(step, $event)\"\r\n ></condition-builder>\r\n </div>\r\n }\r\n </div>\r\n</ng-template>\r\n", styles: ["@charset \"UTF-8\";.popover-backdrop{position:fixed;inset:0;background:#0000004d;z-index:999}.transformation-popover{position:fixed;z-index:1000;width:420px;background:#fff;border-radius:12px;box-shadow:0 10px 40px #0003;transform:translate(-50%,-50%);animation:popoverIn .2s ease-out}@keyframes popoverIn{0%{opacity:0;transform:translate(-50%,-50%) scale(.95)}to{opacity:1;transform:translate(-50%,-50%) scale(1)}}.popover-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid #e2e8f0;background:#f8fafc;border-radius:12px 12px 0 0}.popover-title{font-size:16px;font-weight:600;color:#1e293b}.close-btn{width:32px;height:32px;line-height:32px}.close-btn mat-icon{font-size:20px;width:20px;height:20px}.popover-content{padding:20px;max-height:500px;overflow-y:auto}.mapping-info{background:#f1f5f9;border-radius:8px;padding:12px 16px;margin-bottom:16px}.info-row{display:flex;align-items:center;gap:8px}.info-row+.info-row{margin-top:8px}.info-label{font-size:12px;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:.5px;width:60px}.info-value{font-size:14px;color:#1e293b;font-weight:500}.full-width{width:100%}.config-section{margin-top:12px}.or-divider{text-align:center;font-size:11px;color:#94a3b8;margin:8px 0}.condition-section{margin-top:16px;padding-top:12px;border-top:1px dashed #e2e8f0}.condition-checkbox{font-size:13px;color:#64748b}.condition-content{margin-top:12px}.config-row{display:flex;gap:12px}.config-row mat-form-field{flex:1}.preview-section{margin-top:16px;padding:12px 16px;background:linear-gradient(135deg,#eff6ff,#f0fdf4);border-radius:8px;border:1px solid #e0e7ff}.preview-label{font-size:11px;font-weight:600;color:#6366f1;text-transform:uppercase;letter-spacing:.5px;display:block;margin-bottom:6px}.preview-value{font-size:14px;color:#1e293b;font-family:Monaco,Menlo,monospace;word-break:break-all;min-height:20px}.steps-container{display:flex;flex-direction:column;gap:12px}.step-card{background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;overflow:hidden;transition:all .2s ease}.step-card.expanded{border-color:#6366f1;border-width:2px;background:#fff}.step-collapsed{display:grid;grid-template-columns:auto 1fr;gap:0 12px;padding:12px;cursor:pointer;transition:background .15s ease}.step-collapsed:hover{background:#f1f5f9}.step-collapsed .drag-handle{grid-row:span 2;align-self:center}.step-collapsed-header{display:flex;align-items:center;flex-wrap:wrap;gap:4px}.step-collapsed-header .step-title-row{display:flex;align-items:center;gap:8px;flex:1}.step-collapsed-header .step-number{flex-shrink:0}.condition-badge{display:inline-flex;align-items:center;gap:2px;padding:2px 6px;background:#fef3c7;border:1px solid #f59e0b;border-radius:4px;font-size:10px;font-weight:600;color:#b45309;cursor:help}.condition-badge mat-icon{font-size:12px;width:12px;height:12px}.step-collapsed-actions{display:flex;align-items:center;gap:4px}.step-collapsed-preview{display:flex;align-items:center;gap:8px;margin-top:8px;font-size:12px;font-family:Monaco,Menlo,monospace}.step-input{color:#64748b;max-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.arrow-icon{font-size:14px;width:14px;height:14px;color:#6366f1}.step-output{color:#059669;font-weight:500;max-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.step-expanded{display:grid;grid-template-columns:auto 1fr;gap:0 12px;padding:12px}.step-expanded .drag-handle{grid-row:1;align-self:center}.step-expanded .step-header,.step-expanded .step-content,.step-expanded .step-preview{grid-column:2}.step-header{display:flex;align-items:center;margin-bottom:12px}.step-header .step-number{flex:1}.step-header-actions{display:flex;align-items:center;gap:4px}.step-number{font-size:12px;font-weight:600;color:#6366f1;text-transform:uppercase;letter-spacing:.5px}.expand-btn,.collapse-btn{width:28px;height:28px;line-height:28px}.expand-btn mat-icon,.collapse-btn mat-icon{font-size:18px;width:18px;height:18px;color:#64748b}.expand-btn:hover mat-icon,.collapse-btn:hover mat-icon{color:#6366f1}.remove-step-btn{width:28px;height:28px;line-height:28px}.remove-step-btn mat-icon{font-size:18px;width:18px;height:18px;color:#94a3b8}.remove-step-btn:hover mat-icon{color:#ef4444}.step-preview{display:flex;align-items:center;gap:8px;margin-top:12px;padding:8px 12px;background:#f1f5f9;border-radius:6px}.preview-arrow{font-size:16px;width:16px;height:16px;color:#6366f1}.step-preview-value{font-size:13px;color:#475569;font-family:Monaco,Menlo,monospace;word-break:break-all;flex:1}.add-step-section{padding:12px 20px;border-top:1px solid #e2e8f0;background:#fafafa}.add-step-btn{width:100%;border-style:dashed;color:#64748b}.add-step-btn mat-icon{font-size:18px;width:18px;height:18px;margin-right:4px}.add-step-btn:hover{border-color:#6366f1;color:#6366f1;background:#f5f3ff}.final-preview{margin-top:16px;background:linear-gradient(135deg,#ecfdf5,#d1fae5);border:2px solid #10b981;border-radius:8px;position:relative}.final-preview .preview-label{color:#059669;font-size:12px;font-weight:700}.final-preview .preview-value{color:#065f46;font-weight:600}.final-preview:before{content:\"\\2713\";position:absolute;right:12px;top:10px;color:#10b981;font-size:16px;font-weight:700}.popover-actions{display:flex;align-items:center;gap:8px;padding:16px 20px;border-top:1px solid #e2e8f0;background:#f8fafc;border-radius:0 0 12px 12px}.action-spacer{flex:1}::ng-deep .transformation-popover .mat-mdc-form-field{font-size:14px}::ng-deep .transformation-popover .mat-mdc-text-field-wrapper{background:#fff}::ng-deep .transformation-popover .mat-mdc-form-field-subscript-wrapper{font-size:11px}.drag-handle{cursor:grab;color:#94a3b8;font-size:20px;width:20px;height:20px;flex-shrink:0}.drag-handle:hover{color:#6366f1}.drag-handle:active{cursor:grabbing}.step-card.cdk-drag-preview{box-shadow:0 5px 20px #00000040;border-radius:8px;background:#fff}.step-card.cdk-drag-placeholder{opacity:.4;background:#e2e8f0;border:2px dashed #94a3b8}.step-card.cdk-drag-animating{transition:transform .2s ease}.steps-container.cdk-drop-list-dragging .step-card:not(.cdk-drag-placeholder){transition:transform .2s ease}\n"] }]
|
|
1122
1904
|
}], propDecorators: { mapping: [{
|
|
1123
1905
|
type: Input
|
|
1124
1906
|
}], position: [{
|
|
@@ -1332,7 +2114,7 @@ class ArrayFilterModalComponent {
|
|
|
1332
2114
|
}
|
|
1333
2115
|
}
|
|
1334
2116
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ArrayFilterModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1335
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: ArrayFilterModalComponent, isStandalone: true, selector: "array-filter-modal", inputs: { arrayMapping: "arrayMapping" }, outputs: { save: "save", close: "close" }, ngImport: i0, template: "<div class=\"modal-backdrop\" (click)=\"onBackdropClick($event)\">\n <div class=\"filter-modal\">\n <div class=\"modal-header\">\n <div class=\"header-title\">\n <mat-icon>filter_list</mat-icon>\n <span>Array Filter</span>\n </div>\n <span class=\"array-path\">{{ arrayMapping.sourceArray.name }}[] → {{ arrayMapping.targetArray.name }}[]</span>\n <button mat-icon-button class=\"close-btn\" (click)=\"onClose()\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n\n <div class=\"modal-body\">\n <!-- Filter mode selection -->\n <div class=\"filter-mode\">\n <mat-radio-group [value]=\"filterEnabled()\" (change)=\"filterEnabled.set($event.value)\">\n <mat-radio-button [value]=\"false\" class=\"mode-option\">\n <div class=\"mode-content\">\n <mat-icon>select_all</mat-icon>\n <div class=\"mode-text\">\n <span class=\"mode-label\">No filter</span>\n <span class=\"mode-desc\">Map all records from source to target</span>\n </div>\n </div>\n </mat-radio-button>\n <mat-radio-button [value]=\"true\" class=\"mode-option\">\n <div class=\"mode-content\">\n <mat-icon>filter_alt</mat-icon>\n <div class=\"mode-text\">\n <span class=\"mode-label\">Filter records</span>\n <span class=\"mode-desc\">Only map records matching conditions</span>\n </div>\n </div>\n </mat-radio-button>\n </mat-radio-group>\n </div>\n\n <!-- Conditions builder (only when filter enabled) -->\n @if (filterEnabled()) {\n <mat-divider></mat-divider>\n\n <div class=\"conditions-section\">\n <!-- Root group -->\n <ng-container *ngTemplateOutlet=\"groupTemplate; context: { group: rootGroup(), isRoot: true }\"></ng-container>\n </div>\n }\n </div>\n\n <div class=\"modal-footer\">\n <button mat-button (click)=\"onClose()\">Cancel</button>\n <button mat-flat-button color=\"primary\" (click)=\"onSave()\">\n Apply\n </button>\n </div>\n </div>\n</div>\n\n<!-- Recursive group template -->\n<ng-template #groupTemplate let-group=\"group\" let-isRoot=\"isRoot\" let-parentGroup=\"parentGroup\">\n <div class=\"filter-group\" [class.root-group]=\"isRoot\" [class.nested-group]=\"!isRoot\">\n <!-- Group header with logic toggle -->\n <div class=\"group-header\">\n <div class=\"logic-toggle\">\n <span class=\"logic-label\">Match</span>\n <mat-radio-group [value]=\"group.logic\" (change)=\"onLogicChange(group, $event.value)\">\n <mat-radio-button value=\"and\">ALL (AND)</mat-radio-button>\n <mat-radio-button value=\"or\">ANY (OR)</mat-radio-button>\n </mat-radio-group>\n </div>\n @if (!isRoot) {\n <button mat-icon-button class=\"remove-group-btn\" matTooltip=\"Remove group\" (click)=\"removeItem(parentGroup, group.id)\">\n <mat-icon>close</mat-icon>\n </button>\n }\n </div>\n\n <!-- Group children -->\n <div class=\"group-children\">\n @for (item of group.children; track item.id; let i = $index) {\n <!-- Logic connector between items -->\n @if (i > 0) {\n <div class=\"logic-connector\">\n <span class=\"logic-badge\" [class.and]=\"group.logic === 'and'\" [class.or]=\"group.logic === 'or'\">\n {{ group.logic | uppercase }}\n </span>\n </div>\n }\n\n @if (isCondition(item)) {\n <!-- Condition row -->\n <div class=\"condition-row\">\n <div class=\"condition-inputs\">\n <!-- Field selector -->\n <mat-form-field appearance=\"outline\" class=\"field-select\">\n <mat-label>Field</mat-label>\n <mat-select [value]=\"item.field\" (selectionChange)=\"onFieldChange(item, $event.value)\">\n @for (field of availableFields(); track field.path) {\n <mat-option [value]=\"field.path\">\n {{ field.name }}\n <span class=\"field-type\">({{ field.type }})</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <!-- Operator selector -->\n <mat-form-field appearance=\"outline\" class=\"operator-select\">\n <mat-label>Operator</mat-label>\n <mat-select [value]=\"item.operator\" (selectionChange)=\"onOperatorChange(item, $event.value)\">\n @for (op of getOperatorsForField(item.field); track op.value) {\n <mat-option [value]=\"op.value\">{{ op.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <!-- Value input (only if operator needs value) -->\n @if (operatorNeedsValue(item.operator)) {\n @if (item.valueType === 'boolean') {\n <mat-slide-toggle\n [checked]=\"item.value === true\"\n (change)=\"onValueChange(item, $event.checked)\"\n class=\"bool-toggle\"\n >\n {{ item.value ? 'true' : 'false' }}\n </mat-slide-toggle>\n } @else if (item.valueType === 'number') {\n <mat-form-field appearance=\"outline\" class=\"value-input\">\n <mat-label>Value</mat-label>\n <input\n matInput\n type=\"number\"\n [value]=\"item.value\"\n (input)=\"onValueChange(item, $any($event.target).value)\"\n />\n </mat-form-field>\n } @else {\n <mat-form-field appearance=\"outline\" class=\"value-input\">\n <mat-label>Value</mat-label>\n <input\n matInput\n type=\"text\"\n [value]=\"item.value\"\n (input)=\"onValueChange(item, $any($event.target).value)\"\n />\n </mat-form-field>\n }\n }\n\n <!-- Remove condition button -->\n <button mat-icon-button class=\"remove-btn\" matTooltip=\"Remove condition\" (click)=\"removeItem(group, item.id)\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </div>\n } @else if (isGroup(item)) {\n <!-- Nested group -->\n <ng-container *ngTemplateOutlet=\"groupTemplate; context: { group: item, isRoot: false, parentGroup: group }\"></ng-container>\n }\n }\n\n <!-- Empty state -->\n @if (group.children.length === 0) {\n <div class=\"empty-group\">\n <mat-icon>info_outline</mat-icon>\n <span>No conditions. Add a condition or group.</span>\n </div>\n }\n </div>\n\n <!-- Group actions -->\n <div class=\"group-actions\">\n <button mat-stroked-button class=\"add-condition-btn\" (click)=\"addCondition(group)\">\n <mat-icon>add</mat-icon>\n Add Condition\n </button>\n <button mat-stroked-button class=\"add-group-btn\" (click)=\"addGroup(group)\">\n <mat-icon>folder_open</mat-icon>\n Add Group\n </button>\n </div>\n </div>\n</ng-template>\n", styles: [".modal-backdrop{position:fixed;inset:0;background:#0000004d;z-index:1000;display:flex;align-items:center;justify-content:center;padding:20px}.filter-modal{position:relative;background:#fff;border-radius:12px;box-shadow:0 8px 32px #00000026;width:600px;max-width:100%;max-height:calc(100vh - 40px);display:flex;flex-direction:column;overflow:hidden}.modal-header{display:flex;align-items:center;gap:12px;padding:16px 20px;background:linear-gradient(135deg,#f59e0b,#d97706);color:#fff;flex-shrink:0}.modal-header .header-title{display:flex;align-items:center;gap:8px;font-size:16px;font-weight:600}.modal-header .array-path{flex:1;font-size:13px;opacity:.9;text-align:right;margin-right:8px}.modal-header .close-btn{color:#fff;opacity:.9}.modal-header .close-btn:hover{opacity:1}.modal-body{flex:1;overflow-y:auto;padding:20px;min-height:0}.filter-mode mat-radio-group{display:flex;flex-direction:column;gap:12px}.filter-mode .mode-option ::ng-deep .mdc-form-field{align-items:flex-start}.filter-mode .mode-option ::ng-deep .mdc-radio{margin-top:4px}.filter-mode .mode-content{display:flex;align-items:flex-start;gap:12px;padding:12px 16px;border-radius:8px;background:#f8fafc;border:1px solid #e2e8f0;transition:all .2s ease;cursor:pointer}.filter-mode .mode-content:hover{background:#f1f5f9;border-color:#cbd5e1}.filter-mode .mode-content mat-icon{color:#64748b;margin-top:2px}.filter-mode .mode-text{display:flex;flex-direction:column;gap:2px}.filter-mode .mode-label{font-size:14px;font-weight:500;color:#1e293b}.filter-mode .mode-desc{font-size:12px;color:#64748b}.filter-mode mat-radio-button.mat-mdc-radio-checked .mode-content{background:#fffbeb;border-color:#f59e0b}.filter-mode mat-radio-button.mat-mdc-radio-checked .mode-content mat-icon{color:#f59e0b}mat-divider{margin:20px 0}.conditions-section{display:flex;flex-direction:column;gap:16px}.filter-group{border-radius:8px}.filter-group.root-group{background:#f8fafc;border:1px solid #e2e8f0;padding:16px}.filter-group.nested-group{background:#fff;border:2px dashed #cbd5e1;padding:12px;margin-top:8px}.group-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid #e2e8f0}.logic-toggle{display:flex;align-items:center;gap:12px}.logic-toggle .logic-label{font-size:13px;font-weight:500;color:#475569}.logic-toggle mat-radio-group{display:flex;gap:12px}.logic-toggle mat-radio-button{font-size:12px}.logic-toggle mat-radio-button ::ng-deep .mdc-label{font-size:12px}.remove-group-btn{color:#94a3b8}.remove-group-btn:hover{color:#ef4444}.group-children{display:flex;flex-direction:column}.logic-connector{display:flex;align-items:center;justify-content:center;padding:8px 0}.logic-connector .logic-badge{font-size:10px;font-weight:700;padding:3px 10px;border-radius:12px;letter-spacing:.5px}.logic-connector .logic-badge.and{background:#dbeafe;color:#1d4ed8}.logic-connector .logic-badge.or{background:#fef3c7;color:#b45309}.condition-row .condition-inputs{display:flex;align-items:flex-start;gap:8px;padding:12px;background:#fff;border:1px solid #e2e8f0;border-radius:8px}.condition-row .condition-inputs .field-select{flex:1;min-width:120px}.condition-row .condition-inputs .operator-select{flex:1;min-width:130px}.condition-row .condition-inputs .value-input{flex:1;min-width:100px}.condition-row .condition-inputs .bool-toggle{padding-top:12px;min-width:80px}.condition-row .condition-inputs .remove-btn{color:#94a3b8;align-self:center}.condition-row .condition-inputs .remove-btn:hover{color:#ef4444}.condition-row .condition-inputs mat-form-field ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.nested-group .condition-row .condition-inputs{background:#f8fafc}.field-type{font-size:11px;color:#94a3b8;margin-left:4px}.empty-group{display:flex;align-items:center;gap:8px;padding:16px;background:#fef3c7;border-radius:8px;color:#92400e;font-size:13px}.empty-group mat-icon{font-size:20px;width:20px;height:20px}.group-actions{display:flex;gap:8px;margin-top:12px;padding-top:12px;border-top:1px solid #e2e8f0}.add-condition-btn{color:#f59e0b;border-color:#f59e0b;font-size:12px}.add-condition-btn mat-icon{font-size:16px;width:16px;height:16px}.add-group-btn{color:#6366f1;border-color:#6366f1;font-size:12px}.add-group-btn mat-icon{font-size:16px;width:16px;height:16px}.modal-footer{display:flex;justify-content:flex-end;gap:8px;padding:16px 20px;border-top:1px solid #e2e8f0;background:#f8fafc;flex-shrink:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2$1.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "component", type: i5.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i5.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5$1.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "ngmodule", type: MatRadioModule }, { kind: "directive", type: i6.MatRadioGroup, selector: "mat-radio-group", inputs: ["color", "name", "labelPosition", "value", "selected", "disabled", "required", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioGroup"] }, { kind: "component", type: i6.MatRadioButton, selector: "mat-radio-button", inputs: ["id", "name", "aria-label", "aria-labelledby", "aria-describedby", "disableRipple", "tabIndex", "checked", "value", "labelPosition", "disabled", "required", "color", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioButton"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i7.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i8.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i9.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "pipe", type: i1.UpperCasePipe, name: "uppercase" }] });
|
|
2117
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: ArrayFilterModalComponent, isStandalone: true, selector: "array-filter-modal", inputs: { arrayMapping: "arrayMapping" }, outputs: { save: "save", close: "close" }, ngImport: i0, template: "<div class=\"modal-backdrop\" (click)=\"onBackdropClick($event)\">\n <div class=\"filter-modal\">\n <div class=\"modal-header\">\n <div class=\"header-title\">\n <mat-icon>filter_list</mat-icon>\n <span>Array Filter</span>\n </div>\n <span class=\"array-path\">{{ arrayMapping.sourceArray.name }}[] → {{ arrayMapping.targetArray.name }}[]</span>\n <button mat-icon-button class=\"close-btn\" (click)=\"onClose()\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n\n <div class=\"modal-body\">\n <!-- Filter mode selection -->\n <div class=\"filter-mode\">\n <mat-radio-group [value]=\"filterEnabled()\" (change)=\"filterEnabled.set($event.value)\">\n <mat-radio-button [value]=\"false\" class=\"mode-option\">\n <div class=\"mode-content\">\n <mat-icon>select_all</mat-icon>\n <div class=\"mode-text\">\n <span class=\"mode-label\">No filter</span>\n <span class=\"mode-desc\">Map all records from source to target</span>\n </div>\n </div>\n </mat-radio-button>\n <mat-radio-button [value]=\"true\" class=\"mode-option\">\n <div class=\"mode-content\">\n <mat-icon>filter_alt</mat-icon>\n <div class=\"mode-text\">\n <span class=\"mode-label\">Filter records</span>\n <span class=\"mode-desc\">Only map records matching conditions</span>\n </div>\n </div>\n </mat-radio-button>\n </mat-radio-group>\n </div>\n\n <!-- Conditions builder (only when filter enabled) -->\n @if (filterEnabled()) {\n <mat-divider></mat-divider>\n\n <div class=\"conditions-section\">\n <!-- Root group -->\n <ng-container *ngTemplateOutlet=\"groupTemplate; context: { group: rootGroup(), isRoot: true }\"></ng-container>\n </div>\n }\n </div>\n\n <div class=\"modal-footer\">\n <button mat-button (click)=\"onClose()\">Cancel</button>\n <button mat-flat-button color=\"primary\" (click)=\"onSave()\">\n Apply\n </button>\n </div>\n </div>\n</div>\n\n<!-- Recursive group template -->\n<ng-template #groupTemplate let-group=\"group\" let-isRoot=\"isRoot\" let-parentGroup=\"parentGroup\">\n <div class=\"filter-group\" [class.root-group]=\"isRoot\" [class.nested-group]=\"!isRoot\">\n <!-- Group header with logic toggle -->\n <div class=\"group-header\">\n <div class=\"logic-toggle\">\n <span class=\"logic-label\">Match</span>\n <mat-radio-group [value]=\"group.logic\" (change)=\"onLogicChange(group, $event.value)\">\n <mat-radio-button value=\"and\">ALL (AND)</mat-radio-button>\n <mat-radio-button value=\"or\">ANY (OR)</mat-radio-button>\n </mat-radio-group>\n </div>\n @if (!isRoot) {\n <button mat-icon-button class=\"remove-group-btn\" matTooltip=\"Remove group\" (click)=\"removeItem(parentGroup, group.id)\">\n <mat-icon>close</mat-icon>\n </button>\n }\n </div>\n\n <!-- Group children -->\n <div class=\"group-children\">\n @for (item of group.children; track item.id; let i = $index) {\n <!-- Logic connector between items -->\n @if (i > 0) {\n <div class=\"logic-connector\">\n <span class=\"logic-badge\" [class.and]=\"group.logic === 'and'\" [class.or]=\"group.logic === 'or'\">\n {{ group.logic | uppercase }}\n </span>\n </div>\n }\n\n @if (isCondition(item)) {\n <!-- Condition row -->\n <div class=\"condition-row\">\n <div class=\"condition-inputs\">\n <!-- Field selector -->\n <mat-form-field appearance=\"outline\" class=\"field-select\">\n <mat-label>Field</mat-label>\n <mat-select [value]=\"item.field\" (selectionChange)=\"onFieldChange(item, $event.value)\">\n @for (field of availableFields(); track field.path) {\n <mat-option [value]=\"field.path\">\n {{ field.name }}\n <span class=\"field-type\">({{ field.type }})</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <!-- Operator selector -->\n <mat-form-field appearance=\"outline\" class=\"operator-select\">\n <mat-label>Operator</mat-label>\n <mat-select [value]=\"item.operator\" (selectionChange)=\"onOperatorChange(item, $event.value)\">\n @for (op of getOperatorsForField(item.field); track op.value) {\n <mat-option [value]=\"op.value\">{{ op.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <!-- Value input (only if operator needs value) -->\n @if (operatorNeedsValue(item.operator)) {\n @if (item.valueType === 'boolean') {\n <mat-slide-toggle\n [checked]=\"item.value === true\"\n (change)=\"onValueChange(item, $event.checked)\"\n class=\"bool-toggle\"\n >\n {{ item.value ? 'true' : 'false' }}\n </mat-slide-toggle>\n } @else if (item.valueType === 'number') {\n <mat-form-field appearance=\"outline\" class=\"value-input\">\n <mat-label>Value</mat-label>\n <input\n matInput\n type=\"number\"\n [value]=\"item.value\"\n (input)=\"onValueChange(item, $any($event.target).value)\"\n />\n </mat-form-field>\n } @else {\n <mat-form-field appearance=\"outline\" class=\"value-input\">\n <mat-label>Value</mat-label>\n <input\n matInput\n type=\"text\"\n [value]=\"item.value\"\n (input)=\"onValueChange(item, $any($event.target).value)\"\n />\n </mat-form-field>\n }\n }\n\n <!-- Remove condition button -->\n <button mat-icon-button class=\"remove-btn\" matTooltip=\"Remove condition\" (click)=\"removeItem(group, item.id)\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </div>\n } @else if (isGroup(item)) {\n <!-- Nested group -->\n <ng-container *ngTemplateOutlet=\"groupTemplate; context: { group: item, isRoot: false, parentGroup: group }\"></ng-container>\n }\n }\n\n <!-- Empty state -->\n @if (group.children.length === 0) {\n <div class=\"empty-group\">\n <mat-icon>info_outline</mat-icon>\n <span>No conditions. Add a condition or group.</span>\n </div>\n }\n </div>\n\n <!-- Group actions -->\n <div class=\"group-actions\">\n <button mat-stroked-button class=\"add-condition-btn\" (click)=\"addCondition(group)\">\n <mat-icon>add</mat-icon>\n Add Condition\n </button>\n <button mat-stroked-button class=\"add-group-btn\" (click)=\"addGroup(group)\">\n <mat-icon>folder_open</mat-icon>\n Add Group\n </button>\n </div>\n </div>\n</ng-template>\n", styles: [".modal-backdrop{position:fixed;inset:0;background:#0000004d;z-index:1000;display:flex;align-items:center;justify-content:center;padding:20px}.filter-modal{position:relative;background:#fff;border-radius:12px;box-shadow:0 8px 32px #00000026;width:600px;max-width:100%;max-height:calc(100vh - 40px);display:flex;flex-direction:column;overflow:hidden}.modal-header{display:flex;align-items:center;gap:12px;padding:16px 20px;background:linear-gradient(135deg,#f59e0b,#d97706);color:#fff;flex-shrink:0}.modal-header .header-title{display:flex;align-items:center;gap:8px;font-size:16px;font-weight:600}.modal-header .array-path{flex:1;font-size:13px;opacity:.9;text-align:right;margin-right:8px}.modal-header .close-btn{color:#fff;opacity:.9}.modal-header .close-btn:hover{opacity:1}.modal-body{flex:1;overflow-y:auto;padding:20px;min-height:0}.filter-mode mat-radio-group{display:flex;flex-direction:column;gap:12px}.filter-mode .mode-option ::ng-deep .mdc-form-field{align-items:flex-start}.filter-mode .mode-option ::ng-deep .mdc-radio{margin-top:4px}.filter-mode .mode-content{display:flex;align-items:flex-start;gap:12px;padding:12px 16px;border-radius:8px;background:#f8fafc;border:1px solid #e2e8f0;transition:all .2s ease;cursor:pointer}.filter-mode .mode-content:hover{background:#f1f5f9;border-color:#cbd5e1}.filter-mode .mode-content mat-icon{color:#64748b;margin-top:2px}.filter-mode .mode-text{display:flex;flex-direction:column;gap:2px}.filter-mode .mode-label{font-size:14px;font-weight:500;color:#1e293b}.filter-mode .mode-desc{font-size:12px;color:#64748b}.filter-mode mat-radio-button.mat-mdc-radio-checked .mode-content{background:#fffbeb;border-color:#f59e0b}.filter-mode mat-radio-button.mat-mdc-radio-checked .mode-content mat-icon{color:#f59e0b}mat-divider{margin:20px 0}.conditions-section{display:flex;flex-direction:column;gap:16px}.filter-group{border-radius:8px}.filter-group.root-group{background:#f8fafc;border:1px solid #e2e8f0;padding:16px}.filter-group.nested-group{background:#fff;border:2px dashed #cbd5e1;padding:12px;margin-top:8px}.group-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid #e2e8f0}.logic-toggle{display:flex;align-items:center;gap:12px}.logic-toggle .logic-label{font-size:13px;font-weight:500;color:#475569}.logic-toggle mat-radio-group{display:flex;gap:12px}.logic-toggle mat-radio-button{font-size:12px}.logic-toggle mat-radio-button ::ng-deep .mdc-label{font-size:12px}.remove-group-btn{color:#94a3b8}.remove-group-btn:hover{color:#ef4444}.group-children{display:flex;flex-direction:column}.logic-connector{display:flex;align-items:center;justify-content:center;padding:8px 0}.logic-connector .logic-badge{font-size:10px;font-weight:700;padding:3px 10px;border-radius:12px;letter-spacing:.5px}.logic-connector .logic-badge.and{background:#dbeafe;color:#1d4ed8}.logic-connector .logic-badge.or{background:#fef3c7;color:#b45309}.condition-row .condition-inputs{display:flex;align-items:flex-start;gap:8px;padding:12px;background:#fff;border:1px solid #e2e8f0;border-radius:8px}.condition-row .condition-inputs .field-select{flex:1;min-width:120px}.condition-row .condition-inputs .operator-select{flex:1;min-width:130px}.condition-row .condition-inputs .value-input{flex:1;min-width:100px}.condition-row .condition-inputs .bool-toggle{padding-top:12px;min-width:80px}.condition-row .condition-inputs .remove-btn{color:#94a3b8;align-self:center}.condition-row .condition-inputs .remove-btn:hover{color:#ef4444}.condition-row .condition-inputs mat-form-field ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.nested-group .condition-row .condition-inputs{background:#f8fafc}.field-type{font-size:11px;color:#94a3b8;margin-left:4px}.empty-group{display:flex;align-items:center;gap:8px;padding:16px;background:#fef3c7;border-radius:8px;color:#92400e;font-size:13px}.empty-group mat-icon{font-size:20px;width:20px;height:20px}.group-actions{display:flex;gap:8px;margin-top:12px;padding-top:12px;border-top:1px solid #e2e8f0}.add-condition-btn{color:#f59e0b;border-color:#f59e0b;font-size:12px}.add-condition-btn mat-icon{font-size:16px;width:16px;height:16px}.add-group-btn{color:#6366f1;border-color:#6366f1;font-size:12px}.add-group-btn mat-icon{font-size:16px;width:16px;height:16px}.modal-footer{display:flex;justify-content:flex-end;gap:8px;padding:16px 20px;border-top:1px solid #e2e8f0;background:#f8fafc;flex-shrink:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "component", type: i4.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i4.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "ngmodule", type: MatRadioModule }, { kind: "directive", type: i6.MatRadioGroup, selector: "mat-radio-group", inputs: ["color", "name", "labelPosition", "value", "selected", "disabled", "required", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioGroup"] }, { kind: "component", type: i6.MatRadioButton, selector: "mat-radio-button", inputs: ["id", "name", "aria-label", "aria-labelledby", "aria-describedby", "disableRipple", "tabIndex", "checked", "value", "labelPosition", "disabled", "required", "color", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioButton"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i7$1.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i9.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "pipe", type: i1.UpperCasePipe, name: "uppercase" }] });
|
|
1336
2118
|
}
|
|
1337
2119
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ArrayFilterModalComponent, decorators: [{
|
|
1338
2120
|
type: Component,
|
|
@@ -1536,7 +2318,7 @@ class ArraySelectorModalComponent {
|
|
|
1536
2318
|
}
|
|
1537
2319
|
}
|
|
1538
2320
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ArraySelectorModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1539
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: ArraySelectorModalComponent, isStandalone: true, selector: "array-selector-modal", inputs: { mapping: "mapping" }, outputs: { save: "save", close: "close" }, ngImport: i0, template: "<div class=\"modal-backdrop\" (click)=\"onBackdropClick($event)\">\n <div class=\"selector-modal\">\n <div class=\"modal-header\">\n <div class=\"header-title\">\n <mat-icon>swap_horiz</mat-icon>\n <span>Array to Object</span>\n </div>\n <span class=\"mapping-path\">{{ mapping.sourceArray.name }}[] → {{ mapping.targetObject.name }}</span>\n <button mat-icon-button class=\"close-btn\" (click)=\"onClose()\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n\n <div class=\"modal-body\">\n <p class=\"description\">\n Select which item from the array should be mapped to the target object.\n </p>\n\n <!-- Selection mode -->\n <div class=\"selection-mode\">\n <mat-radio-group [value]=\"selectionMode()\" (change)=\"selectionMode.set($event.value)\">\n <mat-radio-button value=\"first\" class=\"mode-option\">\n <div class=\"mode-content\">\n <mat-icon>first_page</mat-icon>\n <div class=\"mode-text\">\n <span class=\"mode-label\">First item</span>\n <span class=\"mode-desc\">Use the first item in the array</span>\n </div>\n </div>\n </mat-radio-button>\n\n <mat-radio-button value=\"last\" class=\"mode-option\">\n <div class=\"mode-content\">\n <mat-icon>last_page</mat-icon>\n <div class=\"mode-text\">\n <span class=\"mode-label\">Last item</span>\n <span class=\"mode-desc\">Use the last item in the array</span>\n </div>\n </div>\n </mat-radio-button>\n\n <mat-radio-button value=\"condition\" class=\"mode-option\">\n <div class=\"mode-content\">\n <mat-icon>filter_alt</mat-icon>\n <div class=\"mode-text\">\n <span class=\"mode-label\">First matching condition</span>\n <span class=\"mode-desc\">Use the first item that matches the condition</span>\n </div>\n </div>\n </mat-radio-button>\n </mat-radio-group>\n </div>\n\n <!-- Condition builder (only when condition mode selected) -->\n @if (selectionMode() === 'condition') {\n <mat-divider></mat-divider>\n\n <div class=\"conditions-section\">\n <ng-container *ngTemplateOutlet=\"groupTemplate; context: { group: conditionGroup(), isRoot: true }\"></ng-container>\n </div>\n }\n </div>\n\n <div class=\"modal-footer\">\n <button mat-button (click)=\"onClose()\">Cancel</button>\n <button mat-flat-button color=\"primary\" (click)=\"onSave()\">Apply</button>\n </div>\n </div>\n</div>\n\n<!-- Recursive group template -->\n<ng-template #groupTemplate let-group=\"group\" let-isRoot=\"isRoot\" let-parentGroup=\"parentGroup\">\n <div class=\"filter-group\" [class.root-group]=\"isRoot\" [class.nested-group]=\"!isRoot\">\n <div class=\"group-header\">\n <div class=\"logic-toggle\">\n <span class=\"logic-label\">Match</span>\n <mat-radio-group [value]=\"group.logic\" (change)=\"onLogicChange(group, $event.value)\">\n <mat-radio-button value=\"and\">ALL (AND)</mat-radio-button>\n <mat-radio-button value=\"or\">ANY (OR)</mat-radio-button>\n </mat-radio-group>\n </div>\n @if (!isRoot) {\n <button mat-icon-button class=\"remove-group-btn\" matTooltip=\"Remove group\" (click)=\"removeItem(parentGroup, group.id)\">\n <mat-icon>close</mat-icon>\n </button>\n }\n </div>\n\n <div class=\"group-children\">\n @for (item of group.children; track item.id; let i = $index) {\n @if (i > 0) {\n <div class=\"logic-connector\">\n <span class=\"logic-badge\" [class.and]=\"group.logic === 'and'\" [class.or]=\"group.logic === 'or'\">\n {{ group.logic | uppercase }}\n </span>\n </div>\n }\n\n @if (isCondition(item)) {\n <div class=\"condition-row\">\n <div class=\"condition-inputs\">\n <mat-form-field appearance=\"outline\" class=\"field-select\">\n <mat-label>Field</mat-label>\n <mat-select [value]=\"item.field\" (selectionChange)=\"onFieldChange(item, $event.value)\">\n @for (field of availableFields(); track field.path) {\n <mat-option [value]=\"field.path\">\n {{ field.name }}\n <span class=\"field-type\">({{ field.type }})</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field appearance=\"outline\" class=\"operator-select\">\n <mat-label>Operator</mat-label>\n <mat-select [value]=\"item.operator\" (selectionChange)=\"onOperatorChange(item, $event.value)\">\n @for (op of getOperatorsForField(item.field); track op.value) {\n <mat-option [value]=\"op.value\">{{ op.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (operatorNeedsValue(item.operator)) {\n @if (item.valueType === 'boolean') {\n <mat-slide-toggle\n [checked]=\"item.value === true\"\n (change)=\"onValueChange(item, $event.checked)\"\n class=\"bool-toggle\"\n >\n {{ item.value ? 'true' : 'false' }}\n </mat-slide-toggle>\n } @else if (item.valueType === 'number') {\n <mat-form-field appearance=\"outline\" class=\"value-input\">\n <mat-label>Value</mat-label>\n <input matInput type=\"number\" [value]=\"item.value\" (input)=\"onValueChange(item, $any($event.target).value)\" />\n </mat-form-field>\n } @else {\n <mat-form-field appearance=\"outline\" class=\"value-input\">\n <mat-label>Value</mat-label>\n <input matInput type=\"text\" [value]=\"item.value\" (input)=\"onValueChange(item, $any($event.target).value)\" />\n </mat-form-field>\n }\n }\n\n <button mat-icon-button class=\"remove-btn\" matTooltip=\"Remove condition\" (click)=\"removeItem(group, item.id)\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </div>\n } @else if (isGroup(item)) {\n <ng-container *ngTemplateOutlet=\"groupTemplate; context: { group: item, isRoot: false, parentGroup: group }\"></ng-container>\n }\n }\n\n @if (group.children.length === 0) {\n <div class=\"empty-group\">\n <mat-icon>info_outline</mat-icon>\n <span>No conditions. Add a condition to filter.</span>\n </div>\n }\n </div>\n\n <div class=\"group-actions\">\n <button mat-stroked-button class=\"add-condition-btn\" (click)=\"addCondition(group)\">\n <mat-icon>add</mat-icon>\n Add Condition\n </button>\n <button mat-stroked-button class=\"add-group-btn\" (click)=\"addGroup(group)\">\n <mat-icon>folder_open</mat-icon>\n Add Group\n </button>\n </div>\n </div>\n</ng-template>\n", styles: [".modal-backdrop{position:fixed;inset:0;background:#0000004d;z-index:1000;display:flex;align-items:center;justify-content:center;padding:20px}.selector-modal{position:relative;background:#fff;border-radius:12px;box-shadow:0 8px 32px #00000026;width:580px;max-width:100%;max-height:calc(100vh - 40px);display:flex;flex-direction:column;overflow:hidden}.modal-header{display:flex;align-items:center;gap:12px;padding:16px 20px;background:linear-gradient(135deg,#8b5cf6,#6366f1);color:#fff;flex-shrink:0}.modal-header .header-title{display:flex;align-items:center;gap:8px;font-size:16px;font-weight:600}.modal-header .mapping-path{flex:1;font-size:13px;opacity:.9;text-align:right;margin-right:8px}.modal-header .close-btn{color:#fff;opacity:.9}.modal-header .close-btn:hover{opacity:1}.modal-body{flex:1;overflow-y:auto;padding:20px;min-height:0}.description{font-size:14px;color:#64748b;margin:0 0 16px}.selection-mode mat-radio-group{display:flex;flex-direction:column;gap:12px}.selection-mode .mode-option ::ng-deep .mdc-form-field{align-items:flex-start}.selection-mode .mode-option ::ng-deep .mdc-radio{margin-top:4px}.selection-mode .mode-content{display:flex;align-items:flex-start;gap:12px;padding:12px 16px;border-radius:8px;background:#f8fafc;border:1px solid #e2e8f0;transition:all .2s ease;cursor:pointer}.selection-mode .mode-content:hover{background:#f1f5f9;border-color:#cbd5e1}.selection-mode .mode-content mat-icon{color:#64748b;margin-top:2px}.selection-mode .mode-text{display:flex;flex-direction:column;gap:2px}.selection-mode .mode-label{font-size:14px;font-weight:500;color:#1e293b}.selection-mode .mode-desc{font-size:12px;color:#64748b}.selection-mode mat-radio-button.mat-mdc-radio-checked .mode-content{background:#f5f3ff;border-color:#8b5cf6}.selection-mode mat-radio-button.mat-mdc-radio-checked .mode-content mat-icon{color:#8b5cf6}mat-divider{margin:20px 0}.conditions-section{display:flex;flex-direction:column;gap:16px}.filter-group{border-radius:8px}.filter-group.root-group{background:#f8fafc;border:1px solid #e2e8f0;padding:16px}.filter-group.nested-group{background:#fff;border:2px dashed #cbd5e1;padding:12px;margin-top:8px}.group-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid #e2e8f0}.logic-toggle{display:flex;align-items:center;gap:12px}.logic-toggle .logic-label{font-size:13px;font-weight:500;color:#475569}.logic-toggle mat-radio-group{display:flex;gap:12px}.logic-toggle mat-radio-button{font-size:12px}.logic-toggle mat-radio-button ::ng-deep .mdc-label{font-size:12px}.remove-group-btn{color:#94a3b8}.remove-group-btn:hover{color:#ef4444}.group-children{display:flex;flex-direction:column}.logic-connector{display:flex;align-items:center;justify-content:center;padding:8px 0}.logic-connector .logic-badge{font-size:10px;font-weight:700;padding:3px 10px;border-radius:12px;letter-spacing:.5px}.logic-connector .logic-badge.and{background:#dbeafe;color:#1d4ed8}.logic-connector .logic-badge.or{background:#fef3c7;color:#b45309}.condition-row .condition-inputs{display:flex;align-items:flex-start;gap:8px;padding:12px;background:#fff;border:1px solid #e2e8f0;border-radius:8px}.condition-row .condition-inputs .field-select{flex:1;min-width:120px}.condition-row .condition-inputs .operator-select{flex:1;min-width:130px}.condition-row .condition-inputs .value-input{flex:1;min-width:100px}.condition-row .condition-inputs .bool-toggle{padding-top:12px;min-width:80px}.condition-row .condition-inputs .remove-btn{color:#94a3b8;align-self:center}.condition-row .condition-inputs .remove-btn:hover{color:#ef4444}.condition-row .condition-inputs mat-form-field ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.nested-group .condition-row .condition-inputs{background:#f8fafc}.field-type{font-size:11px;color:#94a3b8;margin-left:4px}.empty-group{display:flex;align-items:center;gap:8px;padding:16px;background:#fef3c7;border-radius:8px;color:#92400e;font-size:13px}.empty-group mat-icon{font-size:20px;width:20px;height:20px}.group-actions{display:flex;gap:8px;margin-top:12px;padding-top:12px;border-top:1px solid #e2e8f0}.add-condition-btn{color:#8b5cf6;border-color:#8b5cf6;font-size:12px}.add-condition-btn mat-icon{font-size:16px;width:16px;height:16px}.add-group-btn{color:#6366f1;border-color:#6366f1;font-size:12px}.add-group-btn mat-icon{font-size:16px;width:16px;height:16px}.modal-footer{display:flex;justify-content:flex-end;gap:8px;padding:16px 20px;border-top:1px solid #e2e8f0;background:#f8fafc;flex-shrink:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2$1.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "component", type: i5.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i5.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5$1.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "ngmodule", type: MatRadioModule }, { kind: "directive", type: i6.MatRadioGroup, selector: "mat-radio-group", inputs: ["color", "name", "labelPosition", "value", "selected", "disabled", "required", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioGroup"] }, { kind: "component", type: i6.MatRadioButton, selector: "mat-radio-button", inputs: ["id", "name", "aria-label", "aria-labelledby", "aria-describedby", "disableRipple", "tabIndex", "checked", "value", "labelPosition", "disabled", "required", "color", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioButton"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i7.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i8.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i9.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "pipe", type: i1.UpperCasePipe, name: "uppercase" }] });
|
|
2321
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: ArraySelectorModalComponent, isStandalone: true, selector: "array-selector-modal", inputs: { mapping: "mapping" }, outputs: { save: "save", close: "close" }, ngImport: i0, template: "<div class=\"modal-backdrop\" (click)=\"onBackdropClick($event)\">\n <div class=\"selector-modal\">\n <div class=\"modal-header\">\n <div class=\"header-title\">\n <mat-icon>swap_horiz</mat-icon>\n <span>Array to Object</span>\n </div>\n <span class=\"mapping-path\">{{ mapping.sourceArray.name }}[] → {{ mapping.targetObject.name }}</span>\n <button mat-icon-button class=\"close-btn\" (click)=\"onClose()\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n\n <div class=\"modal-body\">\n <p class=\"description\">\n Select which item from the array should be mapped to the target object.\n </p>\n\n <!-- Selection mode -->\n <div class=\"selection-mode\">\n <mat-radio-group [value]=\"selectionMode()\" (change)=\"selectionMode.set($event.value)\">\n <mat-radio-button value=\"first\" class=\"mode-option\">\n <div class=\"mode-content\">\n <mat-icon>first_page</mat-icon>\n <div class=\"mode-text\">\n <span class=\"mode-label\">First item</span>\n <span class=\"mode-desc\">Use the first item in the array</span>\n </div>\n </div>\n </mat-radio-button>\n\n <mat-radio-button value=\"last\" class=\"mode-option\">\n <div class=\"mode-content\">\n <mat-icon>last_page</mat-icon>\n <div class=\"mode-text\">\n <span class=\"mode-label\">Last item</span>\n <span class=\"mode-desc\">Use the last item in the array</span>\n </div>\n </div>\n </mat-radio-button>\n\n <mat-radio-button value=\"condition\" class=\"mode-option\">\n <div class=\"mode-content\">\n <mat-icon>filter_alt</mat-icon>\n <div class=\"mode-text\">\n <span class=\"mode-label\">First matching condition</span>\n <span class=\"mode-desc\">Use the first item that matches the condition</span>\n </div>\n </div>\n </mat-radio-button>\n </mat-radio-group>\n </div>\n\n <!-- Condition builder (only when condition mode selected) -->\n @if (selectionMode() === 'condition') {\n <mat-divider></mat-divider>\n\n <div class=\"conditions-section\">\n <ng-container *ngTemplateOutlet=\"groupTemplate; context: { group: conditionGroup(), isRoot: true }\"></ng-container>\n </div>\n }\n </div>\n\n <div class=\"modal-footer\">\n <button mat-button (click)=\"onClose()\">Cancel</button>\n <button mat-flat-button color=\"primary\" (click)=\"onSave()\">Apply</button>\n </div>\n </div>\n</div>\n\n<!-- Recursive group template -->\n<ng-template #groupTemplate let-group=\"group\" let-isRoot=\"isRoot\" let-parentGroup=\"parentGroup\">\n <div class=\"filter-group\" [class.root-group]=\"isRoot\" [class.nested-group]=\"!isRoot\">\n <div class=\"group-header\">\n <div class=\"logic-toggle\">\n <span class=\"logic-label\">Match</span>\n <mat-radio-group [value]=\"group.logic\" (change)=\"onLogicChange(group, $event.value)\">\n <mat-radio-button value=\"and\">ALL (AND)</mat-radio-button>\n <mat-radio-button value=\"or\">ANY (OR)</mat-radio-button>\n </mat-radio-group>\n </div>\n @if (!isRoot) {\n <button mat-icon-button class=\"remove-group-btn\" matTooltip=\"Remove group\" (click)=\"removeItem(parentGroup, group.id)\">\n <mat-icon>close</mat-icon>\n </button>\n }\n </div>\n\n <div class=\"group-children\">\n @for (item of group.children; track item.id; let i = $index) {\n @if (i > 0) {\n <div class=\"logic-connector\">\n <span class=\"logic-badge\" [class.and]=\"group.logic === 'and'\" [class.or]=\"group.logic === 'or'\">\n {{ group.logic | uppercase }}\n </span>\n </div>\n }\n\n @if (isCondition(item)) {\n <div class=\"condition-row\">\n <div class=\"condition-inputs\">\n <mat-form-field appearance=\"outline\" class=\"field-select\">\n <mat-label>Field</mat-label>\n <mat-select [value]=\"item.field\" (selectionChange)=\"onFieldChange(item, $event.value)\">\n @for (field of availableFields(); track field.path) {\n <mat-option [value]=\"field.path\">\n {{ field.name }}\n <span class=\"field-type\">({{ field.type }})</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field appearance=\"outline\" class=\"operator-select\">\n <mat-label>Operator</mat-label>\n <mat-select [value]=\"item.operator\" (selectionChange)=\"onOperatorChange(item, $event.value)\">\n @for (op of getOperatorsForField(item.field); track op.value) {\n <mat-option [value]=\"op.value\">{{ op.label }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (operatorNeedsValue(item.operator)) {\n @if (item.valueType === 'boolean') {\n <mat-slide-toggle\n [checked]=\"item.value === true\"\n (change)=\"onValueChange(item, $event.checked)\"\n class=\"bool-toggle\"\n >\n {{ item.value ? 'true' : 'false' }}\n </mat-slide-toggle>\n } @else if (item.valueType === 'number') {\n <mat-form-field appearance=\"outline\" class=\"value-input\">\n <mat-label>Value</mat-label>\n <input matInput type=\"number\" [value]=\"item.value\" (input)=\"onValueChange(item, $any($event.target).value)\" />\n </mat-form-field>\n } @else {\n <mat-form-field appearance=\"outline\" class=\"value-input\">\n <mat-label>Value</mat-label>\n <input matInput type=\"text\" [value]=\"item.value\" (input)=\"onValueChange(item, $any($event.target).value)\" />\n </mat-form-field>\n }\n }\n\n <button mat-icon-button class=\"remove-btn\" matTooltip=\"Remove condition\" (click)=\"removeItem(group, item.id)\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </div>\n } @else if (isGroup(item)) {\n <ng-container *ngTemplateOutlet=\"groupTemplate; context: { group: item, isRoot: false, parentGroup: group }\"></ng-container>\n }\n }\n\n @if (group.children.length === 0) {\n <div class=\"empty-group\">\n <mat-icon>info_outline</mat-icon>\n <span>No conditions. Add a condition to filter.</span>\n </div>\n }\n </div>\n\n <div class=\"group-actions\">\n <button mat-stroked-button class=\"add-condition-btn\" (click)=\"addCondition(group)\">\n <mat-icon>add</mat-icon>\n Add Condition\n </button>\n <button mat-stroked-button class=\"add-group-btn\" (click)=\"addGroup(group)\">\n <mat-icon>folder_open</mat-icon>\n Add Group\n </button>\n </div>\n </div>\n</ng-template>\n", styles: [".modal-backdrop{position:fixed;inset:0;background:#0000004d;z-index:1000;display:flex;align-items:center;justify-content:center;padding:20px}.selector-modal{position:relative;background:#fff;border-radius:12px;box-shadow:0 8px 32px #00000026;width:580px;max-width:100%;max-height:calc(100vh - 40px);display:flex;flex-direction:column;overflow:hidden}.modal-header{display:flex;align-items:center;gap:12px;padding:16px 20px;background:linear-gradient(135deg,#8b5cf6,#6366f1);color:#fff;flex-shrink:0}.modal-header .header-title{display:flex;align-items:center;gap:8px;font-size:16px;font-weight:600}.modal-header .mapping-path{flex:1;font-size:13px;opacity:.9;text-align:right;margin-right:8px}.modal-header .close-btn{color:#fff;opacity:.9}.modal-header .close-btn:hover{opacity:1}.modal-body{flex:1;overflow-y:auto;padding:20px;min-height:0}.description{font-size:14px;color:#64748b;margin:0 0 16px}.selection-mode mat-radio-group{display:flex;flex-direction:column;gap:12px}.selection-mode .mode-option ::ng-deep .mdc-form-field{align-items:flex-start}.selection-mode .mode-option ::ng-deep .mdc-radio{margin-top:4px}.selection-mode .mode-content{display:flex;align-items:flex-start;gap:12px;padding:12px 16px;border-radius:8px;background:#f8fafc;border:1px solid #e2e8f0;transition:all .2s ease;cursor:pointer}.selection-mode .mode-content:hover{background:#f1f5f9;border-color:#cbd5e1}.selection-mode .mode-content mat-icon{color:#64748b;margin-top:2px}.selection-mode .mode-text{display:flex;flex-direction:column;gap:2px}.selection-mode .mode-label{font-size:14px;font-weight:500;color:#1e293b}.selection-mode .mode-desc{font-size:12px;color:#64748b}.selection-mode mat-radio-button.mat-mdc-radio-checked .mode-content{background:#f5f3ff;border-color:#8b5cf6}.selection-mode mat-radio-button.mat-mdc-radio-checked .mode-content mat-icon{color:#8b5cf6}mat-divider{margin:20px 0}.conditions-section{display:flex;flex-direction:column;gap:16px}.filter-group{border-radius:8px}.filter-group.root-group{background:#f8fafc;border:1px solid #e2e8f0;padding:16px}.filter-group.nested-group{background:#fff;border:2px dashed #cbd5e1;padding:12px;margin-top:8px}.group-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid #e2e8f0}.logic-toggle{display:flex;align-items:center;gap:12px}.logic-toggle .logic-label{font-size:13px;font-weight:500;color:#475569}.logic-toggle mat-radio-group{display:flex;gap:12px}.logic-toggle mat-radio-button{font-size:12px}.logic-toggle mat-radio-button ::ng-deep .mdc-label{font-size:12px}.remove-group-btn{color:#94a3b8}.remove-group-btn:hover{color:#ef4444}.group-children{display:flex;flex-direction:column}.logic-connector{display:flex;align-items:center;justify-content:center;padding:8px 0}.logic-connector .logic-badge{font-size:10px;font-weight:700;padding:3px 10px;border-radius:12px;letter-spacing:.5px}.logic-connector .logic-badge.and{background:#dbeafe;color:#1d4ed8}.logic-connector .logic-badge.or{background:#fef3c7;color:#b45309}.condition-row .condition-inputs{display:flex;align-items:flex-start;gap:8px;padding:12px;background:#fff;border:1px solid #e2e8f0;border-radius:8px}.condition-row .condition-inputs .field-select{flex:1;min-width:120px}.condition-row .condition-inputs .operator-select{flex:1;min-width:130px}.condition-row .condition-inputs .value-input{flex:1;min-width:100px}.condition-row .condition-inputs .bool-toggle{padding-top:12px;min-width:80px}.condition-row .condition-inputs .remove-btn{color:#94a3b8;align-self:center}.condition-row .condition-inputs .remove-btn:hover{color:#ef4444}.condition-row .condition-inputs mat-form-field ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.nested-group .condition-row .condition-inputs{background:#f8fafc}.field-type{font-size:11px;color:#94a3b8;margin-left:4px}.empty-group{display:flex;align-items:center;gap:8px;padding:16px;background:#fef3c7;border-radius:8px;color:#92400e;font-size:13px}.empty-group mat-icon{font-size:20px;width:20px;height:20px}.group-actions{display:flex;gap:8px;margin-top:12px;padding-top:12px;border-top:1px solid #e2e8f0}.add-condition-btn{color:#8b5cf6;border-color:#8b5cf6;font-size:12px}.add-condition-btn mat-icon{font-size:16px;width:16px;height:16px}.add-group-btn{color:#6366f1;border-color:#6366f1;font-size:12px}.add-group-btn mat-icon{font-size:16px;width:16px;height:16px}.modal-footer{display:flex;justify-content:flex-end;gap:8px;padding:16px 20px;border-top:1px solid #e2e8f0;background:#f8fafc;flex-shrink:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "component", type: i4.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i4.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "ngmodule", type: MatRadioModule }, { kind: "directive", type: i6.MatRadioGroup, selector: "mat-radio-group", inputs: ["color", "name", "labelPosition", "value", "selected", "disabled", "required", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioGroup"] }, { kind: "component", type: i6.MatRadioButton, selector: "mat-radio-button", inputs: ["id", "name", "aria-label", "aria-labelledby", "aria-describedby", "disableRipple", "tabIndex", "checked", "value", "labelPosition", "disabled", "required", "color", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioButton"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i7$1.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i9.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "pipe", type: i1.UpperCasePipe, name: "uppercase" }] });
|
|
1540
2322
|
}
|
|
1541
2323
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ArraySelectorModalComponent, decorators: [{
|
|
1542
2324
|
type: Component,
|
|
@@ -1575,7 +2357,7 @@ class DefaultValuePopoverComponent {
|
|
|
1575
2357
|
dateValue = null;
|
|
1576
2358
|
ngOnInit() {
|
|
1577
2359
|
if (this.existingValue) {
|
|
1578
|
-
switch (this.
|
|
2360
|
+
switch (this.fieldType) {
|
|
1579
2361
|
case 'string':
|
|
1580
2362
|
this.stringValue = this.existingValue.value || '';
|
|
1581
2363
|
break;
|
|
@@ -1626,7 +2408,7 @@ class DefaultValuePopoverComponent {
|
|
|
1626
2408
|
}
|
|
1627
2409
|
}
|
|
1628
2410
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: DefaultValuePopoverComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1629
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: DefaultValuePopoverComponent, isStandalone: true, selector: "default-value-popover", inputs: { field: "field", existingValue: "existingValue", position: "position" }, outputs: { save: "save", delete: "delete", close: "close" }, ngImport: i0, template: "<div class=\"popover-backdrop\" (click)=\"onBackdropClick($event)\">\n <div class=\"popover-container\">\n <div class=\"popover-header\">\n <div class=\"header-title\">\n <mat-icon>edit</mat-icon>\n <span>Default Value</span>\n </div>\n <button mat-icon-button (click)=\"onClose()\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n\n <div class=\"popover-content\">\n <div class=\"field-info\">\n <span class=\"field-name\">{{ field.name }}</span>\n <span class=\"field-type\">{{ fieldType }}</span>\n </div>\n\n <!-- String input -->\n @if (fieldType === 'string') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Default Value</mat-label>\n <input matInput [(ngModel)]=\"stringValue\" placeholder=\"Enter default value\">\n </mat-form-field>\n }\n\n <!-- Number input -->\n @if (fieldType === 'number') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Default Value</mat-label>\n <input matInput type=\"number\" [(ngModel)]=\"numberValue\" placeholder=\"Enter number\">\n </mat-form-field>\n }\n\n <!-- Boolean input -->\n @if (fieldType === 'boolean') {\n <div class=\"boolean-input\">\n <mat-slide-toggle [(ngModel)]=\"booleanValue\">\n {{ booleanValue ? 'True' : 'False' }}\n </mat-slide-toggle>\n </div>\n }\n\n <!-- Date input -->\n @if (fieldType === 'date') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Default Date</mat-label>\n <input matInput [matDatepicker]=\"picker\" [(ngModel)]=\"dateValue\">\n <mat-datepicker-toggle matIconSuffix [for]=\"picker\"></mat-datepicker-toggle>\n <mat-datepicker #picker></mat-datepicker>\n </mat-form-field>\n }\n </div>\n\n <div class=\"popover-actions\">\n @if (existingValue) {\n <button mat-button color=\"warn\" (click)=\"onDelete()\">\n <mat-icon>delete</mat-icon>\n Remove\n </button>\n }\n <span class=\"spacer\"></span>\n <button mat-button (click)=\"onClose()\">Cancel</button>\n <button mat-flat-button color=\"primary\" (click)=\"onSave()\">Save</button>\n </div>\n </div>\n</div>\n", styles: [".popover-backdrop{position:fixed;top:0;left:0;width:100%;height:100%;background:#0000004d;z-index:1000;display:flex;align-items:center;justify-content:center}.popover-container{background:#fff;border-radius:12px;box-shadow:0 8px 32px #0003;min-width:320px;max-width:400px;overflow:hidden}.popover-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;background:linear-gradient(135deg,#10b981,#059669);color:#fff}.popover-header .header-title{display:flex;align-items:center;gap:8px;font-size:16px;font-weight:600}.popover-header .header-title mat-icon{font-size:20px;width:20px;height:20px}.popover-header button{color:#fff}.popover-content{padding:20px}.field-info{display:flex;align-items:center;gap:8px;margin-bottom:16px;padding:10px 12px;background:#f8fafc;border-radius:8px}.field-info .field-name{font-weight:600;color:#1e293b}.field-info .field-type{font-size:12px;color:#64748b;background:#e2e8f0;padding:2px 8px;border-radius:4px;text-transform:uppercase}.full-width{width:100%}.boolean-input{padding:12px 0}.popover-actions{display:flex;align-items:center;gap:8px;padding:16px 20px;background:#f8fafc;border-top:1px solid #e2e8f0}.popover-actions .spacer{flex:1}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2
|
|
2411
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: DefaultValuePopoverComponent, isStandalone: true, selector: "default-value-popover", inputs: { field: "field", existingValue: "existingValue", position: "position" }, outputs: { save: "save", delete: "delete", close: "close" }, ngImport: i0, template: "<div class=\"popover-backdrop\" (click)=\"onBackdropClick($event)\">\n <div class=\"popover-container\">\n <div class=\"popover-header\">\n <div class=\"header-title\">\n <mat-icon>edit</mat-icon>\n <span>Default Value</span>\n </div>\n <button mat-icon-button (click)=\"onClose()\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n\n <div class=\"popover-content\">\n <div class=\"field-info\">\n <span class=\"field-name\">{{ field.name }}</span>\n <span class=\"field-type\">{{ fieldType }}</span>\n </div>\n\n <!-- String input -->\n @if (fieldType === 'string') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Default Value</mat-label>\n <input matInput [(ngModel)]=\"stringValue\" placeholder=\"Enter default value\">\n </mat-form-field>\n }\n\n <!-- Number input -->\n @if (fieldType === 'number') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Default Value</mat-label>\n <input matInput type=\"number\" [(ngModel)]=\"numberValue\" placeholder=\"Enter number\">\n </mat-form-field>\n }\n\n <!-- Boolean input -->\n @if (fieldType === 'boolean') {\n <div class=\"boolean-input\">\n <mat-slide-toggle [(ngModel)]=\"booleanValue\">\n {{ booleanValue ? 'True' : 'False' }}\n </mat-slide-toggle>\n </div>\n }\n\n <!-- Date input -->\n @if (fieldType === 'date') {\n <mat-form-field appearance=\"outline\" class=\"full-width\">\n <mat-label>Default Date</mat-label>\n <input matInput [matDatepicker]=\"picker\" [(ngModel)]=\"dateValue\">\n <mat-datepicker-toggle matIconSuffix [for]=\"picker\"></mat-datepicker-toggle>\n <mat-datepicker #picker></mat-datepicker>\n </mat-form-field>\n }\n </div>\n\n <div class=\"popover-actions\">\n @if (existingValue) {\n <button mat-button color=\"warn\" (click)=\"onDelete()\">\n <mat-icon>delete</mat-icon>\n Remove\n </button>\n }\n <span class=\"spacer\"></span>\n <button mat-button (click)=\"onClose()\">Cancel</button>\n <button mat-flat-button color=\"primary\" (click)=\"onSave()\">Save</button>\n </div>\n </div>\n</div>\n", styles: [".popover-backdrop{position:fixed;top:0;left:0;width:100%;height:100%;background:#0000004d;z-index:1000;display:flex;align-items:center;justify-content:center}.popover-container{background:#fff;border-radius:12px;box-shadow:0 8px 32px #0003;min-width:320px;max-width:400px;overflow:hidden}.popover-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;background:linear-gradient(135deg,#10b981,#059669);color:#fff}.popover-header .header-title{display:flex;align-items:center;gap:8px;font-size:16px;font-weight:600}.popover-header .header-title mat-icon{font-size:20px;width:20px;height:20px}.popover-header button{color:#fff}.popover-content{padding:20px}.field-info{display:flex;align-items:center;gap:8px;margin-bottom:16px;padding:10px 12px;background:#f8fafc;border-radius:8px}.field-info .field-name{font-weight:600;color:#1e293b}.field-info .field-type{font-size:12px;color:#64748b;background:#e2e8f0;padding:2px 8px;border-radius:4px;text-transform:uppercase}.full-width{width:100%}.boolean-input{padding:12px 0}.popover-actions{display:flex;align-items:center;gap:8px;padding:16px 20px;background:#f8fafc;border-top:1px solid #e2e8f0}.popover-actions .spacer{flex:1}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "ngmodule", type: MatDatepickerModule }, { kind: "component", type: i6$1.MatDatepicker, selector: "mat-datepicker", exportAs: ["matDatepicker"] }, { kind: "directive", type: i6$1.MatDatepickerInput, selector: "input[matDatepicker]", inputs: ["matDatepicker", "min", "max", "matDatepickerFilter"], exportAs: ["matDatepickerInput"] }, { kind: "component", type: i6$1.MatDatepickerToggle, selector: "mat-datepicker-toggle", inputs: ["for", "tabIndex", "aria-label", "disabled", "disableRipple"], exportAs: ["matDatepickerToggle"] }, { kind: "ngmodule", type: MatNativeDateModule }, { kind: "ngmodule", type: MatSelectModule }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i7$1.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }] });
|
|
1630
2412
|
}
|
|
1631
2413
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: DefaultValuePopoverComponent, decorators: [{
|
|
1632
2414
|
type: Component,
|
|
@@ -1657,8 +2439,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
1657
2439
|
}] } });
|
|
1658
2440
|
|
|
1659
2441
|
class DataMapperComponent {
|
|
1660
|
-
sourceSchema
|
|
1661
|
-
|
|
2442
|
+
set sourceSchema(value) {
|
|
2443
|
+
if (value) {
|
|
2444
|
+
this._sourceSchemaInput.set(value);
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
set targetSchema(value) {
|
|
2448
|
+
if (value) {
|
|
2449
|
+
this._targetSchemaInput.set(value);
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
set sourceSchemaRef(value) {
|
|
2453
|
+
this.mappingService.setSourceSchemaRef(value ?? null);
|
|
2454
|
+
}
|
|
2455
|
+
set targetSchemaRef(value) {
|
|
2456
|
+
this.mappingService.setTargetSchemaRef(value ?? null);
|
|
2457
|
+
}
|
|
1662
2458
|
sampleData = {};
|
|
1663
2459
|
mappingsChange = new EventEmitter();
|
|
1664
2460
|
svgContainer;
|
|
@@ -1666,6 +2462,23 @@ class DataMapperComponent {
|
|
|
1666
2462
|
mappingService = inject(MappingService);
|
|
1667
2463
|
svgConnectorService = inject(SvgConnectorService);
|
|
1668
2464
|
transformationService = inject(TransformationService);
|
|
2465
|
+
schemaParserService = inject(SchemaParserService);
|
|
2466
|
+
// Internal signals for schema inputs
|
|
2467
|
+
_sourceSchemaInput = signal(null, ...(ngDevMode ? [{ debugName: "_sourceSchemaInput" }] : []));
|
|
2468
|
+
_targetSchemaInput = signal(null, ...(ngDevMode ? [{ debugName: "_targetSchemaInput" }] : []));
|
|
2469
|
+
// Converted schemas for the tree component
|
|
2470
|
+
sourceSchemaForTree = computed(() => {
|
|
2471
|
+
const schema = this._sourceSchemaInput();
|
|
2472
|
+
if (!schema)
|
|
2473
|
+
return { name: '', fields: [] };
|
|
2474
|
+
return this.schemaParserService.parseSchema(schema, schema.title || 'Source');
|
|
2475
|
+
}, ...(ngDevMode ? [{ debugName: "sourceSchemaForTree" }] : []));
|
|
2476
|
+
targetSchemaForTree = computed(() => {
|
|
2477
|
+
const schema = this._targetSchemaInput();
|
|
2478
|
+
if (!schema)
|
|
2479
|
+
return { name: '', fields: [] };
|
|
2480
|
+
return this.schemaParserService.parseSchema(schema, schema.title || 'Target');
|
|
2481
|
+
}, ...(ngDevMode ? [{ debugName: "targetSchemaForTree" }] : []));
|
|
1669
2482
|
// Field positions from both trees
|
|
1670
2483
|
sourcePositions = new Map();
|
|
1671
2484
|
targetPositions = new Map();
|
|
@@ -1694,6 +2507,12 @@ class DataMapperComponent {
|
|
|
1694
2507
|
dragSourceField = null;
|
|
1695
2508
|
dragStartPoint = null;
|
|
1696
2509
|
resizeObserver;
|
|
2510
|
+
// Endpoint drag state
|
|
2511
|
+
isEndpointDragging = false;
|
|
2512
|
+
endpointDragMappingId = null;
|
|
2513
|
+
endpointDragType = null;
|
|
2514
|
+
endpointDragSourceIndex = null;
|
|
2515
|
+
endpointDragAnchorPoint = null;
|
|
1697
2516
|
ngAfterViewInit() {
|
|
1698
2517
|
this.setupResizeObserver();
|
|
1699
2518
|
}
|
|
@@ -1729,15 +2548,27 @@ class DataMapperComponent {
|
|
|
1729
2548
|
document.body.style.cursor = 'grabbing';
|
|
1730
2549
|
}
|
|
1731
2550
|
onMouseMove(event) {
|
|
1732
|
-
if (!this.
|
|
2551
|
+
if (!this.svgContainer?.nativeElement)
|
|
1733
2552
|
return;
|
|
1734
2553
|
const containerRect = this.svgContainer.nativeElement.getBoundingClientRect();
|
|
1735
2554
|
const currentPoint = {
|
|
1736
2555
|
x: event.clientX - containerRect.left,
|
|
1737
2556
|
y: event.clientY - containerRect.top,
|
|
1738
2557
|
};
|
|
1739
|
-
|
|
1740
|
-
this.
|
|
2558
|
+
// Handle new connection dragging
|
|
2559
|
+
if (this.isDragging && this.dragStartPoint) {
|
|
2560
|
+
const path = this.svgConnectorService.createDragPath(this.dragStartPoint, currentPoint);
|
|
2561
|
+
this.dragPath.set(path);
|
|
2562
|
+
return;
|
|
2563
|
+
}
|
|
2564
|
+
// Handle endpoint dragging
|
|
2565
|
+
if (this.isEndpointDragging && this.endpointDragAnchorPoint) {
|
|
2566
|
+
// Show drag path from anchor to cursor
|
|
2567
|
+
const path = this.endpointDragType === 'source'
|
|
2568
|
+
? this.svgConnectorService.createDragPath(currentPoint, this.endpointDragAnchorPoint)
|
|
2569
|
+
: this.svgConnectorService.createDragPath(this.endpointDragAnchorPoint, currentPoint);
|
|
2570
|
+
this.dragPath.set(path);
|
|
2571
|
+
}
|
|
1741
2572
|
}
|
|
1742
2573
|
onMouseUp(event) {
|
|
1743
2574
|
if (this.isDragging) {
|
|
@@ -1747,8 +2578,53 @@ class DataMapperComponent {
|
|
|
1747
2578
|
this.dragStartPoint = null;
|
|
1748
2579
|
document.body.style.cursor = '';
|
|
1749
2580
|
}
|
|
2581
|
+
if (this.isEndpointDragging) {
|
|
2582
|
+
this.cancelEndpointDrag();
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
onEndpointDragStart(connection, endpointType, sourceIndex, event) {
|
|
2586
|
+
event.stopPropagation();
|
|
2587
|
+
event.preventDefault();
|
|
2588
|
+
if (!this.svgContainer?.nativeElement)
|
|
2589
|
+
return;
|
|
2590
|
+
this.isEndpointDragging = true;
|
|
2591
|
+
this.endpointDragMappingId = connection.mappingId;
|
|
2592
|
+
this.endpointDragType = endpointType;
|
|
2593
|
+
this.endpointDragSourceIndex = sourceIndex;
|
|
2594
|
+
// Set anchor point (the point that stays fixed)
|
|
2595
|
+
if (endpointType === 'source') {
|
|
2596
|
+
// Moving source, anchor is the target
|
|
2597
|
+
this.endpointDragAnchorPoint = connection.targetPoint;
|
|
2598
|
+
}
|
|
2599
|
+
else {
|
|
2600
|
+
// Moving target, anchor is the first source (or merge point for multi-source)
|
|
2601
|
+
this.endpointDragAnchorPoint = connection.sourcePoints[0];
|
|
2602
|
+
}
|
|
2603
|
+
// Update MappingService drag state so schema-tree can detect endpoint dragging
|
|
2604
|
+
this.mappingService.startEndpointDrag(connection.mappingId, endpointType, endpointType === 'source' ? connection.sourcePoints[sourceIndex] : connection.targetPoint, sourceIndex);
|
|
2605
|
+
document.body.style.cursor = 'grabbing';
|
|
2606
|
+
this.updateConnections(); // Update to show connection as being dragged
|
|
2607
|
+
}
|
|
2608
|
+
cancelEndpointDrag() {
|
|
2609
|
+
this.dragPath.set(null);
|
|
2610
|
+
this.isEndpointDragging = false;
|
|
2611
|
+
this.endpointDragMappingId = null;
|
|
2612
|
+
this.endpointDragType = null;
|
|
2613
|
+
this.endpointDragSourceIndex = null;
|
|
2614
|
+
this.endpointDragAnchorPoint = null;
|
|
2615
|
+
document.body.style.cursor = '';
|
|
2616
|
+
this.mappingService.endDrag(); // Reset MappingService drag state
|
|
2617
|
+
this.updateConnections(); // Update to restore connection visibility
|
|
1750
2618
|
}
|
|
1751
2619
|
onFieldDrop(event) {
|
|
2620
|
+
// Handle endpoint drag completion (drop on target field)
|
|
2621
|
+
if (this.isEndpointDragging && this.endpointDragMappingId && this.endpointDragType === 'target') {
|
|
2622
|
+
this.mappingService.changeTargetField(this.endpointDragMappingId, event.field);
|
|
2623
|
+
this.mappingsChange.emit(this.mappingService.allMappings());
|
|
2624
|
+
this.updateConnections();
|
|
2625
|
+
this.cancelEndpointDrag();
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
1752
2628
|
if (!this.dragSourceField)
|
|
1753
2629
|
return;
|
|
1754
2630
|
// Create mapping
|
|
@@ -1762,6 +2638,15 @@ class DataMapperComponent {
|
|
|
1762
2638
|
this.dragPath.set(null);
|
|
1763
2639
|
document.body.style.cursor = '';
|
|
1764
2640
|
}
|
|
2641
|
+
onSourceFieldDrop(event) {
|
|
2642
|
+
// Handle endpoint drag completion (drop on source field)
|
|
2643
|
+
if (this.isEndpointDragging && this.endpointDragMappingId && this.endpointDragType === 'source') {
|
|
2644
|
+
this.mappingService.changeSourceField(this.endpointDragMappingId, event.field, this.endpointDragSourceIndex ?? undefined);
|
|
2645
|
+
this.mappingsChange.emit(this.mappingService.allMappings());
|
|
2646
|
+
this.updateConnections();
|
|
2647
|
+
this.cancelEndpointDrag();
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
1765
2650
|
onConnectionClick(connection, event) {
|
|
1766
2651
|
event.stopPropagation();
|
|
1767
2652
|
this.selectedMappingId.set(connection.mappingId);
|
|
@@ -1854,10 +2739,10 @@ class DataMapperComponent {
|
|
|
1854
2739
|
getExistingDefaultValue(fieldId) {
|
|
1855
2740
|
return this.mappingService.getDefaultValue(fieldId);
|
|
1856
2741
|
}
|
|
1857
|
-
onPopoverSave(
|
|
2742
|
+
onPopoverSave(transformations) {
|
|
1858
2743
|
const mappingId = this.selectedMappingId();
|
|
1859
2744
|
if (mappingId) {
|
|
1860
|
-
this.mappingService.
|
|
2745
|
+
this.mappingService.updateTransformations(mappingId, transformations);
|
|
1861
2746
|
this.mappingsChange.emit(this.mappingService.allMappings());
|
|
1862
2747
|
}
|
|
1863
2748
|
this.closePopover();
|
|
@@ -1920,13 +2805,15 @@ class DataMapperComponent {
|
|
|
1920
2805
|
id: `conn-${mapping.id}`,
|
|
1921
2806
|
mappingId: mapping.id,
|
|
1922
2807
|
paths,
|
|
2808
|
+
sourcePoints,
|
|
1923
2809
|
midPoint,
|
|
1924
2810
|
targetPoint,
|
|
1925
|
-
hasTransformation: mapping.
|
|
2811
|
+
hasTransformation: mapping.transformations.length > 1 || mapping.transformations[0]?.type !== 'direct',
|
|
1926
2812
|
isSelected: mapping.id === selectedId,
|
|
1927
2813
|
isArrayMapping: mapping.isArrayMapping || false,
|
|
1928
2814
|
isArrayToObjectMapping: mapping.isArrayToObjectMapping || false,
|
|
1929
2815
|
hasFilter,
|
|
2816
|
+
isBeingDragged: mapping.id === this.endpointDragMappingId,
|
|
1930
2817
|
});
|
|
1931
2818
|
}
|
|
1932
2819
|
this.connections.set(newConnections);
|
|
@@ -1972,7 +2859,11 @@ class DataMapperComponent {
|
|
|
1972
2859
|
template: 'code',
|
|
1973
2860
|
custom: 'functions',
|
|
1974
2861
|
};
|
|
1975
|
-
|
|
2862
|
+
// For multiple transformations, show a pipeline icon
|
|
2863
|
+
if (mapping.transformations.length > 1) {
|
|
2864
|
+
return 'linear_scale';
|
|
2865
|
+
}
|
|
2866
|
+
return icons[mapping.transformations[0]?.type] || 'settings';
|
|
1976
2867
|
}
|
|
1977
2868
|
clearAllMappings() {
|
|
1978
2869
|
this.mappingService.clearAllMappings();
|
|
@@ -1991,7 +2882,7 @@ class DataMapperComponent {
|
|
|
1991
2882
|
return connection.id;
|
|
1992
2883
|
}
|
|
1993
2884
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: DataMapperComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1994
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: DataMapperComponent, isStandalone: true, selector: "data-mapper", inputs: { sourceSchema: "sourceSchema", targetSchema: "targetSchema", sampleData: "sampleData" }, outputs: { mappingsChange: "mappingsChange" }, host: { listeners: { "document:mousemove": "onMouseMove($event)", "document:mouseup": "onMouseUp($event)" } }, viewQueries: [{ propertyName: "svgContainer", first: true, predicate: ["svgContainer"], descendants: true }, { propertyName: "svgElement", first: true, predicate: ["svgElement"], descendants: true }], ngImport: i0, template: "<div class=\"data-mapper\">\n <!-- Header Toolbar -->\n <div class=\"mapper-toolbar\">\n <div class=\"toolbar-title\">\n <mat-icon>account_tree</mat-icon>\n <span>Data Mapper</span>\n </div>\n <div class=\"toolbar-actions\">\n <span class=\"mapping-count\">{{ mappings().length }} mapping(s)</span>\n <button\n mat-icon-button\n matTooltip=\"Clear all mappings\"\n (click)=\"clearAllMappings()\"\n [disabled]=\"mappings().length === 0\"\n >\n <mat-icon>delete_sweep</mat-icon>\n </button>\n </div>\n </div>\n\n <!-- Main Mapper Area -->\n <div class=\"mapper-container\" #svgContainer>\n <!-- Source Schema Panel -->\n <div class=\"schema-panel source-panel\">\n <schema-tree\n [schema]=\"sourceSchema\"\n [side]=\"'source'\"\n [mappings]=\"mappings()\"\n (fieldDragStart)=\"onFieldDragStart($event)\"\n (fieldPositionsChanged)=\"onSourcePositionsChanged($event)\"\n ></schema-tree>\n </div>\n\n <!-- SVG Connection Layer -->\n <svg class=\"connection-layer\" #svgElement>\n <!-- Existing connections -->\n @for (connection of connections(); track trackByConnectionId($index, connection)) {\n <g class=\"connection-group\" [class.selected]=\"connection.isSelected\" [class.array-mapping]=\"connection.isArrayMapping\" [class.array-to-object-mapping]=\"connection.isArrayToObjectMapping\">\n <!-- Connection paths -->\n @for (path of connection.paths; track $index) {\n <!-- Shadow path for glow effect -->\n <path\n [attr.d]=\"path\"\n fill=\"none\"\n [attr.stroke]=\"connection.isArrayToObjectMapping ? '#8b5cf6' : (connection.isArrayMapping ? '#f59e0b' : (connection.isSelected ? '#8b5cf6' : '#6366f1'))\"\n stroke-width=\"6\"\n stroke-opacity=\"0.2\"\n stroke-linecap=\"round\"\n />\n <!-- Main connection path -->\n <path\n [attr.d]=\"path\"\n class=\"connection-path\"\n [class.selected]=\"connection.isSelected\"\n [class.array-path]=\"connection.isArrayMapping\"\n [class.array-to-object-path]=\"connection.isArrayToObjectMapping\"\n [attr.stroke]=\"connection.isArrayToObjectMapping ? '#8b5cf6' : (connection.isArrayMapping ? '#f59e0b' : (connection.isSelected ? '#8b5cf6' : '#6366f1'))\"\n fill=\"none\"\n stroke-width=\"2.5\"\n stroke-linecap=\"round\"\n [attr.stroke-dasharray]=\"connection.isArrayMapping || connection.isArrayToObjectMapping ? '8,4' : 'none'\"\n (click)=\"onConnectionClick(connection, $event)\"\n />\n <!-- Invisible wider path for easier clicking -->\n <path\n [attr.d]=\"path\"\n class=\"connection-hitbox\"\n fill=\"none\"\n stroke=\"transparent\"\n stroke-width=\"20\"\n (click)=\"onConnectionClick(connection, $event)\"\n />\n }\n\n <!-- Transformation node -->\n <g\n class=\"transformation-node\"\n [class.has-transformation]=\"connection.hasTransformation\"\n [class.is-array-mapping]=\"connection.isArrayMapping\"\n [class.is-array-to-object-mapping]=\"connection.isArrayToObjectMapping\"\n [attr.transform]=\"'translate(' + connection.midPoint.x + ',' + connection.midPoint.y + ')'\"\n (click)=\"onTransformationNodeClick(connection, $event)\"\n >\n <circle r=\"14\" class=\"node-bg\" [attr.fill]=\"connection.isArrayToObjectMapping ? '#8b5cf6' : (connection.isArrayMapping ? '#f59e0b' : '')\" [attr.stroke]=\"connection.isArrayToObjectMapping ? '#8b5cf6' : (connection.isArrayMapping ? '#f59e0b' : '')\" />\n <text\n class=\"node-icon\"\n text-anchor=\"middle\"\n dominant-baseline=\"central\"\n font-family=\"Material Icons\"\n font-size=\"16\"\n [attr.fill]=\"connection.isArrayMapping || connection.isArrayToObjectMapping ? 'white' : ''\"\n >\n {{ getTransformationIcon(connection.mappingId) }}\n </text>\n </g>\n </g>\n }\n\n <!-- Drag preview path -->\n @if (dragPath()) {\n <path\n [attr.d]=\"dragPath()\"\n class=\"drag-path\"\n fill=\"none\"\n stroke=\"#6366f1\"\n stroke-width=\"2.5\"\n stroke-dasharray=\"8,4\"\n stroke-linecap=\"round\"\n />\n }\n </svg>\n\n <!-- Target Schema Panel -->\n <div class=\"schema-panel target-panel\">\n <schema-tree\n [schema]=\"targetSchema\"\n [side]=\"'target'\"\n [mappings]=\"mappings()\"\n [defaultValues]=\"defaultValues()\"\n (fieldDrop)=\"onFieldDrop($event)\"\n (fieldPositionsChanged)=\"onTargetPositionsChanged($event)\"\n (fieldDefaultValueClick)=\"onDefaultValueClick($event)\"\n ></schema-tree>\n </div>\n </div>\n\n <!-- Instructions -->\n @if (mappings().length === 0) {\n <div class=\"empty-state\">\n <mat-icon>drag_indicator</mat-icon>\n <p>Drag fields from the source schema to the target schema to create mappings</p>\n </div>\n }\n</div>\n\n<!-- Transformation Popover -->\n@if (showPopover() && selectedMapping()) {\n <transformation-popover\n [mapping]=\"selectedMapping()!\"\n [position]=\"popoverPosition()!\"\n [sampleData]=\"sampleData\"\n (save)=\"onPopoverSave($event)\"\n (delete)=\"onPopoverDelete()\"\n (close)=\"closePopover()\"\n ></transformation-popover>\n}\n\n<!-- Array Filter Modal -->\n@if (showArrayFilterModal() && selectedArrayMapping()) {\n <array-filter-modal\n [arrayMapping]=\"selectedArrayMapping()!\"\n (save)=\"onArrayFilterSave($event)\"\n (close)=\"closeArrayFilterModal()\"\n ></array-filter-modal>\n}\n\n<!-- Array Selector Modal (for array-to-object) -->\n@if (showArraySelectorModal() && selectedArrayToObjectMapping()) {\n <array-selector-modal\n [mapping]=\"selectedArrayToObjectMapping()!\"\n (save)=\"onArraySelectorSave($event)\"\n (close)=\"closeArraySelectorModal()\"\n ></array-selector-modal>\n}\n\n<!-- Default Value Popover -->\n@if (showDefaultValuePopover() && selectedDefaultValueField()) {\n <default-value-popover\n [field]=\"selectedDefaultValueField()!\"\n [existingValue]=\"getExistingDefaultValue(selectedDefaultValueField()!.id)\"\n [position]=\"defaultValuePopoverPosition()!\"\n (save)=\"onDefaultValueSave($event)\"\n (delete)=\"onDefaultValueDelete()\"\n (close)=\"closeDefaultValuePopover()\"\n ></default-value-popover>\n}\n", styles: [":host{display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden;--data-mapper-bg: #f8fafc;--data-mapper-border-radius: 16px;--data-mapper-shadow: 0 4px 20px rgba(0, 0, 0, .08);--data-mapper-border-color: #e2e8f0;--data-mapper-toolbar-bg: white;--data-mapper-toolbar-border: #e2e8f0;--data-mapper-panel-bg: white;--data-mapper-panel-header-bg: #f8fafc;--data-mapper-panel-width: 320px;--data-mapper-panel-border-radius: 12px;--data-mapper-text-primary: #1e293b;--data-mapper-text-secondary: #64748b;--data-mapper-text-muted: #94a3b8;--data-mapper-accent-primary: #6366f1;--data-mapper-accent-success: #22c55e;--data-mapper-accent-warning: #f59e0b;--data-mapper-accent-danger: #ef4444;--data-mapper-connector-color: #6366f1;--data-mapper-connector-width: 2px;--data-mapper-connector-hover-color: #4f46e5;--data-mapper-spacing-sm: 8px;--data-mapper-spacing-md: 16px;--data-mapper-spacing-lg: 24px;--data-mapper-font-size-sm: 12px;--data-mapper-font-size-md: 14px;--data-mapper-font-size-lg: 18px}.data-mapper{display:flex;flex-direction:column;height:100%;min-height:0;flex:1;background:var(--data-mapper-bg);border-radius:var(--data-mapper-border-radius);overflow:hidden;box-shadow:var(--data-mapper-shadow)}.mapper-toolbar{display:flex;align-items:center;justify-content:space-between;padding:var(--data-mapper-spacing-md) var(--data-mapper-spacing-lg);background:var(--data-mapper-toolbar-bg);border-bottom:1px solid var(--data-mapper-border-color);flex-shrink:0}.toolbar-title{display:flex;align-items:center;gap:12px;font-size:var(--data-mapper-font-size-lg);font-weight:600;color:var(--data-mapper-text-primary)}.toolbar-title mat-icon{color:var(--data-mapper-accent-primary)}.toolbar-actions{display:flex;align-items:center;gap:var(--data-mapper-spacing-md)}.mapping-count{font-size:var(--data-mapper-font-size-sm);color:var(--data-mapper-text-secondary);background:#f1f5f9;padding:6px 12px;border-radius:20px}.mapper-container{flex:1;display:flex;position:relative;padding:24px;gap:0;overflow:hidden;min-height:0}.schema-panel{width:var(--data-mapper-panel-width);flex-shrink:0;z-index:2;height:100%;min-height:0;display:flex;flex-direction:column;overflow:hidden}.schema-panel.source-panel{margin-right:auto}.schema-panel.target-panel{margin-left:auto}.schema-panel schema-tree{height:100%;min-height:0;display:flex;flex-direction:column;overflow:hidden}.connection-layer{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:1}.connection-group{pointer-events:auto;cursor:pointer}.connection-group:hover .connection-path{stroke-width:3;filter:drop-shadow(0 2px 4px rgba(99,102,241,.3))}.connection-group:hover .transformation-node .node-bg{transform:scale(1.1)}.connection-group.selected .connection-path{stroke-width:3;filter:drop-shadow(0 2px 8px rgba(139,92,246,.4))}.connection-path{transition:stroke-width .15s ease,filter .15s ease}.connection-hitbox{cursor:pointer}.transformation-node{cursor:pointer;pointer-events:auto}.transformation-node .node-bg{fill:#fff;stroke:#6366f1;stroke-width:2;transition:transform .15s ease,fill .15s ease}.transformation-node .node-icon{fill:#6366f1;pointer-events:none}.transformation-node.has-transformation .node-bg{fill:#6366f1}.transformation-node.has-transformation .node-icon{fill:#fff}.transformation-node:hover .node-bg{transform:scale(1.15);filter:drop-shadow(0 2px 6px rgba(99,102,241,.4))}.drag-path{pointer-events:none;animation:dashMove .5s linear infinite}@keyframes dashMove{0%{stroke-dashoffset:24}to{stroke-dashoffset:0}}.empty-state{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;color:#94a3b8;pointer-events:none}.empty-state mat-icon{font-size:48px;width:48px;height:48px;margin-bottom:16px;opacity:.5}.empty-state p{font-size:14px;max-width:300px;line-height:1.6}@media(max-width:900px){.schema-panel{width:260px}.mapper-container{padding:16px}}@media(max-width:700px){.mapper-container{flex-direction:column;gap:24px}.schema-panel{width:100%;max-height:300px}.connection-layer{display:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i8.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SchemaTreeComponent, selector: "schema-tree", inputs: ["schema", "side", "mappings", "defaultValues"], outputs: ["fieldDragStart", "fieldDragEnd", "fieldDrop", "fieldPositionsChanged", "fieldDefaultValueClick"] }, { kind: "component", type: TransformationPopoverComponent, selector: "transformation-popover", inputs: ["mapping", "position", "sampleData"], outputs: ["save", "delete", "close"] }, { kind: "component", type: ArrayFilterModalComponent, selector: "array-filter-modal", inputs: ["arrayMapping"], outputs: ["save", "close"] }, { kind: "component", type: ArraySelectorModalComponent, selector: "array-selector-modal", inputs: ["mapping"], outputs: ["save", "close"] }, { kind: "component", type: DefaultValuePopoverComponent, selector: "default-value-popover", inputs: ["field", "existingValue", "position"], outputs: ["save", "delete", "close"] }] });
|
|
2885
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: DataMapperComponent, isStandalone: true, selector: "data-mapper", inputs: { sourceSchema: "sourceSchema", targetSchema: "targetSchema", sourceSchemaRef: "sourceSchemaRef", targetSchemaRef: "targetSchemaRef", sampleData: "sampleData" }, outputs: { mappingsChange: "mappingsChange" }, host: { listeners: { "document:mousemove": "onMouseMove($event)", "document:mouseup": "onMouseUp($event)" } }, viewQueries: [{ propertyName: "svgContainer", first: true, predicate: ["svgContainer"], descendants: true }, { propertyName: "svgElement", first: true, predicate: ["svgElement"], descendants: true }], ngImport: i0, template: "<div class=\"data-mapper\">\n <!-- Header Toolbar -->\n <div class=\"mapper-toolbar\">\n <div class=\"toolbar-title\">\n <mat-icon>account_tree</mat-icon>\n <span>Data Mapper</span>\n </div>\n <div class=\"toolbar-actions\">\n <span class=\"mapping-count\">{{ mappings().length }} mapping(s)</span>\n <button\n mat-icon-button\n matTooltip=\"Clear all mappings\"\n (click)=\"clearAllMappings()\"\n [disabled]=\"mappings().length === 0\"\n >\n <mat-icon>delete_sweep</mat-icon>\n </button>\n </div>\n </div>\n\n <!-- Main Mapper Area -->\n <div class=\"mapper-container\" #svgContainer>\n <!-- Source Schema Panel -->\n <div class=\"schema-panel source-panel\">\n <schema-tree\n [schema]=\"sourceSchemaForTree()\"\n [side]=\"'source'\"\n [mappings]=\"mappings()\"\n (fieldDragStart)=\"onFieldDragStart($event)\"\n (fieldPositionsChanged)=\"onSourcePositionsChanged($event)\"\n (sourceDrop)=\"onSourceFieldDrop($event)\"\n ></schema-tree>\n </div>\n\n <!-- SVG Connection Layer -->\n <svg class=\"connection-layer\" #svgElement>\n <!-- Existing connections -->\n @for (connection of connections(); track trackByConnectionId($index, connection)) {\n <g class=\"connection-group\" [class.selected]=\"connection.isSelected\" [class.array-mapping]=\"connection.isArrayMapping\" [class.array-to-object-mapping]=\"connection.isArrayToObjectMapping\" [class.being-dragged]=\"connection.isBeingDragged\">\n <!-- Connection paths -->\n @for (path of connection.paths; track $index) {\n <!-- Shadow path for glow effect -->\n <path\n [attr.d]=\"path\"\n fill=\"none\"\n [attr.stroke]=\"connection.isArrayToObjectMapping ? '#8b5cf6' : (connection.isArrayMapping ? '#f59e0b' : (connection.isSelected ? '#8b5cf6' : '#6366f1'))\"\n stroke-width=\"6\"\n stroke-opacity=\"0.2\"\n stroke-linecap=\"round\"\n />\n <!-- Main connection path -->\n <path\n [attr.d]=\"path\"\n class=\"connection-path\"\n [class.selected]=\"connection.isSelected\"\n [class.array-path]=\"connection.isArrayMapping\"\n [class.array-to-object-path]=\"connection.isArrayToObjectMapping\"\n [attr.stroke]=\"connection.isArrayToObjectMapping ? '#8b5cf6' : (connection.isArrayMapping ? '#f59e0b' : (connection.isSelected ? '#8b5cf6' : '#6366f1'))\"\n fill=\"none\"\n stroke-width=\"2.5\"\n stroke-linecap=\"round\"\n [attr.stroke-dasharray]=\"connection.isArrayMapping || connection.isArrayToObjectMapping ? '8,4' : 'none'\"\n (click)=\"onConnectionClick(connection, $event)\"\n />\n <!-- Invisible wider path for easier clicking -->\n <path\n [attr.d]=\"path\"\n class=\"connection-hitbox\"\n fill=\"none\"\n stroke=\"transparent\"\n stroke-width=\"20\"\n (click)=\"onConnectionClick(connection, $event)\"\n />\n }\n\n <!-- Transformation node -->\n <g\n class=\"transformation-node\"\n [class.has-transformation]=\"connection.hasTransformation\"\n [class.is-array-mapping]=\"connection.isArrayMapping\"\n [class.is-array-to-object-mapping]=\"connection.isArrayToObjectMapping\"\n [attr.transform]=\"'translate(' + connection.midPoint.x + ',' + connection.midPoint.y + ')'\"\n (click)=\"onTransformationNodeClick(connection, $event)\"\n >\n <circle r=\"14\" class=\"node-bg\" [attr.fill]=\"connection.isArrayToObjectMapping ? '#8b5cf6' : (connection.isArrayMapping ? '#f59e0b' : '')\" [attr.stroke]=\"connection.isArrayToObjectMapping ? '#8b5cf6' : (connection.isArrayMapping ? '#f59e0b' : '')\" />\n <text\n class=\"node-icon\"\n text-anchor=\"middle\"\n dominant-baseline=\"central\"\n font-family=\"Material Icons\"\n font-size=\"16\"\n [attr.fill]=\"connection.isArrayMapping || connection.isArrayToObjectMapping ? 'white' : ''\"\n >\n {{ getTransformationIcon(connection.mappingId) }}\n </text>\n </g>\n\n <!-- Source endpoint circles (draggable) -->\n @for (sourcePoint of connection.sourcePoints; track $index) {\n <circle\n class=\"endpoint-handle source-endpoint\"\n [attr.cx]=\"sourcePoint.x\"\n [attr.cy]=\"sourcePoint.y\"\n r=\"6\"\n [class.selected]=\"connection.isSelected\"\n (mousedown)=\"onEndpointDragStart(connection, 'source', $index, $event)\"\n />\n }\n\n <!-- Target endpoint circle (draggable) -->\n <circle\n class=\"endpoint-handle target-endpoint\"\n [attr.cx]=\"connection.targetPoint.x\"\n [attr.cy]=\"connection.targetPoint.y\"\n r=\"6\"\n [class.selected]=\"connection.isSelected\"\n (mousedown)=\"onEndpointDragStart(connection, 'target', 0, $event)\"\n />\n </g>\n }\n\n <!-- Drag preview path -->\n @if (dragPath()) {\n <path\n [attr.d]=\"dragPath()\"\n class=\"drag-path\"\n fill=\"none\"\n stroke=\"#6366f1\"\n stroke-width=\"2.5\"\n stroke-dasharray=\"8,4\"\n stroke-linecap=\"round\"\n />\n }\n </svg>\n\n <!-- Target Schema Panel -->\n <div class=\"schema-panel target-panel\">\n <schema-tree\n [schema]=\"targetSchemaForTree()\"\n [side]=\"'target'\"\n [mappings]=\"mappings()\"\n [defaultValues]=\"defaultValues()\"\n (fieldDrop)=\"onFieldDrop($event)\"\n (fieldPositionsChanged)=\"onTargetPositionsChanged($event)\"\n (fieldDefaultValueClick)=\"onDefaultValueClick($event)\"\n ></schema-tree>\n </div>\n </div>\n\n <!-- Instructions -->\n @if (mappings().length === 0) {\n <div class=\"empty-state\">\n <mat-icon>drag_indicator</mat-icon>\n <p>Drag fields from the source schema to the target schema to create mappings</p>\n </div>\n }\n</div>\n\n<!-- Transformation Popover -->\n@if (showPopover() && selectedMapping()) {\n <transformation-popover\n [mapping]=\"selectedMapping()!\"\n [position]=\"popoverPosition()!\"\n [sampleData]=\"sampleData\"\n (save)=\"onPopoverSave($event)\"\n (delete)=\"onPopoverDelete()\"\n (close)=\"closePopover()\"\n ></transformation-popover>\n}\n\n<!-- Array Filter Modal -->\n@if (showArrayFilterModal() && selectedArrayMapping()) {\n <array-filter-modal\n [arrayMapping]=\"selectedArrayMapping()!\"\n (save)=\"onArrayFilterSave($event)\"\n (close)=\"closeArrayFilterModal()\"\n ></array-filter-modal>\n}\n\n<!-- Array Selector Modal (for array-to-object) -->\n@if (showArraySelectorModal() && selectedArrayToObjectMapping()) {\n <array-selector-modal\n [mapping]=\"selectedArrayToObjectMapping()!\"\n (save)=\"onArraySelectorSave($event)\"\n (close)=\"closeArraySelectorModal()\"\n ></array-selector-modal>\n}\n\n<!-- Default Value Popover -->\n@if (showDefaultValuePopover() && selectedDefaultValueField()) {\n <default-value-popover\n [field]=\"selectedDefaultValueField()!\"\n [existingValue]=\"getExistingDefaultValue(selectedDefaultValueField()!.id)\"\n [position]=\"defaultValuePopoverPosition()!\"\n (save)=\"onDefaultValueSave($event)\"\n (delete)=\"onDefaultValueDelete()\"\n (close)=\"closeDefaultValuePopover()\"\n ></default-value-popover>\n}\n", styles: [":host{display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden;--data-mapper-bg: #f8fafc;--data-mapper-border-radius: 16px;--data-mapper-shadow: 0 4px 20px rgba(0, 0, 0, .08);--data-mapper-border-color: #e2e8f0;--data-mapper-toolbar-bg: white;--data-mapper-toolbar-border: #e2e8f0;--data-mapper-panel-bg: white;--data-mapper-panel-header-bg: #f8fafc;--data-mapper-panel-width: 320px;--data-mapper-panel-border-radius: 12px;--data-mapper-text-primary: #1e293b;--data-mapper-text-secondary: #64748b;--data-mapper-text-muted: #94a3b8;--data-mapper-accent-primary: #6366f1;--data-mapper-accent-success: #22c55e;--data-mapper-accent-warning: #f59e0b;--data-mapper-accent-danger: #ef4444;--data-mapper-connector-color: #6366f1;--data-mapper-connector-width: 2px;--data-mapper-connector-hover-color: #4f46e5;--data-mapper-spacing-sm: 8px;--data-mapper-spacing-md: 16px;--data-mapper-spacing-lg: 24px;--data-mapper-font-size-sm: 12px;--data-mapper-font-size-md: 14px;--data-mapper-font-size-lg: 18px}.data-mapper{display:flex;flex-direction:column;height:100%;min-height:0;flex:1;background:var(--data-mapper-bg);border-radius:var(--data-mapper-border-radius);overflow:hidden;box-shadow:var(--data-mapper-shadow)}.mapper-toolbar{display:flex;align-items:center;justify-content:space-between;padding:var(--data-mapper-spacing-md) var(--data-mapper-spacing-lg);background:var(--data-mapper-toolbar-bg);border-bottom:1px solid var(--data-mapper-border-color);flex-shrink:0}.toolbar-title{display:flex;align-items:center;gap:12px;font-size:var(--data-mapper-font-size-lg);font-weight:600;color:var(--data-mapper-text-primary)}.toolbar-title mat-icon{color:var(--data-mapper-accent-primary)}.toolbar-actions{display:flex;align-items:center;gap:var(--data-mapper-spacing-md)}.mapping-count{font-size:var(--data-mapper-font-size-sm);color:var(--data-mapper-text-secondary);background:#f1f5f9;padding:6px 12px;border-radius:20px}.mapper-container{flex:1;display:flex;position:relative;padding:24px;gap:0;overflow:hidden;min-height:0}.schema-panel{width:var(--data-mapper-panel-width);flex-shrink:0;z-index:2;height:100%;min-height:0;display:flex;flex-direction:column;overflow:hidden}.schema-panel.source-panel{margin-right:auto}.schema-panel.target-panel{margin-left:auto}.schema-panel schema-tree{height:100%;min-height:0;display:flex;flex-direction:column;overflow:hidden}.connection-layer{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:3}.connection-group{pointer-events:auto;cursor:pointer}.connection-group:hover .connection-path{stroke-width:3;filter:drop-shadow(0 2px 4px rgba(99,102,241,.3))}.connection-group:hover .transformation-node .node-bg{transform:scale(1.1)}.connection-group.selected .connection-path{stroke-width:3;filter:drop-shadow(0 2px 8px rgba(139,92,246,.4))}.connection-group.being-dragged{opacity:.3;pointer-events:none}.connection-group.being-dragged .endpoint-handle{opacity:0}.connection-path{transition:stroke-width .15s ease,filter .15s ease}.connection-hitbox{cursor:pointer}.transformation-node{cursor:pointer;pointer-events:auto}.transformation-node .node-bg{fill:#fff;stroke:#6366f1;stroke-width:2;transition:transform .15s ease,fill .15s ease}.transformation-node .node-icon{fill:#6366f1;pointer-events:none}.transformation-node.has-transformation .node-bg{fill:#6366f1}.transformation-node.has-transformation .node-icon{fill:#fff}.transformation-node:hover .node-bg{transform:scale(1.15);filter:drop-shadow(0 2px 6px rgba(99,102,241,.4))}.drag-path{pointer-events:none;animation:dashMove .5s linear infinite}@keyframes dashMove{0%{stroke-dashoffset:24}to{stroke-dashoffset:0}}.endpoint-handle{fill:#fff;stroke:#6366f1;stroke-width:2;cursor:grab;opacity:0;transition:opacity .15s ease,transform .15s ease,r .15s ease;pointer-events:auto}.endpoint-handle:hover{opacity:1;r:8;fill:#6366f1;cursor:grab}.endpoint-handle:active{cursor:grabbing}.endpoint-handle.selected,.connection-group:hover .endpoint-handle{opacity:.7}.connection-group.selected .endpoint-handle{opacity:1}.empty-state{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;color:#94a3b8;pointer-events:none}.empty-state mat-icon{font-size:48px;width:48px;height:48px;margin-bottom:16px;opacity:.5}.empty-state p{font-size:14px;max-width:300px;line-height:1.6}@media(max-width:900px){.schema-panel{width:260px}.mapper-container{padding:16px}}@media(max-width:700px){.mapper-container{flex-direction:column;gap:24px}.schema-panel{width:100%;max-height:300px}.connection-layer{display:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SchemaTreeComponent, selector: "schema-tree", inputs: ["schema", "side", "mappings", "defaultValues"], outputs: ["fieldDragStart", "fieldDragEnd", "fieldDrop", "sourceDrop", "fieldPositionsChanged", "fieldDefaultValueClick"] }, { kind: "component", type: TransformationPopoverComponent, selector: "transformation-popover", inputs: ["mapping", "position", "sampleData"], outputs: ["save", "delete", "close"] }, { kind: "component", type: ArrayFilterModalComponent, selector: "array-filter-modal", inputs: ["arrayMapping"], outputs: ["save", "close"] }, { kind: "component", type: ArraySelectorModalComponent, selector: "array-selector-modal", inputs: ["mapping"], outputs: ["save", "close"] }, { kind: "component", type: DefaultValuePopoverComponent, selector: "default-value-popover", inputs: ["field", "existingValue", "position"], outputs: ["save", "delete", "close"] }] });
|
|
1995
2886
|
}
|
|
1996
2887
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: DataMapperComponent, decorators: [{
|
|
1997
2888
|
type: Component,
|
|
@@ -2005,11 +2896,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
2005
2896
|
ArrayFilterModalComponent,
|
|
2006
2897
|
ArraySelectorModalComponent,
|
|
2007
2898
|
DefaultValuePopoverComponent,
|
|
2008
|
-
], template: "<div class=\"data-mapper\">\n <!-- Header Toolbar -->\n <div class=\"mapper-toolbar\">\n <div class=\"toolbar-title\">\n <mat-icon>account_tree</mat-icon>\n <span>Data Mapper</span>\n </div>\n <div class=\"toolbar-actions\">\n <span class=\"mapping-count\">{{ mappings().length }} mapping(s)</span>\n <button\n mat-icon-button\n matTooltip=\"Clear all mappings\"\n (click)=\"clearAllMappings()\"\n [disabled]=\"mappings().length === 0\"\n >\n <mat-icon>delete_sweep</mat-icon>\n </button>\n </div>\n </div>\n\n <!-- Main Mapper Area -->\n <div class=\"mapper-container\" #svgContainer>\n <!-- Source Schema Panel -->\n <div class=\"schema-panel source-panel\">\n <schema-tree\n [schema]=\"sourceSchema\"\n [side]=\"'source'\"\n [mappings]=\"mappings()\"\n (fieldDragStart)=\"onFieldDragStart($event)\"\n (fieldPositionsChanged)=\"onSourcePositionsChanged($event)\"\n ></schema-tree>\n </div>\n\n <!-- SVG Connection Layer -->\n <svg class=\"connection-layer\" #svgElement>\n <!-- Existing connections -->\n @for (connection of connections(); track trackByConnectionId($index, connection)) {\n <g class=\"connection-group\" [class.selected]=\"connection.isSelected\" [class.array-mapping]=\"connection.isArrayMapping\" [class.array-to-object-mapping]=\"connection.isArrayToObjectMapping\">\n <!-- Connection paths -->\n @for (path of connection.paths; track $index) {\n <!-- Shadow path for glow effect -->\n <path\n [attr.d]=\"path\"\n fill=\"none\"\n [attr.stroke]=\"connection.isArrayToObjectMapping ? '#8b5cf6' : (connection.isArrayMapping ? '#f59e0b' : (connection.isSelected ? '#8b5cf6' : '#6366f1'))\"\n stroke-width=\"6\"\n stroke-opacity=\"0.2\"\n stroke-linecap=\"round\"\n />\n <!-- Main connection path -->\n <path\n [attr.d]=\"path\"\n class=\"connection-path\"\n [class.selected]=\"connection.isSelected\"\n [class.array-path]=\"connection.isArrayMapping\"\n [class.array-to-object-path]=\"connection.isArrayToObjectMapping\"\n [attr.stroke]=\"connection.isArrayToObjectMapping ? '#8b5cf6' : (connection.isArrayMapping ? '#f59e0b' : (connection.isSelected ? '#8b5cf6' : '#6366f1'))\"\n fill=\"none\"\n stroke-width=\"2.5\"\n stroke-linecap=\"round\"\n [attr.stroke-dasharray]=\"connection.isArrayMapping || connection.isArrayToObjectMapping ? '8,4' : 'none'\"\n (click)=\"onConnectionClick(connection, $event)\"\n />\n <!-- Invisible wider path for easier clicking -->\n <path\n [attr.d]=\"path\"\n class=\"connection-hitbox\"\n fill=\"none\"\n stroke=\"transparent\"\n stroke-width=\"20\"\n (click)=\"onConnectionClick(connection, $event)\"\n />\n }\n\n <!-- Transformation node -->\n <g\n class=\"transformation-node\"\n [class.has-transformation]=\"connection.hasTransformation\"\n [class.is-array-mapping]=\"connection.isArrayMapping\"\n [class.is-array-to-object-mapping]=\"connection.isArrayToObjectMapping\"\n [attr.transform]=\"'translate(' + connection.midPoint.x + ',' + connection.midPoint.y + ')'\"\n (click)=\"onTransformationNodeClick(connection, $event)\"\n >\n <circle r=\"14\" class=\"node-bg\" [attr.fill]=\"connection.isArrayToObjectMapping ? '#8b5cf6' : (connection.isArrayMapping ? '#f59e0b' : '')\" [attr.stroke]=\"connection.isArrayToObjectMapping ? '#8b5cf6' : (connection.isArrayMapping ? '#f59e0b' : '')\" />\n <text\n class=\"node-icon\"\n text-anchor=\"middle\"\n dominant-baseline=\"central\"\n font-family=\"Material Icons\"\n font-size=\"16\"\n [attr.fill]=\"connection.isArrayMapping || connection.isArrayToObjectMapping ? 'white' : ''\"\n >\n {{ getTransformationIcon(connection.mappingId) }}\n </text>\n </g>\n </g>\n }\n\n <!-- Drag preview path -->\n @if (dragPath()) {\n <path\n [attr.d]=\"dragPath()\"\n class=\"drag-path\"\n fill=\"none\"\n stroke=\"#6366f1\"\n stroke-width=\"2.5\"\n stroke-dasharray=\"8,4\"\n stroke-linecap=\"round\"\n />\n }\n </svg>\n\n <!-- Target Schema Panel -->\n <div class=\"schema-panel target-panel\">\n <schema-tree\n [schema]=\"targetSchema\"\n [side]=\"'target'\"\n [mappings]=\"mappings()\"\n [defaultValues]=\"defaultValues()\"\n (fieldDrop)=\"onFieldDrop($event)\"\n (fieldPositionsChanged)=\"onTargetPositionsChanged($event)\"\n (fieldDefaultValueClick)=\"onDefaultValueClick($event)\"\n ></schema-tree>\n </div>\n </div>\n\n <!-- Instructions -->\n @if (mappings().length === 0) {\n <div class=\"empty-state\">\n <mat-icon>drag_indicator</mat-icon>\n <p>Drag fields from the source schema to the target schema to create mappings</p>\n </div>\n }\n</div>\n\n<!-- Transformation Popover -->\n@if (showPopover() && selectedMapping()) {\n <transformation-popover\n [mapping]=\"selectedMapping()!\"\n [position]=\"popoverPosition()!\"\n [sampleData]=\"sampleData\"\n (save)=\"onPopoverSave($event)\"\n (delete)=\"onPopoverDelete()\"\n (close)=\"closePopover()\"\n ></transformation-popover>\n}\n\n<!-- Array Filter Modal -->\n@if (showArrayFilterModal() && selectedArrayMapping()) {\n <array-filter-modal\n [arrayMapping]=\"selectedArrayMapping()!\"\n (save)=\"onArrayFilterSave($event)\"\n (close)=\"closeArrayFilterModal()\"\n ></array-filter-modal>\n}\n\n<!-- Array Selector Modal (for array-to-object) -->\n@if (showArraySelectorModal() && selectedArrayToObjectMapping()) {\n <array-selector-modal\n [mapping]=\"selectedArrayToObjectMapping()!\"\n (save)=\"onArraySelectorSave($event)\"\n (close)=\"closeArraySelectorModal()\"\n ></array-selector-modal>\n}\n\n<!-- Default Value Popover -->\n@if (showDefaultValuePopover() && selectedDefaultValueField()) {\n <default-value-popover\n [field]=\"selectedDefaultValueField()!\"\n [existingValue]=\"getExistingDefaultValue(selectedDefaultValueField()!.id)\"\n [position]=\"defaultValuePopoverPosition()!\"\n (save)=\"onDefaultValueSave($event)\"\n (delete)=\"onDefaultValueDelete()\"\n (close)=\"closeDefaultValuePopover()\"\n ></default-value-popover>\n}\n", styles: [":host{display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden;--data-mapper-bg: #f8fafc;--data-mapper-border-radius: 16px;--data-mapper-shadow: 0 4px 20px rgba(0, 0, 0, .08);--data-mapper-border-color: #e2e8f0;--data-mapper-toolbar-bg: white;--data-mapper-toolbar-border: #e2e8f0;--data-mapper-panel-bg: white;--data-mapper-panel-header-bg: #f8fafc;--data-mapper-panel-width: 320px;--data-mapper-panel-border-radius: 12px;--data-mapper-text-primary: #1e293b;--data-mapper-text-secondary: #64748b;--data-mapper-text-muted: #94a3b8;--data-mapper-accent-primary: #6366f1;--data-mapper-accent-success: #22c55e;--data-mapper-accent-warning: #f59e0b;--data-mapper-accent-danger: #ef4444;--data-mapper-connector-color: #6366f1;--data-mapper-connector-width: 2px;--data-mapper-connector-hover-color: #4f46e5;--data-mapper-spacing-sm: 8px;--data-mapper-spacing-md: 16px;--data-mapper-spacing-lg: 24px;--data-mapper-font-size-sm: 12px;--data-mapper-font-size-md: 14px;--data-mapper-font-size-lg: 18px}.data-mapper{display:flex;flex-direction:column;height:100%;min-height:0;flex:1;background:var(--data-mapper-bg);border-radius:var(--data-mapper-border-radius);overflow:hidden;box-shadow:var(--data-mapper-shadow)}.mapper-toolbar{display:flex;align-items:center;justify-content:space-between;padding:var(--data-mapper-spacing-md) var(--data-mapper-spacing-lg);background:var(--data-mapper-toolbar-bg);border-bottom:1px solid var(--data-mapper-border-color);flex-shrink:0}.toolbar-title{display:flex;align-items:center;gap:12px;font-size:var(--data-mapper-font-size-lg);font-weight:600;color:var(--data-mapper-text-primary)}.toolbar-title mat-icon{color:var(--data-mapper-accent-primary)}.toolbar-actions{display:flex;align-items:center;gap:var(--data-mapper-spacing-md)}.mapping-count{font-size:var(--data-mapper-font-size-sm);color:var(--data-mapper-text-secondary);background:#f1f5f9;padding:6px 12px;border-radius:20px}.mapper-container{flex:1;display:flex;position:relative;padding:24px;gap:0;overflow:hidden;min-height:0}.schema-panel{width:var(--data-mapper-panel-width);flex-shrink:0;z-index:2;height:100%;min-height:0;display:flex;flex-direction:column;overflow:hidden}.schema-panel.source-panel{margin-right:auto}.schema-panel.target-panel{margin-left:auto}.schema-panel schema-tree{height:100%;min-height:0;display:flex;flex-direction:column;overflow:hidden}.connection-layer{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:1}.connection-group{pointer-events:auto;cursor:pointer}.connection-group:hover .connection-path{stroke-width:3;filter:drop-shadow(0 2px 4px rgba(99,102,241,.3))}.connection-group:hover .transformation-node .node-bg{transform:scale(1.1)}.connection-group.selected .connection-path{stroke-width:3;filter:drop-shadow(0 2px 8px rgba(139,92,246,.4))}.connection-path{transition:stroke-width .15s ease,filter .15s ease}.connection-hitbox{cursor:pointer}.transformation-node{cursor:pointer;pointer-events:auto}.transformation-node .node-bg{fill:#fff;stroke:#6366f1;stroke-width:2;transition:transform .15s ease,fill .15s ease}.transformation-node .node-icon{fill:#6366f1;pointer-events:none}.transformation-node.has-transformation .node-bg{fill:#6366f1}.transformation-node.has-transformation .node-icon{fill:#fff}.transformation-node:hover .node-bg{transform:scale(1.15);filter:drop-shadow(0 2px 6px rgba(99,102,241,.4))}.drag-path{pointer-events:none;animation:dashMove .5s linear infinite}@keyframes dashMove{0%{stroke-dashoffset:24}to{stroke-dashoffset:0}}.empty-state{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;color:#94a3b8;pointer-events:none}.empty-state mat-icon{font-size:48px;width:48px;height:48px;margin-bottom:16px;opacity:.5}.empty-state p{font-size:14px;max-width:300px;line-height:1.6}@media(max-width:900px){.schema-panel{width:260px}.mapper-container{padding:16px}}@media(max-width:700px){.mapper-container{flex-direction:column;gap:24px}.schema-panel{width:100%;max-height:300px}.connection-layer{display:none}}\n"] }]
|
|
2899
|
+
], template: "<div class=\"data-mapper\">\n <!-- Header Toolbar -->\n <div class=\"mapper-toolbar\">\n <div class=\"toolbar-title\">\n <mat-icon>account_tree</mat-icon>\n <span>Data Mapper</span>\n </div>\n <div class=\"toolbar-actions\">\n <span class=\"mapping-count\">{{ mappings().length }} mapping(s)</span>\n <button\n mat-icon-button\n matTooltip=\"Clear all mappings\"\n (click)=\"clearAllMappings()\"\n [disabled]=\"mappings().length === 0\"\n >\n <mat-icon>delete_sweep</mat-icon>\n </button>\n </div>\n </div>\n\n <!-- Main Mapper Area -->\n <div class=\"mapper-container\" #svgContainer>\n <!-- Source Schema Panel -->\n <div class=\"schema-panel source-panel\">\n <schema-tree\n [schema]=\"sourceSchemaForTree()\"\n [side]=\"'source'\"\n [mappings]=\"mappings()\"\n (fieldDragStart)=\"onFieldDragStart($event)\"\n (fieldPositionsChanged)=\"onSourcePositionsChanged($event)\"\n (sourceDrop)=\"onSourceFieldDrop($event)\"\n ></schema-tree>\n </div>\n\n <!-- SVG Connection Layer -->\n <svg class=\"connection-layer\" #svgElement>\n <!-- Existing connections -->\n @for (connection of connections(); track trackByConnectionId($index, connection)) {\n <g class=\"connection-group\" [class.selected]=\"connection.isSelected\" [class.array-mapping]=\"connection.isArrayMapping\" [class.array-to-object-mapping]=\"connection.isArrayToObjectMapping\" [class.being-dragged]=\"connection.isBeingDragged\">\n <!-- Connection paths -->\n @for (path of connection.paths; track $index) {\n <!-- Shadow path for glow effect -->\n <path\n [attr.d]=\"path\"\n fill=\"none\"\n [attr.stroke]=\"connection.isArrayToObjectMapping ? '#8b5cf6' : (connection.isArrayMapping ? '#f59e0b' : (connection.isSelected ? '#8b5cf6' : '#6366f1'))\"\n stroke-width=\"6\"\n stroke-opacity=\"0.2\"\n stroke-linecap=\"round\"\n />\n <!-- Main connection path -->\n <path\n [attr.d]=\"path\"\n class=\"connection-path\"\n [class.selected]=\"connection.isSelected\"\n [class.array-path]=\"connection.isArrayMapping\"\n [class.array-to-object-path]=\"connection.isArrayToObjectMapping\"\n [attr.stroke]=\"connection.isArrayToObjectMapping ? '#8b5cf6' : (connection.isArrayMapping ? '#f59e0b' : (connection.isSelected ? '#8b5cf6' : '#6366f1'))\"\n fill=\"none\"\n stroke-width=\"2.5\"\n stroke-linecap=\"round\"\n [attr.stroke-dasharray]=\"connection.isArrayMapping || connection.isArrayToObjectMapping ? '8,4' : 'none'\"\n (click)=\"onConnectionClick(connection, $event)\"\n />\n <!-- Invisible wider path for easier clicking -->\n <path\n [attr.d]=\"path\"\n class=\"connection-hitbox\"\n fill=\"none\"\n stroke=\"transparent\"\n stroke-width=\"20\"\n (click)=\"onConnectionClick(connection, $event)\"\n />\n }\n\n <!-- Transformation node -->\n <g\n class=\"transformation-node\"\n [class.has-transformation]=\"connection.hasTransformation\"\n [class.is-array-mapping]=\"connection.isArrayMapping\"\n [class.is-array-to-object-mapping]=\"connection.isArrayToObjectMapping\"\n [attr.transform]=\"'translate(' + connection.midPoint.x + ',' + connection.midPoint.y + ')'\"\n (click)=\"onTransformationNodeClick(connection, $event)\"\n >\n <circle r=\"14\" class=\"node-bg\" [attr.fill]=\"connection.isArrayToObjectMapping ? '#8b5cf6' : (connection.isArrayMapping ? '#f59e0b' : '')\" [attr.stroke]=\"connection.isArrayToObjectMapping ? '#8b5cf6' : (connection.isArrayMapping ? '#f59e0b' : '')\" />\n <text\n class=\"node-icon\"\n text-anchor=\"middle\"\n dominant-baseline=\"central\"\n font-family=\"Material Icons\"\n font-size=\"16\"\n [attr.fill]=\"connection.isArrayMapping || connection.isArrayToObjectMapping ? 'white' : ''\"\n >\n {{ getTransformationIcon(connection.mappingId) }}\n </text>\n </g>\n\n <!-- Source endpoint circles (draggable) -->\n @for (sourcePoint of connection.sourcePoints; track $index) {\n <circle\n class=\"endpoint-handle source-endpoint\"\n [attr.cx]=\"sourcePoint.x\"\n [attr.cy]=\"sourcePoint.y\"\n r=\"6\"\n [class.selected]=\"connection.isSelected\"\n (mousedown)=\"onEndpointDragStart(connection, 'source', $index, $event)\"\n />\n }\n\n <!-- Target endpoint circle (draggable) -->\n <circle\n class=\"endpoint-handle target-endpoint\"\n [attr.cx]=\"connection.targetPoint.x\"\n [attr.cy]=\"connection.targetPoint.y\"\n r=\"6\"\n [class.selected]=\"connection.isSelected\"\n (mousedown)=\"onEndpointDragStart(connection, 'target', 0, $event)\"\n />\n </g>\n }\n\n <!-- Drag preview path -->\n @if (dragPath()) {\n <path\n [attr.d]=\"dragPath()\"\n class=\"drag-path\"\n fill=\"none\"\n stroke=\"#6366f1\"\n stroke-width=\"2.5\"\n stroke-dasharray=\"8,4\"\n stroke-linecap=\"round\"\n />\n }\n </svg>\n\n <!-- Target Schema Panel -->\n <div class=\"schema-panel target-panel\">\n <schema-tree\n [schema]=\"targetSchemaForTree()\"\n [side]=\"'target'\"\n [mappings]=\"mappings()\"\n [defaultValues]=\"defaultValues()\"\n (fieldDrop)=\"onFieldDrop($event)\"\n (fieldPositionsChanged)=\"onTargetPositionsChanged($event)\"\n (fieldDefaultValueClick)=\"onDefaultValueClick($event)\"\n ></schema-tree>\n </div>\n </div>\n\n <!-- Instructions -->\n @if (mappings().length === 0) {\n <div class=\"empty-state\">\n <mat-icon>drag_indicator</mat-icon>\n <p>Drag fields from the source schema to the target schema to create mappings</p>\n </div>\n }\n</div>\n\n<!-- Transformation Popover -->\n@if (showPopover() && selectedMapping()) {\n <transformation-popover\n [mapping]=\"selectedMapping()!\"\n [position]=\"popoverPosition()!\"\n [sampleData]=\"sampleData\"\n (save)=\"onPopoverSave($event)\"\n (delete)=\"onPopoverDelete()\"\n (close)=\"closePopover()\"\n ></transformation-popover>\n}\n\n<!-- Array Filter Modal -->\n@if (showArrayFilterModal() && selectedArrayMapping()) {\n <array-filter-modal\n [arrayMapping]=\"selectedArrayMapping()!\"\n (save)=\"onArrayFilterSave($event)\"\n (close)=\"closeArrayFilterModal()\"\n ></array-filter-modal>\n}\n\n<!-- Array Selector Modal (for array-to-object) -->\n@if (showArraySelectorModal() && selectedArrayToObjectMapping()) {\n <array-selector-modal\n [mapping]=\"selectedArrayToObjectMapping()!\"\n (save)=\"onArraySelectorSave($event)\"\n (close)=\"closeArraySelectorModal()\"\n ></array-selector-modal>\n}\n\n<!-- Default Value Popover -->\n@if (showDefaultValuePopover() && selectedDefaultValueField()) {\n <default-value-popover\n [field]=\"selectedDefaultValueField()!\"\n [existingValue]=\"getExistingDefaultValue(selectedDefaultValueField()!.id)\"\n [position]=\"defaultValuePopoverPosition()!\"\n (save)=\"onDefaultValueSave($event)\"\n (delete)=\"onDefaultValueDelete()\"\n (close)=\"closeDefaultValuePopover()\"\n ></default-value-popover>\n}\n", styles: [":host{display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden;--data-mapper-bg: #f8fafc;--data-mapper-border-radius: 16px;--data-mapper-shadow: 0 4px 20px rgba(0, 0, 0, .08);--data-mapper-border-color: #e2e8f0;--data-mapper-toolbar-bg: white;--data-mapper-toolbar-border: #e2e8f0;--data-mapper-panel-bg: white;--data-mapper-panel-header-bg: #f8fafc;--data-mapper-panel-width: 320px;--data-mapper-panel-border-radius: 12px;--data-mapper-text-primary: #1e293b;--data-mapper-text-secondary: #64748b;--data-mapper-text-muted: #94a3b8;--data-mapper-accent-primary: #6366f1;--data-mapper-accent-success: #22c55e;--data-mapper-accent-warning: #f59e0b;--data-mapper-accent-danger: #ef4444;--data-mapper-connector-color: #6366f1;--data-mapper-connector-width: 2px;--data-mapper-connector-hover-color: #4f46e5;--data-mapper-spacing-sm: 8px;--data-mapper-spacing-md: 16px;--data-mapper-spacing-lg: 24px;--data-mapper-font-size-sm: 12px;--data-mapper-font-size-md: 14px;--data-mapper-font-size-lg: 18px}.data-mapper{display:flex;flex-direction:column;height:100%;min-height:0;flex:1;background:var(--data-mapper-bg);border-radius:var(--data-mapper-border-radius);overflow:hidden;box-shadow:var(--data-mapper-shadow)}.mapper-toolbar{display:flex;align-items:center;justify-content:space-between;padding:var(--data-mapper-spacing-md) var(--data-mapper-spacing-lg);background:var(--data-mapper-toolbar-bg);border-bottom:1px solid var(--data-mapper-border-color);flex-shrink:0}.toolbar-title{display:flex;align-items:center;gap:12px;font-size:var(--data-mapper-font-size-lg);font-weight:600;color:var(--data-mapper-text-primary)}.toolbar-title mat-icon{color:var(--data-mapper-accent-primary)}.toolbar-actions{display:flex;align-items:center;gap:var(--data-mapper-spacing-md)}.mapping-count{font-size:var(--data-mapper-font-size-sm);color:var(--data-mapper-text-secondary);background:#f1f5f9;padding:6px 12px;border-radius:20px}.mapper-container{flex:1;display:flex;position:relative;padding:24px;gap:0;overflow:hidden;min-height:0}.schema-panel{width:var(--data-mapper-panel-width);flex-shrink:0;z-index:2;height:100%;min-height:0;display:flex;flex-direction:column;overflow:hidden}.schema-panel.source-panel{margin-right:auto}.schema-panel.target-panel{margin-left:auto}.schema-panel schema-tree{height:100%;min-height:0;display:flex;flex-direction:column;overflow:hidden}.connection-layer{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:3}.connection-group{pointer-events:auto;cursor:pointer}.connection-group:hover .connection-path{stroke-width:3;filter:drop-shadow(0 2px 4px rgba(99,102,241,.3))}.connection-group:hover .transformation-node .node-bg{transform:scale(1.1)}.connection-group.selected .connection-path{stroke-width:3;filter:drop-shadow(0 2px 8px rgba(139,92,246,.4))}.connection-group.being-dragged{opacity:.3;pointer-events:none}.connection-group.being-dragged .endpoint-handle{opacity:0}.connection-path{transition:stroke-width .15s ease,filter .15s ease}.connection-hitbox{cursor:pointer}.transformation-node{cursor:pointer;pointer-events:auto}.transformation-node .node-bg{fill:#fff;stroke:#6366f1;stroke-width:2;transition:transform .15s ease,fill .15s ease}.transformation-node .node-icon{fill:#6366f1;pointer-events:none}.transformation-node.has-transformation .node-bg{fill:#6366f1}.transformation-node.has-transformation .node-icon{fill:#fff}.transformation-node:hover .node-bg{transform:scale(1.15);filter:drop-shadow(0 2px 6px rgba(99,102,241,.4))}.drag-path{pointer-events:none;animation:dashMove .5s linear infinite}@keyframes dashMove{0%{stroke-dashoffset:24}to{stroke-dashoffset:0}}.endpoint-handle{fill:#fff;stroke:#6366f1;stroke-width:2;cursor:grab;opacity:0;transition:opacity .15s ease,transform .15s ease,r .15s ease;pointer-events:auto}.endpoint-handle:hover{opacity:1;r:8;fill:#6366f1;cursor:grab}.endpoint-handle:active{cursor:grabbing}.endpoint-handle.selected,.connection-group:hover .endpoint-handle{opacity:.7}.connection-group.selected .endpoint-handle{opacity:1}.empty-state{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;color:#94a3b8;pointer-events:none}.empty-state mat-icon{font-size:48px;width:48px;height:48px;margin-bottom:16px;opacity:.5}.empty-state p{font-size:14px;max-width:300px;line-height:1.6}@media(max-width:900px){.schema-panel{width:260px}.mapper-container{padding:16px}}@media(max-width:700px){.mapper-container{flex-direction:column;gap:24px}.schema-panel{width:100%;max-height:300px}.connection-layer{display:none}}\n"] }]
|
|
2009
2900
|
}], propDecorators: { sourceSchema: [{
|
|
2010
2901
|
type: Input
|
|
2011
2902
|
}], targetSchema: [{
|
|
2012
2903
|
type: Input
|
|
2904
|
+
}], sourceSchemaRef: [{
|
|
2905
|
+
type: Input
|
|
2906
|
+
}], targetSchemaRef: [{
|
|
2907
|
+
type: Input
|
|
2013
2908
|
}], sampleData: [{
|
|
2014
2909
|
type: Input
|
|
2015
2910
|
}], mappingsChange: [{
|
|
@@ -2031,14 +2926,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
2031
2926
|
class SchemaEditorComponent {
|
|
2032
2927
|
set schema(value) {
|
|
2033
2928
|
if (value) {
|
|
2034
|
-
|
|
2035
|
-
this.fields.
|
|
2929
|
+
// Don't overwrite fields if we have uncommitted changes (fields being edited or with empty names)
|
|
2930
|
+
const hasUncommittedChanges = this.fields().some(f => f.isEditing || f.isEditingDefault || f.isEditingValues || !f.name) || this.hasUncommittedChildFields(this.fields());
|
|
2931
|
+
this.schemaName.set(value.title || 'New Schema');
|
|
2932
|
+
if (!hasUncommittedChanges) {
|
|
2933
|
+
this.fields.set(this.jsonSchemaToEditorFields(value));
|
|
2934
|
+
}
|
|
2036
2935
|
}
|
|
2037
2936
|
}
|
|
2937
|
+
hasUncommittedChildFields(fields) {
|
|
2938
|
+
for (const field of fields) {
|
|
2939
|
+
if (field.children) {
|
|
2940
|
+
if (field.children.some(c => c.isEditing || c.isEditingDefault || c.isEditingValues || !c.name)) {
|
|
2941
|
+
return true;
|
|
2942
|
+
}
|
|
2943
|
+
if (this.hasUncommittedChildFields(field.children)) {
|
|
2944
|
+
return true;
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
return false;
|
|
2949
|
+
}
|
|
2950
|
+
showJsonToggle = true;
|
|
2038
2951
|
schemaChange = new EventEmitter();
|
|
2039
2952
|
save = new EventEmitter();
|
|
2040
2953
|
schemaName = signal('New Schema', ...(ngDevMode ? [{ debugName: "schemaName" }] : []));
|
|
2041
2954
|
fields = signal([], ...(ngDevMode ? [{ debugName: "fields" }] : []));
|
|
2955
|
+
// View mode: 'visual' or 'json'
|
|
2956
|
+
viewMode = signal('visual', ...(ngDevMode ? [{ debugName: "viewMode" }] : []));
|
|
2957
|
+
jsonText = signal('', ...(ngDevMode ? [{ debugName: "jsonText" }] : []));
|
|
2958
|
+
jsonError = signal(null, ...(ngDevMode ? [{ debugName: "jsonError" }] : []));
|
|
2042
2959
|
fieldTypes = [
|
|
2043
2960
|
{ value: 'string', label: 'String', icon: 'text_fields' },
|
|
2044
2961
|
{ value: 'number', label: 'Number', icon: 'pin' },
|
|
@@ -2069,7 +2986,8 @@ class SchemaEditorComponent {
|
|
|
2069
2986
|
expanded: false,
|
|
2070
2987
|
};
|
|
2071
2988
|
this.fields.update(fields => [...fields, newField]);
|
|
2072
|
-
|
|
2989
|
+
// Don't emit change here - field has empty name and would be filtered out
|
|
2990
|
+
// Change will be emitted in stopEdit() when user provides a name
|
|
2073
2991
|
}
|
|
2074
2992
|
// Add a child field to an object or array
|
|
2075
2993
|
addChildField(parent) {
|
|
@@ -2085,7 +3003,8 @@ class SchemaEditorComponent {
|
|
|
2085
3003
|
parent.children.push(newField);
|
|
2086
3004
|
parent.expanded = true;
|
|
2087
3005
|
this.fields.update(fields => [...fields]);
|
|
2088
|
-
|
|
3006
|
+
// Don't emit change here - field has empty name and would be filtered out
|
|
3007
|
+
// Change will be emitted in stopEdit() when user provides a name
|
|
2089
3008
|
}
|
|
2090
3009
|
// Delete a field
|
|
2091
3010
|
deleteField(field, parentList) {
|
|
@@ -2167,21 +3086,43 @@ class SchemaEditorComponent {
|
|
|
2167
3086
|
}
|
|
2168
3087
|
// Toggle allowed values editor
|
|
2169
3088
|
toggleValuesEditor(field) {
|
|
3089
|
+
const wasEditingDefault = field.isEditingDefault;
|
|
2170
3090
|
field.isEditingValues = !field.isEditingValues;
|
|
2171
|
-
if (field.isEditingValues
|
|
2172
|
-
|
|
3091
|
+
if (field.isEditingValues) {
|
|
3092
|
+
// Close default editor when opening values editor
|
|
3093
|
+
field.isEditingDefault = false;
|
|
3094
|
+
if (!field.allowedValues) {
|
|
3095
|
+
field.allowedValues = [];
|
|
3096
|
+
}
|
|
2173
3097
|
}
|
|
2174
3098
|
this.fields.update(fields => [...fields]);
|
|
3099
|
+
// Emit change if we closed the default editor (to save any default value)
|
|
3100
|
+
if (wasEditingDefault) {
|
|
3101
|
+
this.emitChange();
|
|
3102
|
+
}
|
|
2175
3103
|
}
|
|
2176
3104
|
// Add allowed value
|
|
2177
3105
|
addAllowedValue(field, input) {
|
|
2178
|
-
|
|
3106
|
+
// Handle both direct input reference and event from button click
|
|
3107
|
+
let inputEl = null;
|
|
3108
|
+
if (input instanceof HTMLInputElement) {
|
|
3109
|
+
inputEl = input;
|
|
3110
|
+
}
|
|
3111
|
+
else {
|
|
3112
|
+
// Find the input by traversing up to .values-header and then querying for .value-input
|
|
3113
|
+
const target = input.target;
|
|
3114
|
+
const header = target.closest('.values-header');
|
|
3115
|
+
inputEl = header?.querySelector('.value-input');
|
|
3116
|
+
}
|
|
3117
|
+
if (!inputEl)
|
|
3118
|
+
return;
|
|
3119
|
+
const value = inputEl.value.trim();
|
|
2179
3120
|
if (value && !field.allowedValues?.includes(value)) {
|
|
2180
3121
|
if (!field.allowedValues) {
|
|
2181
3122
|
field.allowedValues = [];
|
|
2182
3123
|
}
|
|
2183
3124
|
field.allowedValues.push(value);
|
|
2184
|
-
|
|
3125
|
+
inputEl.value = '';
|
|
2185
3126
|
this.fields.update(fields => [...fields]);
|
|
2186
3127
|
this.emitChange();
|
|
2187
3128
|
}
|
|
@@ -2204,6 +3145,52 @@ class SchemaEditorComponent {
|
|
|
2204
3145
|
this.addAllowedValue(field, input);
|
|
2205
3146
|
}
|
|
2206
3147
|
}
|
|
3148
|
+
// Toggle default value editor
|
|
3149
|
+
toggleDefaultEditor(field) {
|
|
3150
|
+
const wasEditingValues = field.isEditingValues;
|
|
3151
|
+
field.isEditingDefault = !field.isEditingDefault;
|
|
3152
|
+
if (field.isEditingDefault) {
|
|
3153
|
+
// Close values editor when opening default editor
|
|
3154
|
+
field.isEditingValues = false;
|
|
3155
|
+
}
|
|
3156
|
+
this.fields.update(fields => [...fields]);
|
|
3157
|
+
// Emit change if we closed the values editor (to save any allowed values)
|
|
3158
|
+
if (wasEditingValues) {
|
|
3159
|
+
this.emitChange();
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
// Update default value
|
|
3163
|
+
onDefaultValueChange(field, value) {
|
|
3164
|
+
if (value === '') {
|
|
3165
|
+
field.defaultValue = undefined;
|
|
3166
|
+
}
|
|
3167
|
+
else if (field.type === 'number') {
|
|
3168
|
+
const num = parseFloat(value);
|
|
3169
|
+
field.defaultValue = isNaN(num) ? undefined : num;
|
|
3170
|
+
}
|
|
3171
|
+
else if (field.type === 'boolean') {
|
|
3172
|
+
field.defaultValue = value === 'true';
|
|
3173
|
+
}
|
|
3174
|
+
else {
|
|
3175
|
+
field.defaultValue = value;
|
|
3176
|
+
}
|
|
3177
|
+
this.fields.update(fields => [...fields]);
|
|
3178
|
+
this.emitChange();
|
|
3179
|
+
}
|
|
3180
|
+
// Clear default value
|
|
3181
|
+
clearDefaultValue(field) {
|
|
3182
|
+
field.defaultValue = undefined;
|
|
3183
|
+
field.isEditingDefault = false;
|
|
3184
|
+
this.fields.update(fields => [...fields]);
|
|
3185
|
+
this.emitChange();
|
|
3186
|
+
}
|
|
3187
|
+
// Handle Enter key in default value input
|
|
3188
|
+
onDefaultValueKeydown(event, field) {
|
|
3189
|
+
if (event.key === 'Enter' || event.key === 'Escape') {
|
|
3190
|
+
field.isEditingDefault = false;
|
|
3191
|
+
this.fields.update(fields => [...fields]);
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
2207
3194
|
// Handle keyboard events in field name input
|
|
2208
3195
|
onFieldNameKeydown(event, field) {
|
|
2209
3196
|
if (event.key === 'Enter') {
|
|
@@ -2232,6 +3219,15 @@ class SchemaEditorComponent {
|
|
|
2232
3219
|
this.emitChange();
|
|
2233
3220
|
}
|
|
2234
3221
|
}
|
|
3222
|
+
// Handle drag and drop reorder
|
|
3223
|
+
onFieldDrop(event) {
|
|
3224
|
+
if (event.previousIndex !== event.currentIndex) {
|
|
3225
|
+
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
|
|
3226
|
+
this.fields.update(fields => [...fields]);
|
|
3227
|
+
// Don't emit change here - it causes parent to reset expanded state
|
|
3228
|
+
// Order change will be captured on next edit or save
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
2235
3231
|
// Check if field can be indented (previous sibling must be object/array)
|
|
2236
3232
|
canIndent(field, parentList) {
|
|
2237
3233
|
const index = parentList.indexOf(field);
|
|
@@ -2255,9 +3251,11 @@ class SchemaEditorComponent {
|
|
|
2255
3251
|
prevSibling.children = [];
|
|
2256
3252
|
}
|
|
2257
3253
|
prevSibling.children.push(field);
|
|
3254
|
+
// Always expand the target (keep open if already open, open if closed)
|
|
2258
3255
|
prevSibling.expanded = true;
|
|
2259
3256
|
this.fields.update(fields => [...fields]);
|
|
2260
|
-
|
|
3257
|
+
// Don't emit change here - it causes parent to reset expanded state
|
|
3258
|
+
// Structure change will be captured on next edit or save
|
|
2261
3259
|
}
|
|
2262
3260
|
// Outdent field - move out of parent to grandparent level
|
|
2263
3261
|
outdentField(field, parentList, level) {
|
|
@@ -2278,7 +3276,8 @@ class SchemaEditorComponent {
|
|
|
2278
3276
|
const parentIndex = grandparentList.indexOf(parent);
|
|
2279
3277
|
grandparentList.splice(parentIndex + 1, 0, field);
|
|
2280
3278
|
this.fields.update(fields => [...fields]);
|
|
2281
|
-
|
|
3279
|
+
// Don't emit change here - it causes parent to reset expanded state
|
|
3280
|
+
// Structure change will be captured on next edit or save
|
|
2282
3281
|
}
|
|
2283
3282
|
// Find the parent field that contains a given list
|
|
2284
3283
|
findParentOfList(searchIn, targetList) {
|
|
@@ -2319,17 +3318,58 @@ class SchemaEditorComponent {
|
|
|
2319
3318
|
}
|
|
2320
3319
|
// Emit change event
|
|
2321
3320
|
emitChange() {
|
|
2322
|
-
this.schemaChange.emit(
|
|
2323
|
-
name: this.schemaName(),
|
|
2324
|
-
fields: this.fields(),
|
|
2325
|
-
});
|
|
3321
|
+
this.schemaChange.emit(this.toJsonSchema());
|
|
2326
3322
|
}
|
|
2327
3323
|
// Save the schema
|
|
2328
3324
|
onSave() {
|
|
2329
|
-
this.save.emit(
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
3325
|
+
this.save.emit(this.toJsonSchema());
|
|
3326
|
+
}
|
|
3327
|
+
// Convert JSON Schema to internal EditorField format
|
|
3328
|
+
jsonSchemaToEditorFields(schema, requiredFields = []) {
|
|
3329
|
+
const fields = [];
|
|
3330
|
+
if (schema.type === 'object' && schema.properties) {
|
|
3331
|
+
const required = schema.required || requiredFields;
|
|
3332
|
+
for (const [name, propSchema] of Object.entries(schema.properties)) {
|
|
3333
|
+
fields.push(this.jsonSchemaPropertyToEditorField(name, propSchema, required.includes(name)));
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
return fields;
|
|
3337
|
+
}
|
|
3338
|
+
jsonSchemaPropertyToEditorField(name, schema, isRequired) {
|
|
3339
|
+
const field = {
|
|
3340
|
+
id: this.generateId(),
|
|
3341
|
+
name,
|
|
3342
|
+
type: this.jsonSchemaTypeToEditorType(schema),
|
|
3343
|
+
description: schema.description,
|
|
3344
|
+
required: isRequired,
|
|
3345
|
+
allowedValues: schema.enum,
|
|
3346
|
+
defaultValue: schema.default,
|
|
3347
|
+
expanded: false,
|
|
3348
|
+
};
|
|
3349
|
+
if (schema.type === 'object' && schema.properties) {
|
|
3350
|
+
field.children = this.jsonSchemaToEditorFields(schema, schema.required);
|
|
3351
|
+
}
|
|
3352
|
+
else if (schema.type === 'array' && schema.items) {
|
|
3353
|
+
if (schema.items.type === 'object' && schema.items.properties) {
|
|
3354
|
+
field.children = this.jsonSchemaToEditorFields(schema.items, schema.items.required);
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
return field;
|
|
3358
|
+
}
|
|
3359
|
+
jsonSchemaTypeToEditorType(schema) {
|
|
3360
|
+
if (schema.format === 'date' || schema.format === 'date-time') {
|
|
3361
|
+
return 'date';
|
|
3362
|
+
}
|
|
3363
|
+
const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
|
|
3364
|
+
switch (type) {
|
|
3365
|
+
case 'string': return 'string';
|
|
3366
|
+
case 'number':
|
|
3367
|
+
case 'integer': return 'number';
|
|
3368
|
+
case 'boolean': return 'boolean';
|
|
3369
|
+
case 'object': return 'object';
|
|
3370
|
+
case 'array': return 'array';
|
|
3371
|
+
default: return 'string';
|
|
3372
|
+
}
|
|
2333
3373
|
}
|
|
2334
3374
|
// Convert to internal JSON format
|
|
2335
3375
|
toJson() {
|
|
@@ -2422,11 +3462,15 @@ class SchemaEditorComponent {
|
|
|
2422
3462
|
if (field.allowedValues && field.allowedValues.length > 0) {
|
|
2423
3463
|
schema['enum'] = field.allowedValues;
|
|
2424
3464
|
}
|
|
3465
|
+
// Add default value
|
|
3466
|
+
if (field.defaultValue !== undefined) {
|
|
3467
|
+
schema['default'] = field.defaultValue;
|
|
3468
|
+
}
|
|
2425
3469
|
return schema;
|
|
2426
3470
|
}
|
|
2427
3471
|
stripEditingState(fields) {
|
|
2428
3472
|
return fields.map(f => {
|
|
2429
|
-
const { isEditing, isEditingValues, ...rest } = f;
|
|
3473
|
+
const { isEditing, isEditingValues, isEditingDefault, ...rest } = f;
|
|
2430
3474
|
return {
|
|
2431
3475
|
...rest,
|
|
2432
3476
|
children: f.children ? this.stripEditingState(f.children) : undefined,
|
|
@@ -2437,8 +3481,50 @@ class SchemaEditorComponent {
|
|
|
2437
3481
|
trackByFieldId(index, field) {
|
|
2438
3482
|
return field.id;
|
|
2439
3483
|
}
|
|
3484
|
+
// --- JSON View Methods ---
|
|
3485
|
+
setViewMode(mode) {
|
|
3486
|
+
if (mode === 'json') {
|
|
3487
|
+
// Sync JSON text from current schema state
|
|
3488
|
+
this.jsonText.set(JSON.stringify(this.toJsonSchema(), null, 2));
|
|
3489
|
+
this.jsonError.set(null);
|
|
3490
|
+
}
|
|
3491
|
+
this.viewMode.set(mode);
|
|
3492
|
+
}
|
|
3493
|
+
onJsonTextChange(text) {
|
|
3494
|
+
this.jsonText.set(text);
|
|
3495
|
+
try {
|
|
3496
|
+
JSON.parse(text);
|
|
3497
|
+
this.jsonError.set(null);
|
|
3498
|
+
}
|
|
3499
|
+
catch (e) {
|
|
3500
|
+
this.jsonError.set(e.message);
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
applyJsonChanges() {
|
|
3504
|
+
try {
|
|
3505
|
+
const parsed = JSON.parse(this.jsonText());
|
|
3506
|
+
// Update internal state from JSON
|
|
3507
|
+
this.schemaName.set(parsed.title || 'New Schema');
|
|
3508
|
+
this.fields.set(this.jsonSchemaToEditorFields(parsed));
|
|
3509
|
+
this.jsonError.set(null);
|
|
3510
|
+
this.emitChange();
|
|
3511
|
+
}
|
|
3512
|
+
catch (e) {
|
|
3513
|
+
this.jsonError.set(e.message);
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
formatJson() {
|
|
3517
|
+
try {
|
|
3518
|
+
const parsed = JSON.parse(this.jsonText());
|
|
3519
|
+
this.jsonText.set(JSON.stringify(parsed, null, 2));
|
|
3520
|
+
this.jsonError.set(null);
|
|
3521
|
+
}
|
|
3522
|
+
catch (e) {
|
|
3523
|
+
this.jsonError.set(e.message);
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
2440
3526
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SchemaEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2441
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: SchemaEditorComponent, isStandalone: true, selector: "schema-editor", inputs: { schema: "schema" }, outputs: { schemaChange: "schemaChange", save: "save" }, ngImport: i0, template: "<div class=\"schema-editor\">\n <!-- Schema Header -->\n <div class=\"editor-header\">\n <div class=\"schema-name-section\">\n <mat-form-field appearance=\"outline\" class=\"schema-name-field\">\n <mat-label>Schema Name</mat-label>\n <input\n #schemaNameInput\n matInput\n [value]=\"schemaName()\"\n (input)=\"onSchemaNameChange($any($event.target).value, schemaNameInput)\"\n placeholder=\"Enter schema name\"\n />\n </mat-form-field>\n </div>\n <div class=\"header-actions\">\n <button mat-flat-button color=\"primary\" (click)=\"addField()\">\n <mat-icon>add</mat-icon>\n Add Field\n </button>\n </div>\n </div>\n\n <!-- Fields List -->\n <div class=\"fields-container\">\n @if (fields().length === 0) {\n <div class=\"empty-state\">\n <mat-icon>schema</mat-icon>\n <p>No fields yet. Click \"Add Field\" to get started.</p>\n </div>\n } @else {\n <div class=\"fields-list\">\n <ng-container *ngTemplateOutlet=\"fieldListTemplate; context: { fields: fields(), level: 0, parentList: fields() }\"></ng-container>\n </div>\n }\n </div>\n</div>\n\n<!-- Recursive Field Template -->\n<ng-template #fieldListTemplate let-fields=\"fields\" let-level=\"level\" let-parentList=\"parentList\">\n @for (field of fields; track trackByFieldId($index, field); let i = $index; let first = $first; let last = $last) {\n <div\n class=\"field-item\"\n [class.has-children]=\"field.children && field.children.length > 0\"\n [class.is-editing]=\"field.isEditing\"\n [class.is-complex]=\"field.type === 'object' || field.type === 'array'\"\n [style.--level]=\"level\"\n >\n <!-- Reorder Buttons -->\n <div class=\"reorder-buttons\">\n <button\n class=\"reorder-btn\"\n [disabled]=\"first\"\n (click)=\"moveFieldUp(field, parentList)\"\n matTooltip=\"Move up\"\n >\n <mat-icon>keyboard_arrow_up</mat-icon>\n </button>\n <button\n class=\"reorder-btn\"\n [disabled]=\"last\"\n (click)=\"moveFieldDown(field, parentList)\"\n matTooltip=\"Move down\"\n >\n <mat-icon>keyboard_arrow_down</mat-icon>\n </button>\n </div>\n\n <!-- Indent/Outdent Buttons -->\n <div class=\"indent-buttons\">\n <button\n class=\"indent-btn\"\n [disabled]=\"!canIndent(field, parentList)\"\n (click)=\"indentField(field, parentList)\"\n matTooltip=\"Move into previous object/array\"\n >\n <mat-icon>chevron_right</mat-icon>\n </button>\n <button\n class=\"indent-btn\"\n [disabled]=\"level === 0\"\n (click)=\"outdentField(field, parentList, level)\"\n matTooltip=\"Move out of parent\"\n >\n <mat-icon>chevron_left</mat-icon>\n </button>\n </div>\n\n <!-- Expand/Collapse -->\n @if (field.type === 'object' || field.type === 'array') {\n <button\n class=\"expand-btn\"\n (click)=\"toggleExpand(field)\"\n matTooltip=\"{{ field.expanded ? 'Collapse' : 'Expand' }}\"\n >\n <mat-icon>{{ field.expanded ? 'expand_more' : 'chevron_right' }}</mat-icon>\n </button>\n } @else {\n <span class=\"expand-placeholder\"></span>\n }\n\n <!-- Type Icon -->\n <mat-icon class=\"type-icon\" [matTooltip]=\"field.type\">{{ getTypeIcon(field.type) }}</mat-icon>\n\n <!-- Field Name -->\n @if (field.isEditing) {\n <input\n class=\"field-name-input\"\n [value]=\"field.name\"\n (input)=\"onFieldNameChange(field, $event)\"\n (blur)=\"stopEdit(field)\"\n (keydown)=\"onFieldNameKeydown($event, field)\"\n placeholder=\"Field name\"\n autofocus\n />\n } @else {\n <span class=\"field-name\" (dblclick)=\"startEdit(field)\">\n {{ field.name || 'unnamed' }}\n @if (field.type === 'array') {\n <span class=\"array-indicator\">[]</span>\n }\n </span>\n }\n\n <!-- Required Toggle -->\n <button\n class=\"required-btn\"\n [class.is-required]=\"field.required\"\n (click)=\"toggleRequired(field)\"\n [matTooltip]=\"field.required ? 'Required (click to make optional)' : 'Optional (click to make required)'\"\n >\n <mat-icon>{{ field.required ? 'star' : 'star_border' }}</mat-icon>\n </button>\n\n <!-- Description Input -->\n <input\n class=\"description-input\"\n [value]=\"field.description || ''\"\n (input)=\"onDescriptionChange(field, $any($event.target).value)\"\n placeholder=\"Description...\"\n />\n\n <!-- Type Selector -->\n <mat-form-field appearance=\"outline\" class=\"type-selector\">\n <mat-select [value]=\"field.type\" (selectionChange)=\"onFieldTypeChange(field, $event.value)\">\n @for (type of fieldTypes; track type.value) {\n <mat-option [value]=\"type.value\">\n <mat-icon>{{ type.icon }}</mat-icon>\n {{ type.label }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <!-- Actions -->\n <div class=\"field-actions\">\n @if (field.type === 'object' || field.type === 'array') {\n <button\n mat-icon-button\n (click)=\"addChildField(field)\"\n matTooltip=\"Add child field\"\n >\n <mat-icon>add_circle_outline</mat-icon>\n </button>\n }\n @if (field.type === 'string' || field.type === 'number') {\n <button\n mat-icon-button\n (click)=\"toggleValuesEditor(field)\"\n [matTooltip]=\"field.allowedValues?.length ? 'Edit allowed values (' + field.allowedValues.length + ')' : 'Add allowed values'\"\n [class.has-values]=\"field.allowedValues?.length\"\n >\n <mat-icon>{{ field.allowedValues?.length ? 'list' : 'list' }}</mat-icon>\n </button>\n }\n <button mat-icon-button [matMenuTriggerFor]=\"fieldMenu\" matTooltip=\"More options\">\n <mat-icon>more_vert</mat-icon>\n </button>\n <mat-menu #fieldMenu=\"matMenu\">\n <button mat-menu-item (click)=\"startEdit(field)\">\n <mat-icon>edit</mat-icon>\n <span>Rename</span>\n </button>\n <button mat-menu-item (click)=\"duplicateField(field, parentList)\">\n <mat-icon>content_copy</mat-icon>\n <span>Duplicate</span>\n </button>\n <button mat-menu-item (click)=\"deleteField(field, parentList)\" class=\"delete-action\">\n <mat-icon>delete</mat-icon>\n <span>Delete</span>\n </button>\n </mat-menu>\n </div>\n </div>\n\n <!-- Allowed Values Editor -->\n @if (field.isEditingValues && (field.type === 'string' || field.type === 'number')) {\n <div class=\"allowed-values-editor\" [style.--level]=\"level\">\n <div class=\"values-header\">\n <span class=\"values-label\">Allowed values:</span>\n <input\n #valueInput\n class=\"value-input\"\n type=\"text\"\n placeholder=\"Type value and press Enter\"\n (keydown)=\"onAllowedValueKeydown($event, field, valueInput)\"\n />\n <button class=\"add-value-btn\" (click)=\"addAllowedValue(field, valueInput)\" matTooltip=\"Add value\">\n <mat-icon>add</mat-icon>\n </button>\n </div>\n @if (field.allowedValues && field.allowedValues.length > 0) {\n <div class=\"values-list\">\n @for (value of field.allowedValues; track value; let vi = $index) {\n <span class=\"value-chip\">\n {{ value }}\n <button class=\"remove-value-btn\" (click)=\"removeAllowedValue(field, vi)\" matTooltip=\"Remove\">\n <mat-icon>close</mat-icon>\n </button>\n </span>\n }\n </div>\n } @else {\n <div class=\"no-values\">No values defined yet</div>\n }\n </div>\n }\n\n <!-- Nested Children -->\n @if ((field.type === 'object' || field.type === 'array') && field.expanded) {\n <div class=\"nested-fields\" [style.--level]=\"level + 1\">\n @if (field.children && field.children.length > 0) {\n <ng-container *ngTemplateOutlet=\"fieldListTemplate; context: { fields: field.children, level: level + 1, parentList: field.children }\"></ng-container>\n } @else {\n <div class=\"empty-nested\">\n <span>No child fields</span>\n <button mat-button (click)=\"addChildField(field)\" color=\"primary\">\n <mat-icon>add</mat-icon>\n Add field\n </button>\n </div>\n }\n </div>\n }\n }\n</ng-template>\n", styles: [":host{--schema-editor-bg: white;--schema-editor-border-radius: 12px;--schema-editor-shadow: 0 4px 20px rgba(0, 0, 0, .08);--schema-editor-border-color: #e2e8f0;--schema-editor-header-bg: white;--schema-editor-header-border: #e2e8f0;--schema-editor-field-bg: #f8fafc;--schema-editor-field-bg-hover: #f1f5f9;--schema-editor-field-bg-editing: #eff6ff;--schema-editor-field-bg-complex: #fefce8;--schema-editor-field-border-radius: 8px;--schema-editor-text-primary: #1e293b;--schema-editor-text-secondary: #64748b;--schema-editor-text-muted: #94a3b8;--schema-editor-accent-primary: #3b82f6;--schema-editor-accent-success: #22c55e;--schema-editor-accent-warning: #f59e0b;--schema-editor-accent-danger: #ef4444;--schema-editor-spacing-sm: 8px;--schema-editor-spacing-md: 16px;--schema-editor-spacing-lg: 24px;--schema-editor-font-size-sm: 12px;--schema-editor-font-size-md: 14px;--schema-editor-font-size-lg: 16px}.schema-editor{background:var(--schema-editor-bg);border-radius:var(--schema-editor-border-radius);box-shadow:var(--schema-editor-shadow);height:100%;display:flex;flex-direction:column;overflow:hidden}.editor-header{display:flex;align-items:center;justify-content:space-between;padding:var(--schema-editor-spacing-md) var(--schema-editor-spacing-lg);border-bottom:1px solid var(--schema-editor-border-color);gap:var(--schema-editor-spacing-md);flex-shrink:0;background:var(--schema-editor-header-bg)}.editor-header .schema-name-section{flex:1;max-width:400px}.editor-header .schema-name-field{width:100%}.editor-header .schema-name-field ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.editor-header .header-actions{display:flex;gap:12px}.fields-container{flex:1;overflow-y:auto;padding:16px;min-height:0}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:60px 24px;color:#94a3b8;text-align:center}.empty-state mat-icon{font-size:64px;width:64px;height:64px;margin-bottom:16px;opacity:.5}.empty-state p{font-size:16px;margin:0}.fields-list{display:flex;flex-direction:column;gap:4px}.field-item{display:flex;align-items:center;gap:var(--schema-editor-spacing-sm);padding:var(--schema-editor-spacing-sm) 12px;padding-left:calc(12px + var(--level, 0) * 24px);background:var(--schema-editor-field-bg);border-radius:var(--schema-editor-field-border-radius);border:1px solid transparent;transition:all .15s ease}.field-item:hover{background:var(--schema-editor-field-bg-hover);border-color:var(--schema-editor-border-color)}.field-item.is-editing{background:var(--schema-editor-field-bg-editing);border-color:var(--schema-editor-accent-primary)}.field-item.is-complex{background:var(--schema-editor-field-bg-complex)}.field-item.is-complex:hover{background:#fef9c3}.field-item.cdk-drag-preview{box-shadow:0 4px 16px #00000026;border:1px solid var(--schema-editor-accent-primary)}.field-item.cdk-drag-placeholder{opacity:.3}.reorder-buttons{display:flex;flex-direction:column;gap:0}.reorder-buttons .reorder-btn{background:none;border:none;padding:0;cursor:pointer;color:#94a3b8;display:flex;align-items:center;justify-content:center;width:20px;height:14px;border-radius:3px;transition:all .15s ease}.reorder-buttons .reorder-btn:hover:not(:disabled){background:#e2e8f0;color:#3b82f6}.reorder-buttons .reorder-btn:disabled{opacity:.3;cursor:default}.reorder-buttons .reorder-btn mat-icon{font-size:16px;width:16px;height:16px}.indent-buttons{display:flex;flex-direction:row;gap:2px}.indent-buttons .indent-btn{background:none;border:none;padding:2px;cursor:pointer;color:#94a3b8;display:flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:3px;transition:all .15s ease}.indent-buttons .indent-btn:hover:not(:disabled){background:#dbeafe;color:#2563eb}.indent-buttons .indent-btn:disabled{opacity:.3;cursor:default}.indent-buttons .indent-btn mat-icon{font-size:16px;width:16px;height:16px}.expand-btn{background:none;border:none;padding:4px;cursor:pointer;color:#64748b;border-radius:4px;display:flex;align-items:center;transition:all .15s ease}.expand-btn:hover{background:#e2e8f0;color:#1e293b}.expand-btn mat-icon{font-size:20px;width:20px;height:20px}.expand-placeholder{width:28px;flex-shrink:0}.type-icon{font-size:18px;width:18px;height:18px;color:#64748b;flex-shrink:0}.field-name{flex:0 0 auto;width:150px;font-size:14px;font-weight:500;color:#1e293b;cursor:pointer;padding:4px 8px;border-radius:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.field-name:hover{background:#e2e8f0}.field-name .array-indicator{color:#f59e0b;font-weight:600;margin-left:2px}.field-name-input{flex:0 0 auto;width:150px;font-size:14px;font-weight:500;color:#1e293b;padding:6px 10px;border:2px solid #3b82f6;border-radius:6px;outline:none;background:#fff}.field-name-input::placeholder{color:#94a3b8;font-weight:400}.required-btn{background:none;border:none;padding:4px;cursor:pointer;color:#cbd5e1;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:all .15s ease;flex-shrink:0}.required-btn:hover{color:#f59e0b;background:#fef3c7}.required-btn.is-required{color:#f59e0b}.required-btn mat-icon{font-size:18px;width:18px;height:18px}.description-input{flex:1;font-size:13px;color:#64748b;padding:6px 10px;border:1px solid #e2e8f0;border-radius:6px;outline:none;background:#f8fafc;min-width:100px;transition:all .15s ease}.description-input:focus{border-color:#3b82f6;background:#fff}.description-input::placeholder{color:#94a3b8;font-style:italic}.type-selector{width:140px;flex-shrink:0}.type-selector ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.type-selector ::ng-deep .mat-mdc-text-field-wrapper{padding:0 8px!important}.type-selector ::ng-deep .mat-mdc-form-field-infix{padding-top:4px!important;padding-bottom:4px!important;min-height:28px!important}.type-selector ::ng-deep .mat-mdc-select{font-size:12px!important}.type-selector ::ng-deep .mat-mdc-select-value-text{font-size:12px!important}.type-selector ::ng-deep .mat-mdc-select-arrow-wrapper{transform:scale(.8)}.field-actions{display:flex;align-items:center;gap:4px;opacity:0;transition:opacity .15s ease}.field-item:hover .field-actions{opacity:1}.field-actions button{color:#64748b}.field-actions button:hover{color:#1e293b}.delete-action{color:#ef4444!important}.delete-action mat-icon{color:#ef4444}.nested-fields{margin-left:calc(var(--level, 0) * 24px + 24px);padding-left:16px;border-left:2px solid #e2e8f0;display:flex;flex-direction:column;gap:4px;margin-top:4px;margin-bottom:8px}.empty-nested{display:flex;align-items:center;gap:12px;padding:12px 16px;color:#94a3b8;font-size:13px;font-style:italic;background:#f8fafc;border-radius:6px;border:1px dashed #e2e8f0}.allowed-values-editor{margin-left:calc(var(--level, 0) * 24px + 48px);margin-top:4px;margin-bottom:8px;padding:12px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px}.allowed-values-editor .values-header{display:flex;align-items:center;gap:8px;margin-bottom:8px}.allowed-values-editor .values-label{font-size:12px;font-weight:500;color:#166534}.allowed-values-editor .value-input{flex:1;padding:6px 10px;font-size:13px;border:1px solid #86efac;border-radius:4px;outline:none;background:#fff}.allowed-values-editor .value-input:focus{border-color:#22c55e}.allowed-values-editor .value-input::placeholder{color:#94a3b8}.allowed-values-editor .add-value-btn{background:#22c55e;border:none;color:#fff;width:28px;height:28px;border-radius:4px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .15s ease}.allowed-values-editor .add-value-btn:hover{background:#16a34a}.allowed-values-editor .add-value-btn mat-icon{font-size:18px;width:18px;height:18px}.allowed-values-editor .values-list{display:flex;flex-wrap:wrap;gap:6px}.allowed-values-editor .value-chip{display:inline-flex;align-items:center;gap:4px;padding:4px 8px;background:#fff;border:1px solid #86efac;border-radius:4px;font-size:12px;color:#166534}.allowed-values-editor .value-chip .remove-value-btn{background:none;border:none;padding:0;cursor:pointer;color:#94a3b8;display:flex;align-items:center;transition:color .15s ease}.allowed-values-editor .value-chip .remove-value-btn:hover{color:#ef4444}.allowed-values-editor .value-chip .remove-value-btn mat-icon{font-size:14px;width:14px;height:14px}.allowed-values-editor .no-values{font-size:12px;color:#94a3b8;font-style:italic}.field-actions .has-values{color:#22c55e!important}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2$1.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5$1.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i5.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i5.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i8.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i7$1.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i7$1.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i7$1.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: DragDropModule }] });
|
|
3527
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: SchemaEditorComponent, isStandalone: true, selector: "schema-editor", inputs: { schema: "schema", showJsonToggle: "showJsonToggle" }, outputs: { schemaChange: "schemaChange", save: "save" }, ngImport: i0, template: "<div class=\"schema-editor\">\n <!-- Schema Header -->\n <div class=\"editor-header\">\n <div class=\"schema-name-section\">\n <mat-form-field appearance=\"outline\" class=\"schema-name-field\">\n <mat-label>Schema Name</mat-label>\n <input\n #schemaNameInput\n matInput\n [value]=\"schemaName()\"\n (input)=\"onSchemaNameChange($any($event.target).value, schemaNameInput)\"\n placeholder=\"Enter schema name\"\n />\n </mat-form-field>\n </div>\n\n @if (showJsonToggle) {\n <mat-button-toggle-group [value]=\"viewMode()\" (change)=\"setViewMode($event.value)\" class=\"view-toggle\">\n <mat-button-toggle value=\"visual\">Visual</mat-button-toggle>\n <mat-button-toggle value=\"json\">JSON</mat-button-toggle>\n </mat-button-toggle-group>\n }\n\n <div class=\"header-actions\">\n @if (viewMode() === 'visual') {\n <button mat-flat-button color=\"primary\" (click)=\"addField()\">\n <mat-icon>add</mat-icon>\n Add Field\n </button>\n } @else {\n <button mat-stroked-button (click)=\"formatJson()\">\n <mat-icon>auto_fix_high</mat-icon>\n Format\n </button>\n <button mat-flat-button color=\"primary\" (click)=\"applyJsonChanges()\" [disabled]=\"jsonError()\">\n <mat-icon>check</mat-icon>\n Apply\n </button>\n }\n </div>\n </div>\n\n <!-- JSON View -->\n @if (viewMode() === 'json') {\n <div class=\"json-view\">\n @if (jsonError()) {\n <div class=\"json-error\">\n <mat-icon>error</mat-icon>\n {{ jsonError() }}\n </div>\n }\n <textarea\n class=\"json-textarea\"\n [value]=\"jsonText()\"\n (input)=\"onJsonTextChange($any($event.target).value)\"\n spellcheck=\"false\"\n ></textarea>\n </div>\n }\n\n <!-- Fields List (Visual View) -->\n <div class=\"fields-container\" [class.hidden]=\"viewMode() === 'json'\">\n @if (fields().length === 0) {\n <div class=\"empty-state\">\n <mat-icon>schema</mat-icon>\n <p>No fields yet. Click \"Add Field\" to get started.</p>\n </div>\n } @else {\n <div\n class=\"fields-list\"\n cdkDropList\n [cdkDropListData]=\"fields()\"\n (cdkDropListDropped)=\"onFieldDrop($event)\"\n >\n @for (field of fields(); track field.id) {\n <div class=\"field-wrapper\" cdkDrag>\n <!-- Drag Placeholder -->\n <div class=\"drag-placeholder\" *cdkDragPlaceholder></div>\n\n <!-- Drag Preview -->\n <div *cdkDragPreview class=\"drag-preview\">\n <mat-icon>{{ getTypeIcon(field.type) }}</mat-icon>\n {{ field.name || 'unnamed' }}\n </div>\n\n <div\n class=\"field-item\"\n [class.is-editing]=\"field.isEditing\"\n [class.is-complex]=\"field.type === 'object' || field.type === 'array'\"\n >\n <!-- Drag Handle -->\n <div class=\"drag-handle\" cdkDragHandle matTooltip=\"Drag to reorder\">\n <mat-icon>drag_indicator</mat-icon>\n </div>\n\n <!-- Indent/Outdent Buttons -->\n <div class=\"indent-buttons\">\n <button\n class=\"indent-btn\"\n [disabled]=\"!canIndent(field, fields())\"\n (click)=\"indentField(field, fields())\"\n matTooltip=\"Move into previous object/array\"\n >\n <mat-icon>chevron_right</mat-icon>\n </button>\n <button\n class=\"indent-btn\"\n disabled\n matTooltip=\"Move out of parent\"\n >\n <mat-icon>chevron_left</mat-icon>\n </button>\n </div>\n\n <!-- Expand/Collapse -->\n @if (field.type === 'object' || field.type === 'array') {\n <button\n class=\"expand-btn\"\n (click)=\"toggleExpand(field)\"\n [matTooltip]=\"field.expanded ? 'Collapse' : 'Expand'\"\n >\n <mat-icon>{{ field.expanded ? 'expand_more' : 'chevron_right' }}</mat-icon>\n </button>\n } @else {\n <span class=\"expand-placeholder\"></span>\n }\n\n <!-- Type Icon -->\n <mat-icon class=\"type-icon\" [matTooltip]=\"field.type\">{{ getTypeIcon(field.type) }}</mat-icon>\n\n <!-- Field Name -->\n @if (field.isEditing) {\n <input\n class=\"field-name-input\"\n [value]=\"field.name\"\n (input)=\"onFieldNameChange(field, $event)\"\n (blur)=\"stopEdit(field)\"\n (keydown)=\"onFieldNameKeydown($event, field)\"\n placeholder=\"Field name\"\n autofocus\n />\n } @else {\n <span class=\"field-name\" (dblclick)=\"startEdit(field)\">\n {{ field.name || 'unnamed' }}\n @if (field.type === 'array') {\n <span class=\"array-indicator\">[]</span>\n }\n </span>\n }\n\n <!-- Required Toggle -->\n <button\n class=\"required-btn\"\n [class.is-required]=\"field.required\"\n (click)=\"toggleRequired(field)\"\n [matTooltip]=\"field.required ? 'Required (click to make optional)' : 'Optional (click to make required)'\"\n >\n <mat-icon>{{ field.required ? 'star' : 'star_border' }}</mat-icon>\n </button>\n\n <!-- Description Input -->\n <input\n class=\"description-input\"\n [value]=\"field.description || ''\"\n (input)=\"onDescriptionChange(field, $any($event.target).value)\"\n placeholder=\"Description...\"\n />\n\n <!-- Type Selector -->\n <mat-form-field appearance=\"outline\" class=\"type-selector\">\n <mat-select [value]=\"field.type\" (selectionChange)=\"onFieldTypeChange(field, $event.value)\">\n @for (type of fieldTypes; track type.value) {\n <mat-option [value]=\"type.value\">\n <mat-icon>{{ type.icon }}</mat-icon>\n {{ type.label }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <!-- Actions -->\n <div class=\"field-actions\">\n @if (field.type === 'object' || field.type === 'array') {\n <button\n mat-icon-button\n (click)=\"addChildField(field)\"\n matTooltip=\"Add child field\"\n >\n <mat-icon>add_circle_outline</mat-icon>\n </button>\n }\n @if (field.type === 'string' || field.type === 'number') {\n <button\n mat-icon-button\n (click)=\"toggleValuesEditor(field)\"\n [matTooltip]=\"field.allowedValues?.length ? 'Edit allowed values (' + field.allowedValues!.length + ')' : 'Add allowed values'\"\n [class.has-values]=\"field.allowedValues?.length\"\n >\n <mat-icon>list</mat-icon>\n </button>\n }\n @if (field.type !== 'object' && field.type !== 'array') {\n <button\n mat-icon-button\n (click)=\"toggleDefaultEditor(field)\"\n [matTooltip]=\"field.defaultValue !== undefined ? 'Default: ' + field.defaultValue : 'Set default value'\"\n [class.has-default]=\"field.defaultValue !== undefined\"\n >\n <mat-icon>{{ field.defaultValue !== undefined ? 'label' : 'label_outline' }}</mat-icon>\n </button>\n }\n <button mat-icon-button [matMenuTriggerFor]=\"fieldMenu\" matTooltip=\"More options\">\n <mat-icon>more_vert</mat-icon>\n </button>\n <mat-menu #fieldMenu=\"matMenu\">\n <button mat-menu-item (click)=\"startEdit(field)\">\n <mat-icon>edit</mat-icon>\n <span>Rename</span>\n </button>\n <button mat-menu-item (click)=\"duplicateField(field, fields())\">\n <mat-icon>content_copy</mat-icon>\n <span>Duplicate</span>\n </button>\n <button mat-menu-item (click)=\"deleteField(field, fields())\" class=\"delete-action\">\n <mat-icon>delete</mat-icon>\n <span>Delete</span>\n </button>\n </mat-menu>\n </div>\n </div>\n\n <!-- Allowed Values Editor -->\n @if (field.isEditingValues && (field.type === 'string' || field.type === 'number')) {\n <div class=\"allowed-values-editor\">\n <div class=\"values-header\">\n <span class=\"values-label\">Allowed values:</span>\n <input\n #valueInput\n class=\"value-input\"\n type=\"text\"\n placeholder=\"Type value and press Enter\"\n (keydown)=\"onAllowedValueKeydown($event, field, valueInput)\"\n />\n <button class=\"add-value-btn\" (click)=\"addAllowedValue(field, $event)\" matTooltip=\"Add value\">\n <mat-icon>add</mat-icon>\n </button>\n </div>\n @if (field.allowedValues && field.allowedValues.length > 0) {\n <div class=\"values-list\">\n @for (value of field.allowedValues; track value; let vi = $index) {\n <span class=\"value-chip\">\n {{ value }}\n <button class=\"remove-value-btn\" (click)=\"removeAllowedValue(field, vi)\" matTooltip=\"Remove\">\n <mat-icon>close</mat-icon>\n </button>\n </span>\n }\n </div>\n } @else {\n <div class=\"no-values\">No values defined yet</div>\n }\n </div>\n }\n\n <!-- Default Value Editor -->\n @if (field.isEditingDefault && field.type !== 'object' && field.type !== 'array') {\n <div class=\"default-value-editor\">\n <div class=\"default-header\">\n <span class=\"default-label\">Default value:</span>\n @if (field.type === 'boolean') {\n <select\n class=\"default-select\"\n [value]=\"field.defaultValue?.toString() || ''\"\n (change)=\"onDefaultValueChange(field, $any($event.target).value)\"\n >\n <option value=\"\">No default</option>\n <option value=\"true\">true</option>\n <option value=\"false\">false</option>\n </select>\n } @else {\n <input\n class=\"default-input\"\n [type]=\"field.type === 'number' ? 'number' : 'text'\"\n [value]=\"field.defaultValue ?? ''\"\n (input)=\"onDefaultValueChange(field, $any($event.target).value)\"\n (keydown)=\"onDefaultValueKeydown($event, field)\"\n placeholder=\"Enter default value\"\n />\n }\n @if (field.defaultValue !== undefined) {\n <button class=\"clear-default-btn\" (click)=\"clearDefaultValue(field)\" matTooltip=\"Clear default\">\n <mat-icon>close</mat-icon>\n </button>\n }\n </div>\n </div>\n }\n\n <!-- Nested Children -->\n @if ((field.type === 'object' || field.type === 'array') && field.expanded && field.children) {\n <div\n class=\"nested-fields\"\n cdkDropList\n [cdkDropListData]=\"field.children\"\n (cdkDropListDropped)=\"onFieldDrop($event)\"\n >\n @if (field.children.length > 0) {\n @for (child of field.children; track child.id) {\n <div class=\"field-wrapper\" cdkDrag>\n <div class=\"drag-placeholder\" *cdkDragPlaceholder></div>\n <div *cdkDragPreview class=\"drag-preview\">\n <mat-icon>{{ getTypeIcon(child.type) }}</mat-icon>\n {{ child.name || 'unnamed' }}\n </div>\n <ng-container *ngTemplateOutlet=\"fieldItemTemplate; context: { field: child, parentList: field.children, level: 1 }\"></ng-container>\n\n <!-- Allowed Values Editor for nested field -->\n @if (child.isEditingValues && (child.type === 'string' || child.type === 'number')) {\n <div class=\"allowed-values-editor\">\n <div class=\"values-header\">\n <span class=\"values-label\">Allowed values:</span>\n <input\n #childValueInput\n class=\"value-input\"\n type=\"text\"\n placeholder=\"Type value and press Enter\"\n (keydown)=\"onAllowedValueKeydown($event, child, childValueInput)\"\n />\n <button class=\"add-value-btn\" (click)=\"addAllowedValue(child, $event)\" matTooltip=\"Add value\">\n <mat-icon>add</mat-icon>\n </button>\n </div>\n @if (child.allowedValues && child.allowedValues.length > 0) {\n <div class=\"values-list\">\n @for (value of child.allowedValues; track value; let vi = $index) {\n <span class=\"value-chip\">\n {{ value }}\n <button class=\"remove-value-btn\" (click)=\"removeAllowedValue(child, vi)\" matTooltip=\"Remove\">\n <mat-icon>close</mat-icon>\n </button>\n </span>\n }\n </div>\n } @else {\n <div class=\"no-values\">No values defined yet</div>\n }\n </div>\n }\n\n <!-- Default Value Editor for nested field -->\n @if (child.isEditingDefault && child.type !== 'object' && child.type !== 'array') {\n <div class=\"default-value-editor\">\n <div class=\"default-header\">\n <span class=\"default-label\">Default value:</span>\n @if (child.type === 'boolean') {\n <select\n class=\"default-select\"\n [value]=\"child.defaultValue?.toString() || ''\"\n (change)=\"onDefaultValueChange(child, $any($event.target).value)\"\n >\n <option value=\"\">No default</option>\n <option value=\"true\">true</option>\n <option value=\"false\">false</option>\n </select>\n } @else {\n <input\n class=\"default-input\"\n [type]=\"child.type === 'number' ? 'number' : 'text'\"\n [value]=\"child.defaultValue ?? ''\"\n (input)=\"onDefaultValueChange(child, $any($event.target).value)\"\n (keydown)=\"onDefaultValueKeydown($event, child)\"\n placeholder=\"Enter default value\"\n />\n }\n @if (child.defaultValue !== undefined) {\n <button class=\"clear-default-btn\" (click)=\"clearDefaultValue(child)\" matTooltip=\"Clear default\">\n <mat-icon>close</mat-icon>\n </button>\n }\n </div>\n </div>\n }\n </div>\n }\n } @else {\n <div class=\"empty-nested\">\n <span>No child fields</span>\n <button mat-button (click)=\"addChildField(field)\" color=\"primary\">\n <mat-icon>add</mat-icon>\n Add field\n </button>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n</div>\n\n<!-- Field Item Template (for nested fields) -->\n<ng-template #fieldItemTemplate let-field=\"field\" let-parentList=\"parentList\" let-level=\"level\">\n <div\n class=\"field-item\"\n [class.is-editing]=\"field.isEditing\"\n [class.is-complex]=\"field.type === 'object' || field.type === 'array'\"\n [style.--level]=\"level\"\n >\n <div class=\"drag-handle\" cdkDragHandle matTooltip=\"Drag to reorder\">\n <mat-icon>drag_indicator</mat-icon>\n </div>\n\n <div class=\"indent-buttons\">\n <button\n class=\"indent-btn\"\n [disabled]=\"!canIndent(field, parentList)\"\n (click)=\"indentField(field, parentList)\"\n matTooltip=\"Move into previous object/array\"\n >\n <mat-icon>chevron_right</mat-icon>\n </button>\n <button\n class=\"indent-btn\"\n (click)=\"outdentField(field, parentList, level)\"\n matTooltip=\"Move out of parent\"\n >\n <mat-icon>chevron_left</mat-icon>\n </button>\n </div>\n\n @if (field.type === 'object' || field.type === 'array') {\n <button class=\"expand-btn\" (click)=\"toggleExpand(field)\" [matTooltip]=\"field.expanded ? 'Collapse' : 'Expand'\">\n <mat-icon>{{ field.expanded ? 'expand_more' : 'chevron_right' }}</mat-icon>\n </button>\n } @else {\n <span class=\"expand-placeholder\"></span>\n }\n\n <mat-icon class=\"type-icon\" [matTooltip]=\"field.type\">{{ getTypeIcon(field.type) }}</mat-icon>\n\n @if (field.isEditing) {\n <input\n class=\"field-name-input\"\n [value]=\"field.name\"\n (input)=\"onFieldNameChange(field, $event)\"\n (blur)=\"stopEdit(field)\"\n (keydown)=\"onFieldNameKeydown($event, field)\"\n placeholder=\"Field name\"\n autofocus\n />\n } @else {\n <span class=\"field-name\" (dblclick)=\"startEdit(field)\">\n {{ field.name || 'unnamed' }}\n @if (field.type === 'array') {\n <span class=\"array-indicator\">[]</span>\n }\n </span>\n }\n\n <button\n class=\"required-btn\"\n [class.is-required]=\"field.required\"\n (click)=\"toggleRequired(field)\"\n [matTooltip]=\"field.required ? 'Required' : 'Optional'\"\n >\n <mat-icon>{{ field.required ? 'star' : 'star_border' }}</mat-icon>\n </button>\n\n <input\n class=\"description-input\"\n [value]=\"field.description || ''\"\n (input)=\"onDescriptionChange(field, $any($event.target).value)\"\n placeholder=\"Description...\"\n />\n\n <mat-form-field appearance=\"outline\" class=\"type-selector\">\n <mat-select [value]=\"field.type\" (selectionChange)=\"onFieldTypeChange(field, $event.value)\">\n @for (type of fieldTypes; track type.value) {\n <mat-option [value]=\"type.value\">\n <mat-icon>{{ type.icon }}</mat-icon>\n {{ type.label }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <div class=\"field-actions\">\n @if (field.type === 'object' || field.type === 'array') {\n <button mat-icon-button (click)=\"addChildField(field)\" matTooltip=\"Add child field\">\n <mat-icon>add_circle_outline</mat-icon>\n </button>\n }\n @if (field.type === 'string' || field.type === 'number') {\n <button\n mat-icon-button\n (click)=\"toggleValuesEditor(field)\"\n [matTooltip]=\"field.allowedValues?.length ? 'Edit allowed values' : 'Add allowed values'\"\n [class.has-values]=\"field.allowedValues?.length\"\n >\n <mat-icon>list</mat-icon>\n </button>\n }\n @if (field.type !== 'object' && field.type !== 'array') {\n <button\n mat-icon-button\n (click)=\"toggleDefaultEditor(field)\"\n [matTooltip]=\"field.defaultValue !== undefined ? 'Default: ' + field.defaultValue : 'Set default value'\"\n [class.has-default]=\"field.defaultValue !== undefined\"\n >\n <mat-icon>{{ field.defaultValue !== undefined ? 'label' : 'label_outline' }}</mat-icon>\n </button>\n }\n <button mat-icon-button [matMenuTriggerFor]=\"nestedMenu\" matTooltip=\"More options\">\n <mat-icon>more_vert</mat-icon>\n </button>\n <mat-menu #nestedMenu=\"matMenu\">\n <button mat-menu-item (click)=\"startEdit(field)\">\n <mat-icon>edit</mat-icon>\n <span>Rename</span>\n </button>\n <button mat-menu-item (click)=\"duplicateField(field, parentList)\">\n <mat-icon>content_copy</mat-icon>\n <span>Duplicate</span>\n </button>\n <button mat-menu-item (click)=\"deleteField(field, parentList)\" class=\"delete-action\">\n <mat-icon>delete</mat-icon>\n <span>Delete</span>\n </button>\n </mat-menu>\n </div>\n </div>\n</ng-template>\n", styles: [":host{--schema-editor-bg: white;--schema-editor-border-radius: 12px;--schema-editor-shadow: 0 4px 20px rgba(0, 0, 0, .08);--schema-editor-border-color: #e2e8f0;--schema-editor-header-bg: white;--schema-editor-header-border: #e2e8f0;--schema-editor-field-bg: #f8fafc;--schema-editor-field-bg-hover: #f1f5f9;--schema-editor-field-bg-editing: #eff6ff;--schema-editor-field-bg-complex: #fefce8;--schema-editor-field-border-radius: 8px;--schema-editor-text-primary: #1e293b;--schema-editor-text-secondary: #64748b;--schema-editor-text-muted: #94a3b8;--schema-editor-accent-primary: #3b82f6;--schema-editor-accent-success: #22c55e;--schema-editor-accent-warning: #f59e0b;--schema-editor-accent-danger: #ef4444;--schema-editor-spacing-sm: 8px;--schema-editor-spacing-md: 16px;--schema-editor-spacing-lg: 24px;--schema-editor-font-size-sm: 12px;--schema-editor-font-size-md: 14px;--schema-editor-font-size-lg: 16px}.schema-editor{background:var(--schema-editor-bg);border-radius:var(--schema-editor-border-radius);box-shadow:var(--schema-editor-shadow);height:100%;display:flex;flex-direction:column;overflow:hidden}.editor-header{display:flex;align-items:center;justify-content:space-between;padding:var(--schema-editor-spacing-md) var(--schema-editor-spacing-lg);border-bottom:1px solid var(--schema-editor-border-color);gap:var(--schema-editor-spacing-md);flex-shrink:0;background:var(--schema-editor-bg)}.editor-header .schema-name-section{flex:1;max-width:400px}.editor-header .schema-name-field{width:100%}.editor-header .schema-name-field ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.editor-header .schema-name-field ::ng-deep .mat-mdc-text-field-wrapper{padding:0 12px}.editor-header .schema-name-field ::ng-deep .mat-mdc-form-field-infix{padding-top:8px;padding-bottom:8px;min-height:36px}.editor-header .schema-name-field ::ng-deep .mdc-notched-outline__leading,.editor-header .schema-name-field ::ng-deep .mdc-notched-outline__notch,.editor-header .schema-name-field ::ng-deep .mdc-notched-outline__trailing{border-color:var(--schema-editor-border-color)!important}.editor-header .schema-name-field ::ng-deep .mdc-notched-outline__notch{border-right:none}.editor-header .schema-name-field ::ng-deep input.mat-mdc-input-element{font-size:14px}.editor-header .schema-name-field ::ng-deep .mat-mdc-floating-label{font-size:13px}.editor-header .header-actions{display:flex;gap:12px}.editor-header .header-actions button{display:flex;align-items:center;gap:6px}.editor-header .view-toggle{border:1px solid var(--schema-editor-border-color);border-radius:8px;overflow:hidden}.editor-header .view-toggle ::ng-deep .mat-button-toggle{background:#fff}.editor-header .view-toggle ::ng-deep .mat-button-toggle.mat-button-toggle-checked{background:var(--schema-editor-accent-primary);color:#fff}.editor-header .view-toggle ::ng-deep .mat-button-toggle .mat-button-toggle-label-content{padding:0 16px;font-size:13px;font-weight:500;line-height:32px}.json-view{flex:1;display:flex;flex-direction:column;min-height:0;overflow:hidden}.json-error{display:flex;align-items:center;gap:8px;padding:10px 16px;background:#fef2f2;border-bottom:1px solid #fecaca;color:#dc2626;font-size:12px;flex-shrink:0}.json-error mat-icon{font-size:16px;width:16px;height:16px}.json-textarea{flex:1;width:100%;padding:16px;border:none;outline:none;resize:none;font-family:SF Mono,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:13px;line-height:1.6;color:var(--schema-editor-text-primary);background:var(--schema-editor-bg);tab-size:2;box-sizing:border-box}.json-textarea:focus{outline:none}.hidden{display:none!important}.fields-container{flex:1;overflow-y:auto;padding:16px;min-height:0}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:60px 24px;color:#94a3b8;text-align:center}.empty-state mat-icon{font-size:64px;width:64px;height:64px;margin-bottom:16px;opacity:.5}.empty-state p{font-size:16px;margin:0}.fields-list{display:block;min-height:50px}.fields-list .field-wrapper{margin-bottom:4px}.field-item{display:flex;align-items:center;gap:var(--schema-editor-spacing-sm);padding:var(--schema-editor-spacing-sm) 12px;padding-left:calc(12px + var(--level, 0) * 24px);background:var(--schema-editor-field-bg);border-radius:var(--schema-editor-field-border-radius);border:1px solid transparent;transition:all .15s ease}.field-item:hover{background:var(--schema-editor-field-bg-hover);border-color:var(--schema-editor-border-color)}.field-item.is-editing{background:var(--schema-editor-field-bg-editing);border-color:var(--schema-editor-accent-primary)}.field-item.is-complex{background:var(--schema-editor-field-bg-complex)}.field-item.is-complex:hover{background:#fef9c3}.drag-handle{display:flex;align-items:center;justify-content:center;cursor:grab;color:#94a3b8;padding:4px;border-radius:4px;transition:all .15s ease}.drag-handle:hover{background:#e2e8f0;color:#64748b}.drag-handle:active{cursor:grabbing}.drag-handle mat-icon{font-size:20px;width:20px;height:20px}.field-wrapper{display:block}::ng-deep .drag-preview{display:flex!important;align-items:center!important;gap:8px!important;padding:12px 16px!important;background:#fff!important;border:2px solid #3b82f6!important;border-radius:8px!important;box-shadow:0 8px 24px #0003!important;font-size:14px!important;font-weight:500!important;color:#1e293b!important;overflow:hidden!important;white-space:nowrap!important;box-sizing:border-box!important}::ng-deep .drag-preview mat-icon{color:#3b82f6!important;font-size:20px!important;width:20px!important;height:20px!important;flex-shrink:0!important;overflow:hidden!important}.drag-placeholder{background:#e2e8f0;border:2px dashed var(--schema-editor-accent-primary);border-radius:var(--schema-editor-field-border-radius);min-height:48px;margin-bottom:4px}.drag-placeholder .placeholder-content{height:100%;min-height:48px}.cdk-drag-animating{transition:transform .2s cubic-bezier(0,0,.2,1)}.cdk-drop-list-dragging .field-wrapper:not(.cdk-drag-placeholder){transition:transform .2s cubic-bezier(0,0,.2,1)}.indent-buttons{display:flex;flex-direction:row;gap:2px}.indent-buttons .indent-btn{background:none;border:none;padding:2px;cursor:pointer;color:#94a3b8;display:flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:3px;transition:all .15s ease}.indent-buttons .indent-btn:hover:not(:disabled){background:#dbeafe;color:#2563eb}.indent-buttons .indent-btn:disabled{opacity:.3;cursor:default}.indent-buttons .indent-btn mat-icon{font-size:16px;width:16px;height:16px}.expand-btn{background:none;border:none;padding:4px;cursor:pointer;color:#64748b;border-radius:4px;display:flex;align-items:center;transition:all .15s ease}.expand-btn:hover{background:#e2e8f0;color:#1e293b}.expand-btn mat-icon{font-size:20px;width:20px;height:20px}.expand-placeholder{width:28px;flex-shrink:0}.type-icon{font-size:18px;width:18px;height:18px;color:#64748b;flex-shrink:0}.field-name{flex:0 0 auto;width:150px;font-size:14px;font-weight:500;color:#1e293b;cursor:pointer;padding:4px 8px;border-radius:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.field-name:hover{background:#e2e8f0}.field-name .array-indicator{color:#f59e0b;font-weight:600;margin-left:2px}.field-name-input{flex:0 0 auto;width:150px;font-size:14px;font-weight:500;color:#1e293b;padding:6px 10px;border:2px solid #3b82f6;border-radius:6px;outline:none;background:#fff}.field-name-input::placeholder{color:#94a3b8;font-weight:400}.required-btn{background:none;border:none;padding:4px;cursor:pointer;color:#cbd5e1;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:all .15s ease;flex-shrink:0}.required-btn:hover{color:#f59e0b;background:#fef3c7}.required-btn.is-required{color:#f59e0b}.required-btn mat-icon{font-size:18px;width:18px;height:18px}.description-input{flex:1;font-size:13px;color:#64748b;padding:6px 10px;border:1px solid #e2e8f0;border-radius:6px;outline:none;background:#f8fafc;min-width:100px;transition:all .15s ease}.description-input:focus{border-color:#3b82f6;background:#fff}.description-input::placeholder{color:#94a3b8;font-style:italic}.type-selector{width:140px;flex-shrink:0}.type-selector ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.type-selector ::ng-deep .mat-mdc-text-field-wrapper{padding:0 8px!important}.type-selector ::ng-deep .mat-mdc-form-field-infix{padding-top:4px!important;padding-bottom:4px!important;min-height:28px!important}.type-selector ::ng-deep .mat-mdc-select{font-size:12px!important}.type-selector ::ng-deep .mat-mdc-select-value-text{font-size:12px!important}.type-selector ::ng-deep .mat-mdc-select-arrow-wrapper{transform:scale(.8)}.field-actions{display:flex;align-items:center;gap:4px;opacity:0;transition:opacity .15s ease}.field-item:hover .field-actions{opacity:1}.field-actions button{color:#64748b}.field-actions button:hover{color:#1e293b}.delete-action{color:#ef4444!important}.delete-action mat-icon{color:#ef4444}.nested-fields{margin-left:calc(var(--level, 0) * 24px + 24px);padding-left:16px;border-left:2px solid #e2e8f0;display:block;min-height:30px;margin-top:4px;margin-bottom:8px}.nested-fields .field-wrapper{margin-bottom:4px}.empty-nested{display:flex;align-items:center;gap:12px;padding:12px 16px;color:#94a3b8;font-size:13px;font-style:italic;background:#f8fafc;border-radius:6px;border:1px dashed #e2e8f0}.allowed-values-editor{margin-left:calc(var(--level, 0) * 24px + 48px);margin-top:4px;margin-bottom:8px;padding:12px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px}.allowed-values-editor .values-header{display:flex;align-items:center;gap:8px;margin-bottom:8px}.allowed-values-editor .values-label{font-size:12px;font-weight:500;color:#166534}.allowed-values-editor .value-input{flex:1;padding:6px 10px;font-size:13px;border:1px solid #86efac;border-radius:4px;outline:none;background:#fff}.allowed-values-editor .value-input:focus{border-color:#22c55e}.allowed-values-editor .value-input::placeholder{color:#94a3b8}.allowed-values-editor .add-value-btn{background:#22c55e;border:none;color:#fff;width:28px;height:28px;border-radius:4px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .15s ease}.allowed-values-editor .add-value-btn:hover{background:#16a34a}.allowed-values-editor .add-value-btn mat-icon{font-size:18px;width:18px;height:18px}.allowed-values-editor .values-list{display:flex;flex-wrap:wrap;gap:6px}.allowed-values-editor .value-chip{display:inline-flex;align-items:center;gap:4px;padding:4px 8px;background:#fff;border:1px solid #86efac;border-radius:4px;font-size:12px;color:#166534}.allowed-values-editor .value-chip .remove-value-btn{background:none;border:none;padding:0;cursor:pointer;color:#94a3b8;display:flex;align-items:center;transition:color .15s ease}.allowed-values-editor .value-chip .remove-value-btn:hover{color:#ef4444}.allowed-values-editor .value-chip .remove-value-btn mat-icon{font-size:14px;width:14px;height:14px}.allowed-values-editor .no-values{font-size:12px;color:#94a3b8;font-style:italic}.field-actions .has-values{color:#22c55e!important}.field-actions .has-default{color:#8b5cf6!important}.default-value-editor{margin-left:calc(var(--level, 0) * 24px + 48px);margin-top:4px;margin-bottom:8px;padding:12px;background:#f5f3ff;border:1px solid #c4b5fd;border-radius:8px}.default-value-editor .default-header{display:flex;align-items:center;gap:8px}.default-value-editor .default-label{font-size:12px;font-weight:500;color:#6d28d9}.default-value-editor .default-input{flex:1;padding:6px 10px;font-size:13px;border:1px solid #a78bfa;border-radius:4px;outline:none;background:#fff;max-width:200px}.default-value-editor .default-input:focus{border-color:#8b5cf6}.default-value-editor .default-input::placeholder{color:#94a3b8}.default-value-editor .default-select{padding:6px 10px;font-size:13px;border:1px solid #a78bfa;border-radius:4px;outline:none;background:#fff;cursor:pointer}.default-value-editor .default-select:focus{border-color:#8b5cf6}.default-value-editor .clear-default-btn{background:none;border:none;padding:4px;cursor:pointer;color:#94a3b8;display:flex;align-items:center;transition:color .15s ease}.default-value-editor .clear-default-btn:hover{color:#ef4444}.default-value-editor .clear-default-btn mat-icon{font-size:16px;width:16px;height:16px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i4.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i4.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i8$1.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i8$1.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i8$1.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatButtonToggleModule }, { kind: "directive", type: i9$1.MatButtonToggleGroup, selector: "mat-button-toggle-group", inputs: ["appearance", "name", "vertical", "value", "multiple", "disabled", "disabledInteractive", "hideSingleSelectionIndicator", "hideMultipleSelectionIndicator"], outputs: ["valueChange", "change"], exportAs: ["matButtonToggleGroup"] }, { kind: "component", type: i9$1.MatButtonToggle, selector: "mat-button-toggle", inputs: ["aria-label", "aria-labelledby", "id", "name", "value", "tabIndex", "disableRipple", "appearance", "checked", "disabled", "disabledInteractive"], outputs: ["change"], exportAs: ["matButtonToggle"] }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i10.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i10.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i10.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "directive", type: i10.CdkDragPreview, selector: "ng-template[cdkDragPreview]", inputs: ["data", "matchSize"] }, { kind: "directive", type: i10.CdkDragPlaceholder, selector: "ng-template[cdkDragPlaceholder]", inputs: ["data"] }] });
|
|
2442
3528
|
}
|
|
2443
3529
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SchemaEditorComponent, decorators: [{
|
|
2444
3530
|
type: Component,
|
|
@@ -2452,10 +3538,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
2452
3538
|
MatSelectModule,
|
|
2453
3539
|
MatTooltipModule,
|
|
2454
3540
|
MatMenuModule,
|
|
3541
|
+
MatButtonToggleModule,
|
|
2455
3542
|
DragDropModule,
|
|
2456
|
-
], template: "<div class=\"schema-editor\">\n <!-- Schema Header -->\n <div class=\"editor-header\">\n <div class=\"schema-name-section\">\n <mat-form-field appearance=\"outline\" class=\"schema-name-field\">\n <mat-label>Schema Name</mat-label>\n <input\n #schemaNameInput\n matInput\n [value]=\"schemaName()\"\n (input)=\"onSchemaNameChange($any($event.target).value, schemaNameInput)\"\n placeholder=\"Enter schema name\"\n />\n </mat-form-field>\n </div>\n <div class=\"header-actions\">\n <button mat-flat-button color=\"primary\" (click)=\"addField()\">\n <mat-icon>add</mat-icon>\n Add Field\n </button>\n </div>\n </div>\n\n <!-- Fields List -->\n <div class=\"fields-container\">\n @if (fields().length === 0) {\n <div class=\"empty-state\">\n <mat-icon>schema</mat-icon>\n <p>No fields yet. Click \"Add Field\" to get started.</p>\n </div>\n } @else {\n <div class=\"fields-list\">\n <ng-container *ngTemplateOutlet=\"fieldListTemplate; context: { fields: fields(), level: 0, parentList: fields() }\"></ng-container>\n </div>\n }\n </div>\n</div>\n\n<!-- Recursive Field Template -->\n<ng-template #fieldListTemplate let-fields=\"fields\" let-level=\"level\" let-parentList=\"parentList\">\n @for (field of fields; track trackByFieldId($index, field); let i = $index; let first = $first; let last = $last) {\n <div\n class=\"field-item\"\n [class.has-children]=\"field.children && field.children.length > 0\"\n [class.is-editing]=\"field.isEditing\"\n [class.is-complex]=\"field.type === 'object' || field.type === 'array'\"\n [style.--level]=\"level\"\n >\n <!-- Reorder Buttons -->\n <div class=\"reorder-buttons\">\n <button\n class=\"reorder-btn\"\n [disabled]=\"first\"\n (click)=\"moveFieldUp(field, parentList)\"\n matTooltip=\"Move up\"\n >\n <mat-icon>keyboard_arrow_up</mat-icon>\n </button>\n <button\n class=\"reorder-btn\"\n [disabled]=\"last\"\n (click)=\"moveFieldDown(field, parentList)\"\n matTooltip=\"Move down\"\n >\n <mat-icon>keyboard_arrow_down</mat-icon>\n </button>\n </div>\n\n <!-- Indent/Outdent Buttons -->\n <div class=\"indent-buttons\">\n <button\n class=\"indent-btn\"\n [disabled]=\"!canIndent(field, parentList)\"\n (click)=\"indentField(field, parentList)\"\n matTooltip=\"Move into previous object/array\"\n >\n <mat-icon>chevron_right</mat-icon>\n </button>\n <button\n class=\"indent-btn\"\n [disabled]=\"level === 0\"\n (click)=\"outdentField(field, parentList, level)\"\n matTooltip=\"Move out of parent\"\n >\n <mat-icon>chevron_left</mat-icon>\n </button>\n </div>\n\n <!-- Expand/Collapse -->\n @if (field.type === 'object' || field.type === 'array') {\n <button\n class=\"expand-btn\"\n (click)=\"toggleExpand(field)\"\n matTooltip=\"{{ field.expanded ? 'Collapse' : 'Expand' }}\"\n >\n <mat-icon>{{ field.expanded ? 'expand_more' : 'chevron_right' }}</mat-icon>\n </button>\n } @else {\n <span class=\"expand-placeholder\"></span>\n }\n\n <!-- Type Icon -->\n <mat-icon class=\"type-icon\" [matTooltip]=\"field.type\">{{ getTypeIcon(field.type) }}</mat-icon>\n\n <!-- Field Name -->\n @if (field.isEditing) {\n <input\n class=\"field-name-input\"\n [value]=\"field.name\"\n (input)=\"onFieldNameChange(field, $event)\"\n (blur)=\"stopEdit(field)\"\n (keydown)=\"onFieldNameKeydown($event, field)\"\n placeholder=\"Field name\"\n autofocus\n />\n } @else {\n <span class=\"field-name\" (dblclick)=\"startEdit(field)\">\n {{ field.name || 'unnamed' }}\n @if (field.type === 'array') {\n <span class=\"array-indicator\">[]</span>\n }\n </span>\n }\n\n <!-- Required Toggle -->\n <button\n class=\"required-btn\"\n [class.is-required]=\"field.required\"\n (click)=\"toggleRequired(field)\"\n [matTooltip]=\"field.required ? 'Required (click to make optional)' : 'Optional (click to make required)'\"\n >\n <mat-icon>{{ field.required ? 'star' : 'star_border' }}</mat-icon>\n </button>\n\n <!-- Description Input -->\n <input\n class=\"description-input\"\n [value]=\"field.description || ''\"\n (input)=\"onDescriptionChange(field, $any($event.target).value)\"\n placeholder=\"Description...\"\n />\n\n <!-- Type Selector -->\n <mat-form-field appearance=\"outline\" class=\"type-selector\">\n <mat-select [value]=\"field.type\" (selectionChange)=\"onFieldTypeChange(field, $event.value)\">\n @for (type of fieldTypes; track type.value) {\n <mat-option [value]=\"type.value\">\n <mat-icon>{{ type.icon }}</mat-icon>\n {{ type.label }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <!-- Actions -->\n <div class=\"field-actions\">\n @if (field.type === 'object' || field.type === 'array') {\n <button\n mat-icon-button\n (click)=\"addChildField(field)\"\n matTooltip=\"Add child field\"\n >\n <mat-icon>add_circle_outline</mat-icon>\n </button>\n }\n @if (field.type === 'string' || field.type === 'number') {\n <button\n mat-icon-button\n (click)=\"toggleValuesEditor(field)\"\n [matTooltip]=\"field.allowedValues?.length ? 'Edit allowed values (' + field.allowedValues.length + ')' : 'Add allowed values'\"\n [class.has-values]=\"field.allowedValues?.length\"\n >\n <mat-icon>{{ field.allowedValues?.length ? 'list' : 'list' }}</mat-icon>\n </button>\n }\n <button mat-icon-button [matMenuTriggerFor]=\"fieldMenu\" matTooltip=\"More options\">\n <mat-icon>more_vert</mat-icon>\n </button>\n <mat-menu #fieldMenu=\"matMenu\">\n <button mat-menu-item (click)=\"startEdit(field)\">\n <mat-icon>edit</mat-icon>\n <span>Rename</span>\n </button>\n <button mat-menu-item (click)=\"duplicateField(field, parentList)\">\n <mat-icon>content_copy</mat-icon>\n <span>Duplicate</span>\n </button>\n <button mat-menu-item (click)=\"deleteField(field, parentList)\" class=\"delete-action\">\n <mat-icon>delete</mat-icon>\n <span>Delete</span>\n </button>\n </mat-menu>\n </div>\n </div>\n\n <!-- Allowed Values Editor -->\n @if (field.isEditingValues && (field.type === 'string' || field.type === 'number')) {\n <div class=\"allowed-values-editor\" [style.--level]=\"level\">\n <div class=\"values-header\">\n <span class=\"values-label\">Allowed values:</span>\n <input\n #valueInput\n class=\"value-input\"\n type=\"text\"\n placeholder=\"Type value and press Enter\"\n (keydown)=\"onAllowedValueKeydown($event, field, valueInput)\"\n />\n <button class=\"add-value-btn\" (click)=\"addAllowedValue(field, valueInput)\" matTooltip=\"Add value\">\n <mat-icon>add</mat-icon>\n </button>\n </div>\n @if (field.allowedValues && field.allowedValues.length > 0) {\n <div class=\"values-list\">\n @for (value of field.allowedValues; track value; let vi = $index) {\n <span class=\"value-chip\">\n {{ value }}\n <button class=\"remove-value-btn\" (click)=\"removeAllowedValue(field, vi)\" matTooltip=\"Remove\">\n <mat-icon>close</mat-icon>\n </button>\n </span>\n }\n </div>\n } @else {\n <div class=\"no-values\">No values defined yet</div>\n }\n </div>\n }\n\n <!-- Nested Children -->\n @if ((field.type === 'object' || field.type === 'array') && field.expanded) {\n <div class=\"nested-fields\" [style.--level]=\"level + 1\">\n @if (field.children && field.children.length > 0) {\n <ng-container *ngTemplateOutlet=\"fieldListTemplate; context: { fields: field.children, level: level + 1, parentList: field.children }\"></ng-container>\n } @else {\n <div class=\"empty-nested\">\n <span>No child fields</span>\n <button mat-button (click)=\"addChildField(field)\" color=\"primary\">\n <mat-icon>add</mat-icon>\n Add field\n </button>\n </div>\n }\n </div>\n }\n }\n</ng-template>\n", styles: [":host{--schema-editor-bg: white;--schema-editor-border-radius: 12px;--schema-editor-shadow: 0 4px 20px rgba(0, 0, 0, .08);--schema-editor-border-color: #e2e8f0;--schema-editor-header-bg: white;--schema-editor-header-border: #e2e8f0;--schema-editor-field-bg: #f8fafc;--schema-editor-field-bg-hover: #f1f5f9;--schema-editor-field-bg-editing: #eff6ff;--schema-editor-field-bg-complex: #fefce8;--schema-editor-field-border-radius: 8px;--schema-editor-text-primary: #1e293b;--schema-editor-text-secondary: #64748b;--schema-editor-text-muted: #94a3b8;--schema-editor-accent-primary: #3b82f6;--schema-editor-accent-success: #22c55e;--schema-editor-accent-warning: #f59e0b;--schema-editor-accent-danger: #ef4444;--schema-editor-spacing-sm: 8px;--schema-editor-spacing-md: 16px;--schema-editor-spacing-lg: 24px;--schema-editor-font-size-sm: 12px;--schema-editor-font-size-md: 14px;--schema-editor-font-size-lg: 16px}.schema-editor{background:var(--schema-editor-bg);border-radius:var(--schema-editor-border-radius);box-shadow:var(--schema-editor-shadow);height:100%;display:flex;flex-direction:column;overflow:hidden}.editor-header{display:flex;align-items:center;justify-content:space-between;padding:var(--schema-editor-spacing-md) var(--schema-editor-spacing-lg);border-bottom:1px solid var(--schema-editor-border-color);gap:var(--schema-editor-spacing-md);flex-shrink:0;background:var(--schema-editor-header-bg)}.editor-header .schema-name-section{flex:1;max-width:400px}.editor-header .schema-name-field{width:100%}.editor-header .schema-name-field ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.editor-header .header-actions{display:flex;gap:12px}.fields-container{flex:1;overflow-y:auto;padding:16px;min-height:0}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:60px 24px;color:#94a3b8;text-align:center}.empty-state mat-icon{font-size:64px;width:64px;height:64px;margin-bottom:16px;opacity:.5}.empty-state p{font-size:16px;margin:0}.fields-list{display:flex;flex-direction:column;gap:4px}.field-item{display:flex;align-items:center;gap:var(--schema-editor-spacing-sm);padding:var(--schema-editor-spacing-sm) 12px;padding-left:calc(12px + var(--level, 0) * 24px);background:var(--schema-editor-field-bg);border-radius:var(--schema-editor-field-border-radius);border:1px solid transparent;transition:all .15s ease}.field-item:hover{background:var(--schema-editor-field-bg-hover);border-color:var(--schema-editor-border-color)}.field-item.is-editing{background:var(--schema-editor-field-bg-editing);border-color:var(--schema-editor-accent-primary)}.field-item.is-complex{background:var(--schema-editor-field-bg-complex)}.field-item.is-complex:hover{background:#fef9c3}.field-item.cdk-drag-preview{box-shadow:0 4px 16px #00000026;border:1px solid var(--schema-editor-accent-primary)}.field-item.cdk-drag-placeholder{opacity:.3}.reorder-buttons{display:flex;flex-direction:column;gap:0}.reorder-buttons .reorder-btn{background:none;border:none;padding:0;cursor:pointer;color:#94a3b8;display:flex;align-items:center;justify-content:center;width:20px;height:14px;border-radius:3px;transition:all .15s ease}.reorder-buttons .reorder-btn:hover:not(:disabled){background:#e2e8f0;color:#3b82f6}.reorder-buttons .reorder-btn:disabled{opacity:.3;cursor:default}.reorder-buttons .reorder-btn mat-icon{font-size:16px;width:16px;height:16px}.indent-buttons{display:flex;flex-direction:row;gap:2px}.indent-buttons .indent-btn{background:none;border:none;padding:2px;cursor:pointer;color:#94a3b8;display:flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:3px;transition:all .15s ease}.indent-buttons .indent-btn:hover:not(:disabled){background:#dbeafe;color:#2563eb}.indent-buttons .indent-btn:disabled{opacity:.3;cursor:default}.indent-buttons .indent-btn mat-icon{font-size:16px;width:16px;height:16px}.expand-btn{background:none;border:none;padding:4px;cursor:pointer;color:#64748b;border-radius:4px;display:flex;align-items:center;transition:all .15s ease}.expand-btn:hover{background:#e2e8f0;color:#1e293b}.expand-btn mat-icon{font-size:20px;width:20px;height:20px}.expand-placeholder{width:28px;flex-shrink:0}.type-icon{font-size:18px;width:18px;height:18px;color:#64748b;flex-shrink:0}.field-name{flex:0 0 auto;width:150px;font-size:14px;font-weight:500;color:#1e293b;cursor:pointer;padding:4px 8px;border-radius:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.field-name:hover{background:#e2e8f0}.field-name .array-indicator{color:#f59e0b;font-weight:600;margin-left:2px}.field-name-input{flex:0 0 auto;width:150px;font-size:14px;font-weight:500;color:#1e293b;padding:6px 10px;border:2px solid #3b82f6;border-radius:6px;outline:none;background:#fff}.field-name-input::placeholder{color:#94a3b8;font-weight:400}.required-btn{background:none;border:none;padding:4px;cursor:pointer;color:#cbd5e1;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:all .15s ease;flex-shrink:0}.required-btn:hover{color:#f59e0b;background:#fef3c7}.required-btn.is-required{color:#f59e0b}.required-btn mat-icon{font-size:18px;width:18px;height:18px}.description-input{flex:1;font-size:13px;color:#64748b;padding:6px 10px;border:1px solid #e2e8f0;border-radius:6px;outline:none;background:#f8fafc;min-width:100px;transition:all .15s ease}.description-input:focus{border-color:#3b82f6;background:#fff}.description-input::placeholder{color:#94a3b8;font-style:italic}.type-selector{width:140px;flex-shrink:0}.type-selector ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.type-selector ::ng-deep .mat-mdc-text-field-wrapper{padding:0 8px!important}.type-selector ::ng-deep .mat-mdc-form-field-infix{padding-top:4px!important;padding-bottom:4px!important;min-height:28px!important}.type-selector ::ng-deep .mat-mdc-select{font-size:12px!important}.type-selector ::ng-deep .mat-mdc-select-value-text{font-size:12px!important}.type-selector ::ng-deep .mat-mdc-select-arrow-wrapper{transform:scale(.8)}.field-actions{display:flex;align-items:center;gap:4px;opacity:0;transition:opacity .15s ease}.field-item:hover .field-actions{opacity:1}.field-actions button{color:#64748b}.field-actions button:hover{color:#1e293b}.delete-action{color:#ef4444!important}.delete-action mat-icon{color:#ef4444}.nested-fields{margin-left:calc(var(--level, 0) * 24px + 24px);padding-left:16px;border-left:2px solid #e2e8f0;display:flex;flex-direction:column;gap:4px;margin-top:4px;margin-bottom:8px}.empty-nested{display:flex;align-items:center;gap:12px;padding:12px 16px;color:#94a3b8;font-size:13px;font-style:italic;background:#f8fafc;border-radius:6px;border:1px dashed #e2e8f0}.allowed-values-editor{margin-left:calc(var(--level, 0) * 24px + 48px);margin-top:4px;margin-bottom:8px;padding:12px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px}.allowed-values-editor .values-header{display:flex;align-items:center;gap:8px;margin-bottom:8px}.allowed-values-editor .values-label{font-size:12px;font-weight:500;color:#166534}.allowed-values-editor .value-input{flex:1;padding:6px 10px;font-size:13px;border:1px solid #86efac;border-radius:4px;outline:none;background:#fff}.allowed-values-editor .value-input:focus{border-color:#22c55e}.allowed-values-editor .value-input::placeholder{color:#94a3b8}.allowed-values-editor .add-value-btn{background:#22c55e;border:none;color:#fff;width:28px;height:28px;border-radius:4px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .15s ease}.allowed-values-editor .add-value-btn:hover{background:#16a34a}.allowed-values-editor .add-value-btn mat-icon{font-size:18px;width:18px;height:18px}.allowed-values-editor .values-list{display:flex;flex-wrap:wrap;gap:6px}.allowed-values-editor .value-chip{display:inline-flex;align-items:center;gap:4px;padding:4px 8px;background:#fff;border:1px solid #86efac;border-radius:4px;font-size:12px;color:#166534}.allowed-values-editor .value-chip .remove-value-btn{background:none;border:none;padding:0;cursor:pointer;color:#94a3b8;display:flex;align-items:center;transition:color .15s ease}.allowed-values-editor .value-chip .remove-value-btn:hover{color:#ef4444}.allowed-values-editor .value-chip .remove-value-btn mat-icon{font-size:14px;width:14px;height:14px}.allowed-values-editor .no-values{font-size:12px;color:#94a3b8;font-style:italic}.field-actions .has-values{color:#22c55e!important}\n"] }]
|
|
3543
|
+
], template: "<div class=\"schema-editor\">\n <!-- Schema Header -->\n <div class=\"editor-header\">\n <div class=\"schema-name-section\">\n <mat-form-field appearance=\"outline\" class=\"schema-name-field\">\n <mat-label>Schema Name</mat-label>\n <input\n #schemaNameInput\n matInput\n [value]=\"schemaName()\"\n (input)=\"onSchemaNameChange($any($event.target).value, schemaNameInput)\"\n placeholder=\"Enter schema name\"\n />\n </mat-form-field>\n </div>\n\n @if (showJsonToggle) {\n <mat-button-toggle-group [value]=\"viewMode()\" (change)=\"setViewMode($event.value)\" class=\"view-toggle\">\n <mat-button-toggle value=\"visual\">Visual</mat-button-toggle>\n <mat-button-toggle value=\"json\">JSON</mat-button-toggle>\n </mat-button-toggle-group>\n }\n\n <div class=\"header-actions\">\n @if (viewMode() === 'visual') {\n <button mat-flat-button color=\"primary\" (click)=\"addField()\">\n <mat-icon>add</mat-icon>\n Add Field\n </button>\n } @else {\n <button mat-stroked-button (click)=\"formatJson()\">\n <mat-icon>auto_fix_high</mat-icon>\n Format\n </button>\n <button mat-flat-button color=\"primary\" (click)=\"applyJsonChanges()\" [disabled]=\"jsonError()\">\n <mat-icon>check</mat-icon>\n Apply\n </button>\n }\n </div>\n </div>\n\n <!-- JSON View -->\n @if (viewMode() === 'json') {\n <div class=\"json-view\">\n @if (jsonError()) {\n <div class=\"json-error\">\n <mat-icon>error</mat-icon>\n {{ jsonError() }}\n </div>\n }\n <textarea\n class=\"json-textarea\"\n [value]=\"jsonText()\"\n (input)=\"onJsonTextChange($any($event.target).value)\"\n spellcheck=\"false\"\n ></textarea>\n </div>\n }\n\n <!-- Fields List (Visual View) -->\n <div class=\"fields-container\" [class.hidden]=\"viewMode() === 'json'\">\n @if (fields().length === 0) {\n <div class=\"empty-state\">\n <mat-icon>schema</mat-icon>\n <p>No fields yet. Click \"Add Field\" to get started.</p>\n </div>\n } @else {\n <div\n class=\"fields-list\"\n cdkDropList\n [cdkDropListData]=\"fields()\"\n (cdkDropListDropped)=\"onFieldDrop($event)\"\n >\n @for (field of fields(); track field.id) {\n <div class=\"field-wrapper\" cdkDrag>\n <!-- Drag Placeholder -->\n <div class=\"drag-placeholder\" *cdkDragPlaceholder></div>\n\n <!-- Drag Preview -->\n <div *cdkDragPreview class=\"drag-preview\">\n <mat-icon>{{ getTypeIcon(field.type) }}</mat-icon>\n {{ field.name || 'unnamed' }}\n </div>\n\n <div\n class=\"field-item\"\n [class.is-editing]=\"field.isEditing\"\n [class.is-complex]=\"field.type === 'object' || field.type === 'array'\"\n >\n <!-- Drag Handle -->\n <div class=\"drag-handle\" cdkDragHandle matTooltip=\"Drag to reorder\">\n <mat-icon>drag_indicator</mat-icon>\n </div>\n\n <!-- Indent/Outdent Buttons -->\n <div class=\"indent-buttons\">\n <button\n class=\"indent-btn\"\n [disabled]=\"!canIndent(field, fields())\"\n (click)=\"indentField(field, fields())\"\n matTooltip=\"Move into previous object/array\"\n >\n <mat-icon>chevron_right</mat-icon>\n </button>\n <button\n class=\"indent-btn\"\n disabled\n matTooltip=\"Move out of parent\"\n >\n <mat-icon>chevron_left</mat-icon>\n </button>\n </div>\n\n <!-- Expand/Collapse -->\n @if (field.type === 'object' || field.type === 'array') {\n <button\n class=\"expand-btn\"\n (click)=\"toggleExpand(field)\"\n [matTooltip]=\"field.expanded ? 'Collapse' : 'Expand'\"\n >\n <mat-icon>{{ field.expanded ? 'expand_more' : 'chevron_right' }}</mat-icon>\n </button>\n } @else {\n <span class=\"expand-placeholder\"></span>\n }\n\n <!-- Type Icon -->\n <mat-icon class=\"type-icon\" [matTooltip]=\"field.type\">{{ getTypeIcon(field.type) }}</mat-icon>\n\n <!-- Field Name -->\n @if (field.isEditing) {\n <input\n class=\"field-name-input\"\n [value]=\"field.name\"\n (input)=\"onFieldNameChange(field, $event)\"\n (blur)=\"stopEdit(field)\"\n (keydown)=\"onFieldNameKeydown($event, field)\"\n placeholder=\"Field name\"\n autofocus\n />\n } @else {\n <span class=\"field-name\" (dblclick)=\"startEdit(field)\">\n {{ field.name || 'unnamed' }}\n @if (field.type === 'array') {\n <span class=\"array-indicator\">[]</span>\n }\n </span>\n }\n\n <!-- Required Toggle -->\n <button\n class=\"required-btn\"\n [class.is-required]=\"field.required\"\n (click)=\"toggleRequired(field)\"\n [matTooltip]=\"field.required ? 'Required (click to make optional)' : 'Optional (click to make required)'\"\n >\n <mat-icon>{{ field.required ? 'star' : 'star_border' }}</mat-icon>\n </button>\n\n <!-- Description Input -->\n <input\n class=\"description-input\"\n [value]=\"field.description || ''\"\n (input)=\"onDescriptionChange(field, $any($event.target).value)\"\n placeholder=\"Description...\"\n />\n\n <!-- Type Selector -->\n <mat-form-field appearance=\"outline\" class=\"type-selector\">\n <mat-select [value]=\"field.type\" (selectionChange)=\"onFieldTypeChange(field, $event.value)\">\n @for (type of fieldTypes; track type.value) {\n <mat-option [value]=\"type.value\">\n <mat-icon>{{ type.icon }}</mat-icon>\n {{ type.label }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <!-- Actions -->\n <div class=\"field-actions\">\n @if (field.type === 'object' || field.type === 'array') {\n <button\n mat-icon-button\n (click)=\"addChildField(field)\"\n matTooltip=\"Add child field\"\n >\n <mat-icon>add_circle_outline</mat-icon>\n </button>\n }\n @if (field.type === 'string' || field.type === 'number') {\n <button\n mat-icon-button\n (click)=\"toggleValuesEditor(field)\"\n [matTooltip]=\"field.allowedValues?.length ? 'Edit allowed values (' + field.allowedValues!.length + ')' : 'Add allowed values'\"\n [class.has-values]=\"field.allowedValues?.length\"\n >\n <mat-icon>list</mat-icon>\n </button>\n }\n @if (field.type !== 'object' && field.type !== 'array') {\n <button\n mat-icon-button\n (click)=\"toggleDefaultEditor(field)\"\n [matTooltip]=\"field.defaultValue !== undefined ? 'Default: ' + field.defaultValue : 'Set default value'\"\n [class.has-default]=\"field.defaultValue !== undefined\"\n >\n <mat-icon>{{ field.defaultValue !== undefined ? 'label' : 'label_outline' }}</mat-icon>\n </button>\n }\n <button mat-icon-button [matMenuTriggerFor]=\"fieldMenu\" matTooltip=\"More options\">\n <mat-icon>more_vert</mat-icon>\n </button>\n <mat-menu #fieldMenu=\"matMenu\">\n <button mat-menu-item (click)=\"startEdit(field)\">\n <mat-icon>edit</mat-icon>\n <span>Rename</span>\n </button>\n <button mat-menu-item (click)=\"duplicateField(field, fields())\">\n <mat-icon>content_copy</mat-icon>\n <span>Duplicate</span>\n </button>\n <button mat-menu-item (click)=\"deleteField(field, fields())\" class=\"delete-action\">\n <mat-icon>delete</mat-icon>\n <span>Delete</span>\n </button>\n </mat-menu>\n </div>\n </div>\n\n <!-- Allowed Values Editor -->\n @if (field.isEditingValues && (field.type === 'string' || field.type === 'number')) {\n <div class=\"allowed-values-editor\">\n <div class=\"values-header\">\n <span class=\"values-label\">Allowed values:</span>\n <input\n #valueInput\n class=\"value-input\"\n type=\"text\"\n placeholder=\"Type value and press Enter\"\n (keydown)=\"onAllowedValueKeydown($event, field, valueInput)\"\n />\n <button class=\"add-value-btn\" (click)=\"addAllowedValue(field, $event)\" matTooltip=\"Add value\">\n <mat-icon>add</mat-icon>\n </button>\n </div>\n @if (field.allowedValues && field.allowedValues.length > 0) {\n <div class=\"values-list\">\n @for (value of field.allowedValues; track value; let vi = $index) {\n <span class=\"value-chip\">\n {{ value }}\n <button class=\"remove-value-btn\" (click)=\"removeAllowedValue(field, vi)\" matTooltip=\"Remove\">\n <mat-icon>close</mat-icon>\n </button>\n </span>\n }\n </div>\n } @else {\n <div class=\"no-values\">No values defined yet</div>\n }\n </div>\n }\n\n <!-- Default Value Editor -->\n @if (field.isEditingDefault && field.type !== 'object' && field.type !== 'array') {\n <div class=\"default-value-editor\">\n <div class=\"default-header\">\n <span class=\"default-label\">Default value:</span>\n @if (field.type === 'boolean') {\n <select\n class=\"default-select\"\n [value]=\"field.defaultValue?.toString() || ''\"\n (change)=\"onDefaultValueChange(field, $any($event.target).value)\"\n >\n <option value=\"\">No default</option>\n <option value=\"true\">true</option>\n <option value=\"false\">false</option>\n </select>\n } @else {\n <input\n class=\"default-input\"\n [type]=\"field.type === 'number' ? 'number' : 'text'\"\n [value]=\"field.defaultValue ?? ''\"\n (input)=\"onDefaultValueChange(field, $any($event.target).value)\"\n (keydown)=\"onDefaultValueKeydown($event, field)\"\n placeholder=\"Enter default value\"\n />\n }\n @if (field.defaultValue !== undefined) {\n <button class=\"clear-default-btn\" (click)=\"clearDefaultValue(field)\" matTooltip=\"Clear default\">\n <mat-icon>close</mat-icon>\n </button>\n }\n </div>\n </div>\n }\n\n <!-- Nested Children -->\n @if ((field.type === 'object' || field.type === 'array') && field.expanded && field.children) {\n <div\n class=\"nested-fields\"\n cdkDropList\n [cdkDropListData]=\"field.children\"\n (cdkDropListDropped)=\"onFieldDrop($event)\"\n >\n @if (field.children.length > 0) {\n @for (child of field.children; track child.id) {\n <div class=\"field-wrapper\" cdkDrag>\n <div class=\"drag-placeholder\" *cdkDragPlaceholder></div>\n <div *cdkDragPreview class=\"drag-preview\">\n <mat-icon>{{ getTypeIcon(child.type) }}</mat-icon>\n {{ child.name || 'unnamed' }}\n </div>\n <ng-container *ngTemplateOutlet=\"fieldItemTemplate; context: { field: child, parentList: field.children, level: 1 }\"></ng-container>\n\n <!-- Allowed Values Editor for nested field -->\n @if (child.isEditingValues && (child.type === 'string' || child.type === 'number')) {\n <div class=\"allowed-values-editor\">\n <div class=\"values-header\">\n <span class=\"values-label\">Allowed values:</span>\n <input\n #childValueInput\n class=\"value-input\"\n type=\"text\"\n placeholder=\"Type value and press Enter\"\n (keydown)=\"onAllowedValueKeydown($event, child, childValueInput)\"\n />\n <button class=\"add-value-btn\" (click)=\"addAllowedValue(child, $event)\" matTooltip=\"Add value\">\n <mat-icon>add</mat-icon>\n </button>\n </div>\n @if (child.allowedValues && child.allowedValues.length > 0) {\n <div class=\"values-list\">\n @for (value of child.allowedValues; track value; let vi = $index) {\n <span class=\"value-chip\">\n {{ value }}\n <button class=\"remove-value-btn\" (click)=\"removeAllowedValue(child, vi)\" matTooltip=\"Remove\">\n <mat-icon>close</mat-icon>\n </button>\n </span>\n }\n </div>\n } @else {\n <div class=\"no-values\">No values defined yet</div>\n }\n </div>\n }\n\n <!-- Default Value Editor for nested field -->\n @if (child.isEditingDefault && child.type !== 'object' && child.type !== 'array') {\n <div class=\"default-value-editor\">\n <div class=\"default-header\">\n <span class=\"default-label\">Default value:</span>\n @if (child.type === 'boolean') {\n <select\n class=\"default-select\"\n [value]=\"child.defaultValue?.toString() || ''\"\n (change)=\"onDefaultValueChange(child, $any($event.target).value)\"\n >\n <option value=\"\">No default</option>\n <option value=\"true\">true</option>\n <option value=\"false\">false</option>\n </select>\n } @else {\n <input\n class=\"default-input\"\n [type]=\"child.type === 'number' ? 'number' : 'text'\"\n [value]=\"child.defaultValue ?? ''\"\n (input)=\"onDefaultValueChange(child, $any($event.target).value)\"\n (keydown)=\"onDefaultValueKeydown($event, child)\"\n placeholder=\"Enter default value\"\n />\n }\n @if (child.defaultValue !== undefined) {\n <button class=\"clear-default-btn\" (click)=\"clearDefaultValue(child)\" matTooltip=\"Clear default\">\n <mat-icon>close</mat-icon>\n </button>\n }\n </div>\n </div>\n }\n </div>\n }\n } @else {\n <div class=\"empty-nested\">\n <span>No child fields</span>\n <button mat-button (click)=\"addChildField(field)\" color=\"primary\">\n <mat-icon>add</mat-icon>\n Add field\n </button>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n</div>\n\n<!-- Field Item Template (for nested fields) -->\n<ng-template #fieldItemTemplate let-field=\"field\" let-parentList=\"parentList\" let-level=\"level\">\n <div\n class=\"field-item\"\n [class.is-editing]=\"field.isEditing\"\n [class.is-complex]=\"field.type === 'object' || field.type === 'array'\"\n [style.--level]=\"level\"\n >\n <div class=\"drag-handle\" cdkDragHandle matTooltip=\"Drag to reorder\">\n <mat-icon>drag_indicator</mat-icon>\n </div>\n\n <div class=\"indent-buttons\">\n <button\n class=\"indent-btn\"\n [disabled]=\"!canIndent(field, parentList)\"\n (click)=\"indentField(field, parentList)\"\n matTooltip=\"Move into previous object/array\"\n >\n <mat-icon>chevron_right</mat-icon>\n </button>\n <button\n class=\"indent-btn\"\n (click)=\"outdentField(field, parentList, level)\"\n matTooltip=\"Move out of parent\"\n >\n <mat-icon>chevron_left</mat-icon>\n </button>\n </div>\n\n @if (field.type === 'object' || field.type === 'array') {\n <button class=\"expand-btn\" (click)=\"toggleExpand(field)\" [matTooltip]=\"field.expanded ? 'Collapse' : 'Expand'\">\n <mat-icon>{{ field.expanded ? 'expand_more' : 'chevron_right' }}</mat-icon>\n </button>\n } @else {\n <span class=\"expand-placeholder\"></span>\n }\n\n <mat-icon class=\"type-icon\" [matTooltip]=\"field.type\">{{ getTypeIcon(field.type) }}</mat-icon>\n\n @if (field.isEditing) {\n <input\n class=\"field-name-input\"\n [value]=\"field.name\"\n (input)=\"onFieldNameChange(field, $event)\"\n (blur)=\"stopEdit(field)\"\n (keydown)=\"onFieldNameKeydown($event, field)\"\n placeholder=\"Field name\"\n autofocus\n />\n } @else {\n <span class=\"field-name\" (dblclick)=\"startEdit(field)\">\n {{ field.name || 'unnamed' }}\n @if (field.type === 'array') {\n <span class=\"array-indicator\">[]</span>\n }\n </span>\n }\n\n <button\n class=\"required-btn\"\n [class.is-required]=\"field.required\"\n (click)=\"toggleRequired(field)\"\n [matTooltip]=\"field.required ? 'Required' : 'Optional'\"\n >\n <mat-icon>{{ field.required ? 'star' : 'star_border' }}</mat-icon>\n </button>\n\n <input\n class=\"description-input\"\n [value]=\"field.description || ''\"\n (input)=\"onDescriptionChange(field, $any($event.target).value)\"\n placeholder=\"Description...\"\n />\n\n <mat-form-field appearance=\"outline\" class=\"type-selector\">\n <mat-select [value]=\"field.type\" (selectionChange)=\"onFieldTypeChange(field, $event.value)\">\n @for (type of fieldTypes; track type.value) {\n <mat-option [value]=\"type.value\">\n <mat-icon>{{ type.icon }}</mat-icon>\n {{ type.label }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <div class=\"field-actions\">\n @if (field.type === 'object' || field.type === 'array') {\n <button mat-icon-button (click)=\"addChildField(field)\" matTooltip=\"Add child field\">\n <mat-icon>add_circle_outline</mat-icon>\n </button>\n }\n @if (field.type === 'string' || field.type === 'number') {\n <button\n mat-icon-button\n (click)=\"toggleValuesEditor(field)\"\n [matTooltip]=\"field.allowedValues?.length ? 'Edit allowed values' : 'Add allowed values'\"\n [class.has-values]=\"field.allowedValues?.length\"\n >\n <mat-icon>list</mat-icon>\n </button>\n }\n @if (field.type !== 'object' && field.type !== 'array') {\n <button\n mat-icon-button\n (click)=\"toggleDefaultEditor(field)\"\n [matTooltip]=\"field.defaultValue !== undefined ? 'Default: ' + field.defaultValue : 'Set default value'\"\n [class.has-default]=\"field.defaultValue !== undefined\"\n >\n <mat-icon>{{ field.defaultValue !== undefined ? 'label' : 'label_outline' }}</mat-icon>\n </button>\n }\n <button mat-icon-button [matMenuTriggerFor]=\"nestedMenu\" matTooltip=\"More options\">\n <mat-icon>more_vert</mat-icon>\n </button>\n <mat-menu #nestedMenu=\"matMenu\">\n <button mat-menu-item (click)=\"startEdit(field)\">\n <mat-icon>edit</mat-icon>\n <span>Rename</span>\n </button>\n <button mat-menu-item (click)=\"duplicateField(field, parentList)\">\n <mat-icon>content_copy</mat-icon>\n <span>Duplicate</span>\n </button>\n <button mat-menu-item (click)=\"deleteField(field, parentList)\" class=\"delete-action\">\n <mat-icon>delete</mat-icon>\n <span>Delete</span>\n </button>\n </mat-menu>\n </div>\n </div>\n</ng-template>\n", styles: [":host{--schema-editor-bg: white;--schema-editor-border-radius: 12px;--schema-editor-shadow: 0 4px 20px rgba(0, 0, 0, .08);--schema-editor-border-color: #e2e8f0;--schema-editor-header-bg: white;--schema-editor-header-border: #e2e8f0;--schema-editor-field-bg: #f8fafc;--schema-editor-field-bg-hover: #f1f5f9;--schema-editor-field-bg-editing: #eff6ff;--schema-editor-field-bg-complex: #fefce8;--schema-editor-field-border-radius: 8px;--schema-editor-text-primary: #1e293b;--schema-editor-text-secondary: #64748b;--schema-editor-text-muted: #94a3b8;--schema-editor-accent-primary: #3b82f6;--schema-editor-accent-success: #22c55e;--schema-editor-accent-warning: #f59e0b;--schema-editor-accent-danger: #ef4444;--schema-editor-spacing-sm: 8px;--schema-editor-spacing-md: 16px;--schema-editor-spacing-lg: 24px;--schema-editor-font-size-sm: 12px;--schema-editor-font-size-md: 14px;--schema-editor-font-size-lg: 16px}.schema-editor{background:var(--schema-editor-bg);border-radius:var(--schema-editor-border-radius);box-shadow:var(--schema-editor-shadow);height:100%;display:flex;flex-direction:column;overflow:hidden}.editor-header{display:flex;align-items:center;justify-content:space-between;padding:var(--schema-editor-spacing-md) var(--schema-editor-spacing-lg);border-bottom:1px solid var(--schema-editor-border-color);gap:var(--schema-editor-spacing-md);flex-shrink:0;background:var(--schema-editor-bg)}.editor-header .schema-name-section{flex:1;max-width:400px}.editor-header .schema-name-field{width:100%}.editor-header .schema-name-field ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.editor-header .schema-name-field ::ng-deep .mat-mdc-text-field-wrapper{padding:0 12px}.editor-header .schema-name-field ::ng-deep .mat-mdc-form-field-infix{padding-top:8px;padding-bottom:8px;min-height:36px}.editor-header .schema-name-field ::ng-deep .mdc-notched-outline__leading,.editor-header .schema-name-field ::ng-deep .mdc-notched-outline__notch,.editor-header .schema-name-field ::ng-deep .mdc-notched-outline__trailing{border-color:var(--schema-editor-border-color)!important}.editor-header .schema-name-field ::ng-deep .mdc-notched-outline__notch{border-right:none}.editor-header .schema-name-field ::ng-deep input.mat-mdc-input-element{font-size:14px}.editor-header .schema-name-field ::ng-deep .mat-mdc-floating-label{font-size:13px}.editor-header .header-actions{display:flex;gap:12px}.editor-header .header-actions button{display:flex;align-items:center;gap:6px}.editor-header .view-toggle{border:1px solid var(--schema-editor-border-color);border-radius:8px;overflow:hidden}.editor-header .view-toggle ::ng-deep .mat-button-toggle{background:#fff}.editor-header .view-toggle ::ng-deep .mat-button-toggle.mat-button-toggle-checked{background:var(--schema-editor-accent-primary);color:#fff}.editor-header .view-toggle ::ng-deep .mat-button-toggle .mat-button-toggle-label-content{padding:0 16px;font-size:13px;font-weight:500;line-height:32px}.json-view{flex:1;display:flex;flex-direction:column;min-height:0;overflow:hidden}.json-error{display:flex;align-items:center;gap:8px;padding:10px 16px;background:#fef2f2;border-bottom:1px solid #fecaca;color:#dc2626;font-size:12px;flex-shrink:0}.json-error mat-icon{font-size:16px;width:16px;height:16px}.json-textarea{flex:1;width:100%;padding:16px;border:none;outline:none;resize:none;font-family:SF Mono,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:13px;line-height:1.6;color:var(--schema-editor-text-primary);background:var(--schema-editor-bg);tab-size:2;box-sizing:border-box}.json-textarea:focus{outline:none}.hidden{display:none!important}.fields-container{flex:1;overflow-y:auto;padding:16px;min-height:0}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:60px 24px;color:#94a3b8;text-align:center}.empty-state mat-icon{font-size:64px;width:64px;height:64px;margin-bottom:16px;opacity:.5}.empty-state p{font-size:16px;margin:0}.fields-list{display:block;min-height:50px}.fields-list .field-wrapper{margin-bottom:4px}.field-item{display:flex;align-items:center;gap:var(--schema-editor-spacing-sm);padding:var(--schema-editor-spacing-sm) 12px;padding-left:calc(12px + var(--level, 0) * 24px);background:var(--schema-editor-field-bg);border-radius:var(--schema-editor-field-border-radius);border:1px solid transparent;transition:all .15s ease}.field-item:hover{background:var(--schema-editor-field-bg-hover);border-color:var(--schema-editor-border-color)}.field-item.is-editing{background:var(--schema-editor-field-bg-editing);border-color:var(--schema-editor-accent-primary)}.field-item.is-complex{background:var(--schema-editor-field-bg-complex)}.field-item.is-complex:hover{background:#fef9c3}.drag-handle{display:flex;align-items:center;justify-content:center;cursor:grab;color:#94a3b8;padding:4px;border-radius:4px;transition:all .15s ease}.drag-handle:hover{background:#e2e8f0;color:#64748b}.drag-handle:active{cursor:grabbing}.drag-handle mat-icon{font-size:20px;width:20px;height:20px}.field-wrapper{display:block}::ng-deep .drag-preview{display:flex!important;align-items:center!important;gap:8px!important;padding:12px 16px!important;background:#fff!important;border:2px solid #3b82f6!important;border-radius:8px!important;box-shadow:0 8px 24px #0003!important;font-size:14px!important;font-weight:500!important;color:#1e293b!important;overflow:hidden!important;white-space:nowrap!important;box-sizing:border-box!important}::ng-deep .drag-preview mat-icon{color:#3b82f6!important;font-size:20px!important;width:20px!important;height:20px!important;flex-shrink:0!important;overflow:hidden!important}.drag-placeholder{background:#e2e8f0;border:2px dashed var(--schema-editor-accent-primary);border-radius:var(--schema-editor-field-border-radius);min-height:48px;margin-bottom:4px}.drag-placeholder .placeholder-content{height:100%;min-height:48px}.cdk-drag-animating{transition:transform .2s cubic-bezier(0,0,.2,1)}.cdk-drop-list-dragging .field-wrapper:not(.cdk-drag-placeholder){transition:transform .2s cubic-bezier(0,0,.2,1)}.indent-buttons{display:flex;flex-direction:row;gap:2px}.indent-buttons .indent-btn{background:none;border:none;padding:2px;cursor:pointer;color:#94a3b8;display:flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:3px;transition:all .15s ease}.indent-buttons .indent-btn:hover:not(:disabled){background:#dbeafe;color:#2563eb}.indent-buttons .indent-btn:disabled{opacity:.3;cursor:default}.indent-buttons .indent-btn mat-icon{font-size:16px;width:16px;height:16px}.expand-btn{background:none;border:none;padding:4px;cursor:pointer;color:#64748b;border-radius:4px;display:flex;align-items:center;transition:all .15s ease}.expand-btn:hover{background:#e2e8f0;color:#1e293b}.expand-btn mat-icon{font-size:20px;width:20px;height:20px}.expand-placeholder{width:28px;flex-shrink:0}.type-icon{font-size:18px;width:18px;height:18px;color:#64748b;flex-shrink:0}.field-name{flex:0 0 auto;width:150px;font-size:14px;font-weight:500;color:#1e293b;cursor:pointer;padding:4px 8px;border-radius:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.field-name:hover{background:#e2e8f0}.field-name .array-indicator{color:#f59e0b;font-weight:600;margin-left:2px}.field-name-input{flex:0 0 auto;width:150px;font-size:14px;font-weight:500;color:#1e293b;padding:6px 10px;border:2px solid #3b82f6;border-radius:6px;outline:none;background:#fff}.field-name-input::placeholder{color:#94a3b8;font-weight:400}.required-btn{background:none;border:none;padding:4px;cursor:pointer;color:#cbd5e1;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:all .15s ease;flex-shrink:0}.required-btn:hover{color:#f59e0b;background:#fef3c7}.required-btn.is-required{color:#f59e0b}.required-btn mat-icon{font-size:18px;width:18px;height:18px}.description-input{flex:1;font-size:13px;color:#64748b;padding:6px 10px;border:1px solid #e2e8f0;border-radius:6px;outline:none;background:#f8fafc;min-width:100px;transition:all .15s ease}.description-input:focus{border-color:#3b82f6;background:#fff}.description-input::placeholder{color:#94a3b8;font-style:italic}.type-selector{width:140px;flex-shrink:0}.type-selector ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.type-selector ::ng-deep .mat-mdc-text-field-wrapper{padding:0 8px!important}.type-selector ::ng-deep .mat-mdc-form-field-infix{padding-top:4px!important;padding-bottom:4px!important;min-height:28px!important}.type-selector ::ng-deep .mat-mdc-select{font-size:12px!important}.type-selector ::ng-deep .mat-mdc-select-value-text{font-size:12px!important}.type-selector ::ng-deep .mat-mdc-select-arrow-wrapper{transform:scale(.8)}.field-actions{display:flex;align-items:center;gap:4px;opacity:0;transition:opacity .15s ease}.field-item:hover .field-actions{opacity:1}.field-actions button{color:#64748b}.field-actions button:hover{color:#1e293b}.delete-action{color:#ef4444!important}.delete-action mat-icon{color:#ef4444}.nested-fields{margin-left:calc(var(--level, 0) * 24px + 24px);padding-left:16px;border-left:2px solid #e2e8f0;display:block;min-height:30px;margin-top:4px;margin-bottom:8px}.nested-fields .field-wrapper{margin-bottom:4px}.empty-nested{display:flex;align-items:center;gap:12px;padding:12px 16px;color:#94a3b8;font-size:13px;font-style:italic;background:#f8fafc;border-radius:6px;border:1px dashed #e2e8f0}.allowed-values-editor{margin-left:calc(var(--level, 0) * 24px + 48px);margin-top:4px;margin-bottom:8px;padding:12px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px}.allowed-values-editor .values-header{display:flex;align-items:center;gap:8px;margin-bottom:8px}.allowed-values-editor .values-label{font-size:12px;font-weight:500;color:#166534}.allowed-values-editor .value-input{flex:1;padding:6px 10px;font-size:13px;border:1px solid #86efac;border-radius:4px;outline:none;background:#fff}.allowed-values-editor .value-input:focus{border-color:#22c55e}.allowed-values-editor .value-input::placeholder{color:#94a3b8}.allowed-values-editor .add-value-btn{background:#22c55e;border:none;color:#fff;width:28px;height:28px;border-radius:4px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .15s ease}.allowed-values-editor .add-value-btn:hover{background:#16a34a}.allowed-values-editor .add-value-btn mat-icon{font-size:18px;width:18px;height:18px}.allowed-values-editor .values-list{display:flex;flex-wrap:wrap;gap:6px}.allowed-values-editor .value-chip{display:inline-flex;align-items:center;gap:4px;padding:4px 8px;background:#fff;border:1px solid #86efac;border-radius:4px;font-size:12px;color:#166534}.allowed-values-editor .value-chip .remove-value-btn{background:none;border:none;padding:0;cursor:pointer;color:#94a3b8;display:flex;align-items:center;transition:color .15s ease}.allowed-values-editor .value-chip .remove-value-btn:hover{color:#ef4444}.allowed-values-editor .value-chip .remove-value-btn mat-icon{font-size:14px;width:14px;height:14px}.allowed-values-editor .no-values{font-size:12px;color:#94a3b8;font-style:italic}.field-actions .has-values{color:#22c55e!important}.field-actions .has-default{color:#8b5cf6!important}.default-value-editor{margin-left:calc(var(--level, 0) * 24px + 48px);margin-top:4px;margin-bottom:8px;padding:12px;background:#f5f3ff;border:1px solid #c4b5fd;border-radius:8px}.default-value-editor .default-header{display:flex;align-items:center;gap:8px}.default-value-editor .default-label{font-size:12px;font-weight:500;color:#6d28d9}.default-value-editor .default-input{flex:1;padding:6px 10px;font-size:13px;border:1px solid #a78bfa;border-radius:4px;outline:none;background:#fff;max-width:200px}.default-value-editor .default-input:focus{border-color:#8b5cf6}.default-value-editor .default-input::placeholder{color:#94a3b8}.default-value-editor .default-select{padding:6px 10px;font-size:13px;border:1px solid #a78bfa;border-radius:4px;outline:none;background:#fff;cursor:pointer}.default-value-editor .default-select:focus{border-color:#8b5cf6}.default-value-editor .clear-default-btn{background:none;border:none;padding:4px;cursor:pointer;color:#94a3b8;display:flex;align-items:center;transition:color .15s ease}.default-value-editor .clear-default-btn:hover{color:#ef4444}.default-value-editor .clear-default-btn mat-icon{font-size:16px;width:16px;height:16px}\n"] }]
|
|
2457
3544
|
}], propDecorators: { schema: [{
|
|
2458
3545
|
type: Input
|
|
3546
|
+
}], showJsonToggle: [{
|
|
3547
|
+
type: Input
|
|
2459
3548
|
}], schemaChange: [{
|
|
2460
3549
|
type: Output
|
|
2461
3550
|
}], save: [{
|
|
@@ -2471,5 +3560,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
2471
3560
|
* Generated bundle index. Do not edit.
|
|
2472
3561
|
*/
|
|
2473
3562
|
|
|
2474
|
-
export { ArrayFilterModalComponent, ArraySelectorModalComponent, DataMapperComponent, DefaultValuePopoverComponent, MappingService, SchemaEditorComponent, SchemaParserService, SchemaTreeComponent, SvgConnectorService, TransformationPopoverComponent, TransformationService };
|
|
3563
|
+
export { ArrayFilterModalComponent, ArraySelectorModalComponent, DataMapperComponent, DefaultValuePopoverComponent, MappingService, SchemaEditorComponent, SchemaParserService, SchemaTreeComponent, SvgConnectorService, TransformationPopoverComponent, TransformationService, addProperty, createEmptySchema, getSchemaType, removeProperty, schemaToFields };
|
|
2475
3564
|
//# sourceMappingURL=expeed-ngx-data-mapper.mjs.map
|