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.
Files changed (64) hide show
  1. package/.claude/settings.local.json +10 -0
  2. package/LICENSE +190 -0
  3. package/PUBLISHING.md +75 -0
  4. package/README.md +214 -0
  5. package/angular.json +121 -0
  6. package/package.json +67 -0
  7. package/projects/demo-app/public/favicon.ico +0 -0
  8. package/projects/demo-app/src/app/app.config.ts +12 -0
  9. package/projects/demo-app/src/app/app.html +36 -0
  10. package/projects/demo-app/src/app/app.routes.ts +62 -0
  11. package/projects/demo-app/src/app/app.scss +65 -0
  12. package/projects/demo-app/src/app/app.ts +11 -0
  13. package/projects/demo-app/src/app/layout/app-layout.component.ts +294 -0
  14. package/projects/demo-app/src/app/pages/mapper-page/mapper-page.component.html +87 -0
  15. package/projects/demo-app/src/app/pages/mapper-page/mapper-page.component.scss +202 -0
  16. package/projects/demo-app/src/app/pages/mapper-page/mapper-page.component.ts +192 -0
  17. package/projects/demo-app/src/app/pages/mappings-page/add-mapping-dialog.component.ts +163 -0
  18. package/projects/demo-app/src/app/pages/mappings-page/mappings-page.component.ts +306 -0
  19. package/projects/demo-app/src/app/pages/schema-creator-page/schema-creator-page.component.ts +88 -0
  20. package/projects/demo-app/src/app/pages/schema-editor-page/schema-editor-page.component.html +108 -0
  21. package/projects/demo-app/src/app/pages/schema-editor-page/schema-editor-page.component.scss +317 -0
  22. package/projects/demo-app/src/app/pages/schema-editor-page/schema-editor-page.component.ts +129 -0
  23. package/projects/demo-app/src/app/services/app-state.service.ts +233 -0
  24. package/projects/demo-app/src/app/services/sample-data.service.ts +228 -0
  25. package/projects/demo-app/src/index.html +15 -0
  26. package/projects/demo-app/src/main.ts +6 -0
  27. package/projects/demo-app/src/styles.scss +54 -0
  28. package/projects/demo-app/tsconfig.app.json +13 -0
  29. package/projects/ngx-data-mapper/ng-package.json +7 -0
  30. package/projects/ngx-data-mapper/package.json +40 -0
  31. package/projects/ngx-data-mapper/src/lib/components/array-filter-modal/array-filter-modal.component.html +183 -0
  32. package/projects/ngx-data-mapper/src/lib/components/array-filter-modal/array-filter-modal.component.scss +352 -0
  33. package/projects/ngx-data-mapper/src/lib/components/array-filter-modal/array-filter-modal.component.ts +277 -0
  34. package/projects/ngx-data-mapper/src/lib/components/array-selector-modal/array-selector-modal.component.html +174 -0
  35. package/projects/ngx-data-mapper/src/lib/components/array-selector-modal/array-selector-modal.component.scss +357 -0
  36. package/projects/ngx-data-mapper/src/lib/components/array-selector-modal/array-selector-modal.component.ts +258 -0
  37. package/projects/ngx-data-mapper/src/lib/components/condition-builder/condition-builder.component.html +139 -0
  38. package/projects/ngx-data-mapper/src/lib/components/condition-builder/condition-builder.component.scss +213 -0
  39. package/projects/ngx-data-mapper/src/lib/components/condition-builder/condition-builder.component.ts +261 -0
  40. package/projects/ngx-data-mapper/src/lib/components/data-mapper/data-mapper.component.html +199 -0
  41. package/projects/ngx-data-mapper/src/lib/components/data-mapper/data-mapper.component.scss +321 -0
  42. package/projects/ngx-data-mapper/src/lib/components/data-mapper/data-mapper.component.ts +618 -0
  43. package/projects/ngx-data-mapper/src/lib/components/default-value-popover/default-value-popover.component.html +67 -0
  44. package/projects/ngx-data-mapper/src/lib/components/default-value-popover/default-value-popover.component.scss +97 -0
  45. package/projects/ngx-data-mapper/src/lib/components/default-value-popover/default-value-popover.component.ts +105 -0
  46. package/projects/ngx-data-mapper/src/lib/components/schema-editor/schema-editor.component.html +552 -0
  47. package/projects/ngx-data-mapper/src/lib/components/schema-editor/schema-editor.component.scss +824 -0
  48. package/projects/ngx-data-mapper/src/lib/components/schema-editor/schema-editor.component.ts +730 -0
  49. package/projects/ngx-data-mapper/src/lib/components/schema-tree/schema-tree.component.html +82 -0
  50. package/projects/ngx-data-mapper/src/lib/components/schema-tree/schema-tree.component.scss +352 -0
  51. package/projects/ngx-data-mapper/src/lib/components/schema-tree/schema-tree.component.ts +225 -0
  52. package/projects/ngx-data-mapper/src/lib/components/transformation-popover/transformation-popover.component.html +346 -0
  53. package/projects/ngx-data-mapper/src/lib/components/transformation-popover/transformation-popover.component.scss +511 -0
  54. package/projects/ngx-data-mapper/src/lib/components/transformation-popover/transformation-popover.component.ts +368 -0
  55. package/projects/ngx-data-mapper/src/lib/models/json-schema.model.ts +164 -0
  56. package/projects/ngx-data-mapper/src/lib/models/schema.model.ts +173 -0
  57. package/projects/ngx-data-mapper/src/lib/services/mapping.service.ts +615 -0
  58. package/projects/ngx-data-mapper/src/lib/services/schema-parser.service.ts +270 -0
  59. package/projects/ngx-data-mapper/src/lib/services/svg-connector.service.ts +135 -0
  60. package/projects/ngx-data-mapper/src/lib/services/transformation.service.ts +453 -0
  61. package/projects/ngx-data-mapper/src/public-api.ts +22 -0
  62. package/projects/ngx-data-mapper/tsconfig.lib.json +13 -0
  63. package/projects/ngx-data-mapper/tsconfig.lib.prod.json +9 -0
  64. 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>