UI: Refactoring ValueFormatProcessor and fixed unit input component

This commit is contained in:
Vladyslav_Prykhodko 2025-05-09 10:19:37 +03:00
parent 76237337b2
commit 9662b263e4
6 changed files with 111 additions and 96 deletions

View File

@ -88,11 +88,11 @@ export class UnitService {
return this.converter.getUnitConverter(unit as string, to); return this.converter.getUnitConverter(unit as string, to);
} }
getTargetUnitSymbol(unit: TbUnitMapping): string { getTargetUnitSymbol(unit: TbUnitMapping | string): string {
if (isObject(unit)) { if (isObject(unit)) {
return isNotEmptyStr(unit[this.currentUnitSystem]) ? unit[this.currentUnitSystem] : unit.from; return isNotEmptyStr(unit[this.currentUnitSystem]) ? unit[this.currentUnitSystem] : (unit as TbUnitMapping).from;
} }
return null; return typeof unit === 'string' ? unit : null;
} }
convertUnitValue(value: number, unit: TbUnitMapping): number; convertUnitValue(value: number, unit: TbUnitMapping): number;

View File

@ -49,7 +49,7 @@
<div class="tb-form-row space-between" *ngIf="!hideDataKeyUnits"> <div class="tb-form-row space-between" *ngIf="!hideDataKeyUnits">
<div translate>widget-config.units-short</div> <div translate>widget-config.units-short</div>
<tb-unit-input <tb-unit-input
[allowConverted]="supportsUnitConversion" [supportsUnitConversion]="supportsUnitConversion"
formControlName="units"> formControlName="units">
</tb-unit-input> </tb-unit-input>
</div> </div>

View File

