Calculated field fixes and improvements
This commit is contained in:
		
							parent
							
								
									0260c966bc
								
							
						
					
					
						commit
						96e292fac5
					
				@ -88,9 +88,8 @@
 | 
			
		||||
                                [matTooltip]="'action.edit' | translate"
 | 
			
		||||
                                matTooltipPosition="above">
 | 
			
		||||
                            <mat-icon
 | 
			
		||||
                              [matBadgeHidden]="!(argumentsFormArray.dirty
 | 
			
		||||
                                  && group.get('refEntityKey').get('type').value === ArgumentType.Rolling
 | 
			
		||||
                                  && calculatedFieldType() === CalculatedFieldType.SIMPLE)"
 | 
			
		||||
                              [matBadgeHidden]="!(group.get('refEntityKey').get('type').value === ArgumentType.Rolling
 | 
			
		||||
                                  && calculatedFieldType === CalculatedFieldType.SIMPLE)"
 | 
			
		||||
                              matBadgeColor="warn"
 | 
			
		||||
                              matBadgeSize="small"
 | 
			
		||||
                              matBadge="*"
 | 
			
		||||
@ -111,7 +110,7 @@
 | 
			
		||||
                <span class="tb-prompt flex items-center justify-center">{{ 'calculated-fields.no-arguments' | translate }}</span>
 | 
			
		||||
            }
 | 
			
		||||
        </div>
 | 
			
		||||
        @if (errorText && this.argumentsFormArray.dirty) {
 | 
			
		||||
        @if (errorText) {
 | 
			
		||||
            <tb-error noMargin [error]="errorText | translate" class="pl-3"/>
 | 
			
		||||
        }
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@ -17,9 +17,7 @@
 | 
			
		||||
import {
 | 
			
		||||
  ChangeDetectorRef,
 | 
			
		||||
  Component,
 | 
			
		||||
  effect,
 | 
			
		||||
  forwardRef,
 | 
			
		||||
  input,
 | 
			
		||||
  Input,
 | 
			
		||||
  OnChanges,
 | 
			
		||||
  Renderer2,
 | 
			
		||||
@ -77,8 +75,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
 | 
			
		||||
  @Input() entityId: EntityId;
 | 
			
		||||
  @Input() tenantId: string;
 | 
			
		||||
  @Input() entityName: string;
 | 
			
		||||
 | 
			
		||||
  calculatedFieldType = input<CalculatedFieldType>()
 | 
			
		||||
  @Input() calculatedFieldType: CalculatedFieldType;
 | 
			
		||||
 | 
			
		||||
  errorText = '';
 | 
			
		||||
  argumentsFormArray = this.fb.array<AbstractControl>([]);
 | 
			
		||||
@ -103,17 +100,12 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
 | 
			
		||||
    this.argumentsFormArray.valueChanges.pipe(takeUntilDestroyed()).subscribe(() => {
 | 
			
		||||
      this.propagateChange(this.getArgumentsObject());
 | 
			
		||||
    });
 | 
			
		||||
    effect(() => {
 | 
			
		||||
      if (this.calculatedFieldType() && this.argumentsFormArray.dirty) {
 | 
			
		||||
        this.argumentsFormArray.updateValueAndValidity();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnChanges(changes: SimpleChanges): void {
 | 
			
		||||
    if (changes.calculatedFieldType?.previousValue
 | 
			
		||||
      && changes.calculatedFieldType.currentValue !== changes.calculatedFieldType.previousValue) {
 | 
			
		||||
      this.argumentsFormArray.markAsDirty();
 | 
			
		||||
      this.argumentsFormArray.updateValueAndValidity();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -142,14 +134,16 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
 | 
			
		||||
    if (this.popoverService.hasPopover(trigger)) {
 | 
			
		||||
      this.popoverService.hidePopover(trigger);
 | 
			
		||||
    } else {
 | 
			
		||||
      const argumentObj = this.argumentsFormArray.at(index)?.getRawValue() ?? {};
 | 
			
		||||
      const ctx = {
 | 
			
		||||
        index,
 | 
			
		||||
        argument: this.argumentsFormArray.at(index)?.getRawValue() ?? {},
 | 
			
		||||
        argument: argumentObj,
 | 
			
		||||
        entityId: this.entityId,
 | 
			
		||||
        calculatedFieldType: this.calculatedFieldType(),
 | 
			
		||||
        calculatedFieldType: this.calculatedFieldType,
 | 
			
		||||
        buttonTitle: this.argumentsFormArray.at(index)?.value ? 'action.apply' : 'action.add',
 | 
			
		||||
        tenantId: this.tenantId,
 | 
			
		||||
        entityName: this.entityName,
 | 
			
		||||
        argumentNames: this.argumentsFormArray.value.map(({ argumentName }) => argumentName).filter(name => name !== argumentObj.argumentName),
 | 
			
		||||
      };
 | 
			
		||||
      this.popoverComponent = this.popoverService.displayPopover(trigger, this.renderer,
 | 
			
		||||
        this.viewContainerRef, CalculatedFieldArgumentPanelComponent, 'left', false, null,
 | 
			
		||||
@ -171,7 +165,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private updateErrorText(): void {
 | 
			
		||||
    if (this.calculatedFieldType() === CalculatedFieldType.SIMPLE
 | 
			
		||||
    if (this.calculatedFieldType === CalculatedFieldType.SIMPLE
 | 
			
		||||
      && this.argumentsFormArray.controls.some(control => control.get('refEntityKey').get('type').value === ArgumentType.Rolling)) {
 | 
			
		||||
      this.errorText = 'calculated-fields.hint.arguments-simple-with-rolling';
 | 
			
		||||
    } else if (!this.argumentsFormArray.controls.length) {
 | 
			
		||||
 | 
			
		||||
@ -65,7 +65,7 @@
 | 
			
		||||
      </div>
 | 
			
		||||
      <ng-container [formGroup]="configFormGroup">
 | 
			
		||||
        <div class="tb-form-panel">
 | 
			
		||||
          <div class="tb-form-panel-title">{{ 'calculated-fields.arguments' | translate }}</div>
 | 
			
		||||
          <div class="tb-form-panel-title">{{ 'calculated-fields.arguments' | translate }}*</div>
 | 
			
		||||
          <tb-calculated-field-arguments-table
 | 
			
		||||
            formControlName="arguments"
 | 
			
		||||
            [entityId]="data.entityId"
 | 
			
		||||
 | 
			
		||||
@ -31,7 +31,15 @@
 | 
			
		||||
                      class="tb-error">
 | 
			
		||||
              warning
 | 
			
		||||
            </mat-icon>
 | 
			
		||||
          } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('pattern')) {
 | 
			
		||||
          } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('duplicateName')) {
 | 
			
		||||
          <mat-icon matSuffix
 | 
			
		||||
                    matTooltipPosition="above"
 | 
			
		||||
                    matTooltipClass="tb-error-tooltip"
 | 
			
		||||
                    [matTooltip]="'calculated-fields.hint.argument-name-duplicate' | translate"
 | 
			
		||||
                    class="tb-error">
 | 
			
		||||
            warning
 | 
			
		||||
          </mat-icon>
 | 
			
		||||
        } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('pattern')) {
 | 
			
		||||
            <mat-icon matSuffix
 | 
			
		||||
                      matTooltipPosition="above"
 | 
			
		||||
                      matTooltipClass="tb-error-tooltip"
 | 
			
		||||
@ -103,25 +111,24 @@
 | 
			
		||||
              <tb-entity-key-autocomplete class="flex-1" formControlName="key" [dataKeyType]="DataKeyType.timeseries" [entityFilter]="entityFilter"/>
 | 
			
		||||
            </div>
 | 
			
		||||
          } @else {
 | 
			
		||||
            <div class="tb-form-row">
 | 
			
		||||
              <div class="fixed-title-width tb-required">{{ 'calculated-fields.attribute-scope' | translate }}</div>
 | 
			
		||||
              <mat-form-field appearance="outline" subscriptSizing="dynamic" class="flex-1">
 | 
			
		||||
                <mat-select formControlName="scope">
 | 
			
		||||
                  <mat-option [value]="AttributeScope.SERVER_SCOPE">
 | 
			
		||||
                    {{ 'calculated-fields.server-attributes' | translate }}
 | 
			
		||||
                  </mat-option>
 | 
			
		||||
                  @if (entityType === ArgumentEntityType.Device
 | 
			
		||||
                  || entityType === ArgumentEntityType.Current && entityId.entityType === EntityType.DEVICE) {
 | 
			
		||||
            @if (isDeviceEntity) {
 | 
			
		||||
              <div class="tb-form-row">
 | 
			
		||||
                <div class="fixed-title-width tb-required">{{ 'calculated-fields.attribute-scope' | translate }}</div>
 | 
			
		||||
                <mat-form-field appearance="outline" subscriptSizing="dynamic" class="flex-1">
 | 
			
		||||
                  <mat-select formControlName="scope">
 | 
			
		||||
                    <mat-option [value]="AttributeScope.SERVER_SCOPE">
 | 
			
		||||
                      {{ 'calculated-fields.server-attributes' | translate }}
 | 
			
		||||
                    </mat-option>
 | 
			
		||||
                    <mat-option [value]="AttributeScope.CLIENT_SCOPE">
 | 
			
		||||
                      {{ 'calculated-fields.client-attributes' | translate }}
 | 
			
		||||
                    </mat-option>
 | 
			
		||||
                    <mat-option [value]="AttributeScope.SHARED_SCOPE">
 | 
			
		||||
                      {{ 'calculated-fields.shared-attributes' | translate }}
 | 
			
		||||
                    </mat-option>
 | 
			
		||||
                  }
 | 
			
		||||
                </mat-select>
 | 
			
		||||
              </mat-form-field>
 | 
			
		||||
            </div>
 | 
			
		||||
                  </mat-select>
 | 
			
		||||
                </mat-form-field>
 | 
			
		||||
              </div>
 | 
			
		||||
            }
 | 
			
		||||
            <div class="tb-form-row">
 | 
			
		||||
              <div class="fixed-title-width tb-required">{{ 'calculated-fields.attribute-key' | translate }}</div>
 | 
			
		||||
              <tb-entity-key-autocomplete
 | 
			
		||||
@ -148,7 +155,7 @@
 | 
			
		||||
          <tb-timeinterval
 | 
			
		||||
            subscriptSizing="dynamic"
 | 
			
		||||
            appearance="outline"
 | 
			
		||||
            class="flex-1"
 | 
			
		||||
            class="time-window-field flex-1"
 | 
			
		||||
            formControlName="timeWindow"
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,22 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2024 The Thingsboard Authors
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
:host ::ng-deep {
 | 
			
		||||
  .time-window-field {
 | 
			
		||||
    .mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field .mdc-notched-outline__notch {
 | 
			
		||||
      border-left: 1px solid rgba(0, 0, 0, 0) !important;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -16,7 +16,7 @@
 | 
			
		||||
 | 
			
		||||
import { ChangeDetectorRef, Component, Input, OnInit, output } from '@angular/core';
 | 
			
		||||
import { TbPopoverComponent } from '@shared/components/popover.component';
 | 
			
		||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
 | 
			
		||||
import { FormBuilder, FormGroup, UntypedFormControl, ValidatorFn, Validators } from '@angular/forms';
 | 
			
		||||
import { charsWithNumRegex, noLeadTrailSpacesRegex } from '@shared/models/regex.constants';
 | 
			
		||||
import {
 | 
			
		||||
  ArgumentEntityType,
 | 
			
		||||
@ -42,6 +42,7 @@ import { MINUTE } from '@shared/models/time/time.models';
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'tb-calculated-field-argument-panel',
 | 
			
		||||
  templateUrl: './calculated-field-argument-panel.component.html',
 | 
			
		||||
  styleUrls: ['./calculated-field-argument-panel.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class CalculatedFieldArgumentPanelComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
@ -52,11 +53,12 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit {
 | 
			
		||||
  @Input() tenantId: string;
 | 
			
		||||
  @Input() entityName: string;
 | 
			
		||||
  @Input() calculatedFieldType: CalculatedFieldType;
 | 
			
		||||
  @Input() argumentNames: string[];
 | 
			
		||||
 | 
			
		||||
  argumentsDataApplied = output<{ value: CalculatedFieldArgumentValue, index: number }>();
 | 
			
		||||
 | 
			
		||||
  argumentFormGroup = this.fb.group({
 | 
			
		||||
    argumentName: ['', [Validators.required, Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]],
 | 
			
		||||
    argumentName: ['', [Validators.required, this.uniqNameRequired(), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]],
 | 
			
		||||
    refEntityId: this.fb.group({
 | 
			
		||||
      entityType: [ArgumentEntityType.Current],
 | 
			
		||||
      id: ['']
 | 
			
		||||
@ -109,6 +111,12 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit {
 | 
			
		||||
    return this.argumentFormGroup.get('refEntityKey') as FormGroup;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get isDeviceEntity(): boolean {
 | 
			
		||||
    return this.entityType === ArgumentEntityType.Device
 | 
			
		||||
      || (this.entityType === ArgumentEntityType.Current
 | 
			
		||||
        && (this.entityId.entityType === EntityType.DEVICE || this.entityId.entityType === EntityType.DEVICE_PROFILE))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.argumentFormGroup.patchValue(this.argument, {emitEvent: false});
 | 
			
		||||
    this.currentEntityFilter = getCalculatedFieldCurrentEntityFilter(this.entityName, this.entityId);
 | 
			
		||||
@ -188,9 +196,21 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit {
 | 
			
		||||
        this.argumentFormGroup.get('refEntityId').get('id').setValue('');
 | 
			
		||||
        this.argumentFormGroup.get('refEntityId')
 | 
			
		||||
          .get('id')[type === ArgumentEntityType.Tenant || type === ArgumentEntityType.Current ? 'disable' : 'enable']();
 | 
			
		||||
        if (!this.isDeviceEntity) {
 | 
			
		||||
          this.refEntityKeyFormGroup.get('scope').setValue(AttributeScope.SERVER_SCOPE);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private uniqNameRequired(): ValidatorFn {
 | 
			
		||||
    return (control: UntypedFormControl) => {
 | 
			
		||||
      const newName = control.value.trim().toLowerCase();
 | 
			
		||||
      const isDuplicate = this.argumentNames?.some(name => name.toLowerCase() === newName);
 | 
			
		||||
 | 
			
		||||
      return isDuplicate ? { duplicateName: true } : null;
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private observeEntityKeyChanges(): void {
 | 
			
		||||
    this.argumentFormGroup.get('refEntityKey').get('type').valueChanges
 | 
			
		||||
      .pipe(takeUntilDestroyed())
 | 
			
		||||
 | 
			
		||||
@ -997,7 +997,6 @@ export class ImportExportService {
 | 
			
		||||
      && !!Object.keys(configuration.arguments).length
 | 
			
		||||
      && isDefined(configuration.expression)
 | 
			
		||||
      && isDefined(configuration.output)
 | 
			
		||||
      && isNotEmptyStr(configuration.output.name);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private validateImportedImage(image: ImageExportData): boolean {
 | 
			
		||||
 | 
			
		||||
@ -1064,6 +1064,7 @@
 | 
			
		||||
            "expression-max-length": "Expression length should be less than 255 characters.",
 | 
			
		||||
            "argument-name-required": "Argument name is required.",
 | 
			
		||||
            "argument-name-pattern": "Argument name is invalid.",
 | 
			
		||||
            "argument-name-duplicate": "Argument with such name already exists.",
 | 
			
		||||
            "argument-name-max-length": "Argument name should be less than 256 characters.",
 | 
			
		||||
            "argument-type-required": "Argument type is required."
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user