UI: Refactoring ValueFormatProcessor and fixed unit input component
This commit is contained in:
parent
76237337b2
commit
9662b263e4
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user