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,352 @@
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
+ .filter-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: 600px;
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, #f59e0b 0%, #d97706 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
+ .array-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
+ .filter-mode {
71
+ mat-radio-group {
72
+ display: flex;
73
+ flex-direction: column;
74
+ gap: 12px;
75
+ }
76
+
77
+ .mode-option {
78
+ ::ng-deep .mdc-form-field {
79
+ align-items: flex-start;
80
+ }
81
+
82
+ ::ng-deep .mdc-radio {
83
+ margin-top: 4px;
84
+ }
85
+ }
86
+
87
+ .mode-content {
88
+ display: flex;
89
+ align-items: flex-start;
90
+ gap: 12px;
91
+ padding: 12px 16px;
92
+ border-radius: 8px;
93
+ background: #f8fafc;
94
+ border: 1px solid #e2e8f0;
95
+ transition: all 0.2s ease;
96
+ cursor: pointer;
97
+
98
+ &:hover {
99
+ background: #f1f5f9;
100
+ border-color: #cbd5e1;
101
+ }
102
+
103
+ mat-icon {
104
+ color: #64748b;
105
+ margin-top: 2px;
106
+ }
107
+ }
108
+
109
+ .mode-text {
110
+ display: flex;
111
+ flex-direction: column;
112
+ gap: 2px;
113
+ }
114
+
115
+ .mode-label {
116
+ font-size: 14px;
117
+ font-weight: 500;
118
+ color: #1e293b;
119
+ }
120
+
121
+ .mode-desc {
122
+ font-size: 12px;
123
+ color: #64748b;
124
+ }
125
+
126
+ mat-radio-button.mat-mdc-radio-checked {
127
+ .mode-content {
128
+ background: #fffbeb;
129
+ border-color: #f59e0b;
130
+
131
+ mat-icon {
132
+ color: #f59e0b;
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ mat-divider {
139
+ margin: 20px 0;
140
+ }
141
+
142
+ .conditions-section {
143
+ display: flex;
144
+ flex-direction: column;
145
+ gap: 16px;
146
+ }
147
+
148
+ // Group styles
149
+ .filter-group {
150
+ border-radius: 8px;
151
+
152
+ &.root-group {
153
+ background: #f8fafc;
154
+ border: 1px solid #e2e8f0;
155
+ padding: 16px;
156
+ }
157
+
158
+ &.nested-group {
159
+ background: white;
160
+ border: 2px dashed #cbd5e1;
161
+ padding: 12px;
162
+ margin-top: 8px;
163
+ }
164
+ }
165
+
166
+ .group-header {
167
+ display: flex;
168
+ align-items: center;
169
+ justify-content: space-between;
170
+ margin-bottom: 12px;
171
+ padding-bottom: 12px;
172
+ border-bottom: 1px solid #e2e8f0;
173
+ }
174
+
175
+ .logic-toggle {
176
+ display: flex;
177
+ align-items: center;
178
+ gap: 12px;
179
+
180
+ .logic-label {
181
+ font-size: 13px;
182
+ font-weight: 500;
183
+ color: #475569;
184
+ }
185
+
186
+ mat-radio-group {
187
+ display: flex;
188
+ gap: 12px;
189
+ }
190
+
191
+ mat-radio-button {
192
+ font-size: 12px;
193
+
194
+ ::ng-deep .mdc-label {
195
+ font-size: 12px;
196
+ }
197
+ }
198
+ }
199
+
200
+ .remove-group-btn {
201
+ color: #94a3b8;
202
+
203
+ &:hover {
204
+ color: #ef4444;
205
+ }
206
+ }
207
+
208
+ .group-children {
209
+ display: flex;
210
+ flex-direction: column;
211
+ }
212
+
213
+ .logic-connector {
214
+ display: flex;
215
+ align-items: center;
216
+ justify-content: center;
217
+ padding: 8px 0;
218
+
219
+ .logic-badge {
220
+ font-size: 10px;
221
+ font-weight: 700;
222
+ padding: 3px 10px;
223
+ border-radius: 12px;
224
+ letter-spacing: 0.5px;
225
+
226
+ &.and {
227
+ background: #dbeafe;
228
+ color: #1d4ed8;
229
+ }
230
+
231
+ &.or {
232
+ background: #fef3c7;
233
+ color: #b45309;
234
+ }
235
+ }
236
+ }
237
+
238
+ .condition-row {
239
+ .condition-inputs {
240
+ display: flex;
241
+ align-items: flex-start;
242
+ gap: 8px;
243
+ padding: 12px;
244
+ background: white;
245
+ border: 1px solid #e2e8f0;
246
+ border-radius: 8px;
247
+
248
+ .field-select {
249
+ flex: 1;
250
+ min-width: 120px;
251
+ }
252
+
253
+ .operator-select {
254
+ flex: 1;
255
+ min-width: 130px;
256
+ }
257
+
258
+ .value-input {
259
+ flex: 1;
260
+ min-width: 100px;
261
+ }
262
+
263
+ .bool-toggle {
264
+ padding-top: 12px;
265
+ min-width: 80px;
266
+ }
267
+
268
+ .remove-btn {
269
+ color: #94a3b8;
270
+ align-self: center;
271
+
272
+ &:hover {
273
+ color: #ef4444;
274
+ }
275
+ }
276
+
277
+ mat-form-field {
278
+ ::ng-deep .mat-mdc-form-field-subscript-wrapper {
279
+ display: none;
280
+ }
281
+ }
282
+ }
283
+ }
284
+
285
+ .nested-group .condition-row .condition-inputs {
286
+ background: #f8fafc;
287
+ }
288
+
289
+ .field-type {
290
+ font-size: 11px;
291
+ color: #94a3b8;
292
+ margin-left: 4px;
293
+ }
294
+
295
+ .empty-group {
296
+ display: flex;
297
+ align-items: center;
298
+ gap: 8px;
299
+ padding: 16px;
300
+ background: #fef3c7;
301
+ border-radius: 8px;
302
+ color: #92400e;
303
+ font-size: 13px;
304
+
305
+ mat-icon {
306
+ font-size: 20px;
307
+ width: 20px;
308
+ height: 20px;
309
+ }
310
+ }
311
+
312
+ .group-actions {
313
+ display: flex;
314
+ gap: 8px;
315
+ margin-top: 12px;
316
+ padding-top: 12px;
317
+ border-top: 1px solid #e2e8f0;
318
+ }
319
+
320
+ .add-condition-btn {
321
+ color: #f59e0b;
322
+ border-color: #f59e0b;
323
+ font-size: 12px;
324
+
325
+ mat-icon {
326
+ font-size: 16px;
327
+ width: 16px;
328
+ height: 16px;
329
+ }
330
+ }
331
+
332
+ .add-group-btn {
333
+ color: #6366f1;
334
+ border-color: #6366f1;
335
+ font-size: 12px;
336
+
337
+ mat-icon {
338
+ font-size: 16px;
339
+ width: 16px;
340
+ height: 16px;
341
+ }
342
+ }
343
+
344
+ .modal-footer {
345
+ display: flex;
346
+ justify-content: flex-end;
347
+ gap: 8px;
348
+ padding: 16px 20px;
349
+ border-top: 1px solid #e2e8f0;
350
+ background: #f8fafc;
351
+ flex-shrink: 0;
352
+ }
@@ -0,0 +1,277 @@
1
+ import { Component, Input, Output, EventEmitter, computed, signal, OnInit } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { FormsModule } from '@angular/forms';
4
+ import { MatButtonModule } from '@angular/material/button';
5
+ import { MatIconModule } from '@angular/material/icon';
6
+ import { MatSelectModule } from '@angular/material/select';
7
+ import { MatInputModule } from '@angular/material/input';
8
+ import { MatFormFieldModule } from '@angular/material/form-field';
9
+ import { MatRadioModule } from '@angular/material/radio';
10
+ import { MatSlideToggleModule } from '@angular/material/slide-toggle';
11
+ import { MatTooltipModule } from '@angular/material/tooltip';
12
+ import { MatDividerModule } from '@angular/material/divider';
13
+ import {
14
+ ArrayMapping,
15
+ ArrayFilterConfig,
16
+ FilterCondition,
17
+ FilterGroup,
18
+ FilterItem,
19
+ FilterOperator,
20
+ SchemaField,
21
+ } from '../../models/schema.model';
22
+
23
+ interface OperatorOption {
24
+ value: FilterOperator;
25
+ label: string;
26
+ needsValue: boolean;
27
+ }
28
+
29
+ @Component({
30
+ selector: 'array-filter-modal',
31
+ standalone: true,
32
+ imports: [
33
+ CommonModule,
34
+ FormsModule,
35
+ MatButtonModule,
36
+ MatIconModule,
37
+ MatSelectModule,
38
+ MatInputModule,
39
+ MatFormFieldModule,
40
+ MatRadioModule,
41
+ MatSlideToggleModule,
42
+ MatTooltipModule,
43
+ MatDividerModule,
44
+ ],
45
+ templateUrl: './array-filter-modal.component.html',
46
+ styleUrl: './array-filter-modal.component.scss',
47
+ })
48
+ export class ArrayFilterModalComponent implements OnInit {
49
+ @Input({ required: true }) arrayMapping!: ArrayMapping;
50
+ @Output() save = new EventEmitter<ArrayFilterConfig | undefined>();
51
+ @Output() close = new EventEmitter<void>();
52
+
53
+ filterEnabled = signal(false);
54
+ rootGroup = signal<FilterGroup>(this.createEmptyGroup());
55
+
56
+ // Available fields from the source array's children
57
+ availableFields = computed(() => {
58
+ const fields: { path: string; name: string; type: string }[] = [];
59
+ this.collectFields(this.arrayMapping.sourceArray.children || [], '', fields);
60
+ return fields;
61
+ });
62
+
63
+ // Operators by type
64
+ stringOperators: OperatorOption[] = [
65
+ { value: 'equals', label: 'equals', needsValue: true },
66
+ { value: 'notEquals', label: 'not equals', needsValue: true },
67
+ { value: 'contains', label: 'contains', needsValue: true },
68
+ { value: 'notContains', label: 'does not contain', needsValue: true },
69
+ { value: 'startsWith', label: 'starts with', needsValue: true },
70
+ { value: 'endsWith', label: 'ends with', needsValue: true },
71
+ { value: 'isEmpty', label: 'is empty', needsValue: false },
72
+ { value: 'isNotEmpty', label: 'is not empty', needsValue: false },
73
+ ];
74
+
75
+ numberOperators: OperatorOption[] = [
76
+ { value: 'equals', label: 'equals', needsValue: true },
77
+ { value: 'notEquals', label: 'not equals', needsValue: true },
78
+ { value: 'greaterThan', label: 'greater than', needsValue: true },
79
+ { value: 'lessThan', label: 'less than', needsValue: true },
80
+ { value: 'greaterThanOrEqual', label: 'greater or equal', needsValue: true },
81
+ { value: 'lessThanOrEqual', label: 'less or equal', needsValue: true },
82
+ ];
83
+
84
+ booleanOperators: OperatorOption[] = [
85
+ { value: 'isTrue', label: 'is true', needsValue: false },
86
+ { value: 'isFalse', label: 'is false', needsValue: false },
87
+ ];
88
+
89
+ ngOnInit(): void {
90
+ // Initialize from existing filter config
91
+ if (this.arrayMapping.filter?.enabled && this.arrayMapping.filter.root) {
92
+ this.filterEnabled.set(true);
93
+ this.rootGroup.set(this.cloneGroup(this.arrayMapping.filter.root));
94
+ }
95
+ }
96
+
97
+ private createEmptyGroup(): FilterGroup {
98
+ return {
99
+ id: this.generateId(),
100
+ type: 'group',
101
+ logic: 'and',
102
+ children: [],
103
+ };
104
+ }
105
+
106
+ private generateId(): string {
107
+ return `filter-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
108
+ }
109
+
110
+ private cloneGroup(group: FilterGroup): FilterGroup {
111
+ return {
112
+ ...group,
113
+ children: group.children.map((child) =>
114
+ child.type === 'group' ? this.cloneGroup(child) : { ...child }
115
+ ),
116
+ };
117
+ }
118
+
119
+ private collectFields(
120
+ fields: SchemaField[],
121
+ prefix: string,
122
+ result: { path: string; name: string; type: string }[]
123
+ ): void {
124
+ for (const field of fields) {
125
+ const path = prefix ? `${prefix}.${field.name}` : field.name;
126
+ if (field.type !== 'object' && field.type !== 'array') {
127
+ result.push({ path, name: field.name, type: field.type });
128
+ }
129
+ if (field.children) {
130
+ this.collectFields(field.children, path, result);
131
+ }
132
+ }
133
+ }
134
+
135
+ getOperatorsForField(fieldPath: string): OperatorOption[] {
136
+ const field = this.availableFields().find((f) => f.path === fieldPath);
137
+ if (!field) return this.stringOperators;
138
+
139
+ switch (field.type) {
140
+ case 'number':
141
+ return this.numberOperators;
142
+ case 'boolean':
143
+ return this.booleanOperators;
144
+ default:
145
+ return this.stringOperators;
146
+ }
147
+ }
148
+
149
+ getFieldType(fieldPath: string): string {
150
+ const field = this.availableFields().find((f) => f.path === fieldPath);
151
+ return field?.type || 'string';
152
+ }
153
+
154
+ operatorNeedsValue(operator: FilterOperator): boolean {
155
+ const allOperators = [...this.stringOperators, ...this.numberOperators, ...this.booleanOperators];
156
+ const op = allOperators.find((o) => o.value === operator);
157
+ return op?.needsValue ?? true;
158
+ }
159
+
160
+ isCondition(item: FilterItem): item is FilterCondition {
161
+ return item.type === 'condition';
162
+ }
163
+
164
+ isGroup(item: FilterItem): item is FilterGroup {
165
+ return item.type === 'group';
166
+ }
167
+
168
+ addCondition(group: FilterGroup): void {
169
+ const fields = this.availableFields();
170
+ const firstField = fields[0];
171
+ const newCondition: FilterCondition = {
172
+ id: this.generateId(),
173
+ type: 'condition',
174
+ field: firstField?.path || '',
175
+ fieldName: firstField?.name || '',
176
+ operator: 'equals',
177
+ value: '',
178
+ valueType: (firstField?.type as 'string' | 'number' | 'boolean') || 'string',
179
+ };
180
+ group.children = [...group.children, newCondition];
181
+ this.triggerUpdate();
182
+ }
183
+
184
+ addGroup(parentGroup: FilterGroup): void {
185
+ const newGroup: FilterGroup = {
186
+ id: this.generateId(),
187
+ type: 'group',
188
+ logic: parentGroup.logic === 'and' ? 'or' : 'and', // Default to opposite logic
189
+ children: [],
190
+ };
191
+ parentGroup.children = [...parentGroup.children, newGroup];
192
+ this.triggerUpdate();
193
+ }
194
+
195
+ removeItem(parentGroup: FilterGroup, itemId: string): void {
196
+ parentGroup.children = parentGroup.children.filter((c) => c.id !== itemId);
197
+ this.triggerUpdate();
198
+ }
199
+
200
+ onFieldChange(condition: FilterCondition, fieldPath: string): void {
201
+ const field = this.availableFields().find((f) => f.path === fieldPath);
202
+ if (field) {
203
+ condition.field = fieldPath;
204
+ condition.fieldName = field.name;
205
+ condition.valueType = field.type as 'string' | 'number' | 'boolean';
206
+ // Reset operator to first valid option for new type
207
+ const operators = this.getOperatorsForField(fieldPath);
208
+ condition.operator = operators[0].value;
209
+ condition.value = '';
210
+ }
211
+ this.triggerUpdate();
212
+ }
213
+
214
+ onOperatorChange(condition: FilterCondition, operator: FilterOperator): void {
215
+ condition.operator = operator;
216
+ if (!this.operatorNeedsValue(operator)) {
217
+ condition.value = '';
218
+ }
219
+ this.triggerUpdate();
220
+ }
221
+
222
+ onValueChange(condition: FilterCondition, value: string | boolean): void {
223
+ if (condition.valueType === 'number') {
224
+ condition.value = parseFloat(value as string) || 0;
225
+ } else {
226
+ condition.value = value;
227
+ }
228
+ this.triggerUpdate();
229
+ }
230
+
231
+ onLogicChange(group: FilterGroup, logic: 'and' | 'or'): void {
232
+ group.logic = logic;
233
+ this.triggerUpdate();
234
+ }
235
+
236
+ private triggerUpdate(): void {
237
+ // Trigger signal update by creating a new reference
238
+ this.rootGroup.set(this.cloneGroup(this.rootGroup()));
239
+ }
240
+
241
+ hasConditions(group: FilterGroup): boolean {
242
+ return group.children.length > 0;
243
+ }
244
+
245
+ countConditions(group: FilterGroup): number {
246
+ let count = 0;
247
+ for (const child of group.children) {
248
+ if (child.type === 'condition') {
249
+ count++;
250
+ } else {
251
+ count += this.countConditions(child);
252
+ }
253
+ }
254
+ return count;
255
+ }
256
+
257
+ onSave(): void {
258
+ if (!this.filterEnabled()) {
259
+ this.save.emit(undefined);
260
+ } else {
261
+ this.save.emit({
262
+ enabled: true,
263
+ root: this.rootGroup(),
264
+ });
265
+ }
266
+ }
267
+
268
+ onClose(): void {
269
+ this.close.emit();
270
+ }
271
+
272
+ onBackdropClick(event: MouseEvent): void {
273
+ if ((event.target as HTMLElement).classList.contains('modal-backdrop')) {
274
+ this.onClose();
275
+ }
276
+ }
277
+ }