Merge branch 'develop/3.5.2' into improvement/move-form-table-style

This commit is contained in:
Igor Kulikov 2023-07-07 18:10:11 +03:00 committed by GitHub
commit afcebf2eda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 346 additions and 108 deletions

View File

@ -180,6 +180,7 @@ import * as SlackConversationAutocompleteComponent from '@shared/components/slac
import * as StringItemsListComponent from '@shared/components/string-items-list.component';
import * as ToggleHeaderComponent from '@shared/components/toggle-header.component';
import * as ToggleSelectComponent from '@shared/components/toggle-select.component';
import * as UnitInputComponent from '@shared/components/unit-input.component';
import * as AddEntityDialogComponent from '@home/components/entity/add-entity-dialog.component';
import * as EntitiesTableComponent from '@home/components/entity/entities-table.component';
@ -480,6 +481,7 @@ class ModulesMap implements IModulesMap {
'@shared/components/string-items-list.component': StringItemsListComponent,
'@shared/components/toggle-header.component': ToggleHeaderComponent,
'@shared/components/toggle-select.component': ToggleSelectComponent,
'@shared/components/unit-input.component': UnitInputComponent,
'@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent,
'@home/components/entity/entities-table.component': EntitiesTableComponent,

View File

@ -42,7 +42,7 @@
#entityAliasAutocomplete="matAutocomplete"
[displayWith]="displayEntityAliasFn">
<mat-option *ngFor="let entityAlias of filteredEntityAliases | async" [value]="entityAlias">
<span [innerHTML]="entityAlias.alias | highlight:searchText"></span>
<span [innerHTML]="entityAlias.alias | highlight:searchText:true:'ig'"></span>
</mat-option>
<mat-option *ngIf="!(filteredEntityAliases | async)?.length" [value]="null" class="tb-not-found">
<div class="tb-not-found-content" (click)="$event.stopPropagation()">

View File

@ -51,9 +51,9 @@
</div>
<div class="tb-form-row space-between">
<div translate>widget-config.units-short</div>
<tb-widget-units
<tb-unit-input
formControlName="units">
</tb-widget-units>
</tb-unit-input>
</div>
<div class="tb-form-row space-between">
<div translate>widget-config.decimals-short</div>

View File

@ -148,9 +148,9 @@
</tb-color-input>
</div>
<div *ngIf="!hideUnits" class="tb-units-field">
<tb-widget-units *ngIf="displayUnitsOrDigits"
<tb-unit-input *ngIf="displayUnitsOrDigits"
formControlName="units">
</tb-widget-units>
</tb-unit-input>
</div>
<div *ngIf="!hideDecimals" class="tb-decimals-field">
<mat-form-field *ngIf="displayUnitsOrDigits" appearance="outline" class="tb-inline-field number" subscriptSizing="dynamic">

View File

@ -42,10 +42,17 @@
}
.tb-color-field, .tb-units-field, .tb-decimals-field {
width: 60px;
display: flex;
flex-direction: row;
place-content: center;
align-items: center;
}
.tb-units-field {
width: 80px;
}
.tb-color-field, .tb-decimals-field {
width: 60px;
}
}

View File

