UI: Refactoring liquid level widgets after review

This commit is contained in:
Vladyslav_Prykhodko 2023-10-24 15:08:16 +03:00
parent 73566db398
commit 0bab29c8ec
13 changed files with 138 additions and 150 deletions

View File

@ -84,7 +84,7 @@
label="{{ 'widgets.liquid-level-card.shape-type' | translate }}" formControlName="selectedShape"> label="{{ 'widgets.liquid-level-card.shape-type' | translate }}" formControlName="selectedShape">
<tb-image-cards-select-option *ngFor="let shape of shapes" <tb-image-cards-select-option *ngFor="let shape of shapes"
[value]="shape" [value]="shape"
[image]="createShapeLayout(shapesImageMap.get(shape), this.levelCardLayouts.simple)"> [image]="createShape(shapesImageMap.get(shape), this.levelCardLayouts.simple)">
{{ shapesTranslationMap.get(shape) | translate }} {{ shapesTranslationMap.get(shape) | translate }}
</tb-image-cards-select-option> </tb-image-cards-select-option>
</tb-image-cards-select> </tb-image-cards-select>
@ -94,8 +94,7 @@
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)" <tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)"
[required]="isRequired('shapeAttributeName')" [required]="isRequired('shapeAttributeName')"
[errorText]="'widgets.liquid-level-card.attribute-name-required' | translate" [errorText]="'widgets.liquid-level-card.attribute-name-required' | translate"
style="flex: 1; width: auto;" style="flex: 1"
asBoxInput colorClearButton class="flex"
formControlName="shapeAttributeName"> formControlName="shapeAttributeName">
</tb-string-autocomplete> </tb-string-autocomplete>
</div> </div>
@ -118,7 +117,7 @@
<tb-image-cards-select-option <tb-image-cards-select-option
*ngFor="let layout of [levelCardLayouts.simple, levelCardLayouts.percentage, levelCardLayouts.absolute]" *ngFor="let layout of [levelCardLayouts.simple, levelCardLayouts.percentage, levelCardLayouts.absolute]"
[value]="layout" [value]="layout"
[image]="createShapeLayout(shapesImageMap.get(levelCardWidgetConfigForm.get('selectedShape').value), layout)"> [image]="createShape(shapesImageMap.get(levelCardWidgetConfigForm.get('selectedShape').value), layout)">
{{ levelCardLayoutTranslationMap.get(layout) | translate }} {{ levelCardLayoutTranslationMap.get(layout) | translate }}
</tb-image-cards-select-option> </tb-image-cards-select-option>
</tb-image-cards-select> </tb-image-cards-select>
@ -144,7 +143,7 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<tb-unit-input [fxShow]="levelCardWidgetConfigForm.get('widgetUnitsSource')?.value !== levelOptions.attribute" <tb-unit-input [fxShow]="levelCardWidgetConfigForm.get('widgetUnitsSource')?.value !== levelOptions.attribute"
asBoxInput colorClearButton class="flex" class="flex"
[tagFilter]="unitsType.capacity" [tagFilter]="unitsType.capacity"
[required]="isRequired('units')" [required]="isRequired('units')"
formControlName="units"> formControlName="units">
@ -153,8 +152,7 @@
[fetchOptionsFn]="fetchOptions.bind(this)" [fetchOptionsFn]="fetchOptions.bind(this)"
[required]="isRequired('widgetUnitsAttributeName')" [required]="isRequired('widgetUnitsAttributeName')"
[errorText]="'widgets.liquid-level-card.attribute-name-required' | translate" [errorText]="'widgets.liquid-level-card.attribute-name-required' | translate"
style="flex: 1; width: auto;" style="flex: 1"
asBoxInput colorClearButton class="flex"
formControlName="widgetUnitsAttributeName"> formControlName="widgetUnitsAttributeName">
</tb-string-autocomplete> </tb-string-autocomplete>
</div> </div>
@ -182,17 +180,14 @@
warning warning
</mat-icon> </mat-icon>
</mat-form-field> </mat-form-field>
<tb-string-autocomplete style="max-width: 25%" <tb-string-autocomplete [fxShow]="levelCardWidgetConfigForm.get('volumeSource')?.value === levelOptions.attribute"
[fxShow]="levelCardWidgetConfigForm.get('volumeSource')?.value === levelOptions.attribute"
[fetchOptionsFn]="fetchOptions.bind(this)" [fetchOptionsFn]="fetchOptions.bind(this)"
[errorText]="'widgets.liquid-level-card.attribute-name-required' | translate" [errorText]="'widgets.liquid-level-card.attribute-name-required' | translate"
[required]="isRequired('volumeAttributeName')" [required]="isRequired('volumeAttributeName')"
style="flex: 1; width: auto;" style="flex: 1"
asBoxInput colorClearButton class="flex"
formControlName="volumeAttributeName"> formControlName="volumeAttributeName">
</tb-string-autocomplete> </tb-string-autocomplete>
<tb-unit-input asBoxInput colorClearButton <tb-unit-input [tagFilter]="unitsType.capacity"
[tagFilter]="unitsType.capacity"
[required]="isRequired('volumeUnits')" [required]="isRequired('volumeUnits')"
style="max-width: 25%" class="flex" style="max-width: 25%" class="flex"
formControlName="volumeUnits"> formControlName="volumeUnits">

