Calculated field fixes and improvements
This commit is contained in:
parent
0260c966bc
commit
96e292fac5
@ -88,9 +88,8 @@
|
|||||||
[matTooltip]="'action.edit' | translate"
|
[matTooltip]="'action.edit' | translate"
|
||||||
matTooltipPosition="above">
|
matTooltipPosition="above">
|
||||||
<mat-icon
|
<mat-icon
|
||||||
[matBadgeHidden]="!(argumentsFormArray.dirty
|
[matBadgeHidden]="!(group.get('refEntityKey').get('type').value === ArgumentType.Rolling
|
||||||
&& group.get('refEntityKey').get('type').value === ArgumentType.Rolling
|
&& calculatedFieldType === CalculatedFieldType.SIMPLE)"
|
||||||
&& calculatedFieldType() === CalculatedFieldType.SIMPLE)"
|
|
||||||
matBadgeColor="warn"
|
matBadgeColor="warn"
|
||||||
matBadgeSize="small"
|
matBadgeSize="small"
|
||||||
matBadge="*"
|
matBadge="*"
|
||||||
@ -111,7 +110,7 @@
|
|||||||
<span class="tb-prompt flex items-center justify-center">{{ 'calculated-fields.no-arguments' | translate }}</span>
|
<span class="tb-prompt flex items-center justify-center">{{ 'calculated-fields.no-arguments' | translate }}</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@if (errorText && this.argumentsFormArray.dirty) {
|
@if (errorText) {
|
||||||
<tb-error noMargin [error]="errorText | translate" class="pl-3"/>
|
<tb-error noMargin [error]="errorText | translate" class="pl-3"/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -17,9 +17,7 @@
|
|||||||
import {
|
import {
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
effect,
|
|
||||||
forwardRef,
|
forwardRef,
|
||||||
input,
|
|
||||||
Input,
|
Input,
|
||||||
OnChanges,
|
OnChanges,
|
||||||
Renderer2,
|
Renderer2,
|
||||||
@ -77,8 +75,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
|
|||||||
@Input() entityId: EntityId;
|
@Input() entityId: EntityId;
|
||||||
@Input() tenantId: string;
|
@Input() tenantId: string;
|
||||||
@Input() entityName: string;
|
@Input() entityName: string;
|
||||||
|
@Input() calculatedFieldType: CalculatedFieldType;
|
||||||
calculatedFieldType = input<CalculatedFieldType>()
|
|
||||||
|
|
||||||
errorText = '';
|
errorText = '';
|
||||||
argumentsFormArray = this.fb.array<AbstractControl>([]);
|
argumentsFormArray = this.fb.array<AbstractControl>([]);
|
||||||
@ -103,17 +100,12 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
|
|||||||
this.argumentsFormArray.valueChanges.pipe(takeUntilDestroyed()).subscribe(() => {
|
this.argumentsFormArray.valueChanges.pipe(takeUntilDestroyed()).subscribe(() => {
|
||||||
this.propagateChange(this.getArgumentsObject());
|
this.propagateChange(this.getArgumentsObject());
|
||||||
});
|
});
|
||||||
effect(() => {
|
|
||||||
if (this.calculatedFieldType() && this.argumentsFormArray.dirty) {
|
|
||||||
this.argumentsFormArray.updateValueAndValidity();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
if (changes.calculatedFieldType?.previousValue
|
if (changes.calculatedFieldType?.previousValue
|
||||||
&& changes.calculatedFieldType.currentValue !== 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)) {
|
if (this.popoverService.hasPopover(trigger)) {
|
||||||
this.popoverService.hidePopover(trigger);
|
this.popoverService.hidePopover(trigger);
|
||||||
} else {
|
} else {
|
||||||
|
const argumentObj = this.argumentsFormArray.at(index)?.getRawValue() ?? {};
|
||||||
const ctx = {
|
const ctx = {
|
||||||
index,
|
index,
|
||||||
argument: this.argumentsFormArray.at(index)?.getRawValue() ?? {},
|
argument: argumentObj,
|
||||||
entityId: this.entityId,
|
entityId: this.entityId,
|
||||||
calculatedFieldType: this.calculatedFieldType(),
|
calculatedFieldType: this.calculatedFieldType,
|
||||||
buttonTitle: this.argumentsFormArray.at(index)?.value ? 'action.apply' : 'action.add',
|
buttonTitle: this.argumentsFormArray.at(index)?.value ? 'action.apply' : 'action.add',
|
||||||
tenantId: this.tenantId,
|
tenantId: this.tenantId,
|
||||||
entityName: this.entityName,
|
entityName: this.entityName,
|
||||||
|
argumentNames: this.argumentsFormArray.value.map(({ argumentName }) => argumentName).filter(name => name !== argumentObj.argumentName),
|
||||||
};
|
};
|
||||||
this.popoverComponent = this.popoverService.displayPopover(trigger, this.renderer,
|
this.popoverComponent = this.popoverService.displayPopover(trigger, this.renderer,
|
||||||
this.viewContainerRef, CalculatedFieldArgumentPanelComponent, 'left', false, null,
|
this.viewContainerRef, CalculatedFieldArgumentPanelComponent, 'left', false, null,
|
||||||
@ -171,7 +165,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updateErrorText(): void {
|
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.argumentsFormArray.controls.some(control => control.get('refEntityKey').get('type').value === ArgumentType.Rolling)) {
|
||||||
this.errorText = 'calculated-fields.hint.arguments-simple-with-rolling';
|
this.errorText = 'calculated-fields.hint.arguments-simple-with-rolling';
|
||||||
} else if (!this.argumentsFormArray.controls.length) {
|
} else if (!this.argumentsFormArray.controls.length) {
|
||||||
|
|||||||
@ -65,7 +65,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<ng-container [formGroup]="configFormGroup">
|
<ng-container [formGroup]="configFormGroup">
|
||||||
<div class="tb-form-panel">
|
<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
|
<tb-calculated-field-arguments-table
|
||||||
formControlName="arguments"
|
formControlName="arguments"
|
||||||
[entityId]="data.entityId"
|
[entityId]="data.entityId"
|
||||||
|
|||||||
@ -31,7 +31,15 @@
|
|||||||
class="tb-error">
|
class="tb-error">
|
||||||
warning
|
warning
|
||||||
</mat-icon>
|
</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
|
<mat-icon matSuffix
|
||||||
matTooltipPosition="above"
|
matTooltipPosition="above"
|
||||||
matTooltipClass="tb-error-tooltip"
|
matTooltipClass="tb-error-tooltip"
|
||||||
@ -103,25 +111,24 @@
|
|||||||
<tb-entity-key-autocomplete class="flex-1" formControlName="key" [dataKeyType]="DataKeyType.timeseries" [entityFilter]="entityFilter"/>
|
<tb-entity-key-autocomplete class="flex-1" formControlName="key" [dataKeyType]="DataKeyType.timeseries" [entityFilter]="entityFilter"/>
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="tb-form-row">
|
@if (isDeviceEntity) {
|
||||||
<div class="fixed-title-width tb-required">{{ 'calculated-fields.attribute-scope' | translate }}</div>
|
<div class="tb-form-row">
|
||||||
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="flex-1">
|
<div class="fixed-title-width tb-required">{{ 'calculated-fields.attribute-scope' | translate }}</div>
|
||||||
<mat-select formControlName="scope">
|
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="flex-1">
|
||||||
<mat-option [value]="AttributeScope.SERVER_SCOPE">
|
<mat-select formControlName="scope">
|
||||||
{{ 'calculated-fields.server-attributes' | translate }}
|
<mat-option [value]="AttributeScope.SERVER_SCOPE">
|
||||||
</mat-option>
|
{{ 'calculated-fields.server-attributes' | translate }}
|
||||||
@if (entityType === ArgumentEntityType.Device
|
</mat-option>
|
||||||
|| entityType === ArgumentEntityType.Current && entityId.entityType === EntityType.DEVICE) {
|
|
||||||
<mat-option [value]="AttributeScope.CLIENT_SCOPE">
|
<mat-option [value]="AttributeScope.CLIENT_SCOPE">
|
||||||
{{ 'calculated-fields.client-attributes' | translate }}
|
{{ 'calculated-fields.client-attributes' | translate }}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
<mat-option [value]="AttributeScope.SHARED_SCOPE">
|
<mat-option [value]="AttributeScope.SHARED_SCOPE">
|
||||||
{{ 'calculated-fields.shared-attributes' | translate }}
|
{{ 'calculated-fields.shared-attributes' | translate }}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
}
|
</mat-select>
|
||||||
</mat-select>
|
</mat-form-field>
|
||||||
</mat-form-field>
|
</div>
|
||||||
</div>
|
}
|
||||||
<div class="tb-form-row">
|
<div class="tb-form-row">
|
||||||
<div class="fixed-title-width tb-required">{{ 'calculated-fields.attribute-key' | translate }}</div>
|
<div class="fixed-title-width tb-required">{{ 'calculated-fields.attribute-key' | translate }}</div>
|
||||||
<tb-entity-key-autocomplete
|
<tb-entity-key-autocomplete
|
||||||
@ -148,7 +155,7 @@
|
|||||||
<tb-timeinterval
|
<tb-timeinterval
|
||||||
subscriptSizing="dynamic"
|
subscriptSizing="dynamic"
|
||||||
appearance="outline"
|
appearance="outline"
|
||||||
class="flex-1"
|
class="time-window-field flex-1"
|
||||||
formControlName="timeWindow"
|
formControlName="timeWindow"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 { ChangeDetectorRef, Component, Input, OnInit, output } from '@angular/core';
|
||||||
import { TbPopoverComponent } from '@shared/components/popover.component';
|
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 { charsWithNumRegex, noLeadTrailSpacesRegex } from '@shared/models/regex.constants';
|
||||||
import {
|
import {
|
||||||
ArgumentEntityType,
|
ArgumentEntityType,
|
||||||
@ -42,6 +42,7 @@ import { MINUTE } from '@shared/models/time/time.models';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'tb-calculated-field-argument-panel',
|
selector: 'tb-calculated-field-argument-panel',
|
||||||
templateUrl: './calculated-field-argument-panel.component.html',
|
templateUrl: './calculated-field-argument-panel.component.html',
|
||||||
|
styleUrls: ['./calculated-field-argument-panel.component.scss']
|
||||||
})
|
})
|
||||||
export class CalculatedFieldArgumentPanelComponent implements OnInit {
|
export class CalculatedFieldArgumentPanelComponent implements OnInit {
|
||||||
|
|
||||||
@ -52,11 +53,12 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit {
|
|||||||
@Input() tenantId: string;
|
@Input() tenantId: string;
|
||||||
@Input() entityName: string;
|
@Input() entityName: string;
|
||||||
@Input() calculatedFieldType: CalculatedFieldType;
|
@Input() calculatedFieldType: CalculatedFieldType;
|
||||||
|
@Input() argumentNames: string[];
|
||||||
|
|
||||||
argumentsDataApplied = output<{ value: CalculatedFieldArgumentValue, index: number }>();
|
argumentsDataApplied = output<{ value: CalculatedFieldArgumentValue, index: number }>();
|
||||||
|
|
||||||
argumentFormGroup = this.fb.group({
|
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({
|
refEntityId: this.fb.group({
|
||||||
entityType: [ArgumentEntityType.Current],
|
entityType: [ArgumentEntityType.Current],
|
||||||
id: ['']
|
id: ['']
|
||||||
@ -109,6 +111,12 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit {
|
|||||||
return this.argumentFormGroup.get('refEntityKey') as FormGroup;
|
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 {
|
ngOnInit(): void {
|
||||||
this.argumentFormGroup.patchValue(this.argument, {emitEvent: false});
|
this.argumentFormGroup.patchValue(this.argument, {emitEvent: false});
|
||||||
this.currentEntityFilter = getCalculatedFieldCurrentEntityFilter(this.entityName, this.entityId);
|
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').setValue('');
|
||||||
this.argumentFormGroup.get('refEntityId')
|
this.argumentFormGroup.get('refEntityId')
|
||||||
.get('id')[type === ArgumentEntityType.Tenant || type === ArgumentEntityType.Current ? 'disable' : 'enable']();
|
.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 {
|
private observeEntityKeyChanges(): void {
|
||||||
this.argumentFormGroup.get('refEntityKey').get('type').valueChanges
|
this.argumentFormGroup.get('refEntityKey').get('type').valueChanges
|
||||||
.pipe(takeUntilDestroyed())
|
.pipe(takeUntilDestroyed())
|
||||||
|
|||||||
@ -997,7 +997,6 @@ export class ImportExportService {
|
|||||||
&& !!Object.keys(configuration.arguments).length
|
&& !!Object.keys(configuration.arguments).length
|
||||||
&& isDefined(configuration.expression)
|
&& isDefined(configuration.expression)
|
||||||
&& isDefined(configuration.output)
|
&& isDefined(configuration.output)
|
||||||
&& isNotEmptyStr(configuration.output.name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private validateImportedImage(image: ImageExportData): boolean {
|
private validateImportedImage(image: ImageExportData): boolean {
|
||||||
|
|||||||
@ -1064,6 +1064,7 @@
|
|||||||
"expression-max-length": "Expression length should be less than 255 characters.",
|
"expression-max-length": "Expression length should be less than 255 characters.",
|
||||||
"argument-name-required": "Argument name is required.",
|
"argument-name-required": "Argument name is required.",
|
||||||
"argument-name-pattern": "Argument name is invalid.",
|
"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-name-max-length": "Argument name should be less than 256 characters.",
|
||||||
"argument-type-required": "Argument type is required."
|
"argument-type-required": "Argument type is required."
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user