@ -17,7 +17,10 @@
&.tb-source-header {
width: 140px;
}
&.tb-color-header, &.tb-units-header, &.tb-decimals-header {
&.tb-units-header {
width: 80px;
}
&.tb-color-header, &.tb-decimals-header {
width: 60px;
}
&.tb-actions-header {

View File

@ -48,9 +48,9 @@
<ng-container *ngIf="modelValue.type !== dataKeyTypes.alarm">
<div class="tb-form-row space-between" *ngIf="!hideDataKeyUnits">
<div translate>widget-config.units-short</div>
<tb-widget-units
<tb-unit-input
formControlName="units">
</tb-widget-units>
</tb-unit-input>
</div>
<div class="tb-form-row space-between" *ngIf="!hideDataKeyDecimals">
<div translate>widget-config.decimals-short</div>

View File

@ -29,7 +29,6 @@ import { FilterSelectComponent } from '@home/components/filter/filter-select.com
import { WidgetSettingsModule } from '@home/components/widget/lib/settings/widget-settings.module';
import { WidgetSettingsComponent } from '@home/components/widget/config/widget-settings.component';
import { TimewindowConfigPanelComponent } from '@home/components/widget/config/timewindow-config-panel.component';
import { WidgetUnitsComponent } from '@home/components/widget/config/widget-units.component';
@NgModule({
declarations:
@ -44,7 +43,6 @@ import { WidgetUnitsComponent } from '@home/components/widget/config/widget-unit
EntityAliasSelectComponent,
FilterSelectComponent,
TimewindowConfigPanelComponent,
WidgetUnitsComponent,
WidgetSettingsComponent
],
imports: [
@ -63,7 +61,6 @@ import { WidgetUnitsComponent } from '@home/components/widget/config/widget-unit
EntityAliasSelectComponent,
FilterSelectComponent,
TimewindowConfigPanelComponent,
WidgetUnitsComponent,
WidgetSettingsComponent
]
})

View File

@ -1,20 +0,0 @@
<!--
Copyright © 2016-2023 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.
-->
<mat-form-field appearance="outline" class="tb-inline-field" subscriptSizing="dynamic">
<input matInput [formControl]="unitsFormControl" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>

View File

@ -1,68 +0,0 @@
///
/// Copyright © 2016-2023 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.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, UntypedFormBuilder } from '@angular/forms';
@Component({
selector: 'tb-widget-units',
templateUrl: './widget-units.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => WidgetUnitsComponent),
multi: true
}
]
})
export class WidgetUnitsComponent implements ControlValueAccessor, OnInit {
@Input()
disabled: boolean;
unitsFormControl: FormControl;
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder) {
}
ngOnInit() {
this.unitsFormControl = this.fb.control('', []);
this.unitsFormControl.valueChanges.subscribe(val => this.propagateChange(val));
}
writeValue(units?: string): void {
this.unitsFormControl.patchValue(units, {emitEvent: false});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.unitsFormControl.disable({emitEvent: false});
} else {
this.unitsFormControl.enable({emitEvent: false});
}
}
}

View File

@ -267,9 +267,9 @@
<div class="tb-form-panel-title" translate>widget-config.data-settings</div>
<div *ngIf="displayUnitsConfig" class="tb-form-row space-between">
<div translate>widget-config.units</div>
<tb-widget-units
<tb-unit-input
formControlName="units">
</tb-widget-units>
</tb-unit-input>
</div>
<div *ngIf="displayUnitsConfig" class="tb-form-row space-between">
<div translate>widget-config.decimals</div>

View File

@ -33,7 +33,7 @@
#entityAutocomplete="matAutocomplete"
[displayWith]="displayEntityFn">
<mat-option *ngFor="let entity of filteredEntities | async" [value]="entity">
<span [innerHTML]="entity.name | highlight:searchText"></span>
<span [innerHTML]="entity.name | highlight:searchText:true:'ig'"></span>
</mat-option>
<mat-option *ngIf="!(filteredEntities | async)?.length" [value]="null">
<span>

View File

@ -25,3 +25,4 @@ export * from './notification/template-autocomplete.component';
export * from './resource/resource-autocomplete.component';
export * from './toggle-header.component';
export * from './toggle-select.component';
export * from './unit-input.component';

View File

@ -0,0 +1,39 @@
<!--
Copyright © 2016-2023 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.
-->
<mat-form-field appearance="outline" class="tb-inline-field" subscriptSizing="dynamic">
<input matInput #unitInput [formControl]="unitsFormControl"
placeholder="{{ 'widget-config.set' | translate }}"
(focusin)="onFocus()"
[matAutocomplete]="unitsAutocomplete">
<button *ngIf="unitsFormControl.value && !disabled"
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-autocomplete
#unitsAutocomplete="matAutocomplete"
class="tb-autocomplete tb-unit-input-autocomplete"
panelWidth="fit-content"
[displayWith]="displayUnitFn">
<mat-option *ngFor="let unit of filteredUnits | async" [value]="unit">
<span class="tb-unit-name" fxFlex [innerHTML]="unit.name | highlight:searchText:true:'ig'"></span>
<span class="tb-unit-symbol" [innerHTML]="unit.symbol | highlight:searchText:true:'ig'"></span>
</mat-option>
</mat-autocomplete>
</mat-form-field>

