UI: Refactoring unit-input and panel

This commit is contained in:
Vladyslav_Prykhodko 2025-04-29 15:35:18 +03:00
parent f5f794191d
commit caa643d157
11 changed files with 245 additions and 243 deletions

View File

@ -158,68 +158,31 @@ export class Converter<
return result / destination.unit.to_anchor; return result / destination.unit.to_anchor;
} }
// toBest(options?: { getDefaultUnit(measureName: TMeasures | (string & {}), unitSystem: UnitSystem): TUnits {
// exclude?: (TUnits | (string & {}))[]; if (!this.isMeasure(measureName)) {
// cutOffNumber?: number; return null;
// system?: TSystems | (string & {}); }
// }): BestResult<TUnits> | null { const measure = this.measureData[measureName];
// if (this.origin == null) let currentUnitSystem = unitSystem;
// throw new OperationOrderError('.toBest must be called after .from'); let units = measure[currentUnitSystem].units;
// if (isUndefinedOrNull(units)) {
// const isNegative = this.val < 0; if (currentUnitSystem === UnitSystem.IMPERIAL) {
// currentUnitSystem = UnitSystem.METRIC;
// let exclude: (TUnits | (string & {}))[] = []; units = measure[currentUnitSystem].units;
// let cutOffNumber = isNegative ? -1 : 1; }
// let system: TSystems | (string & {}) = this.origin.system; if (!units) {
// console.log(`Measure "${measureName}" in ${currentUnitSystem} system is not found.`);
// if (typeof options === 'object') { return null;
// exclude = options.exclude ?? []; }
// cutOffNumber = options.cutOffNumber ?? cutOffNumber; }
// system = options.system ?? this.origin.system; for (const [abbr, unit] of Object.entries(
// } units as Partial<Record<TUnits, Unit>>
// ) as [TUnits, Unit][]) {
// let best: BestResult<TUnits> | null = null; if (unit.to_anchor === 1 && (isUndefinedOrNull(unit.anchor_shift) || unit.anchor_shift === 0)) {
// /** return abbr;
// Looks through every possibility for the 'best' available unit. }
// i.e. Where the value has the fewest numbers before the decimal point, }
// but is still higher than 1. }
// */
// for (const possibility of this.possibilities()) {
// const unit = this.describe(possibility);
// const isIncluded = exclude.indexOf(possibility) === -1;
//
// if (isIncluded && unit.system === system) {
// const result = this.to(possibility);
// if (isNegative ? result > cutOffNumber : result < cutOffNumber) {
// continue;
// }
// if (
// best === null ||
// (isNegative
// ? result <= cutOffNumber && result > best.val
// : result >= cutOffNumber && result < best.val)
// ) {
// best = {
// val: result,
// unit: possibility,
// name: unit.name,
// tags: unit.tags
// };
// }
// }
// }
//
// if (best == null) {
// return {
// val: this.val,
// unit: this.origin.abbr,
// name: this.origin.unit.name,
// tags: this.origin.unit.tags
// };
// }
//
// return best;
// }
getUnit(abbr: TUnits | (string & {})): Conversion<TMeasures, TUnits> | null { getUnit(abbr: TUnits | (string & {})): Conversion<TMeasures, TUnits> | null {
return this.unitCache.get(abbr) ?? null; return this.unitCache.get(abbr) ?? null;

View File

@ -76,6 +76,10 @@ export class UnitService {
return this.converter.describe(abbr); return this.converter.describe(abbr);
} }
getDefaultUnit(measure: AllMeasures, unitSystem: UnitSystem): AllMeasuresUnits {
return this.converter.getDefaultUnit(measure, unitSystem);
}
geUnitConvertor(from: string, to: string): TbUnitConvertor { geUnitConvertor(from: string, to: string): TbUnitConvertor {
return this.converter.convertor(from, to); return this.converter.convertor(from, to);
} }

View File

@ -33,6 +33,7 @@ import {
ColorProcessor, ColorProcessor,
ComponentStyle, ComponentStyle,
DateFormatProcessor, DateFormatProcessor,
FormatValueProcessor,
getDataKey, getDataKey,
getLabel, getLabel,
getSingleTsValue, getSingleTsValue,
@ -46,7 +47,6 @@ import { WidgetComponent } from '@home/components/widget/widget.component';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ImagePipe } from '@shared/pipe/image.pipe'; import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
import { FormatValueProcessor } from '@shared/models/unit.models';
const squareLayoutSize = 160; const squareLayoutSize = 160;
const horizontalLayoutHeight = 80; const horizontalLayoutHeight = 80;

View File

@ -16,53 +16,54 @@
--> -->
<div class="tb-convert-settings-panel"> <div class="tb-convert-settings-panel">
<div class="tb-convert-settings-title">Unit convertion settings</div> <div class="tb-convert-settings-title" translate>unit.convert.units-conversion-settings</div>
<div class="tb-convert-settings-panel-content" [formGroup]="convertUnitForm"> <div class="tb-convert-settings-panel-content" [formGroup]="convertUnitForm">
<div class="tb-form-row"> <div class="tb-form-row">
<div class="min-w-25">From</div> <div class="min-w-25" [class.tb-disabled-label]="convertUnitForm.get('from').disabled" translate>unit.convert.convert-from</div>
<tb-unit-input class="flex-1" [required]="required" formControlName="from"></tb-unit-input> <tb-unit-input #unitFrom class="flex-1" [required]="required" formControlName="from"></tb-unit-input>
</div> </div>
<div class="tb-form-row"> <div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="convertUnit"> <mat-slide-toggle class="mat-slide fixed-title-width" formControlName="convertUnit">
Convert units <div tb-hint-tooltip-icon="{{ 'unit.convert.convert-unit-hint' | translate }}">
{{ 'unit.convert.convert-unit' | translate }}
</div>
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
@if(convertUnitForm.get('convertUnit').value) { <div class="tb-form-row">
<div class="tb-form-row"> <div class="min-w-25" [class.tb-disabled-label]="convertUnitForm.get('METRIC').disabled" translate>unit.convert.to-metric</div>
<div class="min-w-25">Metrical</div> <tb-unit-input class="flex-1"
<tb-unit-input class="flex-1" formControlName="METRIC"
formControlName="METRIC" [unitSystem]="UnitSystem.METRIC"
[unitSystem]="UnitSystem.METRIC" [measure]="measure">
[measure]="measure"> </tb-unit-input>
</tb-unit-input> </div>
</div> <div class="tb-form-row">
<div class="tb-form-row"> <div class="min-w-25" [class.tb-disabled-label]="convertUnitForm.get('IMPERIAL').disabled" translate>unit.convert.to-imperial</div>
<div class="min-w-25">Imperial</div> <tb-unit-input class="flex-1"
<tb-unit-input class="flex-1" formControlName="IMPERIAL"
formControlName="IMPERIAL" [unitSystem]="UnitSystem.IMPERIAL"
[unitSystem]="UnitSystem.IMPERIAL" [measure]="measure">
[measure]="measure"> </tb-unit-input>
</tb-unit-input> </div>
</div> <div class="tb-form-row">
<div class="tb-form-row"> <div class="min-w-25" [class.tb-disabled-label]="convertUnitForm.get('HYBRID').disabled" translate>unit.convert.to-imperial</div>
<div class="min-w-25">Hybrid</div> <tb-unit-input class="flex-1"
<tb-unit-input class="flex-1" formControlName="HYBRID"
formControlName="HYBRID" [measure]="measure">
[measure]="measure"> </tb-unit-input>
</tb-unit-input> </div>
</div>
}
</div> </div>
<div class="tb-convert-settings-panel-buttons"> <div class="tb-convert-settings-panel-buttons">
<button mat-button <button mat-button
color="primary" color="primary"
type="button" type="button"
(click)="cancel()"> (click)="cancel()">
{{ 'action.cancel' | translate }} {{ (disabled ? 'action.close': 'action.cancel') | translate }}
</button> </button>
<button mat-raised-button <button mat-raised-button
color="primary" color="primary"
type="button" type="button"
*ngIf="!disabled"
(click)="applyUnitSettings()" (click)="applyUnitSettings()"
[disabled]="convertUnitForm.invalid || convertUnitForm.pristine"> [disabled]="convertUnitForm.invalid || convertUnitForm.pristine">
{{ 'action.apply' | translate }} {{ 'action.apply' | translate }}

View File

@ -14,14 +14,16 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output, ViewChild, ViewEncapsulation } from '@angular/core';
import { TbUnit, UnitDescription, UnitSystem } from '@shared/models/unit.models'; import { TbUnit, UnitDescription, UnitSystem } from '@shared/models/unit.models';
import { TbPopoverComponent } from '@shared/components/popover.component'; import { TbPopoverComponent } from '@shared/components/popover.component';
import { FormBuilder, Validators } from '@angular/forms'; import { FormBuilder, Validators } from '@angular/forms';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { UnitService } from '@core/services/unit/unit.service'; import { UnitService } from '@core/services/unit/unit.service';
import { AllMeasures } from '@core/services/unit/definitions/all'; import { AllMeasures } from '@core/services/unit/definitions/all';
import { debounceTime } from 'rxjs/operators'; import { debounceTime, first } from 'rxjs/operators';
import { isEmptyStr } from '@core/utils';
import type { UnitInputComponent } from '@shared/components/unit-input.component';
@Component({ @Component({
selector: 'tb-covert-unit-settings-panel', selector: 'tb-covert-unit-settings-panel',
@ -32,12 +34,17 @@ import { debounceTime } from 'rxjs/operators';
}) })
export class ConvertUnitSettingsPanelComponent implements OnInit { export class ConvertUnitSettingsPanelComponent implements OnInit {
@ViewChild('unitFrom', {static: true}) unitFrom: UnitInputComponent;
@Input() @Input()
unit: TbUnit; unit: TbUnit;
@Input() @Input()
required: boolean; required: boolean;
@Input()
disabled: boolean;
@Output() @Output()
unitSettingsApplied = new EventEmitter<TbUnit>(); unitSettingsApplied = new EventEmitter<TbUnit>();
@ -67,15 +74,22 @@ export class ConvertUnitSettingsPanelComponent implements OnInit {
this.convertUnitForm.get('convertUnit').enable({emitEvent: true}); this.convertUnitForm.get('convertUnit').enable({emitEvent: true});
this.measure = unitDescription.measure; this.measure = unitDescription.measure;
if (unitDescription.system === UnitSystem.IMPERIAL) { if (unitDescription.system === UnitSystem.IMPERIAL) {
this.convertUnitForm.get('METRIC').setValue(this.unitService.getDefaultUnit(this.measure, UnitSystem.METRIC), {emitEvent: false});
this.convertUnitForm.get('IMPERIAL').setValue(unit, {emitEvent: false}); this.convertUnitForm.get('IMPERIAL').setValue(unit, {emitEvent: false});
this.convertUnitForm.get('HYBRID').setValue(unit, {emitEvent: false}); this.convertUnitForm.get('HYBRID').setValue(unit, {emitEvent: false});
} else { } else {
this.convertUnitForm.get('METRIC').setValue(unit, {emitEvent: false}); this.convertUnitForm.get('METRIC').setValue(unit, {emitEvent: false});
this.convertUnitForm.get('IMPERIAL').setValue(this.unitService.getDefaultUnit(this.measure, UnitSystem.IMPERIAL), {emitEvent: false});
this.convertUnitForm.get('HYBRID').setValue(unit, {emitEvent: false}); this.convertUnitForm.get('HYBRID').setValue(unit, {emitEvent: false});
} }
} else { } else {
this.convertUnitForm.get('convertUnit').setValue(false, {onlySelf: true}); this.convertUnitForm.get('convertUnit').setValue(false, {onlySelf: true});
this.convertUnitForm.get('convertUnit').disable({emitEvent: false}); this.convertUnitForm.get('convertUnit').disable({emitEvent: false});
this.convertUnitForm.patchValue({
METRIC: '',
IMPERIAL: '',
HYBRID: ''
}, {emitEvent: false});
} }
}) })
@ -91,9 +105,6 @@ export class ConvertUnitSettingsPanelComponent implements OnInit {
this.convertUnitForm.get('IMPERIAL').disable({emitEvent: false}); this.convertUnitForm.get('IMPERIAL').disable({emitEvent: false});
this.convertUnitForm.get('HYBRID').disable({emitEvent: false}); this.convertUnitForm.get('HYBRID').disable({emitEvent: false});
} }
setTimeout(() => {
this.popover.updatePosition();
}, 0);
}); });
} }
@ -120,6 +131,13 @@ export class ConvertUnitSettingsPanelComponent implements OnInit {
} else { } else {
this.convertUnitForm.get('convertUnit').disable({emitEvent: false}); this.convertUnitForm.get('convertUnit').disable({emitEvent: false});
} }
if (this.disabled) {
this.convertUnitForm.disable({emitEvent: false});
} else if (this.unit === null || isEmptyStr(this.unit)) {
this.popover.tbAnimationDone.pipe(first()).subscribe(() => {
this.unitFrom.unitInput.nativeElement.focus();
});
}
} }
cancel() { cancel() {

View File

@ -15,23 +15,20 @@
limitations under the License. limitations under the License.
--> -->
<mat-form-field appearance="outline" class="tb-inline-field tb-suffix-show-on-hover w-full flex-1" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="tb-inline-field tb-suffix-show-on-hover w-full flex-1" subscriptSizing="dynamic"
(click)="openConvertSettingsPopup($event)">
<input matInput #unitInput [formControl]="unitsFormControl" <input matInput #unitInput [formControl]="unitsFormControl"
placeholder="{{ 'widget-config.set' | translate }}" placeholder="{{ 'widget-config.set' | translate }}"
[class.!pointer-events-none]="disabled"
(focusin)="onFocus()" (focusin)="onFocus()"
[matAutocomplete]="unitsAutocomplete"> [matAutocomplete]="unitsAutocomplete"
<button type="button" [matAutocompleteDisabled]="allowConverted">
*ngIf="!disabled && allowConverted"
class="tb-icon-24"
[class.mr-2]="!unitsFormControl.value || disabled || unitsFormControl.invalid"
matSuffix mat-icon-button (click)="openConvertSettingsPopup($event)">
<tb-icon>mdi:tape-measure</tb-icon>
</button>
<button *ngIf="unitsFormControl.value && !disabled && unitsFormControl.valid" <button *ngIf="unitsFormControl.value && !disabled && unitsFormControl.valid"
type="button" type="button"
class="tb-icon-24 mr-2" class="tb-icon-24 mr-2"
[class.mr-2]="!allowConverted || !isUnitMapping"
matSuffix mat-icon-button aria-label="Clear" matSuffix mat-icon-button aria-label="Clear"
(click)="clear()"> (click)="clear($event)">
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
<mat-icon matSuffix <mat-icon matSuffix
@ -41,29 +38,27 @@
*ngIf="unitsFormControl.hasError('required')" class="material-icons tb-suffix-show-always tb-error"> *ngIf="unitsFormControl.hasError('required')" class="material-icons tb-suffix-show-always tb-error">
warning warning
</mat-icon> </mat-icon>
<tb-icon matSuffix
[color]="disabled ? null : 'primary'"
matTooltipPosition="above"
[matTooltip]="'unit.convert.set-units-conversion-settings' | translate"
*ngIf="allowConverted && isUnitMapping" class="material-icons tb-icon-24 tb-suffix-show-always">
mdi:swap-vertical-circle-outline
</tb-icon>
<mat-autocomplete <mat-autocomplete
#unitsAutocomplete="matAutocomplete" #unitsAutocomplete="matAutocomplete"
class="tb-autocomplete tb-unit-input-autocomplete" class="tb-autocomplete tb-unit-input-autocomplete"
panelWidth="fit-content" panelWidth="fit-content"
[displayWith]="displayUnitFn.bind(this)"> [displayWith]="displayUnitFn.bind(this)">
@for (group of filteredUnits | async; track group[0]) { @for (group of filteredUnits | async; track group[0]) {
@if ((fetchUnits$ | async).length > 1) { <mat-optgroup [label]="'unit.measures.' + group[0] | translate">
<mat-optgroup [label]="'unit.measures.' + group[0] | translate">
@for(unit of group[1]; track unit.abbr) {
<mat-option [value]="unit">
<span class="tb-unit-name flex-1" [innerHTML]="unit.name | highlight:searchText:true:'ig'"></span>
<span class="tb-unit-symbol" [innerHTML]="unit.abbr | highlight:searchText:true:'ig'"></span>
</mat-option>
}
</mat-optgroup>
} @else {
@for(unit of group[1]; track unit.abbr) { @for(unit of group[1]; track unit.abbr) {
<mat-option [value]="unit"> <mat-option [value]="unit">
<span class="tb-unit-name flex-1" [innerHTML]="unit.name | highlight:searchText:true:'ig'"></span> <span class="tb-unit-name flex-1" [innerHTML]="unit.name | highlight:searchText:true:'ig'"></span>
<span class="tb-unit-symbol" [innerHTML]="unit.abbr | highlight:searchText:true:'ig'"></span> <span class="tb-unit-symbol" [innerHTML]="unit.abbr | highlight:searchText:true:'ig'"></span>
</mat-option> </mat-option>
} }
} </mat-optgroup>
} }
</mat-autocomplete> </mat-autocomplete>
</mat-form-field> </mat-form-field>

View File

@ -15,6 +15,7 @@
*/ */
.tb-autocomplete.tb-unit-input-autocomplete { .tb-autocomplete.tb-unit-input-autocomplete {
.mat-mdc-optgroup-label { .mat-mdc-optgroup-label {
min-height: 36px;
.mdc-list-item__primary-text { .mdc-list-item__primary-text {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;

View File

@ -37,7 +37,7 @@ import { AllMeasures } from '@core/services/unit/definitions/all';
import { UnitService } from '@core/services/unit/unit.service'; import { UnitService } from '@core/services/unit/unit.service';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { ConvertUnitSettingsPanelComponent } from '@shared/components/convert-unit-settings-panel.component'; import { ConvertUnitSettingsPanelComponent } from '@shared/components/convert-unit-settings-panel.component';
import { isNotEmptyStr } from '@core/utils'; import { isNotEmptyStr, isObject } from '@core/utils';
@Component({ @Component({
selector: 'tb-unit-input', selector: 'tb-unit-input',
@ -59,7 +59,7 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit, OnChang
unitsFormControl: FormControl<TbUnit | UnitDescription>; unitsFormControl: FormControl<TbUnit | UnitDescription>;
@Input() @Input({transform: booleanAttribute})
disabled: boolean; disabled: boolean;
@Input({transform: booleanAttribute}) @Input({transform: booleanAttribute})
@ -81,13 +81,13 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit, OnChang
searchText = ''; searchText = '';
isGroupOption = false; isUnitMapping = false;
private dirty = false; private dirty = false;
private modelValue: TbUnit | null; private modelValue: TbUnit | null;
fetchUnits$: Observable<Array<[AllMeasures, Array<UnitDescription>]>> = null; private fetchUnits$: Observable<Array<[AllMeasures, Array<UnitDescription>]>> = null;
private propagateChange = (_val: any) => {}; private propagateChange = (_val: any) => {};
@ -109,9 +109,6 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit, OnChang
}), }),
mergeMap(symbol => this.fetchUnits(symbol)) mergeMap(symbol => this.fetchUnits(symbol))
); );
if (!!this.measure || !!this.tagFilter) {
this.isGroupOption = true;
}
} }
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
@ -121,11 +118,6 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit, OnChang
if (propName === 'measure' || propName === 'unitSystem') { if (propName === 'measure' || propName === 'unitSystem') {
this.fetchUnits$ = null; this.fetchUnits$ = null;
this.dirty = true; this.dirty = true;
if (!!this.measure || !!this.tagFilter) {
this.isGroupOption = true;
} else {
this.isGroupOption = false;
}
} }
} }
} }
@ -136,8 +128,10 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit, OnChang
this.modelValue = symbol; this.modelValue = symbol;
if (typeof symbol === 'string') { if (typeof symbol === 'string') {
this.unitsFormControl.patchValue(this.unitService.getUnitDescription(symbol) ?? symbol, {emitEvent: false}); this.unitsFormControl.patchValue(this.unitService.getUnitDescription(symbol) ?? symbol, {emitEvent: false});
this.isUnitMapping = false;
} else { } else {
this.unitsFormControl.patchValue(symbol, {emitEvent: false}); this.unitsFormControl.patchValue(symbol, {emitEvent: false});
this.isUnitMapping = symbol !== null;
} }
this.dirty = true; this.dirty = true;
} }
@ -172,18 +166,25 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit, OnChang
} }
} }
clear() { clear($event: Event) {
$event.stopPropagation();
this.unitsFormControl.patchValue(null, {emitEvent: true}); this.unitsFormControl.patchValue(null, {emitEvent: true});
setTimeout(() => { if (!this.allowConverted) {
this.unitInput.nativeElement.blur(); setTimeout(() => {
this.unitInput.nativeElement.focus(); this.unitInput.nativeElement.blur();
}, 0); this.unitInput.nativeElement.focus();
}, 0);
}
} }
openConvertSettingsPopup($event: Event) { openConvertSettingsPopup($event: Event) {
if (!this.allowConverted) {
return;
}
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
this.unitInput.nativeElement.blur();
const trigger = this.elementRef.nativeElement; const trigger = this.elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) { if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger); this.popoverService.hidePopover(trigger);
@ -196,7 +197,8 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit, OnChang
preferredPlacement: ['left', 'bottom', 'top'], preferredPlacement: ['left', 'bottom', 'top'],
context: { context: {
unit: this.getTbUnit(this.unitsFormControl.value), unit: this.getTbUnit(this.unitsFormControl.value),
required: this.required required: this.required,
disabled: this.disabled,
}, },
isModal: true isModal: true
}); });
@ -212,6 +214,7 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit, OnChang
const res = this.getTbUnit(value); const res = this.getTbUnit(value);
if (this.modelValue !== res) { if (this.modelValue !== res) {
this.modelValue = res; this.modelValue = res;
this.isUnitMapping = (res !== null && isObject(res));
this.propagateChange(this.modelValue); this.propagateChange(this.modelValue);
} }
} }

