This is an automated email from the ASF dual-hosted git repository.

mfholz pushed a commit to branch add-reactive-forms
in repository https://gitbox.apache.org/repos/asf/streampipes.git

commit 7592ba63af09d95f02f84484e6540b9d404eee63
Author: Marcelfrueh <[email protected]>
AuthorDate: Tue May 20 13:06:48 2025 +0200

    feat: add reactive forms to Configuration/sites
---
 ui/deployment/i18n/de.json                         |  2 +
 ui/deployment/i18n/en.json                         |  2 +
 ui/src/app/configuration/configuration.module.ts   |  2 +
 .../edit-location-area.component.html              | 20 +++++++--
 .../edit-location-area.component.scss              | 11 +++++
 .../edit-location-area.component.ts                | 28 +++++++++++--
 .../edit-location/edit-location.component.html     | 12 ++++--
 .../edit-location/edit-location.component.ts       | 16 +++++++-
 .../manage-site/manage-site-dialog.component.html  |  2 +
 .../manage-site/manage-site-dialog.component.ts    | 13 +++++-
 .../single-marker-map.component.ts                 | 47 +++++++++++++++++++++-
 .../core-ui/static-properties/input.validator.ts   | 17 +++++++-
 12 files changed, 155 insertions(+), 17 deletions(-)

diff --git a/ui/deployment/i18n/de.json b/ui/deployment/i18n/de.json
index 5352dd885b..b1ef0972c8 100644
--- a/ui/deployment/i18n/de.json
+++ b/ui/deployment/i18n/de.json
@@ -287,6 +287,8 @@
   "Authorized Groups": "Autorisierte Gruppen",
   "Group selection": "Auswahl der Gruppe",
   "(no log messages available)": "(keine Protokollmeldungen verfügbar)",
+  "Site label is required": "Standort-Label ist erforderlich",
+  "This site already exists": "Standort existiert bereits",
   "success": "Erfolg",
   "error": "Fehler",
   "waiting": "Warten",
diff --git a/ui/deployment/i18n/en.json b/ui/deployment/i18n/en.json
index 9264e48626..06efeb24a0 100644
--- a/ui/deployment/i18n/en.json
+++ b/ui/deployment/i18n/en.json
@@ -287,6 +287,8 @@
   "Authorized Groups": null,
   "Group selection": null,
   "(no log messages available)": null,
+  "Site label is required": null,
+  "This site already exists": null,
   "success": null,
   "error": null,
   "waiting": null,
diff --git a/ui/src/app/configuration/configuration.module.ts 
b/ui/src/app/configuration/configuration.module.ts
index accdfdec37..3e2b9e9930 100644
--- a/ui/src/app/configuration/configuration.module.ts
+++ b/ui/src/app/configuration/configuration.module.ts
@@ -99,6 +99,7 @@ import { MatDialogModule } from '@angular/material/dialog';
 import { MatProgressBarModule } from '@angular/material/progress-bar';
 import { GenericStorageItemComponent } from 
'./export/export-dialog/generic-storage-items/generic-storage-item/generic-storage-item.component';
 import { GenericStorageItemsComponent } from 