View File

@ -0,0 +1,40 @@
/**
* Copyright © 2016-2023 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.
*/
.tb-autocomplete.tb-unit-input-autocomplete {
.mat-mdc-option {
border-bottom: none;
.mdc-list-item__primary-text {
flex: 1;
display: flex;
flex-direction: row;
gap: 8px;
.tb-unit-name, .tb-unit-symbol {
font-size: 14px;
font-weight: 400;
line-height: 20px;
letter-spacing: 0.2px;
}
.tb-unit-symbol {
color: rgba(0, 0, 0, 0.38);
min-width: 22px;
text-align: end;
b {
color: rgba(0, 0, 0, 0.87);
}
}
}
}
}

View File

@ -0,0 +1,148 @@
///
/// Copyright © 2016-2023 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.
///
import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, UntypedFormBuilder } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { searchUnits, Unit, unitBySymbol, units } from '@shared/models/unit.models';
import { map, mergeMap, startWith, tap } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'tb-unit-input',
templateUrl: './unit-input.component.html',
styleUrls: ['./unit-input.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => UnitInputComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class UnitInputComponent implements ControlValueAccessor, OnInit {
unitsFormControl: FormControl;
modelValue: string | null;
@Input()
disabled: boolean;
@ViewChild('unitInput', {static: true}) unitInput: ElementRef;
filteredUnits: Observable<Array<Unit | string>>;
searchText = '';
private dirty = false;
private translatedUnits: Array<Unit> = units.map(u => ({symbol: u.symbol,
name: this.translate.instant(u.name),
tags: u.tags}));
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder,
private translate: TranslateService) {
}
ngOnInit() {
this.unitsFormControl = this.fb.control('', []);
this.filteredUnits = this.unitsFormControl.valueChanges
.pipe(
tap(value => {
this.updateView(value);
}),
startWith<string | Unit>(''),
map(value => (value as Unit)?.symbol ? (value as Unit).symbol : (value ? value as string : '')),
mergeMap(symbol => this.fetchUnits(symbol) )
);
}
writeValue(symbol?: string): void {
this.searchText = '';
this.modelValue = symbol;
let res: Unit | string = null;
if (symbol) {
const unit = unitBySymbol(symbol);
res = unit ? unit : symbol;
}
this.unitsFormControl.patchValue(res, {emitEvent: false});
this.dirty = true;
}
onFocus() {
if (this.dirty) {
this.unitsFormControl.updateValueAndValidity({onlySelf: true, emitEvent: true});
this.dirty = false;
}
}
updateView(value: Unit | string | null) {
const res: string = (value as Unit)?.symbol ? (value as Unit)?.symbol : (value as string);
if (this.modelValue !== res) {
this.modelValue = res;
this.propagateChange(this.modelValue);
}
}
displayUnitFn(unit?: Unit | string): string | undefined {
if (unit) {
if ((unit as Unit).symbol) {
return (unit as Unit).symbol;
} else {
return unit as string;
}
}
return undefined;
}
fetchUnits(searchText?: string): Observable<Array<Unit | string>> {
this.searchText = searchText;
const result = searchUnits(this.translatedUnits, searchText);
if (result.length) {
return of(result);
} else {
return of([]);
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.unitsFormControl.disable({emitEvent: false});
} else {
this.unitsFormControl.enable({emitEvent: false});
}
}
clear() {
this.unitsFormControl.patchValue(null, {emitEvent: true});
setTimeout(() => {
this.unitInput.nativeElement.blur();
this.unitInput.nativeElement.focus();
}, 0);
}
}