View File

@ -15,9 +15,6 @@
/// ///
import { AllMeasures } from '@core/services/unit/definitions/all'; import { AllMeasures } from '@core/services/unit/definitions/all';
import { Injector } from '@angular/core';
import { isDefinedAndNotNull, isNotEmptyStr, isNumeric } from '@core/utils';
import { UnitService } from '@core/services/unit/unit.service';
export enum UnitsType { export enum UnitsType {
capacity = 'capacity' capacity = 'capacity'
@ -74,104 +71,3 @@ export const searchUnits = (_units: Array<UnitDescription>, searchText: string):
u.name.toUpperCase().includes(searchText) || u.name.toUpperCase().includes(searchText) ||
searchUnitTags(u, searchText) searchUnitTags(u, searchText)
); );
export interface FormatValueSettingProcessor {
dec?: number;
units?: TbUnit;
showZeroDecimals?: boolean;
}
export abstract class FormatValueProcessor {
static fromSettings($injector: Injector, settings: FormatValueSettingProcessor): FormatValueProcessor {
if (typeof settings.units !== 'string' && isDefinedAndNotNull(settings.units?.from)) {
return new ConvertUnitProcessor($injector, settings)
} else {
return new SimpleUnitProcessor($injector, settings);
}
}
protected constructor(protected $injector: Injector,
protected settings: FormatValueSettingProcessor) {
}
abstract format(value: any): string;
}
export class SimpleUnitProcessor extends FormatValueProcessor {
private readonly isDefinedUnit: boolean;
private readonly isDefinedDec: boolean;
private readonly showZeroDecimals: boolean;
constructor(protected $injector: Injector,
protected settings: FormatValueSettingProcessor) {
super($injector, settings);
this.isDefinedUnit = isNotEmptyStr(settings.units);
this.isDefinedDec = isDefinedAndNotNull(settings.dec);
this.showZeroDecimals = !!settings.showZeroDecimals;
}
format(value: any): string {
if (isDefinedAndNotNull(value) && isNumeric(value) && (this.isDefinedDec || this.isDefinedUnit || Number(value).toString() === value)) {
let formatted = value;
if (this.isDefinedDec) {
formatted = Number(formatted).toFixed(this.settings.dec);
}
if (!this.showZeroDecimals) {
formatted = Number(formatted)
}
formatted = formatted.toString();
if (this.isDefinedUnit) {
formatted += ` ${this.settings.units}`;
}
return formatted;
}
return value ?? '';
}
}
export class ConvertUnitProcessor extends FormatValueProcessor {
private readonly isDefinedDec: boolean;
private readonly showZeroDecimals: boolean;
private readonly unitConvertor: TbUnitConvertor;
private readonly unitAbbr: string;
constructor(protected $injector: Injector,
protected settings: FormatValueSettingProcessor) {
super($injector, settings);
const unitService = this.$injector.get(UnitService);
const userUnitSystem = unitService.getUnitSystem();
const unit = settings.units as TbUnitMapping;
const fromUnit = unit.from;
this.unitAbbr = isNotEmptyStr(unit[userUnitSystem]) ? unit[userUnitSystem] : fromUnit;
try {
this.unitConvertor = unitService.geUnitConvertor(fromUnit, this.unitAbbr);
} catch (e) {/**/}
this.isDefinedDec = isDefinedAndNotNull(settings.dec);
this.showZeroDecimals = !!settings.showZeroDecimals;
}
format(value: any): string {
if (isDefinedAndNotNull(value) && isNumeric(value)) {
let formatted: number | string = Number(value);
if (this.unitConvertor) {
formatted = this.unitConvertor(value);
}
if (this.isDefinedDec) {
formatted = Number(formatted).toFixed(this.settings.dec);
}
if (!this.showZeroDecimals) {
formatted = Number(formatted)
}
formatted = formatted.toString();
if (this.unitAbbr) {
formatted += ` ${this.unitAbbr}`;
}
return formatted;
}
return value ?? '';
}
}