'./export/export-dialog/generic-storage-items/generic-storage-items.component';
+import { TranslatePipe } from '@ngx-translate/core';
 
 @NgModule({
     imports: [
@@ -197,6 +198,7 @@ import { GenericStorageItemsComponent } from 
'./export/export-dialog/generic-sto
         MatSort,
         MatListModule,
         MatDialogModule,
+        TranslatePipe,
     ],
     declarations: [
         ServiceConfigsComponent,
diff --git 
a/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location-area/edit-location-area.component.html
 
b/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location-area/edit-location-area.component.html
index ad0392c3e0..b3dbc836fa 100644
--- 
a/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location-area/edit-location-area.component.html
+++ 
b/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location-area/edit-location-area.component.html
@@ -16,7 +16,12 @@
   ~
   -->
 
-<div fxLayout="column" class="w-100" fxLayoutGap="10px">
+<div
+    fxLayout="column"
+    class="w-100"
+    fxLayoutGap="10px"
+    [formGroup]="areaControlGroup"
+>
     <div
         *ngFor="let area of site.areas; let i = index"
         fxLayout="row"
@@ -50,16 +55,25 @@
                 <input
                     data-cy="sites-dialog-new-area-input"
                     matInput
-                    [(ngModel)]="newArea"
+                    formControlName="areaControl"
                 />
+                <mat-error
+                    *ngIf="
+                        areaControlGroup
+                            .get('areaControl')
+                            .hasError('forbiddenName')
+                    "
+                    >{{ 'This site already exists' | translate }}</mat-error
+                >
             </mat-form-field>
         </div>
         <div fxLayoutAlign="end center">
             <button
                 data-cy="sites-dialog-add-area-button"
                 mat-icon-button
-                color="accent"
+                class="custom-icon-button"
                 (click)="addNewArea()"
+                [disabled]="isAddAreaDisabled"
             >
                 <mat-icon>add</mat-icon>
             </button>
diff --git 
a/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location-area/edit-location-area.component.scss
 
b/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location-area/edit-location-area.component.scss
index d1743a813a..8edf5ad359 100644
--- 
a/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location-area/edit-location-area.component.scss
+++ 
b/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location-area/edit-location-area.component.scss
@@ -26,3 +26,14 @@
     padding: 5px;
     font-weight: bold;
 }
+
+.custom-icon-button {
+    margin-top: 1px;
+}
+
+.custom-icon-button:disabled {
+    color: grey;
+    opacity: 0.4;
+    cursor: not-allowed;
+    margin-top: 1px;
+}
diff --git 
a/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location-area/edit-location-area.component.ts
 
b/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location-area/edit-location-area.component.ts
index ddd7baab40..c0d5fb379e 100644
--- 
a/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location-area/edit-location-area.component.ts
+++ 
b/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location-area/edit-location-area.component.ts
@@ -16,25 +16,45 @@
  *
  */
 
-import { Component, Input } from '@angular/core';
+import { Component, Input, OnInit } from '@angular/core';
 import { AssetSiteDesc } from '@streampipes/platform-services';
+import { FormControl, FormGroup } from '@angular/forms';
+import { checkForDuplicatesValidator } from 
'../../../../../core-ui/static-properties/input.validator';
 
 @Component({
     selector: 'sp-edit-asset-location-area-component',
     templateUrl: './edit-location-area.component.html',
     styleUrls: ['./edit-location-area.component.scss'],
 })
-export class EditAssetLocationAreaComponent {
+export class EditAssetLocationAreaComponent implements OnInit {
     @Input()
     site: AssetSiteDesc;
 
-    newArea: string = '';
+    areaControlGroup: FormGroup;
+
+    ngOnInit(): void {
+        this.areaControlGroup = new FormGroup({
+            areaControl: new FormControl('', [
+                checkForDuplicatesValidator(() => this.site.areas),
+            ]),
+        });
+    }
 
     addNewArea(): void {
-        this.site.areas.push(this.newArea);
+        this.site.areas.push(this.areaControlGroup.get('areaControl').value);
+        this.areaControlGroup.get('areaControl').reset();
     }
 
     removeArea(area: string): void {
         this.site.areas.splice(this.site.areas.indexOf(area), 1);
     }
+
+    get isAddAreaDisabled(): boolean {
+        const value = this.areaControlGroup.get('areaControl')?.value;
+        const trimmedValue = value?.trim();
+        return (
+            !trimmedValue ||
+            this.areaControlGroup.get('areaControl').hasError('forbiddenName')
+        );
+    }
 }
diff --git 
a/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location.component.html
 
b/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location.component.html
index 0c24fba58f..a6f96db8c8 100644
--- 
a/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location.component.html
+++ 
b/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location.component.html
@@ -15,8 +15,7 @@
   ~ limitations under the License.
   ~
   -->
-
-<div fxLayout="column">
+<div fxLayout="column" [formGroup]="siteAreaControl">
     <sp-basic-field-description
         fxFlex="100"
         descriptionPanelWidth="30"
@@ -27,8 +26,14 @@
             <input
                 data-cy="sites-dialog-site-input"
                 matInput
-                [(ngModel)]="site.label"
+                formControlName="label"
+                placeholder="New site"
             />
+            <mat-error
+                *ngIf="siteAreaControl.get('label').hasError('required')"
+            >
+                {{ 'Site label is required' | translate }}
+            </mat-error>
         </mat-form-field>
     </sp-basic-field-description>
     <sp-basic-field-description
@@ -53,6 +58,7 @@
             fxFlex="100"
             [locationConfig]="locationConfig"
             [assetLocation]="site.location"
+            formControlName="location"
         >
         </sp-single-marker-map>
     </sp-basic-field-description>
diff --git 
a/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location.component.ts
 
b/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location.component.ts
index 9b1bc554c9..260adfc593 100644
--- 
a/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location.component.ts
+++ 
b/ui/src/app/configuration/dialog/manage-site/edit-location/edit-location.component.ts
@@ -16,17 +16,29 @@
  *
  */
 
-import { Component, Input } from '@angular/core';
+import { Component, Input, OnInit } from '@angular/core';
 import { AssetSiteDesc, LocationConfig } from '@streampipes/platform-services';
+import { FormControl, FormGroup, Validators } from '@angular/forms';
 
 @Component({
     selector: 'sp-edit-asset-location-component',
     templateUrl: './edit-location.component.html',
 })
-export class EditAssetLocationComponent {
+export class EditAssetLocationComponent implements OnInit {
     @Input()
     site: AssetSiteDesc;
 
     @Input()
     locationConfig: LocationConfig;
+
+    siteAreaControl: FormGroup;
+
+    ngOnInit() {
+        this.siteAreaControl = new FormGroup({
+            label: new FormControl(this.site.label || '', [
+                Validators.required,
+            ]),
+            location: new FormControl(this.site.location || null, []),
+        });
+    }
 }
diff --git 
a/ui/src/app/configuration/dialog/manage-site/manage-site-dialog.component.html 
b/ui/src/app/configuration/dialog/manage-site/manage-site-dialog.component.html
index 5cff55e084..47d8fa9b6d 100644
--- 
a/ui/src/app/configuration/dialog/manage-site/manage-site-dialog.component.html
+++ 
b/ui/src/app/configuration/dialog/manage-site/manage-site-dialog.component.html
@@ -20,6 +20,7 @@
     <div class="sp-dialog-content p-15">
         <div *ngIf="clonedSite !== undefined">
             <sp-edit-asset-location-component
+                #editLocation
                 [site]="clonedSite"
                 [locationConfig]="locationConfig"
             >
@@ -35,6 +36,7 @@
             color="accent"
             (click)="store()"
             style="margin-right: 10px"
+            [disabled]="editLocationComponent?.siteAreaControl.invalid"
         >
             Save changes
         </button>
diff --git 
a/ui/src/app/configuration/dialog/manage-site/manage-site-dialog.component.ts 
b/ui/src/app/configuration/dialog/manage-site/manage-site-dialog.component.ts
index 2a9b8b30a3..3846e7fd1b 100644
--- 
a/ui/src/app/configuration/dialog/manage-site/manage-site-dialog.component.ts
+++ 
b/ui/src/app/configuration/dialog/manage-site/manage-site-dialog.component.ts
@@ -16,7 +16,7 @@
  *
  */
 
-import { Component, Input, OnInit } from '@angular/core';
+import { Component, Input, OnInit, ViewChild } from '@angular/core';
 import { DialogRef } from '@streampipes/shared-ui';
 import {
     AssetConstants,
@@ -24,6 +24,7 @@ import {
     GenericStorageService,
     LocationConfig,
 } from '@streampipes/platform-services';
+import { EditAssetLocationComponent } from 
'./edit-location/edit-location.component';
 
 @Component({
     selector: 'sp-manage-site-dialog-component',
@@ -37,6 +38,9 @@ export class ManageSiteDialogComponent implements OnInit {
     @Input()
     locationConfig: LocationConfig;
 
+    @ViewChild('editLocation')
+    editLocationComponent: EditAssetLocationComponent;
+
     clonedSite: AssetSiteDesc;
     createMode = false;
 
@@ -61,7 +65,7 @@ export class ManageSiteDialogComponent implements OnInit {
         this.clonedSite = {
             appDocType: AssetConstants.ASSET_SITES_APP_DOC_NAME,
             _id: undefined,
-            label: 'New site',
+            label: '',
             location: { coordinates: { latitude: 0, longitude: 0 } },
             areas: [],
         };
@@ -69,6 +73,11 @@ export class ManageSiteDialogComponent implements OnInit {
     }
 
     store(): void {
+        const formData = this.editLocationComponent?.siteAreaControl;
+        const { label, location } = formData.value;
+        this.clonedSite.label = label;
+        this.clonedSite.location = location;
+
         const observable = this.createMode
             ? this.genericStorageService.createDocument(
                   AssetConstants.ASSET_SITES_APP_DOC_NAME,
diff --git 
a/ui/src/app/core-ui/single-marker-map/single-marker-map.component.ts 
b/ui/src/app/core-ui/single-marker-map/single-marker-map.component.ts
index 2336f51769..deb55ce41a 100644
--- a/ui/src/app/core-ui/single-marker-map/single-marker-map.component.ts
+++ b/ui/src/app/core-ui/single-marker-map/single-marker-map.component.ts
@@ -16,7 +16,7 @@
  *
  */
 
-import { Component, Input, OnInit } from '@angular/core';
+import { Component, forwardRef, Input, OnInit } from '@angular/core';
 import {
     icon,
     Layer,
@@ -32,12 +32,20 @@ import {
     LatLng,
     LocationConfig,
 } from '@streampipes/platform-services';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
 
 @Component({
     selector: 'sp-single-marker-map',
     templateUrl: './single-marker-map.component.html',
+    providers: [
+        {
+            provide: NG_VALUE_ACCESSOR,
+            useExisting: forwardRef(() => SingleMarkerMapComponent),
+            multi: true,
+        },
+    ],
 })
-export class SingleMarkerMapComponent implements OnInit {
+export class SingleMarkerMapComponent implements OnInit, ControlValueAccessor {
     @Input()
     locationConfig: LocationConfig;
 
@@ -101,6 +109,7 @@ export class SingleMarkerMapComponent implements OnInit {
 
     onZoomChange(zoom: number): void {
         this.assetLocation.zoom = zoom;
+        this.emitChange();
     }
 
     onMarkerAdded(e: LeafletMouseEvent) {
@@ -109,6 +118,7 @@ export class SingleMarkerMapComponent implements OnInit {
                 latitude: e.latlng.lat,
                 longitude: e.latlng.lng,
             });
+            this.emitChange();
         }
     }
 
@@ -126,4 +136,37 @@ export class SingleMarkerMapComponent implements OnInit {
             this.assetLocation.coordinates = location;
         }
     }
+
+    private onChange: (_: AssetLocation) => void = () => {};
+    private onTouched: () => void = () => {};
+
+    registerOnChange(fn: any): void {
+        this.onChange = fn;
+    }
+
+    registerOnTouched(fn: any): void {
+        this.onTouched = fn;
+    }
+
+    setDisabledState?(isDisabled: boolean): void {
+        this.readonly = isDisabled;
+    }
+
+    private emitChange() {
+        this.onChange(this.assetLocation);
+        this.onTouched();
+    }
+
+    writeValue(value: AssetLocation): void {
+        if (value) {
+            this.assetLocation = value;
+            if (this.map) {
+                this.addMarker(value.coordinates);
+                this.map.setView(
+                    [value.coordinates.latitude, value.coordinates.longitude],
+                    value.zoom || 1,
+                );
+            }
+        }
+    }
 }
diff --git a/ui/src/app/core-ui/static-properties/input.validator.ts 
b/ui/src/app/core-ui/static-properties/input.validator.ts
index ee493b944c..a728d4a52e 100644
--- a/ui/src/app/core-ui/static-properties/input.validator.ts
+++ b/ui/src/app/core-ui/static-properties/input.validator.ts
@@ -16,7 +16,7 @@
  *
  */
 
-import { AbstractControl } from '@angular/forms';
+import { AbstractControl, ValidationErrors, ValidatorFn } from 
'@angular/forms';
 
 export function ValidateUrl(control: AbstractControl) {
     if (control.value == null) {
@@ -49,3 +49,18 @@ export function ValidateString(control: AbstractControl) {
     }
     return null;
 }
+
+export function checkForDuplicatesValidator(
+    getExistingNames: () => string[],
+): ValidatorFn {
+    return (control: AbstractControl): ValidationErrors | null => {
+        if (!control.value || typeof control.value !== 'string') {
+            return null;
+        }
+        const existingNames = getExistingNames();
+
+        const isDuplicate = existingNames.includes(control.value);
+
+        return isDuplicate ? { forbiddenName: { value: control.value } } : 
null;
+    };
+}

Reply via email to