View File

@ -0,0 +1,70 @@
///
/// Copyright © 2016-2023 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.
///
export interface Unit {
name: string;
symbol: string;
tags: string[];
}
export const units: Array<Unit> = [
{
name: 'unit.celsius',
symbol: '°C',
tags: ['temperature']
},
{
name: 'unit.kelvin',
symbol: 'K',
tags: ['temperature']
},
{
name: 'unit.fahrenheit',
symbol: '°F',
tags: ['temperature']
},
{
name: 'unit.percentage',
symbol: '%',
tags: ['percentage']
},
{
name: 'unit.second',
symbol: 's',
tags: ['time']
},
{
name: 'unit.minute',
symbol: 'min',
tags: ['time']
},
{
name: 'unit.hour',
symbol: 'h',
tags: ['time']
}
];
export const unitBySymbol = (symbol: string): Unit => units.find(u => u.symbol === symbol);
const searchUnitTags = (unit: Unit, searchText: string): boolean =>
!!unit.tags.find(t => t.toUpperCase().includes(searchText.toUpperCase()));
export const searchUnits = (_units: Array<Unit>, searchText: string): Array<Unit> => _units.filter(
u => u.symbol.toUpperCase().includes(searchText.toUpperCase()) ||
u.name.toUpperCase().includes(searchText.toUpperCase()) ||
searchUnitTags(u, searchText)
);

View File

@ -18,11 +18,10 @@ import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'highlight' })
export class HighlightPipe implements PipeTransform {
transform(text: string, search): string {
transform(text: string, search: string, includes = false, flags = 'i'): string {
const pattern = search
.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
const regex = new RegExp('^' + pattern, 'i');
const regex = new RegExp((!includes ? '^' : '') + pattern, flags);
return search ? text.replace(regex, match => `<b>${match}</b>`) : text;
}
}

View File

@ -194,6 +194,7 @@ import { ShortNumberPipe } from '@shared/pipe/short-number.pipe';
import { ToggleHeaderComponent, ToggleOption } from '@shared/components/toggle-header.component';
import { RuleChainSelectComponent } from '@shared/components/rule-chain/rule-chain-select.component';
import { ToggleSelectComponent } from '@shared/components/toggle-select.component';
import { UnitInputComponent } from '@shared/components/unit-input.component';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService;
@ -367,6 +368,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
ToggleHeaderComponent,
ToggleOption,
ToggleSelectComponent,
UnitInputComponent,
RuleChainSelectComponent
],
imports: [
@ -597,6 +599,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
ToggleHeaderComponent,
ToggleOption,
ToggleSelectComponent,
UnitInputComponent,
RuleChainSelectComponent
]
})

View File

@ -3860,6 +3860,15 @@
"just-now": "Just now",
"ago": "ago"
},
"unit": {
"celsius": "Celsius",
"kelvin": "Kelvin",
"fahrenheit": "Fahrenheit",
"percentage": "Percentage",
"second": "Second",
"minute": "Minute",
"hour": "Hour"
},
"user": {
"user": "User",
"users": "Users",

View File

@ -177,9 +177,15 @@
opacity: 0;
}
}
&:not(.mat-mdc-form-field-has-icon-suffix) {
.mat-mdc-text-field-wrapper {
&.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) {
padding-right: 12px;
}
}
}
.mat-mdc-text-field-wrapper {
&.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) {
padding-left: 12px;
&:not(.mdc-text-field--focused):not(.mdc-text-field--disabled):not(:hover) {
.mdc-notched-outline__leading, .mdc-notched-outline__trailing {
@ -233,7 +239,9 @@
}
&.number {
.mat-mdc-text-field-wrapper {
&.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) {
padding-right: 4px;
}
.mat-mdc-form-field-infix {
input.mdc-text-field__input[type=number]::-webkit-inner-spin-button,
input.mdc-text-field__input[type=number]::-webkit-outer-spin-button {