diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index b7c382f6a0..4a2b4f30f2 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -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, diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html index 574209c6f8..3a12e76749 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html +++ b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html @@ -42,7 +42,7 @@ #entityAliasAutocomplete="matAutocomplete" [displayWith]="displayEntityAliasFn"> - +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html index c87c367230..7de230fe64 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html @@ -51,9 +51,9 @@
widget-config.units-short
- - +
widget-config.decimals-short
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html index 18e307787a..b603f98a4d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html @@ -148,9 +148,9 @@
- - +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss index 5e4d80e360..41a003985e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss @@ -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; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss index b99bce368e..6ce13d7adc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss @@ -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 { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html index 2687065dc7..a32735e0d1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html @@ -48,9 +48,9 @@
widget-config.units-short
- - +
widget-config.decimals-short
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts index 186a89537e..3777973eff 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts @@ -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 ] }) diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.html b/ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.html deleted file mode 100644 index 1381ad31a8..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.ts deleted file mode 100644 index e61511698d..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.ts +++ /dev/null @@ -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}); - } - } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index c1b150acdf..ab226987ca 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -267,9 +267,9 @@
widget-config.data-settings
widget-config.units
- - +
widget-config.decimals
diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html index 6d8a424352..fdf6b2aeac 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html @@ -33,7 +33,7 @@ #entityAutocomplete="matAutocomplete" [displayWith]="displayEntityFn"> - + diff --git a/ui-ngx/src/app/shared/components/public-api.ts b/ui-ngx/src/app/shared/components/public-api.ts index 32c709ecb0..3ed56d0256 100644 --- a/ui-ngx/src/app/shared/components/public-api.ts +++ b/ui-ngx/src/app/shared/components/public-api.ts @@ -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'; diff --git a/ui-ngx/src/app/shared/components/unit-input.component.html b/ui-ngx/src/app/shared/components/unit-input.component.html new file mode 100644 index 0000000000..dee26af8d0 --- /dev/null +++ b/ui-ngx/src/app/shared/components/unit-input.component.html @@ -0,0 +1,39 @@ + + + + + + + + + + + diff --git a/ui-ngx/src/app/shared/components/unit-input.component.scss b/ui-ngx/src/app/shared/components/unit-input.component.scss new file mode 100644 index 0000000000..25280e51ec --- /dev/null +++ b/ui-ngx/src/app/shared/components/unit-input.component.scss @@ -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); + } + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/unit-input.component.ts b/ui-ngx/src/app/shared/components/unit-input.component.ts new file mode 100644 index 0000000000..4b70c31cec --- /dev/null +++ b/ui-ngx/src/app/shared/components/unit-input.component.ts @@ -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>; + + searchText = ''; + + private dirty = false; + + private translatedUnits: Array = 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(''), + 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> { + 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); + } +} diff --git a/ui-ngx/src/app/shared/models/unit.models.ts b/ui-ngx/src/app/shared/models/unit.models.ts new file mode 100644 index 0000000000..7ac0d4db57 --- /dev/null +++ b/ui-ngx/src/app/shared/models/unit.models.ts @@ -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 = [ + { + 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, searchText: string): Array => _units.filter( + u => u.symbol.toUpperCase().includes(searchText.toUpperCase()) || + u.name.toUpperCase().includes(searchText.toUpperCase()) || + searchUnitTags(u, searchText) +); diff --git a/ui-ngx/src/app/shared/pipe/highlight.pipe.ts b/ui-ngx/src/app/shared/pipe/highlight.pipe.ts index 0f8595fc3b..5d712c5b93 100644 --- a/ui-ngx/src/app/shared/pipe/highlight.pipe.ts +++ b/ui-ngx/src/app/shared/pipe/highlight.pipe.ts @@ -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 => `${match}`) : text; } } diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index cf8b271171..f7c37e4761 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -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 ] }) diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index c5ec1fca40..82f10b7f4c 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -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", diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss index a112a633f9..1f27e59279 100644 --- a/ui-ngx/src/form.scss +++ b/ui-ngx/src/form.scss @@ -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-right: 12px; 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 { - padding-right: 4px; + &.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 {