@ -22,11 +22,11 @@
[class.!pointer-events-none]="disabled" [class.!pointer-events-none]="disabled"
(focusin)="onFocus()" (focusin)="onFocus()"
[matAutocomplete]="unitsAutocomplete" [matAutocomplete]="unitsAutocomplete"
[matAutocompleteDisabled]="allowConverted"> [matAutocompleteDisabled]="supportsUnitConversion">
<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" [class.mr-2]="!supportsUnitConversion || !isUnitMapping"
matSuffix mat-icon-button aria-label="Clear" matSuffix mat-icon-button aria-label="Clear"
(click)="clear($event)"> (click)="clear($event)">
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
@ -42,7 +42,7 @@
[color]="disabled ? null : 'primary'" [color]="disabled ? null : 'primary'"
matTooltipPosition="above" matTooltipPosition="above"
[matTooltip]="'unit.convert.set-units-conversion-settings' | translate" [matTooltip]="'unit.convert.set-units-conversion-settings' | translate"
*ngIf="allowConverted && isUnitMapping" class="material-icons tb-icon-24 tb-suffix-show-always"> *ngIf="supportsUnitConversion && isUnitMapping" class="material-icons tb-suffix-show-always mr-2 !p-0">
mdi:swap-vertical-circle-outline mdi:swap-vertical-circle-outline
</tb-icon> </tb-icon>
<mat-autocomplete <mat-autocomplete
@ -50,7 +50,7 @@
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]) {
<mat-optgroup [label]="'unit.measures.' + group[0] | translate"> <mat-optgroup [label]="'unit.measures.' + group[0] | translate">
@for(unit of group[1]; track unit.abbr) { @for(unit of group[1]; track unit.abbr) {
<mat-option [value]="unit"> <mat-option [value]="unit">

View File

@ -31,7 +31,14 @@ import {
} from '@angular/core'; } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormControl, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { ControlValueAccessor, FormBuilder, FormControl, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Observable, of, shareReplay } from 'rxjs'; import { Observable, of, shareReplay } from 'rxjs';
import { AllMeasures, TbUnit, UnitInfo, UnitsType, UnitSystem } from '@shared/models/unit.models'; import {
AllMeasures,
getSourceTbUnitSymbol,
TbUnit,
UnitInfo,
UnitsType,
UnitSystem
} from '@shared/models/unit.models';
import { map, mergeMap } from 'rxjs/operators'; import { map, mergeMap } from 'rxjs/operators';
import { UnitService } from '@core/services/unit.service'; import { UnitService } from '@core/services/unit.service';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
@ -74,9 +81,9 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit, OnChang
unitSystem: UnitSystem; unitSystem: UnitSystem;
@Input({transform: booleanAttribute}) @Input({transform: booleanAttribute})
allowConverted = false; supportsUnitConversion = false;
filteredUnits: Observable<Array<[AllMeasures, Array<UnitInfo>]>>; filteredUnits$: Observable<Array<[AllMeasures, Array<UnitInfo>]>>;
searchText = ''; searchText = '';
@ -100,11 +107,10 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit, OnChang
ngOnInit() { ngOnInit() {
this.unitsFormControl = this.fb.control<TbUnit | UnitInfo>('', this.required ? [Validators.required] : []); this.unitsFormControl = this.fb.control<TbUnit | UnitInfo>('', this.required ? [Validators.required] : []);
this.filteredUnits = this.unitsFormControl.valueChanges this.filteredUnits$ = this.unitsFormControl.valueChanges.pipe(
.pipe(
map(value => { map(value => {
this.updateView(value); this.updateModel(value);
return this.getUnitSymbol(value); return getSourceTbUnitSymbol(value);
}), }),
mergeMap(symbol => this.fetchUnits(symbol)) mergeMap(symbol => this.fetchUnits(symbol))
); );
@ -144,7 +150,7 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit, OnChang
displayUnitFn(unit?: TbUnit | UnitInfo): string | undefined { displayUnitFn(unit?: TbUnit | UnitInfo): string | undefined {
if (unit) { if (unit) {
return this.getUnitSymbol(unit); return getSourceTbUnitSymbol(unit);
} }
return undefined; return undefined;
} }
@ -168,7 +174,7 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit, OnChang
clear($event: Event) { clear($event: Event) {
$event.stopPropagation(); $event.stopPropagation();
this.unitsFormControl.patchValue(null, {emitEvent: true}); this.unitsFormControl.patchValue(null, {emitEvent: true});
if (!this.allowConverted) { if (!this.supportsUnitConversion) {
setTimeout(() => { setTimeout(() => {
this.unitInput.nativeElement.blur(); this.unitInput.nativeElement.blur();
this.unitInput.nativeElement.focus(); this.unitInput.nativeElement.focus();
@ -177,40 +183,38 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit, OnChang
} }
openConvertSettingsPopup($event: Event) { openConvertSettingsPopup($event: Event) {
if (!this.allowConverted) { if (!this.supportsUnitConversion) {
return; return;
} }
if ($event) {
$event.stopPropagation(); $event.stopPropagation();
}
this.unitInput.nativeElement.blur(); 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);
} else { } else {
const convertUnitSettingsPanelPopover = this.popoverService.displayPopover({ const popover = this.popoverService.displayPopover({
trigger, trigger,
renderer: this.renderer, renderer: this.renderer,
componentType: ConvertUnitSettingsPanelComponent, componentType: ConvertUnitSettingsPanelComponent,
hostView: this.viewContainerRef, hostView: this.viewContainerRef,
preferredPlacement: ['left', 'bottom', 'top'], preferredPlacement: ['left', 'bottom', 'top'],
context: { context: {
unit: this.getTbUnit(this.unitsFormControl.value), unit: this.extractTbUnit(this.unitsFormControl.value),
required: this.required, required: this.required,
disabled: this.disabled, disabled: this.disabled,
}, },
isModal: true isModal: true
}); });
convertUnitSettingsPanelPopover.tbComponentRef.instance.unitSettingsApplied.subscribe((unitSetting) => { popover.tbComponentRef.instance.unitSettingsApplied.subscribe((unitSetting) => {
convertUnitSettingsPanelPopover.hide(); popover.hide();
this.unitsFormControl.patchValue(unitSetting, {emitEvent: false}); this.unitsFormControl.patchValue(unitSetting, {emitEvent: false});
this.updateView(unitSetting); this.updateModel(unitSetting);
}); });
} }
} }
private updateView(value: UnitInfo | TbUnit ) { private updateModel(value: UnitInfo | TbUnit ) {
const res = this.getTbUnit(value); const res = this.extractTbUnit(value);
if (this.modelValue !== res) { if (this.modelValue !== res) {
this.modelValue = res; this.modelValue = res;
this.isUnitMapping = (res !== null && isObject(res)); this.isUnitMapping = (res !== null && isObject(res));
@ -220,12 +224,12 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit, OnChang
private fetchUnits(searchText?: string): Observable<Array<[AllMeasures, Array<UnitInfo>]>> { private fetchUnits(searchText?: string): Observable<Array<[AllMeasures, Array<UnitInfo>]>> {
this.searchText = searchText; this.searchText = searchText;
return this.unitsConstant().pipe( return this.getGroupedUnits().pipe(
map(unit => this.searchUnit(unit, searchText)) map(unit => this.searchUnit(unit, searchText))
); );
} }
private unitsConstant(): Observable<Array<[AllMeasures, Array<UnitInfo>]>> { private getGroupedUnits(): Observable<Array<[AllMeasures, Array<UnitInfo>]>> {
if (this.fetchUnits$ === null) { if (this.fetchUnits$ === null) {
this.fetchUnits$ = of(this.unitService.getUnitsGroupedByMeasure(this.measure, this.unitSystem)).pipe( this.fetchUnits$ = of(this.unitService.getUnitsGroupedByMeasure(this.measure, this.unitSystem)).pipe(
map(data => { map(data => {
@ -258,23 +262,13 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit, OnChang
return units; return units;
} }
private getUnitSymbol(value: TbUnit | UnitInfo | null): string { private extractTbUnit(value: TbUnit | UnitInfo | null): TbUnit {
if (value === null) {
return '';
}
if (typeof value === 'string') {
return value;
}
if ('abbr' in value) {
return value.abbr;
}
return value.from;
}
private getTbUnit(value: TbUnit | UnitInfo | null): TbUnit {
if (value === null) { if (value === null) {
return null; return null;
} }
if (value === undefined) {
return undefined;
}
if (typeof value === 'string') { if (typeof value === 'string') {
return value; return value;
} }

View File

@ -650,3 +650,16 @@ export function getUnitConverter(translate: TranslateService): Converter {
const unitCache = buildUnitCache(allMeasures, translate); const unitCache = buildUnitCache(allMeasures, translate);
return new Converter(allMeasures, unitCache); return new Converter(allMeasures, unitCache);
} }
export const getSourceTbUnitSymbol = (value: TbUnit | UnitInfo | null): string => {
if (value === null || value === undefined) {
return '';
}
if (typeof value === 'string') {
return value;
}
if ('abbr' in value) {
return value.abbr;
}
return value.from;
}

View File

@ -862,105 +862,113 @@ export class AutoDateFormatProcessor extends DateFormatProcessor {
} }
} }
export interface ValueFormatSettingProcessor { export interface ValueFormatSettings {
dec?: number; decimals?: number;
units?: TbUnit; units?: TbUnit;
showZeroDecimals?: boolean; showZeroDecimals?: boolean;
ignoreUnitSymbol?: boolean;
} }
export abstract class ValueFormatProcessor { export abstract class ValueFormatProcessor {
static fromSettings($injector: Injector, settings: ValueFormatSettingProcessor): ValueFormatProcessor { protected isDefinedDecimals: boolean;
protected hideZeroDecimals: boolean;
protected unitSymbol: string;
static fromSettings($injector: Injector, settings: ValueFormatSettings): ValueFormatProcessor {
if (settings.units !== null && typeof settings.units === 'object') { if (settings.units !== null && typeof settings.units === 'object') {
return new ConverterValueFormatProcessor($injector, settings) return new UnitConverterValueFormatProcessor($injector, settings)
} else {
return new SimpleValueFormatProcessor($injector, settings);
} }
return new SimpleValueFormatProcessor($injector, settings);
} }
protected constructor(protected $injector: Injector, protected constructor(protected $injector: Injector,
protected settings: ValueFormatSettingProcessor) { protected settings: ValueFormatSettings) {
} }
abstract update(value: any): string; abstract format(value: any): string;
protected formatValue(value: number): string {
let formatted: number | string = value;
if (this.isDefinedDecimals) {
formatted = formatted.toFixed(this.settings.decimals);
}
if (this.hideZeroDecimals) {
formatted = Number(formatted);
}
formatted = formatted.toString();
if (this.unitSymbol) {
formatted += ` ${this.unitSymbol}`;
}
return formatted;
}
} }
export class SimpleValueFormatProcessor extends ValueFormatProcessor { export class SimpleValueFormatProcessor extends ValueFormatProcessor {
private readonly isDefinedUnit: boolean; private readonly isDefinedUnit: boolean;
private readonly isDefinedDec: boolean;
private readonly hideZeroDecimals: boolean;
constructor(protected $injector: Injector, constructor(protected $injector: Injector,
protected settings: ValueFormatSettingProcessor) { protected settings: ValueFormatSettings) {
super($injector, settings); super($injector, settings);
this.isDefinedUnit = isNotEmptyStr(settings.units); this.unitSymbol = !settings.ignoreUnitSymbol && isNotEmptyStr(settings.units) ? (settings.units as string) : null;
this.isDefinedDec = isDefinedAndNotNull(settings.dec); this.isDefinedDecimals = isDefinedAndNotNull(settings.decimals);
this.hideZeroDecimals = !settings.showZeroDecimals; this.hideZeroDecimals = !settings.showZeroDecimals;
} }
update(value: any): string { format(value: any): string {
if (isDefinedAndNotNull(value) && isNumeric(value) && (this.isDefinedDec || this.isDefinedUnit || Number(value).toString() === value)) { if (isDefinedAndNotNull(value) && isNumeric(value) && (this.isDefinedDecimals || this.isDefinedUnit || Number(value).toString() === value)) {
let formatted = value; return this.formatValue(Number(value));
if (this.isDefinedDec) {
formatted = Number(formatted).toFixed(this.settings.dec);
}
if (this.hideZeroDecimals) {
formatted = Number(formatted)
}
formatted = formatted.toString();
if (this.isDefinedUnit) {
formatted += ` ${this.settings.units}`;
}
return formatted;
} }
return value ?? ''; return value ?? '';
} }
} }
export class ConverterValueFormatProcessor extends ValueFormatProcessor { export class UnitConverterValueFormatProcessor extends ValueFormatProcessor {
private readonly isDefinedDec: boolean;
private readonly hideZeroDecimals: boolean;
private readonly unitConverter: TbUnitConverter; private readonly unitConverter: TbUnitConverter;
private readonly unitAbbr: string;
constructor(protected $injector: Injector, constructor(protected $injector: Injector,
protected settings: ValueFormatSettingProcessor) { protected settings: ValueFormatSettings) {
super($injector, settings); super($injector, settings);
const unitService = this.$injector.get(UnitService); const unitService = this.$injector.get(UnitService);
const unit = settings.units as TbUnitMapping; const unit = settings.units as TbUnitMapping;
this.unitAbbr = unitService.getTargetUnitSymbol(unit); this.unitSymbol = settings.ignoreUnitSymbol ? null : unitService.getTargetUnitSymbol(unit);
try { try {
this.unitConverter = unitService.geUnitConverter(unit); this.unitConverter = unitService.geUnitConverter(unit);
} catch (e) {/**/} } catch (e) {
console.warn('Failed to create unit converter:', e);
}
this.isDefinedDec = isDefinedAndNotNull(settings.dec); this.isDefinedDecimals = isDefinedAndNotNull(settings.decimals);
this.hideZeroDecimals = !settings.showZeroDecimals; this.hideZeroDecimals = !settings.showZeroDecimals;
} }
update(value: any): string { format(value: any): string {
if (isDefinedAndNotNull(value) && isNumeric(value)) { if (isDefinedAndNotNull(value) && isNumeric(value)) {
let formatted: number | string = Number(value); let formatted = Number(value);
if (this.unitConverter) { if (this.unitConverter) {
formatted = this.unitConverter(value); formatted = this.unitConverter(value);
} }
if (this.isDefinedDec) { return this.formatValue(formatted);
formatted = Number(formatted).toFixed(this.settings.dec);
}
if (this.hideZeroDecimals) {
formatted = Number(formatted)
}
formatted = formatted.toString();
if (this.unitAbbr) {
formatted += ` ${this.unitAbbr}`;
}
return formatted;
} }
return value ?? ''; return value ?? '';
} }
} }
export const createValueFormatterFromSettings = (ctx: WidgetContext): ValueFormatProcessor => {
let decimals = ctx.decimals;
let units = ctx.units;
const dataKey = getDataKey(ctx.datasources);
if (isDefinedAndNotNull(dataKey?.decimals)) {
decimals = dataKey.decimals;
}
if (dataKey?.units) {
units = dataKey.units;
}
return ValueFormatProcessor.fromSettings(ctx.$injector, {units: units, decimals: decimals});
}
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) {