View File

@ -14,7 +14,15 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { isDefinedAndNotNull, isNumber, isNumeric, isUndefinedOrNull, mergeDeep, parseFunction } from '@core/utils'; import {
isDefinedAndNotNull,
isNotEmptyStr,
isNumber,
isNumeric,
isUndefinedOrNull,
mergeDeep,
parseFunction
} from '@core/utils';
import { import {
DataEntry, DataEntry,
DataKey, DataKey,
@ -45,6 +53,8 @@ import {
WidgetSubscriptionCallbacks, WidgetSubscriptionCallbacks,
WidgetSubscriptionOptions WidgetSubscriptionOptions
} from '@core/api/widget-api.models'; } from '@core/api/widget-api.models';
import { UnitService } from '@core/services/unit/unit.service';
import { TbUnit, TbUnitConvertor, TbUnitMapping } from '@shared/models/unit.models';
export type ComponentStyle = {[klass: string]: any}; export type ComponentStyle = {[klass: string]: any};
@ -852,6 +862,107 @@ export class AutoDateFormatProcessor extends DateFormatProcessor {
} }
} }
export interface FormatValueSettingProcessor {
dec?: number;
units?: TbUnit;
showZeroDecimals?: boolean;
}
export abstract class FormatValueProcessor {
static fromSettings($injector: Injector, settings: FormatValueSettingProcessor): FormatValueProcessor {
if (typeof settings.units !== 'string' && isDefinedAndNotNull(settings.units?.from)) {
return new ConvertUnitProcessor($injector, settings)
} else {
return new SimpleUnitProcessor($injector, settings);
}
}
protected constructor(protected $injector: Injector,
protected settings: FormatValueSettingProcessor) {
}
abstract format(value: any): string;
}
export class SimpleUnitProcessor extends FormatValueProcessor {
private readonly isDefinedUnit: boolean;
private readonly isDefinedDec: boolean;
private readonly showZeroDecimals: boolean;
constructor(protected $injector: Injector,
protected settings: FormatValueSettingProcessor) {
super($injector, settings);
this.isDefinedUnit = isNotEmptyStr(settings.units);
this.isDefinedDec = isDefinedAndNotNull(settings.dec);
this.showZeroDecimals = !!settings.showZeroDecimals;
}
format(value: any): string {
if (isDefinedAndNotNull(value) && isNumeric(value) && (this.isDefinedDec || this.isDefinedUnit || Number(value).toString() === value)) {
let formatted = value;
if (this.isDefinedDec) {
formatted = Number(formatted).toFixed(this.settings.dec);
}
if (!this.showZeroDecimals) {
formatted = Number(formatted)
}
formatted = formatted.toString();
if (this.isDefinedUnit) {
formatted += ` ${this.settings.units}`;
}
return formatted;
}
return value ?? '';
}
}
export class ConvertUnitProcessor extends FormatValueProcessor {
private readonly isDefinedDec: boolean;
private readonly showZeroDecimals: boolean;
private readonly unitConvertor: TbUnitConvertor;
private readonly unitAbbr: string;
constructor(protected $injector: Injector,
protected settings: FormatValueSettingProcessor) {
super($injector, settings);
const unitService = this.$injector.get(UnitService);
const userUnitSystem = unitService.getUnitSystem();
const unit = settings.units as TbUnitMapping;
const fromUnit = unit.from;
this.unitAbbr = isNotEmptyStr(unit[userUnitSystem]) ? unit[userUnitSystem] : fromUnit;
try {
this.unitConvertor = unitService.geUnitConvertor(fromUnit, this.unitAbbr);
} catch (e) {/**/}
this.isDefinedDec = isDefinedAndNotNull(settings.dec);
this.showZeroDecimals = !!settings.showZeroDecimals;
}
format(value: any): string {
if (isDefinedAndNotNull(value) && isNumeric(value)) {
let formatted: number | string = Number(value);
if (this.unitConvertor) {
formatted = this.unitConvertor(value);
}
if (this.isDefinedDec) {
formatted = Number(formatted).toFixed(this.settings.dec);
}
if (!this.showZeroDecimals) {
formatted = Number(formatted)
}
formatted = formatted.toString();
if (this.unitAbbr) {
formatted += ` ${this.unitAbbr}`;
}
return formatted;
}
return value ?? '';
}
}
const intervalToFormatTimeUnit = (interval: Interval): FormatTimeUnit => { const intervalToFormatTimeUnit = (interval: Interval): FormatTimeUnit => {
const intervalValue = IntervalMath.numberValue(interval); const intervalValue = IntervalMath.numberValue(interval);
if (intervalValue < SECOND) { if (intervalValue < SECOND) {

View File

@ -5847,6 +5847,16 @@
"background-blur": "Background blur" "background-blur": "Background blur"
}, },
"unit": { "unit": {
"convert": {
"set-units-conversion-settings": "Set units conversion settings",
"units-conversion-settings": "Units conversion settings",
"convert-from": "Convert from",
"to-metric": "To metric",
"to-imperial": "To imperial",
"to-hybrid": "To hybrid",
"convert-unit": "Convert unit",
"convert-unit-hint": "Work only with the default unit"
},
"unit-system": "Unit system", "unit-system": "Unit system",
"unit-system-type": { "unit-system-type": {
"AUTO": "Auto", "AUTO": "Auto",