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,82 @@
1
+ <div class="schema-tree" [class.source]="side === 'source'" [class.target]="side === 'target'">
2
+ <div class="schema-header">
3
+ @if (showSchemaName) {
4
+ <span class="schema-title">{{ schema.name }}</span>
5
+ }
6
+ <span class="schema-badge">{{ side === 'source' ? 'Source' : 'Target' }}</span>
7
+ </div>
8
+
9
+ <div class="schema-fields" #schemaFields>
10
+ <ng-container *ngTemplateOutlet="fieldList; context: { fields: schema.fields, level: 0 }"></ng-container>
11
+ </div>
12
+ </div>
13
+
14
+ <ng-template #fieldList let-fields="fields" let-level="level">
15
+ @for (field of fields; track trackByFieldId($index, field)) {
16
+ <div
17
+ #fieldItem
18
+ class="field-item"
19
+ [class.mapped]="isFieldMapped(field)"
20
+ [class.has-default]="hasDefaultValue(field)"
21
+ [class.has-children]="field.children && field.children.length > 0"
22
+ [class.expanded]="field.expanded"
23
+ [class.is-array]="field.type === 'array'"
24
+ [class.draggable]="side === 'source' && ((!field.children || field.children.length === 0) || field.type === 'array') && !isEndpointDragMode()"
25
+ [class.droppable]="(side === 'target' && ((!field.children || field.children.length === 0) || field.type === 'array' || field.type === 'object')) || (side === 'source' && isSourceEndpointDragging() && ((!field.children || field.children.length === 0) || field.type === 'array'))"
26
+ [class.endpoint-drop-target]="(side === 'source' && isSourceEndpointDragging()) || (side === 'target' && isTargetEndpointDragging())"
27
+ [class.clickable]="side === 'target' && (!isFieldMapped(field) || hasDefaultValue(field)) && field.type !== 'object' && field.type !== 'array'"
28
+ [style.padding-left.px]="16 + level * 20"
29
+ [attr.data-field-id]="field.id"
30
+ (mousedown)="onDragStart($event, field)"
31
+ (mouseup)="onDrop($event, field)"
32
+ (click)="onFieldClick($event, field)"
33
+ >
34
+ <!-- Expand/Collapse button for nested objects -->
35
+ @if (field.children && field.children.length > 0) {
36
+ <button class="expand-btn" (click)="toggleExpand(field, $event)">
37
+ <mat-icon>{{ field.expanded ? 'expand_more' : 'chevron_right' }}</mat-icon>
38
+ </button>
39
+ } @else {
40
+ <span class="expand-placeholder"></span>
41
+ }
42
+
43
+ <!-- Field type icon -->
44
+ <mat-icon class="type-icon" [matTooltip]="field.type">{{ getTypeIcon(field.type) }}</mat-icon>
45
+
46
+ <!-- Field name with array indicator -->
47
+ <span class="field-name">{{ field.name }}@if (field.type === 'array') {<span class="array-indicator">[]</span>}</span>
48
+
49
+ <!-- Mapping indicator -->
50
+ @if (isFieldMapped(field)) {
51
+ <span class="mapping-indicator" [matTooltip]="getFieldMappingCount(field) + ' mapping(s)'">
52
+ <mat-icon>{{ field.type === 'array' ? 'loop' : 'link' }}</mat-icon>
53
+ @if (getFieldMappingCount(field) > 1) {
54
+ <span class="mapping-count">{{ getFieldMappingCount(field) }}</span>
55
+ }
56
+ </span>
57
+ }
58
+
59
+ <!-- Default value indicator -->
60
+ @if (hasDefaultValue(field)) {
61
+ <span class="default-indicator" [matTooltip]="'Default: ' + getDefaultValueDisplay(field)">
62
+ <mat-icon>edit</mat-icon>
63
+ <span class="default-value">{{ getDefaultValueDisplay(field) }}</span>
64
+ </span>
65
+ }
66
+
67
+ <!-- Connection point - show for leaf nodes, arrays, and objects (on target for array-to-object) -->
68
+ @if ((!field.children || field.children.length === 0) || field.type === 'array' || (side === 'target' && field.type === 'object')) {
69
+ <div class="connection-point" [class.source]="side === 'source'" [class.target]="side === 'target'" [class.array-point]="field.type === 'array'" [class.object-point]="field.type === 'object'">
70
+ <span class="point-dot"></span>
71
+ </div>
72
+ }
73
+ </div>
74
+
75
+ <!-- Nested children -->
76
+ @if (field.children && field.children.length > 0 && field.expanded) {
77
+ <div class="nested-fields">
78
+ <ng-container *ngTemplateOutlet="fieldList; context: { fields: field.children, level: level + 1 }"></ng-container>
79
+ </div>
80
+ }
81
+ }
82
+ </ng-template>
@@ -0,0 +1,352 @@
1
+ :host {
2
+ display: flex;
3
+ flex-direction: column;
4
+ height: 100%;
5
+ min-height: 0;
6
+ overflow: hidden;
7
+ }
8
+
9
+ .schema-tree {
10
+ background: var(--data-mapper-panel-bg, var(--surface-card, #ffffff));
11
+ border-radius: var(--data-mapper-panel-border-radius, 12px);
12
+ border: var(--data-mapper-panel-border, none);
13
+ box-shadow: var(--data-mapper-panel-shadow, 0 2px 8px rgba(0, 0, 0, 0.08));
14
+ overflow: hidden;
15
+ height: 100%;
16
+ min-height: 0;
17
+ display: flex;
18
+ flex-direction: column;
19
+ flex: 1;
20
+
21
+ &.source {
22
+ .schema-header {
23
+ background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
24
+ }
25
+
26
+ .connection-point {
27
+ right: 8px;
28
+ }
29
+ }
30
+
31
+ &.target {
32
+ .schema-header {
33
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
34
+ }
35
+
36
+ .connection-point {
37
+ left: 8px;
38
+ }
39
+ }
40
+ }
41
+
42
+ .schema-header {
43
+ padding: 16px 20px;
44
+ color: white;
45
+ display: flex;
46
+ align-items: center;
47
+ justify-content: space-between;
48
+ gap: 12px;
49
+ flex-shrink: 0;
50
+ }
51
+
52
+ .schema-title {
53
+ font-size: 16px;
54
+ font-weight: 600;
55
+ letter-spacing: 0.3px;
56
+ }
57
+
58
+ .schema-badge {
59
+ font-size: 11px;
60
+ font-weight: 500;
61
+ text-transform: uppercase;
62
+ letter-spacing: 0.5px;
63
+ background: rgba(255, 255, 255, 0.2);
64
+ padding: 4px 10px;
65
+ border-radius: 20px;
66
+ }
67
+
68
+ .schema-fields {
69
+ flex: 1;
70
+ overflow-y: auto;
71
+ overflow-x: hidden;
72
+ padding: 8px 0;
73
+ min-height: 0;
74
+ }
75
+
76
+ .field-item {
77
+ display: flex;
78
+ align-items: center;
79
+ padding: 10px 16px;
80
+ gap: 8px;
81
+ cursor: default;
82
+ transition: background-color 0.15s ease;
83
+ position: relative;
84
+ user-select: none;
85
+
86
+ &:hover {
87
+ background-color: var(--surface-hover, #f8fafc);
88
+ }
89
+
90
+ &.mapped {
91
+ background-color: var(--surface-mapped, #f0fdf4);
92
+
93
+ &:hover {
94
+ background-color: #dcfce7;
95
+ }
96
+ }
97
+
98
+ &.draggable {
99
+ cursor: grab;
100
+
101
+ &:active {
102
+ cursor: grabbing;
103
+ }
104
+
105
+ &:hover .connection-point .point-dot {
106
+ transform: scale(1.3);
107
+ background: #6366f1;
108
+ }
109
+ }
110
+
111
+ &.droppable {
112
+ cursor: pointer;
113
+
114
+ &:hover .connection-point .point-dot {
115
+ transform: scale(1.3);
116
+ background: #10b981;
117
+ }
118
+ }
119
+
120
+ &.endpoint-drop-target {
121
+ background-color: #fef3c7;
122
+
123
+ &:hover {
124
+ background-color: #fde68a;
125
+ }
126
+
127
+ .connection-point .point-dot {
128
+ animation: pulse-drop-target 1s ease-in-out infinite;
129
+ }
130
+ }
131
+ }
132
+
133
+ @keyframes pulse-drop-target {
134
+ 0%, 100% {
135
+ transform: scale(1);
136
+ box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.3);
137
+ }
138
+ 50% {
139
+ transform: scale(1.2);
140
+ box-shadow: 0 0 0 6px rgba(245, 158, 11, 0.2);
141
+ }
142
+ }
143
+
144
+ .expand-btn {
145
+ background: none;
146
+ border: none;
147
+ padding: 0;
148
+ width: 24px;
149
+ height: 24px;
150
+ display: flex;
151
+ align-items: center;
152
+ justify-content: center;
153
+ cursor: pointer;
154
+ color: #94a3b8;
155
+ border-radius: 4px;
156
+ transition: all 0.15s ease;
157
+
158
+ &:hover {
159
+ background-color: #e2e8f0;
160
+ color: #475569;
161
+ }
162
+
163
+ mat-icon {
164
+ font-size: 20px;
165
+ width: 20px;
166
+ height: 20px;
167
+ }
168
+ }
169
+
170
+ .expand-placeholder {
171
+ width: 24px;
172
+ height: 24px;
173
+ flex-shrink: 0;
174
+ }
175
+
176
+ .type-icon {
177
+ font-size: 18px;
178
+ width: 18px;
179
+ height: 18px;
180
+ color: #64748b;
181
+ flex-shrink: 0;
182
+ }
183
+
184
+ .field-name {
185
+ flex: 1;
186
+ font-size: 14px;
187
+ color: #1e293b;
188
+ font-weight: 500;
189
+ overflow: hidden;
190
+ text-overflow: ellipsis;
191
+ white-space: nowrap;
192
+
193
+ .array-indicator {
194
+ color: #f59e0b;
195
+ font-weight: 600;
196
+ margin-left: 2px;
197
+ }
198
+ }
199
+
200
+ .field-item.is-array {
201
+ background-color: #fffbeb;
202
+
203
+ &:hover {
204
+ background-color: #fef3c7;
205
+ }
206
+
207
+ &.mapped {
208
+ background-color: #fef3c7;
209
+ }
210
+
211
+ .type-icon {
212
+ color: #f59e0b;
213
+ }
214
+ }
215
+
216
+ .connection-point.array-point {
217
+ .point-dot {
218
+ background: #f59e0b;
219
+ box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.3);
220
+ }
221
+ }
222
+
223
+ .connection-point.object-point {
224
+ .point-dot {
225
+ background: #8b5cf6;
226
+ box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.3);
227
+ }
228
+ }
229
+
230
+ .mapping-indicator {
231
+ display: flex;
232
+ align-items: center;
233
+ gap: 2px;
234
+ color: #10b981;
235
+
236
+ mat-icon {
237
+ font-size: 16px;
238
+ width: 16px;
239
+ height: 16px;
240
+ }
241
+
242
+ .mapping-count {
243
+ font-size: 11px;
244
+ font-weight: 600;
245
+ background: #10b981;
246
+ color: white;
247
+ padding: 1px 5px;
248
+ border-radius: 10px;
249
+ min-width: 16px;
250
+ text-align: center;
251
+ }
252
+ }
253
+
254
+ .connection-point {
255
+ position: absolute;
256
+ top: 50%;
257
+ transform: translateY(-50%);
258
+ width: 20px;
259
+ height: 20px;
260
+ display: flex;
261
+ align-items: center;
262
+ justify-content: center;
263
+
264
+ &.source {
265
+ right: 8px;
266
+ }
267
+
268
+ &.target {
269
+ left: 8px;
270
+ }
271
+ }
272
+
273
+ .point-dot {
274
+ width: 10px;
275
+ height: 10px;
276
+ border-radius: 50%;
277
+ background: #cbd5e1;
278
+ transition: all 0.2s ease;
279
+ box-shadow: 0 0 0 3px rgba(203, 213, 225, 0.3);
280
+
281
+ .mapped & {
282
+ background: #10b981;
283
+ box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.3);
284
+ }
285
+ }
286
+
287
+ .nested-fields {
288
+ border-left: 2px solid #e2e8f0;
289
+ margin-left: 28px;
290
+ }
291
+
292
+ .field-item.has-default {
293
+ background-color: #eff6ff;
294
+
295
+ &:hover {
296
+ background-color: #dbeafe;
297
+ }
298
+ }
299
+
300
+ .field-item.clickable {
301
+ cursor: pointer;
302
+
303
+ &:hover:not(.has-default) {
304
+ background-color: #f1f5f9;
305
+
306
+ &::after {
307
+ content: 'Click to set default';
308
+ position: absolute;
309
+ right: 32px;
310
+ font-size: 11px;
311
+ color: #64748b;
312
+ font-style: italic;
313
+ }
314
+ }
315
+
316
+ &.has-default:hover {
317
+ &::after {
318
+ content: 'Click to edit';
319
+ position: absolute;
320
+ right: 32px;
321
+ font-size: 11px;
322
+ color: #3b82f6;
323
+ font-style: italic;
324
+ }
325
+ }
326
+ }
327
+
328
+ .default-indicator {
329
+ display: flex;
330
+ align-items: center;
331
+ gap: 4px;
332
+ color: #3b82f6;
333
+ font-size: 12px;
334
+ background: #dbeafe;
335
+ padding: 2px 8px;
336
+ border-radius: 4px;
337
+ max-width: 100px;
338
+ overflow: hidden;
339
+
340
+ mat-icon {
341
+ font-size: 14px;
342
+ width: 14px;
343
+ height: 14px;
344
+ }
345
+
346
+ .default-value {
347
+ overflow: hidden;
348
+ text-overflow: ellipsis;
349
+ white-space: nowrap;
350
+ font-weight: 500;
351
+ }
352
+ }
@@ -0,0 +1,225 @@
1
+ import {
2
+ Component,
3
+ Input,
4
+ Output,
5
+ EventEmitter,
6
+ ElementRef,
7
+ ViewChild,
8
+ ViewChildren,
9
+ QueryList,
10
+ AfterViewInit,
11
+ OnDestroy,
12
+ inject,
13
+ } from '@angular/core';
14
+ import { CommonModule } from '@angular/common';
15
+ import { MatIconModule } from '@angular/material/icon';
16
+ import { MatTooltipModule } from '@angular/material/tooltip';
17
+ import { SchemaField, FieldMapping, DefaultValue } from '../../models/schema.model';
18
+ import { MappingService } from '../../services/mapping.service';
19
+
20
+ export interface FieldPositionEvent {
21
+ field: SchemaField;
22
+ element: HTMLElement;
23
+ rect: DOMRect;
24
+ }
25
+
26
+ @Component({
27
+ selector: 'schema-tree',
28
+ standalone: true,
29
+ imports: [CommonModule, MatIconModule, MatTooltipModule],
30
+ templateUrl: './schema-tree.component.html',
31
+ styleUrl: './schema-tree.component.scss',
32
+ })
33
+ export class SchemaTreeComponent implements AfterViewInit, OnDestroy {
34
+ @Input() schema!: { name: string; fields: SchemaField[] };
35
+ @Input() side: 'source' | 'target' = 'source';
36
+ @Input() mappings: FieldMapping[] = [];
37
+ @Input() defaultValues: DefaultValue[] = [];
38
+ @Input() showSchemaName: boolean = true;
39
+
40
+ @Output() fieldDragStart = new EventEmitter<FieldPositionEvent>();
41
+ @Output() fieldDragEnd = new EventEmitter<void>();
42
+ @Output() fieldDrop = new EventEmitter<FieldPositionEvent>();
43
+ @Output() sourceDrop = new EventEmitter<FieldPositionEvent>(); // For endpoint dragging - drop on source field
44
+ @Output() fieldPositionsChanged = new EventEmitter<Map<string, DOMRect>>();
45
+ @Output() fieldDefaultValueClick = new EventEmitter<FieldPositionEvent>();
46
+
47
+ @ViewChild('schemaFields') schemaFieldsContainer!: ElementRef<HTMLDivElement>;
48
+ @ViewChildren('fieldItem') fieldItems!: QueryList<ElementRef>;
49
+
50
+ private mappingService = inject(MappingService);
51
+ private resizeObserver!: ResizeObserver;
52
+ private scrollHandler = () => this.onScroll();
53
+
54
+ ngAfterViewInit(): void {
55
+ this.emitFieldPositions();
56
+
57
+ this.resizeObserver = new ResizeObserver(() => {
58
+ this.emitFieldPositions();
59
+ });
60
+
61
+ this.fieldItems.changes.subscribe(() => {
62
+ this.emitFieldPositions();
63
+ });
64
+
65
+ // Add scroll listener to update connector positions
66
+ if (this.schemaFieldsContainer?.nativeElement) {
67
+ this.schemaFieldsContainer.nativeElement.addEventListener('scroll', this.scrollHandler, { passive: true });
68
+ }
69
+ }
70
+
71
+ ngOnDestroy(): void {
72
+ if (this.resizeObserver) {
73
+ this.resizeObserver.disconnect();
74
+ }
75
+ if (this.schemaFieldsContainer?.nativeElement) {
76
+ this.schemaFieldsContainer.nativeElement.removeEventListener('scroll', this.scrollHandler);
77
+ }
78
+ }
79
+
80
+ onScroll(): void {
81
+ this.emitFieldPositions();
82
+ }
83
+
84
+ emitFieldPositions(): void {
85
+ setTimeout(() => {
86
+ const positions = new Map<string, DOMRect>();
87
+ this.fieldItems.forEach((item) => {
88
+ const fieldId = item.nativeElement.getAttribute('data-field-id');
89
+ if (fieldId) {
90
+ positions.set(fieldId, item.nativeElement.getBoundingClientRect());
91
+ }
92
+ });
93
+ this.fieldPositionsChanged.emit(positions);
94
+ });
95
+ }
96
+
97
+ toggleExpand(field: SchemaField, event: Event): void {
98
+ event.stopPropagation();
99
+ field.expanded = !field.expanded;
100
+ setTimeout(() => this.emitFieldPositions(), 50);
101
+ }
102
+
103
+ onDragStart(event: MouseEvent, field: SchemaField): void {
104
+ if (this.side !== 'source') return;
105
+
106
+ // Don't start new drag if endpoint dragging is in progress
107
+ const dragState = this.mappingService.currentDragState();
108
+ if (dragState.isDragging) return;
109
+
110
+ const element = event.currentTarget as HTMLElement;
111
+ const rect = element.getBoundingClientRect();
112
+
113
+ this.fieldDragStart.emit({ field, element, rect });
114
+ }
115
+
116
+ isEndpointDragMode(): boolean {
117
+ const dragState = this.mappingService.currentDragState();
118
+ return dragState.isDragging && (dragState.dragMode === 'move-source' || dragState.dragMode === 'move-target');
119
+ }
120
+
121
+ isSourceEndpointDragging(): boolean {
122
+ const dragState = this.mappingService.currentDragState();
123
+ return dragState.isDragging && dragState.dragMode === 'move-source';
124
+ }
125
+
126
+ isTargetEndpointDragging(): boolean {
127
+ const dragState = this.mappingService.currentDragState();
128
+ return dragState.isDragging && dragState.dragMode === 'move-target';
129
+ }
130
+
131
+ onDragOver(event: DragEvent): void {
132
+ if (this.side === 'target') {
133
+ event.preventDefault();
134
+ }
135
+ }
136
+
137
+ onDrop(event: MouseEvent, field: SchemaField): void {
138
+ const element = event.currentTarget as HTMLElement;
139
+ const rect = element.getBoundingClientRect();
140
+
141
+ // Check if endpoint dragging is in progress (via MappingService drag state)
142
+ const dragState = this.mappingService.currentDragState();
143
+
144
+ // Handle source drop during endpoint dragging (moving source endpoint)
145
+ if (this.side === 'source' && dragState.isDragging && dragState.dragMode === 'move-source') {
146
+ this.sourceDrop.emit({ field, element, rect });
147
+ return;
148
+ }
149
+
150
+ // Handle target drop (either endpoint dragging or new mapping)
151
+ if (this.side === 'target') {
152
+ this.fieldDrop.emit({ field, element, rect });
153
+ }
154
+ }
155
+
156
+ getTypeIcon(type: string): string {
157
+ const icons: Record<string, string> = {
158
+ string: 'text_fields',
159
+ number: 'pin',
160
+ boolean: 'toggle_on',
161
+ object: 'data_object',
162
+ array: 'data_array',
163
+ date: 'calendar_today',
164
+ };
165
+ return icons[type] || 'help_outline';
166
+ }
167
+
168
+ isFieldMapped(field: SchemaField): boolean {
169
+ if (this.side === 'source') {
170
+ return this.mappings.some((m) =>
171
+ m.sourceFields.some((sf) => sf.id === field.id)
172
+ );
173
+ } else {
174
+ return this.mappings.some((m) => m.targetField.id === field.id);
175
+ }
176
+ }
177
+
178
+ getFieldMappingCount(field: SchemaField): number {
179
+ if (this.side === 'source') {
180
+ return this.mappings.filter((m) =>
181
+ m.sourceFields.some((sf) => sf.id === field.id)
182
+ ).length;
183
+ } else {
184
+ const mapping = this.mappings.find((m) => m.targetField.id === field.id);
185
+ return mapping ? mapping.sourceFields.length : 0;
186
+ }
187
+ }
188
+
189
+ hasDefaultValue(field: SchemaField): boolean {
190
+ return this.defaultValues.some(d => d.targetField.id === field.id);
191
+ }
192
+
193
+ getDefaultValueDisplay(field: SchemaField): string {
194
+ const defaultValue = this.defaultValues.find(d => d.targetField.id === field.id);
195
+ if (!defaultValue || defaultValue.value === null) return '';
196
+
197
+ if (defaultValue.targetField.type === 'date' && defaultValue.value) {
198
+ return new Date(defaultValue.value as string).toLocaleDateString();
199
+ }
200
+ return String(defaultValue.value);
201
+ }
202
+
203
+ onFieldClick(event: MouseEvent, field: SchemaField): void {
204
+ // Only handle clicks on target fields that are leaf nodes (no children) or have specific types
205
+ if (this.side !== 'target') return;
206
+ if (field.type === 'object' || field.type === 'array') return;
207
+
208
+ // Don't trigger if the field is already mapped (unless it has a default value)
209
+ if (this.isFieldMapped(field) && !this.hasDefaultValue(field)) return;
210
+
211
+ // Allow clicking on unmapped fields OR fields with default values (to edit them)
212
+ if (!this.isFieldMapped(field) || this.hasDefaultValue(field)) {
213
+ event.stopPropagation();
214
+
215
+ const element = event.currentTarget as HTMLElement;
216
+ const rect = element.getBoundingClientRect();
217
+
218
+ this.fieldDefaultValueClick.emit({ field, element, rect });
219
+ }
220
+ }
221
+
222
+ trackByFieldId(index: number, field: SchemaField): string {
223
+ return field.id;
224
+ }
225
+ }