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);
}
getTargetUnitSymbol(unit: TbUnitMapping): string {
getTargetUnitSymbol(unit: TbUnitMapping | string): string {
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;

View File

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

View File

@ -22,11 +22,11 @@
[class.!pointer-events-none]="disabled"
(focusin)="onFocus()"
[matAutocomplete]="unitsAutocomplete"
[matAutocompleteDisabled]="allowConverted">
[matAutocompleteDisabled]="supportsUnitConversion">
<button *ngIf="unitsFormControl.value && !disabled && unitsFormControl.valid"
type="button"
class="tb-icon-24 mr-2"
[class.mr-2]="!allowConverted || !isUnitMapping"
[class.mr-2]="!supportsUnitConversion || !isUnitMapping"
matSuffix mat-icon-button aria-label="Clear"
(click)="clear($event)">
<mat-icon class="material-icons">close</mat-icon>
@ -42,7 +42,7 @@
[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">
*ngIf="supportsUnitConversion && isUnitMapping" class="material-icons tb-suffix-show-always mr-2 !p-0">
mdi:swap-vertical-circle-outline
</tb-icon>
<mat-autocomplete
@ -50,7 +50,7 @@
class="tb-autocomplete tb-unit-input-autocomplete"
panelWidth="fit-content"
[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">
@for(unit of group[1]; track unit.abbr) {
<mat-option [value]="unit">

View File

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

View File

@ -650,3 +650,16 @@ export function getUnitConverter(translate: TranslateService): Converter {
const unitCache = buildUnitCache(allMeasures, translate);
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 {
dec?: number;
export interface ValueFormatSettings {
decimals?: number;
units?: TbUnit;
showZeroDecimals?: boolean;
ignoreUnitSymbol?: boolean;
}
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') {
return new ConverterValueFormatProcessor($injector, settings)
} else {
return new SimpleValueFormatProcessor($injector, settings);
return new UnitConverterValueFormatProcessor($injector, settings)
}
return new SimpleValueFormatProcessor($injector, settings);
}
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 {
private readonly isDefinedUnit: boolean;
private readonly isDefinedDec: boolean;
private readonly hideZeroDecimals: boolean;
constructor(protected $injector: Injector,
protected settings: ValueFormatSettingProcessor) {
protected settings: ValueFormatSettings) {
super($injector, settings);
this.isDefinedUnit = isNotEmptyStr(settings.units);
this.isDefinedDec = isDefinedAndNotNull(settings.dec);
this.unitSymbol = !settings.ignoreUnitSymbol && isNotEmptyStr(settings.units) ? (settings.units as string) : null;
this.isDefinedDecimals = isDefinedAndNotNull(settings.decimals);
this.hideZeroDecimals = !settings.showZeroDecimals;
}
update(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.hideZeroDecimals) {
formatted = Number(formatted)
}
formatted = formatted.toString();
if (this.isDefinedUnit) {
formatted += ` ${this.settings.units}`;
}
return formatted;
format(value: any): string {
if (isDefinedAndNotNull(value) && isNumeric(value) && (this.isDefinedDecimals || this.isDefinedUnit || Number(value).toString() === value)) {
return this.formatValue(Number(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 unitAbbr: string;
constructor(protected $injector: Injector,
protected settings: ValueFormatSettingProcessor) {
protected settings: ValueFormatSettings) {
super($injector, settings);
const unitService = this.$injector.get(UnitService);
const unit = settings.units as TbUnitMapping;
this.unitAbbr = unitService.getTargetUnitSymbol(unit);
this.unitSymbol = settings.ignoreUnitSymbol ? null : unitService.getTargetUnitSymbol(unit);
try {
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;
}
update(value: any): string {
format(value: any): string {
if (isDefinedAndNotNull(value) && isNumeric(value)) {
let formatted: number | string = Number(value);
let formatted = Number(value);
if (this.unitConverter) {
formatted = this.unitConverter(value);
}
if (this.isDefinedDec) {
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 this.formatValue(formatted);
}
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 intervalValue = IntervalMath.numberValue(interval);
if (intervalValue < SECOND) {