View File

@ -43,23 +43,22 @@ import {
} from '@shared/models/widget-settings.models'; } from '@shared/models/widget-settings.models';
import { import {
CapacityUnits, CapacityUnits,
createAbsoluteLayout, createShapeLayout,
createPercentLayout,
levelCardDefaultSettings, levelCardDefaultSettings,
LevelCardLayout, LevelCardLayout,
levelCardLayoutTranslations, levelCardLayoutTranslations,
LevelCardWidgetSettings, LevelCardWidgetSettings,
LevelSelectOptions, LevelSelectOptions,
loadSvgShapesMapping,
optionsFilter, optionsFilter,
Shapes, Shapes,
shapesTranslations, shapesTranslations
svgMapping
} from '@home/components/widget/lib/indicator/liquid-level-widget.models'; } from '@home/components/widget/lib/indicator/liquid-level-widget.models';
import { UnitsType } from '@shared/models/unit.models'; import { UnitsType } from '@shared/models/unit.models';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { ImageCardsSelectComponent } from '@home/components/widget/lib/settings/common/image-cards-select.component'; import { ImageCardsSelectComponent } from '@home/components/widget/lib/settings/common/image-cards-select.component';
import { map, publishReplay, refCount, tap } from 'rxjs/operators'; import { map, share, tap } from 'rxjs/operators';
import { forkJoin, Observable, of } from 'rxjs'; import { Observable, of, ReplaySubject } from 'rxjs';
import { ResourcesService } from '@core/services/resources.service'; import { ResourcesService } from '@core/services/resources.service';
import { UnitInputComponent } from '@shared/components/unit-input.component'; import { UnitInputComponent } from '@shared/components/unit-input.component';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
@ -242,7 +241,7 @@ export class LiquidLevelCardBasicConfigComponent extends BasicWidgetConfigCompon
actions: [configData.config.actions || {}, []] actions: [configData.config.actions || {}, []]
}); });
this.levelCardWidgetConfigForm.get('selectedShape').valueChanges.subscribe((shape) => { this.levelCardWidgetConfigForm.get('selectedShape').valueChanges.subscribe(() => {
this.cd.detectChanges(); this.cd.detectChanges();
this.layoutsImageCardsSelect?.imageCardsSelectOptions.notifyOnChanges(); this.layoutsImageCardsSelect?.imageCardsSelectOptions.notifyOnChanges();
}); });
@ -562,21 +561,8 @@ export class LiquidLevelCardBasicConfigComponent extends BasicWidgetConfigCompon
} }
private createSvgShapesMapping(): void { private createSvgShapesMapping(): void {
const obsArray: Array<Observable<{svg: string; shape: Shapes}>> = []; loadSvgShapesMapping(this.resourcesService).subscribe(shapeMap => {
for (const shape of this.shapes) { this.shapesImageMap = shapeMap;
const svgUrl = svgMapping.get(shape).svg;
const obs = this.resourcesService.loadJsonResource<string>(svgUrl).pipe(
map((svg) => ({svg, shape}))
);
obsArray.push(obs);
}
forkJoin(obsArray).subscribe((svgData) => {
for (const data of svgData) {
this.shapesImageMap.set(data.shape, data.svg);
}
this.cd.detectChanges(); this.cd.detectChanges();
this.layoutsImageCardsSelect?.imageCardsSelectOptions.notifyOnChanges(); this.layoutsImageCardsSelect?.imageCardsSelectOptions.notifyOnChanges();
@ -584,25 +570,8 @@ export class LiquidLevelCardBasicConfigComponent extends BasicWidgetConfigCompon
}); });
} }
public createShapeLayout(svg: string, layout: LevelCardLayout): SafeUrl { createShape(svg: string, layout: LevelCardLayout): SafeUrl {
if (svg && layout) { return createShapeLayout(svg, layout, this.sanitizer);
const parser = new DOMParser();
const svgImage = parser.parseFromString(svg, 'image/svg+xml');
if (layout === this.levelCardLayouts.simple) {
svgImage.querySelector('.container-overlay').remove();
} else if (layout === this.levelCardLayouts.percentage) {
svgImage.querySelector('.absolute-overlay').remove();
svgImage.querySelector('.percentage-value-container').innerHTML = createPercentLayout();
} else {
svgImage.querySelector('.absolute-value-container').innerHTML = createAbsoluteLayout();
svgImage.querySelector('.percentage-overlay').remove();
}
const encodedSvg = encodeURIComponent(svgImage.documentElement.outerHTML);
return this.sanitizer.bypassSecurityTrustResourceUrl(`data:image/svg+xml,${encodedSvg}`);
}
} }
public isRequired(formControlName: string): boolean { public isRequired(formControlName: string): boolean {
@ -649,8 +618,12 @@ export class LiquidLevelCardBasicConfigComponent extends BasicWidgetConfigCompon
fetchObservable = of([]); fetchObservable = of([]);
} }
return fetchObservable.pipe( return fetchObservable.pipe(
publishReplay(1), share({
refCount() connector: () => new ReplaySubject(1),
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: false
})
); );
} }
} }

