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,228 @@
1
+ import { Injectable, inject } from '@angular/core';
2
+ import { SchemaParserService, SchemaDocument, ModelRegistry } from '@expeed/ngx-data-mapper';
3
+
4
+ @Injectable({
5
+ providedIn: 'root',
6
+ })
7
+ export class SampleDataService {
8
+ private schemaParser = inject(SchemaParserService);
9
+
10
+ constructor() {
11
+ // Register models on service initialization
12
+ this.schemaParser.registerModels(this.getModelDefinitions());
13
+ }
14
+
15
+ // Model definitions registry - these could come from an API
16
+ getModelDefinitions(): ModelRegistry {
17
+ return {
18
+ Customer: {
19
+ type: 'object',
20
+ properties: {
21
+ id: { type: 'string', description: 'Unique identifier' },
22
+ firstName: { type: 'string', description: 'Customer first name' },
23
+ lastName: { type: 'string', description: 'Customer last name' },
24
+ middleName: { type: 'string', description: 'Customer middle name' },
25
+ prefix: { type: 'string', description: 'Name prefix (Mr, Mrs, Dr)' },
26
+ suffix: { type: 'string', description: 'Name suffix (Jr, Sr, III)' },
27
+ email: { type: 'string', description: 'Email address' },
28
+ secondaryEmail: { type: 'string', description: 'Secondary email' },
29
+ phone: { type: 'string', description: 'Phone number with area code' },
30
+ mobilePhone: { type: 'string', description: 'Mobile phone number' },
31
+ workPhone: { type: 'string', description: 'Work phone number' },
32
+ fax: { type: 'string', description: 'Fax number' },
33
+ birthDate: { type: 'string', format: 'date', description: 'Date of birth' },
34
+ gender: { type: 'string', description: 'Gender' },
35
+ nationality: { type: 'string', description: 'Nationality' },
36
+ language: { type: 'string', description: 'Preferred language' },
37
+ timezone: { type: 'string', description: 'Timezone' },
38
+ createdAt: { type: 'string', format: 'date-time', description: 'Record creation timestamp' },
39
+ updatedAt: { type: 'string', format: 'date-time', description: 'Last update timestamp' },
40
+ addresses: {
41
+ type: 'array',
42
+ description: 'List of customer addresses',
43
+ items: {
44
+ type: 'object',
45
+ properties: {
46
+ street: { type: 'string', description: 'Street address' },
47
+ street2: { type: 'string', description: 'Street address line 2' },
48
+ city: { type: 'string', description: 'City name' },
49
+ state: { type: 'string', description: 'State code' },
50
+ zipCode: { type: 'string', description: 'Postal code' },
51
+ country: { type: 'string', description: 'Country code' },
52
+ isPrimary: { type: 'boolean', description: 'Primary address flag' },
53
+ addressType: { type: 'string', description: 'Type (home, work, billing)' },
54
+ },
55
+ },
56
+ },
57
+ accountBalance: { type: 'number', description: 'Current account balance' },
58
+ creditLimit: { type: 'number', description: 'Credit limit' },
59
+ loyaltyPoints: { type: 'number', description: 'Loyalty points balance' },
60
+ isActive: { type: 'boolean', description: 'Account active status' },
61
+ isVerified: { type: 'boolean', description: 'Email verified status' },
62
+ marketingOptIn: { type: 'boolean', description: 'Marketing opt-in' },
63
+ tags: {
64
+ type: 'array',
65
+ items: { type: 'string' },
66
+ description: 'Customer tags',
67
+ },
68
+ notes: { type: 'string', description: 'Internal notes' },
69
+ referralCode: { type: 'string', description: 'Referral code' },
70
+ referredBy: { type: 'string', description: 'Referred by customer ID' },
71
+ },
72
+ },
73
+ UserProfile: {
74
+ type: 'object',
75
+ properties: {
76
+ id: { type: 'string' },
77
+ fullName: { type: 'string', description: 'Full display name' },
78
+ displayName: { type: 'string', description: 'Public display name' },
79
+ initials: { type: 'string', description: 'Name initials' },
80
+ emailAddress: { type: 'string' },
81
+ alternateEmail: { type: 'string', description: 'Alternate email' },
82
+ areaCode: { type: 'string', description: 'Phone area code only' },
83
+ phoneNumber: { type: 'string' },
84
+ mobileNumber: { type: 'string' },
85
+ birthYear: { type: 'string' },
86
+ birthMonth: { type: 'string' },
87
+ age: { type: 'number', description: 'Calculated age' },
88
+ createdAt: { type: 'string', format: 'date-time' },
89
+ lastLoginAt: { type: 'string', format: 'date-time' },
90
+ preferredLanguage: { type: 'string' },
91
+ timezoneName: { type: 'string' },
92
+ locations: {
93
+ type: 'array',
94
+ description: 'List of user locations',
95
+ items: {
96
+ type: 'object',
97
+ properties: {
98
+ fullAddress: { type: 'string', description: 'Combined street and city' },
99
+ streetLine1: { type: 'string' },
100
+ streetLine2: { type: 'string' },
101
+ region: { type: 'string', description: 'State or region' },
102
+ postalCode: { type: 'string', description: 'Postal code' },
103
+ countryCode: { type: 'string' },
104
+ isDefault: { type: 'boolean', description: 'Default location flag' },
105
+ locationType: { type: 'string' },
106
+ },
107
+ },
108
+ },
109
+ location: {
110
+ type: 'object',
111
+ properties: {
112
+ fullAddress: { type: 'string', description: 'Combined street and city' },
113
+ region: { type: 'string' },
114
+ postalCode: { type: 'string' },
115
+ country: { type: 'string' },
116
+ },
117
+ },
118
+ balance: { type: 'string', description: 'Formatted balance with currency' },
119
+ availableCredit: { type: 'string' },
120
+ rewardPoints: { type: 'number' },
121
+ status: { type: 'string' },
122
+ accountType: { type: 'string' },
123
+ memberSince: { type: 'string' },
124
+ isEmailVerified: { type: 'boolean' },
125
+ isMobileVerified: { type: 'boolean' },
126
+ hasOptedInMarketing: { type: 'boolean' },
127
+ profileImageUrl: { type: 'string' },
128
+ bio: { type: 'string' },
129
+ },
130
+ },
131
+ Order: {
132
+ type: 'object',
133
+ properties: {
134
+ id: { type: 'string' },
135
+ orderNumber: { type: 'string' },
136
+ customerId: { type: 'string' },
137
+ items: {
138
+ type: 'array',
139
+ items: {
140
+ type: 'object',
141
+ properties: {
142
+ productId: { type: 'string' },
143
+ productName: { type: 'string' },
144
+ quantity: { type: 'integer' },
145
+ unitPrice: { type: 'number' },
146
+ },
147
+ },
148
+ },
149
+ totalAmount: { type: 'number' },
150
+ status: { type: 'string' },
151
+ createdAt: { type: 'string', format: 'date-time' },
152
+ },
153
+ },
154
+ };
155
+ }
156
+
157
+ getSourceSchema(): SchemaDocument {
158
+ // Using object format with $ref and exclude
159
+ return {
160
+ $ref: '#Customer',
161
+ exclude: ['id', 'createdAt', 'updatedAt', 'tags'],
162
+ title: 'Customer',
163
+ };
164
+ }
165
+
166
+ getTargetSchema(): SchemaDocument {
167
+ // Using object format with $ref and exclude
168
+ return {
169
+ $ref: '#UserProfile',
170
+ exclude: ['id', 'createdAt'],
171
+ title: 'User Profile',
172
+ };
173
+ }
174
+
175
+ // Alternative: Get schema with inline definitions
176
+ getSchemaWithInlineDefinitions(): SchemaDocument {
177
+ return {
178
+ $ref: '#/definitions/Order',
179
+ definitions: {
180
+ Order: {
181
+ type: 'object',
182
+ properties: {
183
+ orderId: { type: 'string' },
184
+ customer: { $ref: '#/definitions/CustomerRef' },
185
+ total: { type: 'number' },
186
+ },
187
+ },
188
+ CustomerRef: {
189
+ type: 'object',
190
+ properties: {
191
+ id: { type: 'string' },
192
+ name: { type: 'string' },
193
+ },
194
+ },
195
+ },
196
+ };
197
+ }
198
+
199
+ getSampleData(): Record<string, unknown> {
200
+ return {
201
+ firstName: 'John',
202
+ lastName: 'Smith',
203
+ email: 'john.smith@example.com',
204
+ phone: '555-123-4567',
205
+ birthDate: '1990-05-15',
206
+ address: {
207
+ street: '123 Main Street',
208
+ city: 'New York',
209
+ state: 'NY',
210
+ zipCode: '10001',
211
+ country: 'US',
212
+ },
213
+ accountBalance: 1234.56,
214
+ isActive: true,
215
+ };
216
+ }
217
+
218
+ // Method to parse a schema document
219
+ parseSchema(schema: SchemaDocument, name: string): SchemaDocument {
220
+ // Just return the schema document - data-mapper handles conversion
221
+ return { ...schema, title: name };
222
+ }
223
+
224
+ // Method to register additional models
225
+ registerModels(models: ModelRegistry): void {
226
+ this.schemaParser.registerModels(models);
227
+ }
228
+ }
@@ -0,0 +1,15 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>DataMapperApp</title>
6
+ <base href="/">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ <link rel="icon" type="image/x-icon" href="favicon.ico">
9
+ <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
10
+ <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
11
+ </head>
12
+ <body>
13
+ <app-root></app-root>
14
+ </body>
15
+ </html>
@@ -0,0 +1,6 @@
1
+ import { bootstrapApplication } from '@angular/platform-browser';
2
+ import { appConfig } from './app/app.config';
3
+ import { App } from './app/app';
4
+
5
+ bootstrapApplication(App, appConfig)
6
+ .catch((err) => console.error(err));
@@ -0,0 +1,54 @@
1
+
2
+ // Include theming for Angular Material with `mat.theme()`.
3
+ // This Sass mixin will define CSS variables that are used for styling Angular Material
4
+ // components according to the Material 3 design spec.
5
+ // Learn more about theming and how to use it for your application's
6
+ // custom components at https://material.angular.dev/guide/theming
7
+ @use '@angular/material' as mat;
8
+
9
+ html {
10
+ height: 100%;
11
+ @include mat.theme((
12
+ color: (
13
+ primary: mat.$azure-palette,
14
+ tertiary: mat.$blue-palette,
15
+ ),
16
+ typography: Roboto,
17
+ density: 0,
18
+ ));
19
+ }
20
+
21
+ body {
22
+ // Default the application to a light color theme. This can be changed to
23
+ // `dark` to enable the dark color theme, or to `light dark` to defer to the
24
+ // user's system settings.
25
+ color-scheme: light;
26
+
27
+ // Set a default background, font and text colors for the application using
28
+ // Angular Material's system-level CSS variables. Learn more about these
29
+ // variables at https://material.angular.dev/guide/system-variables
30
+ background-color: var(--mat-sys-surface);
31
+ color: var(--mat-sys-on-surface);
32
+ font: var(--mat-sys-body-medium);
33
+
34
+ // Reset the user agent margin.
35
+ margin: 0;
36
+ height: 100%;
37
+ }
38
+ /* You can add global styles to this file, and also import other style files */
39
+
40
+ // Smaller dropdown options for type selector
41
+ .mat-mdc-select-panel {
42
+ .mat-mdc-option {
43
+ font-size: 12px !important;
44
+ min-height: 36px !important;
45
+ padding: 0 12px !important;
46
+
47
+ .mat-icon {
48
+ font-size: 16px !important;
49
+ width: 16px !important;
50
+ height: 16px !important;
51
+ margin-right: 8px !important;
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../../out-tsc/app",
5
+ "types": []
6
+ },
7
+ "files": [
8
+ "src/main.ts"
9
+ ],
10
+ "include": [
11
+ "src/**/*.d.ts"
12
+ ]
13
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3
+ "dest": "../../dist/ngx-data-mapper",
4
+ "lib": {
5
+ "entryFile": "src/public-api.ts"
6
+ }
7
+ }
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@expeed/ngx-data-mapper",
3
+ "version": "1.2.2",
4
+ "description": "Visual data mapping components for Angular - drag-and-drop field mapping with transformations, schema editor with JSON Schema export",
5
+ "keywords": [
6
+ "angular",
7
+ "data-mapper",
8
+ "field-mapping",
9
+ "schema-editor",
10
+ "json-schema",
11
+ "drag-drop",
12
+ "visual-mapping",
13
+ "etl",
14
+ "data-transformation"
15
+ ],
16
+ "license": "Apache-2.0",
17
+ "author": "Expeed Software",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/Expeed-Software/angular-data-mapper.git"
21
+ },
22
+ "homepage": "https://github.com/Expeed-Software/angular-data-mapper#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/Expeed-Software/angular-data-mapper/issues"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "peerDependencies": {
30
+ "@angular/common": "^21.0.0",
31
+ "@angular/core": "^21.0.0",
32
+ "@angular/forms": "^21.0.0",
33
+ "@angular/cdk": "^21.0.0",
34
+ "@angular/material": "^21.0.0"
35
+ },
36
+ "dependencies": {
37
+ "tslib": "^2.3.0"
38
+ },
39
+ "sideEffects": false
40
+ }
@@ -0,0 +1,183 @@
1
+ <div class="modal-backdrop" (click)="onBackdropClick($event)">
2
+ <div class="filter-modal">
3
+ <div class="modal-header">
4
+ <div class="header-title">
5
+ <mat-icon>filter_list</mat-icon>
6
+ <span>Array Filter</span>
7
+ </div>
8
+ <span class="array-path">{{ arrayMapping.sourceArray.name }}[] &rarr; {{ arrayMapping.targetArray.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
+ <!-- Filter mode selection -->
16
+ <div class="filter-mode">
17
+ <mat-radio-group [value]="filterEnabled()" (change)="filterEnabled.set($event.value)">
18
+ <mat-radio-button [value]="false" class="mode-option">
19
+ <div class="mode-content">
20
+ <mat-icon>select_all</mat-icon>
21
+ <div class="mode-text">
22
+ <span class="mode-label">No filter</span>
23
+ <span class="mode-desc">Map all records from source to target</span>
24
+ </div>
25
+ </div>
26
+ </mat-radio-button>
27
+ <mat-radio-button [value]="true" class="mode-option">
28
+ <div class="mode-content">
29
+ <mat-icon>filter_alt</mat-icon>
30
+ <div class="mode-text">
31
+ <span class="mode-label">Filter records</span>
32
+ <span class="mode-desc">Only map records matching conditions</span>
33
+ </div>
34
+ </div>
35
+ </mat-radio-button>
36
+ </mat-radio-group>
37
+ </div>
38
+
39
+ <!-- Conditions builder (only when filter enabled) -->
40
+ @if (filterEnabled()) {
41
+ <mat-divider></mat-divider>
42
+
43
+ <div class="conditions-section">
44
+ <!-- Root group -->
45
+ <ng-container *ngTemplateOutlet="groupTemplate; context: { group: rootGroup(), isRoot: true }"></ng-container>
46
+ </div>
47
+ }
48
+ </div>
49
+
50
+ <div class="modal-footer">
51
+ <button mat-button (click)="onClose()">Cancel</button>
52
+ <button mat-flat-button color="primary" (click)="onSave()">
53
+ Apply
54
+ </button>
55
+ </div>
56
+ </div>
57
+ </div>
58
+
59
+ <!-- Recursive group template -->
60
+ <ng-template #groupTemplate let-group="group" let-isRoot="isRoot" let-parentGroup="parentGroup">
61
+ <div class="filter-group" [class.root-group]="isRoot" [class.nested-group]="!isRoot">
62
+ <!-- Group header with logic toggle -->
63
+ <div class="group-header">
64
+ <div class="logic-toggle">
65
+ <span class="logic-label">Match</span>
66
+ <mat-radio-group [value]="group.logic" (change)="onLogicChange(group, $event.value)">
67
+ <mat-radio-button value="and">ALL (AND)</mat-radio-button>
68
+ <mat-radio-button value="or">ANY (OR)</mat-radio-button>
69
+ </mat-radio-group>
70
+ </div>
71
+ @if (!isRoot) {
72
+ <button mat-icon-button class="remove-group-btn" matTooltip="Remove group" (click)="removeItem(parentGroup, group.id)">
73
+ <mat-icon>close</mat-icon>
74
+ </button>
75
+ }
76
+ </div>
77
+
78
+ <!-- Group children -->
79
+ <div class="group-children">
80
+ @for (item of group.children; track item.id; let i = $index) {
81
+ <!-- Logic connector between items -->
82
+ @if (i > 0) {
83
+ <div class="logic-connector">
84
+ <span class="logic-badge" [class.and]="group.logic === 'and'" [class.or]="group.logic === 'or'">
85
+ {{ group.logic | uppercase }}
86
+ </span>
87
+ </div>
88
+ }
89
+
90
+ @if (isCondition(item)) {
91
+ <!-- Condition row -->
92
+ <div class="condition-row">
93
+ <div class="condition-inputs">
94
+ <!-- Field selector -->
95
+ <mat-form-field appearance="outline" class="field-select">
96
+ <mat-label>Field</mat-label>
97
+ <mat-select [value]="item.field" (selectionChange)="onFieldChange(item, $event.value)">
98
+ @for (field of availableFields(); track field.path) {
99
+ <mat-option [value]="field.path">
100
+ {{ field.name }}
101
+ <span class="field-type">({{ field.type }})</span>
102
+ </mat-option>
103
+ }
104
+ </mat-select>
105
+ </mat-form-field>
106
+
107
+ <!-- Operator selector -->
108
+ <mat-form-field appearance="outline" class="operator-select">
109
+ <mat-label>Operator</mat-label>
110
+ <mat-select [value]="item.operator" (selectionChange)="onOperatorChange(item, $event.value)">
111
+ @for (op of getOperatorsForField(item.field); track op.value) {
112
+ <mat-option [value]="op.value">{{ op.label }}</mat-option>
113
+ }
114
+ </mat-select>
115
+ </mat-form-field>
116
+
117
+ <!-- Value input (only if operator needs value) -->
118
+ @if (operatorNeedsValue(item.operator)) {
119
+ @if (item.valueType === 'boolean') {
120
+ <mat-slide-toggle
121
+ [checked]="item.value === true"
122
+ (change)="onValueChange(item, $event.checked)"
123
+ class="bool-toggle"
124
+ >
125
+ {{ item.value ? 'true' : 'false' }}
126
+ </mat-slide-toggle>
127
+ } @else if (item.valueType === 'number') {
128
+ <mat-form-field appearance="outline" class="value-input">
129
+ <mat-label>Value</mat-label>
130
+ <input
131
+ matInput
132
+ type="number"
133
+ [value]="item.value"
134
+ (input)="onValueChange(item, $any($event.target).value)"
135
+ />
136
+ </mat-form-field>
137
+ } @else {
138
+ <mat-form-field appearance="outline" class="value-input">
139
+ <mat-label>Value</mat-label>
140
+ <input
141
+ matInput
142
+ type="text"
143
+ [value]="item.value"
144
+ (input)="onValueChange(item, $any($event.target).value)"
145
+ />
146
+ </mat-form-field>
147
+ }
148
+ }
149
+
150
+ <!-- Remove condition button -->
151
+ <button mat-icon-button class="remove-btn" matTooltip="Remove condition" (click)="removeItem(group, item.id)">
152
+ <mat-icon>close</mat-icon>
153
+ </button>
154
+ </div>
155
+ </div>
156
+ } @else if (isGroup(item)) {
157
+ <!-- Nested group -->
158
+ <ng-container *ngTemplateOutlet="groupTemplate; context: { group: item, isRoot: false, parentGroup: group }"></ng-container>
159
+ }
160
+ }
161
+
162
+ <!-- Empty state -->
163
+ @if (group.children.length === 0) {
164
+ <div class="empty-group">
165
+ <mat-icon>info_outline</mat-icon>
166
+ <span>No conditions. Add a condition or group.</span>
167
+ </div>
168
+ }
169
+ </div>
170
+
171
+ <!-- Group actions -->
172
+ <div class="group-actions">
173
+ <button mat-stroked-button class="add-condition-btn" (click)="addCondition(group)">
174
+ <mat-icon>add</mat-icon>
175
+ Add Condition
176
+ </button>
177
+ <button mat-stroked-button class="add-group-btn" (click)="addGroup(group)">
178
+ <mat-icon>folder_open</mat-icon>
179
+ Add Group
180
+ </button>
181
+ </div>
182
+ </div>
183
+ </ng-template>