@expeed/ngx-data-mapper 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,2475 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { signal, computed, Injectable, EventEmitter, inject, ViewChildren, ViewChild, Output, Input, Component, HostListener } from '@angular/core';
|
|
3
|
+
import * as i1 from '@angular/common';
|
|
4
|
+
import { CommonModule } from '@angular/common';
|
|
5
|
+
import * as i3 from '@angular/material/icon';
|
|
6
|
+
import { MatIconModule } from '@angular/material/icon';
|
|
7
|
+
import * as i2$1 from '@angular/material/button';
|
|
8
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
9
|
+
import * as i8 from '@angular/material/tooltip';
|
|
10
|
+
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
11
|
+
import * as i2 from '@angular/forms';
|
|
12
|
+
import { FormsModule } from '@angular/forms';
|
|
13
|
+
import * as i5 from '@angular/material/select';
|
|
14
|
+
import { MatSelectModule } from '@angular/material/select';
|
|
15
|
+
import * as i5$1 from '@angular/material/input';
|
|
16
|
+
import { MatInputModule } from '@angular/material/input';
|
|
17
|
+
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
18
|
+
import * as i6 from '@angular/material/radio';
|
|
19
|
+
import { MatRadioModule } from '@angular/material/radio';
|
|
20
|
+
import * as i7 from '@angular/material/slide-toggle';
|
|
21
|
+
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
|
22
|
+
import * as i9 from '@angular/material/divider';
|
|
23
|
+
import { MatDividerModule } from '@angular/material/divider';
|
|
24
|
+
import * as i6$1 from '@angular/material/datepicker';
|
|
25
|
+
import { MatDatepickerModule } from '@angular/material/datepicker';
|
|
26
|
+
import { MatNativeDateModule } from '@angular/material/core';
|
|
27
|
+
import * as i7$1 from '@angular/material/menu';
|
|
28
|
+
import { MatMenuModule } from '@angular/material/menu';
|
|
29
|
+
import { DragDropModule } from '@angular/cdk/drag-drop';
|
|
30
|
+
|
|
31
|
+
class MappingService {
|
|
32
|
+
mappings = signal([], ...(ngDevMode ? [{ debugName: "mappings" }] : []));
|
|
33
|
+
arrayMappings = signal([], ...(ngDevMode ? [{ debugName: "arrayMappings" }] : []));
|
|
34
|
+
arrayToObjectMappings = signal([], ...(ngDevMode ? [{ debugName: "arrayToObjectMappings" }] : []));
|
|
35
|
+
defaultValues = signal([], ...(ngDevMode ? [{ debugName: "defaultValues" }] : []));
|
|
36
|
+
selectedMappingId = signal(null, ...(ngDevMode ? [{ debugName: "selectedMappingId" }] : []));
|
|
37
|
+
dragState = signal({
|
|
38
|
+
isDragging: false,
|
|
39
|
+
sourceField: null,
|
|
40
|
+
startPoint: null,
|
|
41
|
+
currentPoint: null,
|
|
42
|
+
}, ...(ngDevMode ? [{ debugName: "dragState" }] : []));
|
|
43
|
+
allMappings = computed(() => this.mappings(), ...(ngDevMode ? [{ debugName: "allMappings" }] : []));
|
|
44
|
+
allArrayMappings = computed(() => this.arrayMappings(), ...(ngDevMode ? [{ debugName: "allArrayMappings" }] : []));
|
|
45
|
+
allArrayToObjectMappings = computed(() => this.arrayToObjectMappings(), ...(ngDevMode ? [{ debugName: "allArrayToObjectMappings" }] : []));
|
|
46
|
+
allDefaultValues = computed(() => this.defaultValues(), ...(ngDevMode ? [{ debugName: "allDefaultValues" }] : []));
|
|
47
|
+
selectedMapping = computed(() => {
|
|
48
|
+
const id = this.selectedMappingId();
|
|
49
|
+
return this.mappings().find((m) => m.id === id) || null;
|
|
50
|
+
}, ...(ngDevMode ? [{ debugName: "selectedMapping" }] : []));
|
|
51
|
+
currentDragState = computed(() => this.dragState(), ...(ngDevMode ? [{ debugName: "currentDragState" }] : []));
|
|
52
|
+
generateId() {
|
|
53
|
+
return `mapping-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
54
|
+
}
|
|
55
|
+
startDrag(field, startPoint) {
|
|
56
|
+
this.dragState.set({
|
|
57
|
+
isDragging: true,
|
|
58
|
+
sourceField: field,
|
|
59
|
+
startPoint,
|
|
60
|
+
currentPoint: startPoint,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
updateDragPosition(currentPoint) {
|
|
64
|
+
this.dragState.update((state) => ({
|
|
65
|
+
...state,
|
|
66
|
+
currentPoint,
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
endDrag() {
|
|
70
|
+
this.dragState.set({
|
|
71
|
+
isDragging: false,
|
|
72
|
+
sourceField: null,
|
|
73
|
+
startPoint: null,
|
|
74
|
+
currentPoint: null,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
createMapping(sourceFields, targetField, transformation) {
|
|
78
|
+
// Check if this is an array-to-array mapping
|
|
79
|
+
const sourceField = sourceFields[0];
|
|
80
|
+
if (sourceField.type === 'array' && targetField.type === 'array') {
|
|
81
|
+
return this.createArrayMapping(sourceField, targetField);
|
|
82
|
+
}
|
|
83
|
+
// Check if this is an array-to-object mapping
|
|
84
|
+
if (sourceField.type === 'array' && targetField.type === 'object') {
|
|
85
|
+
return this.createArrayToObjectMapping(sourceField, targetField);
|
|
86
|
+
}
|
|
87
|
+
// Check if fields are within arrays and need array context
|
|
88
|
+
const arrayMappingId = this.findOrCreateArrayContext(sourceField, targetField);
|
|
89
|
+
// Check if fields need array-to-object context
|
|
90
|
+
const arrayToObjectMappingId = this.findOrCreateArrayToObjectContext(sourceField, targetField);
|
|
91
|
+
const existingMapping = this.mappings().find((m) => m.targetField.id === targetField.id);
|
|
92
|
+
if (existingMapping) {
|
|
93
|
+
// Add source fields to existing mapping (for concat scenarios)
|
|
94
|
+
const updatedMapping = {
|
|
95
|
+
...existingMapping,
|
|
96
|
+
sourceFields: [
|
|
97
|
+
...existingMapping.sourceFields,
|
|
98
|
+
...sourceFields.filter((sf) => !existingMapping.sourceFields.some((esf) => esf.id === sf.id)),
|
|
99
|
+
],
|
|
100
|
+
transformation: existingMapping.sourceFields.length + sourceFields.length > 1
|
|
101
|
+
? { type: 'concat', separator: ' ', ...transformation }
|
|
102
|
+
: transformation || { type: 'direct' },
|
|
103
|
+
};
|
|
104
|
+
this.mappings.update((mappings) => mappings.map((m) => (m.id === existingMapping.id ? updatedMapping : m)));
|
|
105
|
+
return updatedMapping;
|
|
106
|
+
}
|
|
107
|
+
const newMapping = {
|
|
108
|
+
id: this.generateId(),
|
|
109
|
+
sourceFields,
|
|
110
|
+
targetField,
|
|
111
|
+
transformation: transformation || { type: 'direct' },
|
|
112
|
+
isArrayMapping: false, // Only true for array-to-array connections
|
|
113
|
+
arrayMappingId, // Links to parent array mapping if within array context
|
|
114
|
+
isArrayToObjectMapping: false,
|
|
115
|
+
arrayToObjectMappingId, // Links to parent array-to-object mapping if applicable
|
|
116
|
+
};
|
|
117
|
+
this.mappings.update((mappings) => [...mappings, newMapping]);
|
|
118
|
+
// If this is part of an array mapping, add to itemMappings
|
|
119
|
+
if (arrayMappingId) {
|
|
120
|
+
this.arrayMappings.update((ams) => ams.map((am) => am.id === arrayMappingId
|
|
121
|
+
? { ...am, itemMappings: [...am.itemMappings, newMapping] }
|
|
122
|
+
: am));
|
|
123
|
+
}
|
|
124
|
+
// If this is part of an array-to-object mapping, add to itemMappings
|
|
125
|
+
if (arrayToObjectMappingId) {
|
|
126
|
+
this.arrayToObjectMappings.update((ams) => ams.map((am) => am.id === arrayToObjectMappingId
|
|
127
|
+
? { ...am, itemMappings: [...am.itemMappings, newMapping] }
|
|
128
|
+
: am));
|
|
129
|
+
}
|
|
130
|
+
return newMapping;
|
|
131
|
+
}
|
|
132
|
+
createArrayMapping(sourceArray, targetArray) {
|
|
133
|
+
// Check if array mapping already exists
|
|
134
|
+
const existingArrayMapping = this.arrayMappings().find((am) => am.sourceArray.id === sourceArray.id && am.targetArray.id === targetArray.id);
|
|
135
|
+
if (existingArrayMapping) {
|
|
136
|
+
// Return a dummy field mapping representing the array mapping
|
|
137
|
+
const existing = this.mappings().find(m => m.id === existingArrayMapping.id);
|
|
138
|
+
if (existing)
|
|
139
|
+
return existing;
|
|
140
|
+
}
|
|
141
|
+
const arrayMapping = {
|
|
142
|
+
id: this.generateId(),
|
|
143
|
+
sourceArray,
|
|
144
|
+
targetArray,
|
|
145
|
+
itemMappings: [],
|
|
146
|
+
};
|
|
147
|
+
this.arrayMappings.update((ams) => [...ams, arrayMapping]);
|
|
148
|
+
// Also create a field mapping to visualize the array connection
|
|
149
|
+
const fieldMapping = {
|
|
150
|
+
id: arrayMapping.id,
|
|
151
|
+
sourceFields: [sourceArray],
|
|
152
|
+
targetField: targetArray,
|
|
153
|
+
transformation: { type: 'direct' },
|
|
154
|
+
isArrayMapping: true,
|
|
155
|
+
};
|
|
156
|
+
this.mappings.update((mappings) => [...mappings, fieldMapping]);
|
|
157
|
+
return fieldMapping;
|
|
158
|
+
}
|
|
159
|
+
createArrayToObjectMapping(sourceArray, targetObject) {
|
|
160
|
+
// Check if array-to-object mapping already exists
|
|
161
|
+
const existingMapping = this.arrayToObjectMappings().find((am) => am.sourceArray.id === sourceArray.id && am.targetObject.id === targetObject.id);
|
|
162
|
+
if (existingMapping) {
|
|
163
|
+
const existing = this.mappings().find(m => m.id === existingMapping.id);
|
|
164
|
+
if (existing)
|
|
165
|
+
return existing;
|
|
166
|
+
}
|
|
167
|
+
const arrayToObjectMapping = {
|
|
168
|
+
id: this.generateId(),
|
|
169
|
+
sourceArray,
|
|
170
|
+
targetObject,
|
|
171
|
+
selector: { mode: 'first' }, // Default to first item
|
|
172
|
+
itemMappings: [],
|
|
173
|
+
};
|
|
174
|
+
this.arrayToObjectMappings.update((ams) => [...ams, arrayToObjectMapping]);
|
|
175
|
+
// Create a field mapping to visualize the connection
|
|
176
|
+
const fieldMapping = {
|
|
177
|
+
id: arrayToObjectMapping.id,
|
|
178
|
+
sourceFields: [sourceArray],
|
|
179
|
+
targetField: targetObject,
|
|
180
|
+
transformation: { type: 'direct' },
|
|
181
|
+
isArrayToObjectMapping: true,
|
|
182
|
+
};
|
|
183
|
+
this.mappings.update((mappings) => [...mappings, fieldMapping]);
|
|
184
|
+
return fieldMapping;
|
|
185
|
+
}
|
|
186
|
+
findOrCreateArrayContext(sourceField, targetField) {
|
|
187
|
+
// If both fields are array items, check if an array mapping exists
|
|
188
|
+
if (sourceField.isArrayItem && targetField.isArrayItem) {
|
|
189
|
+
const existingArrayMapping = this.arrayMappings().find((am) => am.sourceArray.path === sourceField.parentArrayPath &&
|
|
190
|
+
am.targetArray.path === targetField.parentArrayPath);
|
|
191
|
+
if (existingArrayMapping) {
|
|
192
|
+
return existingArrayMapping.id;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
findOrCreateArrayToObjectContext(sourceField, targetField) {
|
|
198
|
+
// If source is array item and target is within an object (not array item)
|
|
199
|
+
if (sourceField.isArrayItem && !targetField.isArrayItem && targetField.path.includes('.')) {
|
|
200
|
+
// Find the parent object path
|
|
201
|
+
const targetParts = targetField.path.split('.');
|
|
202
|
+
const parentObjectPath = targetParts.slice(0, -1).join('.');
|
|
203
|
+
const existingMapping = this.arrayToObjectMappings().find((am) => am.sourceArray.path === sourceField.parentArrayPath &&
|
|
204
|
+
am.targetObject.path === parentObjectPath);
|
|
205
|
+
if (existingMapping) {
|
|
206
|
+
return existingMapping.id;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
getArrayMapping(id) {
|
|
212
|
+
return this.arrayMappings().find((am) => am.id === id);
|
|
213
|
+
}
|
|
214
|
+
getArrayMappingForField(field) {
|
|
215
|
+
if (!field.parentArrayPath)
|
|
216
|
+
return undefined;
|
|
217
|
+
return this.arrayMappings().find((am) => am.sourceArray.path === field.parentArrayPath ||
|
|
218
|
+
am.targetArray.path === field.parentArrayPath);
|
|
219
|
+
}
|
|
220
|
+
removeArrayMapping(arrayMappingId) {
|
|
221
|
+
// Remove the array mapping
|
|
222
|
+
this.arrayMappings.update((ams) => ams.filter((am) => am.id !== arrayMappingId));
|
|
223
|
+
// Remove all field mappings associated with this array mapping
|
|
224
|
+
this.mappings.update((mappings) => mappings.filter((m) => m.arrayMappingId !== arrayMappingId && m.id !== arrayMappingId));
|
|
225
|
+
}
|
|
226
|
+
updateArrayFilter(arrayMappingId, filter) {
|
|
227
|
+
this.arrayMappings.update((ams) => ams.map((am) => am.id === arrayMappingId ? { ...am, filter } : am));
|
|
228
|
+
}
|
|
229
|
+
getArrayToObjectMapping(id) {
|
|
230
|
+
return this.arrayToObjectMappings().find((am) => am.id === id);
|
|
231
|
+
}
|
|
232
|
+
updateArrayToObjectSelector(mappingId, selector) {
|
|
233
|
+
this.arrayToObjectMappings.update((ams) => ams.map((am) => am.id === mappingId ? { ...am, selector } : am));
|
|
234
|
+
}
|
|
235
|
+
removeArrayToObjectMapping(mappingId) {
|
|
236
|
+
// Remove the array-to-object mapping
|
|
237
|
+
this.arrayToObjectMappings.update((ams) => ams.filter((am) => am.id !== mappingId));
|
|
238
|
+
// Remove all field mappings associated with this mapping
|
|
239
|
+
this.mappings.update((mappings) => mappings.filter((m) => m.arrayToObjectMappingId !== mappingId && m.id !== mappingId));
|
|
240
|
+
}
|
|
241
|
+
updateMapping(mappingId, updates) {
|
|
242
|
+
this.mappings.update((mappings) => mappings.map((m) => (m.id === mappingId ? { ...m, ...updates } : m)));
|
|
243
|
+
}
|
|
244
|
+
updateTransformation(mappingId, transformation) {
|
|
245
|
+
this.mappings.update((mappings) => mappings.map((m) => m.id === mappingId ? { ...m, transformation } : m));
|
|
246
|
+
}
|
|
247
|
+
removeMapping(mappingId) {
|
|
248
|
+
this.mappings.update((mappings) => mappings.filter((m) => m.id !== mappingId));
|
|
249
|
+
if (this.selectedMappingId() === mappingId) {
|
|
250
|
+
this.selectedMappingId.set(null);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
removeSourceFromMapping(mappingId, sourceFieldId) {
|
|
254
|
+
const mapping = this.mappings().find((m) => m.id === mappingId);
|
|
255
|
+
if (!mapping)
|
|
256
|
+
return;
|
|
257
|
+
if (mapping.sourceFields.length <= 1) {
|
|
258
|
+
this.removeMapping(mappingId);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
this.mappings.update((mappings) => mappings.map((m) => m.id === mappingId
|
|
262
|
+
? {
|
|
263
|
+
...m,
|
|
264
|
+
sourceFields: m.sourceFields.filter((sf) => sf.id !== sourceFieldId),
|
|
265
|
+
transformation: m.sourceFields.length - 1 === 1
|
|
266
|
+
? { type: 'direct' }
|
|
267
|
+
: m.transformation,
|
|
268
|
+
}
|
|
269
|
+
: m));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
selectMapping(mappingId) {
|
|
273
|
+
this.selectedMappingId.set(mappingId);
|
|
274
|
+
}
|
|
275
|
+
getMappingForTarget(targetFieldId) {
|
|
276
|
+
return this.mappings().find((m) => m.targetField.id === targetFieldId);
|
|
277
|
+
}
|
|
278
|
+
getMappingsForSource(sourceFieldId) {
|
|
279
|
+
return this.mappings().filter((m) => m.sourceFields.some((sf) => sf.id === sourceFieldId));
|
|
280
|
+
}
|
|
281
|
+
clearAllMappings() {
|
|
282
|
+
this.mappings.set([]);
|
|
283
|
+
this.arrayMappings.set([]);
|
|
284
|
+
this.arrayToObjectMappings.set([]);
|
|
285
|
+
this.defaultValues.set([]);
|
|
286
|
+
this.selectedMappingId.set(null);
|
|
287
|
+
}
|
|
288
|
+
// Default value methods
|
|
289
|
+
setDefaultValue(targetField, value) {
|
|
290
|
+
const valueType = this.getValueType(targetField.type);
|
|
291
|
+
const existingDefault = this.defaultValues().find(d => d.targetField.id === targetField.id);
|
|
292
|
+
if (existingDefault) {
|
|
293
|
+
const updated = { ...existingDefault, value, valueType };
|
|
294
|
+
this.defaultValues.update(dv => dv.map(d => d.id === existingDefault.id ? updated : d));
|
|
295
|
+
return updated;
|
|
296
|
+
}
|
|
297
|
+
const newDefault = {
|
|
298
|
+
id: this.generateId(),
|
|
299
|
+
targetField,
|
|
300
|
+
value,
|
|
301
|
+
valueType,
|
|
302
|
+
};
|
|
303
|
+
this.defaultValues.update(dv => [...dv, newDefault]);
|
|
304
|
+
return newDefault;
|
|
305
|
+
}
|
|
306
|
+
getDefaultValue(targetFieldId) {
|
|
307
|
+
return this.defaultValues().find(d => d.targetField.id === targetFieldId);
|
|
308
|
+
}
|
|
309
|
+
removeDefaultValue(targetFieldId) {
|
|
310
|
+
this.defaultValues.update(dv => dv.filter(d => d.targetField.id !== targetFieldId));
|
|
311
|
+
}
|
|
312
|
+
hasDefaultValue(targetFieldId) {
|
|
313
|
+
return this.defaultValues().some(d => d.targetField.id === targetFieldId);
|
|
314
|
+
}
|
|
315
|
+
getValueType(fieldType) {
|
|
316
|
+
switch (fieldType) {
|
|
317
|
+
case 'number': return 'number';
|
|
318
|
+
case 'boolean': return 'boolean';
|
|
319
|
+
case 'date': return 'date';
|
|
320
|
+
default: return 'string';
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
exportMappings() {
|
|
324
|
+
const exportData = {
|
|
325
|
+
mappings: this.mappings(),
|
|
326
|
+
arrayMappings: this.arrayMappings(),
|
|
327
|
+
arrayToObjectMappings: this.arrayToObjectMappings(),
|
|
328
|
+
defaultValues: this.defaultValues(),
|
|
329
|
+
};
|
|
330
|
+
return JSON.stringify(exportData, null, 2);
|
|
331
|
+
}
|
|
332
|
+
importMappings(json) {
|
|
333
|
+
try {
|
|
334
|
+
const data = JSON.parse(json);
|
|
335
|
+
if (data.mappings) {
|
|
336
|
+
this.mappings.set(data.mappings);
|
|
337
|
+
this.arrayMappings.set(data.arrayMappings || []);
|
|
338
|
+
this.arrayToObjectMappings.set(data.arrayToObjectMappings || []);
|
|
339
|
+
this.defaultValues.set(data.defaultValues || []);
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
// Legacy format
|
|
343
|
+
this.mappings.set(data);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch (e) {
|
|
347
|
+
console.error('Failed to import mappings:', e);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: MappingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
351
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: MappingService, providedIn: 'root' });
|
|
352
|
+
}
|
|
353
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: MappingService, decorators: [{
|
|
354
|
+
type: Injectable,
|
|
355
|
+
args: [{
|
|
356
|
+
providedIn: 'root',
|
|
357
|
+
}]
|
|
358
|
+
}] });
|
|
359
|
+
|
|
360
|
+
class TransformationService {
|
|
361
|
+
applyTransformation(sourceValues, sourceFields, config) {
|
|
362
|
+
const values = sourceFields.map((f) => this.getValueByPath(sourceValues, f.path));
|
|
363
|
+
switch (config.type) {
|
|
364
|
+
case 'direct':
|
|
365
|
+
return String(values[0] ?? '');
|
|
366
|
+
case 'concat':
|
|
367
|
+
if (config.template) {
|
|
368
|
+
return this.applyTemplate(config.template, sourceFields, sourceValues);
|
|
369
|
+
}
|
|
370
|
+
return values.join(config.separator ?? ' ');
|
|
371
|
+
case 'substring':
|
|
372
|
+
const str = String(values[0] ?? '');
|
|
373
|
+
return str.substring(config.startIndex ?? 0, config.endIndex ?? str.length);
|
|
374
|
+
case 'replace':
|
|
375
|
+
return String(values[0] ?? '').replace(new RegExp(config.searchValue ?? '', 'g'), config.replaceValue ?? '');
|
|
376
|
+
case 'uppercase':
|
|
377
|
+
return String(values[0] ?? '').toUpperCase();
|
|
378
|
+
case 'lowercase':
|
|
379
|
+
return String(values[0] ?? '').toLowerCase();
|
|
380
|
+
case 'dateFormat':
|
|
381
|
+
return this.formatDate(values[0], config.inputFormat, config.outputFormat);
|
|
382
|
+
case 'extractYear':
|
|
383
|
+
return this.extractDatePart(values[0], 'year');
|
|
384
|
+
case 'extractMonth':
|
|
385
|
+
return this.extractDatePart(values[0], 'month');
|
|
386
|
+
case 'extractDay':
|
|
387
|
+
return this.extractDatePart(values[0], 'day');
|
|
388
|
+
case 'extractHour':
|
|
389
|
+
return this.extractDatePart(values[0], 'hour');
|
|
390
|
+
case 'extractMinute':
|
|
391
|
+
return this.extractDatePart(values[0], 'minute');
|
|
392
|
+
case 'extractSecond':
|
|
393
|
+
return this.extractDatePart(values[0], 'second');
|
|
394
|
+
case 'numberFormat':
|
|
395
|
+
return this.formatNumber(values[0], config.decimalPlaces, config.prefix, config.suffix);
|
|
396
|
+
case 'template':
|
|
397
|
+
return this.applyTemplate(config.template ?? '', sourceFields, sourceValues);
|
|
398
|
+
case 'custom':
|
|
399
|
+
return this.applyCustomExpression(config.expression ?? '', sourceFields, sourceValues);
|
|
400
|
+
default:
|
|
401
|
+
return String(values[0] ?? '');
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
getValueByPath(obj, path) {
|
|
405
|
+
return path.split('.').reduce((acc, part) => {
|
|
406
|
+
if (acc && typeof acc === 'object') {
|
|
407
|
+
return acc[part];
|
|
408
|
+
}
|
|
409
|
+
return undefined;
|
|
410
|
+
}, obj);
|
|
411
|
+
}
|
|
412
|
+
applyTemplate(template, sourceFields, sourceValues) {
|
|
413
|
+
let result = template;
|
|
414
|
+
sourceFields.forEach((field) => {
|
|
415
|
+
const value = this.getValueByPath(sourceValues, field.path);
|
|
416
|
+
result = result.replace(new RegExp(`\\{${field.name}\\}`, 'g'), String(value ?? ''));
|
|
417
|
+
result = result.replace(new RegExp(`\\{${field.path}\\}`, 'g'), String(value ?? ''));
|
|
418
|
+
});
|
|
419
|
+
return result;
|
|
420
|
+
}
|
|
421
|
+
formatDate(value, inputFormat, outputFormat) {
|
|
422
|
+
if (!value)
|
|
423
|
+
return '';
|
|
424
|
+
try {
|
|
425
|
+
const date = new Date(value);
|
|
426
|
+
if (isNaN(date.getTime()))
|
|
427
|
+
return String(value);
|
|
428
|
+
// Simple format implementation
|
|
429
|
+
const format = outputFormat ?? 'YYYY-MM-DD';
|
|
430
|
+
return format
|
|
431
|
+
.replace('YYYY', date.getFullYear().toString())
|
|
432
|
+
.replace('MM', (date.getMonth() + 1).toString().padStart(2, '0'))
|
|
433
|
+
.replace('DD', date.getDate().toString().padStart(2, '0'))
|
|
434
|
+
.replace('HH', date.getHours().toString().padStart(2, '0'))
|
|
435
|
+
.replace('mm', date.getMinutes().toString().padStart(2, '0'))
|
|
436
|
+
.replace('ss', date.getSeconds().toString().padStart(2, '0'));
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
return String(value);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
extractDatePart(value, part) {
|
|
443
|
+
if (!value)
|
|
444
|
+
return '';
|
|
445
|
+
try {
|
|
446
|
+
const date = new Date(value);
|
|
447
|
+
if (isNaN(date.getTime()))
|
|
448
|
+
return String(value);
|
|
449
|
+
switch (part) {
|
|
450
|
+
case 'year':
|
|
451
|
+
return date.getFullYear().toString();
|
|
452
|
+
case 'month':
|
|
453
|
+
return (date.getMonth() + 1).toString().padStart(2, '0');
|
|
454
|
+
case 'day':
|
|
455
|
+
return date.getDate().toString().padStart(2, '0');
|
|
456
|
+
case 'hour':
|
|
457
|
+
return date.getHours().toString().padStart(2, '0');
|
|
458
|
+
case 'minute':
|
|
459
|
+
return date.getMinutes().toString().padStart(2, '0');
|
|
460
|
+
case 'second':
|
|
461
|
+
return date.getSeconds().toString().padStart(2, '0');
|
|
462
|
+
default:
|
|
463
|
+
return String(value);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
return String(value);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
formatNumber(value, decimalPlaces, prefix, suffix) {
|
|
471
|
+
if (value === null || value === undefined)
|
|
472
|
+
return '';
|
|
473
|
+
const num = Number(value);
|
|
474
|
+
if (isNaN(num))
|
|
475
|
+
return String(value);
|
|
476
|
+
let formatted = decimalPlaces !== undefined
|
|
477
|
+
? num.toFixed(decimalPlaces)
|
|
478
|
+
: num.toString();
|
|
479
|
+
return `${prefix ?? ''}${formatted}${suffix ?? ''}`;
|
|
480
|
+
}
|
|
481
|
+
applyCustomExpression(expression, sourceFields, sourceValues) {
|
|
482
|
+
try {
|
|
483
|
+
// Create a safe context with field values
|
|
484
|
+
const context = {};
|
|
485
|
+
sourceFields.forEach((field) => {
|
|
486
|
+
context[field.name] = this.getValueByPath(sourceValues, field.path);
|
|
487
|
+
});
|
|
488
|
+
// Very basic expression evaluation - in production use a proper parser
|
|
489
|
+
const fn = new Function(...Object.keys(context), `return ${expression}`);
|
|
490
|
+
return String(fn(...Object.values(context)));
|
|
491
|
+
}
|
|
492
|
+
catch (e) {
|
|
493
|
+
console.error('Custom expression error:', e);
|
|
494
|
+
return '[Error]';
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
getTransformationLabel(type) {
|
|
498
|
+
const labels = {
|
|
499
|
+
direct: 'Direct Mapping',
|
|
500
|
+
concat: 'Concatenate',
|
|
501
|
+
substring: 'Substring',
|
|
502
|
+
replace: 'Find & Replace',
|
|
503
|
+
uppercase: 'Uppercase',
|
|
504
|
+
lowercase: 'Lowercase',
|
|
505
|
+
dateFormat: 'Date Format',
|
|
506
|
+
extractYear: 'Extract Year',
|
|
507
|
+
extractMonth: 'Extract Month',
|
|
508
|
+
extractDay: 'Extract Day',
|
|
509
|
+
extractHour: 'Extract Hour',
|
|
510
|
+
extractMinute: 'Extract Minute',
|
|
511
|
+
extractSecond: 'Extract Second',
|
|
512
|
+
numberFormat: 'Number Format',
|
|
513
|
+
template: 'Template',
|
|
514
|
+
custom: 'Custom Expression',
|
|
515
|
+
};
|
|
516
|
+
return labels[type];
|
|
517
|
+
}
|
|
518
|
+
getAvailableTransformations() {
|
|
519
|
+
return [
|
|
520
|
+
{ type: 'direct', label: 'Direct Mapping' },
|
|
521
|
+
{ type: 'concat', label: 'Concatenate', category: 'String' },
|
|
522
|
+
{ type: 'substring', label: 'Substring', category: 'String' },
|
|
523
|
+
{ type: 'replace', label: 'Find & Replace', category: 'String' },
|
|
524
|
+
{ type: 'uppercase', label: 'Uppercase', category: 'String' },
|
|
525
|
+
{ type: 'lowercase', label: 'Lowercase', category: 'String' },
|
|
526
|
+
{ type: 'template', label: 'Template', category: 'String' },
|
|
527
|
+
{ type: 'dateFormat', label: 'Format Date', category: 'Date' },
|
|
528
|
+
{ type: 'extractYear', label: 'Extract Year', category: 'Date' },
|
|
529
|
+
{ type: 'extractMonth', label: 'Extract Month', category: 'Date' },
|
|
530
|
+
{ type: 'extractDay', label: 'Extract Day', category: 'Date' },
|
|
531
|
+
{ type: 'extractHour', label: 'Extract Hour', category: 'Date' },
|
|
532
|
+
{ type: 'extractMinute', label: 'Extract Minute', category: 'Date' },
|
|
533
|
+
{ type: 'extractSecond', label: 'Extract Second', category: 'Date' },
|
|
534
|
+
{ type: 'numberFormat', label: 'Number Format', category: 'Number' },
|
|
535
|
+
{ type: 'custom', label: 'Custom Expression', category: 'Advanced' },
|
|
536
|
+
];
|
|
537
|
+
}
|
|
538
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: TransformationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
539
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: TransformationService, providedIn: 'root' });
|
|
540
|
+
}
|
|
541
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: TransformationService, decorators: [{
|
|
542
|
+
type: Injectable,
|
|
543
|
+
args: [{
|
|
544
|
+
providedIn: 'root',
|
|
545
|
+
}]
|
|
546
|
+
}] });
|
|
547
|
+
|
|
548
|
+
class SvgConnectorService {
|
|
549
|
+
createBezierPath(start, end) {
|
|
550
|
+
const dx = end.x - start.x;
|
|
551
|
+
const controlPointOffset = Math.min(Math.abs(dx) * 0.5, 150);
|
|
552
|
+
const cp1x = start.x + controlPointOffset;
|
|
553
|
+
const cp1y = start.y;
|
|
554
|
+
const cp2x = end.x - controlPointOffset;
|
|
555
|
+
const cp2y = end.y;
|
|
556
|
+
return `M ${start.x} ${start.y} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${end.x} ${end.y}`;
|
|
557
|
+
}
|
|
558
|
+
createMultiSourcePath(sources, target) {
|
|
559
|
+
const mergeX = target.x - 80;
|
|
560
|
+
const mergeY = target.y;
|
|
561
|
+
const mergePoint = { x: mergeX, y: mergeY };
|
|
562
|
+
const paths = sources.map((source) => {
|
|
563
|
+
// Path from source to merge point
|
|
564
|
+
const dx1 = mergeX - source.x;
|
|
565
|
+
const cp1Offset = Math.min(Math.abs(dx1) * 0.4, 100);
|
|
566
|
+
return `M ${source.x} ${source.y} C ${source.x + cp1Offset} ${source.y}, ${mergeX - cp1Offset} ${mergeY}, ${mergeX} ${mergeY}`;
|
|
567
|
+
});
|
|
568
|
+
// Add path from merge point to target
|
|
569
|
+
const finalPath = `M ${mergeX} ${mergeY} L ${target.x} ${target.y}`;
|
|
570
|
+
paths.push(finalPath);
|
|
571
|
+
return { paths, mergePoint };
|
|
572
|
+
}
|
|
573
|
+
getMidPoint(start, end) {
|
|
574
|
+
return {
|
|
575
|
+
x: (start.x + end.x) / 2,
|
|
576
|
+
y: (start.y + end.y) / 2,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
getMultiSourceMidPoint(sources, target) {
|
|
580
|
+
const mergeX = target.x - 80;
|
|
581
|
+
return {
|
|
582
|
+
x: mergeX,
|
|
583
|
+
y: target.y,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
calculateConnectionPoint(rect, side, containerRect) {
|
|
587
|
+
const relativeY = rect.top - containerRect.top + rect.height / 2;
|
|
588
|
+
if (side === 'source') {
|
|
589
|
+
return {
|
|
590
|
+
x: rect.right - containerRect.left,
|
|
591
|
+
y: relativeY,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
return {
|
|
596
|
+
x: rect.left - containerRect.left,
|
|
597
|
+
y: relativeY,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
isPointNearPath(point, pathStart, pathEnd, threshold = 10) {
|
|
602
|
+
// Simplified hit detection using distance to line segment
|
|
603
|
+
const A = point.x - pathStart.x;
|
|
604
|
+
const B = point.y - pathStart.y;
|
|
605
|
+
const C = pathEnd.x - pathStart.x;
|
|
606
|
+
const D = pathEnd.y - pathStart.y;
|
|
607
|
+
const dot = A * C + B * D;
|
|
608
|
+
const lenSq = C * C + D * D;
|
|
609
|
+
let param = -1;
|
|
610
|
+
if (lenSq !== 0) {
|
|
611
|
+
param = dot / lenSq;
|
|
612
|
+
}
|
|
613
|
+
let xx, yy;
|
|
614
|
+
if (param < 0) {
|
|
615
|
+
xx = pathStart.x;
|
|
616
|
+
yy = pathStart.y;
|
|
617
|
+
}
|
|
618
|
+
else if (param > 1) {
|
|
619
|
+
xx = pathEnd.x;
|
|
620
|
+
yy = pathEnd.y;
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
xx = pathStart.x + param * C;
|
|
624
|
+
yy = pathStart.y + param * D;
|
|
625
|
+
}
|
|
626
|
+
const dx = point.x - xx;
|
|
627
|
+
const dy = point.y - yy;
|
|
628
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
629
|
+
return distance <= threshold;
|
|
630
|
+
}
|
|
631
|
+
createDragPath(start, end) {
|
|
632
|
+
return this.createBezierPath(start, end);
|
|
633
|
+
}
|
|
634
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SvgConnectorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
635
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SvgConnectorService, providedIn: 'root' });
|
|
636
|
+
}
|
|
637
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SvgConnectorService, decorators: [{
|
|
638
|
+
type: Injectable,
|
|
639
|
+
args: [{
|
|
640
|
+
providedIn: 'root',
|
|
641
|
+
}]
|
|
642
|
+
}] });
|
|
643
|
+
|
|
644
|
+
class SchemaParserService {
|
|
645
|
+
modelRegistry = {};
|
|
646
|
+
idCounter = 0;
|
|
647
|
+
registerModels(models) {
|
|
648
|
+
this.modelRegistry = { ...this.modelRegistry, ...models };
|
|
649
|
+
}
|
|
650
|
+
clearRegistry() {
|
|
651
|
+
this.modelRegistry = {};
|
|
652
|
+
}
|
|
653
|
+
parseSchema(schemaJson, schemaName = 'Schema') {
|
|
654
|
+
const schema = typeof schemaJson === 'string' ? JSON.parse(schemaJson) : schemaJson;
|
|
655
|
+
this.idCounter = 0;
|
|
656
|
+
// Extract definitions from the schema document itself
|
|
657
|
+
const localDefs = schema.$defs || schema.definitions || {};
|
|
658
|
+
const combinedRegistry = { ...this.modelRegistry, ...localDefs };
|
|
659
|
+
let resolvedSchema;
|
|
660
|
+
if (schema.$ref) {
|
|
661
|
+
// Resolve the reference
|
|
662
|
+
resolvedSchema = this.resolveRef(schema.$ref, combinedRegistry);
|
|
663
|
+
}
|
|
664
|
+
else if (schema.properties) {
|
|
665
|
+
// Direct schema with properties
|
|
666
|
+
resolvedSchema = schema;
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
throw new Error('Schema must have either $ref or properties');
|
|
670
|
+
}
|
|
671
|
+
// Build fields from the resolved schema
|
|
672
|
+
let fields = this.buildFields(resolvedSchema, combinedRegistry, '');
|
|
673
|
+
// Apply exclude filter
|
|
674
|
+
if (schema.exclude && schema.exclude.length > 0) {
|
|
675
|
+
fields = this.applyExclude(fields, schema.exclude);
|
|
676
|
+
}
|
|
677
|
+
// Apply include filter (only if specified)
|
|
678
|
+
if (schema.include && schema.include.length > 0) {
|
|
679
|
+
fields = this.applyInclude(fields, schema.include);
|
|
680
|
+
}
|
|
681
|
+
return {
|
|
682
|
+
name: schema.title || schemaName,
|
|
683
|
+
fields,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
resolveRef(ref, registry) {
|
|
687
|
+
// Handle different ref formats:
|
|
688
|
+
// #model, #/definitions/model, #/$defs/model, model
|
|
689
|
+
let modelName;
|
|
690
|
+
if (ref.startsWith('#/$defs/')) {
|
|
691
|
+
modelName = ref.substring(8);
|
|
692
|
+
}
|
|
693
|
+
else if (ref.startsWith('#/definitions/')) {
|
|
694
|
+
modelName = ref.substring(14);
|
|
695
|
+
}
|
|
696
|
+
else if (ref.startsWith('#')) {
|
|
697
|
+
modelName = ref.substring(1);
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
modelName = ref;
|
|
701
|
+
}
|
|
702
|
+
const resolved = registry[modelName];
|
|
703
|
+
if (!resolved) {
|
|
704
|
+
throw new Error(`Cannot resolve reference: ${ref}. Model "${modelName}" not found in registry.`);
|
|
705
|
+
}
|
|
706
|
+
// If the resolved schema also has a $ref, resolve it recursively
|
|
707
|
+
if (resolved.$ref) {
|
|
708
|
+
return this.resolveRef(resolved.$ref, registry);
|
|
709
|
+
}
|
|
710
|
+
return resolved;
|
|
711
|
+
}
|
|
712
|
+
buildFields(schema, registry, parentPath, arrayContext) {
|
|
713
|
+
const fields = [];
|
|
714
|
+
if (!schema.properties) {
|
|
715
|
+
return fields;
|
|
716
|
+
}
|
|
717
|
+
for (const [name, propSchema] of Object.entries(schema.properties)) {
|
|
718
|
+
const path = parentPath ? `${parentPath}.${name}` : name;
|
|
719
|
+
const field = this.buildField(name, propSchema, registry, path, arrayContext);
|
|
720
|
+
fields.push(field);
|
|
721
|
+
}
|
|
722
|
+
return fields;
|
|
723
|
+
}
|
|
724
|
+
buildField(name, schema, registry, path, arrayContext) {
|
|
725
|
+
// Resolve $ref if present
|
|
726
|
+
let resolvedSchema = schema;
|
|
727
|
+
if (schema.$ref) {
|
|
728
|
+
resolvedSchema = { ...this.resolveRef(schema.$ref, registry), ...schema };
|
|
729
|
+
delete resolvedSchema.$ref;
|
|
730
|
+
}
|
|
731
|
+
const fieldType = this.mapType(resolvedSchema);
|
|
732
|
+
const field = {
|
|
733
|
+
id: `field-${++this.idCounter}-${name}`,
|
|
734
|
+
name,
|
|
735
|
+
type: fieldType,
|
|
736
|
+
path,
|
|
737
|
+
description: resolvedSchema.description,
|
|
738
|
+
isArrayItem: arrayContext?.isArrayItem,
|
|
739
|
+
parentArrayPath: arrayContext?.parentArrayPath,
|
|
740
|
+
};
|
|
741
|
+
// Handle nested objects
|
|
742
|
+
if (fieldType === 'object' && resolvedSchema.properties) {
|
|
743
|
+
field.children = this.buildFields(resolvedSchema, registry, path, arrayContext);
|
|
744
|
+
field.expanded = true;
|
|
745
|
+
}
|
|
746
|
+
// Handle arrays with object items
|
|
747
|
+
if (fieldType === 'array' && resolvedSchema.items) {
|
|
748
|
+
let itemSchema = resolvedSchema.items;
|
|
749
|
+
if (itemSchema.$ref) {
|
|
750
|
+
itemSchema = this.resolveRef(itemSchema.$ref, registry);
|
|
751
|
+
}
|
|
752
|
+
if (itemSchema.properties) {
|
|
753
|
+
// Mark children as array items with reference to parent array
|
|
754
|
+
field.children = this.buildFields(itemSchema, registry, `${path}[]`, {
|
|
755
|
+
isArrayItem: true,
|
|
756
|
+
parentArrayPath: path,
|
|
757
|
+
});
|
|
758
|
+
field.expanded = true;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return field;
|
|
762
|
+
}
|
|
763
|
+
mapType(schema) {
|
|
764
|
+
const type = schema.type;
|
|
765
|
+
const format = schema.format;
|
|
766
|
+
// Check format first for date types
|
|
767
|
+
if (format === 'date' || format === 'date-time' || format === 'time') {
|
|
768
|
+
return 'date';
|
|
769
|
+
}
|
|
770
|
+
switch (type) {
|
|
771
|
+
case 'string':
|
|
772
|
+
return 'string';
|
|
773
|
+
case 'number':
|
|
774
|
+
case 'integer':
|
|
775
|
+
return 'number';
|
|
776
|
+
case 'boolean':
|
|
777
|
+
return 'boolean';
|
|
778
|
+
case 'object':
|
|
779
|
+
return 'object';
|
|
780
|
+
case 'array':
|
|
781
|
+
return 'array';
|
|
782
|
+
default:
|
|
783
|
+
// If type is not specified but has properties, it's an object
|
|
784
|
+
if (schema.properties) {
|
|
785
|
+
return 'object';
|
|
786
|
+
}
|
|
787
|
+
return 'string';
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
applyExclude(fields, exclude) {
|
|
791
|
+
return fields
|
|
792
|
+
.filter((field) => !exclude.includes(field.name) && !exclude.includes(field.path))
|
|
793
|
+
.map((field) => {
|
|
794
|
+
if (field.children) {
|
|
795
|
+
// Filter nested fields by checking both full path and relative name
|
|
796
|
+
const filteredChildren = this.applyExclude(field.children, exclude);
|
|
797
|
+
return { ...field, children: filteredChildren };
|
|
798
|
+
}
|
|
799
|
+
return field;
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
applyInclude(fields, include) {
|
|
803
|
+
return fields
|
|
804
|
+
.filter((field) => {
|
|
805
|
+
// Include if field name or path matches
|
|
806
|
+
if (include.includes(field.name) || include.includes(field.path)) {
|
|
807
|
+
return true;
|
|
808
|
+
}
|
|
809
|
+
// Include if any child path matches
|
|
810
|
+
if (field.children) {
|
|
811
|
+
return this.hasIncludedChild(field.children, include);
|
|
812
|
+
}
|
|
813
|
+
return false;
|
|
814
|
+
})
|
|
815
|
+
.map((field) => {
|
|
816
|
+
if (field.children) {
|
|
817
|
+
// Keep parent but filter children
|
|
818
|
+
const filteredChildren = this.applyInclude(field.children, include);
|
|
819
|
+
return { ...field, children: filteredChildren.length > 0 ? filteredChildren : field.children };
|
|
820
|
+
}
|
|
821
|
+
return field;
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
hasIncludedChild(fields, include) {
|
|
825
|
+
return fields.some((field) => {
|
|
826
|
+
if (include.includes(field.name) || include.includes(field.path)) {
|
|
827
|
+
return true;
|
|
828
|
+
}
|
|
829
|
+
if (field.children) {
|
|
830
|
+
return this.hasIncludedChild(field.children, include);
|
|
831
|
+
}
|
|
832
|
+
return false;
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
// Utility method to create a schema document from model name
|
|
836
|
+
createSchemaFromRef(modelRef, options) {
|
|
837
|
+
return {
|
|
838
|
+
$ref: modelRef,
|
|
839
|
+
title: options?.title,
|
|
840
|
+
exclude: options?.exclude,
|
|
841
|
+
include: options?.include,
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SchemaParserService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
845
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SchemaParserService, providedIn: 'root' });
|
|
846
|
+
}
|
|
847
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SchemaParserService, decorators: [{
|
|
848
|
+
type: Injectable,
|
|
849
|
+
args: [{
|
|
850
|
+
providedIn: 'root',
|
|
851
|
+
}]
|
|
852
|
+
}] });
|
|
853
|
+
|
|
854
|
+
class SchemaTreeComponent {
|
|
855
|
+
schema;
|
|
856
|
+
side = 'source';
|
|
857
|
+
mappings = [];
|
|
858
|
+
defaultValues = [];
|
|
859
|
+
fieldDragStart = new EventEmitter();
|
|
860
|
+
fieldDragEnd = new EventEmitter();
|
|
861
|
+
fieldDrop = new EventEmitter();
|
|
862
|
+
fieldPositionsChanged = new EventEmitter();
|
|
863
|
+
fieldDefaultValueClick = new EventEmitter();
|
|
864
|
+
schemaFieldsContainer;
|
|
865
|
+
fieldItems;
|
|
866
|
+
mappingService = inject(MappingService);
|
|
867
|
+
resizeObserver;
|
|
868
|
+
scrollHandler = () => this.onScroll();
|
|
869
|
+
ngAfterViewInit() {
|
|
870
|
+
this.emitFieldPositions();
|
|
871
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
872
|
+
this.emitFieldPositions();
|
|
873
|
+
});
|
|
874
|
+
this.fieldItems.changes.subscribe(() => {
|
|
875
|
+
this.emitFieldPositions();
|
|
876
|
+
});
|
|
877
|
+
// Add scroll listener to update connector positions
|
|
878
|
+
if (this.schemaFieldsContainer?.nativeElement) {
|
|
879
|
+
this.schemaFieldsContainer.nativeElement.addEventListener('scroll', this.scrollHandler, { passive: true });
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
ngOnDestroy() {
|
|
883
|
+
if (this.resizeObserver) {
|
|
884
|
+
this.resizeObserver.disconnect();
|
|
885
|
+
}
|
|
886
|
+
if (this.schemaFieldsContainer?.nativeElement) {
|
|
887
|
+
this.schemaFieldsContainer.nativeElement.removeEventListener('scroll', this.scrollHandler);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
onScroll() {
|
|
891
|
+
this.emitFieldPositions();
|
|
892
|
+
}
|
|
893
|
+
emitFieldPositions() {
|
|
894
|
+
setTimeout(() => {
|
|
895
|
+
const positions = new Map();
|
|
896
|
+
this.fieldItems.forEach((item) => {
|
|
897
|
+
const fieldId = item.nativeElement.getAttribute('data-field-id');
|
|
898
|
+
if (fieldId) {
|
|
899
|
+
positions.set(fieldId, item.nativeElement.getBoundingClientRect());
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
this.fieldPositionsChanged.emit(positions);
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
toggleExpand(field, event) {
|
|
906
|
+
event.stopPropagation();
|
|
907
|
+
field.expanded = !field.expanded;
|
|
908
|
+
setTimeout(() => this.emitFieldPositions(), 50);
|
|
909
|
+
}
|
|
910
|
+
onDragStart(event, field) {
|
|
911
|
+
if (this.side !== 'source')
|
|
912
|
+
return;
|
|
913
|
+
const element = event.currentTarget;
|
|
914
|
+
const rect = element.getBoundingClientRect();
|
|
915
|
+
this.fieldDragStart.emit({ field, element, rect });
|
|
916
|
+
}
|
|
917
|
+
onDragOver(event) {
|
|
918
|
+
if (this.side === 'target') {
|
|
919
|
+
event.preventDefault();
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
onDrop(event, field) {
|
|
923
|
+
if (this.side !== 'target')
|
|
924
|
+
return;
|
|
925
|
+
const element = event.currentTarget;
|
|
926
|
+
const rect = element.getBoundingClientRect();
|
|
927
|
+
this.fieldDrop.emit({ field, element, rect });
|
|
928
|
+
}
|
|
929
|
+
getTypeIcon(type) {
|
|
930
|
+
const icons = {
|
|
931
|
+
string: 'text_fields',
|
|
932
|
+
number: 'pin',
|
|
933
|
+
boolean: 'toggle_on',
|
|
934
|
+
object: 'data_object',
|
|
935
|
+
array: 'data_array',
|
|
936
|
+
date: 'calendar_today',
|
|
937
|
+
};
|
|
938
|
+
return icons[type] || 'help_outline';
|
|
939
|
+
}
|
|
940
|
+
isFieldMapped(field) {
|
|
941
|
+
if (this.side === 'source') {
|
|
942
|
+
return this.mappings.some((m) => m.sourceFields.some((sf) => sf.id === field.id));
|
|
943
|
+
}
|
|
944
|
+
else {
|
|
945
|
+
return this.mappings.some((m) => m.targetField.id === field.id);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
getFieldMappingCount(field) {
|
|
949
|
+
if (this.side === 'source') {
|
|
950
|
+
return this.mappings.filter((m) => m.sourceFields.some((sf) => sf.id === field.id)).length;
|
|
951
|
+
}
|
|
952
|
+
else {
|
|
953
|
+
const mapping = this.mappings.find((m) => m.targetField.id === field.id);
|
|
954
|
+
return mapping ? mapping.sourceFields.length : 0;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
hasDefaultValue(field) {
|
|
958
|
+
return this.defaultValues.some(d => d.targetField.id === field.id);
|
|
959
|
+
}
|
|
960
|
+
getDefaultValueDisplay(field) {
|
|
961
|
+
const defaultValue = this.defaultValues.find(d => d.targetField.id === field.id);
|
|
962
|
+
if (!defaultValue || defaultValue.value === null)
|
|
963
|
+
return '';
|
|
964
|
+
if (defaultValue.valueType === 'date' && defaultValue.value) {
|
|
965
|
+
return new Date(defaultValue.value).toLocaleDateString();
|
|
966
|
+
}
|
|
967
|
+
return String(defaultValue.value);
|
|
968
|
+
}
|
|
969
|
+
onFieldClick(event, field) {
|
|
970
|
+
// Only handle clicks on target fields that are leaf nodes (no children) or have specific types
|
|
971
|
+
if (this.side !== 'target')
|
|
972
|
+
return;
|
|
973
|
+
if (field.type === 'object' || field.type === 'array')
|
|
974
|
+
return;
|
|
975
|
+
// Don't trigger if the field is already mapped (unless it has a default value)
|
|
976
|
+
if (this.isFieldMapped(field) && !this.hasDefaultValue(field))
|
|
977
|
+
return;
|
|
978
|
+
// Allow clicking on unmapped fields OR fields with default values (to edit them)
|
|
979
|
+
if (!this.isFieldMapped(field) || this.hasDefaultValue(field)) {
|
|
980
|
+
event.stopPropagation();
|
|
981
|
+
const element = event.currentTarget;
|
|
982
|
+
const rect = element.getBoundingClientRect();
|
|
983
|
+
this.fieldDefaultValueClick.emit({ field, element, rect });
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
trackByFieldId(index, field) {
|
|
987
|
+
return field.id;
|
|
988
|
+
}
|
|
989
|
+
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"] }] });
|
|
991
|
+
}
|
|
992
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SchemaTreeComponent, decorators: [{
|
|
993
|
+
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 #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"] }]
|
|
995
|
+
}], propDecorators: { schema: [{
|
|
996
|
+
type: Input
|
|
997
|
+
}], side: [{
|
|
998
|
+
type: Input
|
|
999
|
+
}], mappings: [{
|
|
1000
|
+
type: Input
|
|
1001
|
+
}], defaultValues: [{
|
|
1002
|
+
type: Input
|
|
1003
|
+
}], fieldDragStart: [{
|
|
1004
|
+
type: Output
|
|
1005
|
+
}], fieldDragEnd: [{
|
|
1006
|
+
type: Output
|
|
1007
|
+
}], fieldDrop: [{
|
|
1008
|
+
type: Output
|
|
1009
|
+
}], fieldPositionsChanged: [{
|
|
1010
|
+
type: Output
|
|
1011
|
+
}], fieldDefaultValueClick: [{
|
|
1012
|
+
type: Output
|
|
1013
|
+
}], schemaFieldsContainer: [{
|
|
1014
|
+
type: ViewChild,
|
|
1015
|
+
args: ['schemaFields']
|
|
1016
|
+
}], fieldItems: [{
|
|
1017
|
+
type: ViewChildren,
|
|
1018
|
+
args: ['fieldItem']
|
|
1019
|
+
}] } });
|
|
1020
|
+
|
|
1021
|
+
class TransformationPopoverComponent {
|
|
1022
|
+
mapping;
|
|
1023
|
+
position = { x: 0, y: 0 };
|
|
1024
|
+
sampleData = {};
|
|
1025
|
+
save = new EventEmitter();
|
|
1026
|
+
delete = new EventEmitter();
|
|
1027
|
+
close = new EventEmitter();
|
|
1028
|
+
transformationService = inject(TransformationService);
|
|
1029
|
+
transformationType = 'direct';
|
|
1030
|
+
config = { type: 'direct' };
|
|
1031
|
+
preview = '';
|
|
1032
|
+
availableTransformations = this.transformationService.getAvailableTransformations();
|
|
1033
|
+
ngOnInit() {
|
|
1034
|
+
this.initFromMapping();
|
|
1035
|
+
}
|
|
1036
|
+
ngOnChanges(changes) {
|
|
1037
|
+
if (changes['mapping'] || changes['sampleData']) {
|
|
1038
|
+
this.initFromMapping();
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
initFromMapping() {
|
|
1042
|
+
if (this.mapping) {
|
|
1043
|
+
this.config = { ...this.mapping.transformation };
|
|
1044
|
+
this.transformationType = this.config.type;
|
|
1045
|
+
this.updatePreview();
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
onTypeChange() {
|
|
1049
|
+
this.config = {
|
|
1050
|
+
...this.config,
|
|
1051
|
+
type: this.transformationType,
|
|
1052
|
+
};
|
|
1053
|
+
// Set defaults based on type
|
|
1054
|
+
switch (this.transformationType) {
|
|
1055
|
+
case 'concat':
|
|
1056
|
+
this.config.separator = this.config.separator ?? ' ';
|
|
1057
|
+
this.config.template = this.config.template ?? this.getDefaultTemplate();
|
|
1058
|
+
break;
|
|
1059
|
+
case 'substring':
|
|
1060
|
+
this.config.startIndex = this.config.startIndex ?? 0;
|
|
1061
|
+
this.config.endIndex = this.config.endIndex ?? 10;
|
|
1062
|
+
break;
|
|
1063
|
+
case 'replace':
|
|
1064
|
+
this.config.searchValue = this.config.searchValue ?? '';
|
|
1065
|
+
this.config.replaceValue = this.config.replaceValue ?? '';
|
|
1066
|
+
break;
|
|
1067
|
+
case 'dateFormat':
|
|
1068
|
+
this.config.outputFormat = this.config.outputFormat ?? 'YYYY-MM-DD';
|
|
1069
|
+
break;
|
|
1070
|
+
case 'numberFormat':
|
|
1071
|
+
this.config.decimalPlaces = this.config.decimalPlaces ?? 2;
|
|
1072
|
+
break;
|
|
1073
|
+
}
|
|
1074
|
+
this.updatePreview();
|
|
1075
|
+
}
|
|
1076
|
+
getDefaultTemplate() {
|
|
1077
|
+
return this.mapping.sourceFields.map((f) => `{${f.name}}`).join(' ');
|
|
1078
|
+
}
|
|
1079
|
+
updatePreview() {
|
|
1080
|
+
if (!this.mapping || !this.sampleData) {
|
|
1081
|
+
this.preview = '';
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
this.preview = this.transformationService.applyTransformation(this.sampleData, this.mapping.sourceFields, this.config);
|
|
1085
|
+
}
|
|
1086
|
+
onConfigChange() {
|
|
1087
|
+
this.updatePreview();
|
|
1088
|
+
}
|
|
1089
|
+
onSave() {
|
|
1090
|
+
this.save.emit(this.config);
|
|
1091
|
+
}
|
|
1092
|
+
onDelete() {
|
|
1093
|
+
this.delete.emit();
|
|
1094
|
+
}
|
|
1095
|
+
onClose() {
|
|
1096
|
+
this.close.emit();
|
|
1097
|
+
}
|
|
1098
|
+
getSourceFieldNames() {
|
|
1099
|
+
return this.mapping.sourceFields.map((f) => f.name).join(', ');
|
|
1100
|
+
}
|
|
1101
|
+
getPopoverStyle() {
|
|
1102
|
+
return {
|
|
1103
|
+
left: `${this.position.x}px`,
|
|
1104
|
+
top: `${this.position.y}px`,
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
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"] }] });
|
|
1109
|
+
}
|
|
1110
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: TransformationPopoverComponent, decorators: [{
|
|
1111
|
+
type: Component,
|
|
1112
|
+
args: [{ selector: 'transformation-popover', standalone: true, imports: [
|
|
1113
|
+
CommonModule,
|
|
1114
|
+
FormsModule,
|
|
1115
|
+
MatIconModule,
|
|
1116
|
+
MatButtonModule,
|
|
1117
|
+
MatSelectModule,
|
|
1118
|
+
MatInputModule,
|
|
1119
|
+
MatFormFieldModule,
|
|
1120
|
+
MatTooltipModule,
|
|
1121
|
+
], 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"] }]
|
|
1122
|
+
}], propDecorators: { mapping: [{
|
|
1123
|
+
type: Input
|
|
1124
|
+
}], position: [{
|
|
1125
|
+
type: Input
|
|
1126
|
+
}], sampleData: [{
|
|
1127
|
+
type: Input
|
|
1128
|
+
}], save: [{
|
|
1129
|
+
type: Output
|
|
1130
|
+
}], delete: [{
|
|
1131
|
+
type: Output
|
|
1132
|
+
}], close: [{
|
|
1133
|
+
type: Output
|
|
1134
|
+
}] } });
|
|
1135
|
+
|
|
1136
|
+
class ArrayFilterModalComponent {
|
|
1137
|
+
arrayMapping;
|
|
1138
|
+
save = new EventEmitter();
|
|
1139
|
+
close = new EventEmitter();
|
|
1140
|
+
filterEnabled = signal(false, ...(ngDevMode ? [{ debugName: "filterEnabled" }] : []));
|
|
1141
|
+
rootGroup = signal(this.createEmptyGroup(), ...(ngDevMode ? [{ debugName: "rootGroup" }] : []));
|
|
1142
|
+
// Available fields from the source array's children
|
|
1143
|
+
availableFields = computed(() => {
|
|
1144
|
+
const fields = [];
|
|
1145
|
+
this.collectFields(this.arrayMapping.sourceArray.children || [], '', fields);
|
|
1146
|
+
return fields;
|
|
1147
|
+
}, ...(ngDevMode ? [{ debugName: "availableFields" }] : []));
|
|
1148
|
+
// Operators by type
|
|
1149
|
+
stringOperators = [
|
|
1150
|
+
{ value: 'equals', label: 'equals', needsValue: true },
|
|
1151
|
+
{ value: 'notEquals', label: 'not equals', needsValue: true },
|
|
1152
|
+
{ value: 'contains', label: 'contains', needsValue: true },
|
|
1153
|
+
{ value: 'notContains', label: 'does not contain', needsValue: true },
|
|
1154
|
+
{ value: 'startsWith', label: 'starts with', needsValue: true },
|
|
1155
|
+
{ value: 'endsWith', label: 'ends with', needsValue: true },
|
|
1156
|
+
{ value: 'isEmpty', label: 'is empty', needsValue: false },
|
|
1157
|
+
{ value: 'isNotEmpty', label: 'is not empty', needsValue: false },
|
|
1158
|
+
];
|
|
1159
|
+
numberOperators = [
|
|
1160
|
+
{ value: 'equals', label: 'equals', needsValue: true },
|
|
1161
|
+
{ value: 'notEquals', label: 'not equals', needsValue: true },
|
|
1162
|
+
{ value: 'greaterThan', label: 'greater than', needsValue: true },
|
|
1163
|
+
{ value: 'lessThan', label: 'less than', needsValue: true },
|
|
1164
|
+
{ value: 'greaterThanOrEqual', label: 'greater or equal', needsValue: true },
|
|
1165
|
+
{ value: 'lessThanOrEqual', label: 'less or equal', needsValue: true },
|
|
1166
|
+
];
|
|
1167
|
+
booleanOperators = [
|
|
1168
|
+
{ value: 'isTrue', label: 'is true', needsValue: false },
|
|
1169
|
+
{ value: 'isFalse', label: 'is false', needsValue: false },
|
|
1170
|
+
];
|
|
1171
|
+
ngOnInit() {
|
|
1172
|
+
// Initialize from existing filter config
|
|
1173
|
+
if (this.arrayMapping.filter?.enabled && this.arrayMapping.filter.root) {
|
|
1174
|
+
this.filterEnabled.set(true);
|
|
1175
|
+
this.rootGroup.set(this.cloneGroup(this.arrayMapping.filter.root));
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
createEmptyGroup() {
|
|
1179
|
+
return {
|
|
1180
|
+
id: this.generateId(),
|
|
1181
|
+
type: 'group',
|
|
1182
|
+
logic: 'and',
|
|
1183
|
+
children: [],
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
generateId() {
|
|
1187
|
+
return `filter-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
1188
|
+
}
|
|
1189
|
+
cloneGroup(group) {
|
|
1190
|
+
return {
|
|
1191
|
+
...group,
|
|
1192
|
+
children: group.children.map((child) => child.type === 'group' ? this.cloneGroup(child) : { ...child }),
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
collectFields(fields, prefix, result) {
|
|
1196
|
+
for (const field of fields) {
|
|
1197
|
+
const path = prefix ? `${prefix}.${field.name}` : field.name;
|
|
1198
|
+
if (field.type !== 'object' && field.type !== 'array') {
|
|
1199
|
+
result.push({ path, name: field.name, type: field.type });
|
|
1200
|
+
}
|
|
1201
|
+
if (field.children) {
|
|
1202
|
+
this.collectFields(field.children, path, result);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
getOperatorsForField(fieldPath) {
|
|
1207
|
+
const field = this.availableFields().find((f) => f.path === fieldPath);
|
|
1208
|
+
if (!field)
|
|
1209
|
+
return this.stringOperators;
|
|
1210
|
+
switch (field.type) {
|
|
1211
|
+
case 'number':
|
|
1212
|
+
return this.numberOperators;
|
|
1213
|
+
case 'boolean':
|
|
1214
|
+
return this.booleanOperators;
|
|
1215
|
+
default:
|
|
1216
|
+
return this.stringOperators;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
getFieldType(fieldPath) {
|
|
1220
|
+
const field = this.availableFields().find((f) => f.path === fieldPath);
|
|
1221
|
+
return field?.type || 'string';
|
|
1222
|
+
}
|
|
1223
|
+
operatorNeedsValue(operator) {
|
|
1224
|
+
const allOperators = [...this.stringOperators, ...this.numberOperators, ...this.booleanOperators];
|
|
1225
|
+
const op = allOperators.find((o) => o.value === operator);
|
|
1226
|
+
return op?.needsValue ?? true;
|
|
1227
|
+
}
|
|
1228
|
+
isCondition(item) {
|
|
1229
|
+
return item.type === 'condition';
|
|
1230
|
+
}
|
|
1231
|
+
isGroup(item) {
|
|
1232
|
+
return item.type === 'group';
|
|
1233
|
+
}
|
|
1234
|
+
addCondition(group) {
|
|
1235
|
+
const fields = this.availableFields();
|
|
1236
|
+
const firstField = fields[0];
|
|
1237
|
+
const newCondition = {
|
|
1238
|
+
id: this.generateId(),
|
|
1239
|
+
type: 'condition',
|
|
1240
|
+
field: firstField?.path || '',
|
|
1241
|
+
fieldName: firstField?.name || '',
|
|
1242
|
+
operator: 'equals',
|
|
1243
|
+
value: '',
|
|
1244
|
+
valueType: firstField?.type || 'string',
|
|
1245
|
+
};
|
|
1246
|
+
group.children = [...group.children, newCondition];
|
|
1247
|
+
this.triggerUpdate();
|
|
1248
|
+
}
|
|
1249
|
+
addGroup(parentGroup) {
|
|
1250
|
+
const newGroup = {
|
|
1251
|
+
id: this.generateId(),
|
|
1252
|
+
type: 'group',
|
|
1253
|
+
logic: parentGroup.logic === 'and' ? 'or' : 'and', // Default to opposite logic
|
|
1254
|
+
children: [],
|
|
1255
|
+
};
|
|
1256
|
+
parentGroup.children = [...parentGroup.children, newGroup];
|
|
1257
|
+
this.triggerUpdate();
|
|
1258
|
+
}
|
|
1259
|
+
removeItem(parentGroup, itemId) {
|
|
1260
|
+
parentGroup.children = parentGroup.children.filter((c) => c.id !== itemId);
|
|
1261
|
+
this.triggerUpdate();
|
|
1262
|
+
}
|
|
1263
|
+
onFieldChange(condition, fieldPath) {
|
|
1264
|
+
const field = this.availableFields().find((f) => f.path === fieldPath);
|
|
1265
|
+
if (field) {
|
|
1266
|
+
condition.field = fieldPath;
|
|
1267
|
+
condition.fieldName = field.name;
|
|
1268
|
+
condition.valueType = field.type;
|
|
1269
|
+
// Reset operator to first valid option for new type
|
|
1270
|
+
const operators = this.getOperatorsForField(fieldPath);
|
|
1271
|
+
condition.operator = operators[0].value;
|
|
1272
|
+
condition.value = '';
|
|
1273
|
+
}
|
|
1274
|
+
this.triggerUpdate();
|
|
1275
|
+
}
|
|
1276
|
+
onOperatorChange(condition, operator) {
|
|
1277
|
+
condition.operator = operator;
|
|
1278
|
+
if (!this.operatorNeedsValue(operator)) {
|
|
1279
|
+
condition.value = '';
|
|
1280
|
+
}
|
|
1281
|
+
this.triggerUpdate();
|
|
1282
|
+
}
|
|
1283
|
+
onValueChange(condition, value) {
|
|
1284
|
+
if (condition.valueType === 'number') {
|
|
1285
|
+
condition.value = parseFloat(value) || 0;
|
|
1286
|
+
}
|
|
1287
|
+
else {
|
|
1288
|
+
condition.value = value;
|
|
1289
|
+
}
|
|
1290
|
+
this.triggerUpdate();
|
|
1291
|
+
}
|
|
1292
|
+
onLogicChange(group, logic) {
|
|
1293
|
+
group.logic = logic;
|
|
1294
|
+
this.triggerUpdate();
|
|
1295
|
+
}
|
|
1296
|
+
triggerUpdate() {
|
|
1297
|
+
// Trigger signal update by creating a new reference
|
|
1298
|
+
this.rootGroup.set(this.cloneGroup(this.rootGroup()));
|
|
1299
|
+
}
|
|
1300
|
+
hasConditions(group) {
|
|
1301
|
+
return group.children.length > 0;
|
|
1302
|
+
}
|
|
1303
|
+
countConditions(group) {
|
|
1304
|
+
let count = 0;
|
|
1305
|
+
for (const child of group.children) {
|
|
1306
|
+
if (child.type === 'condition') {
|
|
1307
|
+
count++;
|
|
1308
|
+
}
|
|
1309
|
+
else {
|
|
1310
|
+
count += this.countConditions(child);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
return count;
|
|
1314
|
+
}
|
|
1315
|
+
onSave() {
|
|
1316
|
+
if (!this.filterEnabled()) {
|
|
1317
|
+
this.save.emit(undefined);
|
|
1318
|
+
}
|
|
1319
|
+
else {
|
|
1320
|
+
this.save.emit({
|
|
1321
|
+
enabled: true,
|
|
1322
|
+
root: this.rootGroup(),
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
onClose() {
|
|
1327
|
+
this.close.emit();
|
|
1328
|
+
}
|
|
1329
|
+
onBackdropClick(event) {
|
|
1330
|
+
if (event.target.classList.contains('modal-backdrop')) {
|
|
1331
|
+
this.onClose();
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
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" }] });
|
|
1336
|
+
}
|
|
1337
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ArrayFilterModalComponent, decorators: [{
|
|
1338
|
+
type: Component,
|
|
1339
|
+
args: [{ selector: 'array-filter-modal', standalone: true, imports: [
|
|
1340
|
+
CommonModule,
|
|
1341
|
+
FormsModule,
|
|
1342
|
+
MatButtonModule,
|
|
1343
|
+
MatIconModule,
|
|
1344
|
+
MatSelectModule,
|
|
1345
|
+
MatInputModule,
|
|
1346
|
+
MatFormFieldModule,
|
|
1347
|
+
MatRadioModule,
|
|
1348
|
+
MatSlideToggleModule,
|
|
1349
|
+
MatTooltipModule,
|
|
1350
|
+
MatDividerModule,
|
|
1351
|
+
], 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"] }]
|
|
1352
|
+
}], propDecorators: { arrayMapping: [{
|
|
1353
|
+
type: Input,
|
|
1354
|
+
args: [{ required: true }]
|
|
1355
|
+
}], save: [{
|
|
1356
|
+
type: Output
|
|
1357
|
+
}], close: [{
|
|
1358
|
+
type: Output
|
|
1359
|
+
}] } });
|
|
1360
|
+
|
|
1361
|
+
class ArraySelectorModalComponent {
|
|
1362
|
+
mapping;
|
|
1363
|
+
save = new EventEmitter();
|
|
1364
|
+
close = new EventEmitter();
|
|
1365
|
+
selectionMode = signal('first', ...(ngDevMode ? [{ debugName: "selectionMode" }] : []));
|
|
1366
|
+
conditionGroup = signal(this.createEmptyGroup(), ...(ngDevMode ? [{ debugName: "conditionGroup" }] : []));
|
|
1367
|
+
// Available fields from the source array's children
|
|
1368
|
+
availableFields = computed(() => {
|
|
1369
|
+
const fields = [];
|
|
1370
|
+
this.collectFields(this.mapping.sourceArray.children || [], '', fields);
|
|
1371
|
+
return fields;
|
|
1372
|
+
}, ...(ngDevMode ? [{ debugName: "availableFields" }] : []));
|
|
1373
|
+
// Operators by type
|
|
1374
|
+
stringOperators = [
|
|
1375
|
+
{ value: 'equals', label: 'equals', needsValue: true },
|
|
1376
|
+
{ value: 'notEquals', label: 'not equals', needsValue: true },
|
|
1377
|
+
{ value: 'contains', label: 'contains', needsValue: true },
|
|
1378
|
+
{ value: 'notContains', label: 'does not contain', needsValue: true },
|
|
1379
|
+
{ value: 'startsWith', label: 'starts with', needsValue: true },
|
|
1380
|
+
{ value: 'endsWith', label: 'ends with', needsValue: true },
|
|
1381
|
+
{ value: 'isEmpty', label: 'is empty', needsValue: false },
|
|
1382
|
+
{ value: 'isNotEmpty', label: 'is not empty', needsValue: false },
|
|
1383
|
+
];
|
|
1384
|
+
numberOperators = [
|
|
1385
|
+
{ value: 'equals', label: 'equals', needsValue: true },
|
|
1386
|
+
{ value: 'notEquals', label: 'not equals', needsValue: true },
|
|
1387
|
+
{ value: 'greaterThan', label: 'greater than', needsValue: true },
|
|
1388
|
+
{ value: 'lessThan', label: 'less than', needsValue: true },
|
|
1389
|
+
{ value: 'greaterThanOrEqual', label: 'greater or equal', needsValue: true },
|
|
1390
|
+
{ value: 'lessThanOrEqual', label: 'less or equal', needsValue: true },
|
|
1391
|
+
];
|
|
1392
|
+
booleanOperators = [
|
|
1393
|
+
{ value: 'isTrue', label: 'is true', needsValue: false },
|
|
1394
|
+
{ value: 'isFalse', label: 'is false', needsValue: false },
|
|
1395
|
+
];
|
|
1396
|
+
ngOnInit() {
|
|
1397
|
+
// Initialize from existing selector config
|
|
1398
|
+
if (this.mapping.selector) {
|
|
1399
|
+
this.selectionMode.set(this.mapping.selector.mode);
|
|
1400
|
+
if (this.mapping.selector.condition) {
|
|
1401
|
+
this.conditionGroup.set(this.cloneGroup(this.mapping.selector.condition));
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
createEmptyGroup() {
|
|
1406
|
+
return {
|
|
1407
|
+
id: this.generateId(),
|
|
1408
|
+
type: 'group',
|
|
1409
|
+
logic: 'and',
|
|
1410
|
+
children: [],
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
generateId() {
|
|
1414
|
+
return `filter-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
1415
|
+
}
|
|
1416
|
+
cloneGroup(group) {
|
|
1417
|
+
return {
|
|
1418
|
+
...group,
|
|
1419
|
+
children: group.children.map((child) => child.type === 'group' ? this.cloneGroup(child) : { ...child }),
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
collectFields(fields, prefix, result) {
|
|
1423
|
+
for (const field of fields) {
|
|
1424
|
+
const path = prefix ? `${prefix}.${field.name}` : field.name;
|
|
1425
|
+
if (field.type !== 'object' && field.type !== 'array') {
|
|
1426
|
+
result.push({ path, name: field.name, type: field.type });
|
|
1427
|
+
}
|
|
1428
|
+
if (field.children) {
|
|
1429
|
+
this.collectFields(field.children, path, result);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
getOperatorsForField(fieldPath) {
|
|
1434
|
+
const field = this.availableFields().find((f) => f.path === fieldPath);
|
|
1435
|
+
if (!field)
|
|
1436
|
+
return this.stringOperators;
|
|
1437
|
+
switch (field.type) {
|
|
1438
|
+
case 'number':
|
|
1439
|
+
return this.numberOperators;
|
|
1440
|
+
case 'boolean':
|
|
1441
|
+
return this.booleanOperators;
|
|
1442
|
+
default:
|
|
1443
|
+
return this.stringOperators;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
operatorNeedsValue(operator) {
|
|
1447
|
+
const allOperators = [...this.stringOperators, ...this.numberOperators, ...this.booleanOperators];
|
|
1448
|
+
const op = allOperators.find((o) => o.value === operator);
|
|
1449
|
+
return op?.needsValue ?? true;
|
|
1450
|
+
}
|
|
1451
|
+
isCondition(item) {
|
|
1452
|
+
return item.type === 'condition';
|
|
1453
|
+
}
|
|
1454
|
+
isGroup(item) {
|
|
1455
|
+
return item.type === 'group';
|
|
1456
|
+
}
|
|
1457
|
+
addCondition(group) {
|
|
1458
|
+
const fields = this.availableFields();
|
|
1459
|
+
const firstField = fields[0];
|
|
1460
|
+
const newCondition = {
|
|
1461
|
+
id: this.generateId(),
|
|
1462
|
+
type: 'condition',
|
|
1463
|
+
field: firstField?.path || '',
|
|
1464
|
+
fieldName: firstField?.name || '',
|
|
1465
|
+
operator: 'equals',
|
|
1466
|
+
value: '',
|
|
1467
|
+
valueType: firstField?.type || 'string',
|
|
1468
|
+
};
|
|
1469
|
+
group.children = [...group.children, newCondition];
|
|
1470
|
+
this.triggerUpdate();
|
|
1471
|
+
}
|
|
1472
|
+
addGroup(parentGroup) {
|
|
1473
|
+
const newGroup = {
|
|
1474
|
+
id: this.generateId(),
|
|
1475
|
+
type: 'group',
|
|
1476
|
+
logic: parentGroup.logic === 'and' ? 'or' : 'and',
|
|
1477
|
+
children: [],
|
|
1478
|
+
};
|
|
1479
|
+
parentGroup.children = [...parentGroup.children, newGroup];
|
|
1480
|
+
this.triggerUpdate();
|
|
1481
|
+
}
|
|
1482
|
+
removeItem(parentGroup, itemId) {
|
|
1483
|
+
parentGroup.children = parentGroup.children.filter((c) => c.id !== itemId);
|
|
1484
|
+
this.triggerUpdate();
|
|
1485
|
+
}
|
|
1486
|
+
onFieldChange(condition, fieldPath) {
|
|
1487
|
+
const field = this.availableFields().find((f) => f.path === fieldPath);
|
|
1488
|
+
if (field) {
|
|
1489
|
+
condition.field = fieldPath;
|
|
1490
|
+
condition.fieldName = field.name;
|
|
1491
|
+
condition.valueType = field.type;
|
|
1492
|
+
const operators = this.getOperatorsForField(fieldPath);
|
|
1493
|
+
condition.operator = operators[0].value;
|
|
1494
|
+
condition.value = '';
|
|
1495
|
+
}
|
|
1496
|
+
this.triggerUpdate();
|
|
1497
|
+
}
|
|
1498
|
+
onOperatorChange(condition, operator) {
|
|
1499
|
+
condition.operator = operator;
|
|
1500
|
+
if (!this.operatorNeedsValue(operator)) {
|
|
1501
|
+
condition.value = '';
|
|
1502
|
+
}
|
|
1503
|
+
this.triggerUpdate();
|
|
1504
|
+
}
|
|
1505
|
+
onValueChange(condition, value) {
|
|
1506
|
+
if (condition.valueType === 'number') {
|
|
1507
|
+
condition.value = parseFloat(value) || 0;
|
|
1508
|
+
}
|
|
1509
|
+
else {
|
|
1510
|
+
condition.value = value;
|
|
1511
|
+
}
|
|
1512
|
+
this.triggerUpdate();
|
|
1513
|
+
}
|
|
1514
|
+
onLogicChange(group, logic) {
|
|
1515
|
+
group.logic = logic;
|
|
1516
|
+
this.triggerUpdate();
|
|
1517
|
+
}
|
|
1518
|
+
triggerUpdate() {
|
|
1519
|
+
this.conditionGroup.set(this.cloneGroup(this.conditionGroup()));
|
|
1520
|
+
}
|
|
1521
|
+
onSave() {
|
|
1522
|
+
const config = {
|
|
1523
|
+
mode: this.selectionMode(),
|
|
1524
|
+
};
|
|
1525
|
+
if (this.selectionMode() === 'condition') {
|
|
1526
|
+
config.condition = this.conditionGroup();
|
|
1527
|
+
}
|
|
1528
|
+
this.save.emit(config);
|
|
1529
|
+
}
|
|
1530
|
+
onClose() {
|
|
1531
|
+
this.close.emit();
|
|
1532
|
+
}
|
|
1533
|
+
onBackdropClick(event) {
|
|
1534
|
+
if (event.target.classList.contains('modal-backdrop')) {
|
|
1535
|
+
this.onClose();
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
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" }] });
|
|
1540
|
+
}
|
|
1541
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ArraySelectorModalComponent, decorators: [{
|
|
1542
|
+
type: Component,
|
|
1543
|
+
args: [{ selector: 'array-selector-modal', standalone: true, imports: [
|
|
1544
|
+
CommonModule,
|
|
1545
|
+
FormsModule,
|
|
1546
|
+
MatButtonModule,
|
|
1547
|
+
MatIconModule,
|
|
1548
|
+
MatSelectModule,
|
|
1549
|
+
MatInputModule,
|
|
1550
|
+
MatFormFieldModule,
|
|
1551
|
+
MatRadioModule,
|
|
1552
|
+
MatSlideToggleModule,
|
|
1553
|
+
MatTooltipModule,
|
|
1554
|
+
MatDividerModule,
|
|
1555
|
+
], 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"] }]
|
|
1556
|
+
}], propDecorators: { mapping: [{
|
|
1557
|
+
type: Input,
|
|
1558
|
+
args: [{ required: true }]
|
|
1559
|
+
}], save: [{
|
|
1560
|
+
type: Output
|
|
1561
|
+
}], close: [{
|
|
1562
|
+
type: Output
|
|
1563
|
+
}] } });
|
|
1564
|
+
|
|
1565
|
+
class DefaultValuePopoverComponent {
|
|
1566
|
+
field;
|
|
1567
|
+
existingValue;
|
|
1568
|
+
position;
|
|
1569
|
+
save = new EventEmitter();
|
|
1570
|
+
delete = new EventEmitter();
|
|
1571
|
+
close = new EventEmitter();
|
|
1572
|
+
stringValue = '';
|
|
1573
|
+
numberValue = null;
|
|
1574
|
+
booleanValue = false;
|
|
1575
|
+
dateValue = null;
|
|
1576
|
+
ngOnInit() {
|
|
1577
|
+
if (this.existingValue) {
|
|
1578
|
+
switch (this.existingValue.valueType) {
|
|
1579
|
+
case 'string':
|
|
1580
|
+
this.stringValue = this.existingValue.value || '';
|
|
1581
|
+
break;
|
|
1582
|
+
case 'number':
|
|
1583
|
+
this.numberValue = this.existingValue.value;
|
|
1584
|
+
break;
|
|
1585
|
+
case 'boolean':
|
|
1586
|
+
this.booleanValue = this.existingValue.value;
|
|
1587
|
+
break;
|
|
1588
|
+
case 'date':
|
|
1589
|
+
this.dateValue = this.existingValue.value ? new Date(this.existingValue.value) : null;
|
|
1590
|
+
break;
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
get fieldType() {
|
|
1595
|
+
if (this.field.type === 'date' || this.field.name.toLowerCase().includes('date')) {
|
|
1596
|
+
return 'date';
|
|
1597
|
+
}
|
|
1598
|
+
return this.field.type;
|
|
1599
|
+
}
|
|
1600
|
+
onSave() {
|
|
1601
|
+
let value;
|
|
1602
|
+
switch (this.fieldType) {
|
|
1603
|
+
case 'number':
|
|
1604
|
+
value = this.numberValue;
|
|
1605
|
+
break;
|
|
1606
|
+
case 'boolean':
|
|
1607
|
+
value = this.booleanValue;
|
|
1608
|
+
break;
|
|
1609
|
+
case 'date':
|
|
1610
|
+
value = this.dateValue;
|
|
1611
|
+
break;
|
|
1612
|
+
default:
|
|
1613
|
+
value = this.stringValue || null;
|
|
1614
|
+
}
|
|
1615
|
+
this.save.emit(value);
|
|
1616
|
+
}
|
|
1617
|
+
onDelete() {
|
|
1618
|
+
this.delete.emit();
|
|
1619
|
+
}
|
|
1620
|
+
onClose() {
|
|
1621
|
+
this.close.emit();
|
|
1622
|
+
}
|
|
1623
|
+
onBackdropClick(event) {
|
|
1624
|
+
if (event.target.classList.contains('popover-backdrop')) {
|
|
1625
|
+
this.onClose();
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
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$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: "directive", type: i5.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.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
|
+
}
|
|
1631
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: DefaultValuePopoverComponent, decorators: [{
|
|
1632
|
+
type: Component,
|
|
1633
|
+
args: [{ selector: 'default-value-popover', standalone: true, imports: [
|
|
1634
|
+
CommonModule,
|
|
1635
|
+
FormsModule,
|
|
1636
|
+
MatButtonModule,
|
|
1637
|
+
MatIconModule,
|
|
1638
|
+
MatInputModule,
|
|
1639
|
+
MatFormFieldModule,
|
|
1640
|
+
MatDatepickerModule,
|
|
1641
|
+
MatNativeDateModule,
|
|
1642
|
+
MatSelectModule,
|
|
1643
|
+
MatSlideToggleModule,
|
|
1644
|
+
], 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"] }]
|
|
1645
|
+
}], propDecorators: { field: [{
|
|
1646
|
+
type: Input
|
|
1647
|
+
}], existingValue: [{
|
|
1648
|
+
type: Input
|
|
1649
|
+
}], position: [{
|
|
1650
|
+
type: Input
|
|
1651
|
+
}], save: [{
|
|
1652
|
+
type: Output
|
|
1653
|
+
}], delete: [{
|
|
1654
|
+
type: Output
|
|
1655
|
+
}], close: [{
|
|
1656
|
+
type: Output
|
|
1657
|
+
}] } });
|
|
1658
|
+
|
|
1659
|
+
class DataMapperComponent {
|
|
1660
|
+
sourceSchema;
|
|
1661
|
+
targetSchema;
|
|
1662
|
+
sampleData = {};
|
|
1663
|
+
mappingsChange = new EventEmitter();
|
|
1664
|
+
svgContainer;
|
|
1665
|
+
svgElement;
|
|
1666
|
+
mappingService = inject(MappingService);
|
|
1667
|
+
svgConnectorService = inject(SvgConnectorService);
|
|
1668
|
+
transformationService = inject(TransformationService);
|
|
1669
|
+
// Field positions from both trees
|
|
1670
|
+
sourcePositions = new Map();
|
|
1671
|
+
targetPositions = new Map();
|
|
1672
|
+
// Visual state
|
|
1673
|
+
connections = signal([], ...(ngDevMode ? [{ debugName: "connections" }] : []));
|
|
1674
|
+
dragPath = signal(null, ...(ngDevMode ? [{ debugName: "dragPath" }] : []));
|
|
1675
|
+
selectedMappingId = signal(null, ...(ngDevMode ? [{ debugName: "selectedMappingId" }] : []));
|
|
1676
|
+
popoverPosition = signal(null, ...(ngDevMode ? [{ debugName: "popoverPosition" }] : []));
|
|
1677
|
+
// Array filter modal state
|
|
1678
|
+
showArrayFilterModal = signal(false, ...(ngDevMode ? [{ debugName: "showArrayFilterModal" }] : []));
|
|
1679
|
+
selectedArrayMapping = signal(null, ...(ngDevMode ? [{ debugName: "selectedArrayMapping" }] : []));
|
|
1680
|
+
// Array selector modal state (for array-to-object)
|
|
1681
|
+
showArraySelectorModal = signal(false, ...(ngDevMode ? [{ debugName: "showArraySelectorModal" }] : []));
|
|
1682
|
+
selectedArrayToObjectMapping = signal(null, ...(ngDevMode ? [{ debugName: "selectedArrayToObjectMapping" }] : []));
|
|
1683
|
+
// Default value popover state
|
|
1684
|
+
showDefaultValuePopover = signal(false, ...(ngDevMode ? [{ debugName: "showDefaultValuePopover" }] : []));
|
|
1685
|
+
selectedDefaultValueField = signal(null, ...(ngDevMode ? [{ debugName: "selectedDefaultValueField" }] : []));
|
|
1686
|
+
defaultValuePopoverPosition = signal(null, ...(ngDevMode ? [{ debugName: "defaultValuePopoverPosition" }] : []));
|
|
1687
|
+
// Computed values
|
|
1688
|
+
mappings = computed(() => this.mappingService.allMappings(), ...(ngDevMode ? [{ debugName: "mappings" }] : []));
|
|
1689
|
+
arrayMappings = computed(() => this.mappingService.allArrayMappings(), ...(ngDevMode ? [{ debugName: "arrayMappings" }] : []));
|
|
1690
|
+
defaultValues = computed(() => this.mappingService.allDefaultValues(), ...(ngDevMode ? [{ debugName: "defaultValues" }] : []));
|
|
1691
|
+
selectedMapping = computed(() => this.mappingService.selectedMapping(), ...(ngDevMode ? [{ debugName: "selectedMapping" }] : []));
|
|
1692
|
+
showPopover = computed(() => this.selectedMappingId() !== null && this.popoverPosition() !== null && !this.showArrayFilterModal(), ...(ngDevMode ? [{ debugName: "showPopover" }] : []));
|
|
1693
|
+
isDragging = false;
|
|
1694
|
+
dragSourceField = null;
|
|
1695
|
+
dragStartPoint = null;
|
|
1696
|
+
resizeObserver;
|
|
1697
|
+
ngAfterViewInit() {
|
|
1698
|
+
this.setupResizeObserver();
|
|
1699
|
+
}
|
|
1700
|
+
ngOnDestroy() {
|
|
1701
|
+
if (this.resizeObserver) {
|
|
1702
|
+
this.resizeObserver.disconnect();
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
setupResizeObserver() {
|
|
1706
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
1707
|
+
this.updateConnections();
|
|
1708
|
+
});
|
|
1709
|
+
if (this.svgContainer?.nativeElement) {
|
|
1710
|
+
this.resizeObserver.observe(this.svgContainer.nativeElement);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
onSourcePositionsChanged(positions) {
|
|
1714
|
+
this.sourcePositions = positions;
|
|
1715
|
+
this.updateConnections();
|
|
1716
|
+
}
|
|
1717
|
+
onTargetPositionsChanged(positions) {
|
|
1718
|
+
this.targetPositions = positions;
|
|
1719
|
+
this.updateConnections();
|
|
1720
|
+
}
|
|
1721
|
+
onFieldDragStart(event) {
|
|
1722
|
+
if (!this.svgContainer?.nativeElement)
|
|
1723
|
+
return;
|
|
1724
|
+
const containerRect = this.svgContainer.nativeElement.getBoundingClientRect();
|
|
1725
|
+
const startPoint = this.svgConnectorService.calculateConnectionPoint(event.rect, 'source', containerRect);
|
|
1726
|
+
this.isDragging = true;
|
|
1727
|
+
this.dragSourceField = event.field;
|
|
1728
|
+
this.dragStartPoint = startPoint;
|
|
1729
|
+
document.body.style.cursor = 'grabbing';
|
|
1730
|
+
}
|
|
1731
|
+
onMouseMove(event) {
|
|
1732
|
+
if (!this.isDragging || !this.dragStartPoint || !this.svgContainer?.nativeElement)
|
|
1733
|
+
return;
|
|
1734
|
+
const containerRect = this.svgContainer.nativeElement.getBoundingClientRect();
|
|
1735
|
+
const currentPoint = {
|
|
1736
|
+
x: event.clientX - containerRect.left,
|
|
1737
|
+
y: event.clientY - containerRect.top,
|
|
1738
|
+
};
|
|
1739
|
+
const path = this.svgConnectorService.createDragPath(this.dragStartPoint, currentPoint);
|
|
1740
|
+
this.dragPath.set(path);
|
|
1741
|
+
}
|
|
1742
|
+
onMouseUp(event) {
|
|
1743
|
+
if (this.isDragging) {
|
|
1744
|
+
this.dragPath.set(null);
|
|
1745
|
+
this.isDragging = false;
|
|
1746
|
+
this.dragSourceField = null;
|
|
1747
|
+
this.dragStartPoint = null;
|
|
1748
|
+
document.body.style.cursor = '';
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
onFieldDrop(event) {
|
|
1752
|
+
if (!this.dragSourceField)
|
|
1753
|
+
return;
|
|
1754
|
+
// Create mapping
|
|
1755
|
+
const mapping = this.mappingService.createMapping([this.dragSourceField], event.field);
|
|
1756
|
+
this.mappingsChange.emit(this.mappingService.allMappings());
|
|
1757
|
+
this.updateConnections();
|
|
1758
|
+
// Reset drag state
|
|
1759
|
+
this.isDragging = false;
|
|
1760
|
+
this.dragSourceField = null;
|
|
1761
|
+
this.dragStartPoint = null;
|
|
1762
|
+
this.dragPath.set(null);
|
|
1763
|
+
document.body.style.cursor = '';
|
|
1764
|
+
}
|
|
1765
|
+
onConnectionClick(connection, event) {
|
|
1766
|
+
event.stopPropagation();
|
|
1767
|
+
this.selectedMappingId.set(connection.mappingId);
|
|
1768
|
+
this.mappingService.selectMapping(connection.mappingId);
|
|
1769
|
+
// Position popover at click location
|
|
1770
|
+
this.popoverPosition.set({
|
|
1771
|
+
x: event.clientX,
|
|
1772
|
+
y: event.clientY,
|
|
1773
|
+
});
|
|
1774
|
+
this.updateConnections();
|
|
1775
|
+
}
|
|
1776
|
+
onTransformationNodeClick(connection, event) {
|
|
1777
|
+
event.stopPropagation();
|
|
1778
|
+
// If it's an array mapping, show the filter modal
|
|
1779
|
+
if (connection.isArrayMapping) {
|
|
1780
|
+
const arrayMapping = this.mappingService.getArrayMapping(connection.mappingId);
|
|
1781
|
+
if (arrayMapping) {
|
|
1782
|
+
this.selectedArrayMapping.set(arrayMapping);
|
|
1783
|
+
this.showArrayFilterModal.set(true);
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
// If it's an array-to-object mapping, show the selector modal
|
|
1788
|
+
if (connection.isArrayToObjectMapping) {
|
|
1789
|
+
const arrayToObjectMapping = this.mappingService.getArrayToObjectMapping(connection.mappingId);
|
|
1790
|
+
if (arrayToObjectMapping) {
|
|
1791
|
+
this.selectedArrayToObjectMapping.set(arrayToObjectMapping);
|
|
1792
|
+
this.showArraySelectorModal.set(true);
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
this.onConnectionClick(connection, event);
|
|
1797
|
+
}
|
|
1798
|
+
onArrayFilterSave(filter) {
|
|
1799
|
+
const arrayMapping = this.selectedArrayMapping();
|
|
1800
|
+
if (arrayMapping) {
|
|
1801
|
+
this.mappingService.updateArrayFilter(arrayMapping.id, filter);
|
|
1802
|
+
this.mappingsChange.emit(this.mappingService.allMappings());
|
|
1803
|
+
}
|
|
1804
|
+
this.closeArrayFilterModal();
|
|
1805
|
+
this.updateConnections();
|
|
1806
|
+
}
|
|
1807
|
+
closeArrayFilterModal() {
|
|
1808
|
+
this.showArrayFilterModal.set(false);
|
|
1809
|
+
this.selectedArrayMapping.set(null);
|
|
1810
|
+
}
|
|
1811
|
+
onArraySelectorSave(selector) {
|
|
1812
|
+
const mapping = this.selectedArrayToObjectMapping();
|
|
1813
|
+
if (mapping) {
|
|
1814
|
+
this.mappingService.updateArrayToObjectSelector(mapping.id, selector);
|
|
1815
|
+
this.mappingsChange.emit(this.mappingService.allMappings());
|
|
1816
|
+
}
|
|
1817
|
+
this.closeArraySelectorModal();
|
|
1818
|
+
this.updateConnections();
|
|
1819
|
+
}
|
|
1820
|
+
closeArraySelectorModal() {
|
|
1821
|
+
this.showArraySelectorModal.set(false);
|
|
1822
|
+
this.selectedArrayToObjectMapping.set(null);
|
|
1823
|
+
}
|
|
1824
|
+
// Default value methods
|
|
1825
|
+
onDefaultValueClick(event) {
|
|
1826
|
+
this.selectedDefaultValueField.set(event.field);
|
|
1827
|
+
this.defaultValuePopoverPosition.set({
|
|
1828
|
+
x: event.rect.right,
|
|
1829
|
+
y: event.rect.top + event.rect.height / 2,
|
|
1830
|
+
});
|
|
1831
|
+
this.showDefaultValuePopover.set(true);
|
|
1832
|
+
}
|
|
1833
|
+
onDefaultValueSave(value) {
|
|
1834
|
+
const field = this.selectedDefaultValueField();
|
|
1835
|
+
if (field) {
|
|
1836
|
+
this.mappingService.setDefaultValue(field, value);
|
|
1837
|
+
this.mappingsChange.emit(this.mappingService.allMappings());
|
|
1838
|
+
}
|
|
1839
|
+
this.closeDefaultValuePopover();
|
|
1840
|
+
}
|
|
1841
|
+
onDefaultValueDelete() {
|
|
1842
|
+
const field = this.selectedDefaultValueField();
|
|
1843
|
+
if (field) {
|
|
1844
|
+
this.mappingService.removeDefaultValue(field.id);
|
|
1845
|
+
this.mappingsChange.emit(this.mappingService.allMappings());
|
|
1846
|
+
}
|
|
1847
|
+
this.closeDefaultValuePopover();
|
|
1848
|
+
}
|
|
1849
|
+
closeDefaultValuePopover() {
|
|
1850
|
+
this.showDefaultValuePopover.set(false);
|
|
1851
|
+
this.selectedDefaultValueField.set(null);
|
|
1852
|
+
this.defaultValuePopoverPosition.set(null);
|
|
1853
|
+
}
|
|
1854
|
+
getExistingDefaultValue(fieldId) {
|
|
1855
|
+
return this.mappingService.getDefaultValue(fieldId);
|
|
1856
|
+
}
|
|
1857
|
+
onPopoverSave(config) {
|
|
1858
|
+
const mappingId = this.selectedMappingId();
|
|
1859
|
+
if (mappingId) {
|
|
1860
|
+
this.mappingService.updateTransformation(mappingId, config);
|
|
1861
|
+
this.mappingsChange.emit(this.mappingService.allMappings());
|
|
1862
|
+
}
|
|
1863
|
+
this.closePopover();
|
|
1864
|
+
this.updateConnections();
|
|
1865
|
+
}
|
|
1866
|
+
onPopoverDelete() {
|
|
1867
|
+
const mappingId = this.selectedMappingId();
|
|
1868
|
+
if (mappingId) {
|
|
1869
|
+
this.mappingService.removeMapping(mappingId);
|
|
1870
|
+
this.mappingsChange.emit(this.mappingService.allMappings());
|
|
1871
|
+
}
|
|
1872
|
+
this.closePopover();
|
|
1873
|
+
this.updateConnections();
|
|
1874
|
+
}
|
|
1875
|
+
closePopover() {
|
|
1876
|
+
this.selectedMappingId.set(null);
|
|
1877
|
+
this.popoverPosition.set(null);
|
|
1878
|
+
this.mappingService.selectMapping(null);
|
|
1879
|
+
this.updateConnections();
|
|
1880
|
+
}
|
|
1881
|
+
updateConnections() {
|
|
1882
|
+
if (!this.svgContainer?.nativeElement)
|
|
1883
|
+
return;
|
|
1884
|
+
const containerRect = this.svgContainer.nativeElement.getBoundingClientRect();
|
|
1885
|
+
const mappings = this.mappingService.allMappings();
|
|
1886
|
+
const selectedId = this.selectedMappingId();
|
|
1887
|
+
const newConnections = [];
|
|
1888
|
+
for (const mapping of mappings) {
|
|
1889
|
+
const targetRect = this.targetPositions.get(mapping.targetField.id);
|
|
1890
|
+
if (!targetRect)
|
|
1891
|
+
continue;
|
|
1892
|
+
const targetPoint = this.svgConnectorService.calculateConnectionPoint(targetRect, 'target', containerRect);
|
|
1893
|
+
const sourcePoints = [];
|
|
1894
|
+
for (const sourceField of mapping.sourceFields) {
|
|
1895
|
+
const sourceRect = this.sourcePositions.get(sourceField.id);
|
|
1896
|
+
if (sourceRect) {
|
|
1897
|
+
sourcePoints.push(this.svgConnectorService.calculateConnectionPoint(sourceRect, 'source', containerRect));
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
if (sourcePoints.length === 0)
|
|
1901
|
+
continue;
|
|
1902
|
+
let paths;
|
|
1903
|
+
let midPoint;
|
|
1904
|
+
if (sourcePoints.length === 1) {
|
|
1905
|
+
paths = [this.svgConnectorService.createBezierPath(sourcePoints[0], targetPoint)];
|
|
1906
|
+
midPoint = this.svgConnectorService.getMidPoint(sourcePoints[0], targetPoint);
|
|
1907
|
+
}
|
|
1908
|
+
else {
|
|
1909
|
+
const result = this.svgConnectorService.createMultiSourcePath(sourcePoints, targetPoint);
|
|
1910
|
+
paths = result.paths;
|
|
1911
|
+
midPoint = result.mergePoint;
|
|
1912
|
+
}
|
|
1913
|
+
// Check if this array mapping has a filter
|
|
1914
|
+
let hasFilter = false;
|
|
1915
|
+
if (mapping.isArrayMapping) {
|
|
1916
|
+
const arrayMapping = this.mappingService.getArrayMapping(mapping.id);
|
|
1917
|
+
hasFilter = arrayMapping?.filter?.enabled === true && (arrayMapping?.filter?.root?.children?.length ?? 0) > 0;
|
|
1918
|
+
}
|
|
1919
|
+
newConnections.push({
|
|
1920
|
+
id: `conn-${mapping.id}`,
|
|
1921
|
+
mappingId: mapping.id,
|
|
1922
|
+
paths,
|
|
1923
|
+
midPoint,
|
|
1924
|
+
targetPoint,
|
|
1925
|
+
hasTransformation: mapping.transformation.type !== 'direct',
|
|
1926
|
+
isSelected: mapping.id === selectedId,
|
|
1927
|
+
isArrayMapping: mapping.isArrayMapping || false,
|
|
1928
|
+
isArrayToObjectMapping: mapping.isArrayToObjectMapping || false,
|
|
1929
|
+
hasFilter,
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
this.connections.set(newConnections);
|
|
1933
|
+
}
|
|
1934
|
+
getTransformationIcon(mappingId) {
|
|
1935
|
+
const mapping = this.mappings().find((m) => m.id === mappingId);
|
|
1936
|
+
if (!mapping)
|
|
1937
|
+
return 'settings';
|
|
1938
|
+
// Show filter icon for array mappings with filter, otherwise loop icon
|
|
1939
|
+
if (mapping.isArrayMapping) {
|
|
1940
|
+
const arrayMapping = this.mappingService.getArrayMapping(mappingId);
|
|
1941
|
+
if (arrayMapping?.filter?.enabled && (arrayMapping?.filter?.root?.children?.length ?? 0) > 0) {
|
|
1942
|
+
return 'filter_alt';
|
|
1943
|
+
}
|
|
1944
|
+
return 'loop';
|
|
1945
|
+
}
|
|
1946
|
+
// Show appropriate icon for array-to-object mappings
|
|
1947
|
+
if (mapping.isArrayToObjectMapping) {
|
|
1948
|
+
const atoMapping = this.mappingService.getArrayToObjectMapping(mappingId);
|
|
1949
|
+
if (atoMapping?.selector.mode === 'first')
|
|
1950
|
+
return 'first_page';
|
|
1951
|
+
if (atoMapping?.selector.mode === 'last')
|
|
1952
|
+
return 'last_page';
|
|
1953
|
+
if (atoMapping?.selector.mode === 'condition')
|
|
1954
|
+
return 'filter_alt';
|
|
1955
|
+
return 'swap_horiz';
|
|
1956
|
+
}
|
|
1957
|
+
const icons = {
|
|
1958
|
+
direct: 'arrow_forward',
|
|
1959
|
+
concat: 'merge',
|
|
1960
|
+
substring: 'content_cut',
|
|
1961
|
+
replace: 'find_replace',
|
|
1962
|
+
uppercase: 'text_fields',
|
|
1963
|
+
lowercase: 'text_fields',
|
|
1964
|
+
dateFormat: 'calendar_today',
|
|
1965
|
+
extractYear: 'event',
|
|
1966
|
+
extractMonth: 'event',
|
|
1967
|
+
extractDay: 'event',
|
|
1968
|
+
extractHour: 'schedule',
|
|
1969
|
+
extractMinute: 'schedule',
|
|
1970
|
+
extractSecond: 'schedule',
|
|
1971
|
+
numberFormat: 'pin',
|
|
1972
|
+
template: 'code',
|
|
1973
|
+
custom: 'functions',
|
|
1974
|
+
};
|
|
1975
|
+
return icons[mapping.transformation.type] || 'settings';
|
|
1976
|
+
}
|
|
1977
|
+
clearAllMappings() {
|
|
1978
|
+
this.mappingService.clearAllMappings();
|
|
1979
|
+
this.mappingsChange.emit([]);
|
|
1980
|
+
this.updateConnections();
|
|
1981
|
+
}
|
|
1982
|
+
exportMappings() {
|
|
1983
|
+
return this.mappingService.exportMappings();
|
|
1984
|
+
}
|
|
1985
|
+
importMappings(json) {
|
|
1986
|
+
this.mappingService.importMappings(json);
|
|
1987
|
+
this.mappingsChange.emit(this.mappingService.allMappings());
|
|
1988
|
+
this.updateConnections();
|
|
1989
|
+
}
|
|
1990
|
+
trackByConnectionId(index, connection) {
|
|
1991
|
+
return connection.id;
|
|
1992
|
+
}
|
|
1993
|
+
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"] }] });
|
|
1995
|
+
}
|
|
1996
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: DataMapperComponent, decorators: [{
|
|
1997
|
+
type: Component,
|
|
1998
|
+
args: [{ selector: 'data-mapper', standalone: true, imports: [
|
|
1999
|
+
CommonModule,
|
|
2000
|
+
MatIconModule,
|
|
2001
|
+
MatButtonModule,
|
|
2002
|
+
MatTooltipModule,
|
|
2003
|
+
SchemaTreeComponent,
|
|
2004
|
+
TransformationPopoverComponent,
|
|
2005
|
+
ArrayFilterModalComponent,
|
|
2006
|
+
ArraySelectorModalComponent,
|
|
2007
|
+
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"] }]
|
|
2009
|
+
}], propDecorators: { sourceSchema: [{
|
|
2010
|
+
type: Input
|
|
2011
|
+
}], targetSchema: [{
|
|
2012
|
+
type: Input
|
|
2013
|
+
}], sampleData: [{
|
|
2014
|
+
type: Input
|
|
2015
|
+
}], mappingsChange: [{
|
|
2016
|
+
type: Output
|
|
2017
|
+
}], svgContainer: [{
|
|
2018
|
+
type: ViewChild,
|
|
2019
|
+
args: ['svgContainer']
|
|
2020
|
+
}], svgElement: [{
|
|
2021
|
+
type: ViewChild,
|
|
2022
|
+
args: ['svgElement']
|
|
2023
|
+
}], onMouseMove: [{
|
|
2024
|
+
type: HostListener,
|
|
2025
|
+
args: ['document:mousemove', ['$event']]
|
|
2026
|
+
}], onMouseUp: [{
|
|
2027
|
+
type: HostListener,
|
|
2028
|
+
args: ['document:mouseup', ['$event']]
|
|
2029
|
+
}] } });
|
|
2030
|
+
|
|
2031
|
+
class SchemaEditorComponent {
|
|
2032
|
+
set schema(value) {
|
|
2033
|
+
if (value) {
|
|
2034
|
+
this.schemaName.set(value.name);
|
|
2035
|
+
this.fields.set(this.cloneFields(value.fields));
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
schemaChange = new EventEmitter();
|
|
2039
|
+
save = new EventEmitter();
|
|
2040
|
+
schemaName = signal('New Schema', ...(ngDevMode ? [{ debugName: "schemaName" }] : []));
|
|
2041
|
+
fields = signal([], ...(ngDevMode ? [{ debugName: "fields" }] : []));
|
|
2042
|
+
fieldTypes = [
|
|
2043
|
+
{ value: 'string', label: 'String', icon: 'text_fields' },
|
|
2044
|
+
{ value: 'number', label: 'Number', icon: 'pin' },
|
|
2045
|
+
{ value: 'boolean', label: 'Boolean', icon: 'toggle_on' },
|
|
2046
|
+
{ value: 'date', label: 'Date', icon: 'calendar_today' },
|
|
2047
|
+
{ value: 'object', label: 'Object', icon: 'data_object' },
|
|
2048
|
+
{ value: 'array', label: 'Array', icon: 'data_array' },
|
|
2049
|
+
];
|
|
2050
|
+
generateId() {
|
|
2051
|
+
return `field-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
2052
|
+
}
|
|
2053
|
+
cloneFields(fields) {
|
|
2054
|
+
return fields.map(f => ({
|
|
2055
|
+
...f,
|
|
2056
|
+
children: f.children ? this.cloneFields(f.children) : undefined,
|
|
2057
|
+
}));
|
|
2058
|
+
}
|
|
2059
|
+
getTypeIcon(type) {
|
|
2060
|
+
return this.fieldTypes.find(t => t.value === type)?.icon || 'help_outline';
|
|
2061
|
+
}
|
|
2062
|
+
// Add a new field at the root level
|
|
2063
|
+
addField() {
|
|
2064
|
+
const newField = {
|
|
2065
|
+
id: this.generateId(),
|
|
2066
|
+
name: '',
|
|
2067
|
+
type: 'string',
|
|
2068
|
+
isEditing: true,
|
|
2069
|
+
expanded: false,
|
|
2070
|
+
};
|
|
2071
|
+
this.fields.update(fields => [...fields, newField]);
|
|
2072
|
+
this.emitChange();
|
|
2073
|
+
}
|
|
2074
|
+
// Add a child field to an object or array
|
|
2075
|
+
addChildField(parent) {
|
|
2076
|
+
if (!parent.children) {
|
|
2077
|
+
parent.children = [];
|
|
2078
|
+
}
|
|
2079
|
+
const newField = {
|
|
2080
|
+
id: this.generateId(),
|
|
2081
|
+
name: '',
|
|
2082
|
+
type: 'string',
|
|
2083
|
+
isEditing: true,
|
|
2084
|
+
};
|
|
2085
|
+
parent.children.push(newField);
|
|
2086
|
+
parent.expanded = true;
|
|
2087
|
+
this.fields.update(fields => [...fields]);
|
|
2088
|
+
this.emitChange();
|
|
2089
|
+
}
|
|
2090
|
+
// Delete a field
|
|
2091
|
+
deleteField(field, parentList) {
|
|
2092
|
+
const index = parentList.indexOf(field);
|
|
2093
|
+
if (index > -1) {
|
|
2094
|
+
parentList.splice(index, 1);
|
|
2095
|
+
this.fields.update(fields => [...fields]);
|
|
2096
|
+
this.emitChange();
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
// Duplicate a field
|
|
2100
|
+
duplicateField(field, parentList) {
|
|
2101
|
+
const index = parentList.indexOf(field);
|
|
2102
|
+
if (index > -1) {
|
|
2103
|
+
const clone = {
|
|
2104
|
+
...field,
|
|
2105
|
+
id: this.generateId(),
|
|
2106
|
+
name: field.name + '_copy',
|
|
2107
|
+
children: field.children ? this.cloneFields(field.children) : undefined,
|
|
2108
|
+
isEditing: false,
|
|
2109
|
+
};
|
|
2110
|
+
parentList.splice(index + 1, 0, clone);
|
|
2111
|
+
this.fields.update(fields => [...fields]);
|
|
2112
|
+
this.emitChange();
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
// Toggle field expansion
|
|
2116
|
+
toggleExpand(field) {
|
|
2117
|
+
field.expanded = !field.expanded;
|
|
2118
|
+
this.fields.update(fields => [...fields]);
|
|
2119
|
+
}
|
|
2120
|
+
// Start editing a field
|
|
2121
|
+
startEdit(field) {
|
|
2122
|
+
field.isEditing = true;
|
|
2123
|
+
this.fields.update(fields => [...fields]);
|
|
2124
|
+
}
|
|
2125
|
+
// Stop editing a field
|
|
2126
|
+
stopEdit(field) {
|
|
2127
|
+
field.isEditing = false;
|
|
2128
|
+
if (!field.name.trim()) {
|
|
2129
|
+
field.name = 'unnamed';
|
|
2130
|
+
}
|
|
2131
|
+
this.fields.update(fields => [...fields]);
|
|
2132
|
+
this.emitChange();
|
|
2133
|
+
}
|
|
2134
|
+
// Handle field name input - only allow valid property name characters
|
|
2135
|
+
onFieldNameChange(field, event) {
|
|
2136
|
+
const input = event.target;
|
|
2137
|
+
// Remove invalid characters: only allow letters, numbers, underscores, and dollar signs
|
|
2138
|
+
// Property names should start with letter, underscore, or dollar sign
|
|
2139
|
+
const sanitized = input.value.replace(/[^a-zA-Z0-9_$]/g, '');
|
|
2140
|
+
field.name = sanitized;
|
|
2141
|
+
// Update input value if it was sanitized
|
|
2142
|
+
if (input.value !== sanitized) {
|
|
2143
|
+
input.value = sanitized;
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
// Handle field type change
|
|
2147
|
+
onFieldTypeChange(field, type) {
|
|
2148
|
+
field.type = type;
|
|
2149
|
+
// Initialize children array for object/array types
|
|
2150
|
+
if ((type === 'object' || type === 'array') && !field.children) {
|
|
2151
|
+
field.children = [];
|
|
2152
|
+
}
|
|
2153
|
+
this.fields.update(fields => [...fields]);
|
|
2154
|
+
this.emitChange();
|
|
2155
|
+
}
|
|
2156
|
+
// Toggle required status
|
|
2157
|
+
toggleRequired(field) {
|
|
2158
|
+
field.required = !field.required;
|
|
2159
|
+
this.fields.update(fields => [...fields]);
|
|
2160
|
+
this.emitChange();
|
|
2161
|
+
}
|
|
2162
|
+
// Update field description
|
|
2163
|
+
onDescriptionChange(field, description) {
|
|
2164
|
+
field.description = description;
|
|
2165
|
+
this.fields.update(fields => [...fields]);
|
|
2166
|
+
this.emitChange();
|
|
2167
|
+
}
|
|
2168
|
+
// Toggle allowed values editor
|
|
2169
|
+
toggleValuesEditor(field) {
|
|
2170
|
+
field.isEditingValues = !field.isEditingValues;
|
|
2171
|
+
if (field.isEditingValues && !field.allowedValues) {
|
|
2172
|
+
field.allowedValues = [];
|
|
2173
|
+
}
|
|
2174
|
+
this.fields.update(fields => [...fields]);
|
|
2175
|
+
}
|
|
2176
|
+
// Add allowed value
|
|
2177
|
+
addAllowedValue(field, input) {
|
|
2178
|
+
const value = input.value.trim();
|
|
2179
|
+
if (value && !field.allowedValues?.includes(value)) {
|
|
2180
|
+
if (!field.allowedValues) {
|
|
2181
|
+
field.allowedValues = [];
|
|
2182
|
+
}
|
|
2183
|
+
field.allowedValues.push(value);
|
|
2184
|
+
input.value = '';
|
|
2185
|
+
this.fields.update(fields => [...fields]);
|
|
2186
|
+
this.emitChange();
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
// Remove allowed value
|
|
2190
|
+
removeAllowedValue(field, index) {
|
|
2191
|
+
if (field.allowedValues) {
|
|
2192
|
+
field.allowedValues.splice(index, 1);
|
|
2193
|
+
if (field.allowedValues.length === 0) {
|
|
2194
|
+
field.allowedValues = undefined;
|
|
2195
|
+
}
|
|
2196
|
+
this.fields.update(fields => [...fields]);
|
|
2197
|
+
this.emitChange();
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
// Handle Enter key in allowed value input
|
|
2201
|
+
onAllowedValueKeydown(event, field, input) {
|
|
2202
|
+
if (event.key === 'Enter') {
|
|
2203
|
+
event.preventDefault();
|
|
2204
|
+
this.addAllowedValue(field, input);
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
// Handle keyboard events in field name input
|
|
2208
|
+
onFieldNameKeydown(event, field) {
|
|
2209
|
+
if (event.key === 'Enter') {
|
|
2210
|
+
this.stopEdit(field);
|
|
2211
|
+
}
|
|
2212
|
+
else if (event.key === 'Escape') {
|
|
2213
|
+
field.isEditing = false;
|
|
2214
|
+
this.fields.update(fields => [...fields]);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
// Move field up in list
|
|
2218
|
+
moveFieldUp(field, parentList) {
|
|
2219
|
+
const index = parentList.indexOf(field);
|
|
2220
|
+
if (index > 0) {
|
|
2221
|
+
[parentList[index - 1], parentList[index]] = [parentList[index], parentList[index - 1]];
|
|
2222
|
+
this.fields.update(fields => [...fields]);
|
|
2223
|
+
this.emitChange();
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
// Move field down in list
|
|
2227
|
+
moveFieldDown(field, parentList) {
|
|
2228
|
+
const index = parentList.indexOf(field);
|
|
2229
|
+
if (index < parentList.length - 1) {
|
|
2230
|
+
[parentList[index], parentList[index + 1]] = [parentList[index + 1], parentList[index]];
|
|
2231
|
+
this.fields.update(fields => [...fields]);
|
|
2232
|
+
this.emitChange();
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
// Check if field can be indented (previous sibling must be object/array)
|
|
2236
|
+
canIndent(field, parentList) {
|
|
2237
|
+
const index = parentList.indexOf(field);
|
|
2238
|
+
if (index <= 0)
|
|
2239
|
+
return false;
|
|
2240
|
+
const prevSibling = parentList[index - 1];
|
|
2241
|
+
return prevSibling.type === 'object' || prevSibling.type === 'array';
|
|
2242
|
+
}
|
|
2243
|
+
// Indent field - move into previous sibling's children
|
|
2244
|
+
indentField(field, parentList) {
|
|
2245
|
+
const index = parentList.indexOf(field);
|
|
2246
|
+
if (index <= 0)
|
|
2247
|
+
return;
|
|
2248
|
+
const prevSibling = parentList[index - 1];
|
|
2249
|
+
if (prevSibling.type !== 'object' && prevSibling.type !== 'array')
|
|
2250
|
+
return;
|
|
2251
|
+
// Remove from current list
|
|
2252
|
+
parentList.splice(index, 1);
|
|
2253
|
+
// Add to previous sibling's children
|
|
2254
|
+
if (!prevSibling.children) {
|
|
2255
|
+
prevSibling.children = [];
|
|
2256
|
+
}
|
|
2257
|
+
prevSibling.children.push(field);
|
|
2258
|
+
prevSibling.expanded = true;
|
|
2259
|
+
this.fields.update(fields => [...fields]);
|
|
2260
|
+
this.emitChange();
|
|
2261
|
+
}
|
|
2262
|
+
// Outdent field - move out of parent to grandparent level
|
|
2263
|
+
outdentField(field, parentList, level) {
|
|
2264
|
+
if (level === 0)
|
|
2265
|
+
return;
|
|
2266
|
+
// Find the parent object/array that contains this list
|
|
2267
|
+
const parent = this.findParentOfList(this.fields(), parentList);
|
|
2268
|
+
if (!parent)
|
|
2269
|
+
return;
|
|
2270
|
+
// Find the grandparent list
|
|
2271
|
+
const grandparentList = this.findParentList(this.fields(), parent);
|
|
2272
|
+
if (!grandparentList)
|
|
2273
|
+
return;
|
|
2274
|
+
// Remove from current list
|
|
2275
|
+
const index = parentList.indexOf(field);
|
|
2276
|
+
parentList.splice(index, 1);
|
|
2277
|
+
// Add to grandparent list after the parent
|
|
2278
|
+
const parentIndex = grandparentList.indexOf(parent);
|
|
2279
|
+
grandparentList.splice(parentIndex + 1, 0, field);
|
|
2280
|
+
this.fields.update(fields => [...fields]);
|
|
2281
|
+
this.emitChange();
|
|
2282
|
+
}
|
|
2283
|
+
// Find the parent field that contains a given list
|
|
2284
|
+
findParentOfList(searchIn, targetList) {
|
|
2285
|
+
for (const field of searchIn) {
|
|
2286
|
+
if (field.children === targetList) {
|
|
2287
|
+
return field;
|
|
2288
|
+
}
|
|
2289
|
+
if (field.children) {
|
|
2290
|
+
const found = this.findParentOfList(field.children, targetList);
|
|
2291
|
+
if (found)
|
|
2292
|
+
return found;
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
return null;
|
|
2296
|
+
}
|
|
2297
|
+
// Find the list that contains a given field
|
|
2298
|
+
findParentList(searchIn, targetField) {
|
|
2299
|
+
if (searchIn.includes(targetField)) {
|
|
2300
|
+
return searchIn;
|
|
2301
|
+
}
|
|
2302
|
+
for (const field of searchIn) {
|
|
2303
|
+
if (field.children) {
|
|
2304
|
+
const found = this.findParentList(field.children, targetField);
|
|
2305
|
+
if (found)
|
|
2306
|
+
return found;
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
return null;
|
|
2310
|
+
}
|
|
2311
|
+
// Update schema name - only allow valid characters
|
|
2312
|
+
onSchemaNameChange(name, input) {
|
|
2313
|
+
const sanitized = name.replace(/[^a-zA-Z0-9_$]/g, '');
|
|
2314
|
+
this.schemaName.set(sanitized);
|
|
2315
|
+
if (input && input.value !== sanitized) {
|
|
2316
|
+
input.value = sanitized;
|
|
2317
|
+
}
|
|
2318
|
+
this.emitChange();
|
|
2319
|
+
}
|
|
2320
|
+
// Emit change event
|
|
2321
|
+
emitChange() {
|
|
2322
|
+
this.schemaChange.emit({
|
|
2323
|
+
name: this.schemaName(),
|
|
2324
|
+
fields: this.fields(),
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
// Save the schema
|
|
2328
|
+
onSave() {
|
|
2329
|
+
this.save.emit({
|
|
2330
|
+
name: this.schemaName(),
|
|
2331
|
+
fields: this.fields(),
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2334
|
+
// Convert to internal JSON format
|
|
2335
|
+
toJson() {
|
|
2336
|
+
return JSON.stringify({
|
|
2337
|
+
name: this.schemaName(),
|
|
2338
|
+
fields: this.stripEditingState(this.fields()),
|
|
2339
|
+
}, null, 2);
|
|
2340
|
+
}
|
|
2341
|
+
// Convert to valid JSON Schema format
|
|
2342
|
+
toJsonSchema() {
|
|
2343
|
+
const required = [];
|
|
2344
|
+
const properties = {};
|
|
2345
|
+
for (const field of this.fields()) {
|
|
2346
|
+
if (field.required && field.name) {
|
|
2347
|
+
required.push(field.name);
|
|
2348
|
+
}
|
|
2349
|
+
if (field.name) {
|
|
2350
|
+
properties[field.name] = this.fieldToJsonSchema(field);
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
const schema = {
|
|
2354
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
2355
|
+
type: 'object',
|
|
2356
|
+
title: this.schemaName(),
|
|
2357
|
+
properties,
|
|
2358
|
+
};
|
|
2359
|
+
if (required.length > 0) {
|
|
2360
|
+
schema['required'] = required;
|
|
2361
|
+
}
|
|
2362
|
+
return schema;
|
|
2363
|
+
}
|
|
2364
|
+
fieldToJsonSchema(field) {
|
|
2365
|
+
const schema = {};
|
|
2366
|
+
// Map type
|
|
2367
|
+
if (field.type === 'date') {
|
|
2368
|
+
schema['type'] = 'string';
|
|
2369
|
+
schema['format'] = 'date-time';
|
|
2370
|
+
}
|
|
2371
|
+
else if (field.type === 'array') {
|
|
2372
|
+
schema['type'] = 'array';
|
|
2373
|
+
if (field.children && field.children.length > 0) {
|
|
2374
|
+
// If array has children, treat first child as item schema
|
|
2375
|
+
const itemProperties = {};
|
|
2376
|
+
const itemRequired = [];
|
|
2377
|
+
for (const child of field.children) {
|
|
2378
|
+
if (child.name) {
|
|
2379
|
+
itemProperties[child.name] = this.fieldToJsonSchema(child);
|
|
2380
|
+
if (child.required) {
|
|
2381
|
+
itemRequired.push(child.name);
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
const items = {
|
|
2386
|
+
type: 'object',
|
|
2387
|
+
properties: itemProperties,
|
|
2388
|
+
};
|
|
2389
|
+
if (itemRequired.length > 0) {
|
|
2390
|
+
items['required'] = itemRequired;
|
|
2391
|
+
}
|
|
2392
|
+
schema['items'] = items;
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
else if (field.type === 'object') {
|
|
2396
|
+
schema['type'] = 'object';
|
|
2397
|
+
if (field.children && field.children.length > 0) {
|
|
2398
|
+
const childProperties = {};
|
|
2399
|
+
const childRequired = [];
|
|
2400
|
+
for (const child of field.children) {
|
|
2401
|
+
if (child.name) {
|
|
2402
|
+
childProperties[child.name] = this.fieldToJsonSchema(child);
|
|
2403
|
+
if (child.required) {
|
|
2404
|
+
childRequired.push(child.name);
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
schema['properties'] = childProperties;
|
|
2409
|
+
if (childRequired.length > 0) {
|
|
2410
|
+
schema['required'] = childRequired;
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
else {
|
|
2415
|
+
schema['type'] = field.type;
|
|
2416
|
+
}
|
|
2417
|
+
// Add description
|
|
2418
|
+
if (field.description) {
|
|
2419
|
+
schema['description'] = field.description;
|
|
2420
|
+
}
|
|
2421
|
+
// Add enum for allowed values
|
|
2422
|
+
if (field.allowedValues && field.allowedValues.length > 0) {
|
|
2423
|
+
schema['enum'] = field.allowedValues;
|
|
2424
|
+
}
|
|
2425
|
+
return schema;
|
|
2426
|
+
}
|
|
2427
|
+
stripEditingState(fields) {
|
|
2428
|
+
return fields.map(f => {
|
|
2429
|
+
const { isEditing, isEditingValues, ...rest } = f;
|
|
2430
|
+
return {
|
|
2431
|
+
...rest,
|
|
2432
|
+
children: f.children ? this.stripEditingState(f.children) : undefined,
|
|
2433
|
+
};
|
|
2434
|
+
});
|
|
2435
|
+
}
|
|
2436
|
+
// Track by function for ngFor
|
|
2437
|
+
trackByFieldId(index, field) {
|
|
2438
|
+
return field.id;
|
|
2439
|
+
}
|
|
2440
|
+
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 }] });
|
|
2442
|
+
}
|
|
2443
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SchemaEditorComponent, decorators: [{
|
|
2444
|
+
type: Component,
|
|
2445
|
+
args: [{ selector: 'schema-editor', standalone: true, imports: [
|
|
2446
|
+
CommonModule,
|
|
2447
|
+
FormsModule,
|
|
2448
|
+
MatButtonModule,
|
|
2449
|
+
MatIconModule,
|
|
2450
|
+
MatInputModule,
|
|
2451
|
+
MatFormFieldModule,
|
|
2452
|
+
MatSelectModule,
|
|
2453
|
+
MatTooltipModule,
|
|
2454
|
+
MatMenuModule,
|
|
2455
|
+
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"] }]
|
|
2457
|
+
}], propDecorators: { schema: [{
|
|
2458
|
+
type: Input
|
|
2459
|
+
}], schemaChange: [{
|
|
2460
|
+
type: Output
|
|
2461
|
+
}], save: [{
|
|
2462
|
+
type: Output
|
|
2463
|
+
}] } });
|
|
2464
|
+
|
|
2465
|
+
/*
|
|
2466
|
+
* Public API Surface of ngx-data-mapper
|
|
2467
|
+
*/
|
|
2468
|
+
// Models
|
|
2469
|
+
|
|
2470
|
+
/**
|
|
2471
|
+
* Generated bundle index. Do not edit.
|
|
2472
|
+
*/
|
|
2473
|
+
|
|
2474
|
+
export { ArrayFilterModalComponent, ArraySelectorModalComponent, DataMapperComponent, DefaultValuePopoverComponent, MappingService, SchemaEditorComponent, SchemaParserService, SchemaTreeComponent, SvgConnectorService, TransformationPopoverComponent, TransformationService };
|
|
2475
|
+
//# sourceMappingURL=expeed-ngx-data-mapper.mjs.map
|