View File

@ -25,6 +25,7 @@ import { AppState } from '@core/core.state';
import { AbstractControl, UntypedFormGroup } from '@angular/forms'; import { AbstractControl, UntypedFormGroup } from '@angular/forms';
import { DataKey, DatasourceType, KeyInfo, WidgetConfigMode } from '@shared/models/widget.models'; import { DataKey, DatasourceType, KeyInfo, WidgetConfigMode } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { isDefinedAndNotNull } from '@core/utils'; import { isDefinedAndNotNull } from '@core/utils';
export type WidgetConfigCallbacks = DatasourceCallbacks & WidgetActionCallbacks; export type WidgetConfigCallbacks = DatasourceCallbacks & WidgetActionCallbacks;

View File

@ -20,7 +20,9 @@ import { isDefined, isDefinedAndNotNull, isNumber, isString } from '@core/utils'
import { import {
CapacityUnits, CapacityUnits,
ConversionType, ConversionType,
convertLiters, createAbsoluteLayout, createPercentLayout, convertLiters,
createAbsoluteLayout,
createPercentLayout,
extractValue, extractValue,
levelCardDefaultSettings, levelCardDefaultSettings,
LevelCardLayout, LevelCardLayout,
@ -40,11 +42,12 @@ import {
DateFormatProcessor, DateFormatProcessor,
inlineTextStyle inlineTextStyle
} from '@shared/models/widget-settings.models'; } from '@shared/models/widget-settings.models';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import { ResourcesService } from '@core/services/resources.service'; import { ResourcesService } from '@core/services/resources.service';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
@Component({ @Component({
selector: 'tb-liquid-level-widget', selector: 'tb-liquid-level-widget',
template: '' template: ''
@ -128,7 +131,7 @@ export class LiquidLevelWidgetComponent implements OnInit {
}); });
} }
private declareStyles():void { private declareStyles(): void {
this.tankColor = ColorProcessor.fromSettings(this.settings.tankColor); this.tankColor = ColorProcessor.fromSettings(this.settings.tankColor);
this.volumeColor = ColorProcessor.fromSettings(this.settings.volumeColor); this.volumeColor = ColorProcessor.fromSettings(this.settings.volumeColor);
this.valueColor = ColorProcessor.fromSettings(this.settings.valueColor); this.valueColor = ColorProcessor.fromSettings(this.settings.valueColor);
@ -328,7 +331,12 @@ export class LiquidLevelWidgetComponent implements OnInit {
this.updateLevel(newYPos, percentage); this.updateLevel(newYPos, percentage);
} }
private calculatePosition(percentage, limits): number { private calculatePosition(percentage: number, limits: SvgLimits): number {
if (percentage > 100) {
return limits.max;
} if (percentage <= 0) {
return limits.min;
}
return limits.min + (percentage / 100) * (limits.max - limits.min); return limits.min + (percentage / 100) * (limits.max - limits.min);
} }
@ -368,7 +376,7 @@ export class LiquidLevelWidgetComponent implements OnInit {
} }
} }
private updateShapeColor(value): void { private updateShapeColor(value: number): void {
const shapeStrokes = this.ctx.$container.find('.tb-shape-stroke'); const shapeStrokes = this.ctx.$container.find('.tb-shape-stroke');
const shapeFill = this.ctx.$container.find('.tb-shape-fill'); const shapeFill = this.ctx.$container.find('.tb-shape-fill');
this.tankColor.update(value); this.tankColor.update(value);
@ -504,7 +512,7 @@ export class LiquidLevelWidgetComponent implements OnInit {
} }
} }
public cardClick($event) { public cardClick($event: Event) {
this.ctx.actionsApi.cardClick($event); this.ctx.actionsApi.cardClick($event);
} }
} }

