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,174 @@
1
+ <div class="modal-backdrop" (click)="onBackdropClick($event)">
2
+ <div class="selector-modal">
3
+ <div class="modal-header">
4
+ <div class="header-title">
5
+ <mat-icon>swap_horiz</mat-icon>
6
+ <span>Array to Object</span>
7
+ </div>
8
+ <span class="mapping-path">{{ mapping.sourceArray.name }}[] &rarr; {{ mapping.targetObject.name }}</span>
9
+ <button mat-icon-button class="close-btn" (click)="onClose()">
10
+ <mat-icon>close</mat-icon>
11
+ </button>
12
+ </div>
13
+
14
+ <div class="modal-body">
15
+ <p class="description">
16
+ Select which item from the array should be mapped to the target object.
17
+ </p>
18
+
19
+ <!-- Selection mode -->
20
+ <div class="selection-mode">
21
+ <mat-radio-group [value]="selectionMode()" (change)="selectionMode.set($event.value)">
22
+ <mat-radio-button value="first" class="mode-option">
23
+ <div class="mode-content">
24
+ <mat-icon>first_page</mat-icon>
25
+ <div class="mode-text">
26
+ <span class="mode-label">First item</span>
27
+ <span class="mode-desc">Use the first item in the array</span>
28
+ </div>
29
+ </div>
30
+ </mat-radio-button>
31
+
32
+ <mat-radio-button value="last" class="mode-option">
33
+ <div class="mode-content">
34
+ <mat-icon>last_page</mat-icon>
35
+ <div class="mode-text">
36
+ <span class="mode-label">Last item</span>
37
+ <span class="mode-desc">Use the last item in the array</span>
38
+ </div>
39
+ </div>
40
+ </mat-radio-button>
41
+
42
+ <mat-radio-button value="condition" class="mode-option">
43
+ <div class="mode-content">
44
+ <mat-icon>filter_alt</mat-icon>
45
+ <div class="mode-text">
46
+ <span class="mode-label">First matching condition</span>
47
+ <span class="mode-desc">Use the first item that matches the condition</span>
48
+ </div>
49
+ </div>
50
+ </mat-radio-button>
51
+ </mat-radio-group>
52
+ </div>
53
+
54
+ <!-- Condition builder (only when condition mode selected) -->
55
+ @if (selectionMode() === 'condition') {
56
+ <mat-divider></mat-divider>
57
+
58
+ <div class="conditions-section">
59
+ <ng-container *ngTemplateOutlet="groupTemplate; context: { group: conditionGroup(), isRoot: true }"></ng-container>
60
+ </div>
61
+ }
62
+ </div>
63
+
64
+ <div class="modal-footer">
65
+ <button mat-button (click)="onClose()">Cancel</button>
66
+ <button mat-flat-button color="primary" (click)="onSave()">Apply</button>
67
+ </div>
68
+ </div>
69
+ </div>
70
+
71
+ <!-- Recursive group template -->
72
+ <ng-template #groupTemplate let-group="group" let-isRoot="isRoot" let-parentGroup="parentGroup">
73
+ <div class="filter-group" [class.root-group]="isRoot" [class.nested-group]="!isRoot">
74
+ <div class="group-header">
75
+ <div class="logic-toggle">
76
+ <span class="logic-label">Match</span>
77
+ <mat-radio-group [value]="group.logic" (change)="onLogicChange(group, $event.value)">
78
+ <mat-radio-button value="and">ALL (AND)</mat-radio-button>
79
+ <mat-radio-button value="or">ANY (OR)</mat-radio-button>
80
+ </mat-radio-group>
81
+ </div>
82
+ @if (!isRoot) {
83
+ <button mat-icon-button class="remove-group-btn" matTooltip="Remove group" (click)="removeItem(parentGroup, group.id)">
84
+ <mat-icon>close</mat-icon>
85
+ </button>
86
+ }
87
+ </div>
88
+
89
+ <div class="group-children">
90
+ @for (item of group.children; track item.id; let i = $index) {
91
+ @if (i > 0) {
92
+ <div class="logic-connector">
93
+ <span class="logic-badge" [class.and]="group.logic === 'and'" [class.or]="group.logic === 'or'">
94
+ {{ group.logic | uppercase }}
95
+ </span>
96
+ </div>
97
+ }
98
+
99
+ @if (isCondition(item)) {
100
+ <div class="condition-row">
101
+ <div class="condition-inputs">
102
+ <mat-form-field appearance="outline" class="field-select">
103
+ <mat-label>Field</mat-label>
104
+ <mat-select [value]="item.field" (selectionChange)="onFieldChange(item, $event.value)">
105
+ @for (field of availableFields(); track field.path) {
106
+ <mat-option [value]="field.path">
107
+ {{ field.name }}
108
+ <span class="field-type">({{ field.type }})</span>
109
+ </mat-option>
110
+ }
111
+ </mat-select>
112
+ </mat-form-field>
113
+
114
+ <mat-form-field appearance="outline" class="operator-select">
115
+ <mat-label>Operator</mat-label>
116
+ <mat-select [value]="item.operator" (selectionChange)="onOperatorChange(item, $event.value)">
117
+ @for (op of getOperatorsForField(item.field); track op.value) {
118
+ <mat-option [value]="op.value">{{ op.label }}</mat-option>
119
+ }
120
+ </mat-select>
121
+ </mat-form-field>
122
+
123
+ @if (operatorNeedsValue(item.operator)) {
124
+ @if (item.valueType === 'boolean') {
125
+ <mat-slide-toggle
126
+ [checked]="item.value === true"
127
+ (change)="onValueChange(item, $event.checked)"
128
+ class="bool-toggle"
129
+ >
130
+ {{ item.value ? 'true' : 'false' }}
131
+ </mat-slide-toggle>
132
+ } @else if (item.valueType === 'number') {
133
+ <mat-form-field appearance="outline" class="value-input">
134
+ <mat-label>Value</mat-label>
135
+ <input matInput type="number" [value]="item.value" (input)="onValueChange(item, $any($event.target).value)" />
136
+ </mat-form-field>
137
+ } @else {
138
+ <mat-form-field appearance="outline" class="value-input">
139
+ <mat-label>Value</mat-label>
140
+ <input matInput type="text" [value]="item.value" (input)="onValueChange(item, $any($event.target).value)" />
141
+ </mat-form-field>
142
+ }
143
+ }
144
+
145
+ <button mat-icon-button class="remove-btn" matTooltip="Remove condition" (click)="removeItem(group, item.id)">
146
+ <mat-icon>close</mat-icon>
147
+ </button>
148
+ </div>
149
+ </div>
150
+ } @else if (isGroup(item)) {
151
+ <ng-container *ngTemplateOutlet="groupTemplate; context: { group: item, isRoot: false, parentGroup: group }"></ng-container>
152
+ }
153
+ }
154
+
155
+ @if (group.children.length === 0) {
156
+ <div class="empty-group">
157
+ <mat-icon>info_outline</mat-icon>
158
+ <span>No conditions. Add a condition to filter.</span>
159
+ </div>
160
+ }
161
+ </div>
162
+
163
+ <div class="group-actions">
164
+ <button mat-stroked-button class="add-condition-btn" (click)="addCondition(group)">
165
+ <mat-icon>add</mat-icon>
166
+ Add Condition
167
+ </button>
168
+ <button mat-stroked-button class="add-group-btn" (click)="addGroup(group)">
169
+ <mat-icon>folder_open</mat-icon>
170
+ Add Group
171
+ </button>
172
+ </div>
173
+ </div>
174
+ </ng-template>
@@ -0,0 +1,357 @@
1
+ .modal-backdrop {
2
+ position: fixed;
3
+ top: 0;
4
+ left: 0;
5
+ right: 0;
6
+ bottom: 0;
7
+ background: rgba(0, 0, 0, 0.3);
8
+ z-index: 1000;
9
+ display: flex;
10
+ align-items: center;
11
+ justify-content: center;
12
+ padding: 20px;
13
+ }
14
+
15
+ .selector-modal {
16
+ position: relative;
17
+ background: white;
18
+ border-radius: 12px;
19
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
20
+ width: 580px;
21
+ max-width: 100%;
22
+ max-height: calc(100vh - 40px);
23
+ display: flex;
24
+ flex-direction: column;
25
+ overflow: hidden;
26
+ }
27
+
28
+ .modal-header {
29
+ display: flex;
30
+ align-items: center;
31
+ gap: 12px;
32
+ padding: 16px 20px;
33
+ background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%);
34
+ color: white;
35
+ flex-shrink: 0;
36
+
37
+ .header-title {
38
+ display: flex;
39
+ align-items: center;
40
+ gap: 8px;
41
+ font-size: 16px;
42
+ font-weight: 600;
43
+ }
44
+
45
+ .mapping-path {
46
+ flex: 1;
47
+ font-size: 13px;
48
+ opacity: 0.9;
49
+ text-align: right;
50
+ margin-right: 8px;
51
+ }
52
+
53
+ .close-btn {
54
+ color: white;
55
+ opacity: 0.9;
56
+
57
+ &:hover {
58
+ opacity: 1;
59
+ }
60
+ }
61
+ }
62
+
63
+ .modal-body {
64
+ flex: 1;
65
+ overflow-y: auto;
66
+ padding: 20px;
67
+ min-height: 0;
68
+ }
69
+
70
+ .description {
71
+ font-size: 14px;
72
+ color: #64748b;
73
+ margin: 0 0 16px 0;
74
+ }
75
+
76
+ .selection-mode {
77
+ mat-radio-group {
78
+ display: flex;
79
+ flex-direction: column;
80
+ gap: 12px;
81
+ }
82
+
83
+ .mode-option {
84
+ ::ng-deep .mdc-form-field {
85
+ align-items: flex-start;
86
+ }
87
+
88
+ ::ng-deep .mdc-radio {
89
+ margin-top: 4px;
90
+ }
91
+ }
92
+
93
+ .mode-content {
94
+ display: flex;
95
+ align-items: flex-start;
96
+ gap: 12px;
97
+ padding: 12px 16px;
98
+ border-radius: 8px;
99
+ background: #f8fafc;
100
+ border: 1px solid #e2e8f0;
101
+ transition: all 0.2s ease;
102
+ cursor: pointer;
103
+
104
+ &:hover {
105
+ background: #f1f5f9;
106
+ border-color: #cbd5e1;
107
+ }
108
+
109
+ mat-icon {
110
+ color: #64748b;
111
+ margin-top: 2px;
112
+ }
113
+ }
114
+
115
+ .mode-text {
116
+ display: flex;
117
+ flex-direction: column;
118
+ gap: 2px;
119
+ }
120
+
121
+ .mode-label {
122
+ font-size: 14px;
123
+ font-weight: 500;
124
+ color: #1e293b;
125
+ }
126
+
127
+ .mode-desc {
128
+ font-size: 12px;
129
+ color: #64748b;
130
+ }
131
+
132
+ mat-radio-button.mat-mdc-radio-checked {
133
+ .mode-content {
134
+ background: #f5f3ff;
135
+ border-color: #8b5cf6;
136
+
137
+ mat-icon {
138
+ color: #8b5cf6;
139
+ }
140
+ }
141
+ }
142
+ }
143
+
144
+ mat-divider {
145
+ margin: 20px 0;
146
+ }
147
+
148
+ .conditions-section {
149
+ display: flex;
150
+ flex-direction: column;
151
+ gap: 16px;
152
+ }
153
+
154
+ .filter-group {
155
+ border-radius: 8px;
156
+
157
+ &.root-group {
158
+ background: #f8fafc;
159
+ border: 1px solid #e2e8f0;
160
+ padding: 16px;
161
+ }
162
+
163
+ &.nested-group {
164
+ background: white;
165
+ border: 2px dashed #cbd5e1;
166
+ padding: 12px;
167
+ margin-top: 8px;
168
+ }
169
+ }
170
+
171
+ .group-header {
172
+ display: flex;
173
+ align-items: center;
174
+ justify-content: space-between;
175
+ margin-bottom: 12px;
176
+ padding-bottom: 12px;
177
+ border-bottom: 1px solid #e2e8f0;
178
+ }
179
+
180
+ .logic-toggle {
181
+ display: flex;
182
+ align-items: center;
183
+ gap: 12px;
184
+
185
+ .logic-label {
186
+ font-size: 13px;
187
+ font-weight: 500;
188
+ color: #475569;
189
+ }
190
+
191
+ mat-radio-group {
192
+ display: flex;
193
+ gap: 12px;
194
+ }
195
+
196
+ mat-radio-button {
197
+ font-size: 12px;
198
+
199
+ ::ng-deep .mdc-label {
200
+ font-size: 12px;
201
+ }
202
+ }
203
+ }
204
+
205
+ .remove-group-btn {
206
+ color: #94a3b8;
207
+
208
+ &:hover {
209
+ color: #ef4444;
210
+ }
211
+ }
212
+
213
+ .group-children {
214
+ display: flex;
215
+ flex-direction: column;
216
+ }
217
+
218
+ .logic-connector {
219
+ display: flex;
220
+ align-items: center;
221
+ justify-content: center;
222
+ padding: 8px 0;
223
+
224
+ .logic-badge {
225
+ font-size: 10px;
226
+ font-weight: 700;
227
+ padding: 3px 10px;
228
+ border-radius: 12px;
229
+ letter-spacing: 0.5px;
230
+
231
+ &.and {
232
+ background: #dbeafe;
233
+ color: #1d4ed8;
234
+ }
235
+
236
+ &.or {
237
+ background: #fef3c7;
238
+ color: #b45309;
239
+ }
240
+ }
241
+ }
242
+
243
+ .condition-row {
244
+ .condition-inputs {
245
+ display: flex;
246
+ align-items: flex-start;
247
+ gap: 8px;
248
+ padding: 12px;
249
+ background: white;
250
+ border: 1px solid #e2e8f0;
251
+ border-radius: 8px;
252
+
253
+ .field-select {
254
+ flex: 1;
255
+ min-width: 120px;
256
+ }
257
+
258
+ .operator-select {
259
+ flex: 1;
260
+ min-width: 130px;
261
+ }
262
+
263
+ .value-input {
264
+ flex: 1;
265
+ min-width: 100px;
266
+ }
267
+
268
+ .bool-toggle {
269
+ padding-top: 12px;
270
+ min-width: 80px;
271
+ }
272
+
273
+ .remove-btn {
274
+ color: #94a3b8;
275
+ align-self: center;
276
+
277
+ &:hover {
278
+ color: #ef4444;
279
+ }
280
+ }
281
+
282
+ mat-form-field {
283
+ ::ng-deep .mat-mdc-form-field-subscript-wrapper {
284
+ display: none;
285
+ }
286
+ }
287
+ }
288
+ }
289
+
290
+ .nested-group .condition-row .condition-inputs {
291
+ background: #f8fafc;
292
+ }
293
+
294
+ .field-type {
295
+ font-size: 11px;
296
+ color: #94a3b8;
297
+ margin-left: 4px;
298
+ }
299
+
300
+ .empty-group {
301
+ display: flex;
302
+ align-items: center;
303
+ gap: 8px;
304
+ padding: 16px;
305
+ background: #fef3c7;
306
+ border-radius: 8px;
307
+ color: #92400e;
308
+ font-size: 13px;
309
+
310
+ mat-icon {
311
+ font-size: 20px;
312
+ width: 20px;
313
+ height: 20px;
314
+ }
315
+ }
316
+
317
+ .group-actions {
318
+ display: flex;
319
+ gap: 8px;
320
+ margin-top: 12px;
321
+ padding-top: 12px;
322
+ border-top: 1px solid #e2e8f0;
323
+ }
324
+
325
+ .add-condition-btn {
326
+ color: #8b5cf6;
327
+ border-color: #8b5cf6;
328
+ font-size: 12px;
329
+
330
+ mat-icon {
331
+ font-size: 16px;
332
+ width: 16px;
333
+ height: 16px;
334
+ }
335
+ }
336
+
337
+ .add-group-btn {
338
+ color: #6366f1;
339
+ border-color: #6366f1;
340
+ font-size: 12px;
341
+
342
+ mat-icon {
343
+ font-size: 16px;
344
+ width: 16px;
345
+ height: 16px;
346
+ }
347
+ }
348
+
349
+ .modal-footer {
350
+ display: flex;
351
+ justify-content: flex-end;
352
+ gap: 8px;
353
+ padding: 16px 20px;
354
+ border-top: 1px solid #e2e8f0;
355
+ background: #f8fafc;
356
+ flex-shrink: 0;
357
+ }