angular-data-mapper 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +10 -0
- package/LICENSE +190 -0
- package/PUBLISHING.md +75 -0
- package/README.md +214 -0
- package/angular.json +121 -0
- package/package.json +67 -0
- package/projects/demo-app/public/favicon.ico +0 -0
- package/projects/demo-app/src/app/app.config.ts +12 -0
- package/projects/demo-app/src/app/app.html +36 -0
- package/projects/demo-app/src/app/app.routes.ts +62 -0
- package/projects/demo-app/src/app/app.scss +65 -0
- package/projects/demo-app/src/app/app.ts +11 -0
- package/projects/demo-app/src/app/layout/app-layout.component.ts +294 -0
- package/projects/demo-app/src/app/pages/mapper-page/mapper-page.component.html +87 -0
- package/projects/demo-app/src/app/pages/mapper-page/mapper-page.component.scss +202 -0
- package/projects/demo-app/src/app/pages/mapper-page/mapper-page.component.ts +192 -0
- package/projects/demo-app/src/app/pages/mappings-page/add-mapping-dialog.component.ts +163 -0
- package/projects/demo-app/src/app/pages/mappings-page/mappings-page.component.ts +306 -0
- package/projects/demo-app/src/app/pages/schema-creator-page/schema-creator-page.component.ts +88 -0
- package/projects/demo-app/src/app/pages/schema-editor-page/schema-editor-page.component.html +108 -0
- package/projects/demo-app/src/app/pages/schema-editor-page/schema-editor-page.component.scss +317 -0
- package/projects/demo-app/src/app/pages/schema-editor-page/schema-editor-page.component.ts +129 -0
- package/projects/demo-app/src/app/services/app-state.service.ts +233 -0
- package/projects/demo-app/src/app/services/sample-data.service.ts +228 -0
- package/projects/demo-app/src/index.html +15 -0
- package/projects/demo-app/src/main.ts +6 -0
- package/projects/demo-app/src/styles.scss +54 -0
- package/projects/demo-app/tsconfig.app.json +13 -0
- package/projects/ngx-data-mapper/ng-package.json +7 -0
- package/projects/ngx-data-mapper/package.json +40 -0
- package/projects/ngx-data-mapper/src/lib/components/array-filter-modal/array-filter-modal.component.html +183 -0
- package/projects/ngx-data-mapper/src/lib/components/array-filter-modal/array-filter-modal.component.scss +352 -0
- package/projects/ngx-data-mapper/src/lib/components/array-filter-modal/array-filter-modal.component.ts +277 -0
- package/projects/ngx-data-mapper/src/lib/components/array-selector-modal/array-selector-modal.component.html +174 -0
- package/projects/ngx-data-mapper/src/lib/components/array-selector-modal/array-selector-modal.component.scss +357 -0
- package/projects/ngx-data-mapper/src/lib/components/array-selector-modal/array-selector-modal.component.ts +258 -0
- package/projects/ngx-data-mapper/src/lib/components/condition-builder/condition-builder.component.html +139 -0
- package/projects/ngx-data-mapper/src/lib/components/condition-builder/condition-builder.component.scss +213 -0
- package/projects/ngx-data-mapper/src/lib/components/condition-builder/condition-builder.component.ts +261 -0
- package/projects/ngx-data-mapper/src/lib/components/data-mapper/data-mapper.component.html +199 -0
- package/projects/ngx-data-mapper/src/lib/components/data-mapper/data-mapper.component.scss +321 -0
- package/projects/ngx-data-mapper/src/lib/components/data-mapper/data-mapper.component.ts +618 -0
- package/projects/ngx-data-mapper/src/lib/components/default-value-popover/default-value-popover.component.html +67 -0
- package/projects/ngx-data-mapper/src/lib/components/default-value-popover/default-value-popover.component.scss +97 -0
- package/projects/ngx-data-mapper/src/lib/components/default-value-popover/default-value-popover.component.ts +105 -0
- package/projects/ngx-data-mapper/src/lib/components/schema-editor/schema-editor.component.html +552 -0
- package/projects/ngx-data-mapper/src/lib/components/schema-editor/schema-editor.component.scss +824 -0
- package/projects/ngx-data-mapper/src/lib/components/schema-editor/schema-editor.component.ts +730 -0
- package/projects/ngx-data-mapper/src/lib/components/schema-tree/schema-tree.component.html +82 -0
- package/projects/ngx-data-mapper/src/lib/components/schema-tree/schema-tree.component.scss +352 -0
- package/projects/ngx-data-mapper/src/lib/components/schema-tree/schema-tree.component.ts +225 -0
- package/projects/ngx-data-mapper/src/lib/components/transformation-popover/transformation-popover.component.html +346 -0
- package/projects/ngx-data-mapper/src/lib/components/transformation-popover/transformation-popover.component.scss +511 -0
- package/projects/ngx-data-mapper/src/lib/components/transformation-popover/transformation-popover.component.ts +368 -0
- package/projects/ngx-data-mapper/src/lib/models/json-schema.model.ts +164 -0
- package/projects/ngx-data-mapper/src/lib/models/schema.model.ts +173 -0
- package/projects/ngx-data-mapper/src/lib/services/mapping.service.ts +615 -0
- package/projects/ngx-data-mapper/src/lib/services/schema-parser.service.ts +270 -0
- package/projects/ngx-data-mapper/src/lib/services/svg-connector.service.ts +135 -0
- package/projects/ngx-data-mapper/src/lib/services/transformation.service.ts +453 -0
- package/projects/ngx-data-mapper/src/public-api.ts +22 -0
- package/projects/ngx-data-mapper/tsconfig.lib.json +13 -0
- package/projects/ngx-data-mapper/tsconfig.lib.prod.json +9 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
Input,
|
|
4
|
+
Output,
|
|
5
|
+
EventEmitter,
|
|
6
|
+
ElementRef,
|
|
7
|
+
ViewChild,
|
|
8
|
+
AfterViewInit,
|
|
9
|
+
OnDestroy,
|
|
10
|
+
HostListener,
|
|
11
|
+
inject,
|
|
12
|
+
signal,
|
|
13
|
+
computed,
|
|
14
|
+
} from '@angular/core';
|
|
15
|
+
import { CommonModule } from '@angular/common';
|
|
16
|
+
import { MatIconModule } from '@angular/material/icon';
|
|
17
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
18
|
+
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
19
|
+
import {
|
|
20
|
+
SchemaField,
|
|
21
|
+
SchemaDefinition,
|
|
22
|
+
FieldMapping,
|
|
23
|
+
TransformationConfig,
|
|
24
|
+
ArrayMapping,
|
|
25
|
+
ArrayFilterConfig,
|
|
26
|
+
ArrayToObjectMapping,
|
|
27
|
+
ArraySelectorConfig,
|
|
28
|
+
DefaultValue,
|
|
29
|
+
} from '../../models/schema.model';
|
|
30
|
+
import { JsonSchema } from '../../models/json-schema.model';
|
|
31
|
+
import { MappingService } from '../../services/mapping.service';
|
|
32
|
+
import { SvgConnectorService, Point } from '../../services/svg-connector.service';
|
|
33
|
+
import { TransformationService } from '../../services/transformation.service';
|
|
34
|
+
import { SchemaParserService, SchemaDocument } from '../../services/schema-parser.service';
|
|
35
|
+
import { SchemaTreeComponent, FieldPositionEvent } from '../schema-tree/schema-tree.component';
|
|
36
|
+
import { TransformationPopoverComponent } from '../transformation-popover/transformation-popover.component';
|
|
37
|
+
import { ArrayFilterModalComponent } from '../array-filter-modal/array-filter-modal.component';
|
|
38
|
+
import { ArraySelectorModalComponent } from '../array-selector-modal/array-selector-modal.component';
|
|
39
|
+
import { DefaultValuePopoverComponent } from '../default-value-popover/default-value-popover.component';
|
|
40
|
+
|
|
41
|
+
interface VisualConnection {
|
|
42
|
+
id: string;
|
|
43
|
+
mappingId: string;
|
|
44
|
+
paths: string[];
|
|
45
|
+
sourcePoints: Point[];
|
|
46
|
+
midPoint: Point;
|
|
47
|
+
targetPoint: Point;
|
|
48
|
+
hasTransformation: boolean;
|
|
49
|
+
isSelected: boolean;
|
|
50
|
+
isArrayMapping: boolean;
|
|
51
|
+
isArrayToObjectMapping: boolean;
|
|
52
|
+
hasFilter: boolean;
|
|
53
|
+
isBeingDragged: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@Component({
|
|
57
|
+
selector: 'data-mapper',
|
|
58
|
+
standalone: true,
|
|
59
|
+
imports: [
|
|
60
|
+
CommonModule,
|
|
61
|
+
MatIconModule,
|
|
62
|
+
MatButtonModule,
|
|
63
|
+
MatTooltipModule,
|
|
64
|
+
SchemaTreeComponent,
|
|
65
|
+
TransformationPopoverComponent,
|
|
66
|
+
ArrayFilterModalComponent,
|
|
67
|
+
ArraySelectorModalComponent,
|
|
68
|
+
DefaultValuePopoverComponent,
|
|
69
|
+
],
|
|
70
|
+
templateUrl: './data-mapper.component.html',
|
|
71
|
+
styleUrl: './data-mapper.component.scss',
|
|
72
|
+
})
|
|
73
|
+
export class DataMapperComponent implements AfterViewInit, OnDestroy {
|
|
74
|
+
@Input() set sourceSchema(value: JsonSchema | SchemaDocument) {
|
|
75
|
+
if (value) {
|
|
76
|
+
this._sourceSchemaInput.set(value);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
@Input() set targetSchema(value: JsonSchema | SchemaDocument) {
|
|
80
|
+
if (value) {
|
|
81
|
+
this._targetSchemaInput.set(value);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
@Input() set sourceSchemaRef(value: string | null | undefined) {
|
|
85
|
+
this.mappingService.setSourceSchemaRef(value ?? null);
|
|
86
|
+
}
|
|
87
|
+
@Input() set targetSchemaRef(value: string | null | undefined) {
|
|
88
|
+
this.mappingService.setTargetSchemaRef(value ?? null);
|
|
89
|
+
}
|
|
90
|
+
@Input() sampleData: Record<string, unknown> = {};
|
|
91
|
+
|
|
92
|
+
@Output() mappingsChange = new EventEmitter<FieldMapping[]>();
|
|
93
|
+
|
|
94
|
+
@ViewChild('svgContainer') svgContainer!: ElementRef<HTMLDivElement>;
|
|
95
|
+
@ViewChild('svgElement') svgElement!: ElementRef<SVGSVGElement>;
|
|
96
|
+
|
|
97
|
+
private mappingService = inject(MappingService);
|
|
98
|
+
private svgConnectorService = inject(SvgConnectorService);
|
|
99
|
+
private transformationService = inject(TransformationService);
|
|
100
|
+
private schemaParserService = inject(SchemaParserService);
|
|
101
|
+
|
|
102
|
+
// Internal signals for schema inputs
|
|
103
|
+
private _sourceSchemaInput = signal<JsonSchema | SchemaDocument | null>(null);
|
|
104
|
+
private _targetSchemaInput = signal<JsonSchema | SchemaDocument | null>(null);
|
|
105
|
+
|
|
106
|
+
// Converted schemas for the tree component
|
|
107
|
+
readonly sourceSchemaForTree = computed(() => {
|
|
108
|
+
const schema = this._sourceSchemaInput();
|
|
109
|
+
if (!schema) return { name: '', fields: [] };
|
|
110
|
+
return this.schemaParserService.parseSchema(schema as SchemaDocument, schema.title || 'Source');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
readonly targetSchemaForTree = computed(() => {
|
|
114
|
+
const schema = this._targetSchemaInput();
|
|
115
|
+
if (!schema) return { name: '', fields: [] };
|
|
116
|
+
return this.schemaParserService.parseSchema(schema as SchemaDocument, schema.title || 'Target');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Field positions from both trees
|
|
120
|
+
private sourcePositions = new Map<string, DOMRect>();
|
|
121
|
+
private targetPositions = new Map<string, DOMRect>();
|
|
122
|
+
|
|
123
|
+
// Visual state
|
|
124
|
+
connections = signal<VisualConnection[]>([]);
|
|
125
|
+
dragPath = signal<string | null>(null);
|
|
126
|
+
selectedMappingId = signal<string | null>(null);
|
|
127
|
+
popoverPosition = signal<{ x: number; y: number } | null>(null);
|
|
128
|
+
|
|
129
|
+
// Array filter modal state
|
|
130
|
+
showArrayFilterModal = signal(false);
|
|
131
|
+
selectedArrayMapping = signal<ArrayMapping | null>(null);
|
|
132
|
+
|
|
133
|
+
// Array selector modal state (for array-to-object)
|
|
134
|
+
showArraySelectorModal = signal(false);
|
|
135
|
+
selectedArrayToObjectMapping = signal<ArrayToObjectMapping | null>(null);
|
|
136
|
+
|
|
137
|
+
// Default value popover state
|
|
138
|
+
showDefaultValuePopover = signal(false);
|
|
139
|
+
selectedDefaultValueField = signal<SchemaField | null>(null);
|
|
140
|
+
defaultValuePopoverPosition = signal<{ x: number; y: number } | null>(null);
|
|
141
|
+
|
|
142
|
+
// Computed values
|
|
143
|
+
readonly mappings = computed(() => this.mappingService.allMappings());
|
|
144
|
+
readonly arrayMappings = computed(() => this.mappingService.allArrayMappings());
|
|
145
|
+
readonly defaultValues = computed(() => this.mappingService.allDefaultValues());
|
|
146
|
+
readonly selectedMapping = computed(() => this.mappingService.selectedMapping());
|
|
147
|
+
readonly showPopover = computed(() => this.selectedMappingId() !== null && this.popoverPosition() !== null && !this.showArrayFilterModal());
|
|
148
|
+
|
|
149
|
+
private isDragging = false;
|
|
150
|
+
private dragSourceField: SchemaField | null = null;
|
|
151
|
+
private dragStartPoint: Point | null = null;
|
|
152
|
+
private resizeObserver!: ResizeObserver;
|
|
153
|
+
|
|
154
|
+
// Endpoint drag state
|
|
155
|
+
private isEndpointDragging = false;
|
|
156
|
+
private endpointDragMappingId: string | null = null;
|
|
157
|
+
private endpointDragType: 'source' | 'target' | null = null;
|
|
158
|
+
private endpointDragSourceIndex: number | null = null;
|
|
159
|
+
private endpointDragAnchorPoint: Point | null = null;
|
|
160
|
+
|
|
161
|
+
ngAfterViewInit(): void {
|
|
162
|
+
this.setupResizeObserver();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
ngOnDestroy(): void {
|
|
166
|
+
if (this.resizeObserver) {
|
|
167
|
+
this.resizeObserver.disconnect();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private setupResizeObserver(): void {
|
|
172
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
173
|
+
this.updateConnections();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (this.svgContainer?.nativeElement) {
|
|
177
|
+
this.resizeObserver.observe(this.svgContainer.nativeElement);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
onSourcePositionsChanged(positions: Map<string, DOMRect>): void {
|
|
182
|
+
this.sourcePositions = positions;
|
|
183
|
+
this.updateConnections();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
onTargetPositionsChanged(positions: Map<string, DOMRect>): void {
|
|
187
|
+
this.targetPositions = positions;
|
|
188
|
+
this.updateConnections();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
onFieldDragStart(event: FieldPositionEvent): void {
|
|
192
|
+
if (!this.svgContainer?.nativeElement) return;
|
|
193
|
+
|
|
194
|
+
const containerRect = this.svgContainer.nativeElement.getBoundingClientRect();
|
|
195
|
+
const startPoint = this.svgConnectorService.calculateConnectionPoint(
|
|
196
|
+
event.rect,
|
|
197
|
+
'source',
|
|
198
|
+
containerRect
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
this.isDragging = true;
|
|
202
|
+
this.dragSourceField = event.field;
|
|
203
|
+
this.dragStartPoint = startPoint;
|
|
204
|
+
|
|
205
|
+
document.body.style.cursor = 'grabbing';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
@HostListener('document:mousemove', ['$event'])
|
|
209
|
+
onMouseMove(event: MouseEvent): void {
|
|
210
|
+
if (!this.svgContainer?.nativeElement) return;
|
|
211
|
+
|
|
212
|
+
const containerRect = this.svgContainer.nativeElement.getBoundingClientRect();
|
|
213
|
+
const currentPoint: Point = {
|
|
214
|
+
x: event.clientX - containerRect.left,
|
|
215
|
+
y: event.clientY - containerRect.top,
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// Handle new connection dragging
|
|
219
|
+
if (this.isDragging && this.dragStartPoint) {
|
|
220
|
+
const path = this.svgConnectorService.createDragPath(this.dragStartPoint, currentPoint);
|
|
221
|
+
this.dragPath.set(path);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Handle endpoint dragging
|
|
226
|
+
if (this.isEndpointDragging && this.endpointDragAnchorPoint) {
|
|
227
|
+
// Show drag path from anchor to cursor
|
|
228
|
+
const path = this.endpointDragType === 'source'
|
|
229
|
+
? this.svgConnectorService.createDragPath(currentPoint, this.endpointDragAnchorPoint)
|
|
230
|
+
: this.svgConnectorService.createDragPath(this.endpointDragAnchorPoint, currentPoint);
|
|
231
|
+
this.dragPath.set(path);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
@HostListener('document:mouseup', ['$event'])
|
|
236
|
+
onMouseUp(event: MouseEvent): void {
|
|
237
|
+
if (this.isDragging) {
|
|
238
|
+
this.dragPath.set(null);
|
|
239
|
+
this.isDragging = false;
|
|
240
|
+
this.dragSourceField = null;
|
|
241
|
+
this.dragStartPoint = null;
|
|
242
|
+
document.body.style.cursor = '';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (this.isEndpointDragging) {
|
|
246
|
+
this.cancelEndpointDrag();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
onEndpointDragStart(
|
|
251
|
+
connection: VisualConnection,
|
|
252
|
+
endpointType: 'source' | 'target',
|
|
253
|
+
sourceIndex: number,
|
|
254
|
+
event: MouseEvent
|
|
255
|
+
): void {
|
|
256
|
+
event.stopPropagation();
|
|
257
|
+
event.preventDefault();
|
|
258
|
+
|
|
259
|
+
if (!this.svgContainer?.nativeElement) return;
|
|
260
|
+
|
|
261
|
+
this.isEndpointDragging = true;
|
|
262
|
+
this.endpointDragMappingId = connection.mappingId;
|
|
263
|
+
this.endpointDragType = endpointType;
|
|
264
|
+
this.endpointDragSourceIndex = sourceIndex;
|
|
265
|
+
|
|
266
|
+
// Set anchor point (the point that stays fixed)
|
|
267
|
+
if (endpointType === 'source') {
|
|
268
|
+
// Moving source, anchor is the target
|
|
269
|
+
this.endpointDragAnchorPoint = connection.targetPoint;
|
|
270
|
+
} else {
|
|
271
|
+
// Moving target, anchor is the first source (or merge point for multi-source)
|
|
272
|
+
this.endpointDragAnchorPoint = connection.sourcePoints[0];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Update MappingService drag state so schema-tree can detect endpoint dragging
|
|
276
|
+
this.mappingService.startEndpointDrag(
|
|
277
|
+
connection.mappingId,
|
|
278
|
+
endpointType,
|
|
279
|
+
endpointType === 'source' ? connection.sourcePoints[sourceIndex] : connection.targetPoint,
|
|
280
|
+
sourceIndex
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
document.body.style.cursor = 'grabbing';
|
|
284
|
+
this.updateConnections(); // Update to show connection as being dragged
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private cancelEndpointDrag(): void {
|
|
288
|
+
this.dragPath.set(null);
|
|
289
|
+
this.isEndpointDragging = false;
|
|
290
|
+
this.endpointDragMappingId = null;
|
|
291
|
+
this.endpointDragType = null;
|
|
292
|
+
this.endpointDragSourceIndex = null;
|
|
293
|
+
this.endpointDragAnchorPoint = null;
|
|
294
|
+
document.body.style.cursor = '';
|
|
295
|
+
this.mappingService.endDrag(); // Reset MappingService drag state
|
|
296
|
+
this.updateConnections(); // Update to restore connection visibility
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
onFieldDrop(event: FieldPositionEvent): void {
|
|
300
|
+
// Handle endpoint drag completion (drop on target field)
|
|
301
|
+
if (this.isEndpointDragging && this.endpointDragMappingId && this.endpointDragType === 'target') {
|
|
302
|
+
this.mappingService.changeTargetField(this.endpointDragMappingId, event.field);
|
|
303
|
+
this.mappingsChange.emit(this.mappingService.allMappings());
|
|
304
|
+
this.updateConnections();
|
|
305
|
+
this.cancelEndpointDrag();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (!this.dragSourceField) return;
|
|
310
|
+
|
|
311
|
+
// Create mapping
|
|
312
|
+
const mapping = this.mappingService.createMapping(
|
|
313
|
+
[this.dragSourceField],
|
|
314
|
+
event.field
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
this.mappingsChange.emit(this.mappingService.allMappings());
|
|
318
|
+
this.updateConnections();
|
|
319
|
+
|
|
320
|
+
// Reset drag state
|
|
321
|
+
this.isDragging = false;
|
|
322
|
+
this.dragSourceField = null;
|
|
323
|
+
this.dragStartPoint = null;
|
|
324
|
+
this.dragPath.set(null);
|
|
325
|
+
document.body.style.cursor = '';
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
onSourceFieldDrop(event: FieldPositionEvent): void {
|
|
329
|
+
// Handle endpoint drag completion (drop on source field)
|
|
330
|
+
if (this.isEndpointDragging && this.endpointDragMappingId && this.endpointDragType === 'source') {
|
|
331
|
+
this.mappingService.changeSourceField(
|
|
332
|
+
this.endpointDragMappingId,
|
|
333
|
+
event.field,
|
|
334
|
+
this.endpointDragSourceIndex ?? undefined
|
|
335
|
+
);
|
|
336
|
+
this.mappingsChange.emit(this.mappingService.allMappings());
|
|
337
|
+
this.updateConnections();
|
|
338
|
+
this.cancelEndpointDrag();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
onConnectionClick(connection: VisualConnection, event: MouseEvent): void {
|
|
343
|
+
event.stopPropagation();
|
|
344
|
+
|
|
345
|
+
this.selectedMappingId.set(connection.mappingId);
|
|
346
|
+
this.mappingService.selectMapping(connection.mappingId);
|
|
347
|
+
|
|
348
|
+
// Position popover at click location
|
|
349
|
+
this.popoverPosition.set({
|
|
350
|
+
x: event.clientX,
|
|
351
|
+
y: event.clientY,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
this.updateConnections();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
onTransformationNodeClick(connection: VisualConnection, event: MouseEvent): void {
|
|
358
|
+
event.stopPropagation();
|
|
359
|
+
|
|
360
|
+
// If it's an array mapping, show the filter modal
|
|
361
|
+
if (connection.isArrayMapping) {
|
|
362
|
+
const arrayMapping = this.mappingService.getArrayMapping(connection.mappingId);
|
|
363
|
+
if (arrayMapping) {
|
|
364
|
+
this.selectedArrayMapping.set(arrayMapping);
|
|
365
|
+
this.showArrayFilterModal.set(true);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// If it's an array-to-object mapping, show the selector modal
|
|
371
|
+
if (connection.isArrayToObjectMapping) {
|
|
372
|
+
const arrayToObjectMapping = this.mappingService.getArrayToObjectMapping(connection.mappingId);
|
|
373
|
+
if (arrayToObjectMapping) {
|
|
374
|
+
this.selectedArrayToObjectMapping.set(arrayToObjectMapping);
|
|
375
|
+
this.showArraySelectorModal.set(true);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
this.onConnectionClick(connection, event);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
onArrayFilterSave(filter: ArrayFilterConfig | undefined): void {
|
|
384
|
+
const arrayMapping = this.selectedArrayMapping();
|
|
385
|
+
if (arrayMapping) {
|
|
386
|
+
this.mappingService.updateArrayFilter(arrayMapping.id, filter);
|
|
387
|
+
this.mappingsChange.emit(this.mappingService.allMappings());
|
|
388
|
+
}
|
|
389
|
+
this.closeArrayFilterModal();
|
|
390
|
+
this.updateConnections();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
closeArrayFilterModal(): void {
|
|
394
|
+
this.showArrayFilterModal.set(false);
|
|
395
|
+
this.selectedArrayMapping.set(null);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
onArraySelectorSave(selector: ArraySelectorConfig): void {
|
|
399
|
+
const mapping = this.selectedArrayToObjectMapping();
|
|
400
|
+
if (mapping) {
|
|
401
|
+
this.mappingService.updateArrayToObjectSelector(mapping.id, selector);
|
|
402
|
+
this.mappingsChange.emit(this.mappingService.allMappings());
|
|
403
|
+
}
|
|
404
|
+
this.closeArraySelectorModal();
|
|
405
|
+
this.updateConnections();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
closeArraySelectorModal(): void {
|
|
409
|
+
this.showArraySelectorModal.set(false);
|
|
410
|
+
this.selectedArrayToObjectMapping.set(null);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Default value methods
|
|
414
|
+
onDefaultValueClick(event: FieldPositionEvent): void {
|
|
415
|
+
this.selectedDefaultValueField.set(event.field);
|
|
416
|
+
this.defaultValuePopoverPosition.set({
|
|
417
|
+
x: event.rect.right,
|
|
418
|
+
y: event.rect.top + event.rect.height / 2,
|
|
419
|
+
});
|
|
420
|
+
this.showDefaultValuePopover.set(true);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
onDefaultValueSave(value: string | number | boolean | Date | null): void {
|
|
424
|
+
const field = this.selectedDefaultValueField();
|
|
425
|
+
if (field) {
|
|
426
|
+
this.mappingService.setDefaultValue(field, value);
|
|
427
|
+
this.mappingsChange.emit(this.mappingService.allMappings());
|
|
428
|
+
}
|
|
429
|
+
this.closeDefaultValuePopover();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
onDefaultValueDelete(): void {
|
|
433
|
+
const field = this.selectedDefaultValueField();
|
|
434
|
+
if (field) {
|
|
435
|
+
this.mappingService.removeDefaultValue(field.id);
|
|
436
|
+
this.mappingsChange.emit(this.mappingService.allMappings());
|
|
437
|
+
}
|
|
438
|
+
this.closeDefaultValuePopover();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
closeDefaultValuePopover(): void {
|
|
442
|
+
this.showDefaultValuePopover.set(false);
|
|
443
|
+
this.selectedDefaultValueField.set(null);
|
|
444
|
+
this.defaultValuePopoverPosition.set(null);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
getExistingDefaultValue(fieldId: string): DefaultValue | undefined {
|
|
448
|
+
return this.mappingService.getDefaultValue(fieldId);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
onPopoverSave(transformations: TransformationConfig[]): void {
|
|
452
|
+
const mappingId = this.selectedMappingId();
|
|
453
|
+
if (mappingId) {
|
|
454
|
+
this.mappingService.updateTransformations(mappingId, transformations);
|
|
455
|
+
this.mappingsChange.emit(this.mappingService.allMappings());
|
|
456
|
+
}
|
|
457
|
+
this.closePopover();
|
|
458
|
+
this.updateConnections();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
onPopoverDelete(): void {
|
|
462
|
+
const mappingId = this.selectedMappingId();
|
|
463
|
+
if (mappingId) {
|
|
464
|
+
this.mappingService.removeMapping(mappingId);
|
|
465
|
+
this.mappingsChange.emit(this.mappingService.allMappings());
|
|
466
|
+
}
|
|
467
|
+
this.closePopover();
|
|
468
|
+
this.updateConnections();
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
closePopover(): void {
|
|
472
|
+
this.selectedMappingId.set(null);
|
|
473
|
+
this.popoverPosition.set(null);
|
|
474
|
+
this.mappingService.selectMapping(null);
|
|
475
|
+
this.updateConnections();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private updateConnections(): void {
|
|
479
|
+
if (!this.svgContainer?.nativeElement) return;
|
|
480
|
+
|
|
481
|
+
const containerRect = this.svgContainer.nativeElement.getBoundingClientRect();
|
|
482
|
+
const mappings = this.mappingService.allMappings();
|
|
483
|
+
const selectedId = this.selectedMappingId();
|
|
484
|
+
|
|
485
|
+
const newConnections: VisualConnection[] = [];
|
|
486
|
+
|
|
487
|
+
for (const mapping of mappings) {
|
|
488
|
+
const targetRect = this.targetPositions.get(mapping.targetField.id);
|
|
489
|
+
if (!targetRect) continue;
|
|
490
|
+
|
|
491
|
+
const targetPoint = this.svgConnectorService.calculateConnectionPoint(
|
|
492
|
+
targetRect,
|
|
493
|
+
'target',
|
|
494
|
+
containerRect
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
const sourcePoints: Point[] = [];
|
|
498
|
+
for (const sourceField of mapping.sourceFields) {
|
|
499
|
+
const sourceRect = this.sourcePositions.get(sourceField.id);
|
|
500
|
+
if (sourceRect) {
|
|
501
|
+
sourcePoints.push(
|
|
502
|
+
this.svgConnectorService.calculateConnectionPoint(
|
|
503
|
+
sourceRect,
|
|
504
|
+
'source',
|
|
505
|
+
containerRect
|
|
506
|
+
)
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (sourcePoints.length === 0) continue;
|
|
512
|
+
|
|
513
|
+
let paths: string[];
|
|
514
|
+
let midPoint: Point;
|
|
515
|
+
|
|
516
|
+
if (sourcePoints.length === 1) {
|
|
517
|
+
paths = [this.svgConnectorService.createBezierPath(sourcePoints[0], targetPoint)];
|
|
518
|
+
midPoint = this.svgConnectorService.getMidPoint(sourcePoints[0], targetPoint);
|
|
519
|
+
} else {
|
|
520
|
+
const result = this.svgConnectorService.createMultiSourcePath(sourcePoints, targetPoint);
|
|
521
|
+
paths = result.paths;
|
|
522
|
+
midPoint = result.mergePoint;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Check if this array mapping has a filter
|
|
526
|
+
let hasFilter = false;
|
|
527
|
+
if (mapping.isArrayMapping) {
|
|
528
|
+
const arrayMapping = this.mappingService.getArrayMapping(mapping.id);
|
|
529
|
+
hasFilter = arrayMapping?.filter?.enabled === true && (arrayMapping?.filter?.root?.children?.length ?? 0) > 0;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
newConnections.push({
|
|
533
|
+
id: `conn-${mapping.id}`,
|
|
534
|
+
mappingId: mapping.id,
|
|
535
|
+
paths,
|
|
536
|
+
sourcePoints,
|
|
537
|
+
midPoint,
|
|
538
|
+
targetPoint,
|
|
539
|
+
hasTransformation: mapping.transformations.length > 1 || mapping.transformations[0]?.type !== 'direct',
|
|
540
|
+
isSelected: mapping.id === selectedId,
|
|
541
|
+
isArrayMapping: mapping.isArrayMapping || false,
|
|
542
|
+
isArrayToObjectMapping: mapping.isArrayToObjectMapping || false,
|
|
543
|
+
hasFilter,
|
|
544
|
+
isBeingDragged: mapping.id === this.endpointDragMappingId,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
this.connections.set(newConnections);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
getTransformationIcon(mappingId: string): string {
|
|
552
|
+
const mapping = this.mappings().find((m) => m.id === mappingId);
|
|
553
|
+
if (!mapping) return 'settings';
|
|
554
|
+
|
|
555
|
+
// Show filter icon for array mappings with filter, otherwise loop icon
|
|
556
|
+
if (mapping.isArrayMapping) {
|
|
557
|
+
const arrayMapping = this.mappingService.getArrayMapping(mappingId);
|
|
558
|
+
if (arrayMapping?.filter?.enabled && (arrayMapping?.filter?.root?.children?.length ?? 0) > 0) {
|
|
559
|
+
return 'filter_alt';
|
|
560
|
+
}
|
|
561
|
+
return 'loop';
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Show appropriate icon for array-to-object mappings
|
|
565
|
+
if (mapping.isArrayToObjectMapping) {
|
|
566
|
+
const atoMapping = this.mappingService.getArrayToObjectMapping(mappingId);
|
|
567
|
+
if (atoMapping?.selector.mode === 'first') return 'first_page';
|
|
568
|
+
if (atoMapping?.selector.mode === 'last') return 'last_page';
|
|
569
|
+
if (atoMapping?.selector.mode === 'condition') return 'filter_alt';
|
|
570
|
+
return 'swap_horiz';
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const icons: Record<string, string> = {
|
|
574
|
+
direct: 'arrow_forward',
|
|
575
|
+
concat: 'merge',
|
|
576
|
+
substring: 'content_cut',
|
|
577
|
+
replace: 'find_replace',
|
|
578
|
+
uppercase: 'text_fields',
|
|
579
|
+
lowercase: 'text_fields',
|
|
580
|
+
dateFormat: 'calendar_today',
|
|
581
|
+
extractYear: 'event',
|
|
582
|
+
extractMonth: 'event',
|
|
583
|
+
extractDay: 'event',
|
|
584
|
+
extractHour: 'schedule',
|
|
585
|
+
extractMinute: 'schedule',
|
|
586
|
+
extractSecond: 'schedule',
|
|
587
|
+
numberFormat: 'pin',
|
|
588
|
+
template: 'code',
|
|
589
|
+
custom: 'functions',
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
// For multiple transformations, show a pipeline icon
|
|
593
|
+
if (mapping.transformations.length > 1) {
|
|
594
|
+
return 'linear_scale';
|
|
595
|
+
}
|
|
596
|
+
return icons[mapping.transformations[0]?.type] || 'settings';
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
clearAllMappings(): void {
|
|
600
|
+
this.mappingService.clearAllMappings();
|
|
601
|
+
this.mappingsChange.emit([]);
|
|
602
|
+
this.updateConnections();
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
exportMappings(): string {
|
|
606
|
+
return this.mappingService.exportMappings();
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
importMappings(json: string): void {
|
|
610
|
+
this.mappingService.importMappings(json);
|
|
611
|
+
this.mappingsChange.emit(this.mappingService.allMappings());
|
|
612
|
+
this.updateConnections();
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
trackByConnectionId(index: number, connection: VisualConnection): string {
|
|
616
|
+
return connection.id;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<div class="popover-backdrop" (click)="onBackdropClick($event)">
|
|
2
|
+
<div class="popover-container">
|
|
3
|
+
<div class="popover-header">
|
|
4
|
+
<div class="header-title">
|
|
5
|
+
<mat-icon>edit</mat-icon>
|
|
6
|
+
<span>Default Value</span>
|
|
7
|
+
</div>
|
|
8
|
+
<button mat-icon-button (click)="onClose()">
|
|
9
|
+
<mat-icon>close</mat-icon>
|
|
10
|
+
</button>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div class="popover-content">
|
|
14
|
+
<div class="field-info">
|
|
15
|
+
<span class="field-name">{{ field.name }}</span>
|
|
16
|
+
<span class="field-type">{{ fieldType }}</span>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<!-- String input -->
|
|
20
|
+
@if (fieldType === 'string') {
|
|
21
|
+
<mat-form-field appearance="outline" class="full-width">
|
|
22
|
+
<mat-label>Default Value</mat-label>
|
|
23
|
+
<input matInput [(ngModel)]="stringValue" placeholder="Enter default value">
|
|
24
|
+
</mat-form-field>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
<!-- Number input -->
|
|
28
|
+
@if (fieldType === 'number') {
|
|
29
|
+
<mat-form-field appearance="outline" class="full-width">
|
|
30
|
+
<mat-label>Default Value</mat-label>
|
|
31
|
+
<input matInput type="number" [(ngModel)]="numberValue" placeholder="Enter number">
|
|
32
|
+
</mat-form-field>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
<!-- Boolean input -->
|
|
36
|
+
@if (fieldType === 'boolean') {
|
|
37
|
+
<div class="boolean-input">
|
|
38
|
+
<mat-slide-toggle [(ngModel)]="booleanValue">
|
|
39
|
+
{{ booleanValue ? 'True' : 'False' }}
|
|
40
|
+
</mat-slide-toggle>
|
|
41
|
+
</div>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
<!-- Date input -->
|
|
45
|
+
@if (fieldType === 'date') {
|
|
46
|
+
<mat-form-field appearance="outline" class="full-width">
|
|
47
|
+
<mat-label>Default Date</mat-label>
|
|
48
|
+
<input matInput [matDatepicker]="picker" [(ngModel)]="dateValue">
|
|
49
|
+
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
|
|
50
|
+
<mat-datepicker #picker></mat-datepicker>
|
|
51
|
+
</mat-form-field>
|
|
52
|
+
}
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div class="popover-actions">
|
|
56
|
+
@if (existingValue) {
|
|
57
|
+
<button mat-button color="warn" (click)="onDelete()">
|
|
58
|
+
<mat-icon>delete</mat-icon>
|
|
59
|
+
Remove
|
|
60
|
+
</button>
|
|
61
|
+
}
|
|
62
|
+
<span class="spacer"></span>
|
|
63
|
+
<button mat-button (click)="onClose()">Cancel</button>
|
|
64
|
+
<button mat-flat-button color="primary" (click)="onSave()">Save</button>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|