View File

@ -24,12 +24,14 @@ import {
} from '@shared/models/widget-settings.models'; } from '@shared/models/widget-settings.models';
import { DataKey, WidgetConfig } from '@shared/models/widget.models'; import { DataKey, WidgetConfig } from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { Observable, of } from 'rxjs'; import { forkJoin, Observable, of } from 'rxjs';
import { singleEntityFilterFromDeviceId } from '@shared/models/query/query.models'; import { singleEntityFilterFromDeviceId } from '@shared/models/query/query.models';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { catchError, mergeMap } from 'rxjs/operators'; import { catchError, map, mergeMap } from 'rxjs/operators';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { ResourcesService } from '@core/services/resources.service';
export interface SvgInfo { export interface SvgInfo {
svg: string; svg: string;
@ -123,7 +125,7 @@ export enum ConversionType {
from = 'from', from = 'from',
} }
export const svgMapping = new Map<string, SvgInfo>( export const svgMapping = new Map<Shapes, SvgInfo>(
[ [
[ [
Shapes.vOval, Shapes.vOval,
@ -421,8 +423,49 @@ export const fetchEntityKeys = (entityAliasId: string, dataKeyTypes: Array<DataK
aliasInfo.entityFilter, aliasInfo.entityFilter,
dataKeyTypes, [], dataKeyTypes, [],
{ignoreLoading: true, ignoreErrors: true} {ignoreLoading: true, ignoreErrors: true}
).pipe(
catchError(() => of([]))
)), )),
catchError(() => of([] as Array<DataKey>)) catchError(() => of([] as Array<DataKey>))
); );
export const createShapeLayout = (svg: string, layout: LevelCardLayout, sanitizer: DomSanitizer): SafeUrl => {
if (svg && layout) {
const parser = new DOMParser();
const svgImage = parser.parseFromString(svg, 'image/svg+xml');
if (layout === LevelCardLayout.simple) {
svgImage.querySelector('.container-overlay').remove();
} else if (layout === LevelCardLayout.percentage) {
svgImage.querySelector('.absolute-overlay').remove();
svgImage.querySelector('.percentage-value-container').innerHTML = createPercentLayout();
} else {
svgImage.querySelector('.absolute-value-container').innerHTML = createAbsoluteLayout();
svgImage.querySelector('.percentage-overlay').remove();
}
const encodedSvg = encodeURIComponent(svgImage.documentElement.outerHTML);
return sanitizer.bypassSecurityTrustResourceUrl(`data:image/svg+xml,${encodedSvg}`);
}
};
export const loadSvgShapesMapping = (resourcesService: ResourcesService): Observable<Map<Shapes, string>> => {
const obsArray: Array<Observable<{svg: string; shape: Shapes}>> = [];
const shapesImageMap: Map<Shapes, string> = new Map();
svgMapping.forEach((value, shape) => {
const obs = resourcesService.loadJsonResource<string>(value.svg).pipe(
map((svg) => ({svg, shape}))
);
obsArray.push(obs);
});
return forkJoin(obsArray).pipe(
map(svgData => {
for (const data of svgData) {
shapesImageMap.set(data.shape, data.svg);
}
return shapesImageMap;
})
);
};

View File

@ -16,7 +16,7 @@
--> -->
<ng-container *ngIf="levelCardWidgetSettingsForm" [formGroup]="levelCardWidgetSettingsForm"> <ng-container *ngIf="levelCardWidgetSettingsForm" [formGroup]="levelCardWidgetSettingsForm">
<div style="display: flex; flex-direction: column; gap: 16px;"> <div class="tb-form-panel no-padding no-border">
<div class="tb-form-panel"> <div class="tb-form-panel">
<div fxFlex fxLayout="column" class="tb-form-row space-between"> <div fxFlex fxLayout="column" class="tb-form-row space-between">
<div fxFlex fxLayout="row" style="width: 100%;" fxLayoutAlign="space-between center"> <div fxFlex fxLayout="row" style="width: 100%;" fxLayoutAlign="space-between center">
@ -39,7 +39,7 @@
label="{{ 'widgets.liquid-level-card.shape-type' | translate }}" formControlName="selectedShape"> label="{{ 'widgets.liquid-level-card.shape-type' | translate }}" formControlName="selectedShape">
<tb-image-cards-select-option *ngFor="let shape of shapes" <tb-image-cards-select-option *ngFor="let shape of shapes"
[value]="shape" [value]="shape"
[image]="createShapeLayout(shapesImageMap.get(shape), this.levelCardLayouts.simple)"> [image]="createShape(shapesImageMap.get(shape), this.levelCardLayouts.simple)">
{{ shapesTranslationMap.get(shape) | translate }} {{ shapesTranslationMap.get(shape) | translate }}
</tb-image-cards-select-option> </tb-image-cards-select-option>
</tb-image-cards-select> </tb-image-cards-select>
@ -49,8 +49,7 @@
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)" <tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)"
[required]="isRequired('shapeAttributeName')" [required]="isRequired('shapeAttributeName')"
[errorText]="'widgets.liquid-level-card.attribute-name-required' | translate" [errorText]="'widgets.liquid-level-card.attribute-name-required' | translate"
style="flex: 1; width: auto;" style="flex: 1"
asBoxInput colorClearButton class="flex"
formControlName="shapeAttributeName"> formControlName="shapeAttributeName">
</tb-string-autocomplete> </tb-string-autocomplete>
</div> </div>
@ -77,7 +76,7 @@
<tb-image-cards-select-option <tb-image-cards-select-option
*ngFor="let layout of [levelCardLayouts.simple, levelCardLayouts.percentage, levelCardLayouts.absolute]" *ngFor="let layout of [levelCardLayouts.simple, levelCardLayouts.percentage, levelCardLayouts.absolute]"
[value]="layout" [value]="layout"
[image]="createShapeLayout(shapesImageMap.get(levelCardWidgetSettingsForm.get('selectedShape').value), layout)"> [image]="createShape(shapesImageMap.get(levelCardWidgetSettingsForm.get('selectedShape').value), layout)">
{{ levelCardLayoutTranslationMap.get(layout) | translate }} {{ levelCardLayoutTranslationMap.get(layout) | translate }}
</tb-image-cards-select-option> </tb-image-cards-select-option>
</tb-image-cards-select> </tb-image-cards-select>
@ -103,7 +102,7 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<tb-unit-input [fxShow]="levelCardWidgetSettingsForm.get('widgetUnitsSource')?.value !== volumeOptions.attribute" <tb-unit-input [fxShow]="levelCardWidgetSettingsForm.get('widgetUnitsSource')?.value !== volumeOptions.attribute"
asBoxInput colorClearButton class="flex" class="flex"
[tagFilter]="unitsType.capacity" [tagFilter]="unitsType.capacity"
[required]="isRequired('units')" [required]="isRequired('units')"
formControlName="units"> formControlName="units">
@ -112,8 +111,7 @@
[fetchOptionsFn]="fetchOptions.bind(this)" [fetchOptionsFn]="fetchOptions.bind(this)"
[required]="isRequired('widgetUnitsAttributeName')" [required]="isRequired('widgetUnitsAttributeName')"
[errorText]="'widgets.liquid-level-card.attribute-name-required' | translate" [errorText]="'widgets.liquid-level-card.attribute-name-required' | translate"
style="flex: 1; width: auto;" style="flex: 1"
asBoxInput colorClearButton class="flex"
formControlName="widgetUnitsAttributeName"> formControlName="widgetUnitsAttributeName">
</tb-string-autocomplete> </tb-string-autocomplete>
</div> </div>
@ -141,17 +139,14 @@
warning warning
</mat-icon> </mat-icon>
</mat-form-field> </mat-form-field>
<tb-string-autocomplete style="max-width: 25%" <tb-string-autocomplete [fxShow]="levelCardWidgetSettingsForm.get('volumeSource')?.value === volumeOptions.attribute"
[fxShow]="levelCardWidgetSettingsForm.get('volumeSource')?.value === volumeOptions.attribute"
[fetchOptionsFn]="fetchOptions.bind(this)" [fetchOptionsFn]="fetchOptions.bind(this)"
[required]="isRequired('volumeAttributeName')" [required]="isRequired('volumeAttributeName')"
[errorText]="'widgets.liquid-level-card.attribute-name-required' | translate" [errorText]="'widgets.liquid-level-card.attribute-name-required' | translate"
style="flex: 1; width: auto;" style="flex: 1"
asBoxInput colorClearButton class="flex"
formControlName="volumeAttributeName"> formControlName="volumeAttributeName">
</tb-string-autocomplete> </tb-string-autocomplete>
<tb-unit-input asBoxInput colorClearButton <tb-unit-input [tagFilter]="unitsType.capacity"
[tagFilter]="unitsType.capacity"
[required]="isRequired('volumeUnits')" [required]="isRequired('volumeUnits')"
style="max-width: 25%" class="flex" style="max-width: 25%" class="flex"
formControlName="volumeUnits"> formControlName="volumeUnits">

View File

@ -27,30 +27,26 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { formatValue, isDefined } from '@core/utils'; import { formatValue, isDefined } from '@core/utils';
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { DateFormatProcessor, DateFormatSettings } from '@shared/models/widget-settings.models';
import { import {
DateFormatProcessor, CapacityUnits,
DateFormatSettings createShapeLayout,
} from '@shared/models/widget-settings.models'; fetchEntityKeys,
import { fetchEntityKeysForDevice,
levelCardDefaultSettings, levelCardDefaultSettings,
LevelCardLayout, LevelCardLayout,
levelCardLayoutTranslations, levelCardLayoutTranslations,
Shapes,
shapesTranslations,
svgMapping,
CapacityUnits,
LevelSelectOptions, LevelSelectOptions,
createPercentLayout, loadSvgShapesMapping,
createAbsoluteLayout,
optionsFilter, optionsFilter,
fetchEntityKeysForDevice, Shapes,
fetchEntityKeys shapesTranslations
} from '@home/components/widget/lib/indicator/liquid-level-widget.models'; } from '@home/components/widget/lib/indicator/liquid-level-widget.models';
import { UnitsType } from '@shared/models/unit.models'; import { UnitsType } from '@shared/models/unit.models';
import { ImageCardsSelectComponent } from '@home/components/widget/lib/settings/common/image-cards-select.component'; import { ImageCardsSelectComponent } from '@home/components/widget/lib/settings/common/image-cards-select.component';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { forkJoin, Observable, of } from 'rxjs'; import { Observable, of, ReplaySubject } from 'rxjs';
import { map, publishReplay, refCount, tap } from 'rxjs/operators'; import { map, share, tap } from 'rxjs/operators';
import { ResourcesService } from '@core/services/resources.service'; import { ResourcesService } from '@core/services/resources.service';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
@ -201,7 +197,7 @@ export class LiquidLevelCardWidgetSettingsComponent extends WidgetSettingsCompon
tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []], tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []],
}); });
this.levelCardWidgetSettingsForm.get('selectedShape').valueChanges.subscribe((shape) => { this.levelCardWidgetSettingsForm.get('selectedShape').valueChanges.subscribe(() => {
this.cd.detectChanges(); this.cd.detectChanges();
this.layoutsImageCardsSelect?.imageCardsSelectOptions.notifyOnChanges(); this.layoutsImageCardsSelect?.imageCardsSelectOptions.notifyOnChanges();
}); });
@ -390,21 +386,8 @@ export class LiquidLevelCardWidgetSettingsComponent extends WidgetSettingsCompon
} }
private createSvgShapesMapping(): void { private createSvgShapesMapping(): void {
const obsArray: Array<Observable<{svg: string; shape: Shapes}>> = []; loadSvgShapesMapping(this.resourcesService).subscribe(shapeMap => {
for (const shape of this.shapes) { this.shapesImageMap = shapeMap;
const svgUrl = svgMapping.get(shape).svg;
const obs = this.resourcesService.loadJsonResource<string>(svgUrl).pipe(
map((svg) => ({svg, shape}))
);
obsArray.push(obs);
}
forkJoin(obsArray).subscribe((svgData) => {
for (const data of svgData) {
this.shapesImageMap.set(data.shape, data.svg);
}
this.cd.detectChanges(); this.cd.detectChanges();
this.layoutsImageCardsSelect?.imageCardsSelectOptions.notifyOnChanges(); this.layoutsImageCardsSelect?.imageCardsSelectOptions.notifyOnChanges();
@ -412,25 +395,8 @@ export class LiquidLevelCardWidgetSettingsComponent extends WidgetSettingsCompon
}); });
} }
public createShapeLayout(svg: string, layout: LevelCardLayout): SafeUrl { createShape(svg: string, layout: LevelCardLayout): SafeUrl {
if (svg && layout) { return createShapeLayout(svg, layout, this.sanitizer);
const parser = new DOMParser();
const svgImage = parser.parseFromString(svg, 'image/svg+xml');
if (layout === this.levelCardLayouts.simple) {
svgImage.querySelector('.container-overlay').remove();
} else if (layout === this.levelCardLayouts.percentage) {
svgImage.querySelector('.absolute-overlay').remove();
svgImage.querySelector('.percentage-value-container').innerHTML = createPercentLayout();
} else {
svgImage.querySelector('.absolute-value-container').innerHTML = createAbsoluteLayout();
svgImage.querySelector('.percentage-overlay').remove();
}
const encodedSvg = encodeURIComponent(svgImage.documentElement.outerHTML);
return this.sanitizer.bypassSecurityTrustResourceUrl(`data:image/svg+xml,${encodedSvg}`);
}
} }
public isRequired(formControlName: string): boolean { public isRequired(formControlName: string): boolean {
@ -509,8 +475,12 @@ export class LiquidLevelCardWidgetSettingsComponent extends WidgetSettingsCompon
fetchObservable = of([]); fetchObservable = of([]);
} }
return fetchObservable.pipe( return fetchObservable.pipe(
publishReplay(1), share({
refCount() connector: () => new ReplaySubject(1),
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: false
})
); );
} }
} }

