From 0a45184f4d7bc96752414abf398ae28b37b2b683 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 3 Nov 2025 12:20:55 +0200 Subject: [PATCH] UI: Calculated field refactor arguments name validators --- ...lculated-field-argument-panel.component.ts | 34 ++++++++----------- .../propagate-arguments-table.component.ts | 10 ++++-- ...-geofencing-zone-groups-panel.component.ts | 30 ++++++---------- .../shared/models/calculated-field.models.ts | 27 ++++++++++++++- 4 files changed, 59 insertions(+), 42 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts index e58145cf05..c61153d6b9 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts @@ -25,7 +25,7 @@ import { ViewChild } from '@angular/core'; import { TbPopoverComponent } from '@shared/components/popover.component'; -import { FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { charsWithNumRegex, oneSpaceInsideRegex } from '@shared/models/regex.constants'; import { ArgumentEntityType, @@ -34,7 +34,10 @@ import { ArgumentType, ArgumentTypeTranslations, CalculatedFieldArgumentValue, - getCalculatedFieldCurrentEntityFilter + FORBIDDEN_NAMES, + forbiddenNamesValidator, + getCalculatedFieldCurrentEntityFilter, + uniqueNameValidator } from '@shared/models/calculated-field.models'; import { debounceTime, distinctUntilChanged, filter } from 'rxjs/operators'; import { EntityType } from '@shared/models/entity-type.models'; @@ -74,6 +77,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI @Input() defaultValueRequired = false; @Input() hint: string; @Input() predefinedEntityFilter: EntityFilter; + @Input() forbiddenNames = FORBIDDEN_NAMES; @Input() argumentEntityTypes = Object.values(ArgumentEntityType).filter(value => value !== ArgumentEntityType.RelationQuery) as ArgumentEntityType[]; @ViewChild('entityAutocomplete') entityAutocomplete: EntityAutocompleteComponent; @@ -86,7 +90,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI readonly defaultLimit = Math.floor(this.maxDataPointsPerRollingArg / 10); argumentFormGroup = this.fb.group({ - argumentName: ['', [Validators.required, this.uniqNameRequired(), this.forbiddenArgumentNameValidator(), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], + argumentName: ['', [Validators.required, Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], refEntityId: [null], refEntityKey: this.fb.group({ type: [ArgumentType.LatestTelemetry, [Validators.required]], @@ -141,6 +145,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI } ngOnInit(): void { + this.updatedFormValidators(); this.updatedArgumentType(); this.argumentFormGroup.patchValue(this.argument, {emitEvent: false}); this.currentEntityFilter = getCalculatedFieldCurrentEntityFilter(this.entityName, this.entityId); @@ -185,6 +190,12 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI this.popover.hide(); } + private updatedFormValidators(): void { + this.argumentFormGroup.get('argumentName').addValidators( + [uniqueNameValidator(this.usedArgumentNames), forbiddenNamesValidator(this.forbiddenNames)]); + this.argumentFormGroup.get('argumentName').updateValueAndValidity({emitEvent: false}); + } + private updatedArgumentType(): void { let argumentType = ArgumentEntityType.Current; if (this.argument.refEntityId?.entityType) { @@ -255,15 +266,6 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI }); } - private uniqNameRequired(): ValidatorFn { - return (control: FormControl) => { - const newName = control.value.trim().toLowerCase(); - const isDuplicate = this.usedArgumentNames?.some(name => name.toLowerCase() === newName); - - return isDuplicate ? { duplicateName: true } : null; - }; - } - private observeEntityKeyChanges(): void { this.argumentFormGroup.get('refEntityKey').get('type').valueChanges .pipe(takeUntilDestroyed()) @@ -297,14 +299,6 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI } } - private forbiddenArgumentNameValidator(): ValidatorFn { - return (control: FormControl) => { - const trimmedValue = control.value.trim().toLowerCase(); - const forbiddenArgumentNames = ['ctx', 'e', 'pi', 'propagationCtx']; - return forbiddenArgumentNames.includes(trimmedValue) ? { forbiddenName: true } : null; - }; - } - private updatedRefEntityIdState(type: ArgumentEntityType): void { const isEntityWithId = !!type && type !== ArgumentEntityType.Tenant && type !== ArgumentEntityType.Current; this.argumentFormGroup.get('refEntityId')[isEntityWithId ? 'enable' : 'disable'](); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts index 04d1dbf91b..2e53376f77 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts @@ -31,7 +31,12 @@ import { AppState } from '@core/core.state'; import { CalculatedFieldArgumentsTableComponent } from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component'; -import { ArgumentEntityType, ArgumentType, CalculatedFieldArgumentValue } from '@shared/models/calculated-field.models'; +import { + ArgumentEntityType, + ArgumentType, + CalculatedFieldArgumentValue, + FORBIDDEN_NAMES +} from '@shared/models/calculated-field.models'; import { isDefined } from '@core/utils'; import { NULL_UUID } from '@shared/models/id/has-uuid'; @@ -88,7 +93,8 @@ export class PropagateArgumentsTableComponent extends CalculatedFieldArgumentsTa this.displayColumns = ['name', 'type', 'key', 'actions']; this.panelAdditionalCtx = { argumentEntityTypes: [ArgumentEntityType.Current], - isOutputKey: true + isOutputKey: true, + forbiddenNames: [...FORBIDDEN_NAMES, 'propagationCtx'], }; } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts index 19e54f25dd..adffc5fb7e 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts @@ -32,11 +32,14 @@ import { ArgumentEntityTypeTranslations, CalculatedFieldGeofencing, CalculatedFieldGeofencingValue, + FORBIDDEN_NAMES, + forbiddenNamesValidator, GeofencingDirectionLevelTranslations, GeofencingDirectionTranslations, GeofencingReportStrategy, GeofencingReportStrategyTranslations, - getCalculatedFieldCurrentEntityFilter + getCalculatedFieldCurrentEntityFilter, + uniqueNameValidator } from '@shared/models/calculated-field.models'; import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators'; import { EntityType } from '@shared/models/entity-type.models'; @@ -75,7 +78,7 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit readonly maxRelationLevelPerCfArgument = getCurrentAuthState(this.store).maxRelationLevelPerCfArgument; geofencingFormGroup = this.fb.group({ - name: ['', [Validators.required, this.uniqNameRequired(), this.forbiddenNameValidator(), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], + name: ['', [Validators.required, forbiddenNamesValidator(FORBIDDEN_NAMES), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], refEntityId: this.fb.group({ entityType: [ArgumentEntityType.Current], id: [''] @@ -132,6 +135,7 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit } ngOnInit(): void { + this.updatedFormValidators(); this.geofencingFormGroup.patchValue(this.zone, {emitEvent: false}); if (this.zone.refDynamicSourceConfiguration?.type) { this.refEntityIdFormGroup.get('entityType').setValue(this.zone.refDynamicSourceConfiguration.type, {emitEvent: false}); @@ -158,6 +162,11 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit return of(['Contains', 'Manages']).pipe(map(name => name?.filter(option => option.toLowerCase().includes(search)))); } + private updatedFormValidators(): void { + this.geofencingFormGroup.get('name').addValidators(uniqueNameValidator(this.usedNames)); + this.geofencingFormGroup.get('name').updateValueAndValidity({emitEvent: false}); + } + private observeCreateRelationZonesChanges(): void { this.geofencingFormGroup.get('createRelationsWithMatchedZones').valueChanges .pipe(takeUntilDestroyed()) @@ -264,29 +273,12 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit }); } - private uniqNameRequired(): ValidatorFn { - return (control: FormControl) => { - const newName = control.value.trim().toLowerCase(); - const isDuplicate = this.usedNames?.some(name => name.toLowerCase() === newName); - - return isDuplicate ? { duplicateName: true } : null; - }; - } - private levelsRequired(): ValidatorFn { return (control: FormControl) => { return control.value.length ? null : { levelsRequired: true }; }; } - private forbiddenNameValidator(): ValidatorFn { - return (control: FormControl) => { - const trimmedValue = control.value.trim().toLowerCase(); - const forbiddenNames = ['ctx', 'e', 'pi']; - return forbiddenNames.includes(trimmedValue) ? { forbiddenName: true } : null; - }; - } - levelsFormArray(): UntypedFormArray { return this.refDynamicSourceFormGroup.get('levels') as UntypedFormArray; } diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index c792c2d06f..432f826ebc 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -30,7 +30,9 @@ import { endGroupHighlightRule } from '@shared/models/ace/ace.models'; import { EntitySearchDirection } from '@shared/models/relation.models'; -import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; +import { AbstractControl, FormControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +export const FORBIDDEN_NAMES = ['ctx', 'e', 'pi']; interface BaseCalculatedField extends Omit, 'label'>, HasVersion, HasEntityDebugSettings, HasTenantId, ExportableEntity { entityId: EntityId; @@ -904,3 +906,26 @@ export function notEmptyObjectValidator(): ValidatorFn { return null; }; } + +export function forbiddenNamesValidator(forbiddenNames: string[]): ValidatorFn { + const forbiddenNameSet = new Set(forbiddenNames); + + return (control: FormControl) => { + const trimmedValue = (control.value || '').trim(); + return forbiddenNameSet.has(trimmedValue) ? { forbiddenName: true } : null; + }; +} + +export function uniqueNameValidator(existingNames: string[]): ValidatorFn { + const namesSet = new Set((existingNames || []).map(name => name.toLowerCase())); + + return (control: FormControl) => { + const newName = (control.value || '').trim().toLowerCase(); + + if (!newName) { + return null; + } + + return namesSet.has(newName) ? { duplicateName: true } : null; + }; +}