View File

@ -32,11 +32,12 @@ import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs';
import { map, share, startWith, takeUntil } from 'rxjs/operators'; import { map, share, startWith, takeUntil } from 'rxjs/operators';
import { BreakpointObserver } from '@angular/cdk/layout'; import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants'; import { MediaBreakpoints } from '@shared/models/constants';
import { SafeUrl } from '@angular/platform-browser';
export interface ImageCardsSelectOption { export interface ImageCardsSelectOption {
name: string; name: string;
value: any; value: any;
image: string; image: string | SafeUrl;
} }
@Directive( @Directive(
@ -49,7 +50,7 @@ export class ImageCardsSelectOptionDirective {
@Input() value: any; @Input() value: any;
@Input() image: string; @Input() image: string | SafeUrl;
get viewValue(): string { get viewValue(): string {
return (this._element?.nativeElement.textContent || '').trim(); return (this._element?.nativeElement.textContent || '').trim();

View File

@ -64,7 +64,7 @@ import {
import { SignalStrengthWidgetComponent } from '@home/components/widget/lib/indicator/signal-strength-widget.component'; import { SignalStrengthWidgetComponent } from '@home/components/widget/lib/indicator/signal-strength-widget.component';
import { ValueChartCardWidgetComponent } from '@home/components/widget/lib/cards/value-chart-card-widget.component'; import { ValueChartCardWidgetComponent } from '@home/components/widget/lib/cards/value-chart-card-widget.component';
import { ProgressBarWidgetComponent } from '@home/components/widget/lib/cards/progress-bar-widget.component'; import { ProgressBarWidgetComponent } from '@home/components/widget/lib/cards/progress-bar-widget.component';
import { LiquidLevelWidgetComponent } from '@home/components/widget/lib/indicator/liquid-level-widget'; import { LiquidLevelWidgetComponent } from '@home/components/widget/lib/indicator/liquid-level-widget.component';
@NgModule({ @NgModule({
declarations: declarations:

View File

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<mat-form-field [appearance]="appearance" [ngClass]="ngClass" <mat-form-field [appearance]="appearance" [class]="additionalClass"
[subscriptSizing]="subscriptSizing" style="width: 100%"> [subscriptSizing]="subscriptSizing" style="width: 100%">
<mat-label *ngIf="label">{{label}}</mat-label> <mat-label *ngIf="label">{{label}}</mat-label>
<input matInput #nameInput [formControl]="selectionFormControl" <input matInput #nameInput [formControl]="selectionFormControl"

View File

@ -16,7 +16,7 @@
:host { :host {
mat-form-field { mat-form-field {
display: flex; display: flex;
flex: 1 1 0%; flex: 1;
max-width: 100%; max-width: 100%;
box-sizing: border-box; box-sizing: border-box;

View File

@ -33,6 +33,7 @@ import { Observable, of } from 'rxjs';
import { tap, map, switchMap, take } from 'rxjs/operators'; import { tap, map, switchMap, take } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
@Component({ @Component({
selector: 'tb-string-autocomplete', selector: 'tb-string-autocomplete',
@ -48,41 +49,42 @@ import { coerceBoolean } from '@shared/decorators/coercion';
}) })
export class StringAutocompleteComponent implements ControlValueAccessor, OnInit { export class StringAutocompleteComponent implements ControlValueAccessor, OnInit {
@ViewChild('nameInput', {static: true}) nameInput: ElementRef;
@Input() @Input()
disabled: boolean; disabled: boolean;
@coerceBoolean()
@Input() @Input()
required: boolean = false; @coerceBoolean()
required = false;
@Input() fetchOptionsFn: (searchText?: string) => Observable<Array<string>>; @Input()
fetchOptionsFn: (searchText?: string) => Observable<Array<string>>;
@ViewChild('nameInput', {static: true}) nameInput: ElementRef;
@Input() @Input()
placeholderText: string = this.translate.instant('widget-config.set'); placeholderText: string = this.translate.instant('widget-config.set');
@Input() @Input()
subscriptSizing: string = 'dynamic'; subscriptSizing: SubscriptSizing = 'dynamic';
@Input() @Input()
ngClass: string | string[] | Set<string> | { [klass: string]: any; } = 'tb-inline-field tb-suffix-show-on-hover'; additionalClass: string | string[] | Record<string, boolean | undefined | null> = 'tb-inline-field tb-suffix-show-on-hover';
@Input() @Input()
appearance: string = 'outline'; appearance: MatFormFieldAppearance = 'outline';
@Input() @Input()
label: string; label: string;
@Input() @Input()
tooltipClass: string = 'tb-error-tooltip'; tooltipClass = 'tb-error-tooltip';
@Input() @Input()
errorText: string; errorText: string;
@coerceBoolean()
@Input() @Input()
showInlineError: boolean = false; @coerceBoolean()
showInlineError = false;
selectionFormControl: FormControl; selectionFormControl: FormControl;

View File

@ -62,9 +62,9 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit {
@Input() @Input()
disabled: boolean; disabled: boolean;
@coerceBoolean()
@Input() @Input()
required: boolean = false; @coerceBoolean()
required = false;
@Input() @Input()
tagFilter: UnitsType; tagFilter: UnitsType;